mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
349 lines
10 KiB
Go
349 lines
10 KiB
Go
4 months ago
|
// 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"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"tailscale.com/types/lazy"
|
||
|
"tailscale.com/util/syspolicy/internal"
|
||
|
)
|
||
|
|
||
|
// 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 | Visibility | PreferenceOption | time.Duration
|
||
|
}
|
||
|
|
||
|
// Definition defines policy key, scope and value type.
|
||
|
type Definition struct {
|
||
|
key 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 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() 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[Key]*Definition
|
||
|
|
||
|
var (
|
||
|
definitions lazy.SyncValue[DefinitionMap]
|
||
|
|
||
|
definitionsMu sync.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 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) {
|
||
|
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
|
||
|
|
||
|
// Has reports whether l contains the target platform.
|
||
|
func (l PlatformList) Has(target string) bool {
|
||
|
if len(l) == 0 {
|
||
|
return true
|
||
|
}
|
||
|
return slices.ContainsFunc(l, func(os string) bool {
|
||
|
return strings.EqualFold(os, target)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// 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) {
|
||
|
switch {
|
||
|
case len(*l) == 0:
|
||
|
// No-op. An empty list indicates no platform restrictions.
|
||
|
case len(l2) == 0:
|
||
|
// Merging with an empty list results in an empty list.
|
||
|
*l = l2
|
||
|
default:
|
||
|
// Append, sort and dedup.
|
||
|
*l = append(*l, l2...)
|
||
|
slices.Sort(*l)
|
||
|
*l = slices.Compact(*l)
|
||
|
}
|
||
|
}
|