lanscaping: remove syspolicy

-rwxr-xr-x@ 1 bradfitz  staff  10361090 Jan 11 10:45 /Users/bradfitz/bin/tailscaled.min
-rwxr-xr-x@ 1 bradfitz  staff  10748056 Jan 11 10:45 /Users/bradfitz/bin/tailscaled.minlinux

Change-Id: I06703ba9b80a0947df526828acab222b4dc0f893
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2025-01-11 08:56:09 -08:00
parent 79b8c00c7a
commit 6279f23266
23 changed files with 15 additions and 2642 deletions

View File

@@ -2,12 +2,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
L filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
L filippo.io/edwards25519/field from filippo.io/edwards25519
github.com/go-json-experiment/json from tailscale.com/types/opt+
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
D github.com/google/uuid from tailscale.com/util/quarantine
github.com/gorilla/csrf from tailscale.com/client/web
github.com/gorilla/securecookie from github.com/gorilla/csrf
@@ -111,11 +105,9 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/cloudenv from tailscale.com/hostinfo
tailscale.com/util/cmpver from tailscale.com/clientupdate
tailscale.com/util/ctxkey from tailscale.com/types/logger
💣 tailscale.com/util/deephash from tailscale.com/util/syspolicy/setting
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/groupmember from tailscale.com/client/web
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/lineiter from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns
@@ -126,14 +118,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/slicesx from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/syspolicy from tailscale.com/ipn
tailscale.com/util/syspolicy/internal from tailscale.com/util/syspolicy+
tailscale.com/util/syspolicy/internal/loggerx from tailscale.com/util/syspolicy+
tailscale.com/util/syspolicy/internal/metrics from tailscale.com/util/syspolicy/source
tailscale.com/util/syspolicy/rsop from tailscale.com/util/syspolicy
tailscale.com/util/syspolicy/setting from tailscale.com/client/tailscale+
tailscale.com/util/syspolicy/source from tailscale.com/util/syspolicy+
tailscale.com/util/testenv from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/testenv from tailscale.com/cmd/tailscale/cli
tailscale.com/util/truncate from tailscale.com/cmd/tailscale/cli
tailscale.com/util/usermetric from tailscale.com/health
tailscale.com/util/vizerror from tailscale.com/tailcfg+
@@ -154,7 +140,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/crypto/pbkdf2 from software.sslmate.com/src/go-pkcs12
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
golang.org/x/exp/maps from tailscale.com/util/syspolicy/internal/metrics+
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
@@ -209,7 +194,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
embed from crypto/internal/nistec+
encoding from encoding/gob+
encoding/asn1 from crypto/x509+
encoding/base32 from github.com/go-json-experiment/json
encoding/base64 from encoding/json+
encoding/binary from compress/gzip+
encoding/gob from github.com/gorilla/securecookie
@@ -233,10 +217,10 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
io from archive/tar+
io/fs from archive/tar+
io/ioutil from github.com/mitchellh/go-ps+
iter from github.com/go-json-experiment/json/jsontext+
iter from maps+
log from expvar+
log/internal from log
maps from golang.org/x/exp/maps+
maps from net/http+
math from archive/tar+
math/big from crypto/dsa+
math/bits from compress/flate+

View File

@@ -2,12 +2,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/bits-and-blooms/bitset from github.com/gaissmai/bart
github.com/gaissmai/bart from tailscale.com/net/ipset+
github.com/go-json-experiment/json from tailscale.com/types/opt+
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/netmon
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
@@ -132,13 +126,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/set from tailscale.com/control/controlclient+
tailscale.com/util/singleflight from tailscale.com/control/controlclient
tailscale.com/util/slicesx from tailscale.com/control/controlclient+
tailscale.com/util/syspolicy from tailscale.com/control/controlclient+
tailscale.com/util/syspolicy/internal from tailscale.com/util/syspolicy+
tailscale.com/util/syspolicy/internal/loggerx from tailscale.com/util/syspolicy+
tailscale.com/util/syspolicy/internal/metrics from tailscale.com/util/syspolicy/source
tailscale.com/util/syspolicy/rsop from tailscale.com/ipn/ipnlocal+
tailscale.com/util/syspolicy/setting from tailscale.com/ipn/localapi+
tailscale.com/util/syspolicy/source from tailscale.com/util/syspolicy+
tailscale.com/util/syspolicy from tailscale.com/ipn/ipnlocal
tailscale.com/util/syspolicy/setting from tailscale.com/util/syspolicy+
tailscale.com/util/syspolicy/source from tailscale.com/util/syspolicy
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
tailscale.com/util/testenv from tailscale.com/control/controlclient+
tailscale.com/util/uniq from tailscale.com/ipn/ipnlocal+
@@ -171,7 +161,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
golang.org/x/exp/maps from tailscale.com/ipn/store/mem+
golang.org/x/exp/maps from tailscale.com/ipn/store/mem
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
golang.org/x/net/dns/dnsmessage from net
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
@@ -222,7 +212,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
embed from crypto/internal/nistec+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base32 from github.com/go-json-experiment/json
encoding/base64 from encoding/json+
encoding/binary from compress/gzip+
encoding/hex from crypto/x509+
@@ -239,7 +228,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
io from bufio+
io/fs from crypto/x509+
L io/ioutil from github.com/tailscale/netlink
iter from github.com/go-json-experiment/json/jsontext+
iter from maps+
log from expvar+
log/internal from log
maps from golang.org/x/exp/maps+

View File

@@ -47,7 +47,6 @@ import (
"tailscale.com/util/clientmetric"
"tailscale.com/util/multierr"
"tailscale.com/util/singleflight"
"tailscale.com/util/syspolicy"
"tailscale.com/util/testenv"
"tailscale.com/util/zstdframe"
)
@@ -559,10 +558,6 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
return regen, opt.URL, nil, err
}
tailnet, err := syspolicy.GetString(syspolicy.Tailnet, "")
if err != nil {
c.logf("unable to provide Tailnet field in register request. err: %v", err)
}
now := c.clock.Now().Round(time.Second)
request := tailcfg.RegisterRequest{
Version: 1,
@@ -572,7 +567,6 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
Followup: opt.URL,
Timestamp: &now,
Ephemeral: (opt.Flags & LoginEphemeral) != 0,
Tailnet: tailnet,
}
if opt.Logout {
request.Expiry = time.Unix(123, 0) // far in the past

View File

@@ -79,7 +79,6 @@ import (
"tailscale.com/util/set"
"tailscale.com/util/slicesx"
"tailscale.com/util/syspolicy"
"tailscale.com/util/syspolicy/rsop"
"tailscale.com/util/testenv"
"tailscale.com/util/uniq"
"tailscale.com/util/usermetric"
@@ -448,15 +447,6 @@ func NewLocalBackend(logf logger.Logf, sys *tsd.System, loginFlags controlclient
}
}
if b.unregisterSysPolicyWatch, err = b.registerSysPolicyWatch(); err != nil {
return nil, err
}
defer func() {
if err != nil {
b.unregisterSysPolicyWatch()
}
}()
netMon := sys.NetMon.Get()
// Default filter blocks everything and logs nothing, until Start() is called.
@@ -514,8 +504,6 @@ func (b *LocalBackend) SetComponentDebugLogging(component string, until time.Tim
switch component {
case "magicsock":
setEnabled = b.MagicConn().SetDebugLoggingEnabled
case "syspolicy":
setEnabled = syspolicy.SetDebugLoggingEnabled
}
if setEnabled == nil || !slices.Contains(ipn.DebuggableComponents, component) {
return fmt.Errorf("unknown component %q", component)
@@ -1346,9 +1334,6 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
b.logf("SetControlClientStatus failed to select auto exit node: %v", err)
}
}
if applySysPolicy(prefs, b.lastSuggestedExitNode) {
prefsChanged = true
}
if setExitNodeID(prefs, curNetMap) {
prefsChanged = true
}
@@ -1492,101 +1477,6 @@ var preferencePolicies = []preferencePolicyInfo{
},
}
// applySysPolicy overwrites configured preferences with policies that may be
// configured by the system administrator in an OS-specific way.
func applySysPolicy(prefs *ipn.Prefs, lastSuggestedExitNode tailcfg.StableNodeID) (anyChange bool) {
if controlURL, err := syspolicy.GetString(syspolicy.ControlURL, prefs.ControlURL); err == nil && prefs.ControlURL != controlURL {
prefs.ControlURL = controlURL
anyChange = true
}
if exitNodeIDStr, _ := syspolicy.GetString(syspolicy.ExitNodeID, ""); exitNodeIDStr != "" {
exitNodeID := tailcfg.StableNodeID(exitNodeIDStr)
if shouldAutoExitNode() && lastSuggestedExitNode != "" {
exitNodeID = lastSuggestedExitNode
}
// Note: when exitNodeIDStr == "auto" && lastSuggestedExitNode == "",
// then exitNodeID is now "auto" which will never match a peer's node ID.
// When there is no a peer matching the node ID, traffic will blackhole,
// preventing accidental non-exit-node usage when a policy is in effect that requires an exit node.
if prefs.ExitNodeID != exitNodeID || prefs.ExitNodeIP.IsValid() {
anyChange = true
}
prefs.ExitNodeID = exitNodeID
prefs.ExitNodeIP = netip.Addr{}
} else if exitNodeIPStr, _ := syspolicy.GetString(syspolicy.ExitNodeIP, ""); exitNodeIPStr != "" {
exitNodeIP, err := netip.ParseAddr(exitNodeIPStr)
if exitNodeIP.IsValid() && err == nil {
if prefs.ExitNodeID != "" || prefs.ExitNodeIP != exitNodeIP {
anyChange = true
}
prefs.ExitNodeID = ""
prefs.ExitNodeIP = exitNodeIP
}
}
for _, opt := range preferencePolicies {
if po, err := syspolicy.GetPreferenceOption(opt.key); err == nil {
curVal := opt.get(prefs.View())
newVal := po.ShouldEnable(curVal)
if curVal != newVal {
opt.set(prefs, newVal)
anyChange = true
}
}
}
return 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 = syspolicy.RegisterChangeCallback(b.sysPolicyChanged); err != nil {
return nil, fmt.Errorf("syspolicy: LocalBacked failed to register policy change callback: %v", err)
}
if prefs, anyChange := b.applySysPolicy(); anyChange {
b.logf("syspolicy: changed initial profile prefs: %v", prefs.Pretty())
}
b.refreshAllowedSuggestions()
return unregister, nil
}
// applySysPolicy overwrites the current profile's preferences with policies
// that may be configured by the system administrator in an OS-specific way.
//
// b.mu must not be held.
func (b *LocalBackend) applySysPolicy() (_ ipn.PrefsView, anyChange bool) {
unlock := b.lockAndGetUnlock()
prefs := b.pm.CurrentPrefs().AsStruct()
if !applySysPolicy(prefs, b.lastSuggestedExitNode) {
unlock.UnlockEarly()
return prefs.View(), false
}
return b.setPrefsLockedOnEntry(prefs, unlock), true
}
// sysPolicyChanged is a callback triggered by syspolicy when it detects
// a change in one or more syspolicy settings.
func (b *LocalBackend) sysPolicyChanged(policy *rsop.PolicyChange) {
if policy.HasChanged(syspolicy.AllowedSuggestedExitNodes) {
b.refreshAllowedSuggestions()
// Re-evaluate exit node suggestion now that the policy setting has changed.
b.mu.Lock()
_, err := b.suggestExitNodeLocked(nil)
b.mu.Unlock()
if err != nil && !errors.Is(err, ErrNoPreferredDERP) {
b.logf("failed to select auto exit node: %v", err)
}
// If [syspolicy.ExitNodeID] is set to `auto:any`, the suggested exit node ID
// will be used when [applySysPolicy] updates the current profile's prefs.
}
if prefs, anyChange := b.applySysPolicy(); anyChange {
b.logf("syspolicy: changed profile prefs: %v", prefs.Pretty())
}
}
var _ controlclient.NetmapDeltaUpdater = (*LocalBackend)(nil)
// UpdateNetmapDelta implements controlclient.NetmapDeltaUpdater.
@@ -1915,11 +1805,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
}
if b.state != ipn.Running && b.conf == nil && opts.AuthKey == "" {
sysak, _ := syspolicy.GetString(syspolicy.AuthKey, "")
if sysak != "" {
b.logf("Start: setting opts.AuthKey by syspolicy, len=%v", len(sysak))
opts.AuthKey = strings.TrimSpace(sysak)
}
}
hostinfo := hostinfo.New()
@@ -3646,10 +3531,6 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
if oldp.Valid() {
newp.Persist = oldp.Persist().AsStruct() // caller isn't allowed to override this
}
// applySysPolicyToPrefsLocked returns whether it updated newp,
// but everything in this function treats b.prefs as completely new
// anyway, so its return value can be ignored here.
applySysPolicy(newp, b.lastSuggestedExitNode)
// setExitNodeID does likewise. No-op if no exit node resolution is needed.
setExitNodeID(newp, netMap)
// We do this to avoid holding the lock while doing everything else.

View File

@@ -38,8 +38,6 @@ import (
"tailscale.com/util/httpm"
"tailscale.com/util/mak"
"tailscale.com/util/rands"
"tailscale.com/util/syspolicy/rsop"
"tailscale.com/util/syspolicy/setting"
"tailscale.com/version"
"tailscale.com/wgengine/magicsock"
)
@@ -51,7 +49,6 @@ type localAPIHandler func(*Handler, http.ResponseWriter, *http.Request)
// then it's a prefix match.
var handler = map[string]localAPIHandler{
// The prefix match handlers end with a slash:
"policy/": (*Handler).servePolicy,
"profiles/": (*Handler).serveProfiles,
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
@@ -773,53 +770,6 @@ func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
e.Encode(prefs)
}
func (h *Handler) servePolicy(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "policy access denied", http.StatusForbidden)
return
}
suffix, ok := strings.CutPrefix(r.URL.EscapedPath(), "/localapi/v0/policy/")
if !ok {
http.Error(w, "misconfigured", http.StatusInternalServerError)
return
}
var scope setting.PolicyScope
if suffix == "" {
scope = setting.DefaultScope()
} else if err := scope.UnmarshalText([]byte(suffix)); err != nil {
http.Error(w, fmt.Sprintf("%q is not a valid scope", suffix), http.StatusBadRequest)
return
}
policy, err := rsop.PolicyFor(scope)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var effectivePolicy *setting.Snapshot
switch r.Method {
case "GET":
effectivePolicy = policy.Get()
case "POST":
effectivePolicy, err = policy.Reload()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
default:
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w)
e.SetIndent("", "\t")
e.Encode(effectivePolicy)
}
type resJSON struct {
Error string `json:",omitempty"`
}

View File

@@ -26,7 +26,6 @@ import (
"tailscale.com/types/preftype"
"tailscale.com/types/views"
"tailscale.com/util/dnsname"
"tailscale.com/util/syspolicy"
)
// DefaultControlURL is the URL base of the control plane
@@ -690,10 +689,7 @@ func (p PrefsView) ControlURLOrDefault() string {
// If not configured, or if the configured value is a legacy name equivalent to
// the default, then DefaultControlURL is returned instead.
func (p *Prefs) ControlURLOrDefault() string {
controlURL, err := syspolicy.GetString(syspolicy.ControlURL, p.ControlURL)
if err != nil {
controlURL = p.ControlURL
}
controlURL := p.ControlURL
if controlURL != "" {
if controlURL != DefaultControlURL && IsLoginServerSynonym(controlURL) {

View File

@@ -2,129 +2,3 @@
// SPDX-License-Identifier: BSD-3-Clause
package opt
import (
"fmt"
"reflect"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
)
// Value is an optional value to be JSON-encoded.
// With [encoding/json], a zero Value is marshaled as a JSON null.
// With [github.com/go-json-experiment/json], a zero Value is omitted from the
// JSON object if the Go struct field specified with omitzero.
// The omitempty tag option should never be used with Value fields.
type Value[T any] struct {
value T
set bool
}
// Equal reports whether the receiver and the other value are equal.
// If the template type T in Value[T] implements an Equal method, it will be used
// instead of the == operator for comparing values.
type equatable[T any] interface {
// Equal reports whether the receiver and the other values are equal.
Equal(other T) bool
}
// ValueOf returns an optional Value containing the specified value.
// It treats nil slices and maps as empty slices and maps.
func ValueOf[T any](v T) Value[T] {
return Value[T]{value: v, set: true}
}
// String implements [fmt.Stringer].
func (o Value[T]) String() string {
if !o.set {
return fmt.Sprintf("(empty[%T])", o.value)
}
return fmt.Sprint(o.value)
}
// Set assigns the specified value to the optional value o.
func (o *Value[T]) Set(v T) {
*o = ValueOf(v)
}
// Clear resets o to an empty state.
func (o *Value[T]) Clear() {
*o = Value[T]{}
}
// IsSet reports whether o has a value set.
func (o *Value[T]) IsSet() bool {
return o.set
}
// Get returns the value of o.
// If a value hasn't been set, a zero value of T will be returned.
func (o Value[T]) Get() T {
return o.value
}
// GetOr returns the value of o or def if a value hasn't been set.
func (o Value[T]) GetOr(def T) T {
if o.set {
return o.value
}
return def
}
// Get returns the value and a flag indicating whether the value is set.
func (o Value[T]) GetOk() (v T, ok bool) {
return o.value, o.set
}
// Equal reports whether o is equal to v.
// Two optional values are equal if both are empty,
// or if both are set and the underlying values are equal.
// If the template type T implements an Equal(T) bool method, it will be used
// instead of the == operator for value comparison.
// If T is not comparable, it returns false.
func (o Value[T]) Equal(v Value[T]) bool {
if o.set != v.set {
return false
}
if !o.set {
return true
}
ov := any(o.value)
if eq, ok := ov.(equatable[T]); ok {
return eq.Equal(v.value)
}
if reflect.TypeFor[T]().Comparable() {
return ov == any(v.value)
}
return false
}
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (o Value[T]) MarshalJSONV2(enc *jsontext.Encoder, opts jsonv2.Options) error {
if !o.set {
return enc.WriteToken(jsontext.Null)
}
return jsonv2.MarshalEncode(enc, &o.value, opts)
}
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (o *Value[T]) UnmarshalJSONV2(dec *jsontext.Decoder, opts jsonv2.Options) error {
if dec.PeekKind() == 'n' {
*o = Value[T]{}
_, err := dec.ReadToken() // read null
return err
}
o.set = true
return jsonv2.UnmarshalDecode(dec, &o.value, opts)
}
// MarshalJSON implements [json.Marshaler].
func (o Value[T]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(o) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (o *Value[T]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, o) // uses UnmarshalJSONV2
}

View File

@@ -1,178 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package prefs
import (
"fmt"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/opt"
"tailscale.com/types/ptr"
"tailscale.com/types/views"
"tailscale.com/util/must"
)
// Item is a single preference item that can be configured.
// T must either be an immutable type or implement the [views.ViewCloner] interface.
type Item[T any] struct {
preference[T]
}
// ItemOf returns an [Item] configured with the specified value and [Options].
func ItemOf[T any](v T, opts ...Options) Item[T] {
return Item[T]{preferenceOf(opt.ValueOf(must.Get(deepClone(v))), opts...)}
}
// ItemWithOpts returns an unconfigured [Item] with the specified [Options].
func ItemWithOpts[T any](opts ...Options) Item[T] {
return Item[T]{preferenceOf(opt.Value[T]{}, opts...)}
}
// SetValue configures the preference with the specified value.
// It fails and returns [ErrManaged] if p is a managed preference,
// and [ErrReadOnly] if p is a read-only preference.
func (i *Item[T]) SetValue(val T) error {
return i.preference.SetValue(must.Get(deepClone(val)))
}
// SetManagedValue configures the preference with the specified value
// and marks the preference as managed.
func (i *Item[T]) SetManagedValue(val T) {
i.preference.SetManagedValue(must.Get(deepClone(val)))
}
// Clone returns a copy of i that aliases no memory with i.
// It is a runtime error to call [Item.Clone] if T contains pointers
// but does not implement [views.Cloner].
func (i Item[T]) Clone() *Item[T] {
res := ptr.To(i)
if v, ok := i.ValueOk(); ok {
res.s.Value.Set(must.Get(deepClone(v)))
}
return res
}
// Equal reports whether i and i2 are equal.
// If the template type T implements an Equal(T) bool method, it will be used
// instead of the == operator for value comparison.
// If T is not comparable, it reports false.
func (i Item[T]) Equal(i2 Item[T]) bool {
if i.s.Metadata != i2.s.Metadata {
return false
}
return i.s.Value.Equal(i2.s.Value)
}
func deepClone[T any](v T) (T, error) {
if c, ok := any(v).(views.Cloner[T]); ok {
return c.Clone(), nil
}
if !views.ContainsPointers[T]() {
return v, nil
}
var zero T
return zero, fmt.Errorf("%T contains pointers, but does not implement Clone", v)
}
// ItemView is a read-only view of an [Item][T], where T is a mutable type
// implementing [views.ViewCloner].
type ItemView[T views.ViewCloner[T, V], V views.StructView[T]] struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *Item[T]
}
// ItemViewOf returns a read-only view of i.
// It is used by [tailscale.com/cmd/viewer].
func ItemViewOf[T views.ViewCloner[T, V], V views.StructView[T]](i *Item[T]) ItemView[T, V] {
return ItemView[T, V]{i}
}
// Valid reports whether the underlying [Item] is non-nil.
func (iv ItemView[T, V]) Valid() bool {
return iv.ж != nil
}
// AsStruct implements [views.StructView] by returning a clone of the preference
// which aliases no memory with the original.
func (iv ItemView[T, V]) AsStruct() *Item[T] {
if iv.ж == nil {
return nil
}
return iv.ж.Clone()
}
// IsSet reports whether the preference has a value set.
func (iv ItemView[T, V]) IsSet() bool {
return iv.ж.IsSet()
}
// Value returns a read-only view of the value if the preference has a value set.
// Otherwise, it returns a read-only view of its default value.
func (iv ItemView[T, V]) Value() V {
return iv.ж.Value().View()
}
// ValueOk returns a read-only view of the value and true if the preference has a value set.
// Otherwise, it returns an invalid view and false.
func (iv ItemView[T, V]) ValueOk() (val V, ok bool) {
if val, ok := iv.ж.ValueOk(); ok {
return val.View(), true
}
return val, false
}
// DefaultValue returns a read-only view of the default value of the preference.
func (iv ItemView[T, V]) DefaultValue() V {
return iv.ж.DefaultValue().View()
}
// IsManaged reports whether the preference is managed via MDM, Group Policy, or similar means.
func (iv ItemView[T, V]) IsManaged() bool {
return iv.ж.IsManaged()
}
// IsReadOnly reports whether the preference is read-only and cannot be changed by user.
func (iv ItemView[T, V]) IsReadOnly() bool {
return iv.ж.IsReadOnly()
}
// Equal reports whether iv and iv2 are equal.
func (iv ItemView[T, V]) Equal(iv2 ItemView[T, V]) bool {
if !iv.Valid() && !iv2.Valid() {
return true
}
if iv.Valid() != iv2.Valid() {
return false
}
return iv.ж.Equal(*iv2.ж)
}
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (iv ItemView[T, V]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return iv.ж.MarshalJSONV2(out, opts)
}
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (iv *ItemView[T, V]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
var x Item[T]
if err := x.UnmarshalJSONV2(in, opts); err != nil {
return err
}
iv.ж = &x
return nil
}
// MarshalJSON implements [json.Marshaler].
func (iv ItemView[T, V]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(iv) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (iv *ItemView[T, V]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, iv) // uses UnmarshalJSONV2
}

View File

@@ -1,159 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package prefs
import (
"maps"
"net/netip"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"golang.org/x/exp/constraints"
"tailscale.com/types/opt"
"tailscale.com/types/ptr"
"tailscale.com/types/views"
)
// MapKeyType is a constraint allowing types that can be used as [Map] and [StructMap] keys.
// To satisfy this requirement, a type must be comparable and must encode as a JSON string.
// See [jsonv2.Marshal] for more details.
type MapKeyType interface {
~string | constraints.Integer | netip.Addr | netip.Prefix | netip.AddrPort
}
// Map is a preference type that holds immutable key-value pairs.
type Map[K MapKeyType, V ImmutableType] struct {
preference[map[K]V]
}
// MapOf returns a map configured with the specified value and [Options].
func MapOf[K MapKeyType, V ImmutableType](v map[K]V, opts ...Options) Map[K, V] {
return Map[K, V]{preferenceOf(opt.ValueOf(v), opts...)}
}
// MapWithOpts returns an unconfigured [Map] with the specified [Options].
func MapWithOpts[K MapKeyType, V ImmutableType](opts ...Options) Map[K, V] {
return Map[K, V]{preferenceOf(opt.Value[map[K]V]{}, opts...)}
}
// View returns a read-only view of m.
func (m *Map[K, V]) View() MapView[K, V] {
return MapView[K, V]{m}
}
// Clone returns a copy of m that aliases no memory with m.
func (m Map[K, V]) Clone() *Map[K, V] {
res := ptr.To(m)
if v, ok := m.s.Value.GetOk(); ok {
res.s.Value.Set(maps.Clone(v))
}
return res
}
// Equal reports whether m and m2 are equal.
func (m Map[K, V]) Equal(m2 Map[K, V]) bool {
if m.s.Metadata != m2.s.Metadata {
return false
}
v1, ok1 := m.s.Value.GetOk()
v2, ok2 := m2.s.Value.GetOk()
if ok1 != ok2 {
return false
}
return !ok1 || maps.Equal(v1, v2)
}
// MapView is a read-only view of a [Map].
type MapView[K MapKeyType, V ImmutableType] struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *Map[K, V]
}
// Valid reports whether the underlying [Map] is non-nil.
func (mv MapView[K, V]) Valid() bool {
return mv.ж != nil
}
// AsStruct implements [views.StructView] by returning a clone of the [Map]
// which aliases no memory with the original.
func (mv MapView[K, V]) AsStruct() *Map[K, V] {
if mv.ж == nil {
return nil
}
return mv.ж.Clone()
}
// IsSet reports whether the preference has a value set.
func (mv MapView[K, V]) IsSet() bool {
return mv.ж.IsSet()
}
// Value returns a read-only view of the value if the preference has a value set.
// Otherwise, it returns a read-only view of its default value.
func (mv MapView[K, V]) Value() views.Map[K, V] {
return views.MapOf(mv.ж.Value())
}
// ValueOk returns a read-only view of the value and true if the preference has a value set.
// Otherwise, it returns an invalid view and false.
func (mv MapView[K, V]) ValueOk() (val views.Map[K, V], ok bool) {
if v, ok := mv.ж.ValueOk(); ok {
return views.MapOf(v), true
}
return views.Map[K, V]{}, false
}
// DefaultValue returns a read-only view of the default value of the preference.
func (mv MapView[K, V]) DefaultValue() views.Map[K, V] {
return views.MapOf(mv.ж.DefaultValue())
}
// Managed reports whether the preference is managed via MDM, Group Policy, or similar means.
func (mv MapView[K, V]) Managed() bool {
return mv.ж.IsManaged()
}
// ReadOnly reports whether the preference is read-only and cannot be changed by user.
func (mv MapView[K, V]) ReadOnly() bool {
return mv.ж.IsReadOnly()
}
// Equal reports whether mv and mv2 are equal.
func (mv MapView[K, V]) Equal(mv2 MapView[K, V]) bool {
if !mv.Valid() && !mv2.Valid() {
return true
}
if mv.Valid() != mv2.Valid() {
return false
}
return mv.ж.Equal(*mv2.ж)
}
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (mv MapView[K, V]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return mv.ж.MarshalJSONV2(out, opts)
}
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (mv *MapView[K, V]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
var x Map[K, V]
if err := x.UnmarshalJSONV2(in, opts); err != nil {
return err
}
mv.ж = &x
return nil
}
// MarshalJSON implements [json.Marshaler].
func (mv MapView[K, V]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(mv) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (mv *MapView[K, V]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, mv) // uses UnmarshalJSONV2
}

View File

@@ -4,8 +4,6 @@
package syspolicy
import (
"tailscale.com/util/syspolicy/internal"
"tailscale.com/util/syspolicy/rsop"
"tailscale.com/util/syspolicy/setting"
"tailscale.com/util/syspolicy/source"
)
@@ -30,27 +28,6 @@ type Handler interface {
ReadStringArray(key string) ([]string, error)
}
// RegisterHandler wraps and registers the specified handler as the device's
// policy [source.Store] for the program's lifetime.
//
// Deprecated: using [RegisterStore] should be preferred.
func RegisterHandler(h Handler) {
rsop.RegisterStore("DeviceHandler", setting.DeviceScope, WrapHandler(h))
}
// TB is a subset of testing.TB that we use to set up test helpers.
// It's defined here to avoid pulling in the testing package.
type TB = internal.TB
// SetHandlerForTest wraps and sets the specified handler as the device's policy
// [source.Store] for the duration of tb.
//
// Deprecated: using [MustRegisterStoreForTest] should be preferred.
func SetHandlerForTest(tb TB, h Handler) {
RegisterWellKnownSettingsForTest(tb)
MustRegisterStoreForTest(tb, "DeviceHandler-TestOnly", setting.DefaultScope(), WrapHandler(h))
}
var _ source.Store = (*handlerStore)(nil)
// handlerStore is a [source.Store] that calls the underlying [Handler].

View File

@@ -6,9 +6,6 @@
package internal
import (
"bytes"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/lazy"
"tailscale.com/version"
)
@@ -36,31 +33,3 @@ type TB interface {
Fatal(args ...any)
Fatalf(format string, args ...any)
}
// EqualJSONForTest compares the JSON in j1 and j2 for semantic equality.
// It returns "", "", true if j1 and j2 are equal. Otherwise, it returns
// indented versions of j1 and j2 and false.
func EqualJSONForTest(tb TB, j1, j2 jsontext.Value) (s1, s2 string, equal bool) {
tb.Helper()
j1 = j1.Clone()
j2 = j2.Clone()
// Canonicalize JSON values for comparison.
if err := j1.Canonicalize(); err != nil {
tb.Error(err)
}
if err := j2.Canonicalize(); err != nil {
tb.Error(err)
}
// Check and return true if the two values are structurally equal.
if bytes.Equal(j1, j2) {
return "", "", true
}
// Otherwise, format the values for display and return false.
if err := j1.Indent("", "\t"); err != nil {
tb.Fatal(err)
}
if err := j2.Indent("", "\t"); err != nil {
tb.Fatal(err)
}
return j1.String(), j2.String(), false
}

View File

@@ -4,10 +4,7 @@
package syspolicy
import (
"tailscale.com/types/lazy"
"tailscale.com/util/syspolicy/internal"
"tailscale.com/util/syspolicy/setting"
"tailscale.com/util/testenv"
)
// Key is a string that uniquely identifies a policy and must remain unchanged
@@ -127,91 +124,3 @@ const (
// AllowedSuggestedExitNodes's string array value is a list of exit node IDs that restricts which exit nodes are considered when generating suggestions for exit nodes.
AllowedSuggestedExitNodes Key = "AllowedSuggestedExitNodes"
)
// implicitDefinitions is a list of [setting.Definition] that will be registered
// automatically when the policy setting definitions are first used by the syspolicy package hierarchy.
// This includes the first time a policy needs to be read from any source.
var implicitDefinitions = []*setting.Definition{
// Device policy settings (can only be configured on a per-device basis):
setting.NewDefinition(AllowedSuggestedExitNodes, setting.DeviceSetting, setting.StringListValue),
setting.NewDefinition(ApplyUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
setting.NewDefinition(AuthKey, setting.DeviceSetting, setting.StringValue),
setting.NewDefinition(CheckUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
setting.NewDefinition(ControlURL, setting.DeviceSetting, setting.StringValue),
setting.NewDefinition(DeviceSerialNumber, setting.DeviceSetting, setting.StringValue),
setting.NewDefinition(EnableIncomingConnections, setting.DeviceSetting, setting.PreferenceOptionValue),
setting.NewDefinition(EnableRunExitNode, setting.DeviceSetting, setting.PreferenceOptionValue),
setting.NewDefinition(EnableServerMode, setting.DeviceSetting, setting.PreferenceOptionValue),
setting.NewDefinition(EnableTailscaleDNS, setting.DeviceSetting, setting.PreferenceOptionValue),
setting.NewDefinition(EnableTailscaleSubnets, setting.DeviceSetting, setting.PreferenceOptionValue),
setting.NewDefinition(ExitNodeAllowLANAccess, setting.DeviceSetting, setting.PreferenceOptionValue),
setting.NewDefinition(ExitNodeID, setting.DeviceSetting, setting.StringValue),
setting.NewDefinition(ExitNodeIP, setting.DeviceSetting, setting.StringValue),
setting.NewDefinition(FlushDNSOnSessionUnlock, setting.DeviceSetting, setting.BooleanValue),
setting.NewDefinition(LogSCMInteractions, setting.DeviceSetting, setting.BooleanValue),
setting.NewDefinition(LogTarget, setting.DeviceSetting, setting.StringValue),
setting.NewDefinition(MachineCertificateSubject, setting.DeviceSetting, setting.StringValue),
setting.NewDefinition(PostureChecking, setting.DeviceSetting, setting.PreferenceOptionValue),
setting.NewDefinition(Tailnet, setting.DeviceSetting, setting.StringValue),
// User policy settings (can be configured on a user- or device-basis):
setting.NewDefinition(AdminConsoleVisibility, setting.UserSetting, setting.VisibilityValue),
setting.NewDefinition(AutoUpdateVisibility, setting.UserSetting, setting.VisibilityValue),
setting.NewDefinition(ExitNodeMenuVisibility, setting.UserSetting, setting.VisibilityValue),
setting.NewDefinition(KeyExpirationNoticeTime, setting.UserSetting, setting.DurationValue),
setting.NewDefinition(ManagedByCaption, setting.UserSetting, setting.StringValue),
setting.NewDefinition(ManagedByOrganizationName, setting.UserSetting, setting.StringValue),
setting.NewDefinition(ManagedByURL, setting.UserSetting, setting.StringValue),
setting.NewDefinition(NetworkDevicesVisibility, setting.UserSetting, setting.VisibilityValue),
setting.NewDefinition(PreferencesMenuVisibility, setting.UserSetting, setting.VisibilityValue),
setting.NewDefinition(ResetToDefaultsVisibility, setting.UserSetting, setting.VisibilityValue),
setting.NewDefinition(RunExitNodeVisibility, setting.UserSetting, setting.VisibilityValue),
setting.NewDefinition(SuggestedExitNodeVisibility, setting.UserSetting, setting.VisibilityValue),
setting.NewDefinition(TestMenuVisibility, setting.UserSetting, setting.VisibilityValue),
setting.NewDefinition(UpdateMenuVisibility, setting.UserSetting, setting.VisibilityValue),
setting.NewDefinition(OnboardingFlowVisibility, setting.UserSetting, setting.VisibilityValue),
}
func init() {
internal.Init.MustDefer(func() error {
// Avoid implicit [setting.Definition] registration during tests.
// Each test should control which policy settings to register.
// Use [setting.SetDefinitionsForTest] to specify necessary definitions,
// or [setWellKnownSettingsForTest] to set implicit definitions for the test duration.
if testenv.InTest() {
return nil
}
for _, d := range implicitDefinitions {
setting.RegisterDefinition(d)
}
return nil
})
}
var implicitDefinitionMap lazy.SyncValue[setting.DefinitionMap]
// WellKnownSettingDefinition returns a well-known, implicit setting definition by its key,
// or an [ErrNoSuchKey] if a policy setting with the specified key does not exist
// among implicit policy definitions.
func WellKnownSettingDefinition(k Key) (*setting.Definition, error) {
m, err := implicitDefinitionMap.GetErr(func() (setting.DefinitionMap, error) {
return setting.DefinitionMapOf(implicitDefinitions)
})
if err != nil {
return nil, err
}
if d, ok := m[k]; ok {
return d, nil
}
return nil, ErrNoSuchKey
}
// RegisterWellKnownSettingsForTest registers all implicit setting definitions
// for the duration of the test.
func RegisterWellKnownSettingsForTest(tb TB) {
tb.Helper()
err := setting.SetDefinitionsForTest(tb, implicitDefinitions...)
if err != nil {
tb.Fatalf("Failed to register well-known settings: %v", err)
}
}

View File

@@ -2,70 +2,3 @@
// SPDX-License-Identifier: BSD-3-Clause
package setting
import (
"fmt"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
)
// Origin describes where a policy or a policy setting is configured.
type Origin struct {
data settingOrigin
}
// settingOrigin is the marshallable data of an [Origin].
type settingOrigin struct {
Name string `json:",omitzero"`
Scope PolicyScope
}
// NewOrigin returns a new [Origin] with the specified scope.
func NewOrigin(scope PolicyScope) *Origin {
return NewNamedOrigin("", scope)
}
// NewNamedOrigin returns a new [Origin] with the specified scope and name.
func NewNamedOrigin(name string, scope PolicyScope) *Origin {
return &Origin{settingOrigin{name, scope}}
}
// Scope reports the policy [PolicyScope] where the setting is configured.
func (s Origin) Scope() PolicyScope {
return s.data.Scope
}
// Name returns the name of the policy source where the setting is configured,
// or "" if not available.
func (s Origin) Name() string {
return s.data.Name
}
// String implements [fmt.Stringer].
func (s Origin) String() string {
if s.Name() != "" {
return fmt.Sprintf("%s (%v)", s.Name(), s.Scope())
}
return s.Scope().String()
}
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (s Origin) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return jsonv2.MarshalEncode(out, &s.data, opts)
}
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (s *Origin) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
return jsonv2.UnmarshalDecode(in, &s.data, opts)
}
// MarshalJSON implements [json.Marshaler].
func (s Origin) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(s) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (s *Origin) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONV2
}

View File

@@ -8,7 +8,6 @@ import (
"strings"
"tailscale.com/types/lazy"
"tailscale.com/util/syspolicy/internal"
)
var (
@@ -32,15 +31,6 @@ type PolicyScope struct {
profileID string
}
// DefaultScope returns the default [PolicyScope] to be used by a program
// when querying policy settings.
// It returns [DeviceScope], unless explicitly changed with [SetDefaultScope].
func DefaultScope() PolicyScope {
// Allow deferred package init functions to override the default scope.
internal.Init.Do()
return lazyDefaultScope.Get(func() PolicyScope { return DeviceScope })
}
// SetDefaultScope attempts to set the specified scope as the default scope
// to be used by a program when querying policy settings.
// It fails and returns false if called more than once, or if the [DefaultScope]

View File

@@ -2,161 +2,3 @@
// SPDX-License-Identifier: BSD-3-Clause
package setting
import (
"fmt"
"reflect"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/opt"
"tailscale.com/types/structs"
)
// RawItem contains a raw policy setting value as read from a policy store, or an
// error if the requested setting could not be read from the store. As a special
// case, it may also hold a value of the [Visibility], [PreferenceOption],
// or [time.Duration] types. While the policy store interface does not support
// these types natively, and the values of these types have to be unmarshalled
// or converted from strings, these setting types predate the typed policy
// hierarchies, and must be supported at this layer.
type RawItem struct {
_ structs.Incomparable
data rawItemJSON
}
// rawItemJSON holds JSON-marshallable data for [RawItem].
type rawItemJSON struct {
Value RawValue `json:",omitzero"`
Error *ErrorText `json:",omitzero"` // or nil
Origin *Origin `json:",omitzero"` // or nil
}
// RawItemOf returns a [RawItem] with the specified value.
func RawItemOf(value any) RawItem {
return RawItemWith(value, nil, nil)
}
// RawItemWith returns a [RawItem] with the specified value, error and origin.
func RawItemWith(value any, err *ErrorText, origin *Origin) RawItem {
return RawItem{data: rawItemJSON{Value: RawValue{opt.ValueOf(value)}, Error: err, Origin: origin}}
}
// Value returns the value of the policy setting, or nil if the policy setting
// is not configured, or an error occurred while reading it.
func (i RawItem) Value() any {
return i.data.Value.Get()
}
// Error returns the error that occurred when reading the policy setting,
// or nil if no error occurred.
func (i RawItem) Error() error {
if i.data.Error != nil {
return i.data.Error
}
return nil
}
// Origin returns an optional [Origin] indicating where the policy setting is
// configured.
func (i RawItem) Origin() *Origin {
return i.data.Origin
}
// String implements [fmt.Stringer].
func (i RawItem) String() string {
var suffix string
if i.data.Origin != nil {
suffix = fmt.Sprintf(" - {%v}", i.data.Origin)
}
if i.data.Error != nil {
return fmt.Sprintf("Error{%q}%s", i.data.Error.Error(), suffix)
}
return fmt.Sprintf("%v%s", i.data.Value.Value, suffix)
}
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (i RawItem) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return jsonv2.MarshalEncode(out, &i.data, opts)
}
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (i *RawItem) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
return jsonv2.UnmarshalDecode(in, &i.data, opts)
}
// MarshalJSON implements [json.Marshaler].
func (i RawItem) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(i) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (i *RawItem) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, i) // uses UnmarshalJSONV2
}
// RawValue represents a raw policy setting value read from a policy store.
// It is JSON-marshallable and facilitates unmarshalling of JSON values
// into corresponding policy setting types, with special handling for JSON numbers
// (unmarshalled as float64) and JSON string arrays (unmarshalled as []string).
// See also [RawValue.UnmarshalJSONV2].
type RawValue struct {
opt.Value[any]
}
// RawValueType is a constraint that permits raw setting value types.
type RawValueType interface {
bool | uint64 | string | []string
}
// RawValueOf returns a new [RawValue] holding the specified value.
func RawValueOf[T RawValueType](v T) RawValue {
return RawValue{opt.ValueOf[any](v)}
}
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (v RawValue) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return jsonv2.MarshalEncode(out, v.Value, opts)
}
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2] by attempting to unmarshal
// a JSON value as one of the supported policy setting value types (bool, string, uint64, or []string),
// based on the JSON value type. It fails if the JSON value is an object, if it's a JSON number that
// cannot be represented as a uint64, or if a JSON array contains anything other than strings.
func (v *RawValue) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
var valPtr any
switch k := in.PeekKind(); k {
case 't', 'f':
valPtr = new(bool)
case '"':
valPtr = new(string)
case '0':
valPtr = new(uint64) // unmarshal JSON numbers as uint64
case '[', 'n':
valPtr = new([]string) // unmarshal arrays as string slices
case '{':
return fmt.Errorf("unexpected token: %v", k)
default:
panic("unreachable")
}
if err := jsonv2.UnmarshalDecode(in, valPtr, opts); err != nil {
v.Value.Clear()
return err
}
value := reflect.ValueOf(valPtr).Elem().Interface()
v.Value = opt.ValueOf(value)
return nil
}
// MarshalJSON implements [json.Marshaler].
func (v RawValue) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(v) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (v *RawValue) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, v) // uses UnmarshalJSONV2
}
// RawValues is a map of keyed setting values that can be read from a JSON.
type RawValues map[Key]RawValue

View File

@@ -15,7 +15,6 @@ import (
"time"
"tailscale.com/types/lazy"
"tailscale.com/util/syspolicy/internal"
)
// Scope indicates the broadest scope at which a policy setting may apply,
@@ -169,14 +168,6 @@ func (d *Definition) Type() Type {
return d.typ
}
// IsSupported reports whether the policy setting is supported on the current OS.
func (d *Definition) IsSupported() bool {
if d == nil {
return false
}
return d.platforms.HasCurrent()
}
// SupportedPlatforms reports platforms on which the policy setting is supported.
// An empty [PlatformList] indicates that s is available on all platforms.
func (d *Definition) SupportedPlatforms() PlatformList {
@@ -217,103 +208,6 @@ var (
definitionsUsed bool
)
// Register registers a policy setting with the specified key, scope, value type,
// and an optional list of supported platforms. All policy settings must be
// registered before any of them can be used. Register panics if called after
// invoking any functions that use the registered policy definitions. This
// includes calling [Definitions] or [DefinitionOf] directly, or reading any
// policy settings via syspolicy.
func Register(k Key, s Scope, t Type, platforms ...string) {
RegisterDefinition(NewDefinition(k, s, t, platforms...))
}
// RegisterDefinition is like [Register], but accepts a [Definition].
func RegisterDefinition(d *Definition) {
definitionsMu.Lock()
defer definitionsMu.Unlock()
registerLocked(d)
}
func registerLocked(d *Definition) {
if definitionsUsed {
panic("policy definitions are already in use")
}
definitionsList = append(definitionsList, d)
}
func settingDefinitions() (DefinitionMap, error) {
return definitions.GetErr(func() (DefinitionMap, error) {
if err := internal.Init.Do(); err != nil {
return nil, err
}
definitionsMu.Lock()
defer definitionsMu.Unlock()
definitionsUsed = true
return DefinitionMapOf(definitionsList)
})
}
// DefinitionMapOf returns a [DefinitionMap] with the specified settings,
// or an error if any settings have the same key but different type or scope.
func DefinitionMapOf(settings []*Definition) (DefinitionMap, error) {
m := make(DefinitionMap, len(settings))
for _, s := range settings {
if existing, exists := m[s.key]; exists {
if existing.Equal(s) {
// Ignore duplicate setting definitions if they match. It is acceptable
// if the same policy setting was registered more than once
// (e.g. by the syspolicy package itself and by iOS/Android code).
existing.platforms.mergeFrom(s.platforms)
continue
}
return nil, fmt.Errorf("duplicate policy definition: %q", s.key)
}
m[s.key] = s
}
return m, nil
}
// SetDefinitionsForTest allows to register the specified setting definitions
// for the test duration. It is not concurrency-safe, but unlike [Register],
// it does not panic and can be called anytime.
// It returns an error if ds contains two different settings with the same [Key].
func SetDefinitionsForTest(tb lazy.TB, ds ...*Definition) error {
m, err := DefinitionMapOf(ds)
if err != nil {
return err
}
definitions.SetForTest(tb, m, err)
return nil
}
// DefinitionOf returns a setting definition by key,
// or [ErrNoSuchKey] if the specified key does not exist,
// or an error if there are conflicting policy definitions.
func DefinitionOf(k Key) (*Definition, error) {
ds, err := settingDefinitions()
if err != nil {
return nil, err
}
if d, ok := ds[k]; ok {
return d, nil
}
return nil, ErrNoSuchKey
}
// Definitions returns all registered setting definitions,
// or an error if different policies were registered under the same name.
func Definitions() ([]*Definition, error) {
ds, err := settingDefinitions()
if err != nil {
return nil, err
}
res := make([]*Definition, 0, len(ds))
for _, d := range ds {
res = append(res, d)
}
return res, nil
}
// PlatformList is a list of OSes.
// An empty list indicates that all possible platforms are supported.
type PlatformList []string
@@ -328,11 +222,6 @@ func (l PlatformList) Has(target string) bool {
})
}
// HasCurrent is like Has, but for the current platform.
func (l PlatformList) HasCurrent() bool {
return l.Has(internal.OS())
}
// mergeFrom merges l2 into l. Since an empty list indicates no platform restrictions,
// if either l or l2 is empty, the merged result in l will also be empty.
func (l *PlatformList) mergeFrom(l2 PlatformList) {

View File

@@ -2,214 +2,3 @@
// SPDX-License-Identifier: BSD-3-Clause
package setting
import (
"errors"
"iter"
"maps"
"slices"
"strings"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
xmaps "golang.org/x/exp/maps"
"tailscale.com/util/deephash"
)
// Snapshot is an immutable collection of ([Key], [RawItem]) pairs, representing
// a set of policy settings applied at a specific moment in time.
// A nil pointer to [Snapshot] is valid.
type Snapshot struct {
m map[Key]RawItem
sig deephash.Sum // of m
summary Summary
}
// NewSnapshot returns a new [Snapshot] with the specified items and options.
func NewSnapshot(items map[Key]RawItem, opts ...SummaryOption) *Snapshot {
return &Snapshot{m: xmaps.Clone(items), sig: deephash.Hash(&items), summary: SummaryWith(opts...)}
}
// All returns an iterator over policy settings in s. The iteration order is not
// specified and is not guaranteed to be the same from one call to the next.
func (s *Snapshot) All() iter.Seq2[Key, RawItem] {
if s == nil {
return func(yield func(Key, RawItem) bool) {}
}
return maps.All(s.m)
}
// Get returns the value of the policy setting with the specified key
// or nil if it is not configured or has an error.
func (s *Snapshot) Get(k Key) any {
v, _ := s.GetErr(k)
return v
}
// GetErr returns the value of the policy setting with the specified key,
// [ErrNotConfigured] if it is not configured, or an error returned by
// the policy Store if the policy setting could not be read.
func (s *Snapshot) GetErr(k Key) (any, error) {
if s != nil {
if s, ok := s.m[k]; ok {
return s.Value(), s.Error()
}
}
return nil, ErrNotConfigured
}
// GetSetting returns the untyped policy setting with the specified key and true
// if a policy setting with such key has been configured;
// otherwise, it returns zero, false.
func (s *Snapshot) GetSetting(k Key) (setting RawItem, ok bool) {
setting, ok = s.m[k]
return setting, ok
}
// Equal reports whether s and s2 are equal.
func (s *Snapshot) Equal(s2 *Snapshot) bool {
if s == s2 {
return true
}
if !s.EqualItems(s2) {
return false
}
return s.Summary() == s2.Summary()
}
// EqualItems reports whether items in s and s2 are equal.
func (s *Snapshot) EqualItems(s2 *Snapshot) bool {
if s == s2 {
return true
}
if s.Len() != s2.Len() {
return false
}
if s.Len() == 0 {
return true
}
return s.sig == s2.sig
}
// Keys return an iterator over keys in s. The iteration order is not specified
// and is not guaranteed to be the same from one call to the next.
func (s *Snapshot) Keys() iter.Seq[Key] {
if s.m == nil {
return func(yield func(Key) bool) {}
}
return maps.Keys(s.m)
}
// Len reports the number of [RawItem]s in s.
func (s *Snapshot) Len() int {
if s == nil {
return 0
}
return len(s.m)
}
// Summary returns information about s as a whole rather than about specific [RawItem]s in it.
func (s *Snapshot) Summary() Summary {
if s == nil {
return Summary{}
}
return s.summary
}
// String implements [fmt.Stringer]
func (s *Snapshot) String() string {
if s.Len() == 0 && s.Summary().IsEmpty() {
return "{Empty}"
}
var sb strings.Builder
if !s.summary.IsEmpty() {
sb.WriteRune('{')
if s.Len() == 0 {
sb.WriteString("Empty, ")
}
sb.WriteString(s.summary.String())
sb.WriteRune('}')
}
for _, k := range slices.Sorted(s.Keys()) {
if sb.Len() != 0 {
sb.WriteRune('\n')
}
sb.WriteString(string(k))
sb.WriteString(" = ")
sb.WriteString(s.m[k].String())
}
return sb.String()
}
// snapshotJSON holds JSON-marshallable data for [Snapshot].
type snapshotJSON struct {
Summary Summary `json:",omitzero"`
Settings map[Key]RawItem `json:",omitempty"`
}
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (s *Snapshot) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
data := &snapshotJSON{}
if s != nil {
data.Summary = s.summary
data.Settings = s.m
}
return jsonv2.MarshalEncode(out, data, opts)
}
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (s *Snapshot) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
if s == nil {
return errors.New("s must not be nil")
}
data := &snapshotJSON{}
if err := jsonv2.UnmarshalDecode(in, data, opts); err != nil {
return err
}
*s = Snapshot{m: data.Settings, sig: deephash.Hash(&data.Settings), summary: data.Summary}
return nil
}
// MarshalJSON implements [json.Marshaler].
func (s *Snapshot) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(s) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (s *Snapshot) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONV2
}
// MergeSnapshots returns a [Snapshot] that contains all [RawItem]s
// from snapshot1 and snapshot2 and the [Summary] with the narrower [PolicyScope].
// If there's a conflict between policy settings in the two snapshots,
// the policy settings from the snapshot with the broader scope take precedence.
// In other words, policy settings configured for the [DeviceScope] win
// over policy settings configured for a user scope.
func MergeSnapshots(snapshot1, snapshot2 *Snapshot) *Snapshot {
scope1, ok1 := snapshot1.Summary().Scope().GetOk()
scope2, ok2 := snapshot2.Summary().Scope().GetOk()
if ok1 && ok2 && scope1.StrictlyContains(scope2) {
// Swap snapshots if snapshot1 has higher precedence than snapshot2.
snapshot1, snapshot2 = snapshot2, snapshot1
}
if snapshot2.Len() == 0 {
return snapshot1
}
summaryOpts := make([]SummaryOption, 0, 2)
if scope, ok := snapshot1.Summary().Scope().GetOk(); ok {
// Use the scope from snapshot1, if present, which is the more specific snapshot.
summaryOpts = append(summaryOpts, scope)
}
if snapshot1.Len() == 0 {
if origin, ok := snapshot2.Summary().Origin().GetOk(); ok {
// Use the origin from snapshot2 if snapshot1 is empty.
summaryOpts = append(summaryOpts, origin)
}
return &Snapshot{snapshot2.m, snapshot2.sig, SummaryWith(summaryOpts...)}
}
m := make(map[Key]RawItem, snapshot1.Len()+snapshot2.Len())
xmaps.Copy(m, snapshot1.m)
xmaps.Copy(m, snapshot2.m) // snapshot2 has higher precedence
return &Snapshot{m, deephash.Hash(&m), SummaryWith(summaryOpts...)}
}

