diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index 317b124dd6b7f..aec1f116a440d 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -1469,12 +1469,10 @@ func (a *ServerWithRoles) ListUnifiedResources(ctx context.Context, req *proto.L Kinds: req.Kinds, } - // resourceAccessMap is a map of resourceKind to error, that holds any errors returned when checking verbs - // for that resource (list/read) - resourceAccessMap := make(map[string]error) - - // we want to populate the resourceAccessMap at the start of the request for all available kind in the - // unified resource cache + // Populate resourceAccessMap with any access errors the user has for each possible + // resource kind. This allows the access check to occur a single time per resource + // kind instead of once per matching resource. + resourceAccessMap := make(map[string]error, len(services.UnifiedResourceKinds)) for _, kind := range services.UnifiedResourceKinds { actionVerbs := []string{types.VerbList, types.VerbRead} if kind == types.KindNode { @@ -1489,6 +1487,24 @@ func (a *ServerWithRoles) ListUnifiedResources(ctx context.Context, req *proto.L resourceAccessMap[kind] = a.action(apidefaults.Namespace, kind, actionVerbs...) } + // Before doing any listing, verify that the user is allowed to list + // at least one of the requested kinds. If no access is permitted, then + // return an access denied error. + requested := req.Kinds + if len(req.Kinds) == 0 { + requested = services.UnifiedResourceKinds + } + var rbacErrors int + for _, kind := range requested { + if err, ok := resourceAccessMap[kind]; ok && err != nil { + rbacErrors++ + } + } + + if rbacErrors == len(requested) { + return nil, trace.AccessDenied("User does not have access to any of the requested kinds: %v", requested) + } + // Apply any requested additional search_as_roles and/or preview_as_roles // for the duration of the search. if req.UseSearchAsRoles || req.UsePreviewAsRoles { diff --git a/lib/auth/auth_with_roles_test.go b/lib/auth/auth_with_roles_test.go index 30e87ef8b8d26..34aabe4276f8f 100644 --- a/lib/auth/auth_with_roles_test.go +++ b/lib/auth/auth_with_roles_test.go @@ -4579,6 +4579,35 @@ func TestListUnifiedResources_MixedAccess(t *testing.T) { r := resource.GetDatabaseServer() require.Equal(t, types.KindDatabaseServer, r.GetKind()) } + + // Update the roles to prevent access to any resource kinds. + role.SetRules(types.Deny, []types.Rule{{Resources: services.UnifiedResourceKinds, Verbs: []string{types.VerbList, types.VerbRead}}}) + err = srv.Auth().UpsertRole(ctx, role) + require.NoError(t, err) + + // ensure updated roles have propagated to auth cache + flushCache(t, srv.Auth()) + + // Get a new client to test with the new roles. + clt, err = srv.NewClient(identity) + require.NoError(t, err) + + // Validate that an error is returned when no kinds are requested. + resp, err = clt.ListUnifiedResources(ctx, &proto.ListUnifiedResourcesRequest{ + Limit: 20, + SortBy: types.SortBy{IsDesc: true, Field: types.ResourceMetadataName}, + }) + require.True(t, trace.IsAccessDenied(err)) + require.Nil(t, resp) + + // Validate that an error is returned when a subset of kinds are requested. + resp, err = clt.ListUnifiedResources(ctx, &proto.ListUnifiedResourcesRequest{ + Limit: 20, + SortBy: types.SortBy{IsDesc: true, Field: types.ResourceMetadataName}, + Kinds: []string{types.KindNode, types.KindDatabaseServer}, + }) + require.True(t, trace.IsAccessDenied(err)) + require.Nil(t, resp) } // TestListUnifiedResources_WithPredicate will return resources that match the