From 40fd60342a7e9cbb7a6d685092f98898fe5612a8 Mon Sep 17 00:00:00 2001 From: John-Michael Faircloth Date: Tue, 17 Aug 2021 15:55:06 -0500 Subject: [PATCH] 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 --- changelog/12198.txt | 3 + vault/identity_store.go | 1 + vault/identity_store_oidc_provider.go | 163 +++++++++++++++++ vault/identity_store_oidc_provider_test.go | 195 +++++++++++++++++++++ 4 files changed, 362 insertions(+) create mode 100644 changelog/12198.txt create mode 100644 vault/identity_store_oidc_provider.go create mode 100644 vault/identity_store_oidc_provider_test.go diff --git a/changelog/12198.txt b/changelog/12198.txt new file mode 100644 index 000000000..4d7e06141 --- /dev/null +++ b/changelog/12198.txt @@ -0,0 +1,3 @@ +```release-note:feature +**OIDC Identity Provider**: Enable Vault to be an OpenID Connect identity provider. +``` diff --git a/vault/identity_store.go b/vault/identity_store.go index 1a160b175..944a077fe 100644 --- a/vault/identity_store.go +++ b/vault/identity_store.go @@ -110,6 +110,7 @@ func (i *IdentityStore) paths() []*framework.Path { lookupPaths(i), upgradePaths(i), oidcPaths(i), + oidcProviderPaths(i), ) } diff --git a/vault/identity_store_oidc_provider.go b/vault/identity_store_oidc_provider.go new file mode 100644 index 000000000..f49534399 --- /dev/null +++ b/vault/identity_store_oidc_provider.go @@ -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 +} diff --git a/vault/identity_store_oidc_provider_test.go b/vault/identity_store_oidc_provider_test.go new file mode 100644 index 000000000..114830636 --- /dev/null +++ b/vault/identity_store_oidc_provider_test.go @@ -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) +}