Skip to content

Commit

Permalink
Vault: Rotate Static Role Credentials (#100)
Browse files Browse the repository at this point in the history
Vault: Rotate Static Role Credentials

This PR add role-rotate path and relevant acceptance/unit tests, api documentation.
Closes: #74
Acceptance tests (failing on unrelated info test):
Running acceptance tests...
=== RUN   TestPlugin
=== RUN   TestPlugin/TestCloudLifecycle
=== RUN   TestPlugin/TestCloudLifecycle/WriteCloud
=== RUN   TestPlugin/TestCloudLifecycle/ReadCloud
=== RUN   TestPlugin/TestCloudLifecycle/ListClouds
=== RUN   TestPlugin/TestCloudLifecycle/ListClouds/method-LIST
=== PAUSE TestPlugin/TestCloudLifecycle/ListClouds/method-LIST
=== RUN   TestPlugin/TestCloudLifecycle/ListClouds/method-GET
=== PAUSE TestPlugin/TestCloudLifecycle/ListClouds/method-GET
=== CONT  TestPlugin/TestCloudLifecycle/ListClouds/method-LIST
=== CONT  TestPlugin/TestCloudLifecycle/ListClouds/method-GET
=== RUN   TestPlugin/TestCloudLifecycle/DeleteCloud
=== RUN   TestPlugin/TestCredsLifecycle
=== RUN   TestPlugin/TestCredsLifecycle/user_password
=== RUN   TestPlugin/TestCredsLifecycle/root_token
=== RUN   TestPlugin/TestCredsLifecycle/user_token
=== RUN   TestPlugin/TestInfo
info_test.go:42:
Error Trace:    info_test.go:42
Error:          Should NOT be empty, but was &{    }
Test:           TestPlugin/TestInfo
=== RUN   TestPlugin/TestRoleLifecycle
roles_test.go:53: Cloud with name wbnyh80fsd was created
=== RUN   TestPlugin/TestRoleLifecycle/WriteRole
=== RUN   TestPlugin/TestRoleLifecycle/ReadRole
=== RUN   TestPlugin/TestRoleLifecycle/ListRoles
=== RUN   TestPlugin/TestRoleLifecycle/ListRoles/method-LIST
=== PAUSE TestPlugin/TestRoleLifecycle/ListRoles/method-LIST
=== RUN   TestPlugin/TestRoleLifecycle/ListRoles/method-GET
=== PAUSE TestPlugin/TestRoleLifecycle/ListRoles/method-GET
=== CONT  TestPlugin/TestRoleLifecycle/ListRoles/method-LIST
=== CONT  TestPlugin/TestRoleLifecycle/ListRoles/method-GET
=== RUN   TestPlugin/TestRoleLifecycle/DeleteRole
=== CONT  TestPlugin/TestRoleLifecycle
plugin_test.go:337: Cloud with name wbnyh80fsd has been removed
=== RUN   TestPlugin/TestRootRotate
rotate_test.go:65: Cloud with name default1 was created
rotate_test.go:68: Cloud with name xvoi was created
plugin_test.go:337: Cloud with name xvoi has been removed
plugin_test.go:337: Cloud with name default1 has been removed
=== RUN   TestPlugin/TestStaticCredsLifecycle
=== RUN   TestPlugin/TestStaticCredsLifecycle/user_password
=== RUN   TestPlugin/TestStaticCredsLifecycle/user_token
=== RUN   TestPlugin/TestStaticRoleLifecycle
=== RUN   TestPlugin/TestStaticRoleLifecycle/WriteRole
=== RUN   TestPlugin/TestStaticRoleLifecycle/ReadRole
=== RUN   TestPlugin/TestStaticRoleLifecycle/ListRoles
=== RUN   TestPlugin/TestStaticRoleLifecycle/ListRoles/method-LIST
=== PAUSE TestPlugin/TestStaticRoleLifecycle/ListRoles/method-LIST
=== RUN   TestPlugin/TestStaticRoleLifecycle/ListRoles/method-GET
=== PAUSE TestPlugin/TestStaticRoleLifecycle/ListRoles/method-GET
=== CONT  TestPlugin/TestStaticRoleLifecycle/ListRoles/method-LIST
=== CONT  TestPlugin/TestStaticRoleLifecycle/ListRoles/method-GET
=== RUN   TestPlugin/TestStaticRoleLifecycle/DeleteRole
--- FAIL: TestPlugin (22.93s)
--- PASS: TestPlugin/TestCloudLifecycle (0.11s)
--- PASS: TestPlugin/TestCloudLifecycle/WriteCloud (0.10s)
--- PASS: TestPlugin/TestCloudLifecycle/ReadCloud (0.00s)
--- PASS: TestPlugin/TestCloudLifecycle/ListClouds (0.00s)
--- PASS: TestPlugin/TestCloudLifecycle/ListClouds/method-LIST (0.00s)
--- PASS: TestPlugin/TestCloudLifecycle/ListClouds/method-GET (0.00s)
--- PASS: TestPlugin/TestCloudLifecycle/DeleteCloud (0.00s)
--- PASS: TestPlugin/TestCredsLifecycle (5.89s)
--- PASS: TestPlugin/TestCredsLifecycle/user_password (1.90s)
--- PASS: TestPlugin/TestCredsLifecycle/root_token (0.97s)
--- PASS: TestPlugin/TestCredsLifecycle/user_token (2.15s)
--- FAIL: TestPlugin/TestInfo (0.00s)
--- PASS: TestPlugin/TestRoleLifecycle (0.01s)
--- PASS: TestPlugin/TestRoleLifecycle/WriteRole (0.00s)
--- PASS: TestPlugin/TestRoleLifecycle/ReadRole (0.00s)
--- PASS: TestPlugin/TestRoleLifecycle/ListRoles (0.00s)
--- PASS: TestPlugin/TestRoleLifecycle/ListRoles/method-LIST (0.00s)
--- PASS: TestPlugin/TestRoleLifecycle/ListRoles/method-GET (0.00s)
--- PASS: TestPlugin/TestRoleLifecycle/DeleteRole (0.00s)
--- PASS: TestPlugin/TestRootRotate (5.42s)
--- PASS: TestPlugin/TestStaticCredsLifecycle (8.19s)
--- PASS: TestPlugin/TestStaticCredsLifecycle/user_password (3.34s)
--- PASS: TestPlugin/TestStaticCredsLifecycle/user_token (3.77s)
--- PASS: TestPlugin/TestStaticRoleLifecycle (3.09s)
--- PASS: TestPlugin/TestStaticRoleLifecycle/WriteRole (1.13s)
--- PASS: TestPlugin/TestStaticRoleLifecycle/ReadRole (0.01s)
--- PASS: TestPlugin/TestStaticRoleLifecycle/ListRoles (0.00s)
--- PASS: TestPlugin/TestStaticRoleLifecycle/ListRoles/method-LIST (0.01s)
--- PASS: TestPlugin/TestStaticRoleLifecycle/ListRoles/method-GET (0.01s)
--- PASS: TestPlugin/TestStaticRoleLifecycle/DeleteRole (0.00s)
FAIL
FAIL    github.com/opentelekomcloud/vault-plugin-secrets-openstack/acceptance   23.518s
FAIL
make: *** [functional] Error 1

Reviewed-by: Aloento <None>
Reviewed-by: Anton Sidelnikov <None>
  • Loading branch information
artem-lifshits authored Aug 18, 2022
1 parent 384f556 commit bd37e14
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 6 deletions.
12 changes: 12 additions & 0 deletions acceptance/static_creds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ func (p *PluginTest) TestStaticCredsLifecycle() {
require.NoError(t, err)
assertStatusCode(t, http.StatusOK, resp)

resp, err = p.vaultDo(
http.MethodPost,
staticRotateCredsURL(roleName),
nil,
)
require.NoError(t, err)
assertStatusCode(t, http.StatusNoContent, resp)

resp, err = p.vaultDo(
http.MethodDelete,
staticRoleURL(roleName),
Expand All @@ -128,6 +136,10 @@ func staticCredsURL(roleName string) string {
return fmt.Sprintf("/v1/openstack/static-creds/%s", roleName)
}

func staticRotateCredsURL(roleName string) string {
return fmt.Sprintf("/v1/openstack/rotate-role/%s", roleName)
}

func cloudToStaticRoleMap(data testStaticCase) map[string]interface{} {
return fixtures.SanitizedMap(map[string]interface{}{
"cloud": data.cloud,
Expand Down
21 changes: 21 additions & 0 deletions doc/source/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -641,3 +641,24 @@ $ curl \
}
}
```

## Rotate Static Role Credentials

When you have configured Vault with static role, you can use this endpoint to have the Vault rotate the password
for the static user. Password change will be performed.

Once this method is called, password for static user related to static role will be updated.

| Method | Path |
|:-------|:-------------------------------|
| `POST` | `/openstack/rotate-role/:name` |
| `PUT` | `/openstack/rotate-role/:name` |

### Sample Request

```shell
$ curl \
--header "X-Vault-Token: ..." \
--request POST \
http://127.0.0.1:8200/v1/openstack/rotate-role/:name
```
1 change: 1 addition & 0 deletions openstack/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend,
b.pathStaticRole(),
b.pathRotateRoot(),
b.pathCreds(),
b.pathRotateStaticCreds(),
b.pathStaticCreds(),
},
Secrets: []*framework.Secret{
Expand Down
72 changes: 71 additions & 1 deletion openstack/path_static_creds.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,19 @@ import (
)

const (
pathStaticCreds = "static-creds"
pathStaticCreds = "static-creds"
pathStaticCredsRotate = "rotate-role"

staticCredsHelpSyn = "Manage the Openstack static credentials with static roles."
staticCredsHelpDesc = `
This path allows you to read OpenStack secret stored by predefined static roles.
`

rotateStaticHelpSyn = "Rotate static role password."
rotateStaticHelpDesc = `
Rotate the static role user credentials.
Once this method is called, static role will now be the only entity that knows the static user password.
`
)

Expand All @@ -38,6 +46,29 @@ func (b *backend) pathStaticCreds() *framework.Path {
}
}

func (b *backend) pathRotateStaticCreds() *framework.Path {
return &framework.Path{
Pattern: fmt.Sprintf("%s/%s", pathStaticCredsRotate, framework.GenericNameRegex("role")),
Fields: map[string]*framework.FieldSchema{
"role": {
Type: framework.TypeString,
Required: true,
Description: "Specifies name of the static role which credentials will be rotated.",
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.CreateOperation: &framework.PathOperation{
Callback: b.rotateStaticCreds,
},
logical.UpdateOperation: &framework.PathOperation{
Callback: b.rotateStaticCreds,
},
},
HelpSynopsis: rotateStaticHelpSyn,
HelpDescription: rotateStaticHelpDesc,
}
}

func (b *backend) pathStaticCredsRead(ctx context.Context, r *logical.Request, d *framework.FieldData) (*logical.Response, error) {
roleName := d.Get("role").(string)
role, err := getStaticRoleByName(ctx, roleName, r)
Expand Down Expand Up @@ -112,6 +143,45 @@ func (b *backend) pathStaticCredsRead(ctx context.Context, r *logical.Request, d
return &logical.Response{Data: data}, nil
}

func (b *backend) rotateStaticCreds(ctx context.Context, r *logical.Request, d *framework.FieldData) (*logical.Response, error) {
roleName := d.Get("role").(string)
role, err := getStaticRoleByName(ctx, roleName, r)
if err != nil {
return nil, err
}

sharedCloud := b.getSharedCloud(role.Cloud)
if err != nil {
return nil, err
}

client, err := sharedCloud.getClient(ctx, r.Storage)
if err != nil {
return nil, err
}

newPassword, err := Passwords{}.Generate(ctx)
if err != nil {
return nil, err
}

err = users.ChangePassword(client, role.UserID, users.ChangePasswordOpts{
Password: newPassword,
OriginalPassword: role.Secret,
}).ExtractErr()
if err != nil {
return nil, err
}

role.Secret = newPassword

if err := saveStaticRole(ctx, role, r); err != nil {
return nil, err
}

return nil, nil
}

func getScopeFromStaticRole(role *roleStaticEntry) tokens.Scope {
var scope tokens.Scope
switch {
Expand Down
135 changes: 130 additions & 5 deletions openstack/path_static_creds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ func credsStaticPath(name string) string {
return fmt.Sprintf("%s/%s", "static-creds", name)
}

func rotateStaticCreds(name string) string {
return fmt.Sprintf("%s/%s", "rotate-role", name)
}

func TestStaticCredentialsRead_ok(t *testing.T) {
userID, _ := uuid.GenerateUUID()
secret, _ := uuid.GenerateUUID()
Expand Down Expand Up @@ -45,7 +49,7 @@ func TestStaticCredentialsRead_ok(t *testing.T) {
t.Run("user_token", func(t *testing.T) {
require.NoError(t, s.Put(context.Background(), cloudEntry))

roleName := createSaveRandomStaticRole(t, s, projectName, "token", secret)
roleName := createSaveRandomStaticRole(t, s, projectName, "token", secret, "")

res, err := b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.ReadOperation,
Expand All @@ -58,7 +62,7 @@ func TestStaticCredentialsRead_ok(t *testing.T) {
t.Run("user_password", func(t *testing.T) {
require.NoError(t, s.Put(context.Background(), cloudEntry))

roleName := createSaveRandomStaticRole(t, s, projectName, "password", secret)
roleName := createSaveRandomStaticRole(t, s, projectName, "password", secret, "")

res, err := b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.ReadOperation,
Expand All @@ -78,7 +82,7 @@ func TestStaticCredentialsRead_error(t *testing.T) {

b, s := testBackend(t, failVerbRead)

roleName := createSaveRandomStaticRole(t, s, "", "token", secret)
roleName := createSaveRandomStaticRole(t, s, "", "token", secret, "")

_, err := b.HandleRequest(context.Background(), &logical.Request{
Path: credsStaticPath(roleName),
Expand Down Expand Up @@ -120,7 +124,7 @@ func TestStaticCredentialsRead_error(t *testing.T) {

b, s := testBackend(t)

roleName := createSaveRandomStaticRole(t, s, data.ProjectName, data.ServiceType, secret)
roleName := createSaveRandomStaticRole(t, s, data.ProjectName, data.ServiceType, secret, "")

testClient := thClient.ServiceClient()
authURL := testClient.Endpoint + "v3"
Expand All @@ -146,7 +150,127 @@ func TestStaticCredentialsRead_error(t *testing.T) {
}
}

func createSaveRandomStaticRole(t *testing.T, s logical.Storage, projectName, sType string, secret string) string {
func TestRotateStaticCredentials_ok(t *testing.T) {
userID, _ := uuid.GenerateUUID()
secret, _ := uuid.GenerateUUID()
projectName := tools.RandomString("p", 5)

fixtures.SetupKeystoneMock(t, userID, projectName, fixtures.EnabledMocks{
TokenPost: true,
TokenGet: true,
PasswordChange: true,
})

testClient := thClient.ServiceClient()
authURL := testClient.Endpoint + "v3"

b, s := testBackend(t)
cloudEntry, err := logical.StorageEntryJSON(storageCloudKey(testCloudName), &OsCloud{
Name: testCloudName,
AuthURL: authURL,
UserDomainName: testUserDomainName,
Username: testUsername,
Password: testPassword1,
UsernameTemplate: testTemplate1,
})
require.NoError(t, err)

t.Run("user_token", func(t *testing.T) {
require.NoError(t, s.Put(context.Background(), cloudEntry))

roleName := createSaveRandomStaticRole(t, s, projectName, "token", secret, userID)

_, err := b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.CreateOperation,
Path: rotateStaticCreds(roleName),
Storage: s,
})
require.NoError(t, err)
})
t.Run("user_password", func(t *testing.T) {
require.NoError(t, s.Put(context.Background(), cloudEntry))

roleName := createSaveRandomStaticRole(t, s, projectName, "password", secret, userID)

res, err := b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.ReadOperation,
Path: credsStaticPath(roleName),
Storage: s,
})
require.NoError(t, err)
require.NotEmpty(t, res.Data)
})
}

func TestRotateStaticCredentials_error(t *testing.T) {
t.Parallel()

t.Run("read-fail", func(t *testing.T) {
userID, _ := uuid.GenerateUUID()
projectName := tools.RandomString("p", 5)
fixtures.SetupKeystoneMock(t, userID, projectName, fixtures.EnabledMocks{})

b, s := testBackend(t, failVerbRead)

roleName := createSaveRandomStaticRole(t, s, projectName, "password", "", "")

_, err := b.HandleRequest(context.Background(), &logical.Request{
Path: "rotate-role/" + roleName,
Operation: logical.CreateOperation,
Storage: s,
})
require.Error(t, err)
})

cases := map[string]fixtures.EnabledMocks{
"no-change": {
TokenPost: true, TokenGet: true,
},
"no-post": {
TokenGet: true, PasswordChange: true,
},
"no-get": {
TokenPost: true, PasswordChange: true,
},
}

for name, data := range cases {
t.Run(name, func(t *testing.T) {
data := data
userID, _ := uuid.GenerateUUID()
secret, _ := uuid.GenerateUUID()
projectName := tools.RandomString("p", 5)

fixtures.SetupKeystoneMock(t, userID, projectName, data)

testClient := thClient.ServiceClient()
authURL := testClient.Endpoint + "v3"

b, s := testBackend(t)
cloudEntry, err := logical.StorageEntryJSON(storageCloudKey(testCloudName), &OsCloud{
Name: testCloudName,
AuthURL: authURL,
UserDomainName: testUserDomainName,
Username: testUsername,
Password: testPassword1,
UsernameTemplate: testTemplate1,
})
require.NoError(t, err)
require.NoError(t, s.Put(context.Background(), cloudEntry))

roleName := createSaveRandomStaticRole(t, s, projectName, "token", secret, userID)

_, err = b.HandleRequest(context.Background(), &logical.Request{
Path: "rotate-role/" + roleName,
Operation: logical.CreateOperation,
Storage: s,
})
require.Error(t, err)
})
}
}

func createSaveRandomStaticRole(t *testing.T, s logical.Storage, projectName, sType string, secret string, userId string) string {
roleName := randomRoleName()
role := map[string]interface{}{
"name": roleName,
Expand All @@ -156,6 +280,7 @@ func createSaveRandomStaticRole(t *testing.T, s logical.Storage, projectName, sT
"secret_type": sType,
"secret": secret,
"username": roleName,
"user_id": userId,
}
saveRawStaticRole(t, roleName, role, s)

Expand Down

0 comments on commit bd37e14

Please sign in to comment.