tailscale/util/syspolicy/source/policy_reader.go

395 lines
11 KiB
Go
Raw Normal View History

// 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())
}
}