Files
tailscale/util/syspolicy/policytest/policytest.go
T
kari-tsandGitHub 07cefc083d 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>
2026-06-30 12:44:29 -07:00

249 lines
6.2 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package policytest contains test helpers for the syspolicy packages.
package policytest
import (
"fmt"
"maps"
"slices"
"sync"
"time"
"tailscale.com/util/set"
"tailscale.com/util/syspolicy/pkey"
"tailscale.com/util/syspolicy/policyclient"
"tailscale.com/util/syspolicy/ptype"
)
// Config is a [policyclient.Client] implementation with a static mapping of
// values.
//
// It is used for testing purposes to simulate policy client behavior.
//
// It panics if a value is Set with one type and then accessed with a different
// expected type and/or value. Some accessors such as GetPreferenceOption and
// GetVisibility support either a ptype.PreferenceOption/ptype.Visibility in the
// map, or the string representation as supported by their UnmarshalText
// methods.
//
// The map value may be an error to return that error value from the accessor.
type Config map[pkey.Key]any
var _ policyclient.Client = Config{}
// Set sets key to value. The value should be of the correct type that it will
// be read as later. For PreferenceOption and Visibility, you may also set them
// to 'string' values and they'll be UnmarshalText'ed into their correct value
// at Get time.
//
// As a special case, the value can also be of type error to make the accessors
// return that error value.
func (c *Config) Set(key pkey.Key, value any) {
if *c == nil {
*c = make(map[pkey.Key]any)
}
(*c)[key] = value
if w, ok := (*c)[watchersKey].(*watchers); ok && key != watchersKey {
w.mu.Lock()
vals := slices.Collect(maps.Values(w.s))
w.mu.Unlock()
for _, f := range vals {
f(policyChange(key))
}
}
}
// SetMultiple is a batch version of [Config.Set]. It copies the contents of o
// into c and does at most one notification wake-up for the whole batch.
func (c *Config) SetMultiple(o Config) {
if *c == nil {
*c = make(map[pkey.Key]any)
}
maps.Copy(*c, o)
if w, ok := (*c)[watchersKey].(*watchers); ok {
w.mu.Lock()
vals := slices.Collect(maps.Values(w.s))
w.mu.Unlock()
for _, f := range vals {
f(policyChanges(o))
}
}
}
type policyChange pkey.Key
func (pc policyChange) HasChanged(v pkey.Key) bool { return pkey.Key(pc) == v }
func (pc policyChange) HasChangedAnyOf(keys ...pkey.Key) bool {
return slices.Contains(keys, pkey.Key(pc))
}
type policyChanges map[pkey.Key]any
func (pc policyChanges) HasChanged(v pkey.Key) bool {
_, ok := pc[v]
return ok
}
func (pc policyChanges) HasChangedAnyOf(keys ...pkey.Key) bool {
return slices.ContainsFunc(keys, pc.HasChanged)
}
const watchersKey = "_policytest_watchers"
type watchers struct {
mu sync.Mutex
s set.HandleSet[func(policyclient.PolicyChange)]
}
// EnableRegisterChangeCallback makes c support the RegisterChangeCallback
// for testing. Without calling this, the RegisterChangeCallback does nothing.
// For watchers to be notified, use the [Config.Set] method. Changing the map
// directly obviously wouldn't work.
func (c *Config) EnableRegisterChangeCallback() {
if _, ok := (*c)[watchersKey]; !ok {
c.Set(watchersKey, new(watchers))
}
}
func (c Config) GetStringArray(key pkey.Key, defaultVal []string) ([]string, error) {
if val, ok := c[key]; ok {
switch val := val.(type) {
case []string:
return val, nil
case error:
return nil, val
default:
panic(fmt.Sprintf("key %s is not a []string; got %T", key, val))
}
}
return defaultVal, nil
}
func (c Config) GetString(key pkey.Key, defaultVal string) (string, error) {
if val, ok := c[key]; ok {
switch val := val.(type) {
case string:
return val, nil
case error:
return "", val
default:
panic(fmt.Sprintf("key %s is not a string; got %T", key, val))
}
}
return defaultVal, nil
}
func (c Config) GetBoolean(key pkey.Key, defaultVal bool) (bool, error) {
if val, ok := c[key]; ok {
switch val := val.(type) {
case bool:
return val, nil
case error:
return false, val
default:
panic(fmt.Sprintf("key %s is not a bool; got %T", key, val))
}
}
return defaultVal, nil
}
func (c Config) GetUint64(key pkey.Key, defaultVal uint64) (uint64, error) {
if val, ok := c[key]; ok {
switch val := val.(type) {
case uint64:
return val, nil
case error:
return 0, val
default:
panic(fmt.Sprintf("key %s is not a uint64; got %T", key, val))
}
}
return defaultVal, nil
}
func (c Config) GetDuration(key pkey.Key, defaultVal time.Duration) (time.Duration, error) {
if val, ok := c[key]; ok {
switch val := val.(type) {
case time.Duration:
return val, nil
case error:
return 0, val
default:
panic(fmt.Sprintf("key %s is not a time.Duration; got %T", key, val))
}
}
return defaultVal, nil
}
func (c Config) GetPreferenceOption(key pkey.Key, defaultVal ptype.PreferenceOption) (ptype.PreferenceOption, error) {
if val, ok := c[key]; ok {
switch val := val.(type) {
case ptype.PreferenceOption:
return val, nil
case error:
var zero ptype.PreferenceOption
return zero, val
case string:
var p ptype.PreferenceOption
err := p.UnmarshalText(([]byte)(val))
return p, err
default:
panic(fmt.Sprintf("key %s is not a ptype.PreferenceOption", key))
}
}
return defaultVal, nil
}
func (c Config) GetVisibility(key pkey.Key) (ptype.Visibility, error) {
if val, ok := c[key]; ok {
switch val := val.(type) {
case ptype.Visibility:
return val, nil
case error:
var zero ptype.Visibility
return zero, val
case string:
var p ptype.Visibility
err := p.UnmarshalText(([]byte)(val))
return p, err
default:
panic(fmt.Sprintf("key %s is not a ptype.Visibility", key))
}
}
return ptype.Visibility(ptype.ShowChoiceByPolicy), nil
}
func (c Config) HasAnyOf(keys ...pkey.Key) (bool, error) {
for _, key := range keys {
if _, ok := c[key]; ok {
return true, nil
}
}
return false, nil
}
func (c Config) RegisterChangeCallback(uid string, callback func(policyclient.PolicyChange)) (func(), error) {
w, ok := c[watchersKey].(*watchers)
if !ok {
return func() {}, nil
}
w.mu.Lock()
defer w.mu.Unlock()
h := w.s.Add(callback)
return func() {
w.mu.Lock()
defer w.mu.Unlock()
delete(w.s, h)
}, nil
}
func (sp Config) SetDebugLoggingEnabled(enabled bool) {}
func (c Config) GetPolicySnapshot(uid string) (*policyclient.PolicySnapshot, error) {
return nil, nil
}