diff --git a/vault/identity_store_oidc.go b/vault/identity_store_oidc.go index 9a521994dbed..a7204d298a4e 100644 --- a/vault/identity_store_oidc.go +++ b/vault/identity_store_oidc.go @@ -576,7 +576,9 @@ func (i *IdentityStore) pathOIDCReadKey(ctx context.Context, req *logical.Reques }, nil } -// rolesReferencingTargetKeyName returns a map of role names to roles referenced by targetKeyName. +// rolesReferencingTargetKeyName returns a map of role names to roles +// referencing targetKeyName. +// // Note: this is not threadsafe. It is to be called with Lock already held. func (i *IdentityStore) rolesReferencingTargetKeyName(ctx context.Context, req *logical.Request, targetKeyName string) (map[string]role, error) { roleNames, err := req.Storage.List(ctx, roleConfigPath) @@ -605,7 +607,8 @@ func (i *IdentityStore) rolesReferencingTargetKeyName(ctx context.Context, req * } // roleNamesReferencingTargetKeyName returns a slice of strings of role -// names referenced by targetKeyName. +// names referencing targetKeyName. +// // Note: this is not threadsafe. It is to be called with Lock already held. func (i *IdentityStore) roleNamesReferencingTargetKeyName(ctx context.Context, req *logical.Request, targetKeyName string) ([]string, error) { roles, err := i.rolesReferencingTargetKeyName(ctx, req, targetKeyName) @@ -644,6 +647,18 @@ func (i *IdentityStore) pathOIDCDeleteKey(ctx context.Context, req *logical.Requ return logical.ErrorResponse(errorMessage), logical.ErrInvalidRequest } + clientNames, err := i.clientNamesReferencingTargetKeyName(ctx, req, targetKeyName) + if err != nil { + return nil, err + } + + if len(clientNames) > 0 { + errorMessage := fmt.Sprintf("unable to delete key %q because it is currently referenced by these clients: %s", + targetKeyName, strings.Join(clientNames, ", ")) + i.oidcLock.Unlock() + return logical.ErrorResponse(errorMessage), logical.ErrInvalidRequest + } + // key can safely be deleted now err = req.Storage.Delete(ctx, namedKeyConfigPath+targetKeyName) if err != nil { diff --git a/vault/identity_store_oidc_provider.go b/vault/identity_store_oidc_provider.go index e2501fcd4d74..81835358f093 100644 --- a/vault/identity_store_oidc_provider.go +++ b/vault/identity_store_oidc_provider.go @@ -4,8 +4,11 @@ import ( "context" "encoding/base64" "encoding/json" + "fmt" + "sort" "strings" + "github.com/hashicorp/go-secure-stdlib/base62" "github.com/hashicorp/go-secure-stdlib/strutil" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/identitytpl" @@ -22,10 +25,23 @@ type scope struct { Description string `json:"description"` } +type client struct { + RedirectURIs []string `json:"redirect_uris"` + Assignments []string `json:"assignments"` + Key string `json:"key"` + IDTokenTTL int `json:"id_token_ttl"` + AccessTokenTTL int `json:"access_token_ttl"` + + // used for OIDC endpoints + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` +} + const ( oidcProviderPrefix = "oidc_provider/" assignmentPath = oidcProviderPrefix + "assignment/" scopePath = oidcProviderPrefix + "scope/" + clientPath = oidcProviderPrefix + "client/" ) func oidcProviderPaths(i *IdentityStore) []*framework.Path { @@ -118,7 +134,154 @@ func oidcProviderPaths(i *IdentityStore) []*framework.Path { HelpSynopsis: "List OIDC scopes", HelpDescription: "List all configured OIDC scopes in the identity backend.", }, + { + Pattern: "oidc/client/" + framework.GenericNameRegex("name"), + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "Name of the client.", + }, + "redirect_uris": { + Type: framework.TypeCommaStringSlice, + Description: "Comma separated string or array of redirect URIs used by the client. One of these values must exactly match the redirect_uri parameter value used in each authentication request.", + }, + "assignments": { + Type: framework.TypeCommaStringSlice, + Description: "Comma separated string or array of assignment resources.", + }, + "key": { + Type: framework.TypeString, + Description: "A reference to a named key resource. Cannot be modified after creation.", + Required: true, + }, + "id_token_ttl": { + Type: framework.TypeDurationSecond, + Description: "The time-to-live for ID tokens obtained by the client.", + }, + "access_token_ttl": { + Type: framework.TypeDurationSecond, + Description: "The time-to-live for access tokens obtained by the client.", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: i.pathOIDCCreateUpdateClient, + }, + logical.CreateOperation: &framework.PathOperation{ + Callback: i.pathOIDCCreateUpdateClient, + }, + logical.ReadOperation: &framework.PathOperation{ + Callback: i.pathOIDCReadClient, + }, + logical.DeleteOperation: &framework.PathOperation{ + Callback: i.pathOIDCDeleteClient, + }, + }, + ExistenceCheck: i.pathOIDCClientExistenceCheck, + HelpSynopsis: "CRUD operations for OIDC clients.", + HelpDescription: "Create, Read, Update, and Delete OIDC clients.", + }, + { + Pattern: "oidc/client/?$", + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ListOperation: &framework.PathOperation{ + Callback: i.pathOIDCListClient, + }, + }, + HelpSynopsis: "List OIDC clients", + HelpDescription: "List all configured OIDC clients in the identity backend.", + }, + } +} + +// clientsReferencingTargetAssignmentName returns a map of client names to +// clients referencing targetAssignmentName. +func (i *IdentityStore) clientsReferencingTargetAssignmentName(ctx context.Context, req *logical.Request, targetAssignmentName string) (map[string]client, error) { + clientNames, err := req.Storage.List(ctx, clientPath) + if err != nil { + return nil, err + } + + var tempClient client + clients := make(map[string]client) + for _, clientName := range clientNames { + entry, err := req.Storage.Get(ctx, clientPath+clientName) + if err != nil { + return nil, err + } + if entry != nil { + if err := entry.DecodeJSON(&tempClient); err != nil { + return nil, err + } + for _, a := range tempClient.Assignments { + if a == targetAssignmentName { + clients[clientName] = tempClient + } + } + } + } + + return clients, nil +} + +// clientNamesReferencingTargetAssignmentName returns a slice of strings of client +// names referencing targetAssignmentName. +func (i *IdentityStore) clientNamesReferencingTargetAssignmentName(ctx context.Context, req *logical.Request, targetAssignmentName string) ([]string, error) { + clients, err := i.clientsReferencingTargetAssignmentName(ctx, req, targetAssignmentName) + if err != nil { + return nil, err + } + + var names []string + for client, _ := range clients { + names = append(names, client) } + sort.Strings(names) + return names, nil +} + +// clientsReferencingTargetKeyName returns a map of client names to +// clients referencing targetKeyName. +func (i *IdentityStore) clientsReferencingTargetKeyName(ctx context.Context, req *logical.Request, targetKeyName string) (map[string]client, error) { + clientNames, err := req.Storage.List(ctx, clientPath) + if err != nil { + return nil, err + } + + var tempClient client + clients := make(map[string]client) + for _, clientName := range clientNames { + entry, err := req.Storage.Get(ctx, clientPath+clientName) + if err != nil { + return nil, err + } + if entry != nil { + if err := entry.DecodeJSON(&tempClient); err != nil { + return nil, err + } + if tempClient.Key == targetKeyName { + clients[clientName] = tempClient + } + } + } + + return clients, nil +} + +// clientNamesReferencingTargetKeyName returns a slice of strings of client +// names referencing targetKeyName. +func (i *IdentityStore) clientNamesReferencingTargetKeyName(ctx context.Context, req *logical.Request, targetKeyName string) ([]string, error) { + clients, err := i.clientsReferencingTargetKeyName(ctx, req, targetKeyName) + if err != nil { + return nil, err + } + + var names []string + for client, _ := range clients { + names = append(names, client) + } + sort.Strings(names) + return names, nil } // pathOIDCCreateUpdateAssignment is used to create a new assignment or update an existing one @@ -199,7 +362,19 @@ func (i *IdentityStore) pathOIDCReadAssignment(ctx context.Context, req *logical // pathOIDCDeleteAssignment is used to delete an assignment func (i *IdentityStore) pathOIDCDeleteAssignment(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { name := d.Get("name").(string) - err := req.Storage.Delete(ctx, assignmentPath+name) + + clientNames, err := i.clientNamesReferencingTargetAssignmentName(ctx, req, name) + if err != nil { + return nil, err + } + + if len(clientNames) > 0 { + errorMessage := fmt.Sprintf("unable to delete assignment %q because it is currently referenced by these clients: %s", + name, strings.Join(clientNames, ", ")) + return logical.ErrorResponse(errorMessage), logical.ErrInvalidRequest + } + + err = req.Storage.Delete(ctx, assignmentPath+name) if err != nil { return nil, err } @@ -261,7 +436,6 @@ func (i *IdentityStore) pathOIDCCreateUpdateScope(ctx context.Context, req *logi String: scope.Template, Entity: new(logical.Entity), Groups: make([]*logical.Group, 0), - // namespace? }) if err != nil { return logical.ErrorResponse("error parsing template: %s", err.Error()), nil @@ -345,3 +519,168 @@ func (i *IdentityStore) pathOIDCScopeExistenceCheck(ctx context.Context, req *lo return entry != nil, nil } + +// pathOIDCCreateUpdateClient is used to create a new client or update an existing one +func (i *IdentityStore) pathOIDCCreateUpdateClient(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + + var client client + if req.Operation == logical.UpdateOperation { + entry, err := req.Storage.Get(ctx, clientPath+name) + if err != nil { + return nil, err + } + if entry != nil { + if err := entry.DecodeJSON(&client); err != nil { + return nil, err + } + } + } + + if redirectURIsRaw, ok := d.GetOk("redirect_uris"); ok { + client.RedirectURIs = redirectURIsRaw.([]string) + } else if req.Operation == logical.CreateOperation { + client.RedirectURIs = d.Get("redirect_uris").([]string) + } + + if assignmentsRaw, ok := d.GetOk("assignments"); ok { + client.Assignments = assignmentsRaw.([]string) + } else if req.Operation == logical.CreateOperation { + client.Assignments = d.Get("assignments").([]string) + } + + // enforce assignment existence + for _, assignment := range client.Assignments { + entry, err := req.Storage.Get(ctx, assignmentPath+assignment) + if err != nil { + return nil, err + } + if entry == nil { + return logical.ErrorResponse("assignment %q does not exist", assignment), nil + } + } + + if keyRaw, ok := d.GetOk("key"); ok { + key := keyRaw.(string) + if req.Operation == logical.UpdateOperation && client.Key != key { + return logical.ErrorResponse("key modification is not allowed"), nil + } + client.Key = key + } else if req.Operation == logical.CreateOperation { + client.Key = d.Get("key").(string) + } + + if client.Key == "" { + return logical.ErrorResponse("the key parameter is required"), nil + } + + // enforce key existence on client creation + entry, err := req.Storage.Get(ctx, namedKeyConfigPath+client.Key) + if err != nil { + return nil, err + } + if entry == nil { + return logical.ErrorResponse("key %q does not exist", client.Key), nil + } + + if idTokenTTLRaw, ok := d.GetOk("id_token_ttl"); ok { + client.IDTokenTTL = idTokenTTLRaw.(int) + } else if req.Operation == logical.CreateOperation { + client.IDTokenTTL = d.Get("id_token_ttl").(int) + } + + if accessTokenTTLRaw, ok := d.GetOk("access_token_ttl"); ok { + client.AccessTokenTTL = accessTokenTTLRaw.(int) + } else if req.Operation == logical.CreateOperation { + client.AccessTokenTTL = d.Get("access_token_ttl").(int) + } + + if client.ClientID == "" { + // generate client_id + clientID, err := base62.Random(32) + if err != nil { + return nil, err + } + client.ClientID = clientID + } + + if client.ClientSecret == "" { + // generate client_secret + clientSecret, err := base62.Random(64) + if err != nil { + return nil, err + } + client.ClientSecret = clientSecret + } + + // store client + entry, err = logical.StorageEntryJSON(clientPath+name, client) + if err != nil { + return nil, err + } + + if err := req.Storage.Put(ctx, entry); err != nil { + return nil, err + } + + return nil, nil +} + +// pathOIDCListClient is used to list clients +func (i *IdentityStore) pathOIDCListClient(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + clients, err := req.Storage.List(ctx, clientPath) + if err != nil { + return nil, err + } + return logical.ListResponse(clients), nil +} + +// pathOIDCReadClient is used to read an existing client +func (i *IdentityStore) pathOIDCReadClient(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + + entry, err := req.Storage.Get(ctx, clientPath+name) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var client client + if err := entry.DecodeJSON(&client); err != nil { + return nil, err + } + return &logical.Response{ + Data: map[string]interface{}{ + "redirect_uris": client.RedirectURIs, + "assignments": client.Assignments, + "key": client.Key, + "id_token_ttl": client.IDTokenTTL, + "access_token_ttl": client.AccessTokenTTL, + "client_id": client.ClientID, + "client_secret": client.ClientSecret, + }, + }, nil +} + +// pathOIDCDeleteClient is used to delete an client +func (i *IdentityStore) pathOIDCDeleteClient(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + err := req.Storage.Delete(ctx, clientPath+name) + if err != nil { + return nil, err + } + return nil, nil +} + +func (i *IdentityStore) pathOIDCClientExistenceCheck(ctx context.Context, req *logical.Request, d *framework.FieldData) (bool, error) { + name := d.Get("name").(string) + + entry, err := req.Storage.Get(ctx, clientPath+name) + if err != nil { + return false, err + } + + return entry != nil, nil +} diff --git a/vault/identity_store_oidc_provider_test.go b/vault/identity_store_oidc_provider_test.go index c171d30c7612..169b006d5ae3 100644 --- a/vault/identity_store_oidc_provider_test.go +++ b/vault/identity_store_oidc_provider_test.go @@ -8,6 +8,414 @@ import ( "github.com/hashicorp/vault/sdk/logical" ) +// TestOIDC_Path_OIDC_ProviderClient_NoKeyParameter tests that a client cannot +// be created without a key parameter +func TestOIDC_Path_OIDC_ProviderClient_NoKeyParameter(t *testing.T) { + c, _, _ := TestCoreUnsealed(t) + ctx := namespace.RootContext(nil) + storage := &logical.InmemStorage{} + + // Create a test client "test-client1" without a key param -- should fail + resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client1", + Operation: logical.CreateOperation, + Storage: storage, + }) + expectError(t, resp, err) + // validate error message + expectedStrings := map[string]interface{}{ + "the key parameter is required": true, + } + expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings) +} + +// TestOIDC_Path_OIDC_ProviderClient_NilKeyEntry tests that a client cannot be +// created when a key parameter is provided but the key does not exist +func TestOIDC_Path_OIDC_ProviderClient_NilKeyEntry(t *testing.T) { + c, _, _ := TestCoreUnsealed(t) + ctx := namespace.RootContext(nil) + storage := &logical.InmemStorage{} + + // Create a test client "test-client1" with a non-existent key -- should fail + resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client1", + Operation: logical.CreateOperation, + Data: map[string]interface{}{ + "key": "test-key", + }, + Storage: storage, + }) + expectError(t, resp, err) + // validate error message + expectedStrings := map[string]interface{}{ + "key \"test-key\" does not exist": true, + } + expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings) +} + +// TestOIDC_Path_OIDC_ProviderClient_UpdateKey tests that a client +// does not allow key modification on Update operations +func TestOIDC_Path_OIDC_ProviderClient_UpdateKey(t *testing.T) { + c, _, _ := TestCoreUnsealed(t) + ctx := namespace.RootContext(nil) + storage := &logical.InmemStorage{} + + // Create a test key "test-key1" + c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/key/test-key1", + Operation: logical.CreateOperation, + Data: map[string]interface{}{ + "verification_ttl": "2m", + "rotation_period": "2m", + }, + Storage: storage, + }) + + // Create a test key "test-key2" + c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/key/test-key2", + Operation: logical.CreateOperation, + Data: map[string]interface{}{ + "verification_ttl": "2m", + "rotation_period": "2m", + }, + Storage: storage, + }) + + // Create a test client "test-client" -- should succeed + resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client", + Operation: logical.CreateOperation, + Storage: storage, + Data: map[string]interface{}{ + "key": "test-key1", + }, + }) + expectSuccess(t, resp, err) + + // Create a test client "test-client" -- should fail + resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client", + Operation: logical.UpdateOperation, + Storage: storage, + Data: map[string]interface{}{ + "key": "test-key2", + }, + }) + expectError(t, resp, err) + // validate error message + expectedStrings := map[string]interface{}{ + "key modification is not allowed": true, + } + expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings) +} + +// TestOIDC_Path_OIDC_ProviderClient_AssignmentDoesNotExist tests that a client +// cannot be created with assignments that do not exist +func TestOIDC_Path_OIDC_ProviderClient_AssignmentDoesNotExist(t *testing.T) { + c, _, _ := TestCoreUnsealed(t) + ctx := namespace.RootContext(nil) + storage := &logical.InmemStorage{} + + // Create a test key "test-key" + c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/key/test-key", + Operation: logical.CreateOperation, + Data: map[string]interface{}{ + "verification_ttl": "2m", + "rotation_period": "2m", + }, + Storage: storage, + }) + + // Create a test client "test-client" -- should fail + resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client", + Operation: logical.CreateOperation, + Storage: storage, + Data: map[string]interface{}{ + "key": "test-key", + "assignments": "my-assignment", + }, + }) + expectError(t, resp, err) + // validate error message + expectedStrings := map[string]interface{}{ + "assignment \"my-assignment\" does not exist": true, + } + expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings) +} + +// TestOIDC_Path_OIDC_ProviderClient tests CRUD operations for clients +func TestOIDC_Path_OIDC_ProviderClient(t *testing.T) { + c, _, _ := TestCoreUnsealed(t) + ctx := namespace.RootContext(nil) + storage := &logical.InmemStorage{} + + // Create a test key "test-key" + c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/key/test-key", + Operation: logical.CreateOperation, + Data: map[string]interface{}{ + "verification_ttl": "2m", + "rotation_period": "2m", + }, + Storage: storage, + }) + + // Create a test client "test-client" -- should succeed + resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client", + Operation: logical.CreateOperation, + Storage: storage, + Data: map[string]interface{}{ + "key": "test-key", + }, + }) + expectSuccess(t, resp, err) + + // Read "test-client" and validate + resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client", + Operation: logical.ReadOperation, + Storage: storage, + }) + expectSuccess(t, resp, err) + expected := map[string]interface{}{ + "redirect_uris": []string{}, + "assignments": []string{}, + "key": "test-key", + "id_token_ttl": 0, + "access_token_ttl": 0, + "client_id": resp.Data["client_id"], + "client_secret": resp.Data["client_secret"], + } + if diff := deep.Equal(expected, resp.Data); diff != nil { + t.Fatal(diff) + } + + // Create a test assignment "my-assignment" -- should succeed + resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/assignment/my-assignment", + Operation: logical.CreateOperation, + Storage: storage, + }) + expectSuccess(t, resp, err) + + // Update "test-client" -- should succeed + resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client", + Operation: logical.UpdateOperation, + Data: map[string]interface{}{ + "redirect_uris": "http://localhost:3456/callback", + "assignments": "my-assignment", + "key": "test-key", + "id_token_ttl": 0, + "access_token_ttl": 0, + }, + Storage: storage, + }) + expectSuccess(t, resp, err) + + // Read "test-client" again and validate + resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client", + Operation: logical.ReadOperation, + Storage: storage, + }) + expectSuccess(t, resp, err) + expected = map[string]interface{}{ + "redirect_uris": []string{"http://localhost:3456/callback"}, + "assignments": []string{"my-assignment"}, + "key": "test-key", + "id_token_ttl": 0, + "access_token_ttl": 0, + "client_id": resp.Data["client_id"], + "client_secret": resp.Data["client_secret"], + } + if diff := deep.Equal(expected, resp.Data); diff != nil { + t.Fatal(diff) + } + + // Delete test-client -- should succeed + resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client", + Operation: logical.DeleteOperation, + Storage: storage, + }) + expectSuccess(t, resp, err) + + // Read "test-client" again and validate + resp, _ = c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client", + Operation: logical.ReadOperation, + Storage: storage, + }) + if resp != nil { + t.Fatalf("expected nil but got resp: %#v", resp) + } +} + +// TestOIDC_Path_OIDC_ProviderClient_Update tests Update operations for clients +func TestOIDC_Path_OIDC_ProviderClient_Update(t *testing.T) { + c, _, _ := TestCoreUnsealed(t) + ctx := namespace.RootContext(nil) + storage := &logical.InmemStorage{} + + // Create a test key "test-key" + c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/key/test-key", + Operation: logical.CreateOperation, + Data: map[string]interface{}{ + "verification_ttl": "2m", + "rotation_period": "2m", + }, + Storage: storage, + }) + + // Create a test assignment "my-assignment" -- should succeed + resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/assignment/my-assignment", + Operation: logical.CreateOperation, + Storage: storage, + }) + expectSuccess(t, resp, err) + + // Create a test client "test-client" -- should succeed + resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client", + Operation: logical.CreateOperation, + Storage: storage, + Data: map[string]interface{}{ + "redirect_uris": "http://localhost:3456/callback", + "assignments": "my-assignment", + "key": "test-key", + "id_token_ttl": 0, + "access_token_ttl": 0, + }, + }) + expectSuccess(t, resp, err) + + // Read "test-client" and validate + resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client", + Operation: logical.ReadOperation, + Storage: storage, + }) + expectSuccess(t, resp, err) + expected := map[string]interface{}{ + "redirect_uris": []string{"http://localhost:3456/callback"}, + "assignments": []string{"my-assignment"}, + "key": "test-key", + "id_token_ttl": 0, + "access_token_ttl": 0, + "client_id": resp.Data["client_id"], + "client_secret": resp.Data["client_secret"], + } + if diff := deep.Equal(expected, resp.Data); diff != nil { + t.Fatal(diff) + } + + // Update "test-client" -- should succeed + resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client", + Operation: logical.UpdateOperation, + Data: map[string]interface{}{ + "redirect_uris": "http://localhost:3456/callback2", + }, + Storage: storage, + }) + expectSuccess(t, resp, err) + + // Read "test-client" again and validate + resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client", + Operation: logical.ReadOperation, + Storage: storage, + }) + expectSuccess(t, resp, err) + expected = map[string]interface{}{ + "redirect_uris": []string{"http://localhost:3456/callback2"}, + "assignments": []string{"my-assignment"}, + "key": "test-key", + "id_token_ttl": 0, + "access_token_ttl": 0, + "client_id": resp.Data["client_id"], + "client_secret": resp.Data["client_secret"], + } + if diff := deep.Equal(expected, resp.Data); diff != nil { + t.Fatal(diff) + } +} + +// TestOIDC_Path_OIDC_ProviderClient_List tests the List operation for clients +func TestOIDC_Path_OIDC_ProviderClient_List(t *testing.T) { + c, _, _ := TestCoreUnsealed(t) + ctx := namespace.RootContext(nil) + storage := &logical.InmemStorage{} + + // Create a test key "test-key" + c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/key/test-key", + Operation: logical.CreateOperation, + Data: map[string]interface{}{ + "verification_ttl": "2m", + "rotation_period": "2m", + }, + Storage: storage, + }) + + // Prepare two clients, test-client1 and test-client2 + c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client1", + Operation: logical.CreateOperation, + Storage: storage, + Data: map[string]interface{}{ + "key": "test-key", + }, + }) + + c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client2", + Operation: logical.CreateOperation, + Storage: storage, + Data: map[string]interface{}{ + "key": "test-key", + }, + }) + + // list clients + respListClients, listErr := c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client", + Operation: logical.ListOperation, + Storage: storage, + }) + expectSuccess(t, respListClients, listErr) + + // validate list response + expectedStrings := map[string]interface{}{"test-client1": true, "test-client2": true} + expectStrings(t, respListClients.Data["keys"].([]string), expectedStrings) + + // delete test-client2 + c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client2", + Operation: logical.DeleteOperation, + Storage: storage, + }) + + // list clients again and validate response + respListClientAfterDelete, listErrAfterDelete := c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client", + Operation: logical.ListOperation, + Storage: storage, + }) + expectSuccess(t, respListClientAfterDelete, listErrAfterDelete) + + // validate list response + delete(expectedStrings, "test-client2") + expectStrings(t, respListClientAfterDelete.Data["keys"].([]string), expectedStrings) +} + // TestOIDC_Path_OIDC_ProviderScope_ReservedName tests that the reserved name // "openid" cannot be used when creating a scope func TestOIDC_Path_OIDC_ProviderScope_ReservedName(t *testing.T) { @@ -291,6 +699,73 @@ func TestOIDC_Path_OIDC_ProviderAssignment(t *testing.T) { } } +// TestOIDC_Path_OIDC_ProviderAssignment_DeleteWithExistingClient tests that an +// assignment cannot be deleted when it is referenced by a client +func TestOIDC_Path_OIDC_ProviderAssignment_DeleteWithExistingClient(t *testing.T) { + c, _, _ := TestCoreUnsealed(t) + ctx := namespace.RootContext(nil) + storage := &logical.InmemStorage{} + + // Create a test assignment "test-assignment" -- should succeed + resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/assignment/test-assignment", + Operation: logical.CreateOperation, + Storage: storage, + }) + expectSuccess(t, resp, err) + + // Create a test key "test-key" + c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/key/test-key", + Operation: logical.CreateOperation, + Data: map[string]interface{}{ + "verification_ttl": "2m", + "rotation_period": "2m", + }, + Storage: storage, + }) + + // Create a test client "test-client" -- should succeed + resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client", + Operation: logical.CreateOperation, + Storage: storage, + Data: map[string]interface{}{ + "key": "test-key", + "assignments": []string{"test-assignment"}, + }, + }) + expectSuccess(t, resp, err) + + // Delete test-assignment -- should fail + resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/assignment/test-assignment", + Operation: logical.DeleteOperation, + Storage: storage, + }) + expectError(t, resp, err) + // validate error message + expectedStrings := map[string]interface{}{ + "unable to delete assignment \"test-assignment\" because it is currently referenced by these clients: test-client": true, + } + expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings) + + // Read "test-assignment" again and validate + resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/assignment/test-assignment", + Operation: logical.ReadOperation, + Storage: storage, + }) + expectSuccess(t, resp, err) + expected := map[string]interface{}{ + "groups": []string{}, + "entities": []string{}, + } + if diff := deep.Equal(expected, resp.Data); diff != nil { + t.Fatal(diff) + } +} + // TestOIDC_Path_OIDC_ProviderAssignment_Update tests Update operations for assignments func TestOIDC_Path_OIDC_ProviderAssignment_Update(t *testing.T) { c, _, _ := TestCoreUnsealed(t) diff --git a/vault/identity_store_oidc_test.go b/vault/identity_store_oidc_test.go index 09cbd2cdf51e..dd927399aa8c 100644 --- a/vault/identity_store_oidc_test.go +++ b/vault/identity_store_oidc_test.go @@ -455,6 +455,40 @@ func TestOIDC_Path_OIDCKey(t *testing.T) { expectStrings(t, respListKeyAfterDelete.Data["keys"].([]string), expectedStrings) } +// TestOIDC_Path_OIDCKey_DeleteWithExistingClient tests that a key cannot be +// deleted if it is referenced by an existing client +func TestOIDC_Path_OIDCKey_DeleteWithExistingClient(t *testing.T) { + c, _, _ := TestCoreUnsealed(t) + ctx := namespace.RootContext(nil) + storage := &logical.InmemStorage{} + + // Prepare test key test-key + c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/key/test-key", + Operation: logical.CreateOperation, + Storage: storage, + }) + + // Create a test client "test-client" -- should succeed + resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/client/test-client", + Operation: logical.CreateOperation, + Storage: storage, + Data: map[string]interface{}{ + "key": "test-key", + }, + }) + expectSuccess(t, resp, err) + + // Delete test key "test-key" -- should fail + resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ + Path: "oidc/key/test-key", + Operation: logical.DeleteOperation, + Storage: storage, + }) + expectError(t, resp, err) +} + // TestOIDC_PublicKeys tests that public keys are updated by // key creation, rotation, and deletion func TestOIDC_PublicKeys(t *testing.T) {