tailscale/util/syspolicy/source/policy_source.go
Nick Khyl aeb15dea30 util/syspolicy/source: add package for reading policy settings from external stores
We add package defining interfaces for policy stores, enabling creation of policy sources
and reading settings from them. It includes a Windows-specific PlatformPolicyStore for GP and MDM
policies stored in the Registry, and an in-memory TestStore for testing purposes.

We also include an internal package that tracks and reports policy usage metrics when a policy setting
is read from a store. Initially, it will be used only on Windows and Android, as macOS, iOS, and tvOS
report their own metrics. However, we plan to use it across all platforms eventually.

Updates #12687

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-09-03 14:51:14 -05:00

147 lines
5.5 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package source defines interfaces for policy stores,
// facilitates the creation of policy sources, and provides
// functionality for reading policy settings from these sources.
package source
import (
"cmp"
"errors"
"fmt"
"io"
"tailscale.com/types/lazy"
"tailscale.com/util/syspolicy/setting"
)
// ErrStoreClosed is an error returned when attempting to use a [Store] after it
// has been closed.
var ErrStoreClosed = errors.New("the policy store has been closed")
// Store provides methods to read system policy settings from OS-specific storage.
// Implementations must be concurrency-safe, and may also implement
// [Lockable], [Changeable], [Expirable] and [io.Closer].
//
// If a [Store] implementation also implements [io.Closer],
// it will be called by the package to release the resources
// when the store is no longer needed.
type Store interface {
// ReadString returns the value of a [setting.StringValue] with the specified key,
// an [setting.ErrNotConfigured] if the policy setting is not configured, or
// an error on failure.
ReadString(key setting.Key) (string, error)
// ReadUInt64 returns the value of a [setting.IntegerValue] with the specified key,
// an [setting.ErrNotConfigured] if the policy setting is not configured, or
// an error on failure.
ReadUInt64(key setting.Key) (uint64, error)
// ReadBoolean returns the value of a [setting.BooleanValue] with the specified key,
// an [setting.ErrNotConfigured] if the policy setting is not configured, or
// an error on failure.
ReadBoolean(key setting.Key) (bool, error)
// ReadStringArray returns the value of a [setting.StringListValue] with the specified key,
// an [setting.ErrNotConfigured] if the policy setting is not configured, or
// an error on failure.
ReadStringArray(key setting.Key) ([]string, error)
}
// Lockable is an optional interface that [Store] implementations may support.
// Locking a [Store] is not mandatory as [Store] must be concurrency-safe,
// but is recommended to avoid issues where consecutive read calls for related
// policies might return inconsistent results if a policy change occurs between
// the calls. Implementations may use locking to pre-read policies or for
// similar performance optimizations.
type Lockable interface {
// Lock acquires a read lock on the policy store,
// ensuring the store's state remains unchanged while locked.
// Multiple readers can hold the lock simultaneously.
// It returns an error if the store cannot be locked.
Lock() error
// Unlock unlocks the policy store.
// It is a run-time error if the store is not locked on entry to Unlock.
Unlock()
}
// Changeable is an optional interface that [Store] implementations may support
// if the policy settings they contain can be externally changed after being initially read.
type Changeable interface {
// RegisterChangeCallback adds a function that will be called
// whenever there's a policy change in the [Store].
// The returned function can be used to unregister the callback.
RegisterChangeCallback(callback func()) (unregister func(), err error)
}
// Expirable is an optional interface that [Store] implementations may support
// if they can be externally closed or otherwise become invalid while in use.
type Expirable interface {
// Done returns a channel that is closed when the policy [Store] should no longer be used.
// It should return nil if the store never expires.
Done() <-chan struct{}
}
// Source represents a named source of policy settings for a given [setting.PolicyScope].
type Source struct {
name string
scope setting.PolicyScope
store Store
origin *setting.Origin
lazyReader lazy.SyncValue[*Reader]
}
// NewSource returns a new [Source] with the specified name, scope, and store.
func NewSource(name string, scope setting.PolicyScope, store Store) *Source {
return &Source{name: name, scope: scope, store: store, origin: setting.NewNamedOrigin(name, scope)}
}
// Name reports the name of the policy source.
func (s *Source) Name() string {
return s.name
}
// Scope reports the management scope of the policy source.
func (s *Source) Scope() setting.PolicyScope {
return s.scope
}
// Reader returns a [Reader] that reads from this source's [Store].
func (s *Source) Reader() (*Reader, error) {
return s.lazyReader.GetErr(func() (*Reader, error) {
return newReader(s.store, s.origin)
})
}
// Description returns a formatted string with the scope and name of this policy source.
// It can be used for logging or display purposes.
func (s *Source) Description() string {
if s.name != "" {
return fmt.Sprintf("%s (%v)", s.name, s.Scope())
}
return s.Scope().String()
}
// Compare returns an integer comparing s and s2
// by their precedence, following the "last-wins" model.
// The result will be:
//
// -1 if policy settings from s should be processed before policy settings from s2;
// +1 if policy settings from s should be processed after policy settings from s2, overriding s2;
// 0 if the relative processing order of policy settings in s and s2 is unspecified.
func (s *Source) Compare(s2 *Source) int {
return cmp.Compare(s2.Scope().Kind(), s.Scope().Kind())
}
// Close closes the [Source] and the underlying [Store].
func (s *Source) Close() error {
// The [Reader], if any, owns the [Store].
if reader, _ := s.lazyReader.GetErr(func() (*Reader, error) { return nil, ErrStoreClosed }); reader != nil {
return reader.Close()
}
// Otherwise, it is our responsibility to close it.
if closer, ok := s.store.(io.Closer); ok {
return closer.Close()
}
return nil
}