Let allowed_users template mix templated and non-templated parts (#10886)
* Let allowed_users template mix templated and non-templated parts (#10388) * Add documentation * Change test function names * Add documentation * Add changelog entry
This commit is contained in:
parent
1eb73d9ef4
commit
824f097a7d
|
@ -31,6 +31,7 @@ const (
|
||||||
testIP = "127.0.0.1"
|
testIP = "127.0.0.1"
|
||||||
testUserName = "vaultssh"
|
testUserName = "vaultssh"
|
||||||
testAdminUser = "vaultssh"
|
testAdminUser = "vaultssh"
|
||||||
|
testCaKeyType = "ca"
|
||||||
testOTPKeyType = "otp"
|
testOTPKeyType = "otp"
|
||||||
testDynamicKeyType = "dynamic"
|
testDynamicKeyType = "dynamic"
|
||||||
testCIDRList = "127.0.0.1/32"
|
testCIDRList = "127.0.0.1/32"
|
||||||
|
@ -204,7 +205,7 @@ func testSSH(user, host string, auth ssh.AuthMethod, command string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBackend_allowed_users(t *testing.T) {
|
func TestBackend_AllowedUsers(t *testing.T) {
|
||||||
config := logical.TestBackendConfig()
|
config := logical.TestBackendConfig()
|
||||||
config.StorageView = &logical.InmemStorage{}
|
config.StorageView = &logical.InmemStorage{}
|
||||||
|
|
||||||
|
@ -318,6 +319,24 @@ func TestBackend_allowed_users(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBackend_AllowedUsersTemplate(t *testing.T) {
|
||||||
|
testAllowedUsersTemplate(t,
|
||||||
|
"{{ identity.entity.metadata.ssh_username }}",
|
||||||
|
testUserName, map[string]string{
|
||||||
|
"ssh_username": testUserName,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackend_AllowedUsersTemplate_WithStaticPrefix(t *testing.T) {
|
||||||
|
testAllowedUsersTemplate(t,
|
||||||
|
"ssh-{{ identity.entity.metadata.ssh_username }}",
|
||||||
|
"ssh-"+testUserName, map[string]string{
|
||||||
|
"ssh_username": testUserName,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func newTestingFactory(t *testing.T) func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
func newTestingFactory(t *testing.T) func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
||||||
return func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
return func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
||||||
defaultLeaseTTLVal := 2 * time.Minute
|
defaultLeaseTTLVal := 2 * time.Minute
|
||||||
|
@ -1614,6 +1633,63 @@ func getSshCaTestCluster(t *testing.T, userIdentity string) (*vault.TestCluster,
|
||||||
return cluster, userpassToken
|
return cluster, userpassToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAllowedUsersTemplate(t *testing.T, testAllowedUsersTemplate string,
|
||||||
|
expectedValidPrincipal string, testEntityMetadata map[string]string) {
|
||||||
|
cluster, userpassToken := getSshCaTestCluster(t, testUserName)
|
||||||
|
defer cluster.Cleanup()
|
||||||
|
client := cluster.Cores[0].Client
|
||||||
|
|
||||||
|
// set metadata "ssh_username" to userpass username
|
||||||
|
tokenLookupResponse, err := client.Logical().Write("/auth/token/lookup", map[string]interface{}{
|
||||||
|
"token": userpassToken,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
entityID := tokenLookupResponse.Data["entity_id"].(string)
|
||||||
|
_, err = client.Logical().Write("/identity/entity/id/"+entityID, map[string]interface{}{
|
||||||
|
"metadata": testEntityMetadata,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.Logical().Write("ssh/roles/my-role", map[string]interface{}{
|
||||||
|
"key_type": testCaKeyType,
|
||||||
|
"allow_user_certificates": true,
|
||||||
|
"allowed_users": testAllowedUsersTemplate,
|
||||||
|
"allowed_users_template": true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sign SSH key as userpass user
|
||||||
|
client.SetToken(userpassToken)
|
||||||
|
signResponse, err := client.Logical().Write("ssh/sign/my-role", map[string]interface{}{
|
||||||
|
"public_key": testCAPublicKey,
|
||||||
|
"valid_principals": expectedValidPrincipal,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for the expected valid principals of certificate
|
||||||
|
signedKey := signResponse.Data["signed_key"].(string)
|
||||||
|
key, _ := base64.StdEncoding.DecodeString(strings.Split(signedKey, " ")[1])
|
||||||
|
parsedKey, err := ssh.ParsePublicKey(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
actualPrincipals := parsedKey.(*ssh.Certificate).ValidPrincipals
|
||||||
|
if actualPrincipals[0] != expectedValidPrincipal {
|
||||||
|
t.Fatal(
|
||||||
|
fmt.Sprintf("incorrect ValidPrincipals: %v should be %v",
|
||||||
|
actualPrincipals, []string{expectedValidPrincipal}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func configCaStep(caPublicKey, caPrivateKey string) logicaltest.TestStep {
|
func configCaStep(caPublicKey, caPrivateKey string) logicaltest.TestStep {
|
||||||
return logicaltest.TestStep{
|
return logicaltest.TestStep{
|
||||||
Operation: logical.UpdateOperation,
|
Operation: logical.UpdateOperation,
|
||||||
|
|
|
@ -220,7 +220,7 @@ func (b *backend) calculateValidPrincipals(data *framework.FieldData, req *logic
|
||||||
for _, principal := range strutil.RemoveDuplicates(strutil.ParseStringSlice(principalsAllowedByRole, ","), false) {
|
for _, principal := range strutil.RemoveDuplicates(strutil.ParseStringSlice(principalsAllowedByRole, ","), false) {
|
||||||
if role.AllowedUsersTemplate {
|
if role.AllowedUsersTemplate {
|
||||||
// Look for templating markers {{ .* }}
|
// Look for templating markers {{ .* }}
|
||||||
matched, _ := regexp.MatchString(`^{{.+?}}$`, principal)
|
matched, _ := regexp.MatchString(`{{.+?}}`, principal)
|
||||||
if matched {
|
if matched {
|
||||||
if req.EntityID != "" {
|
if req.EntityID != "" {
|
||||||
// Retrieve principal based on template + entityID from request.
|
// Retrieve principal based on template + entityID from request.
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
secrets/ssh: Let allowed_users template mix templated and non-templated parts.
|
||||||
|
```
|
|
@ -132,9 +132,11 @@ This endpoint creates or updates a named role.
|
||||||
then this list enforces it. If this field is set, then credentials can only
|
then this list enforces it. If this field is set, then credentials can only
|
||||||
be created for `default_user` and usernames present in this list. Setting
|
be created for `default_user` and usernames present in this list. Setting
|
||||||
this option will enable all the users with access this role to fetch
|
this option will enable all the users with access this role to fetch
|
||||||
credentials for all other usernames in this list. Use with caution. N.B.: if
|
credentials for all other usernames in this list.
|
||||||
the type is `ca`, an empty list does not allow any user; instead you must use
|
When `allowed_users_template` is set to `true`, this field can contain an identity
|
||||||
`*` to enable this behavior.
|
template with any prefix or suffix, like `ssh-{{identity.entity.id}}-user`.
|
||||||
|
Use with caution. N.B.: if the type is `ca`, an empty list does not allow any user;
|
||||||
|
instead you must use `*` to enable this behavior.
|
||||||
|
|
||||||
- `allowed_users_template` `(bool: false)` - If set, allowed_users can be specified
|
- `allowed_users_template` `(bool: false)` - If set, allowed_users can be specified
|
||||||
using identity template policies. Non-templated users are also permitted.
|
using identity template policies. Non-templated users are also permitted.
|
||||||
|
|
Loading…
Reference in New Issue