From d27374e5ca4c056a287755ff757160823a37af32 Mon Sep 17 00:00:00 2001 From: Fredrik Hoem Grelland <40291976+fredrikhgrelland@users.noreply.github.com> Date: Sat, 15 Feb 2020 20:04:33 +0100 Subject: [PATCH] identity propagation in ssh secrets engine #7547 (#7548) * identity propagation in ssh secrets engine #7547 * flag to enable templating allowed_users ssh (ca) secrets backend. --- builtin/logical/ssh/path_roles.go | 12 ++++++++++ builtin/logical/ssh/path_sign.go | 37 ++++++++++++++++++++++++++----- ui/app/models/role-ssh.js | 6 ++++- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/builtin/logical/ssh/path_roles.go b/builtin/logical/ssh/path_roles.go index 6cd20cba6aa5..0e539b40a390 100644 --- a/builtin/logical/ssh/path_roles.go +++ b/builtin/logical/ssh/path_roles.go @@ -37,6 +37,7 @@ type sshRole struct { Port int `mapstructure:"port" json:"port"` InstallScript string `mapstructure:"install_script" json:"install_script"` AllowedUsers string `mapstructure:"allowed_users" json:"allowed_users"` + AllowedUsersTemplate bool `mapstructure:"allowed_users_template" json:"allowed_users_template"` AllowedDomains string `mapstructure:"allowed_domains" json:"allowed_domains"` KeyOptionSpecs string `mapstructure:"key_option_specs" json:"key_option_specs"` MaxTTL string `mapstructure:"max_ttl" json:"max_ttl"` @@ -182,6 +183,15 @@ func pathRoles(b *backend) *framework.Path { allow any user. `, }, + "allowed_users_template": &framework.FieldSchema{ + Type: framework.TypeBool, + Description: ` + [Not applicable for Dynamic type] [Not applicable for OTP type] [Optional for CA type] + If set, Allowed users can be specified using identity template policies. + Non-templated users are also permitted. + `, + Default: false, + }, "allowed_domains": &framework.FieldSchema{ Type: framework.TypeString, Description: ` @@ -485,6 +495,7 @@ func (b *backend) createCARole(allowedUsers, defaultUser string, data *framework AllowUserCertificates: data.Get("allow_user_certificates").(bool), AllowHostCertificates: data.Get("allow_host_certificates").(bool), AllowedUsers: allowedUsers, + AllowedUsersTemplate: data.Get("allowed_users_template").(bool), AllowedDomains: data.Get("allowed_domains").(string), DefaultUser: defaultUser, AllowBareDomains: data.Get("allow_bare_domains").(bool), @@ -565,6 +576,7 @@ func (b *backend) parseRole(role *sshRole) (map[string]interface{}, error) { result = map[string]interface{}{ "allowed_users": role.AllowedUsers, + "allowed_users_template": role.AllowedUsersTemplate, "allowed_domains": role.AllowedDomains, "default_user": role.DefaultUser, "ttl": int64(ttl.Seconds()), diff --git a/builtin/logical/ssh/path_sign.go b/builtin/logical/ssh/path_sign.go index 95e12af1a42d..090919dc422f 100644 --- a/builtin/logical/ssh/path_sign.go +++ b/builtin/logical/ssh/path_sign.go @@ -9,6 +9,7 @@ import ( "crypto/sha256" "errors" "fmt" + "regexp" "strconv" "strings" "time" @@ -133,12 +134,12 @@ func (b *backend) pathSignCertificate(ctx context.Context, req *logical.Request, var parsedPrincipals []string if certificateType == ssh.HostCert { - parsedPrincipals, err = b.calculateValidPrincipals(data, "", role.AllowedDomains, validateValidPrincipalForHosts(role)) + parsedPrincipals, err = b.calculateValidPrincipals(data, req, role, "", role.AllowedDomains, validateValidPrincipalForHosts(role)) if err != nil { return logical.ErrorResponse(err.Error()), nil } } else { - parsedPrincipals, err = b.calculateValidPrincipals(data, role.DefaultUser, role.AllowedUsers, strutil.StrListContains) + parsedPrincipals, err = b.calculateValidPrincipals(data, req, role, role.DefaultUser, role.AllowedUsers, strutil.StrListContains) if err != nil { return logical.ErrorResponse(err.Error()), nil } @@ -204,7 +205,7 @@ func (b *backend) pathSignCertificate(ctx context.Context, req *logical.Request, return response, nil } -func (b *backend) calculateValidPrincipals(data *framework.FieldData, defaultPrincipal, principalsAllowedByRole string, validatePrincipal func([]string, string) bool) ([]string, error) { +func (b *backend) calculateValidPrincipals(data *framework.FieldData, req *logical.Request, role *sshRole, defaultPrincipal, principalsAllowedByRole string, validatePrincipal func([]string, string) bool) ([]string, error) { validPrincipals := "" validPrincipalsRaw, ok := data.GetOk("valid_principals") if ok { @@ -214,7 +215,33 @@ func (b *backend) calculateValidPrincipals(data *framework.FieldData, defaultPri } parsedPrincipals := strutil.RemoveDuplicates(strutil.ParseStringSlice(validPrincipals, ","), false) - allowedPrincipals := strutil.RemoveDuplicates(strutil.ParseStringSlice(principalsAllowedByRole, ","), false) + // Build list of allowed Principals from template and static principalsAllowedByRole + var allowedPrincipals []string + for _, principal := range strutil.RemoveDuplicates(strutil.ParseStringSlice(principalsAllowedByRole, ","), false) { + if role.AllowedUsersTemplate { + // Look for templating markers {{ .* }} + matched, _ := regexp.MatchString(`^{{.+?}}$`, principal) + if matched { + if req.EntityID != "" { + // Retrieve principal based on template + entityID from request. + templatePrincipal, err := framework.PopulateIdentityTemplate(principal, req.EntityID, b.System()) + if err == nil { + // Template returned a principal + allowedPrincipals = append(allowedPrincipals, templatePrincipal) + } else { + return nil, fmt.Errorf("template '%s' could not be rendered -> %s", principal, err) + } + } + } else { + // Static principal or err template + allowedPrincipals = append(allowedPrincipals, principal) + } + } else { + // Static principal + allowedPrincipals = append(allowedPrincipals, principal) + } + } + switch { case len(parsedPrincipals) == 0: // There is nothing to process @@ -230,7 +257,7 @@ func (b *backend) calculateValidPrincipals(data *framework.FieldData, defaultPri } for _, principal := range parsedPrincipals { - if !validatePrincipal(allowedPrincipals, principal) { + if !validatePrincipal(strutil.RemoveDuplicates(allowedPrincipals, false), principal) { return nil, fmt.Errorf("%v is not a valid value for valid_principals", principal) } } diff --git a/ui/app/models/role-ssh.js b/ui/app/models/role-ssh.js index fb1d6e052354..5f1128d4a607 100644 --- a/ui/app/models/role-ssh.js +++ b/ui/app/models/role-ssh.js @@ -26,6 +26,7 @@ const CA_FIELDS = [ 'allowHostCertificates', 'defaultUser', 'allowedUsers', + 'allowedUsersTemplate', 'allowedDomains', 'ttl', 'maxTtl', @@ -65,8 +66,11 @@ export default DS.Model.extend({ helpText: "Username to use when one isn't specified", }), allowedUsers: attr('string', { + helpText: 'Create a whitelist of users that can use this key (e.g. `admin, dev`, use `*` to allow all.)', + }), + allowedUsersTemplate: attr('boolean', { helpText: - 'Create a whitelist of users that can use this key (e.g. `admin, dev`, or use `*` to allow all)', + 'Specifies that Allowed users can be templated e.g. {{identity.entity.aliases.mount_accessor_xyz.name}}', }), allowedDomains: attr('string', { helpText: