Skip to content

Commit bd37e14

Browse files
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 <None> Reviewed-by: Anton Sidelnikov <None>
1 parent 384f556 commit bd37e14

File tree

5 files changed

+235
-6
lines changed

5 files changed

+235
-6
lines changed

acceptance/static_creds_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ func (p *PluginTest) TestStaticCredsLifecycle() {
105105
require.NoError(t, err)
106106
assertStatusCode(t, http.StatusOK, resp)
107107

108+
resp, err = p.vaultDo(
109+
http.MethodPost,
110+
staticRotateCredsURL(roleName),
111+
nil,
112+
)
113+
require.NoError(t, err)
114+
assertStatusCode(t, http.StatusNoContent, resp)
115+
108116
resp, err = p.vaultDo(
109117
http.MethodDelete,
110118
staticRoleURL(roleName),
@@ -128,6 +136,10 @@ func staticCredsURL(roleName string) string {
128136
return fmt.Sprintf("/v1/openstack/static-creds/%s", roleName)
129137
}
130138

139+
func staticRotateCredsURL(roleName string) string {
140+
return fmt.Sprintf("/v1/openstack/rotate-role/%s", roleName)
141+
}
142+
131143
func cloudToStaticRoleMap(data testStaticCase) map[string]interface{} {
132144
return fixtures.SanitizedMap(map[string]interface{}{
133145
"cloud": data.cloud,

doc/source/api.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,3 +641,24 @@ $ curl \
641641
}
642642
}
643643
```
644+
645+
## Rotate Static Role Credentials
646+
647+
When you have configured Vault with static role, you can use this endpoint to have the Vault rotate the password
648+
for the static user. Password change will be performed.
649+
650+
Once this method is called, password for static user related to static role will be updated.
651+
652+
| Method | Path |
653+
|:-------|:-------------------------------|
654+
| `POST` | `/openstack/rotate-role/:name` |
655+
| `PUT` | `/openstack/rotate-role/:name` |
656+
657+
### Sample Request
658+
659+
```shell
660+
$ curl \
661+
--header "X-Vault-Token: ..." \
662+
--request POST \
663+
http://127.0.0.1:8200/v1/openstack/rotate-role/:name
664+
```

openstack/backend.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend,
5454
b.pathStaticRole(),
5555
b.pathRotateRoot(),
5656
b.pathCreds(),
57+
b.pathRotateStaticCreds(),
5758
b.pathStaticCreds(),
5859
},
5960
Secrets: []*framework.Secret{

openstack/path_static_creds.go

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,19 @@ import (
1010
)
1111

1212
const (
13-
pathStaticCreds = "static-creds"
13+
pathStaticCreds = "static-creds"
14+
pathStaticCredsRotate = "rotate-role"
1415

1516
staticCredsHelpSyn = "Manage the Openstack static credentials with static roles."
1617
staticCredsHelpDesc = `
1718
This path allows you to read OpenStack secret stored by predefined static roles.
19+
`
20+
21+
rotateStaticHelpSyn = "Rotate static role password."
22+
rotateStaticHelpDesc = `
23+
Rotate the static role user credentials.
24+
25+
Once this method is called, static role will now be the only entity that knows the static user password.
1826
`
1927
)
2028

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

49+
func (b *backend) pathRotateStaticCreds() *framework.Path {
50+
return &framework.Path{
51+
Pattern: fmt.Sprintf("%s/%s", pathStaticCredsRotate, framework.GenericNameRegex("role")),
52+
Fields: map[string]*framework.FieldSchema{
53+
"role": {
54+
Type: framework.TypeString,
55+
Required: true,
56+
Description: "Specifies name of the static role which credentials will be rotated.",
57+
},
58+
},
59+
Operations: map[logical.Operation]framework.OperationHandler{
60+
logical.CreateOperation: &framework.PathOperation{
61+
Callback: b.rotateStaticCreds,
62+
},
63+
logical.UpdateOperation: &framework.PathOperation{
64+
Callback: b.rotateStaticCreds,
65+
},
66+
},
67+
HelpSynopsis: rotateStaticHelpSyn,
68+
HelpDescription: rotateStaticHelpDesc,
69+
}
70+
}
71+
4172
func (b *backend) pathStaticCredsRead(ctx context.Context, r *logical.Request, d *framework.FieldData) (*logical.Response, error) {
4273
roleName := d.Get("role").(string)
4374
role, err := getStaticRoleByName(ctx, roleName, r)
@@ -112,6 +143,45 @@ func (b *backend) pathStaticCredsRead(ctx context.Context, r *logical.Request, d
112143
return &logical.Response{Data: data}, nil
113144
}
114145

146+
func (b *backend) rotateStaticCreds(ctx context.Context, r *logical.Request, d *framework.FieldData) (*logical.Response, error) {
147+
roleName := d.Get("role").(string)
148+
role, err := getStaticRoleByName(ctx, roleName, r)
149+
if err != nil {
150+
return nil, err
151+
}
152+
153+
sharedCloud := b.getSharedCloud(role.Cloud)
154+
if err != nil {
155+
return nil, err
156+
}
157+
158+
client, err := sharedCloud.getClient(ctx, r.Storage)
159+
if err != nil {
160+
return nil, err
161+
}
162+
163+
newPassword, err := Passwords{}.Generate(ctx)
164+
if err != nil {
165+
return nil, err
166+
}
167+
168+
err = users.ChangePassword(client, role.UserID, users.ChangePasswordOpts{
169+
Password: newPassword,
170+
OriginalPassword: role.Secret,
171+
}).ExtractErr()
172+
if err != nil {
173+
return nil, err
174+
}
175+
176+
role.Secret = newPassword
177+
178+
if err := saveStaticRole(ctx, role, r); err != nil {
179+
return nil, err
180+
}
181+
182+
return nil, nil
183+
}
184+
115185
func getScopeFromStaticRole(role *roleStaticEntry) tokens.Scope {
116186
var scope tokens.Scope
117187
switch {

openstack/path_static_creds_test.go

Lines changed: 130 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ func credsStaticPath(name string) string {
1717
return fmt.Sprintf("%s/%s", "static-creds", name)
1818
}
1919

20+
func rotateStaticCreds(name string) string {
21+
return fmt.Sprintf("%s/%s", "rotate-role", name)
22+
}
23+
2024
func TestStaticCredentialsRead_ok(t *testing.T) {
2125
userID, _ := uuid.GenerateUUID()
2226
secret, _ := uuid.GenerateUUID()
@@ -45,7 +49,7 @@ func TestStaticCredentialsRead_ok(t *testing.T) {
4549
t.Run("user_token", func(t *testing.T) {
4650
require.NoError(t, s.Put(context.Background(), cloudEntry))
4751

48-
roleName := createSaveRandomStaticRole(t, s, projectName, "token", secret)
52+
roleName := createSaveRandomStaticRole(t, s, projectName, "token", secret, "")
4953

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

61-
roleName := createSaveRandomStaticRole(t, s, projectName, "password", secret)
65+
roleName := createSaveRandomStaticRole(t, s, projectName, "password", secret, "")
6266

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

7983
b, s := testBackend(t, failVerbRead)
8084

81-
roleName := createSaveRandomStaticRole(t, s, "", "token", secret)
85+
roleName := createSaveRandomStaticRole(t, s, "", "token", secret, "")
8286

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

121125
b, s := testBackend(t)
122126

123-
roleName := createSaveRandomStaticRole(t, s, data.ProjectName, data.ServiceType, secret)
127+
roleName := createSaveRandomStaticRole(t, s, data.ProjectName, data.ServiceType, secret, "")
124128

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

149-
func createSaveRandomStaticRole(t *testing.T, s logical.Storage, projectName, sType string, secret string) string {
153+
func TestRotateStaticCredentials_ok(t *testing.T) {
154+
userID, _ := uuid.GenerateUUID()
155+
secret, _ := uuid.GenerateUUID()
156+
projectName := tools.RandomString("p", 5)
157+
158+
fixtures.SetupKeystoneMock(t, userID, projectName, fixtures.EnabledMocks{
159+
TokenPost: true,
160+
TokenGet: true,
161+
PasswordChange: true,
162+
})
163+
164+
testClient := thClient.ServiceClient()
165+
authURL := testClient.Endpoint + "v3"
166+
167+
b, s := testBackend(t)
168+
cloudEntry, err := logical.StorageEntryJSON(storageCloudKey(testCloudName), &OsCloud{
169+
Name: testCloudName,
170+
AuthURL: authURL,
171+
UserDomainName: testUserDomainName,
172+
Username: testUsername,
173+
Password: testPassword1,
174+
UsernameTemplate: testTemplate1,
175+
})
176+
require.NoError(t, err)
177+
178+
t.Run("user_token", func(t *testing.T) {
179+
require.NoError(t, s.Put(context.Background(), cloudEntry))
180+
181+
roleName := createSaveRandomStaticRole(t, s, projectName, "token", secret, userID)
182+
183+
_, err := b.HandleRequest(context.Background(), &logical.Request{
184+
Operation: logical.CreateOperation,
185+
Path: rotateStaticCreds(roleName),
186+
Storage: s,
187+
})
188+
require.NoError(t, err)
189+
})
190+
t.Run("user_password", func(t *testing.T) {
191+
require.NoError(t, s.Put(context.Background(), cloudEntry))
192+
193+
roleName := createSaveRandomStaticRole(t, s, projectName, "password", secret, userID)
194+
195+
res, err := b.HandleRequest(context.Background(), &logical.Request{
196+
Operation: logical.ReadOperation,
197+
Path: credsStaticPath(roleName),
198+
Storage: s,
199+
})
200+
require.NoError(t, err)
201+
require.NotEmpty(t, res.Data)
202+
})
203+
}
204+
205+
func TestRotateStaticCredentials_error(t *testing.T) {
206+
t.Parallel()
207+
208+
t.Run("read-fail", func(t *testing.T) {
209+
userID, _ := uuid.GenerateUUID()
210+
projectName := tools.RandomString("p", 5)
211+
fixtures.SetupKeystoneMock(t, userID, projectName, fixtures.EnabledMocks{})
212+
213+
b, s := testBackend(t, failVerbRead)
214+
215+
roleName := createSaveRandomStaticRole(t, s, projectName, "password", "", "")
216+
217+
_, err := b.HandleRequest(context.Background(), &logical.Request{
218+
Path: "rotate-role/" + roleName,
219+
Operation: logical.CreateOperation,
220+
Storage: s,
221+
})
222+
require.Error(t, err)
223+
})
224+
225+
cases := map[string]fixtures.EnabledMocks{
226+
"no-change": {
227+
TokenPost: true, TokenGet: true,
228+
},
229+
"no-post": {
230+
TokenGet: true, PasswordChange: true,
231+
},
232+
"no-get": {
233+
TokenPost: true, PasswordChange: true,
234+
},
235+
}
236+
237+
for name, data := range cases {
238+
t.Run(name, func(t *testing.T) {
239+
data := data
240+
userID, _ := uuid.GenerateUUID()
241+
secret, _ := uuid.GenerateUUID()
242+
projectName := tools.RandomString("p", 5)
243+
244+
fixtures.SetupKeystoneMock(t, userID, projectName, data)
245+
246+
testClient := thClient.ServiceClient()
247+
authURL := testClient.Endpoint + "v3"
248+
249+
b, s := testBackend(t)
250+
cloudEntry, err := logical.StorageEntryJSON(storageCloudKey(testCloudName), &OsCloud{
251+
Name: testCloudName,
252+
AuthURL: authURL,
253+
UserDomainName: testUserDomainName,
254+
Username: testUsername,
255+
Password: testPassword1,
256+
UsernameTemplate: testTemplate1,
257+
})
258+
require.NoError(t, err)
259+
require.NoError(t, s.Put(context.Background(), cloudEntry))
260+
261+
roleName := createSaveRandomStaticRole(t, s, projectName, "token", secret, userID)
262+
263+
_, err = b.HandleRequest(context.Background(), &logical.Request{
264+
Path: "rotate-role/" + roleName,
265+
Operation: logical.CreateOperation,
266+
Storage: s,
267+
})
268+
require.Error(t, err)
269+
})
270+
}
271+
}
272+
273+
func createSaveRandomStaticRole(t *testing.T, s logical.Storage, projectName, sType string, secret string, userId string) string {
150274
roleName := randomRoleName()
151275
role := map[string]interface{}{
152276
"name": roleName,
@@ -156,6 +280,7 @@ func createSaveRandomStaticRole(t *testing.T, s logical.Storage, projectName, sT
156280
"secret_type": sType,
157281
"secret": secret,
158282
"username": roleName,
283+
"user_id": userId,
159284
}
160285
saveRawStaticRole(t, roleName, role, s)
161286

0 commit comments

Comments
 (0)