From 62f4d47ee5ac2de9c7778314f4993a9f28641da9 Mon Sep 17 00:00:00 2001 From: andres-portainer <91705312+andres-portainer@users.noreply.github.com> Date: Mon, 11 May 2026 10:44:09 -0300 Subject: [PATCH] chore(internal): export endpoints and authorizations so they can be shared between CE and EE BE-12893 (#2464) --- api/internal/authorization/access_control.go | 158 ++----------------- api/internal/endpointutils/endpointutils.go | 57 ++----- pkg/authorization/access_control.go | 152 ++++++++++++++++++ pkg/endpoints/utils.go | 42 +++++ 4 files changed, 215 insertions(+), 194 deletions(-) create mode 100644 pkg/authorization/access_control.go diff --git a/api/internal/authorization/access_control.go b/api/internal/authorization/access_control.go index d3e8e15b72..0ff3a2ead4 100644 --- a/api/internal/authorization/access_control.go +++ b/api/internal/authorization/access_control.go @@ -1,122 +1,32 @@ package authorization import ( - "slices" "strconv" portainer "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/slicesx" "github.com/portainer/portainer/api/stacks/stackutils" + "github.com/portainer/portainer/pkg/authorization" ) -// NewAdministratorsOnlyResourceControl will create a new administrators only resource control associated to the resource specified by the -// identifier and type parameters. -func NewAdministratorsOnlyResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType) *portainer.ResourceControl { - return &portainer.ResourceControl{ - Type: resourceType, - ResourceID: resourceIdentifier, - SubResourceIDs: []string{}, - UserAccesses: []portainer.UserResourceAccess{}, - TeamAccesses: []portainer.TeamResourceAccess{}, - AdministratorsOnly: true, - Public: false, - System: false, - } -} - -// NewPrivateResourceControl will create a new private resource control associated to the resource specified by the -// identifier and type parameters. It automatically assigns it to the user specified by the userID parameter. -func NewPrivateResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userID portainer.UserID) *portainer.ResourceControl { - return &portainer.ResourceControl{ - Type: resourceType, - ResourceID: resourceIdentifier, - SubResourceIDs: []string{}, - UserAccesses: []portainer.UserResourceAccess{ - { - UserID: userID, - AccessLevel: portainer.ReadWriteAccessLevel, - }, - }, - TeamAccesses: []portainer.TeamResourceAccess{}, - AdministratorsOnly: false, - Public: false, - System: false, - } -} - -// NewSystemResourceControl will create a new public resource control with the System flag set to true. -// These kind of resource control are not persisted and are created on the fly by the Portainer API. -func NewSystemResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType) *portainer.ResourceControl { - return &portainer.ResourceControl{ - Type: resourceType, - ResourceID: resourceIdentifier, - SubResourceIDs: []string{}, - UserAccesses: []portainer.UserResourceAccess{}, - TeamAccesses: []portainer.TeamResourceAccess{}, - AdministratorsOnly: false, - Public: true, - System: true, - } -} - -// NewPublicResourceControl will create a new public resource control. -func NewPublicResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType) *portainer.ResourceControl { - return &portainer.ResourceControl{ - Type: resourceType, - ResourceID: resourceIdentifier, - SubResourceIDs: []string{}, - UserAccesses: []portainer.UserResourceAccess{}, - TeamAccesses: []portainer.TeamResourceAccess{}, - AdministratorsOnly: false, - Public: true, - System: false, - } -} +var ( + NewAdministratorsOnlyResourceControl = authorization.NewAdministratorsOnlyResourceControl + NewPrivateResourceControl = authorization.NewPrivateResourceControl + NewSystemResourceControl = authorization.NewSystemResourceControl + NewPublicResourceControl = authorization.NewPublicResourceControl + NewRestrictedResourceControl = authorization.NewRestrictedResourceControl + UserCanAccessResource = authorization.UserCanAccessResource + GetResourceControlByResourceIDAndType = authorization.GetResourceControlByResourceIDAndType + TeamIDs = authorization.TeamIDs +) func NewEmptyRestrictedResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType) *portainer.ResourceControl { return NewRestrictedResourceControl(resourceIdentifier, resourceType, []portainer.UserID{}, []portainer.TeamID{}) } -// NewRestrictedResourceControl will create a new resource control with user and team accesses restrictions. -func NewRestrictedResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userIDs []portainer.UserID, teamIDs []portainer.TeamID) *portainer.ResourceControl { - userAccesses := make([]portainer.UserResourceAccess, 0) - teamAccesses := make([]portainer.TeamResourceAccess, 0) - - for _, id := range userIDs { - access := portainer.UserResourceAccess{ - UserID: id, - AccessLevel: portainer.ReadWriteAccessLevel, - } - - userAccesses = append(userAccesses, access) - } - - for _, id := range teamIDs { - access := portainer.TeamResourceAccess{ - TeamID: id, - AccessLevel: portainer.ReadWriteAccessLevel, - } - - teamAccesses = append(teamAccesses, access) - } - - return &portainer.ResourceControl{ - Type: resourceType, - ResourceID: resourceIdentifier, - SubResourceIDs: []string{}, - UserAccesses: userAccesses, - TeamAccesses: teamAccesses, - AdministratorsOnly: false, - Public: false, - System: false, - } -} - // DecorateStacks will iterate through a list of stacks, check for an associated resource control for each // stack and decorate the stack element if a resource control is found. func DecorateStacks(stacks []portainer.Stack, resourceControls []portainer.ResourceControl) []portainer.Stack { for idx, stack := range stacks { - resourceControl := GetResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl, resourceControls) if resourceControl != nil { stacks[idx].ResourceControl = resourceControl @@ -130,7 +40,6 @@ func DecorateStacks(stacks []portainer.Stack, resourceControls []portainer.Resou // template and decorate the template element if a resource control is found. func DecorateCustomTemplates(templates []portainer.CustomTemplate, resourceControls []portainer.ResourceControl) []portainer.CustomTemplate { for idx, template := range templates { - resourceControl := GetResourceControlByResourceIDAndType(strconv.Itoa(int(template.ID)), portainer.CustomTemplateResourceControl, resourceControls) if resourceControl != nil { templates[idx].ResourceControl = resourceControl @@ -165,48 +74,3 @@ func FilterAuthorizedCustomTemplates(customTemplates []portainer.CustomTemplate, return authorizedTemplates } - -// UserCanAccessResource will valid that a user has permissions defined in the specified resource control -// based on its identifier and the team(s) he is part of. -func UserCanAccessResource(userID portainer.UserID, userTeamIDs []portainer.TeamID, resourceControl *portainer.ResourceControl) bool { - if resourceControl == nil { - return false - } - - for _, authorizedUserAccess := range resourceControl.UserAccesses { - if userID == authorizedUserAccess.UserID { - return true - } - } - - for _, authorizedTeamAccess := range resourceControl.TeamAccesses { - if slices.Contains(userTeamIDs, authorizedTeamAccess.TeamID) { - return true - } - } - - return resourceControl.Public -} - -// GetResourceControlByResourceIDAndType retrieves the first matching resource control in a set of resource controls -// based on the specified id and resource type parameters. -func GetResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType, resourceControls []portainer.ResourceControl) *portainer.ResourceControl { - for i := range resourceControls { - if resourceID == resourceControls[i].ResourceID && resourceType == resourceControls[i].Type { - return &resourceControls[i] - } - - if slices.Contains(resourceControls[i].SubResourceIDs, resourceID) { - return &resourceControls[i] - } - } - - return nil -} - -// TeamIDs extracts the TeamID from each membership. -func TeamIDs(memberships []portainer.TeamMembership) []portainer.TeamID { - return slicesx.Map(memberships, func(m portainer.TeamMembership) portainer.TeamID { - return m.TeamID - }) -} diff --git a/api/internal/endpointutils/endpointutils.go b/api/internal/endpointutils/endpointutils.go index 43ec69fcd4..1fa1aa1ae9 100644 --- a/api/internal/endpointutils/endpointutils.go +++ b/api/internal/endpointutils/endpointutils.go @@ -2,50 +2,24 @@ package endpointutils import ( "errors" - "strings" "time" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/kubernetes/cli" + "github.com/portainer/portainer/pkg/endpoints" + log "github.com/rs/zerolog/log" ) -// TODO: this file should be migrated to package/server-ce/pkg/endpoints - -// IsLocalEndpoint returns true if this is a local environment(endpoint) -func IsLocalEndpoint(endpoint *portainer.Endpoint) bool { - return strings.HasPrefix(endpoint.URL, "unix://") || - strings.HasPrefix(endpoint.URL, "npipe://") || - endpoint.Type == portainer.KubernetesLocalEnvironment -} - -// IsKubernetesEndpoint returns true if this is a kubernetes environment(endpoint) -func IsKubernetesEndpoint(endpoint *portainer.Endpoint) bool { - return endpoint.Type == portainer.KubernetesLocalEnvironment || - endpoint.Type == portainer.AgentOnKubernetesEnvironment || - endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment -} - -// IsDockerEndpoint returns true if this is a docker environment(endpoint) -func IsDockerEndpoint(endpoint *portainer.Endpoint) bool { - return endpoint.Type == portainer.DockerEnvironment || - endpoint.Type == portainer.AgentOnDockerEnvironment || - endpoint.Type == portainer.EdgeAgentOnDockerEnvironment -} - -// IsEdgeEndpoint returns true if this is an Edge endpoint -func IsEdgeEndpoint(endpoint *portainer.Endpoint) bool { - return endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment -} - -// IsAgentEndpoint returns true if this is an Agent endpoint -func IsAgentEndpoint(endpoint *portainer.Endpoint) bool { - return endpoint.Type == portainer.AgentOnDockerEnvironment || - endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || - endpoint.Type == portainer.AgentOnKubernetesEnvironment || - endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment -} +var ( + IsLocalEndpoint = endpoints.IsLocalEndpoint + IsKubernetesEndpoint = endpoints.IsKubernetesEndpoint + IsDockerEndpoint = endpoints.IsDockerEndpoint + IsEdgeEndpoint = endpoints.IsEdgeEndpoint + IsAgentEndpoint = endpoints.IsAgentEndpoint + EndpointSet = endpoints.EndpointSet +) // EndpointPlatformType returns the type of the endpoint based on the environment and container engine func EndpointPlatformType(endpoint *portainer.Endpoint) portainer.PlatformType { @@ -85,17 +59,6 @@ func FilterByExcludeIDs(endpoints []portainer.Endpoint, excludeIds []portainer.E return filteredEndpoints } -// EndpointSet receives an environment(endpoint) array and returns a set -func EndpointSet(endpointIDs []portainer.EndpointID) map[portainer.EndpointID]bool { - set := map[portainer.EndpointID]bool{} - - for _, endpointID := range endpointIDs { - set[endpointID] = true - } - - return set -} - func InitialIngressClassDetection(tx dataservices.DataStoreTx, endpoint *portainer.Endpoint, factory *cli.ClientFactory) { if endpoint.Kubernetes.Flags.IsServerIngressClassDetected { return diff --git a/pkg/authorization/access_control.go b/pkg/authorization/access_control.go new file mode 100644 index 0000000000..6c014a1305 --- /dev/null +++ b/pkg/authorization/access_control.go @@ -0,0 +1,152 @@ +package authorization + +import ( + "slices" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/slicesx" +) + +// NewAdministratorsOnlyResourceControl will create a new administrators only resource control associated to the resource specified by the +// identifier and type parameters +func NewAdministratorsOnlyResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType) *portainer.ResourceControl { + return &portainer.ResourceControl{ + Type: resourceType, + ResourceID: resourceIdentifier, + SubResourceIDs: []string{}, + UserAccesses: []portainer.UserResourceAccess{}, + TeamAccesses: []portainer.TeamResourceAccess{}, + AdministratorsOnly: true, + Public: false, + System: false, + } +} + +// NewPrivateResourceControl will create a new private resource control associated to the resource specified by the +// identifier and type parameters. It automatically assigns it to the user specified by the userID parameter. +func NewPrivateResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userID portainer.UserID) *portainer.ResourceControl { + return &portainer.ResourceControl{ + Type: resourceType, + ResourceID: resourceIdentifier, + SubResourceIDs: []string{}, + UserAccesses: []portainer.UserResourceAccess{ + { + UserID: userID, + AccessLevel: portainer.ReadWriteAccessLevel, + }, + }, + TeamAccesses: []portainer.TeamResourceAccess{}, + AdministratorsOnly: false, + Public: false, + System: false, + } +} + +// NewSystemResourceControl creates a new public resource control with the System flag set to true. +// These resource controls are not persisted and are created on the fly by the Portainer API. +func NewSystemResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType) *portainer.ResourceControl { + return &portainer.ResourceControl{ + Type: resourceType, + ResourceID: resourceIdentifier, + SubResourceIDs: []string{}, + UserAccesses: []portainer.UserResourceAccess{}, + TeamAccesses: []portainer.TeamResourceAccess{}, + AdministratorsOnly: false, + Public: true, + System: true, + } +} + +// NewPublicResourceControl creates a new public resource control. +func NewPublicResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType) *portainer.ResourceControl { + return &portainer.ResourceControl{ + Type: resourceType, + ResourceID: resourceIdentifier, + SubResourceIDs: []string{}, + UserAccesses: []portainer.UserResourceAccess{}, + TeamAccesses: []portainer.TeamResourceAccess{}, + AdministratorsOnly: false, + Public: true, + System: false, + } +} + +// NewRestrictedResourceControl creates a new resource control with user and team access restrictions. +func NewRestrictedResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userIDs []portainer.UserID, teamIDs []portainer.TeamID) *portainer.ResourceControl { + userAccesses := make([]portainer.UserResourceAccess, 0) + teamAccesses := make([]portainer.TeamResourceAccess, 0) + + for _, id := range userIDs { + access := portainer.UserResourceAccess{ + UserID: id, + AccessLevel: portainer.ReadWriteAccessLevel, + } + + userAccesses = append(userAccesses, access) + } + + for _, id := range teamIDs { + access := portainer.TeamResourceAccess{ + TeamID: id, + AccessLevel: portainer.ReadWriteAccessLevel, + } + + teamAccesses = append(teamAccesses, access) + } + + return &portainer.ResourceControl{ + Type: resourceType, + ResourceID: resourceIdentifier, + SubResourceIDs: []string{}, + UserAccesses: userAccesses, + TeamAccesses: teamAccesses, + AdministratorsOnly: false, + Public: false, + System: false, + } +} + +// UserCanAccessResource validates that a user has permissions defined in the specified resource control +// based on their identifier and the team(s) they belong to. +func UserCanAccessResource(userID portainer.UserID, userTeamIDs []portainer.TeamID, resourceControl *portainer.ResourceControl) bool { + if resourceControl == nil { + return false + } + + for _, authorizedUserAccess := range resourceControl.UserAccesses { + if userID == authorizedUserAccess.UserID { + return true + } + } + + for _, authorizedTeamAccess := range resourceControl.TeamAccesses { + if slices.Contains(userTeamIDs, authorizedTeamAccess.TeamID) { + return true + } + } + + return resourceControl.Public +} + +// GetResourceControlByResourceIDAndType retrieves the first matching resource control in a set of resource controls +// based on the specified id and resource type parameters. +func GetResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType, resourceControls []portainer.ResourceControl) *portainer.ResourceControl { + for i := range resourceControls { + if resourceID == resourceControls[i].ResourceID && resourceType == resourceControls[i].Type { + return &resourceControls[i] + } + + if slices.Contains(resourceControls[i].SubResourceIDs, resourceID) { + return &resourceControls[i] + } + } + + return nil +} + +// TeamIDs extracts the TeamID from each membership. +func TeamIDs(memberships []portainer.TeamMembership) []portainer.TeamID { + return slicesx.Map(memberships, func(m portainer.TeamMembership) portainer.TeamID { + return m.TeamID + }) +} diff --git a/pkg/endpoints/utils.go b/pkg/endpoints/utils.go index 746b56b2bb..6c9004e4f7 100644 --- a/pkg/endpoints/utils.go +++ b/pkg/endpoints/utils.go @@ -1,11 +1,53 @@ package endpoints import ( + "strings" + portainer "github.com/portainer/portainer/api" "github.com/Masterminds/semver/v3" ) +// IsLocalEndpoint returns true if this is a local environment(endpoint) +func IsLocalEndpoint(endpoint *portainer.Endpoint) bool { + return strings.HasPrefix(endpoint.URL, "unix://") || + strings.HasPrefix(endpoint.URL, "npipe://") || + endpoint.Type == portainer.KubernetesLocalEnvironment +} + +// IsKubernetesEndpoint returns true if this is a kubernetes environment(endpoint) +func IsKubernetesEndpoint(endpoint *portainer.Endpoint) bool { + return endpoint.Type == portainer.KubernetesLocalEnvironment || + endpoint.Type == portainer.AgentOnKubernetesEnvironment || + endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment +} + +// IsDockerEndpoint returns true if this is a docker environment(endpoint) +func IsDockerEndpoint(endpoint *portainer.Endpoint) bool { + return endpoint.Type == portainer.DockerEnvironment || + endpoint.Type == portainer.AgentOnDockerEnvironment || + endpoint.Type == portainer.EdgeAgentOnDockerEnvironment +} + +// IsAgentEndpoint returns true if this is an Agent endpoint +func IsAgentEndpoint(endpoint *portainer.Endpoint) bool { + return endpoint.Type == portainer.AgentOnDockerEnvironment || + endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || + endpoint.Type == portainer.AgentOnKubernetesEnvironment || + endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment +} + +// EndpointSet receives an environment(endpoint) array and returns a set +func EndpointSet(endpointIDs []portainer.EndpointID) map[portainer.EndpointID]bool { + set := map[portainer.EndpointID]bool{} + + for _, endpointID := range endpointIDs { + set[endpointID] = true + } + + return set +} + // IsRegularAgentEndpoint returns true if this is a regular agent endpoint func IsRegularAgentEndpoint(endpoint *portainer.Endpoint) bool { return endpoint.Type == portainer.AgentOnDockerEnvironment ||