feature: OIDC provider client API (#12272)

* initial commit

* add read and delete operations

* fix bug in delete and add list unit test

* func doc typo fix

* add existence check for assignment

* remove locking on the assignment resource

It is not needed at this time.

* convert Callbacks to Operations

- convert Callbacks to Operations
- add test case for update operations

* add CRUD operations and test cases

* add client api and tests

* remove use of oidcCache

* remove use of oidcCache

* add template validation and update tests

* remove usage of oidcCache

* refactor struct and var names

* harmonize test name conventions

* refactor struct and var names

* add changelog and refactor

- add changelog
- be more explicit in the case where we do not recieve a path field

* refactor

be more explicit in the case where a field is not provided

* remove extra period from changelog

* update scope path to be OIDC provider specific

* refactor naming conventions

* update assignment path

* update scope path

* enforce key existence on client creation

* removed unused name field

* removed unused name field

* removed unused name field

* prevent assignment deletion when ref'ed by a client

* enfoce assignment existence on client create/update

* update scope template description

* error when attempting to created scope with openid reserved name

* fix UT failures after requiring assignment existence

* disallow key deletion when ref'ed by existing client

* generate client_id and client_secret on CreateOp

* do not allow key modification on client update

* return client_id and client_secret on read ops

* small refactor

* fix bug in delete assignment op

* remove client secret get call
This commit is contained in:
John-Michael Faircloth 2021-08-23 08:42:31 -05:00 committed by GitHub
parent e4e8555e3a
commit fec8e8b21a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 867 additions and 4 deletions

View File

@ -576,7 +576,9 @@ func (i *IdentityStore) pathOIDCReadKey(ctx context.Context, req *logical.Reques
}, nil }, 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. // 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) { func (i *IdentityStore) rolesReferencingTargetKeyName(ctx context.Context, req *logical.Request, targetKeyName string) (map[string]role, error) {
roleNames, err := req.Storage.List(ctx, roleConfigPath) 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 // 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. // 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) { func (i *IdentityStore) roleNamesReferencingTargetKeyName(ctx context.Context, req *logical.Request, targetKeyName string) ([]string, error) {
roles, err := i.rolesReferencingTargetKeyName(ctx, req, targetKeyName) 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 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 // key can safely be deleted now
err = req.Storage.Delete(ctx, namedKeyConfigPath+targetKeyName) err = req.Storage.Delete(ctx, namedKeyConfigPath+targetKeyName)
if err != nil { if err != nil {

View File

@ -4,8 +4,11 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt"
"sort"
"strings" "strings"
"github.com/hashicorp/go-secure-stdlib/base62"
"github.com/hashicorp/go-secure-stdlib/strutil" "github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/identitytpl" "github.com/hashicorp/vault/sdk/helper/identitytpl"
@ -22,10 +25,23 @@ type scope struct {
Description string `json:"description"` 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 ( const (
oidcProviderPrefix = "oidc_provider/" oidcProviderPrefix = "oidc_provider/"
assignmentPath = oidcProviderPrefix + "assignment/" assignmentPath = oidcProviderPrefix + "assignment/"
scopePath = oidcProviderPrefix + "scope/" scopePath = oidcProviderPrefix + "scope/"
clientPath = oidcProviderPrefix + "client/"
) )
func oidcProviderPaths(i *IdentityStore) []*framework.Path { func oidcProviderPaths(i *IdentityStore) []*framework.Path {
@ -118,9 +134,156 @@ func oidcProviderPaths(i *IdentityStore) []*framework.Path {
HelpSynopsis: "List OIDC scopes", HelpSynopsis: "List OIDC scopes",
HelpDescription: "List all configured OIDC scopes in the identity backend.", 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 // pathOIDCCreateUpdateAssignment is used to create a new assignment or update an existing one
func (i *IdentityStore) pathOIDCCreateUpdateAssignment(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { func (i *IdentityStore) pathOIDCCreateUpdateAssignment(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string) name := d.Get("name").(string)
@ -199,7 +362,19 @@ func (i *IdentityStore) pathOIDCReadAssignment(ctx context.Context, req *logical
// pathOIDCDeleteAssignment is used to delete an assignment // pathOIDCDeleteAssignment is used to delete an assignment
func (i *IdentityStore) pathOIDCDeleteAssignment(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { func (i *IdentityStore) pathOIDCDeleteAssignment(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string) 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 { if err != nil {
return nil, err return nil, err
} }
@ -261,7 +436,6 @@ func (i *IdentityStore) pathOIDCCreateUpdateScope(ctx context.Context, req *logi
String: scope.Template, String: scope.Template,
Entity: new(logical.Entity), Entity: new(logical.Entity),
Groups: make([]*logical.Group, 0), Groups: make([]*logical.Group, 0),
// namespace?
}) })
if err != nil { if err != nil {
return logical.ErrorResponse("error parsing template: %s", err.Error()), 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 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
}

View File

@ -8,6 +8,414 @@ import (
"github.com/hashicorp/vault/sdk/logical" "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 // TestOIDC_Path_OIDC_ProviderScope_ReservedName tests that the reserved name
// "openid" cannot be used when creating a scope // "openid" cannot be used when creating a scope
func TestOIDC_Path_OIDC_ProviderScope_ReservedName(t *testing.T) { 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 // TestOIDC_Path_OIDC_ProviderAssignment_Update tests Update operations for assignments
func TestOIDC_Path_OIDC_ProviderAssignment_Update(t *testing.T) { func TestOIDC_Path_OIDC_ProviderAssignment_Update(t *testing.T) {
c, _, _ := TestCoreUnsealed(t) c, _, _ := TestCoreUnsealed(t)

View File

@ -455,6 +455,40 @@ func TestOIDC_Path_OIDCKey(t *testing.T) {
expectStrings(t, respListKeyAfterDelete.Data["keys"].([]string), expectedStrings) 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 // TestOIDC_PublicKeys tests that public keys are updated by
// key creation, rotation, and deletion // key creation, rotation, and deletion
func TestOIDC_PublicKeys(t *testing.T) { func TestOIDC_PublicKeys(t *testing.T) {