diff --git a/api/http/handler/gitops/workflows/filter.go b/api/http/handler/gitops/workflows/filter.go index 46ccd51eb..a063688bb 100644 --- a/api/http/handler/gitops/workflows/filter.go +++ b/api/http/handler/gitops/workflows/filter.go @@ -9,6 +9,7 @@ import ( "github.com/portainer/portainer/api/internal/authorization" "github.com/portainer/portainer/api/internal/endpointutils" "github.com/portainer/portainer/api/internal/snapshot" + "github.com/portainer/portainer/api/kubernetes/cli" "github.com/portainer/portainer/api/set" "github.com/portainer/portainer/api/slicesx" "github.com/portainer/portainer/api/stacks/stackutils" @@ -46,12 +47,21 @@ func buildEndpointMap(tx dataservices.DataStoreTx, stacks []portainer.Stack) (ma return m, nil } -func filterStacksByAccess(tx dataservices.DataStoreTx, stacks []portainer.Stack, sc *security.RestrictedRequestContext) ([]portainer.Stack, error) { +type endpointAccess struct { + isKubeAdmin bool + nonAdminNamespaces []string +} + +// filterDockerStacksByAccess filters stacks to only those the current user can access. +func filterDockerStacksByAccess(tx dataservices.DataStoreTx, stacks []portainer.Stack, sc *security.RestrictedRequestContext) ([]portainer.Stack, error) { if sc.IsAdmin { return stacks, nil } - stackResourceIDSet := set.ToSet(slicesx.Map(stacks, func(s portainer.Stack) string { + // do not try to check UAC on kube stacks + filtered, dockerStacks := slicesx.Partition(stacks, func(s portainer.Stack) bool { return s.Type == portainer.KubernetesStack }) + + stackResourceIDSet := set.ToSet(slicesx.Map(dockerStacks, func(s portainer.Stack) string { return stackutils.ResourceControlID(s.EndpointID, s.Name) })) @@ -62,8 +72,51 @@ func filterStacksByAccess(tx dataservices.DataStoreTx, stacks []portainer.Stack, return nil, err } - stacks = authorization.DecorateStacks(stacks, resourceControls) + dockerStacks = authorization.DecorateStacks(dockerStacks, resourceControls) userTeamIDs := authorization.TeamIDs(sc.UserMemberships) - return authorization.FilterAuthorizedStacks(stacks, sc.UserID, userTeamIDs), nil + filtered = append(filtered, authorization.FilterAuthorizedStacks(dockerStacks, sc.UserID, userTeamIDs)...) + return filtered, nil +} + +func resolveKubeAccess(k8sFactory *cli.ClientFactory, sc *security.RestrictedRequestContext, ep *portainer.Endpoint) (endpointAccess, error) { + if sc.IsAdmin { + return endpointAccess{isKubeAdmin: true}, nil + } + + pcli, err := k8sFactory.GetPrivilegedKubeClient(ep) + if err != nil { + return endpointAccess{}, fmt.Errorf("unable to get privileged kube client for endpoint %d: %w", ep.ID, err) + } + + teamIDs := make([]int, 0, len(sc.UserMemberships)) + for _, m := range sc.UserMemberships { + teamIDs = append(teamIDs, int(m.TeamID)) + } + + nonAdminNamespaces, err := pcli.GetNonAdminNamespaces(int(sc.UserID), teamIDs, ep.Kubernetes.Configuration.RestrictDefaultNamespace) + if err != nil { + return endpointAccess{}, fmt.Errorf("unable to retrieve non-admin namespaces for endpoint %d: %w", ep.ID, err) + } + + return endpointAccess{isKubeAdmin: false, nonAdminNamespaces: nonAdminNamespaces}, nil +} + +func buildEndpointAccessMap(k8sFactory *cli.ClientFactory, sc *security.RestrictedRequestContext, endpointMap map[portainer.EndpointID]portainer.Endpoint) (map[portainer.EndpointID]endpointAccess, error) { + result := make(map[portainer.EndpointID]endpointAccess, len(endpointMap)) + + for epID, ep := range endpointMap { + if !endpointutils.IsKubernetesEndpoint(&ep) { + continue + } + + access, err := resolveKubeAccess(k8sFactory, sc, &ep) + if err != nil { + return nil, err + } + + result[epID] = access + } + + return result, nil } diff --git a/api/http/handler/gitops/workflows/list.go b/api/http/handler/gitops/workflows/list.go index dc744cbf9..47f84e6c1 100644 --- a/api/http/handler/gitops/workflows/list.go +++ b/api/http/handler/gitops/workflows/list.go @@ -157,7 +157,7 @@ func (h *Handler) fetchWorkflows(ctx context.Context, sc *security.RestrictedReq return err } - stacks, err = filterStacksByAccess(tx, stacks, sc) + stacks, err = filterDockerStacksByAccess(tx, stacks, sc) if err != nil { return err } @@ -176,7 +176,12 @@ func (h *Handler) fetchWorkflows(ctx context.Context, sc *security.RestrictedReq if err != nil { return nil, err } - entries, err = filterK8SStacks(entries, endpointMap, h.k8sFactory, sc.UserID) + accessMap, err := buildEndpointAccessMap(h.k8sFactory, sc, endpointMap) + if err != nil { + return nil, err + } + + entries, err = filterK8SStacks(entries, endpointMap, h.k8sFactory, accessMap) if err != nil { return nil, err } @@ -197,7 +202,7 @@ func shouldPerformEnvLookup(endpoint *portainer.Endpoint) bool { (endpointutils.IsEdgeEndpoint(endpoint) && !endpoint.Edge.AsyncMode)) } -func filterK8SStacks(items []portainer.Stack, endpointMap map[portainer.EndpointID]portainer.Endpoint, k8sFactory *cli.ClientFactory, userId portainer.UserID) ([]portainer.Stack, error) { +func filterK8SStacks(items []portainer.Stack, endpointMap map[portainer.EndpointID]portainer.Endpoint, k8sFactory *cli.ClientFactory, accessMap map[portainer.EndpointID]endpointAccess) ([]portainer.Stack, error) { k8sStacks, result := slicesx.Partition(items, func(s portainer.Stack) bool { return s.Type == portainer.KubernetesStack }) @@ -212,10 +217,15 @@ func filterK8SStacks(items []portainer.Stack, endpointMap map[portainer.Endpoint continue } - kcl, err := k8sFactory.GetPrivilegedUserKubeClient(&ep, userId) + kcl, err := k8sFactory.GetPrivilegedKubeClient(&ep) if err != nil { return nil, err } + + access := accessMap[envID] + kcl.SetIsKubeAdmin(access.isKubeAdmin) + kcl.SetClientNonAdminNamespaces(access.nonAdminNamespaces) + apps, err := kcl.GetApplications("", "") if err != nil { return nil, err