From 2845f8052acc3cc0fef1dc4228c6d4df5deab9b4 Mon Sep 17 00:00:00 2001 From: Artem Lifshits <55093318+artem-lifshits@users.noreply.github.com> Date: Tue, 25 Oct 2022 14:15:19 +0300 Subject: [PATCH] Vault: added groups/roles check before dynamic roles creation (#112) Vault: added groups/roles check before dynamic roles creation Checks added on roles/groups creation for dynamic roles whether roles/groups exist on the cloud. If role/group does not exist on the cloud error will be raised Refers to: #105 Reviewed-by: Rodion Gyrbu Reviewed-by: Anton Sidelnikov Reviewed-by: Vladimir Vshivkov --- acceptance/roles_test.go | 28 +++++++++----- doc/source/api.md | 4 +- openstack/common/utils.go | 37 +++++++++++++++++++ openstack/fixtures/helpers.go | 57 +++++++++++++++++++++++++++++ openstack/path_role.go | 69 ++++++++++++++++++++++++++++++----- openstack/path_role_test.go | 17 ++++++--- 6 files changed, 184 insertions(+), 28 deletions(-) create mode 100644 openstack/common/utils.go diff --git a/acceptance/roles_test.go b/acceptance/roles_test.go index 617941c..93828e5 100644 --- a/acceptance/roles_test.go +++ b/acceptance/roles_test.go @@ -11,7 +11,6 @@ import ( "github.com/gophercloud/gophercloud/acceptance/tools" "github.com/hashicorp/vault/sdk/helper/jsonutil" - "github.com/opentelekomcloud/vault-plugin-secrets-openstack/openstack" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -42,19 +41,20 @@ func extractRoleData(t *testing.T, resp *http.Response) *roleData { func (p *PluginTest) TestRoleLifecycle() { t := p.T() - cloud := &openstack.OsCloud{ - Name: openstack.RandomString(openstack.NameDefaultSet, 10), - AuthURL: "https://example.com/v3", - UserDomainName: openstack.RandomString(openstack.NameDefaultSet, 10), - Username: openstack.RandomString(openstack.NameDefaultSet, 10), - Password: openstack.RandomString(openstack.PwdDefaultSet, 10), - UsernameTemplate: "u-{{ .RoleName }}-{{ random 4 }}", - } - p.makeCloud(cloud) + cloud := openstackCloudConfig(t) + require.NotEmpty(t, cloud) data := expectedRoleData(cloud.Name) roleName := "test-write" + resp, err := p.vaultDo( + http.MethodPost, + cloudURL(cloudName), + cloudToCloudMap(cloud), + ) + require.NoError(t, err) + assert.Equal(t, http.StatusNoContent, resp.StatusCode, readJSONResponse(t, resp)) + t.Run("WriteRole", func(t *testing.T) { resp, err := p.vaultDo( http.MethodPost, @@ -108,6 +108,14 @@ func (p *PluginTest) TestRoleLifecycle() { require.NoError(t, err) assert.Equal(t, http.StatusNoContent, resp.StatusCode) }) + + resp, err = p.vaultDo( + http.MethodDelete, + cloudURL(cloudName), + nil, + ) + require.NoError(t, err) + assertStatusCode(t, http.StatusNoContent, resp) } func roleURL(name string) string { diff --git a/doc/source/api.md b/doc/source/api.md index 1814fb3..ecb171b 100644 --- a/doc/source/api.md +++ b/doc/source/api.md @@ -162,10 +162,10 @@ created. If the role exists, it will be updated with the new attributes. Valid choices are `token` and `password`. - `user_groups` `(list: [])` - Specifies list of existing OpenStack groups this Vault role is allowed to assume. - This is a comma-separated string or JSON array. + This is a comma-separated string or JSON array. If provided `user_groups` don't exist an error will be raised. - `user_roles` `(list: [])` - Specifies list of existing OpenStack roles this Vault role is allowed to assume. - This is a comma-separated string or JSON array. + This is a comma-separated string or JSON array. If provided `user_roles` don't exist an error will be raised. - `project_id` `(string: )` - Create a project-scoped role with given project ID. Mutually exclusive with `project_name`. diff --git a/openstack/common/utils.go b/openstack/common/utils.go new file mode 100644 index 0000000..54c37f2 --- /dev/null +++ b/openstack/common/utils.go @@ -0,0 +1,37 @@ +package common + +import ( + "github.com/gophercloud/gophercloud/openstack/identity/v3/groups" + "github.com/gophercloud/gophercloud/openstack/identity/v3/roles" +) + +func CheckGroupSlices(groups []groups.Group, userEntities []string) []string { + var existingEntity []string + for _, entity := range groups { + existingEntity = append(existingEntity, entity.Name) + } + return sliceSubtraction(userEntities, existingEntity) +} + +func CheckRolesSlices(roles []roles.Role, userEntities []string) []string { + var existingEntity []string + for _, entity := range roles { + existingEntity = append(existingEntity, entity.Name) + } + return sliceSubtraction(userEntities, existingEntity) +} + +func sliceSubtraction(a, b []string) (diff []string) { + m := make(map[string]bool) + + for _, item := range b { + m[item] = true + } + + for _, item := range a { + if _, ok := m[item]; !ok { + diff = append(diff, item) + } + } + return +} diff --git a/openstack/fixtures/helpers.go b/openstack/fixtures/helpers.go index e1c9a6e..6189f2f 100644 --- a/openstack/fixtures/helpers.go +++ b/openstack/fixtures/helpers.go @@ -222,6 +222,51 @@ func handleListUsers(t *testing.T, w http.ResponseWriter, r *http.Request, userI `, userID, userName) } +func handleListGroups(t *testing.T, w http.ResponseWriter, r *http.Request) { + t.Helper() + + th.TestHeader(t, r, "Accept", "application/json") + th.TestMethod(t, r, "GET") + + w.Header().Add("Content-Type", "application/json") + + _, _ = fmt.Fprintf(w, ` +{ + "groups": [ + { + "domain_id": "698f9bf85ca9437a9b2f41132ab3aa0e", + "create_time": 1663793877134, + "name": "default", + "description": "default group", + "links": { + "next": null, + "previous": null, + "self": "https://example.com/v3/groups/2d54491f3a8447639d02184ef33ea8b6" + }, + "id": "2d54491f3a8447639d02184ef33ea8b6" + }, + { + "domain_id": "698f9bf85ca9437a9b2f41132ab3aa0e", + "create_time": 1663792847545, + "name": "testing", + "description": "test-group", + "links": { + "next": null, + "previous": null, + "self": "https://example.com/v3/groups/c45a98d539524c1e92198d37089e6872" + }, + "id": "c45a98d539524c1e92198d37089e6872" + } + ], + "links": { + "next": null, + "previous": null, + "self": "https://example.com/v3/groups" + } +} +`) +} + func handleProjectList(t *testing.T, w http.ResponseWriter, r *http.Request, projectName string) { t.Helper() @@ -269,6 +314,7 @@ type EnabledMocks struct { UserList bool UserDelete bool UserGet bool + GroupList bool } func SetupKeystoneMock(t *testing.T, userID, projectName string, enabled EnabledMocks) { @@ -353,4 +399,15 @@ func SetupKeystoneMock(t *testing.T, userID, projectName string, enabled Enabled w.WriteHeader(404) } }) + + th.Mux.HandleFunc("/v3/groups", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + if enabled.GroupList { + handleListGroups(t, w, r) + } + default: + w.WriteHeader(404) + } + }) } diff --git a/openstack/path_role.go b/openstack/path_role.go index de0d379..ebc8536 100644 --- a/openstack/path_role.go +++ b/openstack/path_role.go @@ -4,8 +4,12 @@ import ( "context" "errors" "fmt" + "github.com/opentelekomcloud/vault-plugin-secrets-openstack/openstack/common" "time" + "github.com/gophercloud/gophercloud/openstack/identity/v3/groups" + "github.com/gophercloud/gophercloud/openstack/identity/v3/roles" + "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" ) @@ -239,11 +243,13 @@ func (b *backend) pathRoleUpdate(ctx context.Context, req *logical.Request, d *f return logical.ErrorResponse("cloud is required when creating a role"), nil } } - cld, err := b.getSharedCloud(cloudName).getCloudConfig(ctx, req.Storage) + + cloud := b.getSharedCloud(cloudName) + cloudConf, err := cloud.getCloudConfig(ctx, req.Storage) if err != nil { return nil, err } - if cld == nil { + if cloudConf == nil { return logical.ErrorResponse("cloud `%s` doesn't exist", cloudName), nil } @@ -305,18 +311,61 @@ func (b *backend) pathRoleUpdate(ctx context.Context, req *logical.Request, d *f entry.Extensions = ext.(map[string]string) } - if groups, ok := d.GetOk("user_groups"); ok { + if userGroups, ok := d.GetOk("user_groups"); ok { if entry.Root { return logical.ErrorResponse(errInvalidForRoot, "user groups"), nil } - entry.UserGroups = groups.([]string) + client, err := cloud.getClient(ctx, req.Storage) + if err != nil { + return nil, err + } + + token := tokens.Get(client, client.Token()) + user, err := token.ExtractUser() + if err != nil { + return nil, fmt.Errorf("error extracting the user from token: %w", err) + } + + groupPages, err := groups.List(client, groups.ListOpts{ + DomainID: user.Domain.ID, + }).AllPages() + if err != nil { + return nil, err + } + + groupList, err := groups.ExtractGroups(groupPages) + if err != nil { + return nil, err + } + + if v := common.CheckGroupSlices(groupList, userGroups.([]string)); len(v) > 0 { + return nil, fmt.Errorf("group %v doesn't exist", v) + } + entry.UserGroups = userGroups.([]string) } - if roles, ok := d.GetOk("user_roles"); ok { + if userRoles, ok := d.GetOk("user_roles"); ok { if entry.Root { return logical.ErrorResponse(errInvalidForRoot, "user roles"), nil } - entry.UserRoles = roles.([]string) + client, err := cloud.getClient(ctx, req.Storage) + if err != nil { + return nil, err + } + rolePages, err := roles.List(client, nil).AllPages() + if err != nil { + return nil, fmt.Errorf("unable to query roles: %w", err) + } + + roleList, err := roles.ExtractRoles(rolePages) + if err != nil { + return nil, fmt.Errorf("unable to retrieve roles: %w", err) + } + + if v := common.CheckRolesSlices(roleList, userRoles.([]string)); len(v) > 0 { + return nil, fmt.Errorf("role %v doesn't exist", v) + } + entry.UserRoles = userRoles.([]string) } if err := saveRole(ctx, entry, req.Storage); err != nil { @@ -342,7 +391,7 @@ func (b *backend) pathRoleDelete(ctx context.Context, req *logical.Request, d *f } func (b *backend) pathRolesList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - roles, err := req.Storage.List(ctx, rolesStoragePath+"/") + rolesList, err := req.Storage.List(ctx, rolesStoragePath+"/") if err != nil { return nil, err } @@ -350,7 +399,7 @@ func (b *backend) pathRolesList(ctx context.Context, req *logical.Request, d *fr // filter by cloud if cloud, ok := d.GetOk("cloud"); ok { var refinedRoles []string - for _, name := range roles { + for _, name := range rolesList { role, err := getRoleByName(ctx, name, req.Storage) if err != nil { return nil, err @@ -360,8 +409,8 @@ func (b *backend) pathRolesList(ctx context.Context, req *logical.Request, d *fr } refinedRoles = append(refinedRoles, name) } - roles = refinedRoles + rolesList = refinedRoles } - return logical.ListResponse(roles), nil + return logical.ListResponse(rolesList), nil } diff --git a/openstack/path_role_test.go b/openstack/path_role_test.go index 3c36d53..90aab0c 100644 --- a/openstack/path_role_test.go +++ b/openstack/path_role_test.go @@ -3,6 +3,7 @@ package openstack import ( "context" "fmt" + thClient "github.com/gophercloud/gophercloud/testhelper/client" "regexp" "testing" "time" @@ -113,7 +114,6 @@ func TestRoleGet(t *testing.T) { } func TestRoleExistence(t *testing.T) { - t.Parallel() t.Run("existing", func(t *testing.T) { t.Parallel() @@ -313,7 +313,6 @@ func TestRoleDelete(t *testing.T) { } func TestRoleCreate(t *testing.T) { - t.Parallel() id, _ := uuid.GenerateUUID() t.Run("ok", func(t *testing.T) { @@ -363,7 +362,6 @@ func TestRoleCreate(t *testing.T) { for name, data := range cases { t.Run(name, func(t *testing.T) { data := data - t.Parallel() roleName := data.Name inputRole := fixtures.SanitizedMap(roleToMap(data)) @@ -493,12 +491,20 @@ func TestRoleCreate(t *testing.T) { func preCreateCloud(t *testing.T, s logical.Storage) string { t.Helper() + userID, _ := uuid.GenerateUUID() + fixtures.SetupKeystoneMock(t, userID, "", fixtures.EnabledMocks{ + TokenPost: true, + TokenGet: true, + GroupList: true, + }) + name := randomRoleName() cloudStoragePath := storageCloudKey(name) - + testClient := thClient.ServiceClient() + authURL := testClient.Endpoint + "v3" entry, err := logical.StorageEntryJSON(cloudStoragePath, &OsCloud{ Name: testCloudName, - AuthURL: testAuthURL, + AuthURL: authURL, UserDomainName: testUserDomainName, Username: testUsername, Password: testPassword1, @@ -513,7 +519,6 @@ func preCreateCloud(t *testing.T, s logical.Storage) string { } func TestRoleUpdate(t *testing.T) { - t.Parallel() b, s := testBackend(t) cloudName := preCreateCloud(t, s)