Skip to content

Commit

Permalink
Vault: added groups/roles check before dynamic roles creation (#112)
Browse files Browse the repository at this point in the history
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 <[email protected]>
Reviewed-by: Anton Sidelnikov
Reviewed-by: Vladimir Vshivkov
  • Loading branch information
artem-lifshits authored Oct 25, 2022
1 parent 61a3faf commit 2845f80
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 28 deletions.
28 changes: 18 additions & 10 deletions acceptance/roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions doc/source/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <optional>)` - Create a project-scoped role with given project ID. Mutually exclusive with
`project_name`.
Expand Down
37 changes: 37 additions & 0 deletions openstack/common/utils.go
Original file line number Diff line number Diff line change
@@ -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
}
57 changes: 57 additions & 0 deletions openstack/fixtures/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
})
}
69 changes: 59 additions & 10 deletions openstack/path_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand All @@ -342,15 +391,15 @@ 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
}

// 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
Expand All @@ -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
}
17 changes: 11 additions & 6 deletions openstack/path_role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package openstack
import (
"context"
"fmt"
thClient "github.com/gophercloud/gophercloud/testhelper/client"
"regexp"
"testing"
"time"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down

0 comments on commit 2845f80

Please sign in to comment.