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:
John-Michael Faircloth 2021-08-17 15:55:06 -05:00 committed by GitHub
parent 5e86a34e3e
commit 40fd60342a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 362 additions and 0 deletions

3
changelog/12198.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
**OIDC Identity Provider**: Enable Vault to be an OpenID Connect identity provider.
```

View File

@ -110,6 +110,7 @@ func (i *IdentityStore) paths() []*framework.Path {
lookupPaths(i), lookupPaths(i),
upgradePaths(i), upgradePaths(i),
oidcPaths(i), oidcPaths(i),
oidcProviderPaths(i),
) )
} }

View File

@ -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
}

View File

@ -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)
}