From 1a055b02600001e0b6ed52d58c492fe58e14c3c9 Mon Sep 17 00:00:00 2001 From: Lisa Kim Date: Tue, 15 Oct 2024 23:42:28 -0700 Subject: [PATCH] Check request modes while pruning search as roles --- api/types/role.go | 8 + lib/services/access_request.go | 176 +++++++++++------- lib/services/access_request_test.go | 270 ++++++++++++++++++++++------ 3 files changed, 331 insertions(+), 123 deletions(-) diff --git a/api/types/role.go b/api/types/role.go index 1fd5b1b193af3..eb291ccb8af77 100644 --- a/api/types/role.go +++ b/api/types/role.go @@ -142,6 +142,9 @@ type Role interface { // SetKubeResources configures the Kubernetes Resources for the RoleConditionType. SetKubeResources(rct RoleConditionType, pods []KubernetesResource) + // SetRequestMode sets the access request mode. + SetRequestMode(requestMode *AccessRequestMode) + // GetAccessRequestConditions gets allow/deny conditions for access requests. GetAccessRequestConditions(RoleConditionType) AccessRequestConditions // SetAccessRequestConditions sets allow/deny conditions for access requests. @@ -492,6 +495,11 @@ func (r *RoleV6) SetKubeResources(rct RoleConditionType, pods []KubernetesResour } } +// SetRequestMode sets the access request mode. +func (r *RoleV6) SetRequestMode(requestMode *AccessRequestMode) { + r.Spec.Options.RequestMode = requestMode +} + // GetKubeUsers returns kubernetes users func (r *RoleV6) GetKubeUsers(rct RoleConditionType) []string { if rct == Allow { diff --git a/lib/services/access_request.go b/lib/services/access_request.go index ddb6f3c6f1968..fb3930476e5fc 100644 --- a/lib/services/access_request.go +++ b/lib/services/access_request.go @@ -21,7 +21,6 @@ package services import ( "context" "log/slog" - "maps" "slices" "sort" "strings" @@ -59,7 +58,7 @@ const ( // the access request can be reviewed. Defaults to 1 week. requestTTL = 7 * day - InvalidKubernetesKindAccessRequest = "Not allowed to request Kubernetes resource kind" + InvalidKubernetesKindAccessRequest = `Your Teleport roles "request_mode" option field restricts you from requesting kinds` ) // ValidateAccessRequest validates the AccessRequest and sets default values @@ -273,7 +272,12 @@ func CalculateAccessCapabilities(ctx context.Context, clock clockwork.Clock, clt caps.RequireReason = v.requireReason caps.RequestPrompt = v.prompt caps.AutoRequest = v.autoRequest - caps.RequestMode = &types.AccessRequestMode{KubernetesResources: v.requestMode.kubernetesResources} + + protoRequestModeLookup := make(map[string]*types.RequestModeKubernetesResourceValues) + for searchAsRole, requestModes := range v.kubeRequestModeLookup { + protoRequestModeLookup[searchAsRole] = &types.RequestModeKubernetesResourceValues{Values: requestModes} + } + caps.KubeRequestModeLookup = protoRequestModeLookup return &caps, nil } @@ -1031,12 +1035,15 @@ type RequestValidator struct { getter RequestValidatorGetter userState UserState requireReason bool - requestMode struct { - kubernetesResources []types.RequestModeKubernetesResource - } - autoRequest bool - prompt string - opts struct { + // kubeRequestModeLookup is a map of search_as_role to a list + // of collected request modes found from each static role. + // Used to enforce that the request mode found in the static + // role that defined the search_as_role, is respected. + // An empty map or list means no request modes were specified. + kubeRequestModeLookup map[string][]types.RequestModeKubernetesResource + autoRequest bool + prompt string + opts struct { expandVars bool } Roles struct { @@ -1071,10 +1078,11 @@ func NewRequestValidator(ctx context.Context, clock clockwork.Clock, getter Requ } m := RequestValidator{ - clock: clock, - getter: getter, - userState: uls, - logger: slog.With(teleport.ComponentKey, "request.validator"), + clock: clock, + getter: getter, + userState: uls, + logger: slog.With(teleport.ComponentKey, "request.validator"), + kubeRequestModeLookup: make(map[string][]types.RequestModeKubernetesResource), } for _, opt := range opts { opt(&m) @@ -1264,49 +1272,6 @@ func (m *RequestValidator) Validate(ctx context.Context, req types.AccessRequest return trace.Wrap(err) } } - - // Validate kube request kinds. - // If request mode is defined, then any request for kube_cluster will be rejected. - isResourceRequest := len(req.GetRequestedResourceIDs()) > 0 - restrictKubeRequestKinds := len(m.requestMode.kubernetesResources) > 0 - if isResourceRequest && restrictKubeRequestKinds { - if err := enforceKubernetesRequestModes(req.GetRequestedResourceIDs(), m.requestMode.kubernetesResources); err != nil { - return trace.Wrap(err) - } - } - } - - return nil -} - -func enforceKubernetesRequestModes(requestedResourceIDs []types.ResourceID, requestModeKubeResources []types.RequestModeKubernetesResource) error { - allowedKindsLookup := make(map[string]string, len(types.KubernetesResourcesKinds)) - isWildcard := false - - for _, kr := range requestModeKubeResources { - if kr.Kind == types.Wildcard { - isWildcard = true - break - } - allowedKindsLookup[kr.Kind] = kr.Kind - } - - if isWildcard { - for _, kind := range types.KubernetesResourcesKinds { - allowedKindsLookup[kind] = kind - } - } - - for _, id := range requestedResourceIDs { - if id.Kind == types.KindKubernetesCluster { - return trace.BadParameter("%s %q. Allowed kinds: %v.", InvalidKubernetesKindAccessRequest, types.KindKubernetesCluster, slices.Collect(maps.Keys(allowedKindsLookup))) - } - // Filter for kube resources. - if slices.Contains(types.KubernetesResourcesKinds, id.Kind) { - if _, found := allowedKindsLookup[id.Kind]; !found { - return trace.BadParameter("%s %q. Allowed kinds: %v.", InvalidKubernetesKindAccessRequest, id.Kind, slices.Collect(maps.Keys(allowedKindsLookup))) - } - } } return nil @@ -1546,12 +1511,21 @@ func (m *RequestValidator) push(ctx context.Context, role types.Role) error { m.prompt = role.GetOptions().RequestPrompt } - if role.GetOptions().RequestMode != nil { - m.requestMode.kubernetesResources = append(m.requestMode.kubernetesResources, role.GetOptions().RequestMode.KubernetesResources...) - } - allow, deny := role.GetAccessRequestConditions(types.Allow), role.GetAccessRequestConditions(types.Deny) + // Collect all the request modes for the search as roles found for this role. + if len(allow.SearchAsRoles) > 0 && role.GetOptions().RequestMode != nil { + for _, allowedSearchAsRole := range allow.SearchAsRoles { + kubeRequestModes := role.GetOptions().RequestMode.KubernetesResources + // If for some reason, the same search_as_role name got defined in another static role, + // merge the request modes. + if _, exists := m.kubeRequestModeLookup[allowedSearchAsRole]; exists { + kubeRequestModes = append(kubeRequestModes, m.kubeRequestModeLookup[allowedSearchAsRole]...) + } + m.kubeRequestModeLookup[allowedSearchAsRole] = kubeRequestModes + } + } + m.Roles.DenyRequest, err = appendRoleMatchers(m.Roles.DenyRequest, deny.Roles, deny.ClaimsToRoles, m.userState.GetTraits()) if err != nil { return trace.Wrap(err) @@ -1995,10 +1969,13 @@ func (m *RequestValidator) pruneResourceRequestRoles( necessaryRoles := make(map[string]struct{}) for _, resource := range resources { var ( - rolesForResource []types.Role - resourceMatcher *KubeResourcesMatcher + rolesForResource []types.Role + resourceMatcher *KubeResourcesMatcher + rejectedKubeResourceKinds []string + allowedKubeResourceKinds []string + canRequestKubeCluster bool ) - kubernetesResources, err := getKubeResourcesFromResourceIDs(resourceIDs, resource.GetName()) + kubernetesResources, hasKubeClusterKindRequest, err := getKubeResourcesFromResourceIDs(resourceIDs, resource.GetName()) if err != nil { return nil, trace.Wrap(err) } @@ -2015,8 +1992,37 @@ func (m *RequestValidator) pruneResourceRequestRoles( // unless it allows access to another resource. continue } + + // Check kube subresource request mode restrictions for the current role. + if requestModes, exists := m.kubeRequestModeLookup[role.GetName()]; exists && len(requestModes) > 0 { + rejectedKinds, allowedKinds, allValidResources := kubeResourcesMeetsRequestModes(requestModes, kubernetesResources) + rejectedKubeResourceKinds = append(rejectedKubeResourceKinds, rejectedKinds...) + allowedKubeResourceKinds = append(allowedKubeResourceKinds, allowedKinds...) + if !allValidResources { + // Pruning this role because some kube resources did not meet + // the request_mode settings. + continue + } + } else { + // If this role was not a part of the lookup map, it meant + // no request mode options were found, which means this role + // allows "any kind". + if hasKubeClusterKindRequest { + canRequestKubeCluster = true + } + } + rolesForResource = append(rolesForResource, role) } + + if len(rolesForResource) == 0 && len(rejectedKubeResourceKinds) > 0 { + return nil, trace.BadParameter("%s %v for Kubernetes cluster %q. Allowed kinds: %v", InvalidKubernetesKindAccessRequest, apiutils.Deduplicate(rejectedKubeResourceKinds), resource.GetName(), apiutils.Deduplicate(allowedKubeResourceKinds)) + } + + if hasKubeClusterKindRequest && !canRequestKubeCluster { + return nil, trace.BadParameter("%s %v for Kubernetes cluster %q. Allowed kinds: %v", InvalidKubernetesKindAccessRequest, []string{types.KindKubernetesCluster}, resource.GetName(), apiutils.Deduplicate(allowedKubeResourceKinds)) + } + // 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. @@ -2086,6 +2092,36 @@ func countAllowedLogins(role types.Role) int { return len(allowed) } +// kubeResourcesMeetsRequestModes goes through each request modes, and return true if all the +// kube resources matched with the request modes. It returns false along with what kube resource +// kinds got rejected. +// +// Wildcard among the requestModes will be interpreted as "allow any kube resource kind" and +// will return early and takes precedence over other modes in the list. +func kubeResourcesMeetsRequestModes(requestModes []types.RequestModeKubernetesResource, kubernetesResources []types.KubernetesResource) ([]string, []string, bool) { + allowedRequestKinds := make([]string, 0, len(requestModes)) + for _, requestMode := range requestModes { + if requestMode.Kind == types.Wildcard { + return nil, nil, true + } + allowedRequestKinds = append(allowedRequestKinds, requestMode.Kind) + } + + // Collect all the rejected kinds to display to user if on error. + rejectedKubeResourceKinds := make([]string, 0, len(kubernetesResources)) + for _, kubeResource := range kubernetesResources { + if !slices.Contains(allowedRequestKinds, kubeResource.Kind) { + rejectedKubeResourceKinds = append(rejectedKubeResourceKinds, kubeResource.Kind) + } + } + + if len(rejectedKubeResourceKinds) > 0 { + return apiutils.Deduplicate(rejectedKubeResourceKinds), apiutils.Deduplicate(allowedRequestKinds), false + } + + return nil, nil, true +} + func (m *RequestValidator) roleAllowsResource( ctx context.Context, role types.Role, @@ -2146,11 +2182,17 @@ func (m *RequestValidator) getUnderlyingResourcesByResourceIDs(ctx context.Conte } // getKubeResourcesFromResourceIDs returns the Kubernetes Resources requested for -// the configured cluster. -func getKubeResourcesFromResourceIDs(resourceIDs []types.ResourceID, clusterName string) ([]types.KubernetesResource, error) { +// the configured cluster. Also does a check if among the resourceIDs, there +// exists a Kubernetes cluster request and is returned as a boolean. +func getKubeResourcesFromResourceIDs(resourceIDs []types.ResourceID, clusterName string) ([]types.KubernetesResource, bool, error) { kubernetesResources := make([]types.KubernetesResource, 0, len(resourceIDs)) + hasKubeClusterKindRequest := false for _, resourceID := range resourceIDs { + if resourceID.Kind == types.KindKubernetesCluster && resourceID.Name == clusterName { + hasKubeClusterKindRequest = true + continue + } if slices.Contains(types.KubernetesResourcesKinds, resourceID.Kind) && resourceID.Name == clusterName { switch { case slices.Contains(types.KubernetesClusterWideResourceKinds, resourceID.Kind): @@ -2161,7 +2203,7 @@ func getKubeResourcesFromResourceIDs(resourceIDs []types.ResourceID, clusterName default: splits := strings.Split(resourceID.SubResourceName, "/") if len(splits) != 2 { - return nil, trace.BadParameter("subresource name %q does not follow / format", resourceID.SubResourceName) + return nil, false, trace.BadParameter("subresource name %q does not follow / format", resourceID.SubResourceName) } kubernetesResources = append(kubernetesResources, types.KubernetesResource{ Kind: resourceID.Kind, @@ -2171,7 +2213,7 @@ func getKubeResourcesFromResourceIDs(resourceIDs []types.ResourceID, clusterName } } } - return kubernetesResources, nil + return kubernetesResources, hasKubeClusterKindRequest, nil } func newReviewPermissionParser() (*typical.Parser[reviewPermissionContext, bool], error) { diff --git a/lib/services/access_request_test.go b/lib/services/access_request_test.go index 72f74c2ad42be..5c064bd9053f2 100644 --- a/lib/services/access_request_test.go +++ b/lib/services/access_request_test.go @@ -2556,19 +2556,79 @@ func TestValidate_RequestedPendingTTLAndMaxDuration(t *testing.T) { // TestValidate_WithKubernetesRequestMode tests that requests not meeting // kube request modes are rejected. func TestValidate_WithKubernetesRequestMode(t *testing.T) { + myClusterName := "teleport-cluster" + // set up test roles roleDesc := map[string]types.RoleSpecV6{ - "kube-access": {}, - // Undefined modes allows any subresources and kube_cluster - "requester-base": { + "kube-access-wildcard": { + Allow: types.RoleConditions{ + KubernetesLabels: types.Labels{ + "*": {"*"}, + }, + KubernetesResources: []types.KubernetesResource{ + {Kind: "*", Namespace: "*", Name: "*", Verbs: []string{"*"}}, + }, + }, + }, + "kube-access-namespace": { + Allow: types.RoleConditions{ + KubernetesLabels: types.Labels{ + "*": {"*"}, + }, + KubernetesResources: []types.KubernetesResource{ + {Kind: types.KindNamespace, Namespace: "*", Name: "*", Verbs: []string{"*"}}, + }, + }, + }, + "kube-access-pod": { + Allow: types.RoleConditions{ + KubernetesLabels: types.Labels{ + "*": {"*"}, + }, + KubernetesResources: []types.KubernetesResource{ + {Kind: types.KindKubePod, Namespace: "*", Name: "*", Verbs: []string{"*"}}, + }, + }, + }, + "kube-access-deployment": { + Allow: types.RoleConditions{ + KubernetesLabels: types.Labels{ + "*": {"*"}, + }, + KubernetesResources: []types.KubernetesResource{ + {Kind: types.KindKubeDeployment, Namespace: "*", Name: "*", Verbs: []string{"*"}}, + }, + }, + }, + "db-access-wildcard": { + Allow: types.RoleConditions{ + DatabaseLabels: types.Labels{ + "*": {"*"}, + }, + }, + }, + "request-mode-undefined_search-wildcard": { Allow: types.RoleConditions{ Request: &types.AccessRequestConditions{ - SearchAsRoles: []string{"kube-access"}, + SearchAsRoles: []string{"kube-access-wildcard", "db-access-wildcard"}, + }, + }, + }, + "request-mode-pod_search-as-roles-undefined": { + Options: types.RoleOptions{ + RequestMode: &types.AccessRequestMode{ + KubernetesResources: []types.RequestModeKubernetesResource{ + {Kind: types.KindKubePod}, + }, }, }, }, - // Allows requesting ONLY namespace - "request-mode-namespace": { + "request-mode-namespace_search-namespace": { + Allow: types.RoleConditions{ + Request: &types.AccessRequestConditions{ + SearchAsRoles: []string{"kube-access-namespace", "db-access-wildcard"}, + }, + }, Options: types.RoleOptions{ RequestMode: &types.AccessRequestMode{ KubernetesResources: []types.RequestModeKubernetesResource{ @@ -2578,7 +2638,12 @@ func TestValidate_WithKubernetesRequestMode(t *testing.T) { }, }, // Allows requesting for any subresources, but NOT kube_cluster - "request-mode-wildcard": { + "request-mode-wildcard_search-wildcard": { + Allow: types.RoleConditions{ + Request: &types.AccessRequestConditions{ + SearchAsRoles: []string{"kube-access-wildcard", "db-access-wildcard"}, + }, + }, Options: types.RoleOptions{ RequestMode: &types.AccessRequestMode{ KubernetesResources: []types.RequestModeKubernetesResource{ @@ -2587,8 +2652,27 @@ func TestValidate_WithKubernetesRequestMode(t *testing.T) { }, }, }, - // Allows requesting for kinds pods - "request-mode-pods": { + // Allows wildcard search, but should only accept kube secret + "request-mode-secret_search-wildcard": { + Allow: types.RoleConditions{ + Request: &types.AccessRequestConditions{ + SearchAsRoles: []string{"kube-access-wildcard"}, + }, + }, + Options: types.RoleOptions{ + RequestMode: &types.AccessRequestMode{ + KubernetesResources: []types.RequestModeKubernetesResource{ + {Kind: types.KindKubeSecret}, + }, + }, + }, + }, + "request-mode-pod_search-pods": { + Allow: types.RoleConditions{ + Request: &types.AccessRequestConditions{ + SearchAsRoles: []string{"kube-access-pod"}, + }, + }, Options: types.RoleOptions{ RequestMode: &types.AccessRequestMode{ KubernetesResources: []types.RequestModeKubernetesResource{ @@ -2597,6 +2681,20 @@ func TestValidate_WithKubernetesRequestMode(t *testing.T) { }, }, }, + "request-mode-deployment_search-deployment": { + Allow: types.RoleConditions{ + Request: &types.AccessRequestConditions{ + SearchAsRoles: []string{"kube-access-pod"}, + }, + }, + Options: types.RoleOptions{ + RequestMode: &types.AccessRequestMode{ + KubernetesResources: []types.RequestModeKubernetesResource{ + {Kind: types.KindKubeDeployment}, + }, + }, + }, + }, } roles := make(map[string]types.Role) for name, spec := range roleDesc { @@ -2605,85 +2703,137 @@ func TestValidate_WithKubernetesRequestMode(t *testing.T) { roles[name] = role } + // Define a kube server + kube, err := types.NewKubernetesClusterV3(types.Metadata{ + Name: "kube", + }, + types.KubernetesClusterSpecV3{}, + ) + require.NoError(t, err) + kubeServer, err := types.NewKubernetesServerV3FromCluster(kube, "_", "_") + require.NoError(t, err) + + // Define a db server + db, err := types.NewDatabaseV3(types.Metadata{ + Name: "db", + }, types.DatabaseSpecV3{ + Protocol: "postgres", + URI: "example.com:3000", + }) + require.NoError(t, err) + dbServer, err := types.NewDatabaseServerV3(types.Metadata{ + Name: db.GetName(), + }, types.DatabaseServerSpecV3{ + HostID: "db-server", + Hostname: "db-server", + Database: db, + }) + require.NoError(t, err) + + // start test testCases := []struct { - desc string - currentRoles []string - requestResourceIDs []types.ResourceID - wantErr bool + desc string + userStaticRoles []string + requestResourceIDs []types.ResourceID + wantRequestModeErr bool + wantNoRolesConfiguredErr bool + expectedRequestRoles []string }{ { - desc: "request mode undefined allows all kube kinds", - currentRoles: []string{"requester-base"}, + desc: "request_mode undefined with allow search wildcard, allows all kube kinds", + userStaticRoles: []string{"request-mode-undefined_search-wildcard"}, + expectedRequestRoles: []string{"kube-access-wildcard", "db-access-wildcard"}, + requestResourceIDs: []types.ResourceID{ + {Kind: types.KindKubernetesCluster, ClusterName: myClusterName, Name: "kube"}, + {Kind: types.KindDatabase, ClusterName: myClusterName, Name: "db"}, + {Kind: types.KindKubeNamespace, ClusterName: myClusterName, Name: "kube", SubResourceName: "some-namespace"}, + }, + }, + { + desc: "request_mode does not get applied with a role without search_as_roles defined", + userStaticRoles: []string{"request-mode-undefined_search-wildcard", "request-mode-pod_search-as-roles-undefined"}, + expectedRequestRoles: []string{"kube-access-wildcard", "db-access-wildcard"}, requestResourceIDs: []types.ResourceID{ - {Kind: types.KindKubernetesCluster}, - {Kind: types.KindKubeNamespace}, + {Kind: types.KindKubernetesCluster, ClusterName: myClusterName, Name: "kube"}, + {Kind: types.KindDatabase, ClusterName: myClusterName, Name: "db"}, + {Kind: types.KindKubeNamespace, ClusterName: myClusterName, Name: "kube", SubResourceName: "some-namespace"}, + {Kind: types.KindKubeSecret, ClusterName: myClusterName, Name: "kube", SubResourceName: "namespace/secret-name"}, }, }, { - desc: "request mode wildcard allows any kube subresources", - currentRoles: []string{"requester-base", "request-mode-wildcard"}, + desc: "request_mode wildcard allows any kube subresources", + userStaticRoles: []string{"request-mode-wildcard_search-wildcard"}, + expectedRequestRoles: []string{"kube-access-wildcard", "db-access-wildcard"}, requestResourceIDs: []types.ResourceID{ - {Kind: types.KindKubeNamespace}, - {Kind: types.KindKubeSecret}, + {Kind: types.KindKubeNamespace, ClusterName: myClusterName, Name: "kube", SubResourceName: "some-namespace"}, + {Kind: types.KindDatabase, ClusterName: myClusterName, Name: "db"}, + {Kind: types.KindKubeSecret, ClusterName: myClusterName, Name: "kube", SubResourceName: "namespace/secret-name"}, }, }, { - desc: "request mode wildcard rejects kube_cluster kind", - wantErr: true, - currentRoles: []string{"requester-base", "request-mode-wildcard"}, + desc: "request_mode wildcard rejects kube_cluster kind with other valid kinds", + userStaticRoles: []string{"request-mode-wildcard_search-wildcard"}, requestResourceIDs: []types.ResourceID{ - {Kind: types.KindKubeNamespace}, - {Kind: types.KindKubernetesCluster}, + {Kind: types.KindKubeNamespace, ClusterName: myClusterName, Name: "kube", SubResourceName: "some-namespace"}, + {Kind: types.KindDatabase, ClusterName: myClusterName, Name: "db"}, + {Kind: types.KindKubernetesCluster, ClusterName: myClusterName, Name: "kube"}, }, + wantRequestModeErr: true, }, { - desc: "request mode namespace allows namespace kind", - currentRoles: []string{"requester-base", "request-mode-namespace"}, + desc: "reject single kube_cluster request if request mode is defined", + userStaticRoles: []string{"request-mode-wildcard_search-wildcard"}, requestResourceIDs: []types.ResourceID{ - {Kind: types.KindKubeNamespace}, + {Kind: types.KindKubernetesCluster, ClusterName: myClusterName, Name: "kube"}, }, + wantRequestModeErr: true, }, { - desc: "request mode namespace rejects non namespace kind", - wantErr: true, - currentRoles: []string{"requester-base", "request-mode-namespace"}, + desc: "error with `no roles configured` if search_as_roles does not grant kube resource access", + userStaticRoles: []string{"request-mode-namespace_search-namespace"}, + wantNoRolesConfiguredErr: true, requestResourceIDs: []types.ResourceID{ - {Kind: types.KindKubePod}, + {Kind: types.KindDatabase, ClusterName: myClusterName, Name: "db"}, + {Kind: types.KindKubeSecret, ClusterName: myClusterName, Name: "kube", SubResourceName: "namespace/secret-name"}, }, }, { - desc: "with a kube request mode, allows non kube resource", - currentRoles: []string{"requester-base", "request-mode-namespace"}, + desc: "grant all applicable search_as_roles", + userStaticRoles: []string{"request-mode-undefined_search-wildcard", "request-mode-namespace_search-namespace"}, + expectedRequestRoles: []string{"kube-access-wildcard", "kube-access-namespace", "db-access-wildcard"}, requestResourceIDs: []types.ResourceID{ - {Kind: types.KindNode}, - {Kind: types.KindKubeNamespace}, - {Kind: types.KindDatabase}, + {Kind: types.KindKubernetesCluster, ClusterName: myClusterName, Name: "kube"}, + {Kind: types.KindDatabase, ClusterName: myClusterName, Name: "db"}, + {Kind: types.KindKubeNamespace, ClusterName: myClusterName, Name: "kube", SubResourceName: "some-namespace"}, }, }, { - desc: "request mode namespace and pod, allows both", - currentRoles: []string{"requester-base", "request-mode-namespace", "request-mode-pods"}, + desc: "prune search_as_roles that does not meet request mode", + userStaticRoles: []string{ + "request-mode-pod_search-pods", + "request-mode-namespace_search-namespace", + "request-mode-deployment_search-deployment", + }, + expectedRequestRoles: []string{"kube-access-namespace", "db-access-wildcard"}, requestResourceIDs: []types.ResourceID{ - {Kind: types.KindKubePod}, - {Kind: types.KindKubeNamespace}, + {Kind: types.KindDatabase, ClusterName: myClusterName, Name: "db"}, + {Kind: types.KindKubeNamespace, ClusterName: myClusterName, Name: "kube", SubResourceName: "some-namespace"}, }, }, { - desc: "request mode namespace and pod, rejects non namespace and pod", - wantErr: true, - currentRoles: []string{"requester-base", "request-mode-namespace", "request-mode-pods"}, + desc: "request_mode secret allows secret request", + userStaticRoles: []string{"request-mode-secret_search-wildcard"}, + expectedRequestRoles: []string{"kube-access-wildcard"}, requestResourceIDs: []types.ResourceID{ - {Kind: types.KindKubePod}, - {Kind: types.KindKubeSecret}, - {Kind: types.KindKubeNamespace}, + {Kind: types.KindKubeSecret, ClusterName: myClusterName, Name: "kube", SubResourceName: "namespace/secret-name"}, }, }, { - desc: "request mode wildcard takes precedence over other request mode kinds", - currentRoles: []string{"requester-base", "request-mode-namespace", "request-mode-wildcard"}, + desc: "reject request not meeting request_mode even though search_as_roles allows wildcard", + userStaticRoles: []string{"request-mode-secret_search-wildcard"}, + wantRequestModeErr: true, requestResourceIDs: []types.ResourceID{ - {Kind: types.KindKubePod}, - {Kind: types.KindKubeSecret}, + {Kind: types.KindKubeNamespace, ClusterName: myClusterName, Name: "kube", SubResourceName: "some-namespace"}, }, }, } @@ -2692,7 +2842,7 @@ func TestValidate_WithKubernetesRequestMode(t *testing.T) { uls, err := userloginstate.New(header.Metadata{ Name: "test-user", }, userloginstate.Spec{ - Roles: tc.currentRoles, + Roles: tc.userStaticRoles, }) require.NoError(t, err) userStates := map[string]*userloginstate.UserLoginState{ @@ -2702,11 +2852,15 @@ func TestValidate_WithKubernetesRequestMode(t *testing.T) { g := &mockGetter{ roles: roles, userStates: userStates, - clusterName: "my-cluster", + clusterName: myClusterName, + kubeServers: make(map[string]types.KubeServer), + dbServers: make(map[string]types.DatabaseServer), } + g.kubeServers[kube.GetName()] = kubeServer + g.dbServers[dbServer.GetName()] = dbServer req, err := types.NewAccessRequestWithResources( - "some-id", uls.GetName(), []string{"kube-access"}, tc.requestResourceIDs) + "some-id", uls.GetName(), []string{}, tc.requestResourceIDs) require.NoError(t, err) clock := clockwork.NewFakeClock() @@ -2718,11 +2872,15 @@ func TestValidate_WithKubernetesRequestMode(t *testing.T) { require.NoError(t, err) err = validator.Validate(context.Background(), req, identity) - if tc.wantErr { + if tc.wantRequestModeErr { require.Error(t, err) require.Contains(t, err.Error(), InvalidKubernetesKindAccessRequest) + } else if tc.wantNoRolesConfiguredErr { + require.Error(t, err) + require.Contains(t, err.Error(), `no roles configured in the "search_as_roles"`) } else { require.NoError(t, err) + require.ElementsMatch(t, tc.expectedRequestRoles, req.GetRoles()) } }) }