From 34cd3da918bf07dec2a973efc8bcd220fe9f93ce Mon Sep 17 00:00:00 2001 From: Sasha Klizhentas Date: Fri, 10 Aug 2018 14:07:50 -0700 Subject: [PATCH] Support scalar and list values for node_labels This PR references #2136 --- lib/auth/permissions.go | 2 +- lib/services/oidc.go | 47 ------- lib/services/role.go | 117 ++++++++++----- lib/services/role_test.go | 227 +++++++++++++++++++++++------- lib/services/saml.go | 47 ------- lib/services/suite/suite.go | 2 +- lib/srv/regular/sshserver_test.go | 16 +-- lib/utils/utils.go | 20 +++ lib/web/apiserver_test.go | 2 +- tool/tctl/common/collection.go | 6 +- 10 files changed, 298 insertions(+), 188 deletions(-) diff --git a/lib/auth/permissions.go b/lib/auth/permissions.go index 99f777a0b79..b7dd3d1a9df 100644 --- a/lib/auth/permissions.go +++ b/lib/auth/permissions.go @@ -392,7 +392,7 @@ func GetCheckerForBuiltinRole(clusterName string, clusterConfig services.Cluster Allow: services.RoleConditions{ Namespaces: []string{services.Wildcard}, Logins: []string{}, - NodeLabels: map[string]string{services.Wildcard: services.Wildcard}, + NodeLabels: services.Labels{services.Wildcard: []string{services.Wildcard}}, Rules: []services.Rule{ services.NewRule(services.Wildcard, services.RW()), }, diff --git a/lib/services/oidc.go b/lib/services/oidc.go index 501dc6e59ac..26add2606a4 100644 --- a/lib/services/oidc.go +++ b/lib/services/oidc.go @@ -64,8 +64,6 @@ type OIDCConnector interface { GetClaims() []string // MapClaims maps claims to roles MapClaims(claims jose.Claims) []string - // RoleFromTemplate creates a role from a template and claims. - RoleFromTemplate(claims jose.Claims) (Role, error) // Check checks OIDC connector for errors Check() error // CheckAndSetDefaults checks and set default values for any missing fields. @@ -436,51 +434,6 @@ func executeSliceTemplate(raw []string, claims jose.Claims) ([]string, error) { return sl, nil } -// RoleFromTemplate creates a role from a template and claims. -func (o *OIDCConnectorV2) RoleFromTemplate(claims jose.Claims) (Role, error) { - for _, mapping := range o.Spec.ClaimsToRoles { - for claimName := range claims { - // claim name doesn't match - if claimName != mapping.Claim { - continue - } - - // claim value doesn't match - claimValue, ok, _ := claims.StringClaim(claimName) - if ok && claimValue != mapping.Value { - continue - } - - // claim name and value match, if a role template exists, execute template - roleTemplate := mapping.RoleTemplate - if roleTemplate != nil { - // at the moment, only allow templating for role name and logins - executedName, err := executeStringTemplate(roleTemplate.GetName(), claims) - if err != nil { - return nil, trace.Wrap(err) - } - executedLogins, err := executeSliceTemplate(roleTemplate.GetLogins(), claims) - if err != nil { - return nil, trace.Wrap(err) - } - - roleTemplate.SetName(executedName) - roleTemplate.SetLogins(executedLogins) - - // check all fields and make sure we have have a valid role - err = roleTemplate.CheckAndSetDefaults() - if err != nil { - return nil, trace.Wrap(err) - } - - return roleTemplate.V3(), nil - } - } - } - - return nil, trace.BadParameter("no matching claim name/value, claims: %v", claims) -} - // Check returns nil if all parameters are great, err otherwise func (o *OIDCConnectorV2) Check() error { if o.Metadata.Name == "" { diff --git a/lib/services/role.go b/lib/services/role.go index 2be9323d433..394e248e481 100644 --- a/lib/services/role.go +++ b/lib/services/role.go @@ -98,7 +98,7 @@ func NewAdminRole() Role { }, Allow: RoleConditions{ Namespaces: []string{defaults.Namespace}, - NodeLabels: map[string]string{Wildcard: Wildcard}, + NodeLabels: Labels{Wildcard: []string{Wildcard}}, Rules: CopyRulesSlice(AdminUserRules), }, }, @@ -148,7 +148,7 @@ func RoleForUser(u User) Role { }, Allow: RoleConditions{ Namespaces: []string{defaults.Namespace}, - NodeLabels: map[string]string{Wildcard: Wildcard}, + NodeLabels: Labels{Wildcard: []string{Wildcard}}, Rules: CopyRulesSlice(AdminUserRules), }, }, @@ -170,7 +170,7 @@ func RoleForCertAuthority(ca CertAuthority) Role { }, Allow: RoleConditions{ Namespaces: []string{defaults.Namespace}, - NodeLabels: map[string]string{Wildcard: Wildcard}, + NodeLabels: Labels{Wildcard: []string{Wildcard}}, Rules: CopyRulesSlice(DefaultCertAuthorityRules), }, }, @@ -249,9 +249,9 @@ type Role interface { SetNamespaces(RoleConditionType, []string) // GetNodeLabels gets the map of node labels this role is allowed or denied access to. - GetNodeLabels(RoleConditionType) map[string]string + GetNodeLabels(RoleConditionType) Labels // SetNodeLabels sets the map of node labels this role is allowed or denied access to. - SetNodeLabels(RoleConditionType, map[string]string) + SetNodeLabels(RoleConditionType, Labels) // GetRules gets all allow or deny rules. GetRules(rct RoleConditionType) []Rule @@ -312,25 +312,28 @@ func ApplyTraits(r Role, traits map[string][]string) Role { inLabels := r.GetNodeLabels(condition) // to avoid unnecessary allocations if inLabels != nil { - outLabels := make(map[string]string, len(inLabels)) + outLabels := make(Labels, len(inLabels)) // every key will be mapped to the first value - for key, val := range inLabels { + for key, vals := range inLabels { keyVars, err := applyValueTraits(key, traits) if err != nil { // empty key will not match anything - log.Debugf("Setting empty node label pair %q -> %q: %v", key, val, err) + log.Debugf("Setting empty node label pair %q -> %q: %v", key, vals, err) keyVars = []string{""} } - valVars, err := applyValueTraits(val, traits) - if err != nil { - log.Debugf("Setting empty node label value %q -> %q: %v", key, val, err) - // empty value will not match anything - valVars = []string{""} + var values []string + for _, val := range vals { + valVars, err := applyValueTraits(val, traits) + if err != nil { + log.Debugf("Setting empty node label value %q -> %q: %v", key, val, err) + // empty value will not match anything + valVars = []string{""} + } + values = append(values, valVars...) } - - outLabels[keyVars[0]] = valVars[0] + outLabels[keyVars[0]] = utils.Deduplicate(values) } r.SetNodeLabels(condition, outLabels) } @@ -400,7 +403,7 @@ func (r *RoleV3) Equals(other Role) bool { if !utils.StringSlicesEqual(r.GetNamespaces(condition), other.GetNamespaces(condition)) { return false } - if !utils.StringMapsEqual(r.GetNodeLabels(condition), other.GetNodeLabels(condition)) { + if !r.GetNodeLabels(condition).Equals(other.GetNodeLabels(condition)) { return false } if !RuleSlicesEqual(r.GetRules(condition), other.GetRules(condition)) { @@ -527,7 +530,7 @@ func (r *RoleV3) SetNamespaces(rct RoleConditionType, namespaces []string) { } // GetNodeLabels gets the map of node labels this role is allowed or denied access to. -func (r *RoleV3) GetNodeLabels(rct RoleConditionType) map[string]string { +func (r *RoleV3) GetNodeLabels(rct RoleConditionType) Labels { if rct == Allow { return r.Spec.Allow.NodeLabels } @@ -535,13 +538,11 @@ func (r *RoleV3) GetNodeLabels(rct RoleConditionType) map[string]string { } // SetNodeLabels sets the map of node labels this role is allowed or denied access to. -func (r *RoleV3) SetNodeLabels(rct RoleConditionType, labels map[string]string) { - lcopy := utils.CopyStringMap(labels) - +func (r *RoleV3) SetNodeLabels(rct RoleConditionType, labels Labels) { if rct == Allow { - r.Spec.Allow.NodeLabels = lcopy + r.Spec.Allow.NodeLabels = labels.Clone() } else { - r.Spec.Deny.NodeLabels = lcopy + r.Spec.Deny.NodeLabels = labels.Clone() } } @@ -585,7 +586,7 @@ func (r *RoleV3) CheckAndSetDefaults() error { r.Spec.Allow.Namespaces = []string{defaults.Namespace} } if r.Spec.Allow.NodeLabels == nil { - r.Spec.Allow.NodeLabels = map[string]string{Wildcard: Wildcard} + r.Spec.Allow.NodeLabels = Labels{Wildcard: []string{Wildcard}} } if r.Spec.Deny.Namespaces == nil { r.Spec.Deny.Namespaces = []string{defaults.Namespace} @@ -615,7 +616,7 @@ func (r *RoleV3) CheckAndSetDefaults() error { } } for key, val := range r.Spec.Allow.NodeLabels { - if key == Wildcard && val != Wildcard { + if key == Wildcard && (len(val) != 1 || val[0] != Wildcard) { return trace.BadParameter("selector *: is not supported") } } @@ -694,8 +695,9 @@ type RoleConditions struct { // Namespaces is a list of namespaces (used to partition a cluster). The // field should be called "namespaces" when it returns in Teleport 2.4. Namespaces []string `json:"-"` + // NodeLabels is a map of node labels (used to dynamically grant access to nodes). - NodeLabels map[string]string `json:"node_labels,omitempty"` + NodeLabels Labels `json:"node_labels,omitempty"` // Rules is a list of rules and their access levels. Rules are a high level // construct used for access control. @@ -714,7 +716,7 @@ func (r *RoleConditions) Equals(o RoleConditions) bool { if !utils.StringSlicesEqual(r.Namespaces, o.Namespaces) { return false } - if !utils.StringMapsEqual(r.NodeLabels, o.NodeLabels) { + if !r.NodeLabels.Equals(o.NodeLabels) { return false } if len(r.Rules) != len(o.Rules) { @@ -1177,7 +1179,7 @@ func (r *RoleV2) V3() *RoleV3 { Allow: RoleConditions{ Logins: r.GetLogins(), Namespaces: r.GetNamespaces(), - NodeLabels: r.GetNodeLabels(), + NodeLabels: scalarLabels(r.GetNodeLabels()).labels(), }, }, rawObject: *r, @@ -1392,17 +1394,23 @@ func MatchNamespace(selector []string, namespace string) bool { } // MatchLabels matches selector against target -func MatchLabels(selector map[string]string, target map[string]string) bool { +func MatchLabels(selector Labels, target map[string]string) bool { // empty selector matches nothing if len(selector) == 0 { return false } // *: * matches everything even empty target set - if selector[Wildcard] == Wildcard { + selectorValues := selector[Wildcard] + if len(selectorValues) == 1 && selectorValues[0] == Wildcard { return true } - for key, val := range selector { - if targetVal, ok := target[key]; !ok || (val != targetVal && val != Wildcard) { + // otherwise perform full match + for key, selectorValues := range selector { + targetVal, hasKey := target[key] + if !hasKey { + return false + } + if !utils.SliceContainsStr(selectorValues, Wildcard) && !utils.SliceContainsStr(selectorValues, targetVal) { return false } } @@ -1740,6 +1748,50 @@ func BoolOption(v *Bool) Bool { return *v } +// Labels is a wrapper around map +// that can marshal and unmarshal itself +// from scalar and list values +type Labels map[string]utils.Strings + +// Clone returns non-shallow copy of the labels set +func (l Labels) Clone() Labels { + if l == nil { + return nil + } + out := make(Labels, len(l)) + for key, vals := range l { + cvals := make([]string, len(vals)) + copy(cvals, vals) + out[key] = cvals + } + return out +} + +// Equals returns true if two label sets are equal +func (l Labels) Equals(o Labels) bool { + if len(l) != len(o) { + return false + } + for key := range l { + if !utils.StringSlicesEqual(l[key], o[key]) { + return false + } + } + return true +} + +// scalarLabels is a key value map +// with scalar values +type scalarLabels map[string]string + +func (l scalarLabels) labels() Labels { + out := make(Labels, len(l)) + for key, val := range l { + out[key] = []string{val} + } + return out +} + // Bool is a wrapper around boolean values type Bool struct { bool @@ -1788,6 +1840,7 @@ func (b *Bool) UnmarshalYAML(unmarshal func(interface{}) error) error { var boolVar bool if err := unmarshal(&boolVar); err == nil { b.bool = boolVar + return nil } var stringVar string if err := unmarshal(&stringVar); err != nil { @@ -1895,7 +1948,7 @@ const RoleSpecV3SchemaDefinitions = ` "type": "object", "additionalProperties": false, "patternProperties": { - "^[a-zA-Z/.0-9_*]+$": { "type": "string" } + "^[a-zA-Z/.0-9_*]+$": { "anyOf": [{"type": "string"}, { "type": "array", "items": {"type": "string"}}]} } }, "logins": { diff --git a/lib/services/role_test.go b/lib/services/role_test.go index 55c98974e20..5006ac2cc73 100644 --- a/lib/services/role_test.go +++ b/lib/services/role_test.go @@ -198,7 +198,7 @@ func (s *RoleSuite) TestRoleParse(c *C) { PortForwarding: NewBoolOption(true), }, Allow: RoleConditions{ - NodeLabels: map[string]string{Wildcard: Wildcard}, + NodeLabels: Labels{Wildcard: []string{Wildcard}}, Namespaces: []string{defaults.Namespace}, }, Deny: RoleConditions{ @@ -257,7 +257,7 @@ func (s *RoleSuite) TestRoleParse(c *C) { DisconnectExpiredCert: NewBool(true), }, Allow: RoleConditions{ - NodeLabels: map[string]string{"a": "b"}, + NodeLabels: Labels{"a": []string{"b"}}, Namespaces: []string{"default"}, Rules: []Rule{ Rule{ @@ -279,7 +279,7 @@ func (s *RoleSuite) TestRoleParse(c *C) { error: nil, }, { - name: "alternative options forma", + name: "alternative options form", in: `{ "kind": "role", "version": "v3", @@ -329,7 +329,7 @@ func (s *RoleSuite) TestRoleParse(c *C) { DisconnectExpiredCert: NewBool(false), }, Allow: RoleConditions{ - NodeLabels: map[string]string{"a": "b"}, + NodeLabels: Labels{"a": []string{"b"}}, Namespaces: []string{"default"}, Rules: []Rule{ Rule{ @@ -350,6 +350,61 @@ func (s *RoleSuite) TestRoleParse(c *C) { }, error: nil, }, + { + name: "non-scalar and scalar values of labels", + in: `{ + "kind": "role", + "version": "v3", + "metadata": {"name": "name1"}, + "spec": { + "options": { + "cert_format": "standard", + "max_session_ttl": "20h", + "port_forwarding": "yes", + "forward_agent": "yes", + "client_idle_timeout": "never", + "disconnect_expired_cert": "no" + }, + "allow": { + "node_labels": {"a": "b", "key": ["val"], "key2": ["val2", "val3"]} + }, + "deny": { + "logins": ["c"] + } + } + }`, + role: RoleV3{ + Kind: KindRole, + Version: V3, + Metadata: Metadata{ + Name: "name1", + Namespace: defaults.Namespace, + }, + Spec: RoleSpecV3{ + Options: RoleOptions{ + CertificateFormat: teleport.CertificateFormatStandard, + ForwardAgent: NewBool(true), + MaxSessionTTL: NewDuration(20 * time.Hour), + PortForwarding: NewBoolOption(true), + ClientIdleTimeout: NewDuration(0), + DisconnectExpiredCert: NewBool(false), + }, + Allow: RoleConditions{ + NodeLabels: Labels{ + "a": []string{"b"}, + "key": []string{"val"}, + "key2": []string{"val2", "val3"}, + }, + Namespaces: []string{"default"}, + }, + Deny: RoleConditions{ + Namespaces: []string{defaults.Namespace}, + Logins: []string{"c"}, + }, + }, + }, + error: nil, + }, } for i, tc := range testCases { comment := Commentf("test case %v %q", i, tc.name) @@ -374,6 +429,22 @@ func (s *RoleSuite) TestRoleParse(c *C) { } } +// TestLabelCompatibility makes sure that labels +// are serialized in format understood by older servers with +// scalar labels +func (s *RoleSuite) TestLabelCompatibility(c *C) { + labels := Labels{ + "key": []string{"val"}, + } + data, err := json.Marshal(labels) + c.Assert(err, IsNil) + + var out map[string]string + err = json.Unmarshal(data, &out) + c.Assert(err, IsNil) + c.Assert(out, DeepEquals, map[string]string{"key": "val"}) +} + func (s *RoleSuite) TestCheckAccess(c *C) { type check struct { server Server @@ -402,12 +473,12 @@ func (s *RoleSuite) TestCheckAccess(c *C) { } testCases := []struct { name string - roles []RoleV2 + roles []RoleV3 checks []check }{ { name: "empty role set has access to nothing", - roles: []RoleV2{}, + roles: []RoleV3{}, checks: []check{ {server: serverA, login: "root", hasAccess: false}, {server: serverB, login: "root", hasAccess: false}, @@ -416,17 +487,21 @@ func (s *RoleSuite) TestCheckAccess(c *C) { }, { name: "role is limited to default namespace", - roles: []RoleV2{ - RoleV2{ + roles: []RoleV3{ + RoleV3{ Metadata: Metadata{ Name: "name1", Namespace: defaults.Namespace, }, - Spec: RoleSpecV2{ - MaxSessionTTL: Duration{20 * time.Hour}, - Logins: []string{"admin"}, - NodeLabels: map[string]string{Wildcard: Wildcard}, - Namespaces: []string{defaults.Namespace}, + Spec: RoleSpecV3{ + Options: RoleOptions{ + MaxSessionTTL: Duration{20 * time.Hour}, + }, + Allow: RoleConditions{ + Namespaces: []string{defaults.Namespace}, + Logins: []string{"admin"}, + NodeLabels: Labels{Wildcard: []string{Wildcard}}, + }, }, }, }, @@ -441,17 +516,50 @@ func (s *RoleSuite) TestCheckAccess(c *C) { }, { name: "role is limited to labels in default namespace", - roles: []RoleV2{ - RoleV2{ + roles: []RoleV3{ + RoleV3{ Metadata: Metadata{ Name: "name1", Namespace: defaults.Namespace, }, - Spec: RoleSpecV2{ - MaxSessionTTL: Duration{20 * time.Hour}, - Logins: []string{"admin"}, - NodeLabels: map[string]string{"role": "worker"}, - Namespaces: []string{defaults.Namespace}, + Spec: RoleSpecV3{ + Options: RoleOptions{ + MaxSessionTTL: Duration{20 * time.Hour}, + }, + Allow: RoleConditions{ + Logins: []string{"admin"}, + NodeLabels: Labels{"role": []string{"worker"}}, + Namespaces: []string{defaults.Namespace}, + }, + }, + }, + }, + checks: []check{ + {server: serverA, login: "root", hasAccess: false}, + {server: serverA, login: "admin", hasAccess: false}, + {server: serverB, login: "root", hasAccess: false}, + {server: serverB, login: "admin", hasAccess: true}, + {server: serverC, login: "root", hasAccess: false}, + {server: serverC, login: "admin", hasAccess: false}, + }, + }, + { + name: "role matches any label out of multiple labels", + roles: []RoleV3{ + RoleV3{ + Metadata: Metadata{ + Name: "name1", + Namespace: defaults.Namespace, + }, + Spec: RoleSpecV3{ + Options: RoleOptions{ + MaxSessionTTL: Duration{20 * time.Hour}, + }, + Allow: RoleConditions{ + Logins: []string{"admin"}, + NodeLabels: Labels{"role": []string{"worker2", "worker"}}, + Namespaces: []string{defaults.Namespace}, + }, }, }, }, @@ -466,29 +574,37 @@ func (s *RoleSuite) TestCheckAccess(c *C) { }, { name: "one role is more permissive than another", - roles: []RoleV2{ - RoleV2{ + roles: []RoleV3{ + RoleV3{ Metadata: Metadata{ Name: "name1", Namespace: defaults.Namespace, }, - Spec: RoleSpecV2{ - MaxSessionTTL: Duration{20 * time.Hour}, - Logins: []string{"admin"}, - NodeLabels: map[string]string{"role": "worker"}, - Namespaces: []string{defaults.Namespace}, + Spec: RoleSpecV3{ + Options: RoleOptions{ + MaxSessionTTL: Duration{20 * time.Hour}, + }, + Allow: RoleConditions{ + Logins: []string{"admin"}, + NodeLabels: Labels{"role": []string{"worker"}}, + Namespaces: []string{defaults.Namespace}, + }, }, }, - RoleV2{ + RoleV3{ Metadata: Metadata{ Name: "name1", Namespace: defaults.Namespace, }, - Spec: RoleSpecV2{ - MaxSessionTTL: Duration{20 * time.Hour}, - Logins: []string{"root", "admin"}, - NodeLabels: map[string]string{Wildcard: Wildcard}, - Namespaces: []string{Wildcard}, + Spec: RoleSpecV3{ + Options: RoleOptions{ + MaxSessionTTL: Duration{20 * time.Hour}, + }, + Allow: RoleConditions{ + Logins: []string{"root", "admin"}, + NodeLabels: Labels{Wildcard: []string{Wildcard}}, + Namespaces: []string{Wildcard}, + }, }, }, }, @@ -506,7 +622,7 @@ func (s *RoleSuite) TestCheckAccess(c *C) { var set RoleSet for i := range tc.roles { - set = append(set, tc.roles[i].V3()) + set = append(set, &tc.roles[i]) } for j, check := range tc.checks { comment := Commentf("test case %v '%v', check %v", i, tc.name, j) @@ -928,8 +1044,8 @@ func (s *RoleSuite) TestApplyTraits(c *C) { type rule struct { inLogins []string outLogins []string - inLabels map[string]string - outLabels map[string]string + inLabels Labels + outLabels Labels inKubeGroups []string outKubeGroups []string } @@ -1048,12 +1164,12 @@ func (s *RoleSuite) TestApplyTraits(c *C) { "hello": []string{"there"}, }, allow: rule{ - inLabels: map[string]string{`{{external.foo}}`: "{{external.hello}}"}, - outLabels: map[string]string{`bar`: "there"}, + inLabels: Labels{`{{external.foo}}`: []string{"{{external.hello}}"}}, + outLabels: Labels{`bar`: []string{"there"}}, }, deny: rule{ - inLabels: map[string]string{`{{external.hello}}`: "{{external.foo}}"}, - outLabels: map[string]string{`there`: "bar"}, + inLabels: Labels{`{{external.hello}}`: []string{"{{external.foo}}"}}, + outLabels: Labels{`there`: []string{"bar"}}, }, }, @@ -1063,23 +1179,38 @@ func (s *RoleSuite) TestApplyTraits(c *C) { "foo": []string{"bar"}, }, allow: rule{ - inLabels: map[string]string{ - `{{external.foo}}`: "value", - `{{external.missing}}`: "missing", - `missing`: "{{external.missing}}", + inLabels: Labels{ + `{{external.foo}}`: []string{"value"}, + `{{external.missing}}`: []string{"missing"}, + `missing`: []string{"{{external.missing}}", "othervalue"}, + }, + outLabels: Labels{ + `bar`: []string{"value"}, + "missing": []string{"", "othervalue"}, + "": []string{"missing"}, }, - outLabels: map[string]string{`bar`: "value", "missing": "", "": "missing"}, }, }, { - comment: "the first variable value is picked for labels", + comment: "the first variable value is picked for label keys", inTraits: map[string][]string{ "foo": []string{"bar", "baz"}, }, allow: rule{ - inLabels: map[string]string{`{{external.foo}}`: "value"}, - outLabels: map[string]string{`bar`: "value"}, + inLabels: Labels{`{{external.foo}}`: []string{"value"}}, + outLabels: Labels{`bar`: []string{"value"}}, + }, + }, + + { + comment: "all values are expanded for label values", + inTraits: map[string][]string{ + "foo": []string{"bar", "baz"}, + }, + allow: rule{ + inLabels: Labels{`key`: []string{`{{external.foo}}`}}, + outLabels: Labels{`key`: []string{"bar", "baz"}}, }, }, } diff --git a/lib/services/saml.go b/lib/services/saml.go index 523f225f176..02cf5f292e7 100644 --- a/lib/services/saml.go +++ b/lib/services/saml.go @@ -59,8 +59,6 @@ type SAMLConnector interface { GetAttributes() []string // MapAttributes maps attributes to roles MapAttributes(assertionInfo saml2.AssertionInfo) []string - // RoleFromTemplate creates a role from a template and claims. - RoleFromTemplate(assertionInfo saml2.AssertionInfo) (Role, error) // Check checks SAML connector for errors CheckAndSetDefaults() error // SetIssuer sets issuer @@ -497,51 +495,6 @@ func executeSAMLSliceTemplate(raw []string, assertion map[string]string) ([]stri return sl, nil } -// RoleFromTemplate creates a role from a template and claims. -func (o *SAMLConnectorV2) RoleFromTemplate(assertionInfo saml2.AssertionInfo) (Role, error) { - assertionMap := buildAssertionMap(assertionInfo) - for _, mapping := range o.Spec.AttributesToRoles { - for assrName, assrValue := range assertionMap { - // match assertion name - if assrName != mapping.Name { - continue - } - - // match assertion value - if assrValue != mapping.Value { - continue - } - - // claim name and value match, if a role template exists, execute template - roleTemplate := mapping.RoleTemplate - if roleTemplate != nil { - // at the moment, only allow templating for role name and logins - executedName, err := executeSAMLStringTemplate(roleTemplate.GetName(), assertionMap) - if err != nil { - return nil, trace.Wrap(err) - } - executedLogins, err := executeSAMLSliceTemplate(roleTemplate.GetLogins(), assertionMap) - if err != nil { - return nil, trace.Wrap(err) - } - - roleTemplate.SetName(executedName) - roleTemplate.SetLogins(executedLogins) - - // check all fields and make sure we have have a valid role - err = roleTemplate.CheckAndSetDefaults() - if err != nil { - return nil, trace.Wrap(err) - } - - return roleTemplate.V3(), nil - } - } - } - - return nil, trace.BadParameter("no matching assertion name/value, assertions: %v", assertionMap) -} - // GetServiceProvider initialises service provider spec from settings func (o *SAMLConnectorV2) GetServiceProvider(clock clockwork.Clock) (*saml2.SAMLServiceProvider, error) { if o.Metadata.Name == "" { diff --git a/lib/services/suite/suite.go b/lib/services/suite/suite.go index ac1423c1102..3ef65096929 100644 --- a/lib/services/suite/suite.go +++ b/lib/services/suite/suite.go @@ -436,7 +436,7 @@ func (s *ServicesTestSuite) RolesCRUD(c *C) { }, Allow: services.RoleConditions{ Logins: []string{"root", "bob"}, - NodeLabels: map[string]string{services.Wildcard: services.Wildcard}, + NodeLabels: services.Labels{services.Wildcard: []string{services.Wildcard}}, Namespaces: []string{defaults.Namespace}, Rules: []services.Rule{ services.NewRule(services.KindRole, services.RO()), diff --git a/lib/srv/regular/sshserver_test.go b/lib/srv/regular/sshserver_test.go index d79d328a9c5..4cb88501baf 100644 --- a/lib/srv/regular/sshserver_test.go +++ b/lib/srv/regular/sshserver_test.go @@ -75,8 +75,8 @@ type SrvSuite struct { const teleportTestUser = "teleport-test" // wildcardAllow is used in tests to allow access to all labels. -var wildcardAllow map[string]string = map[string]string{ - services.Wildcard: services.Wildcard, +var wildcardAllow = services.Labels{ + services.Wildcard: []string{services.Wildcard}, } var _ = Suite(&SrvSuite{}) @@ -343,27 +343,27 @@ func (s *SrvSuite) TestAllowedUsers(c *C) { func (s *SrvSuite) TestAllowedLabels(c *C) { var tests = []struct { - inLabelMap map[string]string + inLabelMap services.Labels outError bool }{ // Valid static label. { - inLabelMap: map[string]string{"foo": "bar"}, + inLabelMap: services.Labels{"foo": []string{"bar"}}, outError: false, }, // Invalid static label. { - inLabelMap: map[string]string{"foo": "baz"}, + inLabelMap: services.Labels{"foo": []string{"baz"}}, outError: true, }, // Valid dynamic label. { - inLabelMap: map[string]string{"baz": "4"}, + inLabelMap: services.Labels{"baz": []string{"4"}}, outError: false, }, // Invalid dynamic label. { - inLabelMap: map[string]string{"baz": "5"}, + inLabelMap: services.Labels{"baz": []string{"5"}}, outError: true, }, } @@ -1081,7 +1081,7 @@ type upack struct { certSigner ssh.Signer } -func (s *SrvSuite) newUpack(username string, allowedLogins []string, allowedLabels map[string]string) (*upack, error) { +func (s *SrvSuite) newUpack(username string, allowedLogins []string, allowedLabels services.Labels) (*upack, error) { auth := s.server.Auth() upriv, upub, err := auth.GenerateKeyPair("") if err != nil { diff --git a/lib/utils/utils.go b/lib/utils/utils.go index ca0665d3930..3a7a773bf6d 100644 --- a/lib/utils/utils.go +++ b/lib/utils/utils.go @@ -394,6 +394,26 @@ func (s *Strings) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } +// MarshalJSON marshals to scalar value +// if there is only one value in the list +// to list otherwise +func (s Strings) MarshalJSON() ([]byte, error) { + if len(s) == 1 { + return json.Marshal(s[0]) + } + return json.Marshal([]string(s)) +} + +// MarshalYAML marshals to scalar value +// if there is only one value in the list, +// marshals to list otherwise +func (s Strings) MarshalYAML() (interface{}, error) { + if len(s) == 1 { + return s[0], nil + } + return []string(s), nil +} + // Addrs returns strings list converted to address list func (s Strings) Addrs(defaultPort int) ([]NetAddr, error) { addrs := make([]NetAddr, len(s)) diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index c66afa23aa6..b19e32de105 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -447,7 +447,7 @@ func (s *WebSuite) TestSAMLSuccess(c *C) { MaxSessionTTL: services.NewDuration(defaults.MaxCertDuration), }, Allow: services.RoleConditions{ - NodeLabels: map[string]string{services.Wildcard: services.Wildcard}, + NodeLabels: services.Labels{services.Wildcard: []string{services.Wildcard}}, Namespaces: []string{defaults.Namespace}, Rules: []services.Rule{ services.NewRule(services.Wildcard, services.RW()), diff --git a/tool/tctl/common/collection.go b/tool/tctl/common/collection.go index e5c6d94065e..0358ec10f6a 100644 --- a/tool/tctl/common/collection.go +++ b/tool/tctl/common/collection.go @@ -131,13 +131,13 @@ func printActions(rules []services.Rule) string { return strings.Join(pairs, ",") } -func printNodeLabels(labels map[string]string) string { +func printNodeLabels(labels services.Labels) string { pairs := []string{} - for key, val := range labels { + for key, values := range labels { if key == services.Wildcard { return "" } - pairs = append(pairs, fmt.Sprintf("%v=%v", key, val)) + pairs = append(pairs, fmt.Sprintf("%v=%v", key, values)) } return strings.Join(pairs, ",") }