mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-30 05:25:35 +00:00
aeb15dea30
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>
395 lines
11 KiB
Go
395 lines
11 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package source
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"slices"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"tailscale.com/util/mak"
|
|
"tailscale.com/util/set"
|
|
"tailscale.com/util/syspolicy/internal/loggerx"
|
|
"tailscale.com/util/syspolicy/internal/metrics"
|
|
"tailscale.com/util/syspolicy/setting"
|
|
)
|
|
|
|
// Reader reads all configured policy settings from a given [Store].
|
|
// It registers a change callback with the [Store] and maintains the current version
|
|
// of the [setting.Snapshot] by lazily re-reading policy settings from the [Store]
|
|
// whenever a new settings snapshot is requested with [Reader.GetSettings].
|
|
// It is safe for concurrent use.
|
|
type Reader struct {
|
|
store Store
|
|
origin *setting.Origin
|
|
settings []*setting.Definition
|
|
unregisterChangeNotifier func()
|
|
doneCh chan struct{} // closed when [Reader] is closed.
|
|
|
|
mu sync.Mutex
|
|
closing bool
|
|
upToDate bool
|
|
lastPolicy *setting.Snapshot
|
|
sessions set.HandleSet[*ReadingSession]
|
|
}
|
|
|
|
// newReader returns a new [Reader] that reads policy settings from a given [Store].
|
|
// The returned reader takes ownership of the store. If the store implements [io.Closer],
|
|
// the returned reader will close the store when it is closed.
|
|
func newReader(store Store, origin *setting.Origin) (*Reader, error) {
|
|
settings, err := setting.Definitions()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if expirable, ok := store.(Expirable); ok {
|
|
select {
|
|
case <-expirable.Done():
|
|
return nil, ErrStoreClosed
|
|
default:
|
|
}
|
|
}
|
|
|
|
reader := &Reader{store: store, origin: origin, settings: settings, doneCh: make(chan struct{})}
|
|
if changeable, ok := store.(Changeable); ok {
|
|
// We should subscribe to policy change notifications first before reading
|
|
// the policy settings from the store. This way we won't miss any notifications.
|
|
if reader.unregisterChangeNotifier, err = changeable.RegisterChangeCallback(reader.onPolicyChange); err != nil {
|
|
// Errors registering policy change callbacks are non-fatal.
|
|
// TODO(nickkhyl): implement a background policy refresh every X minutes?
|
|
loggerx.Errorf("failed to register %v policy change callback: %v", origin, err)
|
|
}
|
|
}
|
|
|
|
if _, err := reader.reload(true); err != nil {
|
|
if reader.unregisterChangeNotifier != nil {
|
|
reader.unregisterChangeNotifier()
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if expirable, ok := store.(Expirable); ok {
|
|
if waitCh := expirable.Done(); waitCh != nil {
|
|
go func() {
|
|
select {
|
|
case <-waitCh:
|
|
reader.Close()
|
|
case <-reader.doneCh:
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
return reader, nil
|
|
}
|
|
|
|
// GetSettings returns the current [*setting.Snapshot],
|
|
// re-reading it from 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 {
|
|
r.mu.Lock()
|
|
upToDate, lastPolicy := r.upToDate, r.lastPolicy
|
|
r.mu.Unlock()
|
|
if upToDate {
|
|
return lastPolicy
|
|
}
|
|
|
|
policy, err := r.reload(false)
|
|
if err != nil {
|
|
// If the policy fails to reload completely, log an error and return the last cached version.
|
|
// However, errors related to individual policy items are always
|
|
// propagated to callers when they fetch those settings.
|
|
loggerx.Errorf("failed to reload %v policy: %v", r.origin, err)
|
|
}
|
|
return policy
|
|
}
|
|
|
|
// ReadSettings reads policy settings from the underlying [Store] even if no
|
|
// changes were detected. It returns the new [*setting.Snapshot],nil on
|
|
// success or an undefined snapshot (possibly `nil`) along with a non-`nil`
|
|
// error in case of failure.
|
|
func (r *Reader) ReadSettings() (*setting.Snapshot, error) {
|
|
return r.reload(true)
|
|
}
|
|
|
|
// reload is like [Reader.ReadSettings], but allows specifying whether to re-read
|
|
// an unchanged policy, and returns the last [*setting.Snapshot] if the read fails.
|
|
func (r *Reader) reload(force bool) (*setting.Snapshot, error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.upToDate && !force {
|
|
return r.lastPolicy, nil
|
|
}
|
|
|
|
if lockable, ok := r.store.(Lockable); ok {
|
|
if err := lockable.Lock(); err != nil {
|
|
return r.lastPolicy, err
|
|
}
|
|
defer lockable.Unlock()
|
|
}
|
|
|
|
r.upToDate = true
|
|
|
|
metrics.Reset(r.origin)
|
|
|
|
var m map[setting.Key]setting.RawItem
|
|
if lastPolicyCount := r.lastPolicy.Len(); lastPolicyCount > 0 {
|
|
m = make(map[setting.Key]setting.RawItem, lastPolicyCount)
|
|
}
|
|
for _, s := range r.settings {
|
|
if !r.origin.Scope().IsConfigurableSetting(s) {
|
|
// Skip settings that cannot be configured in the current scope.
|
|
continue
|
|
}
|
|
|
|
val, err := readPolicySettingValue(r.store, s)
|
|
if err != nil && (errors.Is(err, setting.ErrNoSuchKey) || errors.Is(err, setting.ErrNotConfigured)) {
|
|
metrics.ReportNotConfigured(r.origin, s)
|
|
continue
|
|
}
|
|
|
|
if err == nil {
|
|
metrics.ReportConfigured(r.origin, s, val)
|
|
} else {
|
|
metrics.ReportError(r.origin, s, err)
|
|
}
|
|
|
|
// If there's an error reading a single policy, such as a value type mismatch,
|
|
// we'll wrap the error to preserve its text and return it
|
|
// whenever someone attempts to fetch the value.
|
|
// Otherwise, the errorText will be nil.
|
|
errorText := setting.MaybeErrorText(err)
|
|
item := setting.RawItemWith(val, errorText, r.origin)
|
|
mak.Set(&m, s.Key(), item)
|
|
}
|
|
|
|
newPolicy := setting.NewSnapshot(m, setting.SummaryWith(r.origin))
|
|
if r.lastPolicy == nil || !newPolicy.EqualItems(r.lastPolicy) {
|
|
r.lastPolicy = newPolicy
|
|
}
|
|
return r.lastPolicy, nil
|
|
}
|
|
|
|
// ReadingSession is like [Reader], but with a channel that's written
|
|
// to when there's a policy change, and closed when the session is terminated.
|
|
type ReadingSession struct {
|
|
reader *Reader
|
|
policyChangedCh chan struct{} // 1-buffered channel
|
|
handle set.Handle // in the reader.sessions
|
|
closeInternal func()
|
|
}
|
|
|
|
// OpenSession opens and returns a new session to r, allowing the caller
|
|
// to get notified whenever a policy change is reported by the [source.Store],
|
|
// or an [ErrStoreClosed] if the reader has already been closed.
|
|
func (r *Reader) OpenSession() (*ReadingSession, error) {
|
|
session := &ReadingSession{
|
|
reader: r,
|
|
policyChangedCh: make(chan struct{}, 1),
|
|
}
|
|
session.closeInternal = sync.OnceFunc(func() { close(session.policyChangedCh) })
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.closing {
|
|
return nil, ErrStoreClosed
|
|
}
|
|
session.handle = r.sessions.Add(session)
|
|
return session, nil
|
|
}
|
|
|
|
// GetSettings is like [Reader.GetSettings].
|
|
func (s *ReadingSession) GetSettings() *setting.Snapshot {
|
|
return s.reader.GetSettings()
|
|
}
|
|
|
|
// ReadSettings is like [Reader.ReadSettings].
|
|
func (s *ReadingSession) ReadSettings() (*setting.Snapshot, error) {
|
|
return s.reader.ReadSettings()
|
|
}
|
|
|
|
// PolicyChanged returns a channel that's written to when
|
|
// there's a policy change, closed when the session is terminated.
|
|
func (s *ReadingSession) PolicyChanged() <-chan struct{} {
|
|
return s.policyChangedCh
|
|
}
|
|
|
|
// Close unregisters this session with the [Reader].
|
|
func (s *ReadingSession) Close() {
|
|
s.reader.mu.Lock()
|
|
delete(s.reader.sessions, s.handle)
|
|
s.closeInternal()
|
|
s.reader.mu.Unlock()
|
|
}
|
|
|
|
// onPolicyChange handles a policy change notification from the [Store],
|
|
// invalidating the current [setting.Snapshot] in r,
|
|
// and notifying the active [ReadingSession]s.
|
|
func (r *Reader) onPolicyChange() {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.upToDate = false
|
|
for _, s := range r.sessions {
|
|
select {
|
|
case s.policyChangedCh <- struct{}{}:
|
|
// Notified.
|
|
default:
|
|
// 1-buffered channel is full, meaning that another policy change
|
|
// notification is already en route.
|
|
}
|
|
}
|
|
}
|
|
|
|
// Close closes the store reader and the underlying store.
|
|
func (r *Reader) Close() error {
|
|
r.mu.Lock()
|
|
if r.closing {
|
|
r.mu.Unlock()
|
|
return nil
|
|
}
|
|
r.closing = true
|
|
r.mu.Unlock()
|
|
|
|
if r.unregisterChangeNotifier != nil {
|
|
r.unregisterChangeNotifier()
|
|
r.unregisterChangeNotifier = nil
|
|
}
|
|
|
|
if closer, ok := r.store.(io.Closer); ok {
|
|
if err := closer.Close(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
r.store = nil
|
|
|
|
close(r.doneCh)
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
for _, c := range r.sessions {
|
|
c.closeInternal()
|
|
}
|
|
r.sessions = nil
|
|
return nil
|
|
}
|
|
|
|
// Done returns a channel that is closed when the reader is closed.
|
|
func (r *Reader) Done() <-chan struct{} {
|
|
return r.doneCh
|
|
}
|
|
|
|
// ReadableSource is a [Source] open for reading.
|
|
type ReadableSource struct {
|
|
*Source
|
|
*ReadingSession
|
|
}
|
|
|
|
// Close closes the underlying [ReadingSession].
|
|
func (s ReadableSource) Close() {
|
|
s.ReadingSession.Close()
|
|
}
|
|
|
|
// ReadableSources is a slice of [ReadableSource].
|
|
type ReadableSources []ReadableSource
|
|
|
|
// Contains reports whether s contains the specified source.
|
|
func (s ReadableSources) Contains(source *Source) bool {
|
|
return s.IndexOf(source) != -1
|
|
}
|
|
|
|
// IndexOf returns position of the specified source in s, or -1
|
|
// if the source does not exist.
|
|
func (s ReadableSources) IndexOf(source *Source) int {
|
|
return slices.IndexFunc(s, func(rs ReadableSource) bool {
|
|
return rs.Source == source
|
|
})
|
|
}
|
|
|
|
// InsertionIndexOf returns the position at which source can be inserted
|
|
// to maintain the sorted order of the readableSources.
|
|
// The return value is unspecified if s is not sorted on entry to InsertionIndexOf.
|
|
func (s ReadableSources) InsertionIndexOf(source *Source) int {
|
|
// Insert new sources after any existing sources with the same precedence,
|
|
// and just before the first source with higher precedence.
|
|
// Just like stable sort, but for insertion.
|
|
// It's okay to use linear search as insertions are rare
|
|
// and we never have more than just a few policy sources.
|
|
higherPrecedence := func(rs ReadableSource) bool { return rs.Compare(source) > 0 }
|
|
if i := slices.IndexFunc(s, higherPrecedence); i != -1 {
|
|
return i
|
|
}
|
|
return len(s)
|
|
}
|
|
|
|
// StableSort sorts [ReadableSource] in s by precedence, so that policy
|
|
// settings from sources with higher precedence (e.g., [DeviceScope])
|
|
// will be read and merged last, overriding any policy settings with
|
|
// the same keys configured in sources with lower precedence
|
|
// (e.g., [CurrentUserScope]).
|
|
func (s *ReadableSources) StableSort() {
|
|
sort.SliceStable(*s, func(i, j int) bool {
|
|
return (*s)[i].Source.Compare((*s)[j].Source) < 0
|
|
})
|
|
}
|
|
|
|
// DeleteAt closes and deletes the i-th source from s.
|
|
func (s *ReadableSources) DeleteAt(i int) {
|
|
(*s)[i].Close()
|
|
*s = slices.Delete(*s, i, i+1)
|
|
}
|
|
|
|
// Close closes and deletes all sources in s.
|
|
func (s *ReadableSources) Close() {
|
|
for _, s := range *s {
|
|
s.Close()
|
|
}
|
|
*s = nil
|
|
}
|
|
|
|
func readPolicySettingValue(store Store, s *setting.Definition) (value any, err error) {
|
|
switch key := s.Key(); s.Type() {
|
|
case setting.BooleanValue:
|
|
return store.ReadBoolean(key)
|
|
case setting.IntegerValue:
|
|
return store.ReadUInt64(key)
|
|
case setting.StringValue:
|
|
return store.ReadString(key)
|
|
case setting.StringListValue:
|
|
return store.ReadStringArray(key)
|
|
case setting.PreferenceOptionValue:
|
|
s, err := store.ReadString(key)
|
|
if err == nil {
|
|
var value setting.PreferenceOption
|
|
if err = value.UnmarshalText([]byte(s)); err == nil {
|
|
return value, nil
|
|
}
|
|
}
|
|
return setting.ShowChoiceByPolicy, err
|
|
case setting.VisibilityValue:
|
|
s, err := store.ReadString(key)
|
|
if err == nil {
|
|
var value setting.Visibility
|
|
if err = value.UnmarshalText([]byte(s)); err == nil {
|
|
return value, nil
|
|
}
|
|
}
|
|
return setting.VisibleByPolicy, err
|
|
case setting.DurationValue:
|
|
s, err := store.ReadString(key)
|
|
if err == nil {
|
|
var value time.Duration
|
|
if value, err = time.ParseDuration(s); err == nil {
|
|
return value, nil
|
|
}
|
|
}
|
|
return nil, err
|
|
default:
|
|
return nil, fmt.Errorf("%w: unsupported setting type: %v", setting.ErrTypeMismatch, s.Type())
|
|
}
|
|
}
|