diff --git a/api/accessrequest/access_request.go b/api/accessrequest/access_request.go index 1af10ac4400d3..7b82cfc8f25a6 100644 --- a/api/accessrequest/access_request.go +++ b/api/accessrequest/access_request.go @@ -58,7 +58,7 @@ func GetResourceDetails(ctx context.Context, clusterName string, lister client.L var resourceIDs []types.ResourceID for _, resourceID := range ids { // We're interested in hostname or friendly name details. These apply to - // nodes, app servers, and user groups. + // nodes, app servers, user groups and Identity Center resources. switch resourceID.Kind { case types.KindNode, types.KindApp, types.KindUserGroup, types.KindIdentityCenterAccount: resourceIDs = append(resourceIDs, resourceID) diff --git a/api/types/constants.go b/api/types/constants.go index 369cc46730bf0..61d830633e80c 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -1296,6 +1296,8 @@ var RequestableResourceKinds = []string{ KindKubeCertificateSigningRequest, KindKubeIngress, KindSAMLIdPServiceProvider, + KindIdentityCenterAccount, + KindIdentityCenterAccountAssignment, } // The list below needs to be kept in sync with `kubernetesResourceKindOptions` diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 2f4efa8c6843e..8d721850fb98a 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -5099,47 +5099,11 @@ func (a *Server) CreateAccessRequestV2(ctx context.Context, req types.AccessRequ } // Look for user groups and associated applications to the request. - requestedResourceIDs := req.GetRequestedResourceIDs() - var additionalResources []types.ResourceID - - var userGroups []types.ResourceID - existingApps := map[string]struct{}{} - for _, resource := range requestedResourceIDs { - switch resource.Kind { - case types.KindApp: - existingApps[resource.Name] = struct{}{} - case types.KindUserGroup: - userGroups = append(userGroups, resource) - } - } - - for _, resource := range userGroups { - if resource.Kind != types.KindUserGroup { - continue - } - - userGroup, err := a.GetUserGroup(ctx, resource.Name) - if err != nil { - return nil, trace.Wrap(err) - } - - for _, app := range userGroup.GetApplications() { - // Only add to the request if we haven't already added it. - if _, ok := existingApps[app]; !ok { - additionalResources = append(additionalResources, types.ResourceID{ - ClusterName: resource.ClusterName, - Kind: types.KindApp, - Name: app, - }) - existingApps[app] = struct{}{} - } - } - } - - if len(additionalResources) > 0 { - requestedResourceIDs = append(requestedResourceIDs, additionalResources...) - req.SetRequestedResourceIDs(requestedResourceIDs) + requestedResourceIDs, err := a.appendImplicitlyRequiredResources(ctx, req.GetRequestedResourceIDs()) + if err != nil { + return nil, trace.Wrap(err, "adding additional implicitly required resources") } + req.SetRequestedResourceIDs(requestedResourceIDs) if req.GetDryRun() { _, promotions := a.generateAccessRequestPromotions(ctx, req) @@ -5169,7 +5133,7 @@ func (a *Server) CreateAccessRequestV2(ctx context.Context, req types.AccessRequ } } - err := a.emitter.EmitAuditEvent(a.closeCtx, &apievents.AccessRequestCreate{ + err = a.emitter.EmitAuditEvent(a.closeCtx, &apievents.AccessRequestCreate{ Metadata: apievents.Metadata{ Type: events.AccessRequestCreateEvent, Code: events.AccessRequestCreateCode, @@ -5251,6 +5215,69 @@ func (a *Server) CreateAccessRequestV2(ctx context.Context, req types.AccessRequ return req, nil } +// appendImplicitlyRequiredResources examines the set of requested resources and adds +// any extra resources that are implicitly required by the request. +func (a *Server) appendImplicitlyRequiredResources(ctx context.Context, resources []types.ResourceID) ([]types.ResourceID, error) { + addedApps := utils.NewSet[string]() + var userGroups []types.ResourceID + var accountAssignments []types.ResourceID + + for _, resource := range resources { + switch resource.Kind { + case types.KindApp: + addedApps.Add(resource.Name) + case types.KindUserGroup: + userGroups = append(userGroups, resource) + case types.KindIdentityCenterAccountAssignment: + accountAssignments = append(accountAssignments, resource) + } + } + + for _, resource := range userGroups { + userGroup, err := a.GetUserGroup(ctx, resource.Name) + if err != nil { + return nil, trace.Wrap(err) + } + + for _, app := range userGroup.GetApplications() { + // Only add to the request if we haven't already added it. + if !addedApps.Contains(app) { + resources = append(resources, types.ResourceID{ + ClusterName: resource.ClusterName, + Kind: types.KindApp, + Name: app, + }) + addedApps.Add(app) + } + } + } + + icAccounts := utils.NewSet[string]() + for _, resource := range accountAssignments { + // The UI needs access to the account associated with an Account Assignment + // in order to display the enclosing Account, otherwise the user will not + // be able to see their assigned permission sets. + assignmentID := services.IdentityCenterAccountAssignmentID(resource.Name) + asmt, err := a.Services.IdentityCenter.GetAccountAssignment(ctx, assignmentID) + if err != nil { + return nil, trace.Wrap(err, "fetching identity center account assignment") + } + + if icAccounts.Contains(asmt.GetSpec().GetAccountId()) { + continue + } + + resources = append(resources, types.ResourceID{ + ClusterName: resource.ClusterName, + Kind: types.KindIdentityCenterAccount, + Name: asmt.GetSpec().GetAccountId(), + }) + icAccounts.Add(asmt.GetSpec().GetAccountId()) + } + + return resources, nil +} + // generateAccessRequestPromotions will return potential access list promotions for an access request. On error, this function will log // the error and return whatever it has. The caller is expected to deal with the possibility of a nil promotions object. func (a *Server) generateAccessRequestPromotions(ctx context.Context, req types.AccessRequest) (types.AccessRequest, *types.AccessRequestAllowedPromotions) {