// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package activationflags import ( "context" "fmt" "maps" "sync" "github.com/hashicorp/vault/sdk/logical" ) const ( storagePathActivationFlags = "activation-flags" IdentityDeduplication = "force-identity-deduplication" ) // FeatureActivationFlags stores activation flags in a local map and storage. // All operations on internal state need to hold read/write locks for the // duration of access. type FeatureActivationFlags struct { activationFlagsLock sync.RWMutex storage logical.Storage activationFlags map[string]bool } // ActivationManager is a lightweight interface for managing and inspecting // feature activation flags. type ActivationManager interface { IsActivationFlagEnabled(featureName string) bool SetActivationFlagEnabled(ctx context.Context, featureName string, activate bool) error } // NewFeatureActivationFlags is a constructor for FeatureActivationFlags. func NewFeatureActivationFlags() *FeatureActivationFlags { return &FeatureActivationFlags{ activationFlags: map[string]bool{}, } } // Initialize loads the activation flags from storage into the in-memory cache. func (f *FeatureActivationFlags) Initialize(ctx context.Context, storage logical.Storage) error { f.activationFlagsLock.Lock() defer f.activationFlagsLock.Unlock() if storage == nil { return fmt.Errorf("unable to access storage") } f.storage = storage entry, err := f.storage.Get(ctx, storagePathActivationFlags) if err != nil { return fmt.Errorf("failed to get activation flags from storage: %w", err) } if entry == nil { f.activationFlags = map[string]bool{} return nil } var activationFlags map[string]bool if err := entry.DecodeJSON(&activationFlags); err != nil { return fmt.Errorf("failed to decode activation flags from storage: %w", err) } f.activationFlags = activationFlags return nil } // Get is the helper function called by the activation-flags API read endpoint. This reads the // actual values from storage, then updates the in-memory cache of the activation-flags. It // returns a slice of the feature names which have already been activated. func (f *FeatureActivationFlags) Get(ctx context.Context) ([]string, error) { // Don't use nil slice declaration, we want the JSON to show "[]" instead of null activated := []string{} _, err := f.ReloadFlagsFromStorage(ctx) if err != nil { return activated, err } f.activationFlagsLock.Lock() defer f.activationFlagsLock.Unlock() for flag, set := range f.activationFlags { if set { activated = append(activated, flag) } } return activated, nil } // ReloadFlagsFromStorage loads the activation flags from storage and updates the in-memory cache. func (f *FeatureActivationFlags) ReloadFlagsFromStorage(ctx context.Context) (map[string]bool, error) { f.activationFlagsLock.Lock() defer f.activationFlagsLock.Unlock() if f.storage == nil { return map[string]bool{}, nil } entry, err := f.storage.Get(ctx, storagePathActivationFlags) if err != nil { return nil, fmt.Errorf("failed to get activation flags from storage: %w", err) } if entry == nil { return map[string]bool{}, nil } var storageActivationFlags map[string]bool if err := entry.DecodeJSON(&storageActivationFlags); err != nil { return nil, fmt.Errorf("failed to decode activation flags from storage: %w", err) } // State Change Logic for Flags // // This logic determines changes to flags, but it does NOT account for flags that have been deleted. // As of this writing, flag removal is not supported for activation flags. // // Valid State Transitions: // 1. Unset (new flag) -> Active // 2. Active -> Inactive // 3. Inactive -> Active // // Behavior notes: // - If a flag does not exist in-memory (`!ok`), it is treated as a new flag. // Nodes should only react to the new flag if its state is being set to "Active". // - If a flag exists in-memory, any change in its value (e.g., Active -> Inactive) is considered valid // and is marked as a state change. // // The resulting `changedFlags` map will store the flags with their new values if they meet the above criteria. changedFlags := map[string]bool{} for flg, v := range storageActivationFlags { oldValue, ok := f.activationFlags[flg] switch { // New flag: handle only if transitioning to "Active (true)" case !ok && v: changedFlags[flg] = v default: // Existing flag: detect state change if oldValue != v { changedFlags[flg] = v } } } // Update the in-memory flags after loading the latest values from storage f.activationFlags = storageActivationFlags return changedFlags, nil } // SetActivationFlagEnabled is the helper function called by the // activation-flags API write endpoint. This stores the boolean value for the // activation-flag feature name into Vault storage across the cluster and // updates the in-memory cache upon success. func (f *FeatureActivationFlags) SetActivationFlagEnabled(ctx context.Context, featureName string, activate bool) (err error) { f.activationFlagsLock.Lock() defer f.activationFlagsLock.Unlock() if f.storage == nil { return fmt.Errorf("unable to access storage") } activationFlags := f.activationFlags clonedFlags := maps.Clone(f.activationFlags) clonedFlags[featureName] = activate // The cloned flags are updated but the in-memory state is only updated on success of the storage update. defer func() { if err == nil { activationFlags[featureName] = activate } }() entry, err := logical.StorageEntryJSON(storagePathActivationFlags, clonedFlags) if err != nil { return fmt.Errorf("failed to marshal object to JSON: %w", err) } err = f.storage.Put(ctx, entry) if err != nil { return fmt.Errorf("failed to save object in storage: %w", err) } return nil } // IsActivationFlagEnabled is true if the specified flag is enabled in the core. func (f *FeatureActivationFlags) IsActivationFlagEnabled(featureName string) bool { f.activationFlagsLock.RLock() defer f.activationFlagsLock.RUnlock() activated, ok := f.activationFlags[featureName] return ok && activated } // ActivateInMem is used to set the activation flag in-memory only for a feature // in tests func (f *FeatureActivationFlags) ActivateInMem(featureName string, activate bool) { f.activationFlagsLock.Lock() defer f.activationFlagsLock.Unlock() f.activationFlags[featureName] = activate }