sys: adds identity_token_key to mounts/auth for enable/tune (#24962)

* sys: adds identity_token_key to mounts/auth for enable/tune

* adds changelog

* adds godoc on new tests

* adds function for identityStoreKeyExists

* use read lock, remove helper func

* tune test in logical_system_test, remove router access method

* fix key existence check in namespaces
This commit is contained in:
Austin Gebauer
2024-01-22 15:28:11 -08:00
committed by GitHub
parent 677d98a821
commit 76a62d5997
7 changed files with 365 additions and 1 deletions

View File

@@ -272,6 +272,8 @@ type MountConfigInput struct {
PluginVersion string `json:"plugin_version,omitempty"`
UserLockoutConfig *UserLockoutConfigInput `json:"user_lockout_config,omitempty"`
DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"`
IdentityTokenKey string `json:"identity_token_key,omitempty"`
// Deprecated: This field will always be blank for newer server responses.
PluginName string `json:"plugin_name,omitempty" mapstructure:"plugin_name"`
}
@@ -305,6 +307,7 @@ type MountConfigOutput struct {
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
UserLockoutConfig *UserLockoutConfigOutput `json:"user_lockout_config,omitempty"`
DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"`
IdentityTokenKey string `json:"identity_token_key,omitempty"`
// Deprecated: This field will always be blank for newer server responses.
PluginName string `json:"plugin_name,omitempty" mapstructure:"plugin_name"`

3
changelog/24962.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
sys: adds configuration of the key used to sign plugin identity tokens during mount enable and tune
```

View File

@@ -1288,7 +1288,7 @@ func TestOIDC_pathOIDCKeyExistenceCheck(t *testing.T) {
t.Fatalf("Expected existence check to return false but instead returned: %t", exists)
}
// Populte storage with a namedKey
// Populate storage with a namedKey
namedKey := &namedKey{}
entry, _ := logical.StorageEntryJSON(namedKeyConfigPath+keyName, namedKey)
if err := storage.Put(ctx, entry); err != nil {

View File

@@ -1277,6 +1277,9 @@ func (b *SystemBackend) mountInfo(ctx context.Context, entry *MountEntry) map[st
if rawVal, ok := entry.synthesizedConfigCache.Load("allowed_managed_keys"); ok {
entryConfig["allowed_managed_keys"] = rawVal.([]string)
}
if rawVal, ok := entry.synthesizedConfigCache.Load("identity_token_key"); ok {
entryConfig["identity_token_key"] = rawVal.(string)
}
if entry.Table == credentialTableType {
entryConfig["token_type"] = entry.Config.TokenType.String()
}
@@ -1498,6 +1501,26 @@ func (b *SystemBackend) handleMount(ctx context.Context, req *logical.Request, d
config.DelegatedAuthAccessors = apiConfig.DelegatedAuthAccessors
}
if apiConfig.IdentityTokenKey != "" {
storage := b.Core.router.MatchingStorageByAPIPath(ctx, mountPathIdentity)
if storage == nil {
return nil, errors.New("failed to find identity storage")
}
identityStore := b.Core.IdentityStore()
identityStore.oidcLock.RLock()
defer identityStore.oidcLock.RUnlock()
k, err := identityStore.getNamedKey(ctx, storage, apiConfig.IdentityTokenKey)
if err != nil {
return nil, fmt.Errorf("failed getting key %q: %w", apiConfig.IdentityTokenKey, err)
}
if k == nil {
return logical.ErrorResponse("key %q does not exist", apiConfig.IdentityTokenKey), nil
}
config.IdentityTokenKey = apiConfig.IdentityTokenKey
}
// Create the mount entry
me := &MountEntry{
Table: mountTableType,
@@ -1954,6 +1977,10 @@ func (b *SystemBackend) handleTuneReadCommon(ctx context.Context, path string) (
resp.Data["allowed_managed_keys"] = rawVal.([]string)
}
if rawVal, ok := mountEntry.synthesizedConfigCache.Load("identity_token_key"); ok {
resp.Data["identity_token_key"] = rawVal.(string)
}
if mountEntry.Config.UserLockoutConfig != nil {
resp.Data["user_lockout_counter_reset_duration"] = int64(mountEntry.Config.UserLockoutConfig.LockoutCounterReset.Seconds())
resp.Data["user_lockout_threshold"] = mountEntry.Config.UserLockoutConfig.LockoutThreshold
@@ -2245,6 +2272,50 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string,
}
}
if rawVal, ok := data.GetOk("identity_token_key"); ok {
identityTokenKey := rawVal.(string)
if identityTokenKey != "" {
storage := b.Core.router.MatchingStorageByAPIPath(ctx, mountPathIdentity)
if storage == nil {
return nil, errors.New("failed to find identity storage")
}
identityStore := b.Core.IdentityStore()
identityStore.oidcLock.RLock()
defer identityStore.oidcLock.RUnlock()
k, err := identityStore.getNamedKey(ctx, storage, identityTokenKey)
if err != nil {
return nil, fmt.Errorf("failed getting key %q: %w", identityTokenKey, err)
}
if k == nil {
return logical.ErrorResponse("key %q does not exist", identityTokenKey), nil
}
}
oldVal := mountEntry.Config.IdentityTokenKey
mountEntry.Config.IdentityTokenKey = identityTokenKey
// Update the mount table
var err error
switch {
case strings.HasPrefix(path, "auth/"):
err = b.Core.persistAuth(ctx, b.Core.auth, &mountEntry.Local)
default:
err = b.Core.persistMounts(ctx, b.Core.mounts, &mountEntry.Local)
}
if err != nil {
mountEntry.Config.IdentityTokenKey = oldVal
return handleError(err)
}
mountEntry.SyncCache()
if b.Core.logger.IsInfo() {
b.Core.logger.Info("mount tuning of identity_token_key successful", "path", path)
}
}
if rawVal, ok := data.GetOk("audit_non_hmac_request_keys"); ok {
auditNonHMACRequestKeys := rawVal.([]string)
@@ -3000,6 +3071,26 @@ func (b *SystemBackend) handleEnableAuth(ctx context.Context, req *logical.Reque
config.AllowedManagedKeys = apiConfig.AllowedManagedKeys
}
if apiConfig.IdentityTokenKey != "" {
storage := b.Core.router.MatchingStorageByAPIPath(ctx, mountPathIdentity)
if storage == nil {
return nil, errors.New("failed to find identity storage")
}
identityStore := b.Core.IdentityStore()
identityStore.oidcLock.RLock()
defer identityStore.oidcLock.RUnlock()
k, err := identityStore.getNamedKey(ctx, storage, apiConfig.IdentityTokenKey)
if err != nil {
return nil, fmt.Errorf("failed getting key %q: %w", apiConfig.IdentityTokenKey, err)
}
if k == nil {
return logical.ErrorResponse("key %q does not exist", apiConfig.IdentityTokenKey), nil
}
config.IdentityTokenKey = apiConfig.IdentityTokenKey
}
// Create the mount entry
me := &MountEntry{
Table: credentialTableType,
@@ -6445,6 +6536,10 @@ This path responds to the following HTTP methods.
"Whether the container runtime is run as a non-privileged (non-root) user.",
"",
},
"identity_token_key": {
"The name of the key used to sign plugin identity tokens. Defaults to the default key.",
"",
},
"leases": {
`View or list lease metadata.`,
`

View File

@@ -3721,6 +3721,11 @@ func (b *SystemBackend) authPaths() []*framework.Path {
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["plugin-catalog_version"][0]),
},
"identity_token_key": {
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["identity_token_key"][0]),
Required: false,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
@@ -3807,6 +3812,10 @@ func (b *SystemBackend) authPaths() []*framework.Path {
Type: framework.TypeString,
Required: false,
},
"identity_token_key": {
Type: framework.TypeString,
Required: false,
},
},
}},
},
@@ -4573,6 +4582,10 @@ func (b *SystemBackend) mountPaths() []*framework.Path {
Type: framework.TypeMap,
Description: strings.TrimSpace(sysHelp["tune_user_lockout_config"][0]),
},
"identity_token_key": {
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["identity_token_key"][0]),
},
},
Operations: map[logical.Operation]framework.OperationHandler{
@@ -4671,6 +4684,10 @@ func (b *SystemBackend) mountPaths() []*framework.Path {
Type: framework.TypeBool,
Required: false,
},
"identity_token_key": {
Type: framework.TypeString,
Required: false,
},
},
}},
},

