This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package source
+226
View File
@@ -0,0 +1,226 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package source
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"math"
"os"
"sort"
"strconv"
"tailscale.com/feature/buildfeatures"
"tailscale.com/util/syspolicy/pkey"
"tailscale.com/util/syspolicy/setting"
)
// hujsonStandardize is set to hujson.Standardize by json_policy_store_hujson.go
// on platforms that have HuJSON support compiled in. When non-nil, the JSON
// policy file may use HuJSON (comments and trailing commas); when nil, the
// file must be pure standard JSON.
var hujsonStandardize func([]byte) ([]byte, error)
var _ Store = (*JSONPolicyStore)(nil)
// JSONPolicyStore is a [Store] backed by a JSON object that maps policy
// setting keys to values. It is a read-only snapshot; the underlying map
// is captured at construction time and never re-read.
//
// JSON values are mapped to policy setting types as follows:
// - strings map to [setting.StringValue], [setting.PreferenceOptionValue],
// [setting.VisibilityValue], and [setting.DurationValue]. For
// [setting.DurationValue], the string is parsed by [time.ParseDuration]
// elsewhere in the package (e.g. "24h", "5m").
// - booleans map to [setting.BooleanValue].
// - numbers map to [setting.IntegerValue]. Negative or non-integer values
// are rejected with [setting.ErrTypeMismatch].
// - arrays of strings map to [setting.StringListValue].
type JSONPolicyStore struct {
m map[string]any
}
// NewJSONPolicyStore returns a new [JSONPolicyStore] backed by the given map.
// A nil or empty map results in a store that reports every key as
// [setting.ErrNotConfigured].
func NewJSONPolicyStore(m map[string]any) *JSONPolicyStore {
return &JSONPolicyStore{m: m}
}
// NewJSONPolicyStoreFromFile reads the file at path and returns a new
// [JSONPolicyStore] backed by its contents. The file must contain a JSON
// object at its top level. JSON numbers are decoded as [json.Number] to
// preserve precision for [setting.IntegerValue] settings.
func NewJSONPolicyStoreFromFile(path string) (*JSONPolicyStore, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return NewJSONPolicyStoreFromBytes(data)
}
// NewJSONPolicyStoreFromBytes is like [NewJSONPolicyStoreFromFile] but reads
// from data instead of a file. When HuJSON support is linked into the build,
// data may be HuJSON (comments and trailing commas allowed); otherwise it
// must be pure standard JSON.
func NewJSONPolicyStoreFromBytes(data []byte) (*JSONPolicyStore, error) {
if buildfeatures.HasHuJSONConf && hujsonStandardize != nil {
std, err := hujsonStandardize(data)
if err != nil {
return nil, fmt.Errorf("syspolicy: parsing HuJSON/JSON: %w", err)
}
data = std
}
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
var m map[string]any
if err := dec.Decode(&m); err != nil {
return nil, fmt.Errorf("syspolicy: parsing JSON: %w", err)
}
return &JSONPolicyStore{m: m}, nil
}
// ReadString implements [Store].
func (s *JSONPolicyStore) ReadString(key pkey.Key) (string, error) {
v, ok := s.m[string(key)]
if !ok {
return "", setting.ErrNotConfigured
}
str, ok := v.(string)
if !ok {
return "", fmt.Errorf("%w: %q is %T, want string", setting.ErrTypeMismatch, key, v)
}
return str, nil
}
// ReadBoolean implements [Store].
func (s *JSONPolicyStore) ReadBoolean(key pkey.Key) (bool, error) {
v, ok := s.m[string(key)]
if !ok {
return false, setting.ErrNotConfigured
}
b, ok := v.(bool)
if !ok {
return false, fmt.Errorf("%w: %q is %T, want bool", setting.ErrTypeMismatch, key, v)
}
return b, nil
}
// ReadUInt64 implements [Store].
func (s *JSONPolicyStore) ReadUInt64(key pkey.Key) (uint64, error) {
v, ok := s.m[string(key)]
if !ok {
return 0, setting.ErrNotConfigured
}
switch n := v.(type) {
case json.Number:
u, err := strconv.ParseUint(n.String(), 10, 64)
if err != nil {
return 0, fmt.Errorf("%w: %q is not a uint64: %v", setting.ErrTypeMismatch, key, err)
}
return u, nil
case float64:
if n < 0 || n > math.MaxUint64 || n != math.Trunc(n) {
return 0, fmt.Errorf("%w: %q (%v) is not a uint64", setting.ErrTypeMismatch, key, n)
}
return uint64(n), nil
default:
return 0, fmt.Errorf("%w: %q is %T, want number", setting.ErrTypeMismatch, key, v)
}
}
// ReadStringArray implements [Store].
func (s *JSONPolicyStore) ReadStringArray(key pkey.Key) ([]string, error) {
v, ok := s.m[string(key)]
if !ok {
return nil, setting.ErrNotConfigured
}
arr, ok := v.([]any)
if !ok {
return nil, fmt.Errorf("%w: %q is %T, want array", setting.ErrTypeMismatch, key, v)
}
res := make([]string, len(arr))
for i, e := range arr {
str, ok := e.(string)
if !ok {
return nil, fmt.Errorf("%w: %q[%d] is %T, want string", setting.ErrTypeMismatch, key, i, e)
}
res[i] = str
}
return res, nil
}
// Validate checks that every key in the parsed JSON corresponds to a
// registered policy setting (per [setting.Definitions]) and that its value
// can be successfully decoded as the registered setting's type. It joins
// all problems into a single error so callers see every issue at once
// instead of one per startup-then-runtime cycle.
//
// Validate triggers registration of any deferred setting definitions, so
// it should only be called after all init-time registrations have run.
func (s *JSONPolicyStore) Validate() error {
defs, err := setting.Definitions()
if err != nil {
return err
}
byKey := make(map[pkey.Key]*setting.Definition, len(defs))
for _, d := range defs {
byKey[d.Key()] = d
}
keys := make([]string, 0, len(s.m))
for k := range s.m {
keys = append(keys, k)
}
sort.Strings(keys)
var errs []error
for _, k := range keys {
def, ok := byKey[pkey.Key(k)]
if !ok {
errs = append(errs, fmt.Errorf("unknown policy setting %q", k))
continue
}
if err := s.validateValue(def); err != nil {
errs = append(errs, fmt.Errorf("%q: %w", k, err))
}
}
return errors.Join(errs...)
}
// validateValue is like [readPolicySettingValue] but is stricter for the
// enum-like [setting.PreferenceOptionValue] and [setting.VisibilityValue]
// types: their runtime UnmarshalText silently coerces unknown strings to a
// default, which is fine at read time but defeats the point of load-time
// validation, so this checks the raw string against the known values.
func (s *JSONPolicyStore) validateValue(def *setting.Definition) error {
key := def.Key()
switch def.Type() {
case setting.PreferenceOptionValue:
str, err := s.ReadString(key)
if err != nil {
return err
}
switch str {
case "always", "never", "user-decides":
return nil
}
return fmt.Errorf(`%w: %q is not a valid PreferenceOption ("always", "never", or "user-decides")`, setting.ErrTypeMismatch, str)
case setting.VisibilityValue:
str, err := s.ReadString(key)
if err != nil {
return err
}
switch str {
case "show", "hide":
return nil
}
return fmt.Errorf(`%w: %q is not a valid Visibility ("show" or "hide")`, setting.ErrTypeMismatch, str)
default:
_, err := readPolicySettingValue(s, def)
return err
}
}
+19
View File
@@ -0,0 +1,19 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios && !android && !ts_omit_hujsonconf
package source
import "github.com/tailscale/hujson"
// Only link the hujson package on platforms that use it, to reduce binary
// size & memory a bit. iOS and Android don't load syspolicy files from
// disk so they don't need HuJSON either.
//
// While the linker's deadcode mostly handles this today, this keeps us
// honest for the future.
func init() {
hujsonStandardize = hujson.Standardize
}
+10 -4
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package source
@@ -91,7 +91,7 @@ func newReader(store Store, origin *setting.Origin) (*Reader, error) {
}
// GetSettings returns the current [*setting.Snapshot],
// re-reading it from from the underlying [Store] only if the policy
// re-reading it 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 {
@@ -125,6 +125,11 @@ func (r *Reader) ReadSettings() (*setting.Snapshot, error) {
func (r *Reader) reload(force bool) (*setting.Snapshot, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.closing {
// The reader is closing (or already closed) and r.store may be nil.
// Return the last known policy instead of reading from the store.
return r.lastPolicy, nil
}
if r.upToDate && !force {
return r.lastPolicy, nil
}
@@ -267,12 +272,13 @@ func (r *Reader) Close() error {
return err
}
}
r.store = nil
close(r.doneCh)
r.mu.Lock()
defer r.mu.Unlock()
// Nil out the store only while holding r.mu; reload reads r.store
// under the same lock, so writing it unlocked would be a data race.
r.store = nil
for _, c := range r.sessions {
c.closeInternal()
}
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package source defines interfaces for policy stores,
+29 -17
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package source
@@ -117,12 +117,12 @@ func NewUserPlatformPolicyStore(token windows.Token) (*PlatformPolicyStore, erro
return newPlatformPolicyStore(gp.UserPolicy, softwareKey, policyLock), nil
}
func newPlatformPolicyStore(scope gp.Scope, softwareKey registry.Key, policyLock *gp.PolicyLock) *PlatformPolicyStore {
func newPlatformPolicyStore(scope gp.Scope, softwareKey registry.Key, policyLock lockableCloser) *PlatformPolicyStore {
return &PlatformPolicyStore{
scope: scope,
softwareKey: softwareKey,
done: make(chan struct{}),
policyLock: &optionalPolicyLock{PolicyLock: policyLock},
policyLock: &optionalPolicyLock{lockableCloser: policyLock},
}
}
@@ -468,13 +468,14 @@ type gpLockState int
const (
gpUnlocked = gpLockState(iota)
gpLocked
gpLockRestricted // the lock could not be acquired due to a restriction in place
gpLockSkipped // the lock was not acquired, but reading proceeds without it
)
// optionalPolicyLock is a wrapper around [gp.PolicyLock] that locks
// and unlocks the underlying [gp.PolicyLock].
//
// If the [gp.PolicyLock.Lock] returns [gp.ErrLockRestricted], the error is ignored,
// If [gp.PolicyLock.Lock] returns [gp.ErrLockRestricted] or
// [windows.ERROR_ACCESS_DENIED], the error is ignored,
// and calling [optionalPolicyLock.Unlock] is a no-op.
//
// The underlying GP lock is kinda optional: it is safe to read policy settings
@@ -483,27 +484,38 @@ const (
//
// It is not safe for concurrent use.
type optionalPolicyLock struct {
*gp.PolicyLock
state gpLockState
lockableCloser // typically a [*gp.PolicyLock]
state gpLockState
}
// Lock acquires the underlying [gp.PolicyLock], returning an error on failure.
//
// If the lock cannot be acquired due to a restriction in place
// (e.g., attempting to acquire a lock while the service is starting),
// the lock is considered to be held, the method returns nil, and a subsequent
// call to [Unlock] is a no-op.
// or because the caller is not allowed to acquire it, the lock is considered
// to be held, the method returns nil, and a subsequent call to [Unlock] is a no-op.
// The latter happens with [windows.ERROR_ACCESS_DENIED] when a user policy lock
// is requested by a process whose user has no interactive logon session, such as
// tailscaled started from an SSH session; policy settings are then read
// without the lock.
//
// It is a runtime error to call Lock when the lock is already held.
func (o *optionalPolicyLock) Lock() error {
if o.state != gpUnlocked {
panic("already locked")
}
switch err := o.PolicyLock.Lock(); err {
case nil:
err := o.lockableCloser.Lock()
switch {
case err == nil:
o.state = gpLocked
return nil
case gp.ErrLockRestricted:
case errors.Is(err, gp.ErrLockRestricted):
loggerx.Errorf("GP lock not acquired: %v", err)
o.state = gpLockRestricted
o.state = gpLockSkipped
return nil
case errors.Is(err, windows.ERROR_ACCESS_DENIED):
loggerx.Errorf("GP lock not acquired (%v); reading policy settings without it", err)
o.state = gpLockSkipped
return nil
default:
return err
@@ -515,10 +527,10 @@ func (o *optionalPolicyLock) Lock() error {
func (o *optionalPolicyLock) Unlock() {
switch o.state {
case gpLocked:
o.PolicyLock.Unlock()
case gpLockRestricted:
// The GP lock wasn't acquired due to a restriction in place
// when [optionalPolicyLock.Lock] was called. Unlock is a no-op.
o.lockableCloser.Unlock()
case gpLockSkipped:
// The GP lock wasn't acquired when [optionalPolicyLock.Lock]
// was called. Unlock is a no-op.
case gpUnlocked:
panic("not locked")
default:
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package source