Skip to content

Commit

Permalink
Implement username generation by template and password generati…
Browse files Browse the repository at this point in the history
…on by `policy` (#60)

Implement `username` generation by `template` and `password` generation by `policy`

Resolve: #58
Resolve: #59

Reviewed-by: Anton Kachurin <[email protected]>
Reviewed-by: Rodion Gyrbu <[email protected]>
Reviewed-by: Anton Sidelnikov <None>
  • Loading branch information
outcatcher authored Apr 12, 2022
1 parent 6cb8577 commit 9368216
Show file tree
Hide file tree
Showing 16 changed files with 295 additions and 114 deletions.
30 changes: 18 additions & 12 deletions acceptance/cloud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ import (
)

type cloudData struct {
AuthURL string `json:"auth_url"`
UserDomainName string `json:"user_domain_name"`
Username string `json:"username"`
Password string `json:"password"`
AuthURL string `json:"auth_url"`
UserDomainName string `json:"user_domain_name"`
Username string `json:"username"`
Password string `json:"password"`
UsernameTemplate string `json:"username_template"`
PasswordPolicy string `json:"password_policy"`
}

func extractCloudData(t *testing.T, resp *http.Response) *cloudData {
Expand All @@ -36,10 +38,12 @@ func (p *PluginTest) TestCloudLifecycle() {
t := p.T()

data := map[string]interface{}{
"auth_url": "https://example.com/v3/",
"username": tools.RandomString("us", 4),
"password": tools.RandomString("", 15),
"user_domain_name": "Default",
"auth_url": "https://example.com/v3/",
"username": tools.RandomString("us", 4),
"password": tools.RandomString("", 15),
"user_domain_name": "Default",
"username_template": "test{{ .RoleName }}{{ .CloudName }}",
"password_policy": tools.RandomString("p", 5),
}
cloudName := "test-write"

Expand All @@ -63,10 +67,12 @@ func (p *PluginTest) TestCloudLifecycle() {
assert.Equal(t, http.StatusOK, resp.StatusCode)

expected := &cloudData{
AuthURL: data["auth_url"].(string),
UserDomainName: data["user_domain_name"].(string),
Username: data["username"].(string),
Password: data["password"].(string),
AuthURL: data["auth_url"].(string),
UserDomainName: data["user_domain_name"].(string),
Username: data["username"].(string),
Password: data["password"].(string),
UsernameTemplate: data["username_template"].(string),
PasswordPolicy: data["password_policy"].(string),
}
assert.Equal(t, expected, extractCloudData(t, resp))
})
Expand Down
26 changes: 19 additions & 7 deletions acceptance/creds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ func (p *PluginTest) TestCredsLifecycle() {
for name, data := range cases {
t.Run(name, func(t *testing.T) {
data := data

_, err := p.vaultDo(
http.MethodPost,
pluginPwdPolicyEndpoint,
map[string]interface{}{
"policy": pwdPolicy,
},
)
require.NoError(t, err)

roleName := openstack.RandomString(openstack.NameDefaultSet, 4)

resp, err := p.vaultDo(
Expand All @@ -74,7 +84,7 @@ func (p *PluginTest) TestCredsLifecycle() {
cloudToRoleMap(data.Root, data.Cloud, data.ProjectID, data.DomainID, data.SecretType, data.UserRoles),
)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode, readJSONResponse(t, resp))
assert.Equal(t, http.StatusNoContent, resp.StatusCode, readJSONResponse(t, resp))

resp, err = p.vaultDo(
http.MethodGet,
Expand All @@ -98,7 +108,7 @@ func (p *PluginTest) TestCredsLifecycle() {
nil,
)
require.NoError(t, err)
assertStatusCode(t, http.StatusOK, resp)
assertStatusCode(t, http.StatusNoContent, resp)

resp, err = p.vaultDo(
http.MethodDelete,
Expand All @@ -117,11 +127,13 @@ func credsURL(roleName string) string {

func cloudToCloudMap(cloud *openstack.OsCloud) map[string]interface{} {
return map[string]interface{}{
"name": cloud.Name,
"auth_url": cloud.AuthURL,
"username": cloud.Username,
"password": cloud.Password,
"user_domain_name": cloud.UserDomainName,
"name": cloud.Name,
"auth_url": cloud.AuthURL,
"username": cloud.Username,
"password": cloud.Password,
"user_domain_name": cloud.UserDomainName,
"username_template": cloud.UsernameTemplate,
"password_policy": cloud.PasswordPolicy,
}
}

Expand Down
42 changes: 35 additions & 7 deletions acceptance/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,37 @@ import (
const (
pluginBin = "vault-plugin-secrets-openstack"
pluginAlias = "openstack"

policyAlias = "openstack-policy"

pwdPolicy = `
length=20
rule "charset" {
charset = "abcdefghijklmnopqrstuvwxyz"
min-chars = 1
}
rule "charset" {
charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
min-chars = 1
}
rule "charset" {
charset = "0123456789"
min-chars = 1
}
rule "charset" {
charset = "!@#$%^&*"
min-chars = 1
}`
)

var (
pluginCatalogEndpoint = fmt.Sprintf("/v1/sys/plugins/catalog/secret/%s", pluginAlias)
pluginMountEndpoint = fmt.Sprintf("/v1/sys/mounts/%s", pluginAlias)
pluginCatalogEndpoint = fmt.Sprintf("/v1/sys/plugins/catalog/secret/%s", pluginAlias)
pluginMountEndpoint = fmt.Sprintf("/v1/sys/mounts/%s", pluginAlias)
pluginPwdPolicyEndpoint = fmt.Sprintf("/v1/sys/policies/password/%s", policyAlias)

cloudBaseEndpoint = fmt.Sprintf("/v1/%s/cloud", pluginAlias)
)
Expand Down Expand Up @@ -242,11 +268,13 @@ func openstackCloudConfig(t *testing.T) *openstack.OsCloud {
require.NoError(t, err)

return &openstack.OsCloud{
Name: cloudName,
AuthURL: cloud.AuthInfo.AuthURL,
UserDomainName: getDomainName(cloud.AuthInfo),
Username: cloud.AuthInfo.Username,
Password: cloud.AuthInfo.Password,
Name: cloudName,
AuthURL: cloud.AuthInfo.AuthURL,
UserDomainName: getDomainName(cloud.AuthInfo),
Username: cloud.AuthInfo.Username,
Password: cloud.AuthInfo.Password,
UsernameTemplate: "vault-{{ .RoleName }}-{{ random 4 }}",
PasswordPolicy: "openstack-policy",
}
}

Expand Down
15 changes: 8 additions & 7 deletions acceptance/roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ 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),
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)

Expand All @@ -61,7 +62,7 @@ func (p *PluginTest) TestRoleLifecycle() {
data,
)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode, readJSONResponse(t, resp))
assert.Equal(t, http.StatusNoContent, resp.StatusCode, readJSONResponse(t, resp))
})

t.Run("ReadRole", func(t *testing.T) {
Expand Down Expand Up @@ -105,7 +106,7 @@ func (p *PluginTest) TestRoleLifecycle() {
nil,
)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
})
}

Expand Down
11 changes: 10 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,23 @@ will overwrite them.

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

* `username_template` `(string)` - Template used for usernames of temporary users. For details on templating syntax
please refer to [Username Templating](https://www.vaultproject.io/docs/concepts/username-templating).
Additional fields available for the template are `.CloudName`, `.RoleName`.

* `password_policy` `(string)` - Specifies a password policy name to use when creating dynamic credentials.
Defaults to generating an alphanumeric password if not set. For details on password policies please refer
to [Password Policies](https://www.vaultproject.io/docs/concepts/password-policies).

### Sample Payload

```json
{
"auth_url": "https://example.com/v3/",
"username": "admin",
"password": "RcigTiYrJjVmEkrV71Cd",
"user_domain_name": "Default"
"user_domain_name": "Default",
"username_template": "user-{{ .RoleName }}-{{ random 4 }}"
}
```

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ require (
github.com/hashicorp/go-plugin v1.4.3 // indirect
github.com/hashicorp/go-retryablehttp v0.6.6 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/base62 v0.1.1 // indirect
github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP
github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/base62 v0.1.1 h1:6KMBnfEv0/kLAz0O76sliN5mXbCDcLfs2kP7ssP7+DQ=
github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc=
github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I=
Expand Down
27 changes: 20 additions & 7 deletions openstack/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type sharedCloud struct {

client *gophercloud.ServiceClient
lock sync.Mutex

passwords *Passwords
}

type backend struct {
Expand All @@ -30,7 +32,7 @@ type backend struct {
clouds map[string]*sharedCloud
}

func Factory(_ context.Context, _ *logical.BackendConfig) (logical.Backend, error) {
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
b := new(backend)
b.Backend = &framework.Backend{
Help: backendHelp,
Expand All @@ -54,14 +56,23 @@ func Factory(_ context.Context, _ *logical.BackendConfig) (logical.Backend, erro
},
BackendType: logical.TypeLogical,
}

if err := b.Setup(ctx, conf); err != nil {
return nil, err
}

return b, nil
}

func (b *backend) getSharedCloud(name string) *sharedCloud {
passwords := &Passwords{PolicyGenerator: b.System()}
if c, ok := b.clouds[name]; ok {
if c.passwords == nil {
c.passwords = passwords
}
return c
}
cloud := &sharedCloud{name: name}
cloud := &sharedCloud{name: name, passwords: passwords}
if b.clouds == nil {
b.clouds = make(map[string]*sharedCloud)
}
Expand Down Expand Up @@ -121,9 +132,11 @@ func (c *sharedCloud) initClient(ctx context.Context, s logical.Storage) error {
}

type OsCloud struct {
Name string `json:"name"`
AuthURL string `json:"auth_url"`
UserDomainName string `json:"user_domain_name"`
Username string `json:"username"`
Password string `json:"password"`
Name string `json:"name"`
AuthURL string `json:"auth_url"`
UserDomainName string `json:"user_domain_name"`
Username string `json:"username"`
Password string `json:"password"`
UsernameTemplate string `json:"username_template"`
PasswordPolicy string `json:"password_policy"`
}
10 changes: 8 additions & 2 deletions openstack/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
th "github.com/gophercloud/gophercloud/testhelper"
thClient "github.com/gophercloud/gophercloud/testhelper/client"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -44,24 +45,29 @@ func testBackend(t *testing.T, fvs ...failVerb) (*backend, logical.Storage) {

config := logical.TestBackendConfig()
config.StorageView = storageView
config.System = logical.TestSystemView()
config.Logger = hclog.NewNullLogger()

b, err := Factory(context.Background(), config)
assert.NoError(t, err)

assert.NoError(t, b.Setup(context.Background(), config))

return b.(*backend), config.StorageView
}

func TestBackend_sharedCloud(t *testing.T) {
expected := &sharedCloud{
client: new(gophercloud.ServiceClient),
lock: sync.Mutex{},
client: new(gophercloud.ServiceClient),
passwords: new(Passwords),
lock: sync.Mutex{},
}
cloudKey := tools.RandomString("cl", 5)
back := backend{
clouds: map[string]*sharedCloud{
cloudKey: expected,
},
Backend: &framework.Backend{},
}

t.Run("existing", func(t *testing.T) {
Expand Down
Loading

0 comments on commit 9368216

Please sign in to comment.