From bd37e14b42f1a88a602411b4f4f77d0a1f9c57d5 Mon Sep 17 00:00:00 2001 From: Artem Lifshits <55093318+artem-lifshits@users.noreply.github.com> Date: Thu, 18 Aug 2022 22:09:20 +0300 Subject: [PATCH] Vault: Rotate Static Role Credentials (#100) 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 Reviewed-by: Anton Sidelnikov --- acceptance/static_creds_test.go | 12 +++ doc/source/api.md | 21 +++++ openstack/backend.go | 1 + openstack/path_static_creds.go | 72 ++++++++++++++- openstack/path_static_creds_test.go | 135 ++++++++++++++++++++++++++-- 5 files changed, 235 insertions(+), 6 deletions(-) diff --git a/acceptance/static_creds_test.go b/acceptance/static_creds_test.go index 7a04a4e..d35f542 100644 --- a/acceptance/static_creds_test.go +++ b/acceptance/static_creds_test.go @@ -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), @@ -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, diff --git a/doc/source/api.md b/doc/source/api.md index 3ac99ab..6ea0ef2 100644 --- a/doc/source/api.md +++ b/doc/source/api.md @@ -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 +``` diff --git a/openstack/backend.go b/openstack/backend.go index 03e6794..3656125 100644 --- a/openstack/backend.go +++ b/openstack/backend.go @@ -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{ diff --git a/openstack/path_static_creds.go b/openstack/path_static_creds.go index 20083df..1e8a0cc 100644 --- a/openstack/path_static_creds.go +++ b/openstack/path_static_creds.go @@ -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. ` ) @@ -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) @@ -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 { diff --git a/openstack/path_static_creds_test.go b/openstack/path_static_creds_test.go index e0ce9ca..2aa8e3a 100644 --- a/openstack/path_static_creds_test.go +++ b/openstack/path_static_creds_test.go @@ -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() @@ -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, @@ -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, @@ -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), @@ -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" @@ -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, @@ -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)