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:
parent
e4e8555e3a
commit
fec8e8b21a
|
@ -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 {
|
||||
|
|
|
@ -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,9 +134,156 @@ 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
|
||||
func (i *IdentityStore) pathOIDCCreateUpdateAssignment(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue