ipn/{ipn,ipnlocal}: add per-user policy snapshots to IPN bus (#20135)
This adds the NotifyInitialPolicy watch option and the Policy field in Notify so that clients can receive the effective policy snapshot via IPN bus. This extends policyclient.Client so ipnlocal can get and watch policy snapshots, which is used by sysPolicyChanged to notify watchers. User-scoped policy store registration, management, and cleanup will be added in a follow-up Updates tailscale/corp#42259 Signed-off-by: kari <kari@tailscale.com>
This commit is contained in:
+2
-1
@@ -247,5 +247,6 @@ func isNotableNotify(n *ipn.Notify) bool {
|
||||
len(n.IncomingFiles) > 0 ||
|
||||
len(n.OutgoingFiles) > 0 ||
|
||||
n.FilesWaiting != nil ||
|
||||
n.SuggestedExitNode != nil
|
||||
n.SuggestedExitNode != nil ||
|
||||
n.Policy != nil
|
||||
}
|
||||
|
||||
+45
-3
@@ -2330,8 +2330,8 @@ func (b *LocalBackend) applyExitNodeSysPolicyLocked(prefs *ipn.Prefs) (anyChange
|
||||
// registerSysPolicyWatch subscribes to syspolicy change notifications
|
||||
// and immediately applies the effective syspolicy settings to the current profile.
|
||||
func (b *LocalBackend) registerSysPolicyWatch() (unregister func(), err error) {
|
||||
if unregister, err = b.polc.RegisterChangeCallback(b.sysPolicyChanged); err != nil {
|
||||
return nil, fmt.Errorf("syspolicy: LocalBacked failed to register policy change callback: %v", err)
|
||||
if unregister, err = b.polc.RegisterChangeCallback("", b.sysPolicyChanged); err != nil {
|
||||
return nil, fmt.Errorf("syspolicy: LocalBackend failed to register policy change callback: %v", err)
|
||||
}
|
||||
if prefs, anyChange := b.reconcilePrefs(); anyChange {
|
||||
b.logf("syspolicy: changed initial profile prefs: %v", prefs.Pretty())
|
||||
@@ -2392,6 +2392,28 @@ func (b *LocalBackend) sysPolicyChanged(policy policyclient.PolicyChange) {
|
||||
}
|
||||
}
|
||||
|
||||
// sysPolicyChangedForSession is called when the effective policy for a session's
|
||||
// user changes. It fetches the new snapshot and sends it to that session.
|
||||
func (b *LocalBackend) sysPolicyChangedForSession(sess *watchSession) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
snapshot, err := b.polc.GetPolicySnapshot("")
|
||||
if err != nil || snapshot == nil {
|
||||
return
|
||||
}
|
||||
|
||||
n := ipn.Notify{Policy: snapshot, Version: version.Long()}
|
||||
for _, f := range b.extHost.Hooks().MutateNotifyLocked {
|
||||
f(&n)
|
||||
}
|
||||
nForSess := b.notifyForSessionLocked(sess, &n)
|
||||
select {
|
||||
case sess.ch <- nForSess:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ controlclient.NetmapDeltaUpdater = (*LocalBackend)(nil)
|
||||
_ controlclient.PacketFilterUpdater = (*LocalBackend)(nil)
|
||||
@@ -3711,7 +3733,10 @@ func (b *LocalBackend) WatchNotificationsAs(ctx context.Context, actor ipnauth.A
|
||||
deadlockDone := b.CheckDeadlocks()
|
||||
b.mu.Lock()
|
||||
|
||||
const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap | ipn.NotifyInitialStatus | ipn.NotifyInitialDriveShares | ipn.NotifyInitialSuggestedExitNode | ipn.NotifyInitialClientVersion | ipn.NotifyPeerWireGuardState
|
||||
const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs |
|
||||
ipn.NotifyInitialNetMap | ipn.NotifyInitialStatus |
|
||||
ipn.NotifyInitialDriveShares | ipn.NotifyInitialSuggestedExitNode |
|
||||
ipn.NotifyInitialClientVersion | ipn.NotifySysPolicyChanges | ipn.NotifyPeerWireGuardState
|
||||
if mask&initialBits != 0 {
|
||||
cn := b.currentNode()
|
||||
ini = &ipn.Notify{Version: version.Long()}
|
||||
@@ -3755,6 +3780,13 @@ func (b *LocalBackend) WatchNotificationsAs(ctx context.Context, actor ipnauth.A
|
||||
ini.ClientVersion = b.lastClientVersion
|
||||
}
|
||||
}
|
||||
if mask&ipn.NotifySysPolicyChanges != 0 {
|
||||
var err error
|
||||
ini.Policy, err = b.polc.GetPolicySnapshot("")
|
||||
if err != nil {
|
||||
b.logf("syspolicy: GetPolicySnapshot(\"\"): %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
@@ -3775,6 +3807,16 @@ func (b *LocalBackend) WatchNotificationsAs(ctx context.Context, actor ipnauth.A
|
||||
b.mu.Unlock()
|
||||
deadlockDone()
|
||||
|
||||
if mask&ipn.NotifySysPolicyChanges != 0 {
|
||||
if unreg, err := b.polc.RegisterChangeCallback("", func(_ policyclient.PolicyChange) {
|
||||
b.sysPolicyChangedForSession(session)
|
||||
}); err == nil {
|
||||
defer unreg()
|
||||
} else {
|
||||
b.logf("syspolicy: RegisterChangeCallback(\"\"): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
metricCurrentWatchIPNBus.Add(1)
|
||||
defer metricCurrentWatchIPNBus.Add(-1)
|
||||
|
||||
|
||||
@@ -71,7 +71,10 @@ import (
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/syspolicy"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/policyclient"
|
||||
"tailscale.com/util/syspolicy/policytest"
|
||||
"tailscale.com/util/syspolicy/rsop"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/syspolicy/source"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
@@ -8481,6 +8484,153 @@ func toStrings[T ~string](in []T) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func TestWatchNotificationsInitialPolicy(t *testing.T) {
|
||||
setting.SetDefinitionsForTest(t,
|
||||
setting.NewDefinition(pkey.AdminConsoleVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
)
|
||||
store := source.NewTestStore(t)
|
||||
rsop.RegisterStoreForTest(t, "TestStore", setting.DeviceScope, store)
|
||||
|
||||
sys := tsd.NewSystem()
|
||||
sys.PolicyClient.Set(testPolicyClient{})
|
||||
lb := newTestLocalBackendWithSys(t, sys)
|
||||
|
||||
nw := newNotificationWatcher(t, lb, &ipnauth.TestActor{})
|
||||
nw.watch(ipn.NotifySysPolicyChanges, []wantedNotification{
|
||||
wantPolicyNotify(),
|
||||
})
|
||||
nw.check()
|
||||
|
||||
nw2 := newNotificationWatcher(t, lb, &ipnauth.TestActor{})
|
||||
nw2.watch(0, nil, unexpectedPolicy)
|
||||
nw2.check()
|
||||
}
|
||||
|
||||
func TestPolicyChangeNotifiesWatcher(t *testing.T) {
|
||||
setting.SetDefinitionsForTest(t,
|
||||
setting.NewDefinition(pkey.AdminConsoleVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
)
|
||||
store := source.NewTestStore(t)
|
||||
rsop.RegisterStoreForTest(t, "TestStore", setting.DeviceScope, store)
|
||||
|
||||
sys := tsd.NewSystem()
|
||||
sys.PolicyClient.Set(testPolicyClient{})
|
||||
lb := newTestLocalBackendWithSys(t, sys)
|
||||
if err := lb.Start(ipn.Options{}); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
|
||||
nw := newNotificationWatcher(t, lb, &ipnauth.TestActor{})
|
||||
nw.watch(ipn.NotifySysPolicyChanges, []wantedNotification{
|
||||
wantPolicyNotify(),
|
||||
wantPolicyWithSetting(pkey.AdminConsoleVisibility, "hide"),
|
||||
})
|
||||
|
||||
store.SetStrings(source.TestSettingOf(pkey.AdminConsoleVisibility, "hide"))
|
||||
|
||||
nw.check()
|
||||
}
|
||||
|
||||
func TestPolicyNotifyPerUser(t *testing.T) {
|
||||
store := source.NewTestStore(t)
|
||||
rsop.RegisterStoreForTest(t, "TestStore", setting.DeviceScope, store)
|
||||
|
||||
sys := tsd.NewSystem()
|
||||
sys.PolicyClient.Set(testPolicyClient{})
|
||||
lb := newTestLocalBackendWithSys(t, sys)
|
||||
|
||||
actorA := &ipnauth.TestActor{UID: "S-1-5-21-1001"}
|
||||
actorB := &ipnauth.TestActor{UID: "S-1-5-21-1002"}
|
||||
|
||||
nwA := newNotificationWatcher(t, lb, actorA)
|
||||
nwA.watch(ipn.NotifySysPolicyChanges, []wantedNotification{
|
||||
wantPolicyNotify(),
|
||||
})
|
||||
nwA.check()
|
||||
|
||||
nwB := newNotificationWatcher(t, lb, actorB)
|
||||
nwB.watch(ipn.NotifySysPolicyChanges, []wantedNotification{
|
||||
wantPolicyNotify(),
|
||||
})
|
||||
nwB.check()
|
||||
|
||||
nwNoPolicy := newNotificationWatcher(t, lb, actorA)
|
||||
nwNoPolicy.watch(0, nil, unexpectedPolicy)
|
||||
|
||||
store.SetStrings(source.TestSettingOf(pkey.AdminConsoleVisibility, "hide"))
|
||||
|
||||
nwNoPolicy.check()
|
||||
}
|
||||
|
||||
func wantPolicyNotify() wantedNotification {
|
||||
return wantedNotification{
|
||||
name: "Policy",
|
||||
cond: func(_ testing.TB, _ ipnauth.Actor, n *ipn.Notify) bool {
|
||||
return n.Policy != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func wantPolicyWithSetting(key pkey.Key, value string) wantedNotification {
|
||||
return wantedNotification{
|
||||
name: fmt.Sprintf("Policy-%s=%s", key, value),
|
||||
cond: func(t testing.TB, _ ipnauth.Actor, n *ipn.Notify) bool {
|
||||
if n.Policy == nil {
|
||||
return false
|
||||
}
|
||||
if n.Policy == nil {
|
||||
return false
|
||||
}
|
||||
got := n.Policy.Get(key)
|
||||
if got == nil {
|
||||
return false
|
||||
}
|
||||
if fmt.Sprint(got) != value {
|
||||
t.Errorf("Policy[%s] = %v (%T); want %q", key, got, got, value)
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func unexpectedPolicy(t testing.TB, _ ipnauth.Actor, n *ipn.Notify) bool {
|
||||
if n.Policy != nil {
|
||||
t.Errorf("unexpected Policy notification")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type testPolicyClient struct {
|
||||
policyclient.NoPolicyClient
|
||||
}
|
||||
|
||||
func (testPolicyClient) GetPolicySnapshot(uid string) (*policyclient.PolicySnapshot, error) {
|
||||
scope := setting.DefaultScope()
|
||||
if uid != "" {
|
||||
scope = setting.UserScopeOf(uid)
|
||||
}
|
||||
p, err := rsop.PolicyFor(scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Get(), nil
|
||||
}
|
||||
|
||||
func (testPolicyClient) RegisterChangeCallback(uid string, cb func(policyclient.PolicyChange)) (func(), error) {
|
||||
scope := setting.DefaultScope()
|
||||
if uid != "" {
|
||||
scope = setting.UserScopeOf(uid)
|
||||
}
|
||||
p, err := rsop.PolicyFor(scope)
|
||||
if err != nil {
|
||||
return func() {}, err
|
||||
}
|
||||
return p.RegisterChangeCallback(func(change policyclient.PolicyChange) {
|
||||
cb(change)
|
||||
}), nil
|
||||
}
|
||||
|
||||
type textUpdate struct {
|
||||
Advertise []string
|
||||
Unadvertise []string
|
||||
|
||||
Reference in New Issue
Block a user