Skip to content

Commit

Permalink
feature: OIDC provider assignment API (#12198)
Browse files Browse the repository at this point in the history
* 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
fairclothjm committed Aug 17, 2021
1 parent 5e505ec commit 9cc94fe
Show file tree
Hide file tree
Showing 4 changed files with 362 additions and 0 deletions.
3 changes: 3 additions & 0 deletions changelog/12198.txt
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.
```
1 change: 1 addition & 0 deletions vault/identity_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func (i *IdentityStore) paths() []*framework.Path {
lookupPaths(i),
upgradePaths(i),
oidcPaths(i),
oidcProviderPaths(i),
)
}

Expand Down
163 changes: 163 additions & 0 deletions vault/identity_store_oidc_provider.go
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
}
195 changes: 195 additions & 0 deletions vault/identity_store_oidc_provider_test.go
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)
}

0 comments on commit 9cc94fe

Please sign in to comment.