WIP: rebase fork onto upstream/main (v1.103.0) #15
@@ -150,7 +150,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
|
||||
W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs
|
||||
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
|
||||
github.com/tailscale/hujson from tailscale.com/ipn/conffile
|
||||
github.com/tailscale/hujson from tailscale.com/ipn/conffile+
|
||||
github.com/tailscale/web-client-prebuilt from tailscale.com/client/web
|
||||
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli+
|
||||
github.com/x448/float16 from github.com/fxamacker/cbor/v2
|
||||
|
||||
@@ -187,7 +187,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
|
||||
W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs
|
||||
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
|
||||
github.com/tailscale/hujson from tailscale.com/ipn/conffile
|
||||
github.com/tailscale/hujson from tailscale.com/ipn/conffile+
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/net/routetable+
|
||||
L 💣 github.com/tailscale/netlink/nl from github.com/tailscale/netlink
|
||||
LD github.com/tailscale/peercred from tailscale.com/ipn/ipnauth
|
||||
@@ -477,7 +477,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/set from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/slicesx from tailscale.com/appc+
|
||||
tailscale.com/util/syspolicy from tailscale.com/feature/syspolicy
|
||||
tailscale.com/util/syspolicy from tailscale.com/feature/syspolicy+
|
||||
tailscale.com/util/syspolicy/internal from tailscale.com/util/syspolicy/setting+
|
||||
tailscale.com/util/syspolicy/internal/loggerx from tailscale.com/util/syspolicy/internal/metrics+
|
||||
tailscale.com/util/syspolicy/internal/metrics from tailscale.com/util/syspolicy/source
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_syspolicy
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/util/syspolicy"
|
||||
)
|
||||
|
||||
// syspolicyFile is the path to a JSON syspolicy file, set via the
|
||||
// --syspolicy-file flag. An empty value disables file-based syspolicy.
|
||||
var syspolicyFile string
|
||||
|
||||
// defaultSyspolicyFile returns the platform-specific default path for the
|
||||
// --syspolicy-file flag. On Windows it sits next to the rest of Tailscale's
|
||||
// machine state under %ProgramData%\Tailscale. On every other platform
|
||||
// (Linux, the BSDs, illumos/Solaris, and tailscaled-without-the-GUI on
|
||||
// macOS) it uses /etc/tailscale, which is where admin-provided
|
||||
// configuration is conventionally placed.
|
||||
//
|
||||
// On Windows, when the file exists, its values take precedence over the
|
||||
// HKLM registry-based platform store on a per-key basis (with the registry
|
||||
// providing fallback values for keys the file does not set), because rsop
|
||||
// merges later-registered same-scope sources over earlier ones.
|
||||
func defaultSyspolicyFile() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
if pd := os.Getenv("ProgramData"); pd != "" {
|
||||
return filepath.Join(pd, "Tailscale", "syspolicy.json")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return "/etc/tailscale/syspolicy.json"
|
||||
}
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&syspolicyFile, "syspolicy-file", defaultSyspolicyFile(),
|
||||
"path to a JSON syspolicy file applied as a device-scope policy source; empty disables")
|
||||
loadSyspolicy.Set(func() {
|
||||
if syspolicyFile == "" {
|
||||
return
|
||||
}
|
||||
if err := syspolicy.LoadJSONPolicyFile("JSONFile", syspolicyFile); err != nil {
|
||||
log.Printf("%v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -183,6 +183,12 @@ var (
|
||||
hookOutboundProxyListen feature.Hook[func() proxyStartFunc]
|
||||
)
|
||||
|
||||
// loadSyspolicy, if set, loads a JSON-file-backed syspolicy source after
|
||||
// command-line flags are parsed, using the path from --syspolicy-file. It
|
||||
// is set when built without ts_omit_syspolicy (see syspolicy.go), and
|
||||
// unset otherwise.
|
||||
var loadSyspolicy feature.Hook[func()]
|
||||
|
||||
// proxyStartFunc is the type of the function returned by
|
||||
// outboundProxyListen, to start the servers on the Listeners
|
||||
// started by hookOutboundProxyListen.
|
||||
@@ -295,6 +301,12 @@ store state on filesystem.`)
|
||||
}
|
||||
}
|
||||
|
||||
// If syspolicy is built in, load the JSON syspolicy file (if any) now
|
||||
// so its settings are visible before anything queries them.
|
||||
if f, ok := loadSyspolicy.GetOk(); ok {
|
||||
f()
|
||||
}
|
||||
|
||||
if buildfeatures.HasTPM {
|
||||
handleTPMFlags()
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
|
||||
W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs
|
||||
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
|
||||
github.com/tailscale/hujson from tailscale.com/ipn/conffile
|
||||
github.com/tailscale/hujson from tailscale.com/ipn/conffile+
|
||||
LD github.com/tailscale/peercred from tailscale.com/ipn/ipnauth
|
||||
github.com/tailscale/web-client-prebuilt from tailscale.com/client/web
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
||||
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
|
||||
W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs
|
||||
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
|
||||
LDW github.com/tailscale/hujson from tailscale.com/ipn/conffile
|
||||
LDW github.com/tailscale/hujson from tailscale.com/ipn/conffile+
|
||||
LDAI github.com/tailscale/peercred from tailscale.com/ipn/ipnauth
|
||||
LDWI github.com/tailscale/web-client-prebuilt from tailscale.com/client/web
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
|
||||
@@ -52,6 +52,7 @@ import (
|
||||
_ "tailscale.com/util/clientmetric"
|
||||
_ "tailscale.com/util/eventbus"
|
||||
_ "tailscale.com/util/osshare"
|
||||
_ "tailscale.com/util/syspolicy"
|
||||
_ "tailscale.com/util/syspolicy/pkey"
|
||||
_ "tailscale.com/util/syspolicy/policyclient"
|
||||
_ "tailscale.com/version"
|
||||
|
||||
@@ -52,6 +52,7 @@ import (
|
||||
_ "tailscale.com/util/clientmetric"
|
||||
_ "tailscale.com/util/eventbus"
|
||||
_ "tailscale.com/util/osshare"
|
||||
_ "tailscale.com/util/syspolicy"
|
||||
_ "tailscale.com/util/syspolicy/pkey"
|
||||
_ "tailscale.com/util/syspolicy/policyclient"
|
||||
_ "tailscale.com/version"
|
||||
|
||||
@@ -52,6 +52,7 @@ import (
|
||||
_ "tailscale.com/util/clientmetric"
|
||||
_ "tailscale.com/util/eventbus"
|
||||
_ "tailscale.com/util/osshare"
|
||||
_ "tailscale.com/util/syspolicy"
|
||||
_ "tailscale.com/util/syspolicy/pkey"
|
||||
_ "tailscale.com/util/syspolicy/policyclient"
|
||||
_ "tailscale.com/version"
|
||||
|
||||
@@ -52,6 +52,7 @@ import (
|
||||
_ "tailscale.com/util/clientmetric"
|
||||
_ "tailscale.com/util/eventbus"
|
||||
_ "tailscale.com/util/osshare"
|
||||
_ "tailscale.com/util/syspolicy"
|
||||
_ "tailscale.com/util/syspolicy/pkey"
|
||||
_ "tailscale.com/util/syspolicy/policyclient"
|
||||
_ "tailscale.com/version"
|
||||
|
||||
@@ -62,6 +62,7 @@ import (
|
||||
_ "tailscale.com/util/eventbus"
|
||||
_ "tailscale.com/util/osdiag"
|
||||
_ "tailscale.com/util/osshare"
|
||||
_ "tailscale.com/util/syspolicy"
|
||||
_ "tailscale.com/util/syspolicy/pkey"
|
||||
_ "tailscale.com/util/syspolicy/policyclient"
|
||||
_ "tailscale.com/util/winutil"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package syspolicy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
|
||||
"tailscale.com/util/syspolicy/rsop"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/syspolicy/source"
|
||||
)
|
||||
|
||||
// LoadJSONPolicyFile loads policy settings from the JSON file at path and
|
||||
// registers them as a [setting.DeviceScope] policy source under sourceName.
|
||||
//
|
||||
// If path does not exist, no source is registered and the function returns
|
||||
// nil. Malformed JSON, unknown setting keys, or values that cannot be
|
||||
// decoded as the registered type for their key all surface as errors here
|
||||
// rather than at first use, and nothing is registered.
|
||||
//
|
||||
// LoadJSONPolicyFile is intended to be called once, early in process
|
||||
// startup, after command-line flags are parsed but before any policy
|
||||
// setting is read.
|
||||
func LoadJSONPolicyFile(sourceName, path string) error {
|
||||
store, err := source.NewJSONPolicyStoreFromFile(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("syspolicy: loading %s: %w", path, err)
|
||||
}
|
||||
if err := store.Validate(); err != nil {
|
||||
return fmt.Errorf("syspolicy: invalid %s:\n%w", path, err)
|
||||
}
|
||||
if _, err := rsop.RegisterStore(sourceName, setting.DeviceScope, store); err != nil {
|
||||
return fmt.Errorf("syspolicy: registering %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/ptype"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
)
|
||||
|
||||
func TestJSONPolicyStoreReadString(t *testing.T) {
|
||||
s := NewJSONPolicyStore(map[string]any{
|
||||
"ControlURL": "https://controlplane.example.com",
|
||||
"AuthKey": "tskey-auth-xxx",
|
||||
"Bool": true,
|
||||
})
|
||||
|
||||
got, err := s.ReadString("ControlURL")
|
||||
if err != nil {
|
||||
t.Fatalf("ReadString(ControlURL) error: %v", err)
|
||||
}
|
||||
if want := "https://controlplane.example.com"; got != want {
|
||||
t.Errorf("ReadString(ControlURL) = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if _, err := s.ReadString("Missing"); !errors.Is(err, setting.ErrNotConfigured) {
|
||||
t.Errorf("ReadString(Missing) err = %v, want ErrNotConfigured", err)
|
||||
}
|
||||
|
||||
if _, err := s.ReadString("Bool"); !errors.Is(err, setting.ErrTypeMismatch) {
|
||||
t.Errorf("ReadString(Bool) err = %v, want ErrTypeMismatch", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONPolicyStoreReadBoolean(t *testing.T) {
|
||||
s := NewJSONPolicyStore(map[string]any{
|
||||
"AlwaysOn": true,
|
||||
"Str": "yes",
|
||||
})
|
||||
|
||||
got, err := s.ReadBoolean("AlwaysOn")
|
||||
if err != nil {
|
||||
t.Fatalf("ReadBoolean(AlwaysOn) error: %v", err)
|
||||
}
|
||||
if !got {
|
||||
t.Errorf("ReadBoolean(AlwaysOn) = false, want true")
|
||||
}
|
||||
|
||||
if _, err := s.ReadBoolean("Missing"); !errors.Is(err, setting.ErrNotConfigured) {
|
||||
t.Errorf("ReadBoolean(Missing) err = %v, want ErrNotConfigured", err)
|
||||
}
|
||||
|
||||
if _, err := s.ReadBoolean("Str"); !errors.Is(err, setting.ErrTypeMismatch) {
|
||||
t.Errorf("ReadBoolean(Str) err = %v, want ErrTypeMismatch", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONPolicyStoreReadStringArray(t *testing.T) {
|
||||
s := NewJSONPolicyStore(map[string]any{
|
||||
"AllowedSuggestedExitNodes": []any{"node-a", "node-b"},
|
||||
"Empty": []any{},
|
||||
"NotArray": "abc",
|
||||
"WrongElem": []any{"a", 1},
|
||||
})
|
||||
|
||||
got, err := s.ReadStringArray("AllowedSuggestedExitNodes")
|
||||
if err != nil {
|
||||
t.Fatalf("ReadStringArray(AllowedSuggestedExitNodes) error: %v", err)
|
||||
}
|
||||
if want := []string{"node-a", "node-b"}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("ReadStringArray = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
got, err = s.ReadStringArray("Empty")
|
||||
if err != nil {
|
||||
t.Fatalf("ReadStringArray(Empty) error: %v", err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Errorf("ReadStringArray(Empty) = %v, want empty", got)
|
||||
}
|
||||
|
||||
if _, err := s.ReadStringArray("Missing"); !errors.Is(err, setting.ErrNotConfigured) {
|
||||
t.Errorf("ReadStringArray(Missing) err = %v, want ErrNotConfigured", err)
|
||||
}
|
||||
if _, err := s.ReadStringArray("NotArray"); !errors.Is(err, setting.ErrTypeMismatch) {
|
||||
t.Errorf("ReadStringArray(NotArray) err = %v, want ErrTypeMismatch", err)
|
||||
}
|
||||
if _, err := s.ReadStringArray("WrongElem"); !errors.Is(err, setting.ErrTypeMismatch) {
|
||||
t.Errorf("ReadStringArray(WrongElem) err = %v, want ErrTypeMismatch", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONPolicyStoreReadUInt64FromMap(t *testing.T) {
|
||||
s := NewJSONPolicyStore(map[string]any{
|
||||
"Float": float64(42),
|
||||
"Negative": float64(-1),
|
||||
"Frac": float64(1.5),
|
||||
"Str": "nope",
|
||||
})
|
||||
|
||||
got, err := s.ReadUInt64("Float")
|
||||
if err != nil {
|
||||
t.Fatalf("ReadUInt64(Float) error: %v", err)
|
||||
}
|
||||
if got != 42 {
|
||||
t.Errorf("ReadUInt64(Float) = %d, want 42", got)
|
||||
}
|
||||
|
||||
if _, err := s.ReadUInt64("Missing"); !errors.Is(err, setting.ErrNotConfigured) {
|
||||
t.Errorf("ReadUInt64(Missing) err = %v, want ErrNotConfigured", err)
|
||||
}
|
||||
for _, k := range []pkey.Key{"Negative", "Frac", "Str"} {
|
||||
if _, err := s.ReadUInt64(k); !errors.Is(err, setting.ErrTypeMismatch) {
|
||||
t.Errorf("ReadUInt64(%s) err = %v, want ErrTypeMismatch", k, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONPolicyStoreReadUInt64FromJSON(t *testing.T) {
|
||||
// json.Number path: bigger than float64 mantissa precision.
|
||||
s, err := NewJSONPolicyStoreFromBytes([]byte(`{"Big": 18446744073709551610}`))
|
||||
if err != nil {
|
||||
t.Fatalf("NewJSONPolicyStoreFromBytes: %v", err)
|
||||
}
|
||||
got, err := s.ReadUInt64("Big")
|
||||
if err != nil {
|
||||
t.Fatalf("ReadUInt64(Big) error: %v", err)
|
||||
}
|
||||
if want := uint64(18446744073709551610); got != want {
|
||||
t.Errorf("ReadUInt64(Big) = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestJSONPolicyStoreDuration verifies that duration-typed settings, which
|
||||
// the reader fetches via ReadString and parses with time.ParseDuration, work
|
||||
// end-to-end with values like "24h".
|
||||
func TestJSONPolicyStoreDuration(t *testing.T) {
|
||||
s := NewJSONPolicyStore(map[string]any{
|
||||
"ReconnectAfter": "24h",
|
||||
"Bad": "notaduration",
|
||||
})
|
||||
def := setting.NewDefinition("ReconnectAfter", setting.DeviceSetting, setting.DurationValue)
|
||||
v, err := readPolicySettingValue(s, def)
|
||||
if err != nil {
|
||||
t.Fatalf("readPolicySettingValue(ReconnectAfter) error: %v", err)
|
||||
}
|
||||
if got, want := v.(time.Duration), 24*time.Hour; got != want {
|
||||
t.Errorf("ReconnectAfter = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
badDef := setting.NewDefinition("Bad", setting.DeviceSetting, setting.DurationValue)
|
||||
if _, err := readPolicySettingValue(s, badDef); err == nil {
|
||||
t.Errorf("readPolicySettingValue(Bad) err = nil, want parse error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONPolicyStorePreferenceOptionAndVisibility(t *testing.T) {
|
||||
s := NewJSONPolicyStore(map[string]any{
|
||||
"CheckUpdates": "always",
|
||||
"AdminConsoleVisibility": "hide",
|
||||
})
|
||||
prefDef := setting.NewDefinition("CheckUpdates", setting.DeviceSetting, setting.PreferenceOptionValue)
|
||||
v, err := readPolicySettingValue(s, prefDef)
|
||||
if err != nil {
|
||||
t.Fatalf("readPolicySettingValue(CheckUpdates) error: %v", err)
|
||||
}
|
||||
if got := v.(ptype.PreferenceOption); got != ptype.AlwaysByPolicy {
|
||||
t.Errorf("CheckUpdates = %v, want AlwaysByPolicy", got)
|
||||
}
|
||||
|
||||
visDef := setting.NewDefinition("AdminConsoleVisibility", setting.UserSetting, setting.VisibilityValue)
|
||||
v, err = readPolicySettingValue(s, visDef)
|
||||
if err != nil {
|
||||
t.Fatalf("readPolicySettingValue(AdminConsoleVisibility) error: %v", err)
|
||||
}
|
||||
if got := v.(ptype.Visibility); got != ptype.HiddenByPolicy {
|
||||
t.Errorf("AdminConsoleVisibility = %v, want HiddenByPolicy", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONPolicyStoreValidate(t *testing.T) {
|
||||
// Register a small set of definitions covering each type that the
|
||||
// validator needs to exercise.
|
||||
if err := setting.SetDefinitionsForTest(t,
|
||||
setting.NewDefinition("ControlURL", setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition("AlwaysOn", setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition("AllowedSuggestedExitNodes", setting.DeviceSetting, setting.StringListValue),
|
||||
setting.NewDefinition("ReconnectAfter", setting.DeviceSetting, setting.DurationValue),
|
||||
setting.NewDefinition("CheckUpdates", setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition("AdminConsoleVisibility", setting.UserSetting, setting.VisibilityValue),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
json map[string]any
|
||||
wantOK bool
|
||||
wantErrSubs []string // substrings the error must contain (only when wantOK is false)
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
json: map[string]any{},
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "all_valid",
|
||||
json: map[string]any{
|
||||
"ControlURL": "https://controlplane.example.com",
|
||||
"AlwaysOn": true,
|
||||
"AllowedSuggestedExitNodes": []any{"node-a", "node-b"},
|
||||
"ReconnectAfter": "24h",
|
||||
"CheckUpdates": "always",
|
||||
"AdminConsoleVisibility": "hide",
|
||||
},
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "unknown_key",
|
||||
json: map[string]any{
|
||||
"ControlURL": "https://controlplane.example.com",
|
||||
"NoSuchThing": "whatever",
|
||||
},
|
||||
wantErrSubs: []string{`unknown policy setting "NoSuchThing"`},
|
||||
},
|
||||
{
|
||||
name: "wrong_type_string",
|
||||
json: map[string]any{
|
||||
"ControlURL": true,
|
||||
},
|
||||
wantErrSubs: []string{`"ControlURL"`, "type mismatch"},
|
||||
},
|
||||
{
|
||||
name: "wrong_type_bool",
|
||||
json: map[string]any{
|
||||
"AlwaysOn": "yes",
|
||||
},
|
||||
wantErrSubs: []string{`"AlwaysOn"`, "type mismatch"},
|
||||
},
|
||||
{
|
||||
name: "wrong_type_array",
|
||||
json: map[string]any{
|
||||
"AllowedSuggestedExitNodes": "node-a",
|
||||
},
|
||||
wantErrSubs: []string{`"AllowedSuggestedExitNodes"`, "type mismatch"},
|
||||
},
|
||||
{
|
||||
name: "bad_duration",
|
||||
json: map[string]any{
|
||||
"ReconnectAfter": "notaduration",
|
||||
},
|
||||
wantErrSubs: []string{`"ReconnectAfter"`, "notaduration"},
|
||||
},
|
||||
{
|
||||
name: "bad_preference_option",
|
||||
json: map[string]any{
|
||||
"CheckUpdates": "sometimes",
|
||||
},
|
||||
wantErrSubs: []string{`"CheckUpdates"`, "sometimes", "always"},
|
||||
},
|
||||
{
|
||||
name: "bad_visibility",
|
||||
json: map[string]any{
|
||||
"AdminConsoleVisibility": "maybe",
|
||||
},
|
||||
wantErrSubs: []string{`"AdminConsoleVisibility"`, "maybe", "show"},
|
||||
},
|
||||
{
|
||||
name: "multiple_errors_reported",
|
||||
json: map[string]any{
|
||||
"AlwaysOn": "yes",
|
||||
"NoSuchThing": 1,
|
||||
},
|
||||
wantErrSubs: []string{`"AlwaysOn"`, `unknown policy setting "NoSuchThing"`},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := NewJSONPolicyStore(tt.json).Validate()
|
||||
if tt.wantOK {
|
||||
if err != nil {
|
||||
t.Fatalf("Validate() = %v, want nil", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatalf("Validate() = nil, want error containing %v", tt.wantErrSubs)
|
||||
}
|
||||
for _, sub := range tt.wantErrSubs {
|
||||
if !strings.Contains(err.Error(), sub) {
|
||||
t.Errorf("Validate() error = %q, want it to contain %q", err, sub)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewJSONPolicyStoreFromBytesHuJSON(t *testing.T) {
|
||||
if hujsonStandardize == nil {
|
||||
t.Skip("HuJSON support not linked into this build")
|
||||
}
|
||||
const hujsonInput = `{
|
||||
// The control plane URL.
|
||||
"ControlURL": "https://controlplane.example.com",
|
||||
"AlwaysOn": true, // trailing comma is OK in HuJSON
|
||||
}`
|
||||
s, err := NewJSONPolicyStoreFromBytes([]byte(hujsonInput))
|
||||
if err != nil {
|
||||
t.Fatalf("NewJSONPolicyStoreFromBytes(HuJSON) error: %v", err)
|
||||
}
|
||||
got, err := s.ReadString("ControlURL")
|
||||
if err != nil {
|
||||
t.Fatalf("ReadString: %v", err)
|
||||
}
|
||||
if want := "https://controlplane.example.com"; got != want {
|
||||
t.Errorf("ReadString = %q, want %q", got, want)
|
||||
}
|
||||
gotBool, err := s.ReadBoolean("AlwaysOn")
|
||||
if err != nil {
|
||||
t.Fatalf("ReadBoolean: %v", err)
|
||||
}
|
||||
if !gotBool {
|
||||
t.Errorf("ReadBoolean = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewJSONPolicyStoreFromFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "syspolicy.json")
|
||||
if err := os.WriteFile(path, []byte(`{"ControlURL": "https://example.com"}`), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s, err := NewJSONPolicyStoreFromFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("NewJSONPolicyStoreFromFile: %v", err)
|
||||
}
|
||||
got, err := s.ReadString("ControlURL")
|
||||
if err != nil {
|
||||
t.Fatalf("ReadString: %v", err)
|
||||
}
|
||||
if want := "https://example.com"; got != want {
|
||||
t.Errorf("ReadString = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if _, err := NewJSONPolicyStoreFromFile(filepath.Join(dir, "missing.json")); err == nil {
|
||||
t.Errorf("NewJSONPolicyStoreFromFile(missing) err = nil, want error")
|
||||
}
|
||||
|
||||
bad := filepath.Join(dir, "bad.json")
|
||||
if err := os.WriteFile(bad, []byte(`not json`), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := NewJSONPolicyStoreFromFile(bad); err == nil {
|
||||
t.Errorf("NewJSONPolicyStoreFromFile(bad) err = nil, want parse error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user