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>
249 lines
6.2 KiB
Go
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
|
|
}
|