diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index b00e541bf4a2f..76dafa7f76738 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -160,6 +160,20 @@ func (a *ServerWithRoles) authConnectorAction(namespace string, resource string, return nil } +// identityCenterAction is a special checker that grants access to Identity Center +// resources. In order to simplify the writing of role condition statements, the +// various Identity Center resources are bundled up under an umbrella +// `KindIdentityCenter` resource kind. This means that if access to the target +// resource is not explicitly denied, then the user has a second chance to get +// access via the generic resource kind. +func (a *ServerWithRoles) identityCenterAction(namespace string, resource string, verbs ...string) error { + err := a.action(namespace, resource, verbs...) + if err == nil || services.IsAccessExplicitlyDenied(err) { + return trace.Wrap(err) + } + return trace.Wrap(a.action(namespace, types.KindIdentityCenter, verbs...)) +} + // actionForListWithCondition extracts a restrictive filter condition to be // added to a list query after a simple resource check fails. func (a *ServerWithRoles) actionForListWithCondition(namespace, resource, identifier string) (*types.WhereExpr, error) { @@ -1321,6 +1335,17 @@ func (c *resourceAccess) checkAccess(resource types.ResourceWithLabels, filter s return true, nil } +type actionChecker func(namespace, resourceKind string, verbs ...string) error + +func (a *ServerWithRoles) selectActionChecker(resourceKind string) actionChecker { + if resourceKind == types.KindIdentityCenterAccount { + // Identity Center resources can be specified multiple ways in a Role + // Condition statement, so we need a special checker to handle it. + return a.identityCenterAction + } + return a.action +} + // ListUnifiedResources returns a paginated list of unified resources filtered by user access. func (a *ServerWithRoles) ListUnifiedResources(ctx context.Context, req *proto.ListUnifiedResourcesRequest) (*proto.ListUnifiedResourcesResponse, error) { filter := services.MatchResourceFilter{ @@ -1358,7 +1383,8 @@ func (a *ServerWithRoles) ListUnifiedResources(ctx context.Context, req *proto.L actionVerbs = []string{types.VerbList} } - resourceAccess.kindAccessMap[kind] = a.action(apidefaults.Namespace, kind, actionVerbs...) + checkAction := a.selectActionChecker(kind) + resourceAccess.kindAccessMap[kind] = checkAction(apidefaults.Namespace, kind, actionVerbs...) } // Before doing any listing, verify that the user is allowed to list @@ -1702,7 +1728,8 @@ func (a *ServerWithRoles) ListResources(ctx context.Context, req proto.ListResou return nil, trace.NotImplemented("resource type %s does not support pagination", req.ResourceType) } - if err := a.action(req.Namespace, req.ResourceType, actionVerbs...); err != nil { + checkAction := a.selectActionChecker(req.ResourceType) + if err := checkAction(req.Namespace, req.ResourceType, actionVerbs...); err != nil { return nil, trace.Wrap(err) } @@ -1829,9 +1856,14 @@ func (r resourceChecker) CanAccess(resource types.Resource) error { } case types.SAMLIdPServiceProvider: return r.CheckAccess(rr, state) + + case types.Resource153Unwrapper: + if checkable, ok := rr.(services.AccessCheckable); ok { + return r.CheckAccess(checkable, state) + } } - return trace.BadParameter("could not check access to resource type %T", r) + return trace.BadParameter("could not check access to resource type %T", resource) } // newResourceAccessChecker creates a resourceAccessChecker for the provided resource type @@ -1846,7 +1878,8 @@ func (a *ServerWithRoles) newResourceAccessChecker(resource string) (resourceAcc types.KindKubeServer, types.KindUserGroup, types.KindUnifiedResource, - types.KindSAMLIdPServiceProvider: + types.KindSAMLIdPServiceProvider, + types.KindIdentityCenterAccount: return &resourceChecker{AccessChecker: a.context.Checker}, nil default: return nil, trace.BadParameter("could not check access to resource type %s", resource) diff --git a/lib/auth/auth_with_roles_test.go b/lib/auth/auth_with_roles_test.go index e780234dbc21d..9c319cd73c2c7 100644 --- a/lib/auth/auth_with_roles_test.go +++ b/lib/auth/auth_with_roles_test.go @@ -5768,11 +5768,18 @@ func TestUnifiedResources_IdentityCenter(t *testing.T) { }) require.NoError(t, err) - t.Run("access denied", func(t *testing.T) { - // Asserts that, with no RBAC or matchers in place, acces to IC Accounts - // is denied by default + setAccountAssignment := func(role types.Role) { + r := role.(*types.RoleV6) + r.Spec.Allow.AccountAssignments = []types.IdentityCenterAccountAssignment{ + { + Account: "11111111", + PermissionSet: "some:arn", + }, + } + } - userNoAccess, _, err := CreateUserAndRole(srv.Auth(), "test", nil, nil) + t.Run("no access", func(t *testing.T) { + userNoAccess, _, err := CreateUserAndRole(srv.Auth(), "no-access", nil, nil) require.NoError(t, err) identity := TestUser(userNoAccess.GetName()) @@ -5780,16 +5787,83 @@ func TestUnifiedResources_IdentityCenter(t *testing.T) { require.NoError(t, err) defer clt.Close() - _, err = clt.ListResources(ctx, proto.ListResourcesRequest{ + resp, err := clt.ListResources(ctx, proto.ListResourcesRequest{ + ResourceType: types.KindIdentityCenterAccount, + Labels: map[string]string{ + types.OriginLabel: apicommon.OriginAWSIdentityCenter, + }, + }) + require.NoError(t, err) + require.Empty(t, resp.Resources) + }) + + t.Run("access via generic kind", func(t *testing.T) { + user, _, err := CreateUserAndRole(srv.Auth(), "read-generic", nil, + []types.Rule{ + types.NewRule(types.KindIdentityCenter, services.RO()), + }, + WithRoleMutator(setAccountAssignment)) + require.NoError(t, err) + + identity := TestUser(user.GetName()) + clt, err := srv.NewClient(identity) + require.NoError(t, err) + defer clt.Close() + + resp, err := clt.ListResources(ctx, proto.ListResourcesRequest{ ResourceType: types.KindIdentityCenterAccount, Labels: map[string]string{ types.OriginLabel: apicommon.OriginAWSIdentityCenter, }, }) - require.True(t, trace.IsAccessDenied(err)) + require.NoError(t, err) + require.Len(t, resp.Resources, 1) + }) + + t.Run("access via specific kind", func(t *testing.T) { + user, _, err := CreateUserAndRole(srv.Auth(), "read-specific", nil, + []types.Rule{ + types.NewRule(types.KindIdentityCenterAccount, services.RO()), + }, + WithRoleMutator(setAccountAssignment)) + require.NoError(t, err) + + identity := TestUser(user.GetName()) + clt, err := srv.NewClient(identity) + require.NoError(t, err) + defer clt.Close() + + resp, err := clt.ListResources(ctx, proto.ListResourcesRequest{ + ResourceType: types.KindIdentityCenterAccount, + }) + require.NoError(t, err) + require.Len(t, resp.Resources, 1) }) - // TODO(tcsc): Add other tests one RBAC implemented + t.Run("denied via specific kind beats allow via generic kind", func(t *testing.T) { + user, _, err := CreateUserAndRole(srv.Auth(), "specific-beats-generic", nil, + []types.Rule{ + types.NewRule(types.KindIdentityCenter, services.RO()), + }, + WithRoleMutator(func(r types.Role) { + setAccountAssignment(r) + r.SetRules(types.Deny, []types.Rule{ + types.NewRule(types.KindIdentityCenterAccount, services.RO()), + }) + })) + require.NoError(t, err) + + identity := TestUser(user.GetName()) + clt, err := srv.NewClient(identity) + require.NoError(t, err) + defer clt.Close() + + _, err = clt.ListResources(ctx, proto.ListResourcesRequest{ + ResourceType: types.KindIdentityCenterAccount, + }) + require.True(t, trace.IsAccessDenied(err), + "Expected Access Denied, got %v", err) + }) } func BenchmarkListUnifiedResourcesFilter(b *testing.B) { diff --git a/lib/services/access_checker.go b/lib/services/access_checker.go index 39da72d5bbf8d..6174f56d7f7ba 100644 --- a/lib/services/access_checker.go +++ b/lib/services/access_checker.go @@ -444,6 +444,18 @@ func (a *accessChecker) CheckAccess(r AccessCheckable, state AccessState, matche if err := a.checkAllowedResources(r); err != nil { return trace.Wrap(err) } + + switch rr := r.(type) { + case types.Resource153Unwrapper: + switch urr := rr.Unwrap().(type) { + case IdentityCenterAccount: + matchers = append(matchers, NewIdentityCenterAccountMatcher(urr)) + + case IdentityCenterAccountAssignment: + matchers = append(matchers, NewIdentityCenterAccountAssignmentMatcher(urr)) + } + } + return trace.Wrap(a.RoleSet.checkAccess(r, a.info.Traits, state, matchers...)) } diff --git a/lib/services/access_request.go b/lib/services/access_request.go index 1e3ef0c8fb9de..67da35cde7381 100644 --- a/lib/services/access_request.go +++ b/lib/services/access_request.go @@ -2191,19 +2191,32 @@ func (m *RequestValidator) pruneResourceRequestRoles( necessaryRoles := make(map[string]struct{}) for _, resource := range resources { var ( - rolesForResource []types.Role - resourceMatcher *KubeResourcesMatcher + rolesForResource []types.Role + matchers []RoleMatcher + kubeResourceMatcher *KubeResourcesMatcher ) kubernetesResources, err := getKubeResourcesFromResourceIDs(resourceIDs, resource.GetName()) if err != nil { return nil, trace.Wrap(err) } if len(kubernetesResources) > 0 { - resourceMatcher = NewKubeResourcesMatcher(kubernetesResources) + kubeResourceMatcher = NewKubeResourcesMatcher(kubernetesResources) + matchers = append(matchers, kubeResourceMatcher) + } + + switch rr := resource.(type) { + case types.Resource153Unwrapper: + switch urr := rr.Unwrap().(type) { + case IdentityCenterAccount: + matchers = append(matchers, NewIdentityCenterAccountMatcher(urr)) + + case IdentityCenterAccountAssignment: + matchers = append(matchers, NewIdentityCenterAccountAssignmentMatcher(urr)) + } } for _, role := range allRoles { - roleAllowsAccess, err := m.roleAllowsResource(role, resource, loginHint, resourceMatcherToMatcherSlice(resourceMatcher)...) + roleAllowsAccess, err := m.roleAllowsResource(role, resource, loginHint, matchers...) if err != nil { return nil, trace.Wrap(err) } @@ -2217,7 +2230,7 @@ func (m *RequestValidator) pruneResourceRequestRoles( // If any of the requested resources didn't match with the provided roles, // we deny the request because the user is trying to request more access // than what is allowed by its search_as_roles. - if resourceMatcher != nil && len(resourceMatcher.Unmatched()) > 0 { + if kubeResourceMatcher != nil && len(kubeResourceMatcher.Unmatched()) > 0 { resourcesStr, err := types.ResourceIDsToString(resourceIDs) if err != nil { return nil, trace.Wrap(err) @@ -2226,7 +2239,7 @@ func (m *RequestValidator) pruneResourceRequestRoles( `no roles configured in the "search_as_roles" for this user allow `+ `access to at least one requested resources. `+ `resources: %s roles: %v unmatched resources: %v`, - resourcesStr, roles, resourceMatcher.Unmatched()) + resourcesStr, roles, kubeResourceMatcher.Unmatched()) } if len(loginHint) > 0 { // If we have a login hint, request the single role with the fewest @@ -2335,15 +2348,6 @@ func (m *RequestValidator) roleAllowsResource( return true, nil } -// resourceMatcherToMatcherSlice returns the resourceMatcher in a RoleMatcher slice -// if the resourceMatcher is not nil, otherwise returns a nil slice. -func resourceMatcherToMatcherSlice(resourceMatcher *KubeResourcesMatcher) []RoleMatcher { - if resourceMatcher == nil { - return nil - } - return []RoleMatcher{resourceMatcher} -} - // getUnderlyingResourcesByResourceIDs gets the underlying resources the user // requested access. Except for resource Kinds present in types.KubernetesResourcesKinds, // the underlying resources are the same as requested. If the resource requested diff --git a/lib/services/identitycenter.go b/lib/services/identitycenter.go index 538f8a84f91ea..2f26246e59a8a 100644 --- a/lib/services/identitycenter.go +++ b/lib/services/identitycenter.go @@ -18,12 +18,15 @@ package services import ( "context" + "fmt" + "github.com/gravitational/trace" "google.golang.org/protobuf/types/known/emptypb" identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1" "github.com/gravitational/teleport/api/types" apiutils "github.com/gravitational/teleport/api/utils" + "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/pagination" ) @@ -232,3 +235,105 @@ type IdentityCenter interface { IdentityCenterPrincipalAssignments IdentityCenterAccountAssignments } + +// NewIdentityCenterAccountMatcher creates a new [IdentityCenterMatcher] +// configured to match the supplied [IdentityCenterAccount]. +func NewIdentityCenterAccountMatcher(account IdentityCenterAccount) RoleMatcher { + return &IdentityCenterAccountMatcher{ + accountID: account.GetSpec().GetId(), + } +} + +// IdentityCenterMatcher implements a [RoleMatcher] for comparing Identity Center +// resources against the AccountAssignments specified in a Role condition. +// +// The same type is used for matching both [IdentityCenterAccount]s and +// [IdentityCenterAccountAssignment]s, the permission set is `nil` when matching +// an Account. +type IdentityCenterAccountMatcher struct { + accountID string +} + +// Match implements Role Matching for Identity Center Account resources. It +// attempts to match the Account Assignments in a Role Condition against a +// known Account ID. +func (m *IdentityCenterAccountMatcher) Match(role types.Role, condition types.RoleConditionType) (bool, error) { + // TODO(tcsc): Expand to cover role template expansion (e.g. {{external.account_assignments}}) + for _, asmt := range role.GetIdentityCenterAccountAssignments(condition) { + accountMatches, err := matchExpression(m.accountID, asmt.Account) + if err != nil { + return false, trace.Wrap(err) + } + + if accountMatches { + return true, nil + } + } + return false, nil +} + +func (m *IdentityCenterAccountMatcher) String() string { + return fmt.Sprintf("IdentityCenterAccountMatcher(account=%v)", m.accountID) +} + +// NewIdentityCenterAccountAssignmentMatcher creates a new [IdentityCenterAccountAssignmentMatcher] +// configured to match the supplied [IdentityCenterAccountAssignment]. +func NewIdentityCenterAccountAssignmentMatcher(account IdentityCenterAccountAssignment) RoleMatcher { + return &IdentityCenterAccountMatcher{ + accountID: account.GetSpec().GetAccountId(), + } +} + +// IdentityCenterMatcher implements a [RoleMatcher] for comparing Identity Center +// resources against the AccountAssignments specified in a Role condition. +// +// The same type is used for matching both [IdentityCenterAccount]s and +// [IdentityCenterAccountAssignment]s, the permission set is `nil` when matching +// an Account. +type IdentityCenterAccountAssignmentMatcher struct { + accountID string + permissionSetARN string +} + +// Match implements Role Matching for Identity Center Account resources. It +// attempts to match the Account Assignments in a Role Condition against a +// known Account ID. +func (m *IdentityCenterAccountAssignmentMatcher) Match(role types.Role, condition types.RoleConditionType) (bool, error) { + // TODO(tcsc): Expand to cover role template expansion (e.g. {{external.account_assignments}}) + for _, asmt := range role.GetIdentityCenterAccountAssignments(condition) { + accountMatches, err := matchExpression(m.accountID, asmt.Account) + if err != nil { + return false, trace.Wrap(err) + } + + if !accountMatches { + continue + } + + permissionSetMatches, err := matchExpression(m.permissionSetARN, asmt.PermissionSet) + if err != nil { + return false, trace.Wrap(err) + } + + if permissionSetMatches { + return true, nil + } + } + return false, nil +} + +func (m *IdentityCenterAccountAssignmentMatcher) String() string { + return fmt.Sprintf("IdentityCenterAccountMatcher(account=%v, permissionSet=%v)", + m.accountID, m.permissionSetARN) +} + +func matchExpression(target, expression string) (bool, error) { + if expression == types.Wildcard { + return true, nil + } + matches, err := utils.MatchString(target, expression) + if err != nil { + return false, trace.Wrap(err) + } + return matches, nil +} diff --git a/lib/services/identitycenter_test.go b/lib/services/identitycenter_test.go index 98de511413325..640e780fd57ab 100644 --- a/lib/services/identitycenter_test.go +++ b/lib/services/identitycenter_test.go @@ -95,3 +95,297 @@ func TestIdentityCenterAccountAssignmentClone(t *testing.T) { require.NotEqual(t, src, dst) require.Equal(t, "original name", dst.Spec.PermissionSet.Name) } + +func TestIdentityCenterAccountMatcher(t *testing.T) { + testCases := []struct { + name string + roleAssignments []types.IdentityCenterAccountAssignment + condition types.RoleConditionType + matcher RoleMatcher + expectMatch require.BoolAssertionFunc + }{ + { + name: "empty nonmatch", + roleAssignments: nil, + condition: types.Allow, + matcher: &IdentityCenterAccountMatcher{ + accountID: "11111111", + }, + expectMatch: require.False, + }, + { + name: "simple account match", + roleAssignments: []types.IdentityCenterAccountAssignment{{ + Account: "11111111", + PermissionSet: "some:arn", + }}, + condition: types.Allow, + matcher: &IdentityCenterAccountMatcher{ + accountID: "11111111", + }, + expectMatch: require.True, + }, + { + name: "multiple account assignments match", + roleAssignments: []types.IdentityCenterAccountAssignment{ + { + Account: "00000000", + PermissionSet: "some:arn", + }, + { + Account: "11111111", + PermissionSet: "some:arn", + }, + }, + condition: types.Allow, + matcher: &IdentityCenterAccountMatcher{ + accountID: "11111111", + }, + expectMatch: require.True, + }, + { + name: "simple account nonmatch", + roleAssignments: []types.IdentityCenterAccountAssignment{{ + Account: "11111111", + PermissionSet: "some:arn", + }}, + condition: types.Allow, + matcher: &IdentityCenterAccountMatcher{ + accountID: "potato", + }, + expectMatch: require.False, + }, + { + name: "multiple account assignments match", + roleAssignments: []types.IdentityCenterAccountAssignment{ + { + Account: "00000000", + PermissionSet: "some:arn", + }, + { + Account: "11111111", + PermissionSet: "some:arn", + }, + }, + condition: types.Allow, + matcher: &IdentityCenterAccountMatcher{ + accountID: "66666666", + }, + expectMatch: require.False, + }, + { + name: "account glob match", + roleAssignments: []types.IdentityCenterAccountAssignment{{ + Account: "*", + PermissionSet: "some:arn", + }}, + condition: types.Allow, + matcher: &IdentityCenterAccountMatcher{ + accountID: "potato", + }, + expectMatch: require.True, + }, + { + name: "account glob nonmatch", + roleAssignments: []types.IdentityCenterAccountAssignment{{ + Account: "*!", + PermissionSet: "some:arn", + }}, + condition: types.Allow, + matcher: &IdentityCenterAccountMatcher{ + accountID: "potato", + }, + expectMatch: require.False, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + roleSpec := types.RoleSpecV6{} + condition := &roleSpec.Deny + if testCase.condition == types.Allow { + condition = &roleSpec.Allow + } + condition.AccountAssignments = append(condition.AccountAssignments, + testCase.roleAssignments...) + + r, err := types.NewRole("test", roleSpec) + require.NoError(t, err) + + match, err := testCase.matcher.Match(r, testCase.condition) + require.NoError(t, err) + + testCase.expectMatch(t, match) + }) + } +} + +func TestIdentityCenterAccountAssignmentMatcher(t *testing.T) { + testCases := []struct { + name string + roleAssignments []types.IdentityCenterAccountAssignment + condition types.RoleConditionType + matcher RoleMatcher + expectMatch require.BoolAssertionFunc + }{ + { + name: "empty nonmatch", + roleAssignments: nil, + condition: types.Allow, + matcher: &IdentityCenterAccountAssignmentMatcher{ + accountID: "11111111", + permissionSetARN: "some:arn", + }, + expectMatch: require.False, + }, + { + name: "simple match", + roleAssignments: []types.IdentityCenterAccountAssignment{{ + Account: "11111111", + PermissionSet: "some:arn", + }}, + condition: types.Allow, + matcher: &IdentityCenterAccountAssignmentMatcher{ + accountID: "11111111", + permissionSetARN: "some:arn", + }, + expectMatch: require.True, + }, + { + name: "multiple match", + roleAssignments: []types.IdentityCenterAccountAssignment{ + { + Account: "00000000", + PermissionSet: "some:arn", + }, + { + Account: "11111111", + PermissionSet: "some:arn", + }, + }, + condition: types.Allow, + matcher: &IdentityCenterAccountAssignmentMatcher{ + accountID: "11111111", + permissionSetARN: "some:arn", + }, + expectMatch: require.True, + }, + { + name: "multiple nonmatch", + roleAssignments: []types.IdentityCenterAccountAssignment{ + { + Account: "00000000", + PermissionSet: "some:arn", + }, + { + Account: "11111111", + PermissionSet: "some:arn", + }, + }, + condition: types.Allow, + matcher: &IdentityCenterAccountAssignmentMatcher{ + accountID: "66666666", + permissionSetARN: "some:other:arn", + }, + expectMatch: require.False, + }, + { + name: "account glob", + roleAssignments: []types.IdentityCenterAccountAssignment{{ + Account: "*1", + PermissionSet: "some:arn", + }}, + condition: types.Allow, + matcher: &IdentityCenterAccountAssignmentMatcher{ + accountID: "11111111", + permissionSetARN: "some:arn", + }, + expectMatch: require.True, + }, + { + name: "account glob nonmatch", + roleAssignments: []types.IdentityCenterAccountAssignment{{ + Account: "*!!!!", + PermissionSet: "some:arn", + }}, + condition: types.Allow, + matcher: &IdentityCenterAccountAssignmentMatcher{ + accountID: "11111111", + permissionSetARN: "some:arn", + }, + expectMatch: require.False, + }, + { + name: "globbed", + roleAssignments: []types.IdentityCenterAccountAssignment{{ + Account: "*", + PermissionSet: "*", + }}, + condition: types.Allow, + matcher: &IdentityCenterAccountAssignmentMatcher{ + accountID: "11111111", + permissionSetARN: "some:arn", + }, + expectMatch: require.True, + }, + { + name: "globbed nonmatch", + roleAssignments: []types.IdentityCenterAccountAssignment{{ + Account: "*", + PermissionSet: ":not:an:arn:*", + }}, + condition: types.Allow, + matcher: &IdentityCenterAccountAssignmentMatcher{ + accountID: "11111111", + permissionSetARN: "some:arn", + }, + expectMatch: require.False, + }, + { + name: "bad account", + roleAssignments: []types.IdentityCenterAccountAssignment{{ + Account: "11111111", + PermissionSet: "some:arn", + }}, + condition: types.Allow, + matcher: &IdentityCenterAccountAssignmentMatcher{ + accountID: "potato", + permissionSetARN: "some:arn", + }, + expectMatch: require.False, + }, + { + name: "bad permissionset arn", + roleAssignments: []types.IdentityCenterAccountAssignment{{ + Account: "11111111", + PermissionSet: "some:arn", + }}, + condition: types.Allow, + matcher: &IdentityCenterAccountAssignmentMatcher{ + accountID: "11111111", + permissionSetARN: "banana", + }, + expectMatch: require.False, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + roleSpec := types.RoleSpecV6{} + condition := &roleSpec.Deny + if testCase.condition == types.Allow { + condition = &roleSpec.Allow + } + condition.AccountAssignments = append(condition.AccountAssignments, + testCase.roleAssignments...) + + r, err := types.NewRole("test", roleSpec) + require.NoError(t, err) + + match, err := testCase.matcher.Match(r, testCase.condition) + require.NoError(t, err) + + testCase.expectMatch(t, match) + }) + } +} diff --git a/lib/services/role.go b/lib/services/role.go index 60891a725e3bd..ad3ea438c2b51 100644 --- a/lib/services/role.go +++ b/lib/services/role.go @@ -2555,6 +2555,19 @@ func rbacDebugLogger() (debugEnabled bool, debugf func(format string, args ...in return } +// resourceRequiresLabelMatching decides if a resource requires label matching +// when making RBAC access decisions. +func resourceRequiresLabelMatching(r AccessCheckable) bool { + // Some resources do not need label matching when assessing whether the user + // should be granted access. Enable it by default, but turn it off in the + // special cases. + switch r.GetKind() { + case types.KindIdentityCenterAccount, types.KindIdentityCenterAccountAssignment: + return false + } + return true +} + func (set RoleSet) checkAccess(r AccessCheckable, traits wrappers.Traits, state AccessState, matchers ...RoleMatcher) error { // Note: logging in this function only happens in debug mode. This is because // adding logging to this function (which is called on every resource returned @@ -2566,6 +2579,7 @@ func (set RoleSet) checkAccess(r AccessCheckable, traits wrappers.Traits, state return ErrSessionMFARequired } + requiresLabelMatching := resourceRequiresLabelMatching(r) namespace := types.ProcessNamespace(r.GetMetadata().Namespace) // Additional message depending on kind of resource @@ -2588,18 +2602,20 @@ func (set RoleSet) checkAccess(r AccessCheckable, traits wrappers.Traits, state if !matchNamespace { continue } - - matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Deny, role, traits, r, isDebugEnabled) - if err != nil { - return trace.Wrap(err) - } - if matchLabels { - debugf("Access to %v %q denied, deny rule in role %q matched; match(namespace=%v, %s)", - r.GetKind(), r.GetName(), role.GetName(), namespaceMessage, labelsMessage) - return trace.AccessDenied("access to %v denied. User does not have permissions. %v", - r.GetKind(), additionalDeniedMessage) + if requiresLabelMatching { + matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Deny, role, traits, r, isDebugEnabled) + if err != nil { + return trace.Wrap(err) + } + if matchLabels { + debugf("Access to %v %q denied, deny rule in role %q matched; match(namespace=%v, %s)", + r.GetKind(), r.GetName(), role.GetName(), namespaceMessage, labelsMessage) + return trace.AccessDenied("access to %v denied. User does not have permissions. %v", + r.GetKind(), additionalDeniedMessage) + } + } else { + debugf("Role label matching skipped for %v %q", r.GetKind(), r.GetName()) } - // Deny rules are greedy on purpose. They will always match if // at least one of the matchers returns true. matchMatchers, matchersMessage, err := RoleMatchers(matchers).MatchAny(role, types.Deny) @@ -2633,17 +2649,21 @@ func (set RoleSet) checkAccess(r AccessCheckable, traits wrappers.Traits, state continue } - matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Allow, role, traits, r, isDebugEnabled) - if err != nil { - return trace.Wrap(err) - } + if requiresLabelMatching { + matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Allow, role, traits, r, isDebugEnabled) + if err != nil { + return trace.Wrap(err) + } - if !matchLabels { - if isDebugEnabled { - errs = append(errs, trace.AccessDenied("role=%v, match(%s)", - role.GetName(), labelsMessage)) + if !matchLabels { + if isDebugEnabled { + errs = append(errs, trace.AccessDenied("role=%v, match(%s)", + role.GetName(), labelsMessage)) + } + continue } - continue + } else { + debugf("Role label matching skipped for %v %q", r.GetKind(), r.GetName()) } // Allow rules are not greedy. They will match only if all of the