From d4bc1ffadbd8f071c8e3cb996a06b83412211324 Mon Sep 17 00:00:00 2001 From: Fredrik Hoem Grelland Date: Fri, 10 Jan 2020 18:10:22 +0100 Subject: [PATCH 1/2] identity propagation in ssh secrets engine #7547 --- builtin/logical/ssh/path_sign.go | 32 +++++++++++++++++++++++++++----- ui/app/models/role-ssh.js | 2 +- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/builtin/logical/ssh/path_sign.go b/builtin/logical/ssh/path_sign.go index 95e12af1a42d..c0d66ef4e54c 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.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.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, defaultPrincipal, principalsAllowedByRole string, validatePrincipal func([]string, string) bool) ([]string, error) { validPrincipals := "" validPrincipalsRaw, ok := data.GetOk("valid_principals") if ok { @@ -214,7 +215,28 @@ 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) { + // 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) + } + } + switch { case len(parsedPrincipals) == 0: // There is nothing to process @@ -230,7 +252,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..81285c2e7d11 100644 --- a/ui/app/models/role-ssh.js +++ b/ui/app/models/role-ssh.js @@ -66,7 +66,7 @@ export default DS.Model.extend({ }), allowedUsers: attr('string', { helpText: - 'Create a whitelist of users that can use this key (e.g. `admin, dev`, or use `*` to allow all)', + 'Create a whitelist of users that can use this key (e.g. `admin, dev`, use `*` to allow all. Supports templated policies e.g. {{identity.entity.aliases.mount_accessor_xyz.name}})', }), allowedDomains: attr('string', { helpText: From ba6d4579d763e9ead3dc0b11eae8faa100e25b5a Mon Sep 17 00:00:00 2001 From: Fredrik Hoem Grelland Date: Fri, 10 Jan 2020 22:12:16 +0100 Subject: [PATCH 2/2] flag to enable templating allowed_users ssh (ca) secrets backend. --- builtin/logical/ssh/path_roles.go | 12 +++++++++++ builtin/logical/ssh/path_sign.go | 35 ++++++++++++++++++------------- ui/app/models/role-ssh.js | 6 +++++- 3 files changed, 37 insertions(+), 16 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 c0d66ef4e54c..090919dc422f 100644 --- a/builtin/logical/ssh/path_sign.go +++ b/builtin/logical/ssh/path_sign.go @@ -134,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, req, "", 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, req, 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 } @@ -205,7 +205,7 @@ func (b *backend) pathSignCertificate(ctx context.Context, req *logical.Request, return response, nil } -func (b *backend) calculateValidPrincipals(data *framework.FieldData, req *logical.Request, 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 { @@ -218,21 +218,26 @@ func (b *backend) calculateValidPrincipals(data *framework.FieldData, req *logic // Build list of allowed Principals from template and static principalsAllowedByRole var allowedPrincipals []string for _, principal := range strutil.RemoveDuplicates(strutil.ParseStringSlice(principalsAllowedByRole, ","), false) { - // 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) + 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 or err template + // Static principal allowedPrincipals = append(allowedPrincipals, principal) } } diff --git a/ui/app/models/role-ssh.js b/ui/app/models/role-ssh.js index 81285c2e7d11..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`, use `*` to allow all. Supports templated policies e.g. {{identity.entity.aliases.mount_accessor_xyz.name}})', + 'Specifies that Allowed users can be templated e.g. {{identity.entity.aliases.mount_accessor_xyz.name}}', }), allowedDomains: attr('string', { helpText: