This commit is contained in:
2026-02-19 10:07:43 +00:00
parent 007438e372
commit 6e637ecf77
1763 changed files with 60820 additions and 279516 deletions

View File

@@ -1,13 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package setting
// Key is a string that uniquely identifies a policy and must remain unchanged
// once established and documented for a given policy setting. It may contain
// alphanumeric characters and zero or more [KeyPathSeparator]s to group
// individual policy settings into categories.
type Key string
// KeyPathSeparator allows logical grouping of policy settings into categories.
const KeyPathSeparator = '/'

View File

@@ -11,6 +11,7 @@ import (
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/opt"
"tailscale.com/types/structs"
"tailscale.com/util/syspolicy/pkey"
)
// RawItem contains a raw policy setting value as read from a policy store, or an
@@ -169,4 +170,4 @@ func (v *RawValue) UnmarshalJSON(b []byte) error {
}
// RawValues is a map of keyed setting values that can be read from a JSON.
type RawValues map[Key]RawValue
type RawValues map[pkey.Key]RawValue

View File

@@ -11,11 +11,14 @@ import (
"fmt"
"slices"
"strings"
"sync"
"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,
@@ -128,12 +131,12 @@ func (t Type) String() string {
// ValueType is a constraint that allows Go types corresponding to [Type].
type ValueType interface {
bool | uint64 | string | []string | Visibility | PreferenceOption | time.Duration
bool | uint64 | string | []string | ptype.Visibility | ptype.PreferenceOption | time.Duration
}
// Definition defines policy key, scope and value type.
type Definition struct {
key Key
key pkey.Key
scope Scope
typ Type
platforms PlatformList
@@ -141,12 +144,12 @@ type Definition struct {
// 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 {
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() Key {
func (d *Definition) Key() pkey.Key {
if d == nil {
return ""
}
@@ -207,12 +210,12 @@ func (d *Definition) Equal(d2 *Definition) bool {
}
// DefinitionMap is a map of setting [Definition] by [Key].
type DefinitionMap map[Key]*Definition
type DefinitionMap map[pkey.Key]*Definition
var (
definitions lazy.SyncValue[DefinitionMap]
definitionsMu sync.Mutex
definitionsMu syncs.Mutex
definitionsList []*Definition
definitionsUsed bool
)
@@ -223,7 +226,7 @@ var (
// 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) {
func Register(k pkey.Key, s Scope, t Type, platforms ...string) {
RegisterDefinition(NewDefinition(k, s, t, platforms...))
}
@@ -277,7 +280,7 @@ func DefinitionMapOf(settings []*Definition) (DefinitionMap, error) {
// 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 {
func SetDefinitionsForTest(tb testenv.TB, ds ...*Definition) error {
m, err := DefinitionMapOf(ds)
if err != nil {
return err
@@ -289,7 +292,7 @@ func SetDefinitionsForTest(tb lazy.TB, ds ...*Definition) error {
// 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) {
func DefinitionOf(k pkey.Key) (*Definition, error) {
ds, err := settingDefinitions()
if err != nil {
return nil, err
@@ -319,33 +322,33 @@ func Definitions() ([]*Definition, error) {
type PlatformList []string
// Has reports whether l contains the target platform.
func (l PlatformList) Has(target string) bool {
if len(l) == 0 {
func (ls PlatformList) Has(target string) bool {
if len(ls) == 0 {
return true
}
return slices.ContainsFunc(l, func(os string) bool {
return slices.ContainsFunc(ls, 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())
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 (l *PlatformList) mergeFrom(l2 PlatformList) {
func (ls *PlatformList) mergeFrom(l2 PlatformList) {
switch {
case len(*l) == 0:
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.
*l = l2
*ls = l2
default:
// Append, sort and dedup.
*l = append(*l, l2...)
slices.Sort(*l)
*l = slices.Compact(*l)
*ls = append(*ls, l2...)
slices.Sort(*ls)
*ls = slices.Compact(*ls)
}
}

View File

@@ -9,39 +9,41 @@ import (
"maps"
"slices"
"strings"
"time"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
xmaps "golang.org/x/exp/maps"
"tailscale.com/util/deephash"
"tailscale.com/util/syspolicy/pkey"
)
// 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
m map[pkey.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 {
func NewSnapshot(items map[pkey.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] {
func (s *Snapshot) All() iter.Seq2[pkey.Key, RawItem] {
if s == nil {
return func(yield func(Key, RawItem) bool) {}
return func(yield func(pkey.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 {
func (s *Snapshot) Get(k pkey.Key) any {
v, _ := s.GetErr(k)
return v
}
@@ -49,7 +51,7 @@ func (s *Snapshot) Get(k Key) any {
// 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) {
func (s *Snapshot) GetErr(k pkey.Key) (any, error) {
if s != nil {
if s, ok := s.m[k]; ok {
return s.Value(), s.Error()
@@ -61,7 +63,7 @@ func (s *Snapshot) GetErr(k Key) (any, error) {
// 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) {
func (s *Snapshot) GetSetting(k pkey.Key) (setting RawItem, ok bool) {
setting, ok = s.m[k]
return setting, ok
}
@@ -93,9 +95,9 @@ func (s *Snapshot) EqualItems(s2 *Snapshot) bool {
// 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] {
func (s *Snapshot) Keys() iter.Seq[pkey.Key] {
if s.m == nil {
return func(yield func(Key) bool) {}
return func(yield func(pkey.Key) bool) {}
}
return maps.Keys(s.m)
}
@@ -143,8 +145,8 @@ func (s *Snapshot) String() string {
// snapshotJSON holds JSON-marshallable data for [Snapshot].
type snapshotJSON struct {
Summary Summary `json:",omitzero"`
Settings map[Key]RawItem `json:",omitempty"`
Summary Summary `json:",omitzero"`
Settings map[pkey.Key]RawItem `json:",omitempty"`
}
var (
@@ -152,6 +154,24 @@ var (
_ jsonv2.UnmarshalerFrom = (*Snapshot)(nil)
)
// As of 2025-07-28, jsonv2 no longer has a default representation for [time.Duration],
// so we need to provide a custom marshaler.
//
// This is temporary until the decision on the default representation is made
// (see https://github.com/golang/go/issues/71631#issuecomment-2981670799).
//
// In the future, we might either use the default representation (if compatible with
// [time.Duration.String]) or specify something like json.WithFormat[time.Duration]("units")
// when golang/go#71664 is implemented.
//
// TODO(nickkhyl): revisit this when the decision on the default [time.Duration]
// representation is made in golang/go#71631 and/or golang/go#71664 is implemented.
var formatDurationAsUnits = jsonv2.JoinOptions(
jsonv2.WithMarshalers(jsonv2.MarshalToFunc(func(e *jsontext.Encoder, t time.Duration) error {
return e.WriteToken(jsontext.String(t.String()))
})),
)
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (s *Snapshot) MarshalJSONTo(out *jsontext.Encoder) error {
data := &snapshotJSON{}
@@ -159,7 +179,7 @@ func (s *Snapshot) MarshalJSONTo(out *jsontext.Encoder) error {
data.Summary = s.summary
data.Settings = s.m
}
return jsonv2.MarshalEncode(out, data)
return jsonv2.MarshalEncode(out, data, formatDurationAsUnits)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
@@ -213,7 +233,7 @@ func MergeSnapshots(snapshot1, snapshot2 *Snapshot) *Snapshot {
}
return &Snapshot{snapshot2.m, snapshot2.sig, SummaryWith(summaryOpts...)}
}
m := make(map[Key]RawItem, snapshot1.Len()+snapshot2.Len())
m := make(map[pkey.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

@@ -1,136 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package setting
import (
"encoding"
)
// PreferenceOption is a policy that governs whether a boolean variable
// is forcibly assigned an administrator-defined value, or allowed to receive
// a user-defined value.
type PreferenceOption byte
const (
ShowChoiceByPolicy PreferenceOption = iota
NeverByPolicy
AlwaysByPolicy
)
// Show returns if the UI option that controls the choice administered by this
// policy should be shown. Currently this is true if and only if the policy is
// [ShowChoiceByPolicy].
func (p PreferenceOption) Show() bool {
return p == ShowChoiceByPolicy
}
// ShouldEnable checks if the choice administered by this policy should be
// enabled. If the administrator has chosen a setting, the administrator's
// setting is returned, otherwise userChoice is returned.
func (p PreferenceOption) ShouldEnable(userChoice bool) bool {
switch p {
case NeverByPolicy:
return false
case AlwaysByPolicy:
return true
default:
return userChoice
}
}
// IsAlways reports whether the preference should always be enabled.
func (p PreferenceOption) IsAlways() bool {
return p == AlwaysByPolicy
}
// IsNever reports whether the preference should always be disabled.
func (p PreferenceOption) IsNever() bool {
return p == NeverByPolicy
}
// WillOverride checks if the choice administered by the policy is different
// from the user's choice.
func (p PreferenceOption) WillOverride(userChoice bool) bool {
return p.ShouldEnable(userChoice) != userChoice
}
// String returns a string representation of p.
func (p PreferenceOption) String() string {
switch p {
case AlwaysByPolicy:
return "always"
case NeverByPolicy:
return "never"
default:
return "user-decides"
}
}
// MarshalText implements [encoding.TextMarshaler].
func (p *PreferenceOption) MarshalText() (text []byte, err error) {
return []byte(p.String()), nil
}
// UnmarshalText implements [encoding.TextUnmarshaler].
// It never fails and sets p to [ShowChoiceByPolicy] if the specified text
// does not represent a valid [PreferenceOption].
func (p *PreferenceOption) UnmarshalText(text []byte) error {
switch string(text) {
case "always":
*p = AlwaysByPolicy
case "never":
*p = NeverByPolicy
default:
*p = ShowChoiceByPolicy
}
return nil
}
// Visibility is a policy that controls whether or not a particular
// component of a user interface is to be shown.
type Visibility byte
var (
_ encoding.TextMarshaler = (*Visibility)(nil)
_ encoding.TextUnmarshaler = (*Visibility)(nil)
)
const (
VisibleByPolicy Visibility = 'v'
HiddenByPolicy Visibility = 'h'
)
// Show reports whether the UI option administered by this policy should be shown.
// Currently this is true if the policy is not [hiddenByPolicy].
func (v Visibility) Show() bool {
return v != HiddenByPolicy
}
// String returns a string representation of v.
func (v Visibility) String() string {
switch v {
case 'h':
return "hide"
default:
return "show"
}
}
// MarshalText implements [encoding.TextMarshaler].
func (v Visibility) MarshalText() (text []byte, err error) {
return []byte(v.String()), nil
}
// UnmarshalText implements [encoding.TextUnmarshaler].
// It never fails and sets v to [VisibleByPolicy] if the specified text
// does not represent a valid [Visibility].
func (v *Visibility) UnmarshalText(text []byte) error {
switch string(text) {
case "hide":
*v = HiddenByPolicy
default:
*v = VisibleByPolicy
}
return nil
}