View File

@@ -433,6 +433,244 @@ func TestSystemBackend_mount_force_no_cache(t *testing.T) {
}
}
// TestSystemBackend_mount_secret_identity_token_key ensures that the identity
// token key can be specified at secret mount enable time.
func TestSystemBackend_mount_secret_identity_token_key(t *testing.T) {
ctx := namespace.RootContext(nil)
core, b, _ := testCoreSystemBackend(t)
// Create a test key
testKey := "test_key"
resp, err := core.identityStore.HandleRequest(ctx, &logical.Request{
Storage: core.identityStore.view,
Path: fmt.Sprintf("oidc/key/%s", testKey),
Operation: logical.CreateOperation,
})
require.NoError(t, err)
require.Nil(t, resp)
tests := []struct {
name string
mountPath string
keyName string
wantErr bool
}{
{
name: "enable secret mount with default key",
mountPath: "mounts/dev/",
keyName: defaultKeyName,
},
{
name: "enable secret mount with empty key",
mountPath: "mounts/test/",
keyName: "",
},
{
name: "enable secret mount with existing key",
mountPath: "mounts/int/",
keyName: testKey,
},
{
name: "enable secret mount with key that does not exist",
mountPath: "mounts/prod/",
keyName: "does_not_exist_key",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Enable the secret mount
req := logical.TestRequest(t, logical.UpdateOperation, tt.mountPath)
req.Data["type"] = "kv"
req.Data["config"] = map[string]interface{}{
"identity_token_key": tt.keyName,
}
resp, err := b.HandleRequest(ctx, req)
if tt.wantErr {
require.Nil(t, err)
require.Equal(t, fmt.Errorf("key %q does not exist", tt.keyName), resp.Error())
return
}
require.NoError(t, err)
require.Nil(t, resp)
// Expect identity token key set on the mount entry
mountEntry := core.router.MatchingMountEntry(ctx, strings.TrimPrefix(tt.mountPath, "mounts/"))
require.NotNil(t, mountEntry)
require.Equal(t, tt.keyName, mountEntry.Config.IdentityTokenKey)
})
}
}
// TestSystemBackend_mount_auth_identity_token_key ensures that the identity
// token key can be specified at auth mount enable time.
func TestSystemBackend_mount_auth_identity_token_key(t *testing.T) {
ctx := namespace.RootContext(nil)
core, b, _ := testCoreSystemBackend(t)
core.credentialBackends["userpass"] = credUserpass.Factory
// Create a test key
testKey := "test_key"
resp, err := core.identityStore.HandleRequest(ctx, &logical.Request{
Storage: core.identityStore.view,
Path: fmt.Sprintf("oidc/key/%s", testKey),
Operation: logical.CreateOperation,
})
require.NoError(t, err)
require.Nil(t, resp)
tests := []struct {
name string
mountPath string
keyName string
wantErr bool
}{
{
name: "enable auth mount with default key",
mountPath: "auth/dev/",
keyName: defaultKeyName,
},
{
name: "enable auth mount with empty key",
mountPath: "auth/test/",
keyName: "",
},
{
name: "enable auth mount with existing key",
mountPath: "auth/int/",
keyName: testKey,
},
{
name: "enable auth mount with key that does not exist",
mountPath: "auth/prod/",
keyName: "does_not_exist_key",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Enable the auth mount
req := logical.TestRequest(t, logical.UpdateOperation, tt.mountPath)
req.Data["type"] = "userpass"
req.Data["config"] = map[string]interface{}{
"identity_token_key": tt.keyName,
}
resp, err := b.HandleRequest(ctx, req)
if tt.wantErr {
require.Nil(t, err)
require.Equal(t, fmt.Errorf("key %q does not exist", tt.keyName), resp.Error())
return
}
require.NoError(t, err)
require.Nil(t, resp)
// Expect identity token key set on the mount entry
mountEntry := core.router.MatchingMountEntry(ctx, tt.mountPath)
require.NotNil(t, mountEntry)
require.Equal(t, tt.keyName, mountEntry.Config.IdentityTokenKey)
})
}
}
// TestSystemBackend_tune_identity_token_key ensures that the identity
// token key can be tuned for existing auth and secret mounts.
func TestSystemBackend_tune_identity_token_key(t *testing.T) {
ctx := namespace.RootContext(nil)
core, b, _ := testCoreSystemBackend(t)
core.credentialBackends["userpass"] = credUserpass.Factory
// Enable an auth mount with an empty identity token key
req := logical.TestRequest(t, logical.UpdateOperation, "auth/dev/")
req.Data["type"] = "userpass"
resp, err := b.HandleRequest(ctx, req)
require.NoError(t, err)
require.Nil(t, resp)
// Enable a secret mount with the default key
req = logical.TestRequest(t, logical.UpdateOperation, "mounts/test/")
req.Data["type"] = "kv"
req.Data["config"] = map[string]interface{}{
"identity_token_key": defaultKeyName,
}
resp, err = b.HandleRequest(ctx, req)
require.NoError(t, err)
require.Nil(t, resp)
// Create a test key
testKey := "test_key"
resp, err = core.identityStore.HandleRequest(ctx, &logical.Request{
Storage: core.identityStore.view,
Path: fmt.Sprintf("oidc/key/%s", testKey),
Operation: logical.CreateOperation,
})
require.NoError(t, err)
require.Nil(t, resp)
tests := []struct {
name string
keyName string
wantErr bool
}{
{
name: "tune mounts to default key",
keyName: defaultKeyName,
},
{
name: "tune mounts to empty key",
keyName: "",
},
{
name: "tune mounts with existing key",
keyName: testKey,
},
{
name: "tune mounts with key that does not exist",
keyName: "does_not_exist_key",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Tune the auth mount
req = logical.TestRequest(t, logical.UpdateOperation, "auth/dev/tune")
req.Data["identity_token_key"] = tt.keyName
resp, err := b.HandleRequest(ctx, req)
if tt.wantErr {
require.Nil(t, err)
require.Equal(t, fmt.Errorf("key %q does not exist", tt.keyName), resp.Error())
return
}
require.NoError(t, err)
require.Nil(t, resp)
// Expect identity token key set on the auth mount entry
mountEntry := core.router.MatchingMountEntry(ctx, "auth/dev/")
require.NotNil(t, mountEntry)
require.Equal(t, tt.keyName, mountEntry.Config.IdentityTokenKey)
// Tune the secret mount
req = logical.TestRequest(t, logical.UpdateOperation, "mounts/test/tune")
req.Data["identity_token_key"] = tt.keyName
resp, err = b.HandleRequest(ctx, req)
if tt.wantErr {
require.Nil(t, err)
require.Equal(t, fmt.Errorf("key %q does not exist", tt.keyName), resp.Error())
return
}
require.NoError(t, err)
require.Nil(t, resp)
// Expect identity token key set on the secret mount entry
mountEntry = core.router.MatchingMountEntry(ctx, "test/")
require.NotNil(t, mountEntry)
require.Equal(t, tt.keyName, mountEntry.Config.IdentityTokenKey)
})
}
}
func TestSystemBackend_mount_invalid(t *testing.T) {
b := testSystemBackend(t)

View File

@@ -366,6 +366,7 @@ type MountConfig struct {
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
UserLockoutConfig *UserLockoutConfig `json:"user_lockout_config,omitempty" mapstructure:"user_lockout_config"`
DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"`
IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"`
// PluginName is the name of the plugin registered in the catalog.
//
@@ -402,6 +403,7 @@ type APIMountConfig struct {
UserLockoutConfig *UserLockoutConfig `json:"user_lockout_config,omitempty" mapstructure:"user_lockout_config"`
PluginVersion string `json:"plugin_version,omitempty" mapstructure:"plugin_version"`
DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"`
IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"`
// PluginName is the name of the plugin registered in the catalog.
//
@@ -509,6 +511,12 @@ func (e *MountEntry) SyncCache() {
} else {
e.synthesizedConfigCache.Store("delegated_auth_accessors", e.Config.DelegatedAuthAccessors)
}
if len(e.Config.IdentityTokenKey) == 0 {
e.synthesizedConfigCache.Delete("identity_token_key")
} else {
e.synthesizedConfigCache.Store("identity_token_key", e.Config.IdentityTokenKey)
}
}
func (entry *MountEntry) Deserialize() map[string]interface{} {