Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355// Copyright (c) Tailscale Inc & contributors// SPDX-License-Identifier: BSD-3-Clause
// Package setting contains types for defining and representing policy settings.// It facilitates the registration of setting definitions using [Register] and [RegisterDefinition],// and the retrieval of registered setting definitions via [Definitions] and [DefinitionOf].// This package is intended for use primarily within the syspolicy package hierarchy.package setting
import ( "fmt" "slices" "strings" "time"
"tailscale.com/syncs" "tailscale.com/types/lazy" "tailscale.com/util/syspolicy/internal" "tailscale.com/util/syspolicy/pkey" "tailscale.com/util/syspolicy/ptype" "tailscale.com/util/testenv")
// Scope indicates the broadest scope at which a policy setting may apply,// and the narrowest scope at which it may be configured.type Scope int8
const ( // DeviceSetting indicates a policy setting that applies to a device, regardless of // which OS user or Tailscale profile is currently active, if any. // It can only be configured at a [DeviceScope]. DeviceSetting Scope = iota // ProfileSetting indicates a policy setting that applies to a Tailscale profile. // It can only be configured for a specific profile or at a [DeviceScope], // in which case it applies to all profiles on the device. ProfileSetting // UserSetting indicates a policy setting that applies to users. // It can be configured for a user, profile, or the entire device. UserSetting
// NumScopes is the number of possible [Scope] values. NumScopes int = iota // must be the last value in the const block.)
// String implements [fmt.Stringer].func (s Scope) String() string { switch s { case DeviceSetting: return "Device" case ProfileSetting: return "Profile" case UserSetting: return "User" default: panic("unreachable") }}
// MarshalText implements [encoding.TextMarshaler].func (s Scope) MarshalText() (text []byte, err error) { return []byte(s.String()), nil}
// UnmarshalText implements [encoding.TextUnmarshaler].func (s *Scope) UnmarshalText(text []byte) error { switch strings.ToLower(string(text)) { case "device": *s = DeviceSetting case "profile": *s = ProfileSetting case "user": *s = UserSetting default: return fmt.Errorf("%q is not a valid scope", string(text)) } return nil}
// Type is a policy setting value type.// Except for [InvalidValue], which represents an invalid policy setting type,// and [PreferenceOptionValue], [VisibilityValue], and [DurationValue],// which have special handling due to their legacy status in the package,// SettingTypes represent the raw value types readable from policy stores.type Type int
const ( // InvalidValue indicates an invalid policy setting value type. InvalidValue Type = iota // BooleanValue indicates a policy setting whose underlying type is a bool. BooleanValue // IntegerValue indicates a policy setting whose underlying type is a uint64. IntegerValue // StringValue indicates a policy setting whose underlying type is a string. StringValue // StringListValue indicates a policy setting whose underlying type is a []string. StringListValue // PreferenceOptionValue indicates a three-state policy setting whose // underlying type is a string, but the actual value is a [PreferenceOption]. PreferenceOptionValue // VisibilityValue indicates a two-state boolean-like policy setting whose // underlying type is a string, but the actual value is a [Visibility]. VisibilityValue // DurationValue indicates an interval/period/duration policy setting whose // underlying type is a string, but the actual value is a [time.Duration]. DurationValue)
// String returns a string representation of t.func (t Type) String() string { switch t { case InvalidValue: return "Invalid" case BooleanValue: return "Boolean" case IntegerValue: return "Integer" case StringValue: return "String" case StringListValue: return "StringList" case PreferenceOptionValue: return "PreferenceOption" case VisibilityValue: return "Visibility" case DurationValue: return "Duration" default: panic("unreachable") }}
// ValueType is a constraint that allows Go types corresponding to [Type].type ValueType interface { bool | uint64 | string | []string | ptype.Visibility | ptype.PreferenceOption | time.Duration}
// Definition defines policy key, scope and value type.type Definition struct { key pkey.Key scope Scope typ Type platforms PlatformList}
// NewDefinition returns a new [Definition] with the specified// key, scope, type and supported platforms (see [PlatformList]).func NewDefinition(k pkey.Key, s Scope, t Type, platforms ...string) *Definition { return &Definition{key: k, scope: s, typ: t, platforms: platforms}}
// Key returns a policy setting's identifier.func (d *Definition) Key() pkey.Key { if d == nil { return "" } return d.key}
// Scope reports the broadest [Scope] the policy setting may apply to.func (d *Definition) Scope() Scope { if d == nil { return 0 } return d.scope}
// Type reports the underlying value type of the policy setting.func (d *Definition) Type() Type { if d == nil { return InvalidValue } 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 { if d == nil { return nil } return d.platforms}
// String implements [fmt.Stringer].func (d *Definition) String() string { if d == nil { return "(nil)" } return fmt.Sprintf("%v(%q, %v)", d.scope, d.key, d.typ)}
// Equal reports whether d and d2 have the same key, type and scope.// It does not check whether both s and s2 are supported on the same platforms.func (d *Definition) Equal(d2 *Definition) bool { if d == d2 { return true } if d == nil || d2 == nil { return false } return d.key == d2.key && d.typ == d2.typ && d.scope == d2.scope}
// DefinitionMap is a map of setting [Definition] by [Key].type DefinitionMap map[pkey.Key]*Definition
var ( definitions lazy.SyncValue[DefinitionMap]
definitionsMu syncs.Mutex definitionsList []*Definition 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 pkey.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 testenv.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 pkey.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
// Has reports whether l contains the target platform.func (ls PlatformList) Has(target string) bool { if len(ls) == 0 { return true } return slices.ContainsFunc(ls, func(os string) bool { return strings.EqualFold(os, target) })}
// HasCurrent is like Has, but for the current platform.func (ls PlatformList) HasCurrent() bool { return ls.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 (ls *PlatformList) mergeFrom(l2 PlatformList) { switch { case len(*ls) == 0: // No-op. An empty list indicates no platform restrictions. case len(l2) == 0: // Merging with an empty list results in an empty list. *ls = l2 default: // Append, sort and dedup. *ls = append(*ls, l2...) slices.Sort(*ls) *ls = slices.Compact(*ls) }}