mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
af3d3c433b
This adds a new package containing generic types to be used for defining preference hierarchies. These include prefs.Item, prefs.List, prefs.StructList, and prefs.StructMap. Each of these types represents a configurable preference, holding the preference's state, value, and metadata. The metadata includes the default value (if it differs from the zero value of the Go type) and flags indicating whether a preference is managed via syspolicy or is hidden/read-only for another reason. This information can be marshaled and sent to the GUI, CLI and web clients as a source of truth regarding preference configuration, management, and visibility/mutability states. We plan to use these types to define device preferences, such as the updater preferences, the permission mode to be used on Windows with #tailscale/corp#18342, and certain global options that are currently exposed as tailscaled flags. We also aim to eventually use these types for profile-local preferences in ipn.Prefs and and as a replacement for ipn.MaskedPrefs. The generic preference types are compatible with the tailscale.com/cmd/viewer and tailscale.com/cmd/cloner utilities. Updates #12736 Signed-off-by: Nick Khyl <nickk@tailscale.com>
180 lines
5.8 KiB
Go
180 lines
5.8 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package prefs contains types and functions to work with arbitrary
|
|
// preference hierarchies.
|
|
//
|
|
// Specifically, the package provides [Item], [List], [Map], [StructList] and [StructMap]
|
|
// types which represent individual preferences in a user-defined prefs struct.
|
|
// A valid prefs struct must contain one or more exported fields of the preference types,
|
|
// either directly or within nested structs, but not pointers to these types.
|
|
// Additionally to preferences, a prefs struct may contain any number of
|
|
// non-preference fields that will be marshalled and unmarshalled but are
|
|
// otherwise ignored by the prefs package.
|
|
//
|
|
// The preference types are compatible with the [tailscale.com/cmd/viewer] and
|
|
// [tailscale.com/cmd/cloner] utilities. It is recommended to generate a read-only view
|
|
// of the user-defined prefs structure and use it in place of prefs whenever the prefs
|
|
// should not be modified.
|
|
package prefs
|
|
|
|
import (
|
|
"errors"
|
|
|
|
jsonv2 "github.com/go-json-experiment/json"
|
|
"github.com/go-json-experiment/json/jsontext"
|
|
"tailscale.com/types/opt"
|
|
)
|
|
|
|
var (
|
|
// ErrManaged is the error returned when attempting to modify a managed preference.
|
|
ErrManaged = errors.New("cannot modify a managed preference")
|
|
// ErrReadOnly is the error returned when attempting to modify a readonly preference.
|
|
ErrReadOnly = errors.New("cannot modify a readonly preference")
|
|
)
|
|
|
|
// metadata holds type-agnostic preference metadata.
|
|
type metadata struct {
|
|
// Managed indicates whether the preference is managed via MDM, Group Policy, or other means.
|
|
Managed bool `json:",omitzero"`
|
|
|
|
// ReadOnly indicates whether the preference is read-only due to any other reasons,
|
|
// such as user's access rights.
|
|
ReadOnly bool `json:",omitzero"`
|
|
}
|
|
|
|
// serializable is a JSON-serializable preference data.
|
|
type serializable[T any] struct {
|
|
// Value is an optional preference value that is set when the preference is
|
|
// configured by the user or managed by an admin.
|
|
Value opt.Value[T] `json:",omitzero"`
|
|
// Default is the default preference value to be used
|
|
// when the preference has not been configured.
|
|
Default T `json:",omitzero"`
|
|
// Metadata is any additional type-agnostic preference metadata to be serialized.
|
|
Metadata metadata `json:",inline"`
|
|
}
|
|
|
|
// preference is an embeddable type that provides a common implementation for
|
|
// concrete preference types, such as [Item], [List], [Map], [StructList] and [StructMap].
|
|
type preference[T any] struct {
|
|
s serializable[T]
|
|
}
|
|
|
|
// preferenceOf returns a preference with the specified value and/or [Options].
|
|
func preferenceOf[T any](v opt.Value[T], opts ...Options) preference[T] {
|
|
var m metadata
|
|
for _, o := range opts {
|
|
o(&m)
|
|
}
|
|
return preference[T]{serializable[T]{Value: v, Metadata: m}}
|
|
}
|
|
|
|
// IsSet reports whether p has a value set.
|
|
func (p preference[T]) IsSet() bool {
|
|
return p.s.Value.IsSet()
|
|
}
|
|
|
|
// Value returns the value of p if the preference has a value set.
|
|
// Otherwise, it returns its default value.
|
|
func (p preference[T]) Value() T {
|
|
val, _ := p.ValueOk()
|
|
return val
|
|
}
|
|
|
|
// ValueOk returns the value of p and true if the preference has a value set.
|
|
// Otherwise, it returns its default value and false.
|
|
func (p preference[T]) ValueOk() (val T, ok bool) {
|
|
if val, ok = p.s.Value.GetOk(); ok {
|
|
return val, true
|
|
}
|
|
return p.DefaultValue(), false
|
|
}
|
|
|
|
// SetValue configures the preference with the specified value.
|
|
// It fails and returns [ErrManaged] if p is a managed preference,
|
|
// and [ErrReadOnly] if p is a read-only preference.
|
|
func (p *preference[T]) SetValue(val T) error {
|
|
switch {
|
|
case p.s.Metadata.Managed:
|
|
return ErrManaged
|
|
case p.s.Metadata.ReadOnly:
|
|
return ErrReadOnly
|
|
default:
|
|
p.s.Value.Set(val)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// ClearValue resets the preference to an unconfigured state.
|
|
// It fails and returns [ErrManaged] if p is a managed preference,
|
|
// and [ErrReadOnly] if p is a read-only preference.
|
|
func (p *preference[T]) ClearValue() error {
|
|
switch {
|
|
case p.s.Metadata.Managed:
|
|
return ErrManaged
|
|
case p.s.Metadata.ReadOnly:
|
|
return ErrReadOnly
|
|
default:
|
|
p.s.Value.Clear()
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// DefaultValue returns the default value of p.
|
|
func (p preference[T]) DefaultValue() T {
|
|
return p.s.Default
|
|
}
|
|
|
|
// SetDefaultValue sets the default value of p.
|
|
func (p *preference[T]) SetDefaultValue(def T) {
|
|
p.s.Default = def
|
|
}
|
|
|
|
// IsManaged reports whether p is managed via MDM, Group Policy, or similar means.
|
|
func (p preference[T]) IsManaged() bool {
|
|
return p.s.Metadata.Managed
|
|
}
|
|
|
|
// SetManagedValue configures the preference with the specified value
|
|
// and marks the preference as managed.
|
|
func (p *preference[T]) SetManagedValue(val T) {
|
|
p.s.Value.Set(val)
|
|
p.s.Metadata.Managed = true
|
|
}
|
|
|
|
// ClearManaged clears the managed flag of the preference without altering its value.
|
|
func (p *preference[T]) ClearManaged() {
|
|
p.s.Metadata.Managed = false
|
|
}
|
|
|
|
// IsReadOnly reports whether p is read-only and cannot be changed by user.
|
|
func (p preference[T]) IsReadOnly() bool {
|
|
return p.s.Metadata.ReadOnly || p.s.Metadata.Managed
|
|
}
|
|
|
|
// SetReadOnly sets the read-only status of p, preventing changes by a user if set to true.
|
|
func (p *preference[T]) SetReadOnly(readonly bool) {
|
|
p.s.Metadata.ReadOnly = readonly
|
|
}
|
|
|
|
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
|
|
func (p preference[T]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
|
|
return jsonv2.MarshalEncode(out, &p.s, opts)
|
|
}
|
|
|
|
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
|
|
func (p *preference[T]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
|
|
return jsonv2.UnmarshalDecode(in, &p.s, opts)
|
|
}
|
|
|
|
// MarshalJSON implements [json.Marshaler].
|
|
func (p preference[T]) MarshalJSON() ([]byte, error) {
|
|
return jsonv2.Marshal(p) // uses MarshalJSONV2
|
|
}
|
|
|
|
// UnmarshalJSON implements [json.Unmarshaler].
|
|
func (p *preference[T]) UnmarshalJSON(b []byte) error {
|
|
return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONV2
|
|
}
|