forked from hashicorp/vault
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature: OIDC provider assignment API (hashicorp#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
- Loading branch information
1 parent
1944d7a
commit 159a997
Showing
4 changed files
with
362 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:feature | ||
**OIDC Identity Provider**: Enable Vault to be an OpenID Connect identity provider. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |