| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // 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)
- }
- }
|