Compare commits

...

1 Commits

Author SHA1 Message Date
portainer-bot[bot]
0b8d0db0be fix(api/workflows): kubernetes UAC (#2507)
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2026-04-29 22:48:43 +00:00
2 changed files with 71 additions and 8 deletions

View File

@@ -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
}

View File

@@ -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