Skip to content

Commit

Permalink
Port activation flags with dynamic registration (#29237)
Browse files Browse the repository at this point in the history
  • Loading branch information
biazmoreira authored Jan 9, 2025
1 parent 357b294 commit ab4e8da
Show file tree
Hide file tree
Showing 8 changed files with 394 additions and 3 deletions.
3 changes: 3 additions & 0 deletions changelog/29237.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
core: Add activation flags. A mechanism for users to opt in to new functionality at a convenient time. Previously used only in Enterprise for SecretSync, activation flags are now available in CE for future features to use.
```
142 changes: 142 additions & 0 deletions helper/activationflags/activation_flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// 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"
)

type FeatureActivationFlags struct {
activationFlagsLock sync.RWMutex
storage logical.Storage
activationFlags map[string]bool
}

func NewFeatureActivationFlags() *FeatureActivationFlags {
return &FeatureActivationFlags{
activationFlags: map[string]bool{},
}
}

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) {
f.activationFlagsLock.Lock()
defer f.activationFlagsLock.Unlock()

// Don't use nil slice declaration, we want the JSON to show "[]" instead of null
activated := []string{}

if f.storage == nil {
return activated, 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 activated, nil
}

var activationFlags map[string]bool
if err := entry.DecodeJSON(&activationFlags); err != nil {
return nil, fmt.Errorf("failed to decode activation flags from storage: %w", err)
}

// Update the in-memory flags after loading the latest values from storage
f.activationFlags = activationFlags

for flag, set := range activationFlags {
if set {
activated = append(activated, flag)
}
}

return activated, nil
}

// Write 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) Write(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
}
7 changes: 7 additions & 0 deletions vault/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import (
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/audit"
"github.com/hashicorp/vault/command/server"
"github.com/hashicorp/vault/helper/activationflags"
"github.com/hashicorp/vault/helper/identity/mfa"
"github.com/hashicorp/vault/helper/locking"
"github.com/hashicorp/vault/helper/metricsutil"
Expand Down Expand Up @@ -739,6 +740,9 @@ type Core struct {
clusterAddrBridge *raft.ClusterAddrBridge

censusManager *CensusManager

// Activation flags for enterprise features that require a one-time activation
FeatureActivationFlags *activationflags.FeatureActivationFlags
}

func (c *Core) ActiveNodeClockSkewMillis() int64 {
Expand Down Expand Up @@ -1448,11 +1452,14 @@ func (c *Core) configureLogicalBackends(backends map[string]logical.Factory, log
// System
logicalBackends[mountTypeSystem] = func(ctx context.Context, config *logical.BackendConfig) (logical.Backend, error) {
sysBackendLogger := logger.Named("system")

c.AddLogger(sysBackendLogger)
b := NewSystemBackend(c, sysBackendLogger, config)

if err := b.Setup(ctx, config); err != nil {
return nil, err
}

return b, nil
}

Expand Down
3 changes: 3 additions & 0 deletions vault/core_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/helper/activationflags"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/limits"
"github.com/hashicorp/vault/sdk/helper/license"
Expand Down Expand Up @@ -59,6 +60,8 @@ func coreInit(c *Core, conf *CoreConfig) error {
c.physical = physical.NewStorageEncoding(c.physical)
}

c.FeatureActivationFlags = activationflags.NewFeatureActivationFlags()

return nil
}

Expand Down
1 change: 1 addition & 0 deletions vault/logical_system.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf
b.Backend.Paths = append(b.Backend.Paths, b.experimentPaths()...)
b.Backend.Paths = append(b.Backend.Paths, b.introspectionPaths()...)
b.Backend.Paths = append(b.Backend.Paths, b.wellKnownPaths()...)
b.Backend.Paths = append(b.Backend.Paths, b.activationFlagsPaths()...)

if core.rawEnabled {
b.Backend.Paths = append(b.Backend.Paths, b.rawPaths()...)
Expand Down
140 changes: 140 additions & 0 deletions vault/logical_system_activation_flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package vault

import (
"context"
"fmt"
"slices"
"strings"

"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)

const (
paramFeatureName = "feature_name"
descFeatureName = "The name of the feature to be activated."
summaryList = "Returns the available and activated activation-flagged features."
summaryUpdate = "Activate a flagged feature."

prefixActivationFlags = "activation-flags"
verbActivationFlagsActivate = "activate"
verbActivationFlagsDeactivate = "deactivate"

fieldActivated = "activated"
fieldUnactivated = "unactivated"

helpSynopsis = "Returns information about Vault's features that require a one-time activation step."
helpDescription = `
This path responds to the following HTTP methods.
GET /
Returns the available and activated activation-flags.
PUT|POST /<feature-name>/activate
Activates the specified feature. Cannot be undone.`
)

// Register CRUD functions dynamically.
// These variables should only be mutated during initialization or server construction.
// It is unsafe to modify them once the Vault core is running.
var (
readActivationFlag = func(ctx context.Context, b *SystemBackend, req *logical.Request, fd *framework.FieldData) (*logical.Response, error) {
return b.readActivationFlag(ctx, req, fd)
}

writeActivationFlag = func(ctx context.Context, b *SystemBackend, req *logical.Request, fd *framework.FieldData, isActivate bool) (*logical.Response, error) {
return b.writeActivationFlagWrite(ctx, req, fd, isActivate)
}
)

func (b *SystemBackend) activationFlagsPaths() []*framework.Path {
return []*framework.Path{
{
Pattern: fmt.Sprintf("%s$", prefixActivationFlags),
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "read",
OperationSuffix: prefixActivationFlags,
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.handleActivationFlagRead,
Summary: summaryList,
},
},
HelpSynopsis: helpSynopsis,
HelpDescription: helpDescription,
},
{
Pattern: fmt.Sprintf("%s/%s/%s", prefixActivationFlags, "activation-test", verbActivationFlagsActivate),
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: prefixActivationFlags,
OperationVerb: verbActivationFlagsActivate,
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: b.handleActivationFlagsActivate,
ForwardPerformanceSecondary: true,
ForwardPerformanceStandby: true,
Summary: summaryUpdate,
},
},
HelpSynopsis: helpSynopsis,
HelpDescription: helpDescription,
},
}
}

func (b *SystemBackend) handleActivationFlagRead(ctx context.Context, req *logical.Request, fd *framework.FieldData) (*logical.Response, error) {
return readActivationFlag(ctx, b, req, fd)
}

func (b *SystemBackend) handleActivationFlagsActivate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
return writeActivationFlag(ctx, b, req, data, true)
}

func (b *SystemBackend) readActivationFlag(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
activationFlags, err := b.Core.FeatureActivationFlags.Get(ctx)
if err != nil {
return nil, err
}

return b.activationFlagsToResponse(activationFlags), nil
}

func (b *SystemBackend) writeActivationFlagWrite(ctx context.Context, req *logical.Request, _ *framework.FieldData, isActivate bool) (*logical.Response, error) {
// We need to manually parse out the feature_name from the path because we can't use FieldSchema parameters
// in the path to make generic endpoints. We need each activation-flag path to be a separate endpoint.
// Path starts out as activation-flags/<feature_name>/verb
// Removes activation-flags/ from the path
trimPrefix := strings.TrimPrefix(req.Path, prefixActivationFlags+"/")
// Removes /verb from the path
featureName := trimPrefix[:strings.LastIndex(trimPrefix, "/")]

err := b.Core.FeatureActivationFlags.Write(ctx, featureName, isActivate)
if err != nil {
return nil, fmt.Errorf("failed to write new activation flags: %w", err)
}

// We read back the value after writing it to storage so that we can try forcing a cache update right away.
// If this fails, it's still okay to proceed as the write has been successful and the cache will get updated
// at the time of an endpoint getting called. However, we can only return the one feature name we just activated
// in the response since the read to retrieve any others did not succeed.
activationFlags, err := b.Core.FeatureActivationFlags.Get(ctx)
if err != nil {
resp := b.activationFlagsToResponse([]string{featureName})
return resp, fmt.Errorf("failed to read activation-flags back after write: %w", err)
}

return b.activationFlagsToResponse(activationFlags), nil
}

func (b *SystemBackend) activationFlagsToResponse(activationFlags []string) *logical.Response {
slices.Sort(activationFlags)
return &logical.Response{
Data: map[string]interface{}{
fieldActivated: activationFlags,
},
}
}
Loading

0 comments on commit ab4e8da

Please sign in to comment.