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 <kari@tailscale.com>
This commit is contained in:
kari-ts authored and GitHub committed 2026-10-14 09:15:08 -07:00
1 parent fd1cc09139
commit 5f47f9ce49
3 files changed
+53 -2

No files matched your search

+31
View File
@@ -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{
+20
View File
@@ -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)
}
+2 -2
View File
@@ -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
}