mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-21 22:51:09 +02:00
* identity: Ensure state is changed on activation This PR introduces some changes to the way activation flags are processed in Vault. Rather than reaching into subsystems and modifying state from the activationflags package, each plugin can now register its own ActivationFunc. Updates to activation flags now trigger the the feature's ActivationFunc, which can encapsulate the associated subsystem state. We include a few bugfixes and minor cosmetic changes, like updates to log lines and godocs. * Check for nil system backend * Move deduplication activation to common file * Add identity dedup activation log lines * Make interface methods clearer * Clean up some comments * More cleanups * fixup! More cleanups * fixup! More cleanups
212 lines
6.3 KiB
Go
212 lines
6.3 KiB
Go
// 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
|
|
}
|