Skip to content

Commit c8ee9df

Browse files
Clouds: add rotation parameters for auto rotate (#127)
Path_Clouds: add rotation parameters for future auto rotate root implementation Path clouds is updated with additional parameters for auto rotate root with periodicfunc. Acceptance tests vault-plugin-secrets-openstack % make functional 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_domain_id_token === RUN TestPlugin/TestCredsLifecycle/root_token === RUN TestPlugin/TestCredsLifecycle/user_token === RUN TestPlugin/TestCredsLifecycle/user_password === 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 === 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 === RUN TestPlugin/TestRootRotate rotate_test.go:65: Cloud with name default1 was created rotate_test.go:68: Cloud with name ixqy was created plugin_test.go:337: Cloud with name ixqy 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_project_id === RUN TestPlugin/TestStaticCredsLifecycle/user_token_project_name === RUN TestPlugin/TestStaticCredsLifecycle/user_domain_id_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 (32.04s) --- PASS: TestPlugin/TestCloudLifecycle (0.05s) --- PASS: TestPlugin/TestCloudLifecycle/WriteCloud (0.05s) --- 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 (8.18s) --- PASS: TestPlugin/TestCredsLifecycle/user_domain_id_token (3.05s) --- PASS: TestPlugin/TestCredsLifecycle/root_token (0.83s) --- PASS: TestPlugin/TestCredsLifecycle/user_token (2.44s) --- PASS: TestPlugin/TestCredsLifecycle/user_password (1.03s) --- FAIL: TestPlugin/TestInfo (0.00s) --- PASS: TestPlugin/TestRoleLifecycle (0.61s) --- PASS: TestPlugin/TestRoleLifecycle/WriteRole (0.60s) --- 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 (4.69s) --- PASS: TestPlugin/TestStaticCredsLifecycle (15.61s) --- PASS: TestPlugin/TestStaticCredsLifecycle/user_password (3.26s) --- PASS: TestPlugin/TestStaticCredsLifecycle/user_token_project_id (3.83s) --- PASS: TestPlugin/TestStaticCredsLifecycle/user_token_project_name (3.76s) --- PASS: TestPlugin/TestStaticCredsLifecycle/user_domain_id_token (3.70s) --- PASS: TestPlugin/TestStaticRoleLifecycle (2.78s) --- PASS: TestPlugin/TestStaticRoleLifecycle/WriteRole (1.03s) --- PASS: TestPlugin/TestStaticRoleLifecycle/ReadRole (0.00s) --- PASS: TestPlugin/TestStaticRoleLifecycle/ListRoles (0.00s) --- PASS: TestPlugin/TestStaticRoleLifecycle/ListRoles/method-GET (0.00s) --- PASS: TestPlugin/TestStaticRoleLifecycle/ListRoles/method-LIST (0.00s) --- PASS: TestPlugin/TestStaticRoleLifecycle/DeleteRole (0.00s) FAIL FAIL github.com/opentelekomcloud/vault-plugin-secrets-openstack/acceptance 32.382s FAIL make: *** [functional] Error 1 Reviewed-by: Aloento
1 parent 8f63890 commit c8ee9df

File tree

4 files changed

+127
-18
lines changed

4 files changed

+127
-18
lines changed

doc/source/api.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ will overwrite them.
2222

2323
* `password` `(string: <required>)` - OpenStack password of the root user.
2424

25+
* `root_password_ttl` `(string: <optional>)` - Password rotation period. Default period is six month.
26+
2527
* `username_template` `(string: "vault{{random 8 | lowercase}}")` - Template used for usernames
2628
of temporary users. For details on templating syntax please refer to
2729
[Username Templating](https://www.vaultproject.io/docs/concepts/username-templating). Additional
@@ -39,7 +41,8 @@ will overwrite them.
3941
"username": "admin",
4042
"password": "RcigTiYrJjVmEkrV71Cd",
4143
"user_domain_name": "Default",
42-
"username_template": "user-{{ .RoleName }}-{{ random 4 }}"
44+
"username_template": "user-{{ .RoleName }}-{{ random 4 }}",
45+
"root_password_ttl": "5h"
4346
}
4447
```
4548

openstack/backend.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,3 @@ func (c *sharedCloud) initClient(ctx context.Context, s logical.Storage) error {
147147

148148
return nil
149149
}
150-
151-
type OsCloud struct {
152-
Name string `json:"name"`
153-
AuthURL string `json:"auth_url"`
154-
UserDomainName string `json:"user_domain_name"`
155-
Username string `json:"username"`
156-
Password string `json:"password"`
157-
UsernameTemplate string `json:"username_template"`
158-
PasswordPolicy string `json:"password_policy"`
159-
}

openstack/path_cloud.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/hashicorp/vault/sdk/framework"
77
"github.com/hashicorp/vault/sdk/logical"
88
"github.com/opentelekomcloud/vault-plugin-secrets-openstack/vars"
9+
"time"
910
)
1011

1112
const (
@@ -20,6 +21,7 @@ Configure the root credentials for an OpenStack cloud using the above parameters
2021
pathCloudListHelpDesc = `List existing OpenStack clouds by name.`
2122

2223
DefaultUsernameTemplate = "vault{{random 8 | lowercase}}"
24+
defaultRootPasswordTTL = 4380 * time.Hour
2325
)
2426

2527
func storageCloudKey(name string) string {
@@ -30,6 +32,18 @@ func pathCloudKey(name string) string {
3032
return fmt.Sprintf("%s/%s", pathCloud, name)
3133
}
3234

35+
type OsCloud struct {
36+
Name string `json:"name"`
37+
AuthURL string `json:"auth_url"`
38+
UserDomainName string `json:"user_domain_name"`
39+
Username string `json:"username"`
40+
Password string `json:"password"`
41+
UsernameTemplate string `json:"username_template"`
42+
PasswordPolicy string `json:"password_policy"`
43+
RootPasswordTTL time.Duration `json:"root_password_ttl"`
44+
RootPasswordExpirationDate time.Time `json:"root_password_expiration_date"`
45+
}
46+
3347
func (c *sharedCloud) getCloudConfig(ctx context.Context, s logical.Storage) (*OsCloud, error) {
3448
entry, err := s.Get(ctx, storageCloudKey(c.name))
3549
if err != nil {
@@ -95,6 +109,12 @@ func (b *backend) pathCloud() *framework.Path {
95109
Type: framework.TypeString,
96110
Description: "Name of the password policy to use to generate passwords for dynamic credentials.",
97111
},
112+
"root_password_ttl": {
113+
Type: framework.TypeDurationSecond,
114+
Default: defaultRootPasswordTTL,
115+
Description: "The TTL of the root password for openstack user. This can be either a number of seconds or a time formatted duration (ex: 24h, 48ds)",
116+
Required: false,
117+
},
98118
},
99119
Operations: map[logical.Operation]framework.OperationHandler{
100120
logical.CreateOperation: &framework.PathOperation{
@@ -176,13 +196,21 @@ func (b *backend) pathCloudCreateUpdate(ctx context.Context, r *logical.Request,
176196
if err != nil {
177197
return logical.ErrorResponse("invalid username template: %w", err), nil
178198
}
179-
} else if r.Operation == logical.CreateOperation {
199+
} else if r.Operation == logical.CreateOperation && cloudConfig.UsernameTemplate == "" {
180200
cloudConfig.UsernameTemplate = DefaultUsernameTemplate
181201
}
182202
if pwdPolicy, ok := d.GetOk("password_policy"); ok {
183203
cloudConfig.PasswordPolicy = pwdPolicy.(string)
184204
}
185205

206+
if rootExpirationRaw, ok := d.GetOk("root_password_ttl"); ok {
207+
cloudConfig.RootPasswordTTL = time.Second * time.Duration(rootExpirationRaw.(int))
208+
} else if r.Operation == logical.CreateOperation && cloudConfig.RootPasswordTTL == 0 {
209+
cloudConfig.RootPasswordTTL = defaultRootPasswordTTL
210+
}
211+
212+
cloudConfig.RootPasswordExpirationDate = time.Now().Add(cloudConfig.RootPasswordTTL)
213+
186214
sCloud.passwords = &Passwords{
187215
PolicyGenerator: b.System(),
188216
PolicyName: cloudConfig.PasswordPolicy,
@@ -212,6 +240,8 @@ func (b *backend) pathCloudRead(ctx context.Context, r *logical.Request, d *fram
212240
"username": cloudConfig.Username,
213241
"username_template": cloudConfig.UsernameTemplate,
214242
"password_policy": cloudConfig.PasswordPolicy,
243+
"root_password_ttl": int(cloudConfig.RootPasswordTTL.Seconds()),
244+
"next_rotation": cloudConfig.RootPasswordExpirationDate.Format(time.RFC822),
215245
},
216246
}, nil
217247
}

openstack/path_cloud_test.go

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ package openstack
22

33
import (
44
"context"
5-
"strings"
6-
"testing"
7-
8-
"github.com/stretchr/testify/require"
9-
105
"github.com/gophercloud/gophercloud/acceptance/tools"
116
"github.com/hashicorp/vault/sdk/logical"
127
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
"strings"
10+
"testing"
1311
)
1412

1513
var (
@@ -25,7 +23,7 @@ var (
2523
testPolicy2 = "openstack"
2624
)
2725

28-
func TestCloudCreate(t *testing.T) {
26+
func TestLifecyle(t *testing.T) {
2927
t.Run("EmptyConfig", func(t *testing.T) {
3028
b, storage := testBackend(t)
3129

@@ -199,3 +197,91 @@ func TestCloudCreate(t *testing.T) {
199197
assert.Len(t, res.Data["keys"], cloudCount)
200198
})
201199
}
200+
201+
func TestConfig(t *testing.T) {
202+
b, s := testBackend(t)
203+
204+
tests := []struct {
205+
name string
206+
config map[string]interface{}
207+
expected map[string]interface{}
208+
wantErr bool
209+
}{
210+
{
211+
name: "root_password_ttl defaults to 6 months",
212+
config: map[string]interface{}{
213+
"auth_url": "https://test-001.com/v3",
214+
"username": "test-username-1",
215+
"user_domain_name": "testUserDomainName",
216+
"password": "testUserPassword",
217+
"username_template": "user-{{ .RoleName }}-{{ random 4 }}",
218+
},
219+
expected: map[string]interface{}{
220+
"auth_url": "https://test-001.com/v3",
221+
"username": "test-username-1",
222+
"user_domain_name": "testUserDomainName",
223+
"username_template": "user-{{ .RoleName }}-{{ random 4 }}",
224+
"root_password_ttl": 15768000,
225+
"password_policy": "",
226+
},
227+
},
228+
{
229+
name: "root_password_ttl is provided",
230+
config: map[string]interface{}{
231+
"auth_url": "https://test-001.com/v3",
232+
"username": "test-username-2",
233+
"user_domain_name": "testUserDomainName",
234+
"password": "testUserPassword",
235+
"root_password_ttl": "1m",
236+
},
237+
expected: map[string]interface{}{
238+
"auth_url": "https://test-001.com/v3",
239+
"username": "test-username-2",
240+
"user_domain_name": "testUserDomainName",
241+
"password_policy": "",
242+
"root_password_ttl": 60,
243+
"username_template": "vault{{random 8 | lowercase}}"},
244+
},
245+
}
246+
247+
for _, tc := range tests {
248+
t.Run(tc.name, func(t *testing.T) {
249+
var cloudName = strings.ToLower(tools.RandomString("cloud", 3))
250+
testConfigCreateUpdate(t, b, s, tc.config, cloudName)
251+
testConfigRead(t, b, s, tc.expected, cloudName)
252+
253+
// Test that updating one element retains the others
254+
tc.expected["user_domain_name"] = "800e371d-ee51-4145-9ac8-5c43e4ceb79b"
255+
configSubset := map[string]interface{}{
256+
"user_domain_name": "800e371d-ee51-4145-9ac8-5c43e4ceb79b",
257+
}
258+
259+
testConfigCreateUpdate(t, b, s, configSubset, cloudName)
260+
testConfigRead(t, b, s, tc.expected, cloudName)
261+
})
262+
}
263+
}
264+
265+
func testConfigCreateUpdate(t *testing.T, b logical.Backend, s logical.Storage, expected map[string]interface{}, name string) {
266+
t.Helper()
267+
_, err := b.HandleRequest(context.Background(), &logical.Request{
268+
Storage: s,
269+
Operation: logical.CreateOperation,
270+
Path: pathCloudKey(name),
271+
Data: expected,
272+
})
273+
require.NoError(t, err)
274+
}
275+
276+
func testConfigRead(t *testing.T, b logical.Backend, s logical.Storage, expected map[string]interface{}, name string) {
277+
t.Helper()
278+
resp, err := b.HandleRequest(context.Background(), &logical.Request{
279+
Storage: s,
280+
Operation: logical.ReadOperation,
281+
Path: pathCloudKey(name),
282+
})
283+
require.NoError(t, err)
284+
285+
expected["next_rotation"] = resp.Data["next_rotation"]
286+
assert.Equal(t, expected, resp.Data)
287+
}

0 commit comments

Comments
 (0)