mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
cmd/tailscale/cli, client/tailscale, ipn/localapi: add tailscale syspolicy {list,reload} commands
In this PR, we add the tailscale syspolicy command with two subcommands: list, which displays policy settings, and reload, which forces a reload of those settings. We also update the LocalAPI and LocalClient to facilitate these additions. Updates #12687 Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
parent
45354dab9b
commit
3f626c0d77
@ -40,6 +40,7 @@
|
|||||||
"tailscale.com/types/dnstype"
|
"tailscale.com/types/dnstype"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/tkatype"
|
"tailscale.com/types/tkatype"
|
||||||
|
"tailscale.com/util/syspolicy/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultLocalClient is the default LocalClient when using the legacy
|
// defaultLocalClient is the default LocalClient when using the legacy
|
||||||
@ -814,6 +815,33 @@ func (lc *LocalClient) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn
|
|||||||
return decodeJSON[*ipn.Prefs](body)
|
return decodeJSON[*ipn.Prefs](body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEffectivePolicy returns the effective policy for the specified scope.
|
||||||
|
func (lc *LocalClient) GetEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
|
||||||
|
scopeID, err := scope.MarshalText()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
body, err := lc.get200(ctx, "/localapi/v0/policy/"+string(scopeID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return decodeJSON[*setting.Snapshot](body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReloadEffectivePolicy reloads the effective policy for the specified scope
|
||||||
|
// by reading and merging policy settings from all applicable policy sources.
|
||||||
|
func (lc *LocalClient) ReloadEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
|
||||||
|
scopeID, err := scope.MarshalText()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
body, err := lc.send(ctx, "POST", "/localapi/v0/policy/"+string(scopeID), 200, http.NoBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return decodeJSON[*setting.Snapshot](body)
|
||||||
|
}
|
||||||
|
|
||||||
// GetDNSOSConfig returns the system DNS configuration for the current device.
|
// GetDNSOSConfig returns the system DNS configuration for the current device.
|
||||||
// That is, it returns the DNS configuration that the system would use if Tailscale weren't being used.
|
// That is, it returns the DNS configuration that the system would use if Tailscale weren't being used.
|
||||||
func (lc *LocalClient) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, error) {
|
func (lc *LocalClient) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, error) {
|
||||||
|
@ -814,7 +814,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
|||||||
tailscale.com/util/syspolicy/internal from tailscale.com/util/syspolicy/setting+
|
tailscale.com/util/syspolicy/internal from tailscale.com/util/syspolicy/setting+
|
||||||
tailscale.com/util/syspolicy/internal/loggerx from tailscale.com/util/syspolicy/internal/metrics+
|
tailscale.com/util/syspolicy/internal/loggerx from tailscale.com/util/syspolicy/internal/metrics+
|
||||||
tailscale.com/util/syspolicy/internal/metrics from tailscale.com/util/syspolicy/source
|
tailscale.com/util/syspolicy/internal/metrics from tailscale.com/util/syspolicy/source
|
||||||
tailscale.com/util/syspolicy/rsop from tailscale.com/util/syspolicy
|
tailscale.com/util/syspolicy/rsop from tailscale.com/util/syspolicy+
|
||||||
tailscale.com/util/syspolicy/setting from tailscale.com/util/syspolicy+
|
tailscale.com/util/syspolicy/setting from tailscale.com/util/syspolicy+
|
||||||
tailscale.com/util/syspolicy/source from tailscale.com/util/syspolicy+
|
tailscale.com/util/syspolicy/source from tailscale.com/util/syspolicy+
|
||||||
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
|
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
|
||||||
|
@ -185,6 +185,7 @@ func newRootCmd() *ffcli.Command {
|
|||||||
logoutCmd,
|
logoutCmd,
|
||||||
switchCmd,
|
switchCmd,
|
||||||
configureCmd,
|
configureCmd,
|
||||||
|
syspolicyCmd,
|
||||||
netcheckCmd,
|
netcheckCmd,
|
||||||
ipCmd,
|
ipCmd,
|
||||||
dnsCmd,
|
dnsCmd,
|
||||||
|
110
cmd/tailscale/cli/syspolicy.go
Normal file
110
cmd/tailscale/cli/syspolicy.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"tailscale.com/util/syspolicy/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
var syspolicyArgs struct {
|
||||||
|
json bool // JSON output mode
|
||||||
|
}
|
||||||
|
|
||||||
|
var syspolicyCmd = &ffcli.Command{
|
||||||
|
Name: "syspolicy",
|
||||||
|
ShortHelp: "Diagnose the MDM and system policy configuration",
|
||||||
|
LongHelp: "The 'tailscale syspolicy' command provides tools for diagnosing the MDM and system policy configuration.",
|
||||||
|
ShortUsage: "tailscale syspolicy <subcommand>",
|
||||||
|
UsageFunc: usageFuncNoDefaultValues,
|
||||||
|
Subcommands: []*ffcli.Command{
|
||||||
|
{
|
||||||
|
Name: "list",
|
||||||
|
ShortUsage: "tailscale syspolicy list",
|
||||||
|
Exec: runSysPolicyList,
|
||||||
|
ShortHelp: "Prints effective policy settings",
|
||||||
|
LongHelp: "The 'tailscale syspolicy list' subcommand displays the effective policy settings and their sources (e.g., MDM or environment variables).",
|
||||||
|
FlagSet: (func() *flag.FlagSet {
|
||||||
|
fs := newFlagSet("syspolicy list")
|
||||||
|
fs.BoolVar(&syspolicyArgs.json, "json", false, "output in JSON format")
|
||||||
|
return fs
|
||||||
|
})(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "reload",
|
||||||
|
ShortUsage: "tailscale syspolicy reload",
|
||||||
|
Exec: runSysPolicyReload,
|
||||||
|
ShortHelp: "Forces a reload of policy settings, even if no changes are detected, and prints the result",
|
||||||
|
LongHelp: "The 'tailscale syspolicy reload' subcommand forces a reload of policy settings, even if no changes are detected, and prints the result.",
|
||||||
|
FlagSet: (func() *flag.FlagSet {
|
||||||
|
fs := newFlagSet("syspolicy reload")
|
||||||
|
fs.BoolVar(&syspolicyArgs.json, "json", false, "output in JSON format")
|
||||||
|
return fs
|
||||||
|
})(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSysPolicyList(ctx context.Context, args []string) error {
|
||||||
|
policy, err := localClient.GetEffectivePolicy(ctx, setting.DefaultScope())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printPolicySettings(policy)
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSysPolicyReload(ctx context.Context, args []string) error {
|
||||||
|
policy, err := localClient.ReloadEffectivePolicy(ctx, setting.DefaultScope())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printPolicySettings(policy)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printPolicySettings(policy *setting.Snapshot) {
|
||||||
|
if syspolicyArgs.json {
|
||||||
|
json, err := json.MarshalIndent(policy, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
errf("syspolicy marshalling error: %v", err)
|
||||||
|
} else {
|
||||||
|
outln(string(json))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if policy.Len() == 0 {
|
||||||
|
outln("No policy settings")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
|
fmt.Fprintln(w, "Name\tOrigin\tValue\tError")
|
||||||
|
fmt.Fprintln(w, "----\t------\t-----\t-----")
|
||||||
|
for _, k := range slices.Sorted(policy.Keys()) {
|
||||||
|
setting, _ := policy.GetSetting(k)
|
||||||
|
var origin string
|
||||||
|
if o := setting.Origin(); o != nil {
|
||||||
|
origin = o.String()
|
||||||
|
}
|
||||||
|
if err := setting.Error(); err != nil {
|
||||||
|
fmt.Fprintf(w, "%s\t%s\t\t{%s}\n", k, origin, err)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "%s\t%s\t%s\t\n", k, origin, setting.Value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
return
|
||||||
|
}
|
@ -403,7 +403,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
tailscale.com/util/syspolicy/internal from tailscale.com/util/syspolicy/setting+
|
tailscale.com/util/syspolicy/internal from tailscale.com/util/syspolicy/setting+
|
||||||
tailscale.com/util/syspolicy/internal/loggerx from tailscale.com/util/syspolicy/internal/metrics+
|
tailscale.com/util/syspolicy/internal/loggerx from tailscale.com/util/syspolicy/internal/metrics+
|
||||||
tailscale.com/util/syspolicy/internal/metrics from tailscale.com/util/syspolicy/source
|
tailscale.com/util/syspolicy/internal/metrics from tailscale.com/util/syspolicy/source
|
||||||
tailscale.com/util/syspolicy/rsop from tailscale.com/util/syspolicy
|
tailscale.com/util/syspolicy/rsop from tailscale.com/util/syspolicy+
|
||||||
tailscale.com/util/syspolicy/setting from tailscale.com/util/syspolicy+
|
tailscale.com/util/syspolicy/setting from tailscale.com/util/syspolicy+
|
||||||
tailscale.com/util/syspolicy/source from tailscale.com/util/syspolicy+
|
tailscale.com/util/syspolicy/source from tailscale.com/util/syspolicy+
|
||||||
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
|
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
|
||||||
|
@ -62,6 +62,8 @@
|
|||||||
"tailscale.com/util/osdiag"
|
"tailscale.com/util/osdiag"
|
||||||
"tailscale.com/util/progresstracking"
|
"tailscale.com/util/progresstracking"
|
||||||
"tailscale.com/util/rands"
|
"tailscale.com/util/rands"
|
||||||
|
"tailscale.com/util/syspolicy/rsop"
|
||||||
|
"tailscale.com/util/syspolicy/setting"
|
||||||
"tailscale.com/version"
|
"tailscale.com/version"
|
||||||
"tailscale.com/wgengine/magicsock"
|
"tailscale.com/wgengine/magicsock"
|
||||||
)
|
)
|
||||||
@ -76,6 +78,7 @@
|
|||||||
"cert/": (*Handler).serveCert,
|
"cert/": (*Handler).serveCert,
|
||||||
"file-put/": (*Handler).serveFilePut,
|
"file-put/": (*Handler).serveFilePut,
|
||||||
"files/": (*Handler).serveFiles,
|
"files/": (*Handler).serveFiles,
|
||||||
|
"policy/": (*Handler).servePolicy,
|
||||||
"profiles/": (*Handler).serveProfiles,
|
"profiles/": (*Handler).serveProfiles,
|
||||||
|
|
||||||
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
|
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
|
||||||
@ -1332,6 +1335,53 @@ func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
|
|||||||
e.Encode(prefs)
|
e.Encode(prefs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) servePolicy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitRead {
|
||||||
|
http.Error(w, "policy access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
suffix, ok := strings.CutPrefix(r.URL.EscapedPath(), "/localapi/v0/policy/")
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "misconfigured", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var scope setting.PolicyScope
|
||||||
|
if suffix == "" {
|
||||||
|
scope = setting.DefaultScope()
|
||||||
|
} else if err := scope.UnmarshalText([]byte(suffix)); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("%q is not a valid scope", suffix), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
policy, err := rsop.PolicyFor(scope)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var effectivePolicy *setting.Snapshot
|
||||||
|
switch r.Method {
|
||||||
|
case "GET":
|
||||||
|
effectivePolicy = policy.Get()
|
||||||
|
case "POST":
|
||||||
|
effectivePolicy, err = policy.Reload()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
e := json.NewEncoder(w)
|
||||||
|
e.SetIndent("", "\t")
|
||||||
|
e.Encode(effectivePolicy)
|
||||||
|
}
|
||||||
|
|
||||||
type resJSON struct {
|
type resJSON struct {
|
||||||
Error string `json:",omitempty"`
|
Error string `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user