mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
Support scalar and list values for node_labels
This PR references #2136
This commit is contained in:
committed by
Alexander Klizhentas
parent
f3eef0dc90
commit
34cd3da918
@@ -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()),
|
||||
},
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
+85
-32
@@ -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 *:<val> 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": {
|
||||
|
||||
+179
-48
@@ -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"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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 "<all nodes>"
|
||||
}
|
||||
pairs = append(pairs, fmt.Sprintf("%v=%v", key, val))
|
||||
pairs = append(pairs, fmt.Sprintf("%v=%v", key, values))
|
||||
}
|
||||
return strings.Join(pairs, ",")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user