View File

@@ -2,99 +2,3 @@
// SPDX-License-Identifier: BSD-3-Clause
package setting
import (
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/opt"
)
// Summary is an immutable [PolicyScope] and [Origin].
type Summary struct {
data summary
}
type summary struct {
Scope opt.Value[PolicyScope] `json:",omitzero"`
Origin opt.Value[Origin] `json:",omitzero"`
}
// SummaryWith returns a [Summary] with the specified options.
func SummaryWith(opts ...SummaryOption) Summary {
var summary Summary
for _, o := range opts {
o.applySummaryOption(&summary)
}
return summary
}
// IsEmpty reports whether s is empty.
func (s Summary) IsEmpty() bool {
return s == Summary{}
}
// Scope reports the [PolicyScope] in s.
func (s Summary) Scope() opt.Value[PolicyScope] {
return s.data.Scope
}
// Origin reports the [Origin] in s.
func (s Summary) Origin() opt.Value[Origin] {
return s.data.Origin
}
// String implements [fmt.Stringer].
func (s Summary) String() string {
if s.IsEmpty() {
return "{Empty}"
}
if origin, ok := s.data.Origin.GetOk(); ok {
return origin.String()
}
return s.data.Scope.String()
}
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (s Summary) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return jsonv2.MarshalEncode(out, &s.data, opts)
}
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (s *Summary) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
return jsonv2.UnmarshalDecode(in, &s.data, opts)
}
// MarshalJSON implements [json.Marshaler].
func (s Summary) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(s) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (s *Summary) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONV2
}
// SummaryOption is an option that configures [Summary]
// The following are allowed options:
//
// - [Summary]
// - [PolicyScope]
// - [Origin]
type SummaryOption interface {
applySummaryOption(summary *Summary)
}
func (s PolicyScope) applySummaryOption(summary *Summary) {
summary.data.Scope.Set(s)
}
func (o Origin) applySummaryOption(summary *Summary) {
summary.data.Origin.Set(o)
if !summary.data.Scope.IsSet() {
summary.data.Scope.Set(o.Scope())
}
}
func (s Summary) applySummaryOption(summary *Summary) {
*summary = s
}

View File

@@ -1,159 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package source
import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"unicode/utf8"
"tailscale.com/util/syspolicy/setting"
)
var lookupEnv = os.LookupEnv // test hook
var _ Store = (*EnvPolicyStore)(nil)
// EnvPolicyStore is a [Store] that reads policy settings from environment variables.
type EnvPolicyStore struct{}
// ReadString implements [Store].
func (s *EnvPolicyStore) ReadString(key setting.Key) (string, error) {
_, str, err := s.lookupSettingVariable(key)
if err != nil {
return "", err
}
return str, nil
}
// ReadUInt64 implements [Store].
func (s *EnvPolicyStore) ReadUInt64(key setting.Key) (uint64, error) {
name, str, err := s.lookupSettingVariable(key)
if err != nil {
return 0, err
}
if str == "" {
return 0, setting.ErrNotConfigured
}
value, err := strconv.ParseUint(str, 0, 64)
if err != nil {
return 0, fmt.Errorf("%s: %w: %q is not a valid uint64", name, setting.ErrTypeMismatch, str)
}
return value, nil
}
// ReadBoolean implements [Store].
func (s *EnvPolicyStore) ReadBoolean(key setting.Key) (bool, error) {
name, str, err := s.lookupSettingVariable(key)
if err != nil {
return false, err
}
if str == "" {
return false, setting.ErrNotConfigured
}
value, err := strconv.ParseBool(str)
if err != nil {
return false, fmt.Errorf("%s: %w: %q is not a valid bool", name, setting.ErrTypeMismatch, str)
}
return value, nil
}
// ReadStringArray implements [Store].
func (s *EnvPolicyStore) ReadStringArray(key setting.Key) ([]string, error) {
_, str, err := s.lookupSettingVariable(key)
if err != nil || str == "" {
return nil, err
}
var dst int
res := strings.Split(str, ",")
for src := range res {
res[dst] = strings.TrimSpace(res[src])
if res[dst] != "" {
dst++
}
}
return res[0:dst], nil
}
func (s *EnvPolicyStore) lookupSettingVariable(key setting.Key) (name, value string, err error) {
name, err = keyToEnvVarName(key)
if err != nil {
return "", "", err
}
value, ok := lookupEnv(name)
if !ok {
return name, "", setting.ErrNotConfigured
}
return name, value, nil
}
var (
errEmptyKey = errors.New("key must not be empty")
errInvalidKey = errors.New("key must consist of alphanumeric characters and slashes")
)
// keyToEnvVarName returns the environment variable name for a given policy
// setting key, or an error if the key is invalid. It converts CamelCase keys into
// underscore-separated words and prepends the variable name with the TS prefix.
// For example: AuthKey => TS_AUTH_KEY, ExitNodeAllowLANAccess => TS_EXIT_NODE_ALLOW_LAN_ACCESS, etc.
//
// It's fine to use this in [EnvPolicyStore] without caching variable names since it's not a hot path.
// [EnvPolicyStore] is not a [Changeable] policy store, so the conversion will only happen once.
func keyToEnvVarName(key setting.Key) (string, error) {
if len(key) == 0 {
return "", errEmptyKey
}
isLower := func(c byte) bool { return 'a' <= c && c <= 'z' }
isUpper := func(c byte) bool { return 'A' <= c && c <= 'Z' }
isLetter := func(c byte) bool { return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') }
isDigit := func(c byte) bool { return '0' <= c && c <= '9' }
words := make([]string, 0, 8)
words = append(words, "TS_DEBUGSYSPOLICY")
var currentWord strings.Builder
for i := 0; i < len(key); i++ {
c := key[i]
if c >= utf8.RuneSelf {
return "", errInvalidKey
}
var split bool
switch {
case isLower(c):
c -= 'a' - 'A' // make upper
split = currentWord.Len() > 0 && !isLetter(key[i-1])
case isUpper(c):
if currentWord.Len() > 0 {
prevUpper := isUpper(key[i-1])
nextLower := i < len(key)-1 && isLower(key[i+1])
split = !prevUpper || nextLower // split on case transition
}
case isDigit(c):
split = currentWord.Len() > 0 && !isDigit(key[i-1])
case c == setting.KeyPathSeparator:
words = append(words, currentWord.String())
currentWord.Reset()
continue
default:
return "", errInvalidKey
}
if split {
words = append(words, currentWord.String())
currentWord.Reset()
}
currentWord.WriteByte(c)
}
if currentWord.Len() > 0 {
words = append(words, currentWord.String())
}
return strings.Join(words, "_"), nil
}

View File

@@ -1,394 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package source
import (
"errors"
"fmt"
"io"
"slices"
"sort"
"sync"
"time"
"tailscale.com/util/mak"
"tailscale.com/util/set"
"tailscale.com/util/syspolicy/internal/loggerx"
"tailscale.com/util/syspolicy/internal/metrics"
"tailscale.com/util/syspolicy/setting"
)
// Reader reads all configured policy settings from a given [Store].
// It registers a change callback with the [Store] and maintains the current version
// of the [setting.Snapshot] by lazily re-reading policy settings from the [Store]
// whenever a new settings snapshot is requested with [Reader.GetSettings].
// It is safe for concurrent use.
type Reader struct {
store Store
origin *setting.Origin
settings []*setting.Definition
unregisterChangeNotifier func()
doneCh chan struct{} // closed when [Reader] is closed.
mu sync.Mutex
closing bool
upToDate bool
lastPolicy *setting.Snapshot
sessions set.HandleSet[*ReadingSession]
}
// newReader returns a new [Reader] that reads policy settings from a given [Store].
// The returned reader takes ownership of the store. If the store implements [io.Closer],
// the returned reader will close the store when it is closed.
func newReader(store Store, origin *setting.Origin) (*Reader, error) {
settings, err := setting.Definitions()
if err != nil {
return nil, err
}
if expirable, ok := store.(Expirable); ok {
select {
case <-expirable.Done():
return nil, ErrStoreClosed
default:
}
}
reader := &Reader{store: store, origin: origin, settings: settings, doneCh: make(chan struct{})}
if changeable, ok := store.(Changeable); ok {
// We should subscribe to policy change notifications first before reading
// the policy settings from the store. This way we won't miss any notifications.
if reader.unregisterChangeNotifier, err = changeable.RegisterChangeCallback(reader.onPolicyChange); err != nil {
// Errors registering policy change callbacks are non-fatal.
// TODO(nickkhyl): implement a background policy refresh every X minutes?
loggerx.Errorf("failed to register %v policy change callback: %v", origin, err)
}
}
if _, err := reader.reload(true); err != nil {
if reader.unregisterChangeNotifier != nil {
reader.unregisterChangeNotifier()
}
return nil, err
}
if expirable, ok := store.(Expirable); ok {
if waitCh := expirable.Done(); waitCh != nil {
go func() {
select {
case <-waitCh:
reader.Close()
case <-reader.doneCh:
}
}()
}
}
return reader, nil
}
// GetSettings returns the current [*setting.Snapshot],
// re-reading it from from the underlying [Store] only if the policy
// has changed since it was read last. It never fails and returns
// the previous version of the policy settings if a read attempt fails.
func (r *Reader) GetSettings() *setting.Snapshot {
r.mu.Lock()
upToDate, lastPolicy := r.upToDate, r.lastPolicy
r.mu.Unlock()
if upToDate {
return lastPolicy
}
policy, err := r.reload(false)
if err != nil {
// If the policy fails to reload completely, log an error and return the last cached version.
// However, errors related to individual policy items are always
// propagated to callers when they fetch those settings.
loggerx.Errorf("failed to reload %v policy: %v", r.origin, err)
}
return policy
}
// ReadSettings reads policy settings from the underlying [Store] even if no
// changes were detected. It returns the new [*setting.Snapshot],nil on
// success or an undefined snapshot (possibly `nil`) along with a non-`nil`
// error in case of failure.
func (r *Reader) ReadSettings() (*setting.Snapshot, error) {
return r.reload(true)
}
// reload is like [Reader.ReadSettings], but allows specifying whether to re-read
// an unchanged policy, and returns the last [*setting.Snapshot] if the read fails.
func (r *Reader) reload(force bool) (*setting.Snapshot, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.upToDate && !force {
return r.lastPolicy, nil
}
if lockable, ok := r.store.(Lockable); ok {
if err := lockable.Lock(); err != nil {
return r.lastPolicy, err
}
defer lockable.Unlock()
}
r.upToDate = true
metrics.Reset(r.origin)
var m map[setting.Key]setting.RawItem
if lastPolicyCount := r.lastPolicy.Len(); lastPolicyCount > 0 {
m = make(map[setting.Key]setting.RawItem, lastPolicyCount)
}
for _, s := range r.settings {
if !r.origin.Scope().IsConfigurableSetting(s) {
// Skip settings that cannot be configured in the current scope.
continue
}
val, err := readPolicySettingValue(r.store, s)
if err != nil && (errors.Is(err, setting.ErrNoSuchKey) || errors.Is(err, setting.ErrNotConfigured)) {
metrics.ReportNotConfigured(r.origin, s)
continue
}
if err == nil {
metrics.ReportConfigured(r.origin, s, val)
} else {
metrics.ReportError(r.origin, s, err)
}
// If there's an error reading a single policy, such as a value type mismatch,
// we'll wrap the error to preserve its text and return it
// whenever someone attempts to fetch the value.
// Otherwise, the errorText will be nil.
errorText := setting.MaybeErrorText(err)
item := setting.RawItemWith(val, errorText, r.origin)
mak.Set(&m, s.Key(), item)
}
newPolicy := setting.NewSnapshot(m, setting.SummaryWith(r.origin))
if r.lastPolicy == nil || !newPolicy.EqualItems(r.lastPolicy) {
r.lastPolicy = newPolicy
}
return r.lastPolicy, nil
}
// ReadingSession is like [Reader], but with a channel that's written
// to when there's a policy change, and closed when the session is terminated.
type ReadingSession struct {
reader *Reader
policyChangedCh chan struct{} // 1-buffered channel
handle set.Handle // in the reader.sessions
closeInternal func()
}
// OpenSession opens and returns a new session to r, allowing the caller
// to get notified whenever a policy change is reported by the [source.Store],
// or an [ErrStoreClosed] if the reader has already been closed.
func (r *Reader) OpenSession() (*ReadingSession, error) {
session := &ReadingSession{
reader: r,
policyChangedCh: make(chan struct{}, 1),
}
session.closeInternal = sync.OnceFunc(func() { close(session.policyChangedCh) })
r.mu.Lock()
defer r.mu.Unlock()
if r.closing {
return nil, ErrStoreClosed
}
session.handle = r.sessions.Add(session)
return session, nil
}
// GetSettings is like [Reader.GetSettings].
func (s *ReadingSession) GetSettings() *setting.Snapshot {
return s.reader.GetSettings()
}
// ReadSettings is like [Reader.ReadSettings].
func (s *ReadingSession) ReadSettings() (*setting.Snapshot, error) {
return s.reader.ReadSettings()
}
// PolicyChanged returns a channel that's written to when
// there's a policy change, closed when the session is terminated.
func (s *ReadingSession) PolicyChanged() <-chan struct{} {
return s.policyChangedCh
}
// Close unregisters this session with the [Reader].
func (s *ReadingSession) Close() {
s.reader.mu.Lock()
delete(s.reader.sessions, s.handle)
s.closeInternal()
s.reader.mu.Unlock()
}
// onPolicyChange handles a policy change notification from the [Store],
// invalidating the current [setting.Snapshot] in r,
// and notifying the active [ReadingSession]s.
func (r *Reader) onPolicyChange() {
r.mu.Lock()
defer r.mu.Unlock()
r.upToDate = false
for _, s := range r.sessions {
select {
case s.policyChangedCh <- struct{}{}:
// Notified.
default:
// 1-buffered channel is full, meaning that another policy change
// notification is already en route.
}
}
}
// Close closes the store reader and the underlying store.
func (r *Reader) Close() error {
r.mu.Lock()
if r.closing {
r.mu.Unlock()
return nil
}
r.closing = true
r.mu.Unlock()
if r.unregisterChangeNotifier != nil {
r.unregisterChangeNotifier()
r.unregisterChangeNotifier = nil
}
if closer, ok := r.store.(io.Closer); ok {
if err := closer.Close(); err != nil {
return err
}
}
r.store = nil
close(r.doneCh)
r.mu.Lock()
defer r.mu.Unlock()
for _, c := range r.sessions {
c.closeInternal()
}
r.sessions = nil
return nil
}
// Done returns a channel that is closed when the reader is closed.
func (r *Reader) Done() <-chan struct{} {
return r.doneCh
}
// ReadableSource is a [Source] open for reading.
type ReadableSource struct {
*Source
*ReadingSession
}
// Close closes the underlying [ReadingSession].
func (s ReadableSource) Close() {
s.ReadingSession.Close()
}
// ReadableSources is a slice of [ReadableSource].
type ReadableSources []ReadableSource
// Contains reports whether s contains the specified source.
func (s ReadableSources) Contains(source *Source) bool {
return s.IndexOf(source) != -1
}
// IndexOf returns position of the specified source in s, or -1
// if the source does not exist.
func (s ReadableSources) IndexOf(source *Source) int {
return slices.IndexFunc(s, func(rs ReadableSource) bool {
return rs.Source == source
})
}
// InsertionIndexOf returns the position at which source can be inserted
// to maintain the sorted order of the readableSources.
// The return value is unspecified if s is not sorted on entry to InsertionIndexOf.
func (s ReadableSources) InsertionIndexOf(source *Source) int {
// Insert new sources after any existing sources with the same precedence,
// and just before the first source with higher precedence.
// Just like stable sort, but for insertion.
// It's okay to use linear search as insertions are rare
// and we never have more than just a few policy sources.
higherPrecedence := func(rs ReadableSource) bool { return rs.Compare(source) > 0 }
if i := slices.IndexFunc(s, higherPrecedence); i != -1 {
return i
}
return len(s)
}
// StableSort sorts [ReadableSource] in s by precedence, so that policy
// settings from sources with higher precedence (e.g., [DeviceScope])
// will be read and merged last, overriding any policy settings with
// the same keys configured in sources with lower precedence
// (e.g., [CurrentUserScope]).
func (s *ReadableSources) StableSort() {
sort.SliceStable(*s, func(i, j int) bool {
return (*s)[i].Source.Compare((*s)[j].Source) < 0
})
}
// DeleteAt closes and deletes the i-th source from s.
func (s *ReadableSources) DeleteAt(i int) {
(*s)[i].Close()
*s = slices.Delete(*s, i, i+1)
}
// Close closes and deletes all sources in s.
func (s *ReadableSources) Close() {
for _, s := range *s {
s.Close()
}
*s = nil
}
func readPolicySettingValue(store Store, s *setting.Definition) (value any, err error) {
switch key := s.Key(); s.Type() {
case setting.BooleanValue:
return store.ReadBoolean(key)
case setting.IntegerValue:
return store.ReadUInt64(key)
case setting.StringValue:
return store.ReadString(key)
case setting.StringListValue:
return store.ReadStringArray(key)
case setting.PreferenceOptionValue:
s, err := store.ReadString(key)
if err == nil {
var value setting.PreferenceOption
if err = value.UnmarshalText([]byte(s)); err == nil {
return value, nil
}
}
return setting.ShowChoiceByPolicy, err
case setting.VisibilityValue:
s, err := store.ReadString(key)
if err == nil {
var value setting.Visibility
if err = value.UnmarshalText([]byte(s)); err == nil {
return value, nil
}
}
return setting.VisibleByPolicy, err
case setting.DurationValue:
s, err := store.ReadString(key)
if err == nil {
var value time.Duration
if value, err = time.ParseDuration(s); err == nil {
return value, nil
}
}
return nil, err
default:
return nil, fmt.Errorf("%w: unsupported setting type: %v", setting.ErrTypeMismatch, s.Type())
}
}

View File

@@ -7,12 +7,8 @@
package source
import (
"cmp"
"errors"
"fmt"
"io"
"tailscale.com/types/lazy"
"tailscale.com/util/syspolicy/setting"
)
@@ -79,68 +75,3 @@ type Expirable interface {
// It should return nil if the store never expires.
Done() <-chan struct{}
}
// Source represents a named source of policy settings for a given [setting.PolicyScope].
type Source struct {
name string
scope setting.PolicyScope
store Store
origin *setting.Origin
lazyReader lazy.SyncValue[*Reader]
}
// NewSource returns a new [Source] with the specified name, scope, and store.
func NewSource(name string, scope setting.PolicyScope, store Store) *Source {
return &Source{name: name, scope: scope, store: store, origin: setting.NewNamedOrigin(name, scope)}
}
// Name reports the name of the policy source.
func (s *Source) Name() string {
return s.name
}
// Scope reports the management scope of the policy source.
func (s *Source) Scope() setting.PolicyScope {
return s.scope
}
// Reader returns a [Reader] that reads from this source's [Store].
func (s *Source) Reader() (*Reader, error) {
return s.lazyReader.GetErr(func() (*Reader, error) {
return newReader(s.store, s.origin)
})
}
// Description returns a formatted string with the scope and name of this policy source.
// It can be used for logging or display purposes.
func (s *Source) Description() string {
if s.name != "" {
return fmt.Sprintf("%s (%v)", s.name, s.Scope())
}
return s.Scope().String()
}
// Compare returns an integer comparing s and s2
// by their precedence, following the "last-wins" model.
// The result will be:
//
// -1 if policy settings from s should be processed before policy settings from s2;
// +1 if policy settings from s should be processed after policy settings from s2, overriding s2;
// 0 if the relative processing order of policy settings in s and s2 is unspecified.
func (s *Source) Compare(s2 *Source) int {
return cmp.Compare(s2.Scope().Kind(), s.Scope().Kind())
}
// Close closes the [Source] and the underlying [Store].
func (s *Source) Close() error {
// The [Reader], if any, owns the [Store].
if reader, _ := s.lazyReader.GetErr(func() (*Reader, error) { return nil, ErrStoreClosed }); reader != nil {
return reader.Close()
}
// Otherwise, it is our responsibility to close it.
if closer, ok := s.store.(io.Closer); ok {
return closer.Close()
}
return nil
}

View File

@@ -1,449 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package source
import (
"fmt"
"sync"
"sync/atomic"
xmaps "golang.org/x/exp/maps"
"tailscale.com/util/mak"
"tailscale.com/util/set"
"tailscale.com/util/slicesx"
"tailscale.com/util/syspolicy/internal"
"tailscale.com/util/syspolicy/setting"
)
var (
_ Store = (*TestStore)(nil)
_ Lockable = (*TestStore)(nil)
_ Changeable = (*TestStore)(nil)
_ Expirable = (*TestStore)(nil)
)
// TestValueType is a constraint that allows types supported by [TestStore].
type TestValueType interface {
bool | uint64 | string | []string
}
// TestSetting is a policy setting in a [TestStore].
type TestSetting[T TestValueType] struct {
// Key is the setting's unique identifier.
Key setting.Key
// Error is the error to be returned by the [TestStore] when reading
// a policy setting with the specified key.
Error error
// Value is the value to be returned by the [TestStore] when reading
// a policy setting with the specified key.
// It is only used if the Error is nil.
Value T
}
// TestSettingOf returns a [TestSetting] representing a policy setting
// configured with the specified key and value.
func TestSettingOf[T TestValueType](key setting.Key, value T) TestSetting[T] {
return TestSetting[T]{Key: key, Value: value}
}
// TestSettingWithError returns a [TestSetting] representing a policy setting
// with the specified key and error.
func TestSettingWithError[T TestValueType](key setting.Key, err error) TestSetting[T] {
return TestSetting[T]{Key: key, Error: err}
}
// testReadOperation describes a single policy setting read operation.
type testReadOperation struct {
// Key is the setting's unique identifier.
Key setting.Key
// Type is a value type of a read operation.
// [setting.BooleanValue], [setting.IntegerValue], [setting.StringValue] or [setting.StringListValue]
Type setting.Type
}
// TestExpectedReads is the number of read operations with the specified details.
type TestExpectedReads struct {
// Key is the setting's unique identifier.
Key setting.Key
// Type is a value type of a read operation.
// [setting.BooleanValue], [setting.IntegerValue], [setting.StringValue] or [setting.StringListValue]
Type setting.Type
// NumTimes is how many times a setting with the specified key and type should have been read.
NumTimes int
}
func (r TestExpectedReads) operation() testReadOperation {
return testReadOperation{r.Key, r.Type}
}
// TestStore is a [Store] that can be used in tests.
type TestStore struct {
tb internal.TB
done chan struct{}
storeLock sync.RWMutex // its RLock is exposed via [Store.Lock]/[Store.Unlock].
storeLockCount atomic.Int32
mu sync.RWMutex
suspendCount int // change callback are suspended if > 0
mr, mw map[setting.Key]any // maps for reading and writing; they're the same unless the store is suspended.
cbs set.HandleSet[func()]
closed bool
readsMu sync.Mutex
reads map[testReadOperation]int // how many times a policy setting was read
}
// NewTestStore returns a new [TestStore].
// The tb will be used to report coding errors detected by the [TestStore].
func NewTestStore(tb internal.TB) *TestStore {
m := make(map[setting.Key]any)
store := &TestStore{
tb: tb,
done: make(chan struct{}),
mr: m,
mw: m,
}
tb.Cleanup(store.Close)
return store
}
// NewTestStoreOf is a shorthand for [NewTestStore] followed by [TestStore.SetBooleans],
// [TestStore.SetUInt64s], [TestStore.SetStrings] or [TestStore.SetStringLists].
func NewTestStoreOf[T TestValueType](tb internal.TB, settings ...TestSetting[T]) *TestStore {
store := NewTestStore(tb)
switch settings := any(settings).(type) {
case []TestSetting[bool]:
store.SetBooleans(settings...)
case []TestSetting[uint64]:
store.SetUInt64s(settings...)
case []TestSetting[string]:
store.SetStrings(settings...)
case []TestSetting[[]string]:
store.SetStringLists(settings...)
}
return store
}
// Lock implements [Lockable].
func (s *TestStore) Lock() error {
s.storeLock.RLock()
s.storeLockCount.Add(1)
return nil
}
// Unlock implements [Lockable].
func (s *TestStore) Unlock() {
if s.storeLockCount.Add(-1) < 0 {
s.tb.Fatal("negative storeLockCount")
}
s.storeLock.RUnlock()
}
// RegisterChangeCallback implements [Changeable].
func (s *TestStore) RegisterChangeCallback(callback func()) (unregister func(), err error) {
s.mu.Lock()
defer s.mu.Unlock()
handle := s.cbs.Add(callback)
return func() {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.cbs, handle)
}, nil
}
// ReadString implements [Store].
func (s *TestStore) ReadString(key setting.Key) (string, error) {
defer s.recordRead(key, setting.StringValue)
s.mu.RLock()
defer s.mu.RUnlock()
v, ok := s.mr[key]
if !ok {
return "", setting.ErrNotConfigured
}
if err, ok := v.(error); ok {
return "", err
}
str, ok := v.(string)
if !ok {
return "", fmt.Errorf("%w in ReadString: got %T", setting.ErrTypeMismatch, v)
}
return str, nil
}
// ReadUInt64 implements [Store].
func (s *TestStore) ReadUInt64(key setting.Key) (uint64, error) {
defer s.recordRead(key, setting.IntegerValue)
s.mu.RLock()
defer s.mu.RUnlock()
v, ok := s.mr[key]
if !ok {
return 0, setting.ErrNotConfigured
}
if err, ok := v.(error); ok {
return 0, err
}
u64, ok := v.(uint64)
if !ok {
return 0, fmt.Errorf("%w in ReadUInt64: got %T", setting.ErrTypeMismatch, v)
}
return u64, nil
}
// ReadBoolean implements [Store].
func (s *TestStore) ReadBoolean(key setting.Key) (bool, error) {
defer s.recordRead(key, setting.BooleanValue)
s.mu.RLock()
defer s.mu.RUnlock()
v, ok := s.mr[key]
if !ok {
return false, setting.ErrNotConfigured
}
if err, ok := v.(error); ok {
return false, err
}
b, ok := v.(bool)
if !ok {
return false, fmt.Errorf("%w in ReadBoolean: got %T", setting.ErrTypeMismatch, v)
}
return b, nil
}
// ReadStringArray implements [Store].
func (s *TestStore) ReadStringArray(key setting.Key) ([]string, error) {
defer s.recordRead(key, setting.StringListValue)
s.mu.RLock()
defer s.mu.RUnlock()
v, ok := s.mr[key]
if !ok {
return nil, setting.ErrNotConfigured
}
if err, ok := v.(error); ok {
return nil, err
}
slice, ok := v.([]string)
if !ok {
return nil, fmt.Errorf("%w in ReadStringArray: got %T", setting.ErrTypeMismatch, v)
}
return slice, nil
}
func (s *TestStore) recordRead(key setting.Key, typ setting.Type) {
s.readsMu.Lock()
op := testReadOperation{key, typ}
num := s.reads[op]
num++
mak.Set(&s.reads, op, num)
s.readsMu.Unlock()
}
func (s *TestStore) ResetCounters() {
s.readsMu.Lock()
clear(s.reads)
s.readsMu.Unlock()
}
// ReadsMustEqual fails the test if the actual reads differs from the specified reads.
func (s *TestStore) ReadsMustEqual(reads ...TestExpectedReads) {
s.tb.Helper()
s.readsMu.Lock()
defer s.readsMu.Unlock()
s.readsMustContainLocked(reads...)
s.readMustNoExtraLocked(reads...)
}
// ReadsMustContain fails the test if the specified reads have not been made,
// or have been made a different number of times. It permits other values to be
// read in addition to the ones being tested.
func (s *TestStore) ReadsMustContain(reads ...TestExpectedReads) {
s.tb.Helper()
s.readsMu.Lock()
defer s.readsMu.Unlock()
s.readsMustContainLocked(reads...)
}
func (s *TestStore) readsMustContainLocked(reads ...TestExpectedReads) {
s.tb.Helper()
for _, r := range reads {
if numTimes := s.reads[r.operation()]; numTimes != r.NumTimes {
s.tb.Errorf("%q (%v) reads: got %v, want %v", r.Key, r.Type, numTimes, r.NumTimes)
}
}
}
func (s *TestStore) readMustNoExtraLocked(reads ...TestExpectedReads) {
s.tb.Helper()
rs := make(set.Set[testReadOperation])
for i := range reads {
rs.Add(reads[i].operation())
}
for ro, num := range s.reads {
if !rs.Contains(ro) {
s.tb.Errorf("%q (%v) reads: got %v, want 0", ro.Key, ro.Type, num)
}
}
}
// Suspend suspends the store, batching changes and notifications
// until [TestStore.Resume] is called the same number of times as Suspend.
func (s *TestStore) Suspend() {
s.mu.Lock()
defer s.mu.Unlock()
if s.suspendCount++; s.suspendCount == 1 {
s.mw = xmaps.Clone(s.mr)
}
}
// Resume resumes the store, applying the changes and invoking
// the change callbacks.
func (s *TestStore) Resume() {
s.storeLock.Lock()
s.mu.Lock()
switch s.suspendCount--; {
case s.suspendCount == 0:
s.mr = s.mw
s.mu.Unlock()
s.storeLock.Unlock()
s.NotifyPolicyChanged()
case s.suspendCount < 0:
s.tb.Fatal("negative suspendCount")
default:
s.mu.Unlock()
s.storeLock.Unlock()
}
}
// SetBooleans sets the specified boolean settings in s.
func (s *TestStore) SetBooleans(settings ...TestSetting[bool]) {
s.storeLock.Lock()
for _, setting := range settings {
if setting.Key == "" {
s.tb.Fatal("empty keys disallowed")
}
s.mu.Lock()
if setting.Error != nil {
mak.Set(&s.mw, setting.Key, any(setting.Error))
} else {
mak.Set(&s.mw, setting.Key, any(setting.Value))
}
s.mu.Unlock()
}
s.storeLock.Unlock()
s.NotifyPolicyChanged()
}
// SetUInt64s sets the specified integer settings in s.
func (s *TestStore) SetUInt64s(settings ...TestSetting[uint64]) {
s.storeLock.Lock()
for _, setting := range settings {
if setting.Key == "" {
s.tb.Fatal("empty keys disallowed")
}
s.mu.Lock()
if setting.Error != nil {
mak.Set(&s.mw, setting.Key, any(setting.Error))
} else {
mak.Set(&s.mw, setting.Key, any(setting.Value))
}
s.mu.Unlock()
}
s.storeLock.Unlock()
s.NotifyPolicyChanged()
}
// SetStrings sets the specified string settings in s.
func (s *TestStore) SetStrings(settings ...TestSetting[string]) {
s.storeLock.Lock()
for _, setting := range settings {
if setting.Key == "" {
s.tb.Fatal("empty keys disallowed")
}
s.mu.Lock()
if setting.Error != nil {
mak.Set(&s.mw, setting.Key, any(setting.Error))
} else {
mak.Set(&s.mw, setting.Key, any(setting.Value))
}
s.mu.Unlock()
}
s.storeLock.Unlock()
s.NotifyPolicyChanged()
}
// SetStrings sets the specified string list settings in s.
func (s *TestStore) SetStringLists(settings ...TestSetting[[]string]) {
s.storeLock.Lock()
for _, setting := range settings {
if setting.Key == "" {
s.tb.Fatal("empty keys disallowed")
}
s.mu.Lock()
if setting.Error != nil {
mak.Set(&s.mw, setting.Key, any(setting.Error))
} else {
mak.Set(&s.mw, setting.Key, any(setting.Value))
}
s.mu.Unlock()
}
s.storeLock.Unlock()
s.NotifyPolicyChanged()
}
// Delete deletes the specified settings from s.
func (s *TestStore) Delete(keys ...setting.Key) {
s.storeLock.Lock()
for _, key := range keys {
s.mu.Lock()
delete(s.mw, key)
s.mu.Unlock()
}
s.storeLock.Unlock()
s.NotifyPolicyChanged()
}
// Clear deletes all settings from s.
func (s *TestStore) Clear() {
s.storeLock.Lock()
s.mu.Lock()
clear(s.mw)
s.mu.Unlock()
s.storeLock.Unlock()
s.NotifyPolicyChanged()
}
func (s *TestStore) NotifyPolicyChanged() {
s.mu.RLock()
if s.suspendCount != 0 {
s.mu.RUnlock()
return
}
cbs := slicesx.MapValues(s.cbs)
s.mu.RUnlock()
var wg sync.WaitGroup
wg.Add(len(cbs))
for _, cb := range cbs {
go func() {
defer wg.Done()
cb()
}()
}
wg.Wait()
}
// Close closes s, notifying its users that it has expired.
func (s *TestStore) Close() {
s.mu.Lock()
defer s.mu.Unlock()
if !s.closed {
close(s.done)
s.closed = true
}
}
// Done implements [Expirable].
func (s *TestStore) Done() <-chan struct{} {
return s.done
}

View File

@@ -11,15 +11,9 @@
package syspolicy
import (
"errors"
"fmt"
"reflect"
"time"
"tailscale.com/util/syspolicy/internal/loggerx"
"tailscale.com/util/syspolicy/rsop"
"tailscale.com/util/syspolicy/setting"
"tailscale.com/util/syspolicy/source"
)
var (
@@ -38,55 +32,28 @@ var (
ErrNoSuchKey = setting.ErrNoSuchKey
)
// RegisterStore registers a new policy [source.Store] with the specified name and [setting.PolicyScope].
//
// It is a shorthand for [rsop.RegisterStore].
func RegisterStore(name string, scope setting.PolicyScope, store source.Store) (*rsop.StoreRegistration, error) {
return rsop.RegisterStore(name, scope, store)
}
// MustRegisterStoreForTest is like [rsop.RegisterStoreForTest], but it fails the test if the store could not be registered.
func MustRegisterStoreForTest(tb TB, name string, scope setting.PolicyScope, store source.Store) *rsop.StoreRegistration {
tb.Helper()
reg, err := rsop.RegisterStoreForTest(tb, name, scope, store)
if err != nil {
tb.Fatalf("Failed to register policy store %q as a %v policy source: %v", name, scope, err)
}
return reg
}
// GetString returns a string policy setting with the specified key,
// or defaultValue if it does not exist.
func GetString(key Key, defaultValue string) (string, error) {
return getCurrentPolicySettingValue(key, defaultValue)
return defaultValue, nil
}
// GetUint64 returns a numeric policy setting with the specified key,
// or defaultValue if it does not exist.
func GetUint64(key Key, defaultValue uint64) (uint64, error) {
return getCurrentPolicySettingValue(key, defaultValue)
return defaultValue, nil
}
// GetBoolean returns a boolean policy setting with the specified key,
// or defaultValue if it does not exist.
func GetBoolean(key Key, defaultValue bool) (bool, error) {
return getCurrentPolicySettingValue(key, defaultValue)
return defaultValue, nil
}
// GetStringArray returns a multi-string policy setting with the specified key,
// or defaultValue if it does not exist.
func GetStringArray(key Key, defaultValue []string) ([]string, error) {
return getCurrentPolicySettingValue(key, defaultValue)
}
// GetPreferenceOption loads a policy from the registry that can be
// managed by an enterprise policy management system and allows administrative
// overrides of users' choices in a way that we do not want tailcontrol to have
// the authority to set. It describes user-decides/always/never options, where
// "always" and "never" remove the user's ability to make a selection. If not
// present or set to a different value, "user-decides" is the default.
func GetPreferenceOption(name Key) (setting.PreferenceOption, error) {
return getCurrentPolicySettingValue(name, setting.ShowChoiceByPolicy)
return defaultValue, nil
}
// GetVisibility loads a policy from the registry that can be managed
@@ -95,7 +62,7 @@ func GetPreferenceOption(name Key) (setting.PreferenceOption, error) {
// true) or "hide" (return true). If not present or set to a different value,
// "show" (return false) is the default.
func GetVisibility(name Key) (setting.Visibility, error) {
return getCurrentPolicySettingValue(name, setting.VisibleByPolicy)
return setting.VisibleByPolicy, nil
}
// GetDuration loads a policy from the registry that can be managed
@@ -104,58 +71,7 @@ func GetVisibility(name Key) (setting.Visibility, error) {
// understands. If the registry value is "" or can not be processed,
// defaultValue is returned instead.
func GetDuration(name Key, defaultValue time.Duration) (time.Duration, error) {
d, err := getCurrentPolicySettingValue(name, defaultValue)
if err != nil {
return d, err
}
if d < 0 {
return defaultValue, nil
}
return d, nil
}
// RegisterChangeCallback adds a function that will be called whenever the effective policy
// for the default scope changes. The returned function can be used to unregister the callback.
func RegisterChangeCallback(cb rsop.PolicyChangeCallback) (unregister func(), err error) {
effective, err := rsop.PolicyFor(setting.DefaultScope())
if err != nil {
return nil, err
}
return effective.RegisterChangeCallback(cb), nil
}
// getCurrentPolicySettingValue returns the value of the policy setting
// specified by its key from the [rsop.Policy] of the [setting.DefaultScope]. It
// returns def if the policy setting is not configured, or an error if it has
// an error or could not be converted to the specified type T.
func getCurrentPolicySettingValue[T setting.ValueType](key Key, def T) (T, error) {
effective, err := rsop.PolicyFor(setting.DefaultScope())
if err != nil {
return def, err
}
value, err := effective.Get().GetErr(key)
if err != nil {
if errors.Is(err, setting.ErrNotConfigured) || errors.Is(err, setting.ErrNoSuchKey) {
return def, nil
}
return def, err
}
if res, ok := value.(T); ok {
return res, nil
}
return convertPolicySettingValueTo(value, def)
}
func convertPolicySettingValueTo[T setting.ValueType](value any, def T) (T, error) {
// Convert [PreferenceOption], [Visibility], or [time.Duration] back to a string
// if someone requests a string instead of the actual setting's value.
// TODO(nickkhyl): check if this behavior is relied upon anywhere besides the old tests.
if reflect.TypeFor[T]().Kind() == reflect.String {
if str, ok := value.(fmt.Stringer); ok {
return any(str.String()).(T), nil
}
}
return def, fmt.Errorf("%w: got %T, want %T", setting.ErrTypeMismatch, value, def)
return defaultValue, nil
}
// SelectControlURL returns the ControlURL to use based on a value in
@@ -198,8 +114,3 @@ func SelectControlURL(reg, disk string) string {
}
return def
}
// SetDebugLoggingEnabled controls whether spammy debug logging is enabled.
func SetDebugLoggingEnabled(v bool) {
loggerx.SetDebugLoggingEnabled(v)
}