From 5f47f9ce49b5270c56c23cd886b7feb7f96e83e3 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Wed, 14 Oct 2026 09:15:08 -0700 Subject: [PATCH] client/local, cmd/tailscale/cli: reload syspolicy for current user (#21712) This makes the syspolicy cli use the caller's user scope instead of the generic user scope. In tailscale/tailscale#21701, we made the LocalAPI policy endpoint resolve unscoped policy requests to the connected user's policy. However, the cli serialized setting.DefaultScope() as /policy/User, which selected the general user policy and not the User(SID) policy used by IPN watchers. This was tested by changing the AlwaysOn policy and verifying that the User(SID) policy was reloaded and the gui's IPN policy change callbacks fired. Updates tailscale/corp#41463 Signed-off-by: kari --- client/local/local_test.go | 31 +++++++++++++++++++++++++++++++ client/local/syspolicy.go | 20 ++++++++++++++++++++ cmd/tailscale/cli/syspolicy.go | 4 ++-- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/client/local/local_test.go b/client/local/local_test.go index 58a87b224..6c81590c6 100644 --- a/client/local/local_test.go +++ b/client/local/local_test.go @@ -14,6 +14,7 @@ import ( "tailscale.com/tstest/deptest" "tailscale.com/tstest/nettest" "tailscale.com/types/key" + "tailscale.com/util/httpm" ) func TestGetServeConfigFromJSON(t *testing.T) { @@ -112,6 +113,36 @@ func TestUserDialSelf(t *testing.T) { } } +func TestEffectivePolicyForCurrentUserUsesUnscopedEndpoint(t *testing.T) { + nw := nettest.GetNetwork(t) + + var gotMethod, gotPath string + ts := nettest.NewHTTPServer(nw, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotMethod = r.Method + gotPath = r.URL.Path + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{}`)) + })) + defer ts.Close() + + lc := &Client{ + Dial: func(ctx context.Context, network, addr string) (net.Conn, error) { + return nw.Dial(ctx, network, ts.Listener.Addr().String()) + }, + } + + if _, err := lc.ReloadEffectivePolicyForCurrentUser(context.Background()); err != nil { + t.Fatalf("ReloadEffectivePolicyForCurrentUser: %v", err) + } + + if gotMethod != httpm.POST { + t.Errorf("method = %q, want %q", gotMethod, httpm.POST) + } + if gotPath != "/localapi/v0/policy/" { + t.Errorf("path = %q, want %q", gotPath, "/localapi/v0/policy/") + } +} + func TestDeps(t *testing.T) { deptest.DepChecker{ BadDeps: map[string]string{ diff --git a/client/local/syspolicy.go b/client/local/syspolicy.go index 4753811bb..e992eff91 100644 --- a/client/local/syspolicy.go +++ b/client/local/syspolicy.go @@ -28,6 +28,16 @@ func (lc *Client) GetEffectivePolicy(ctx context.Context, scope setting.PolicySc return decodeJSON[*setting.Snapshot](body) } +// GetEffectivePolicyForCurrentUser returns the effective policy for the +// user associated with the LocalAPI request. +func (lc *Client) GetEffectivePolicyForCurrentUser(ctx context.Context) (*setting.Snapshot, error) { + body, err := lc.get200(ctx, "/localapi/v0/policy/") + 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 *Client) ReloadEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) { @@ -41,3 +51,13 @@ func (lc *Client) ReloadEffectivePolicy(ctx context.Context, scope setting.Polic } return decodeJSON[*setting.Snapshot](body) } + +// ReloadEffectivePolicyForCurrentUser reloads the effective policy for the +// user associated with the LocalAPI request. +func (lc *Client) ReloadEffectivePolicyForCurrentUser(ctx context.Context) (*setting.Snapshot, error) { + body, err := lc.send(ctx, "POST", "/localapi/v0/policy/", 200, http.NoBody) + if err != nil { + return nil, err + } + return decodeJSON[*setting.Snapshot](body) +} diff --git a/cmd/tailscale/cli/syspolicy.go b/cmd/tailscale/cli/syspolicy.go index e44b01d5f..4cde27afc 100644 --- a/cmd/tailscale/cli/syspolicy.go +++ b/cmd/tailscale/cli/syspolicy.go @@ -61,7 +61,7 @@ func init() { } func runSysPolicyList(ctx context.Context, args []string) error { - policy, err := localClient.GetEffectivePolicy(ctx, setting.DefaultScope()) + policy, err := localClient.GetEffectivePolicyForCurrentUser(ctx) if err != nil { return err } @@ -70,7 +70,7 @@ func runSysPolicyList(ctx context.Context, args []string) error { } func runSysPolicyReload(ctx context.Context, args []string) error { - policy, err := localClient.ReloadEffectivePolicy(ctx, setting.DefaultScope()) + policy, err := localClient.ReloadEffectivePolicyForCurrentUser(ctx) if err != nil { return err }