feature: OIDC provider assignment API (#12198)
* 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 * remove use of oidcCache * refactor struct and var names * harmonize test name conventions * add changelog and refactor - add changelog - be more explicit in the case where we do not recieve a path field * remove extra period from changelog * update assignment path * removed unused name field
This commit is contained in:
parent
5e86a34e3e
commit
40fd60342a
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:feature
|
||||||
|
**OIDC Identity Provider**: Enable Vault to be an OpenID Connect identity provider.
|
||||||
|
```
|
|
@ -110,6 +110,7 @@ func (i *IdentityStore) paths() []*framework.Path {
|
||||||
lookupPaths(i),
|
lookupPaths(i),
|
||||||
upgradePaths(i),
|
upgradePaths(i),
|
||||||
oidcPaths(i),
|
oidcPaths(i),
|
||||||
|
oidcProviderPaths(i),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
package vault
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/sdk/framework"
|
||||||
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
|
)
|
||||||
|
|
||||||
|
type assignment struct {
|
||||||
|
Groups []string `json:"groups"`
|
||||||
|
Entities []string `json:"entities"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
oidcProviderPrefix = "oidc_provider/"
|
||||||
|
assignmentPath = oidcProviderPrefix + "assignment/"
|
||||||
|
)
|
||||||
|
|
||||||
|
func oidcProviderPaths(i *IdentityStore) []*framework.Path {
|
||||||
|
return []*framework.Path{
|
||||||
|
{
|
||||||
|
Pattern: "oidc/assignment/" + framework.GenericNameRegex("name"),
|
||||||
|
Fields: map[string]*framework.FieldSchema{
|
||||||
|
"name": {
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "Name of the assignment",
|
||||||
|
},
|
||||||
|
"entities": {
|
||||||
|
Type: framework.TypeCommaStringSlice,
|
||||||
|
Description: "Comma separated string or array of identity entity names",
|
||||||
|
},
|
||||||
|
"groups": {
|
||||||
|
Type: framework.TypeCommaStringSlice,
|
||||||
|
Description: "Comma separated string or array of identity group names",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Operations: map[logical.Operation]framework.OperationHandler{
|
||||||
|
logical.UpdateOperation: &framework.PathOperation{
|
||||||
|
Callback: i.pathOIDCCreateUpdateAssignment,
|
||||||
|
},
|
||||||
|
logical.CreateOperation: &framework.PathOperation{
|
||||||
|
Callback: i.pathOIDCCreateUpdateAssignment,
|
||||||
|
},
|
||||||
|
logical.ReadOperation: &framework.PathOperation{
|
||||||
|
Callback: i.pathOIDCReadAssignment,
|
||||||
|
},
|
||||||
|
logical.DeleteOperation: &framework.PathOperation{
|
||||||
|
Callback: i.pathOIDCDeleteAssignment,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExistenceCheck: i.pathOIDCAssignmentExistenceCheck,
|
||||||
|
HelpSynopsis: "CRUD operations for OIDC assignments.",
|
||||||
|
HelpDescription: "Create, Read, Update, and Delete OIDC assignments.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pattern: "oidc/assignment/?$",
|
||||||
|
Operations: map[logical.Operation]framework.OperationHandler{
|
||||||
|
logical.ListOperation: &framework.PathOperation{
|
||||||
|
Callback: i.pathOIDCListAssignment,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HelpSynopsis: "List OIDC assignments",
|
||||||
|
HelpDescription: "List all configured OIDC assignments in the identity backend.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
var assignment assignment
|
||||||
|
if req.Operation == logical.UpdateOperation {
|
||||||
|
entry, err := req.Storage.Get(ctx, assignmentPath+name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if entry != nil {
|
||||||
|
if err := entry.DecodeJSON(&assignment); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if entitiesRaw, ok := d.GetOk("entities"); ok {
|
||||||
|
assignment.Entities = entitiesRaw.([]string)
|
||||||
|
} else if req.Operation == logical.CreateOperation {
|
||||||
|
assignment.Entities = d.GetDefaultOrZero("entities").([]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if groupsRaw, ok := d.GetOk("groups"); ok {
|
||||||
|
assignment.Groups = groupsRaw.([]string)
|
||||||
|
} else if req.Operation == logical.CreateOperation {
|
||||||
|
assignment.Groups = d.GetDefaultOrZero("groups").([]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store assignment
|
||||||
|
entry, err := logical.StorageEntryJSON(assignmentPath+name, assignment)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := req.Storage.Put(ctx, entry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathOIDCListAssignment is used to list assignments
|
||||||
|
func (i *IdentityStore) pathOIDCListAssignment(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
|
assignments, err := req.Storage.List(ctx, assignmentPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return logical.ListResponse(assignments), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathOIDCReadAssignment is used to read an existing assignment
|
||||||
|
func (i *IdentityStore) pathOIDCReadAssignment(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
|
name := d.Get("name").(string)
|
||||||
|
|
||||||
|
entry, err := req.Storage.Get(ctx, assignmentPath+name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if entry == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var assignment assignment
|
||||||
|
if err := entry.DecodeJSON(&assignment); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &logical.Response{
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"groups": assignment.Groups,
|
||||||
|
"entities": assignment.Entities,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IdentityStore) pathOIDCAssignmentExistenceCheck(ctx context.Context, req *logical.Request, d *framework.FieldData) (bool, error) {
|
||||||
|
name := d.Get("name").(string)
|
||||||
|
|
||||||
|
entry, err := req.Storage.Get(ctx, assignmentPath+name)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry != nil, nil
|
||||||
|
}
|
|
@ -0,0 +1,195 @@
|
||||||
|
package vault
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-test/deep"
|
||||||
|
"github.com/hashicorp/vault/helper/namespace"
|
||||||
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestOIDC_Path_OIDC_ProviderAssignment tests CRUD operations for assignments
|
||||||
|
func TestOIDC_Path_OIDC_ProviderAssignment(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)
|
||||||
|
|
||||||
|
// Read "test-assignment" 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update "test-assignment" -- should succeed
|
||||||
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||||
|
Path: "oidc/assignment/test-assignment",
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"groups": "my-group",
|
||||||
|
"entities": "my-entity",
|
||||||
|
},
|
||||||
|
Storage: storage,
|
||||||
|
})
|
||||||
|
expectSuccess(t, resp, err)
|
||||||
|
|
||||||
|
// 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{"my-group"},
|
||||||
|
"entities": []string{"my-entity"},
|
||||||
|
}
|
||||||
|
if diff := deep.Equal(expected, resp.Data); diff != nil {
|
||||||
|
t.Fatal(diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete test-assignment -- should succeed
|
||||||
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||||
|
Path: "oidc/assignment/test-assignment",
|
||||||
|
Operation: logical.DeleteOperation,
|
||||||
|
Storage: storage,
|
||||||
|
})
|
||||||
|
expectSuccess(t, resp, err)
|
||||||
|
|
||||||
|
// Read "test-assignment" again and validate
|
||||||
|
resp, _ = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||||
|
Path: "oidc/assignment/test-assignment",
|
||||||
|
Operation: logical.ReadOperation,
|
||||||
|
Storage: storage,
|
||||||
|
})
|
||||||
|
if resp != nil {
|
||||||
|
t.Fatalf("expected nil but got resp: %#v", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOIDC_Path_OIDC_ProviderAssignment_Update tests Update operations for assignments
|
||||||
|
func TestOIDC_Path_OIDC_ProviderAssignment_Update(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,
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"groups": "my-group",
|
||||||
|
"entities": "my-entity",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expectSuccess(t, resp, err)
|
||||||
|
|
||||||
|
// Read "test-assignment" 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{"my-group"},
|
||||||
|
"entities": []string{"my-entity"},
|
||||||
|
}
|
||||||
|
if diff := deep.Equal(expected, resp.Data); diff != nil {
|
||||||
|
t.Fatal(diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update "test-assignment" -- should succeed
|
||||||
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||||
|
Path: "oidc/assignment/test-assignment",
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"groups": "my-group2",
|
||||||
|
},
|
||||||
|
Storage: storage,
|
||||||
|
})
|
||||||
|
expectSuccess(t, resp, err)
|
||||||
|
|
||||||
|
// 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{"my-group2"},
|
||||||
|
"entities": []string{"my-entity"},
|
||||||
|
}
|
||||||
|
if diff := deep.Equal(expected, resp.Data); diff != nil {
|
||||||
|
t.Fatal(diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOIDC_Path_OIDC_ProviderAssignment_List tests the List operation for assignments
|
||||||
|
func TestOIDC_Path_OIDC_ProviderAssignment_List(t *testing.T) {
|
||||||
|
c, _, _ := TestCoreUnsealed(t)
|
||||||
|
ctx := namespace.RootContext(nil)
|
||||||
|
storage := &logical.InmemStorage{}
|
||||||
|
|
||||||
|
// Prepare two assignments, test-assignment1 and test-assignment2
|
||||||
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||||
|
Path: "oidc/assignment/test-assignment1",
|
||||||
|
Operation: logical.CreateOperation,
|
||||||
|
Storage: storage,
|
||||||
|
})
|
||||||
|
|
||||||
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||||
|
Path: "oidc/assignment/test-assignment2",
|
||||||
|
Operation: logical.CreateOperation,
|
||||||
|
Storage: storage,
|
||||||
|
})
|
||||||
|
|
||||||
|
// list assignments
|
||||||
|
respListAssignments, listErr := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||||
|
Path: "oidc/assignment",
|
||||||
|
Operation: logical.ListOperation,
|
||||||
|
Storage: storage,
|
||||||
|
})
|
||||||
|
expectSuccess(t, respListAssignments, listErr)
|
||||||
|
|
||||||
|
// validate list response
|
||||||
|
expectedStrings := map[string]interface{}{"test-assignment1": true, "test-assignment2": true}
|
||||||
|
expectStrings(t, respListAssignments.Data["keys"].([]string), expectedStrings)
|
||||||
|
|
||||||
|
// delete test-assignment2
|
||||||
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||||
|
Path: "oidc/assignment/test-assignment2",
|
||||||
|
Operation: logical.DeleteOperation,
|
||||||
|
Storage: storage,
|
||||||
|
})
|
||||||
|
|
||||||
|
// list assignments again and validate response
|
||||||
|
respListAssignmentAfterDelete, listErrAfterDelete := c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||||
|
Path: "oidc/assignment",
|
||||||
|
Operation: logical.ListOperation,
|
||||||
|
Storage: storage,
|
||||||
|
})
|
||||||
|
expectSuccess(t, respListAssignmentAfterDelete, listErrAfterDelete)
|
||||||
|
|
||||||
|
// validate list response
|
||||||
|
delete(expectedStrings, "test-assignment2")
|
||||||
|
expectStrings(t, respListAssignmentAfterDelete.Data["keys"].([]string), expectedStrings)
|
||||||
|
}
|
Loading…
Reference in New Issue