mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
util: add syspolicy package (#9550)
Add a more generalized package for getting policies. Updates tailcale/corp#10967 Signed-off-by: Claire Wang <claire@tailscale.com> Co-authored-by: Adrian Dewhurst <adrian@tailscale.com>
This commit is contained in:
parent
d71184d674
commit
32c0156311
52
util/syspolicy/handler.go
Normal file
52
util/syspolicy/handler.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package syspolicy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
handlerUsed atomic.Bool
|
||||||
|
handler Handler = defaultHandler{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler reads system policies from OS-specific storage.
|
||||||
|
type Handler interface {
|
||||||
|
// ReadString reads the policy settings value string given the key.
|
||||||
|
ReadString(key string) (string, error)
|
||||||
|
// ReadUInt64 reads the policy settings uint64 value given the key.
|
||||||
|
ReadUInt64(key string) (uint64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrNoSuchKey is returned when the specified key does not have a value set.
|
||||||
|
var ErrNoSuchKey = errors.New("no such key")
|
||||||
|
|
||||||
|
// defaultHandler is the catch all syspolicy type for anything that isn't windows or apple.
|
||||||
|
type defaultHandler struct{}
|
||||||
|
|
||||||
|
func (defaultHandler) ReadString(_ string) (string, error) {
|
||||||
|
return "", ErrNoSuchKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (defaultHandler) ReadUInt64(_ string) (uint64, error) {
|
||||||
|
return 0, ErrNoSuchKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// markHandlerInUse is called before handler methods are called.
|
||||||
|
func markHandlerInUse() {
|
||||||
|
handlerUsed.Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterHandler initializes the policy handler and ensures registration will happen once.
|
||||||
|
func RegisterHandler(h Handler) {
|
||||||
|
// Technically this assignment is not concurrency safe, but in the
|
||||||
|
// event that there was any risk of a data race, we will panic due to
|
||||||
|
// the CompareAndSwap failing.
|
||||||
|
handler = h
|
||||||
|
if !handlerUsed.CompareAndSwap(false, true) {
|
||||||
|
panic("handler was already used before registration")
|
||||||
|
}
|
||||||
|
}
|
19
util/syspolicy/handler_test.go
Normal file
19
util/syspolicy/handler_test.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package syspolicy
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestDefaultHandlerReadValues(t *testing.T) {
|
||||||
|
var h defaultHandler
|
||||||
|
|
||||||
|
got, err := h.ReadString(string(AdminConsoleVisibility))
|
||||||
|
if got != "" || err != ErrNoSuchKey {
|
||||||
|
t.Fatalf("got %v err %v", got, err)
|
||||||
|
}
|
||||||
|
result, err := h.ReadUInt64(string(LogSCMInteractions))
|
||||||
|
if result != 0 || err != ErrNoSuchKey {
|
||||||
|
t.Fatalf("got %v err %v", result, err)
|
||||||
|
}
|
||||||
|
}
|
32
util/syspolicy/handler_windows.go
Normal file
32
util/syspolicy/handler_windows.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package syspolicy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"tailscale.com/util/winutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type windowsHandler struct{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterHandler(windowsHandler{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (windowsHandler) ReadString(key string) (string, error) {
|
||||||
|
s, err := winutil.GetPolicyString(key)
|
||||||
|
if errors.Is(err, winutil.ErrNoValue) {
|
||||||
|
err = ErrNoSuchKey
|
||||||
|
}
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (windowsHandler) ReadUInt64(key string) (uint64, error) {
|
||||||
|
value, err := winutil.GetPolicyInteger(key)
|
||||||
|
if errors.Is(err, winutil.ErrNoValue) {
|
||||||
|
err = ErrNoSuchKey
|
||||||
|
}
|
||||||
|
return value, err
|
||||||
|
}
|
35
util/syspolicy/policy_keys.go
Normal file
35
util/syspolicy/policy_keys.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package syspolicy
|
||||||
|
|
||||||
|
type Key string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Keys with a string value
|
||||||
|
ControlURL Key = "LoginURL" // default ""; if blank, ipn uses ipn.DefaultControlURL.
|
||||||
|
LogTarget Key = "LogTarget" // default ""; if blank logging uses logtail.DefaultHost.
|
||||||
|
|
||||||
|
// Keys with a string value that specifies an option: "always", "never", "user-decides".
|
||||||
|
// The default is "user-decides" unless otherwise stated.
|
||||||
|
EnableIncomingConnections Key = "AllowIncomingConnections"
|
||||||
|
EnableServerMode Key = "UnattendedMode"
|
||||||
|
|
||||||
|
// Keys with a string value that controls visibility: "show", "hide".
|
||||||
|
// The default is "show" unless otherwise stated.
|
||||||
|
AdminConsoleVisibility Key = "AdminConsole"
|
||||||
|
NetworkDevicesVisibility Key = "NetworkDevices"
|
||||||
|
TestMenuVisibility Key = "TestMenu"
|
||||||
|
UpdateMenuVisibility Key = "UpdateMenu"
|
||||||
|
RunExitNodeVisibility Key = "RunExitNode"
|
||||||
|
PreferencesMenuVisibility Key = "PreferencesMenu"
|
||||||
|
|
||||||
|
// Keys with a string value formatted for use with time.ParseDuration().
|
||||||
|
KeyExpirationNoticeTime Key = "KeyExpirationNotice" // default 24 hours
|
||||||
|
|
||||||
|
// Boolean Keys that are only applicable on Windows. Booleans are stored in the registry as
|
||||||
|
// DWORD or QWORD (either is acceptable). 0 means false, and anything else means true.
|
||||||
|
// The default is 0 unless otherwise stated.
|
||||||
|
LogSCMInteractions Key = "LogSCMInteractions"
|
||||||
|
FlushDNSOnSessionUnlock Key = "FlushDNSOnSessionUnlock"
|
||||||
|
)
|
172
util/syspolicy/syspolicy.go
Normal file
172
util/syspolicy/syspolicy.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
// Package syspolicy provides functions to retrieve system settings of a device.
|
||||||
|
package syspolicy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetString(key Key, defaultValue string) (string, error) {
|
||||||
|
markHandlerInUse()
|
||||||
|
v, err := handler.ReadString(string(key))
|
||||||
|
if errors.Is(err, ErrNoSuchKey) {
|
||||||
|
return defaultValue, nil
|
||||||
|
}
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUint64(key Key, defaultValue uint64) (uint64, error) {
|
||||||
|
markHandlerInUse()
|
||||||
|
v, err := handler.ReadUInt64(string(key))
|
||||||
|
if errors.Is(err, ErrNoSuchKey) {
|
||||||
|
return defaultValue, nil
|
||||||
|
}
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 int
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPreferenceOption loads a policy from the registry that can be
|
||||||
|
// managed by an enterprise policy management system and allows administrative
|
||||||
|
// overrides of users' choices in a way that we do not want tailcontrol to have
|
||||||
|
// the authority to set. It describes user-decides/always/never options, where
|
||||||
|
// "always" and "never" remove the user's ability to make a selection. If not
|
||||||
|
// present or set to a different value, "user-decides" is the default.
|
||||||
|
func GetPreferenceOption(name Key) (PreferenceOption, error) {
|
||||||
|
opt, err := GetString(name, "user-decides")
|
||||||
|
if err != nil {
|
||||||
|
return showChoiceByPolicy, err
|
||||||
|
}
|
||||||
|
switch opt {
|
||||||
|
case "always":
|
||||||
|
return alwaysByPolicy, nil
|
||||||
|
case "never":
|
||||||
|
return neverByPolicy, nil
|
||||||
|
default:
|
||||||
|
return showChoiceByPolicy, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visibility is a policy that controls whether or not a particular
|
||||||
|
// component of a user interface is to be shown.
|
||||||
|
type Visibility byte
|
||||||
|
|
||||||
|
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 and only if the policy is visibleByPolicy.
|
||||||
|
func (p Visibility) Show() bool {
|
||||||
|
return p == visibleByPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVisibility loads a policy from the registry that can be managed
|
||||||
|
// by an enterprise policy management system and describes show/hide decisions
|
||||||
|
// for UI elements. The registry value should be a string set to "show" (return
|
||||||
|
// true) or "hide" (return true). If not present or set to a different value,
|
||||||
|
// "show" (return false) is the default.
|
||||||
|
func GetVisibility(name Key) (Visibility, error) {
|
||||||
|
opt, err := GetString(name, "show")
|
||||||
|
if err != nil {
|
||||||
|
return visibleByPolicy, err
|
||||||
|
}
|
||||||
|
switch opt {
|
||||||
|
case "hide":
|
||||||
|
return hiddenByPolicy, nil
|
||||||
|
default:
|
||||||
|
return visibleByPolicy, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDuration loads a policy from the registry that can be managed
|
||||||
|
// by an enterprise policy management system and describes a duration for some
|
||||||
|
// action. The registry value should be a string that time.ParseDuration
|
||||||
|
// understands. If the registry value is "" or can not be processed,
|
||||||
|
// defaultValue is returned instead.
|
||||||
|
func GetDuration(name Key, defaultValue time.Duration) (time.Duration, error) {
|
||||||
|
opt, err := GetString(name, "")
|
||||||
|
if opt == "" || err != nil {
|
||||||
|
return defaultValue, err
|
||||||
|
}
|
||||||
|
v, err := time.ParseDuration(opt)
|
||||||
|
if err != nil || v < 0 {
|
||||||
|
return defaultValue, nil
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectControlURL returns the ControlURL to use based on a value in
|
||||||
|
// the registry (LoginURL) and the one on disk (in the GUI's
|
||||||
|
// prefs.conf). If both are empty, it returns a default value. (It
|
||||||
|
// always return a non-empty value)
|
||||||
|
//
|
||||||
|
// See https://github.com/tailscale/tailscale/issues/2798 for some background.
|
||||||
|
func SelectControlURL(reg, disk string) string {
|
||||||
|
const def = "https://controlplane.tailscale.com"
|
||||||
|
|
||||||
|
// Prior to Dec 2020's commit 739b02e6, the installer
|
||||||
|
// wrote a LoginURL value of https://login.tailscale.com to the registry.
|
||||||
|
const oldRegDef = "https://login.tailscale.com"
|
||||||
|
|
||||||
|
// If they have an explicit value in the registry, use it,
|
||||||
|
// unless it's an old default value from an old installer.
|
||||||
|
// Then we have to see which is better.
|
||||||
|
if reg != "" {
|
||||||
|
if reg != oldRegDef {
|
||||||
|
// Something explicit in the registry that we didn't
|
||||||
|
// set ourselves by the installer.
|
||||||
|
return reg
|
||||||
|
}
|
||||||
|
if disk == "" {
|
||||||
|
// Something in the registry is better than nothing on disk.
|
||||||
|
return reg
|
||||||
|
}
|
||||||
|
if disk != def && disk != oldRegDef {
|
||||||
|
// The value in the registry is the old
|
||||||
|
// default (login.tailscale.com) but the value
|
||||||
|
// on disk is neither our old nor new default
|
||||||
|
// value, so it must be some custom thing that
|
||||||
|
// the user cares about. Prefer the disk value.
|
||||||
|
return disk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if disk != "" {
|
||||||
|
return disk
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
375
util/syspolicy/syspolicy_test.go
Normal file
375
util/syspolicy/syspolicy_test.go
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package syspolicy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testHandler encompasses all data types returned when testing any of the syspolicy
|
||||||
|
// methods that involve getting a policy value.
|
||||||
|
// For keys and the corresponding values, check policy_keys.go.
|
||||||
|
type testHandler struct {
|
||||||
|
t *testing.T
|
||||||
|
key Key
|
||||||
|
s string
|
||||||
|
u64 uint64
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
var someOtherError = errors.New("error other than not found")
|
||||||
|
|
||||||
|
func setHandlerForTest(tb testing.TB, h Handler) {
|
||||||
|
tb.Helper()
|
||||||
|
oldHandler := handler
|
||||||
|
handler = h
|
||||||
|
tb.Cleanup(func() { handler = oldHandler })
|
||||||
|
}
|
||||||
|
|
||||||
|
func (th *testHandler) ReadString(key string) (string, error) {
|
||||||
|
if key != string(th.key) {
|
||||||
|
th.t.Errorf("ReadString(%q) want %q", key, th.key)
|
||||||
|
}
|
||||||
|
return th.s, th.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (th *testHandler) ReadUInt64(key string) (uint64, error) {
|
||||||
|
if key != string(th.key) {
|
||||||
|
th.t.Errorf("ReadUint64(%q) want %q", key, th.key)
|
||||||
|
}
|
||||||
|
return th.u64, th.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetString(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key Key
|
||||||
|
handlerValue string
|
||||||
|
handlerError error
|
||||||
|
defaultValue string
|
||||||
|
wantValue string
|
||||||
|
wantError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "read existing value",
|
||||||
|
key: AdminConsoleVisibility,
|
||||||
|
handlerValue: "hide",
|
||||||
|
wantValue: "hide",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "read non-existing value",
|
||||||
|
key: EnableServerMode,
|
||||||
|
handlerError: ErrNoSuchKey,
|
||||||
|
wantError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "read non-existing value, non-blank default",
|
||||||
|
key: EnableServerMode,
|
||||||
|
handlerError: ErrNoSuchKey,
|
||||||
|
defaultValue: "test",
|
||||||
|
wantValue: "test",
|
||||||
|
wantError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reading value returns other error",
|
||||||
|
key: NetworkDevicesVisibility,
|
||||||
|
handlerError: someOtherError,
|
||||||
|
wantError: someOtherError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
setHandlerForTest(t, &testHandler{
|
||||||
|
t: t,
|
||||||
|
key: tt.key,
|
||||||
|
s: tt.handlerValue,
|
||||||
|
err: tt.handlerError,
|
||||||
|
})
|
||||||
|
value, err := GetString(tt.key, tt.defaultValue)
|
||||||
|
if err != tt.wantError {
|
||||||
|
t.Errorf("err=%q, want %q", err, tt.wantError)
|
||||||
|
}
|
||||||
|
if value != tt.wantValue {
|
||||||
|
t.Errorf("value=%v, want %v", value, tt.wantValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUint64(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key Key
|
||||||
|
handlerValue uint64
|
||||||
|
handlerError error
|
||||||
|
defaultValue uint64
|
||||||
|
wantValue uint64
|
||||||
|
wantError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "read existing value",
|
||||||
|
key: KeyExpirationNoticeTime,
|
||||||
|
handlerValue: 1,
|
||||||
|
wantValue: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "read non-existing value",
|
||||||
|
key: LogSCMInteractions,
|
||||||
|
handlerValue: 0,
|
||||||
|
handlerError: ErrNoSuchKey,
|
||||||
|
wantValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "read non-existing value, non-zero default",
|
||||||
|
key: LogSCMInteractions,
|
||||||
|
defaultValue: 2,
|
||||||
|
handlerError: ErrNoSuchKey,
|
||||||
|
wantValue: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reading value returns other error",
|
||||||
|
key: FlushDNSOnSessionUnlock,
|
||||||
|
handlerError: someOtherError,
|
||||||
|
wantError: someOtherError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
setHandlerForTest(t, &testHandler{
|
||||||
|
t: t,
|
||||||
|
key: tt.key,
|
||||||
|
u64: tt.handlerValue,
|
||||||
|
err: tt.handlerError,
|
||||||
|
})
|
||||||
|
value, err := GetUint64(tt.key, tt.defaultValue)
|
||||||
|
if err != tt.wantError {
|
||||||
|
t.Errorf("err=%q, want %q", err, tt.wantError)
|
||||||
|
}
|
||||||
|
if value != tt.wantValue {
|
||||||
|
t.Errorf("value=%v, want %v", value, tt.wantValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPreferenceOption(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key Key
|
||||||
|
handlerValue string
|
||||||
|
handlerError error
|
||||||
|
wantValue PreferenceOption
|
||||||
|
wantError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "always by policy",
|
||||||
|
key: EnableIncomingConnections,
|
||||||
|
handlerValue: "always",
|
||||||
|
wantValue: alwaysByPolicy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "never by policy",
|
||||||
|
key: EnableIncomingConnections,
|
||||||
|
handlerValue: "never",
|
||||||
|
wantValue: neverByPolicy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "use default",
|
||||||
|
key: EnableIncomingConnections,
|
||||||
|
handlerValue: "",
|
||||||
|
wantValue: showChoiceByPolicy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "read non-existing value",
|
||||||
|
key: EnableIncomingConnections,
|
||||||
|
handlerError: ErrNoSuchKey,
|
||||||
|
wantValue: showChoiceByPolicy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other error is returned",
|
||||||
|
key: EnableIncomingConnections,
|
||||||
|
handlerError: someOtherError,
|
||||||
|
wantValue: showChoiceByPolicy,
|
||||||
|
wantError: someOtherError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
setHandlerForTest(t, &testHandler{
|
||||||
|
t: t,
|
||||||
|
key: tt.key,
|
||||||
|
s: tt.handlerValue,
|
||||||
|
err: tt.handlerError,
|
||||||
|
})
|
||||||
|
option, err := GetPreferenceOption(tt.key)
|
||||||
|
if err != tt.wantError {
|
||||||
|
t.Errorf("err=%q, want %q", err, tt.wantError)
|
||||||
|
}
|
||||||
|
if option != tt.wantValue {
|
||||||
|
t.Errorf("option=%v, want %v", option, tt.wantValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetVisibility(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key Key
|
||||||
|
handlerValue string
|
||||||
|
handlerError error
|
||||||
|
wantValue Visibility
|
||||||
|
wantError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "hidden by policy",
|
||||||
|
key: AdminConsoleVisibility,
|
||||||
|
handlerValue: "hide",
|
||||||
|
wantValue: hiddenByPolicy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "visibility default",
|
||||||
|
key: AdminConsoleVisibility,
|
||||||
|
handlerValue: "show",
|
||||||
|
wantValue: visibleByPolicy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "read non-existing value",
|
||||||
|
key: AdminConsoleVisibility,
|
||||||
|
handlerValue: "show",
|
||||||
|
handlerError: ErrNoSuchKey,
|
||||||
|
wantValue: visibleByPolicy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other error is returned",
|
||||||
|
key: AdminConsoleVisibility,
|
||||||
|
handlerValue: "show",
|
||||||
|
handlerError: someOtherError,
|
||||||
|
wantValue: visibleByPolicy,
|
||||||
|
wantError: someOtherError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
setHandlerForTest(t, &testHandler{
|
||||||
|
t: t,
|
||||||
|
key: tt.key,
|
||||||
|
s: tt.handlerValue,
|
||||||
|
err: tt.handlerError,
|
||||||
|
})
|
||||||
|
visibility, err := GetVisibility(tt.key)
|
||||||
|
if err != tt.wantError {
|
||||||
|
t.Errorf("err=%q, want %q", err, tt.wantError)
|
||||||
|
}
|
||||||
|
if visibility != tt.wantValue {
|
||||||
|
t.Errorf("visibility=%v, want %v", visibility, tt.wantValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDuration(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key Key
|
||||||
|
handlerValue string
|
||||||
|
handlerError error
|
||||||
|
defaultValue time.Duration
|
||||||
|
wantValue time.Duration
|
||||||
|
wantError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "read existing value",
|
||||||
|
key: KeyExpirationNoticeTime,
|
||||||
|
handlerValue: "2h",
|
||||||
|
wantValue: 2 * time.Hour,
|
||||||
|
defaultValue: 24 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid duration value",
|
||||||
|
key: KeyExpirationNoticeTime,
|
||||||
|
handlerValue: "-20",
|
||||||
|
wantValue: 24 * time.Hour,
|
||||||
|
defaultValue: 24 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "read non-existing value",
|
||||||
|
key: KeyExpirationNoticeTime,
|
||||||
|
handlerError: ErrNoSuchKey,
|
||||||
|
wantValue: 24 * time.Hour,
|
||||||
|
defaultValue: 24 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "read non-existing value different default",
|
||||||
|
key: KeyExpirationNoticeTime,
|
||||||
|
handlerError: ErrNoSuchKey,
|
||||||
|
wantValue: 0 * time.Second,
|
||||||
|
defaultValue: 0 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other error is returned",
|
||||||
|
key: KeyExpirationNoticeTime,
|
||||||
|
handlerError: someOtherError,
|
||||||
|
wantValue: 24 * time.Hour,
|
||||||
|
wantError: someOtherError,
|
||||||
|
defaultValue: 24 * time.Hour,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
setHandlerForTest(t, &testHandler{
|
||||||
|
t: t,
|
||||||
|
key: tt.key,
|
||||||
|
s: tt.handlerValue,
|
||||||
|
err: tt.handlerError,
|
||||||
|
})
|
||||||
|
duration, err := GetDuration(tt.key, tt.defaultValue)
|
||||||
|
if err != tt.wantError {
|
||||||
|
t.Errorf("err=%q, want %q", err, tt.wantError)
|
||||||
|
}
|
||||||
|
if duration != tt.wantValue {
|
||||||
|
t.Errorf("duration=%v, want %v", duration, tt.wantValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSelectControlURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
reg, disk, want string
|
||||||
|
}{
|
||||||
|
// Modern default case.
|
||||||
|
{"", "", "https://controlplane.tailscale.com"},
|
||||||
|
|
||||||
|
// For a user who installed prior to Dec 2020, with
|
||||||
|
// stuff in their registry.
|
||||||
|
{"https://login.tailscale.com", "", "https://login.tailscale.com"},
|
||||||
|
|
||||||
|
// Ignore pre-Dec'20 LoginURL from installer if prefs
|
||||||
|
// prefs overridden manually to an on-prem control
|
||||||
|
// server.
|
||||||
|
{"https://login.tailscale.com", "http://on-prem", "http://on-prem"},
|
||||||
|
|
||||||
|
// Something unknown explicitly set in the registry always wins.
|
||||||
|
{"http://explicit-reg", "", "http://explicit-reg"},
|
||||||
|
{"http://explicit-reg", "http://on-prem", "http://explicit-reg"},
|
||||||
|
{"http://explicit-reg", "https://login.tailscale.com", "http://explicit-reg"},
|
||||||
|
{"http://explicit-reg", "https://controlplane.tailscale.com", "http://explicit-reg"},
|
||||||
|
|
||||||
|
// If nothing in the registry, disk wins.
|
||||||
|
{"", "http://on-prem", "http://on-prem"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := SelectControlURL(tt.reg, tt.disk); got != tt.want {
|
||||||
|
t.Errorf("(reg %q, disk %q) = %q; want %q", tt.reg, tt.disk, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user