feat(policy-RBAC): ensure RBAC policy overrides existing RBAC settings [R8S-777] (#1718)
This commit is contained in:
@@ -202,14 +202,6 @@ func (handler *Handler) updateEndpointGroup(tx dataservices.DataStoreTx, endpoin
|
||||
endpointsChanged = true
|
||||
}
|
||||
}
|
||||
|
||||
// Update user authorizations when endpoints are added/removed from the group
|
||||
// since group membership affects access control
|
||||
if endpointsChanged {
|
||||
if err := handler.AuthorizationService.UpdateUsersAuthorizationsTx(tx); err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to update user authorizations", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reconcile endpoints in the group if tags changed (but endpoints weren't already reconciled)
|
||||
|
||||
@@ -161,12 +161,6 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
|
||||
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint.ID)
|
||||
|
||||
if len(endpoint.UserAccessPolicies) > 0 || len(endpoint.TeamAccessPolicies) > 0 {
|
||||
if err := handler.AuthorizationService.UpdateUsersAuthorizationsTx(tx); err != nil {
|
||||
log.Warn().Err(err).Msg("Unable to update user authorizations")
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.EndpointRelation().DeleteEndpointRelation(endpoint.ID); err != nil {
|
||||
log.Warn().Err(err).Msg("Unable to remove environment relation from the database")
|
||||
}
|
||||
|
||||
@@ -496,7 +496,7 @@ func (service *Service) RemoveTeamAccessPolicies(tx dataservices.DataStoreTx, te
|
||||
}
|
||||
}
|
||||
|
||||
return service.UpdateUsersAuthorizationsTx(tx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveUserAccessPolicies will remove all existing access policies associated to the specified user
|
||||
@@ -569,198 +569,14 @@ func (service *Service) RemoveUserAccessPolicies(tx dataservices.DataStoreTx, us
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUserAuthorizations will update the authorizations for the provided userid
|
||||
func (service *Service) UpdateUserAuthorizations(tx dataservices.DataStoreTx, userID portainer.UserID) error {
|
||||
err := service.updateUserAuthorizations(tx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUsersAuthorizations will trigger an update of the authorizations for all the users.
|
||||
// UpdateUsersAuthorizations is a no-op kept for backward compatibility with database migrations.
|
||||
//
|
||||
// Deprecated: This function previously populated the User.EndpointAuthorizations field which is
|
||||
// no longer used. Authorization is now computed dynamically via ResolveUserEndpointAccess.
|
||||
func (service *Service) UpdateUsersAuthorizations() error {
|
||||
return service.UpdateUsersAuthorizationsTx(service.dataStore)
|
||||
}
|
||||
|
||||
func (service *Service) UpdateUsersAuthorizationsTx(tx dataservices.DataStoreTx) error {
|
||||
users, err := tx.User().ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
err := service.updateUserAuthorizations(tx, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) updateUserAuthorizations(tx dataservices.DataStoreTx, userID portainer.UserID) error {
|
||||
user, err := tx.User().Read(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpointAuthorizations, err := service.getAuthorizations(tx, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.EndpointAuthorizations = endpointAuthorizations
|
||||
|
||||
return tx.User().Update(userID, user)
|
||||
}
|
||||
|
||||
func (service *Service) getAuthorizations(tx dataservices.DataStoreTx, user *portainer.User) (portainer.EndpointAuthorizations, error) {
|
||||
endpointAuthorizations := portainer.EndpointAuthorizations{}
|
||||
if user.Role == portainer.AdministratorRole {
|
||||
return endpointAuthorizations, nil
|
||||
}
|
||||
|
||||
userMemberships, err := tx.TeamMembership().TeamMembershipsByUserID(user.ID)
|
||||
if err != nil {
|
||||
return endpointAuthorizations, err
|
||||
}
|
||||
|
||||
endpoints, err := tx.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return endpointAuthorizations, err
|
||||
}
|
||||
|
||||
endpointGroups, err := tx.EndpointGroup().ReadAll()
|
||||
if err != nil {
|
||||
return endpointAuthorizations, err
|
||||
}
|
||||
|
||||
roles, err := tx.Role().ReadAll()
|
||||
if err != nil {
|
||||
return endpointAuthorizations, err
|
||||
}
|
||||
|
||||
endpointAuthorizations = getUserEndpointAuthorizations(user, endpoints, endpointGroups, roles, userMemberships)
|
||||
|
||||
return endpointAuthorizations, nil
|
||||
}
|
||||
|
||||
func getUserEndpointAuthorizations(user *portainer.User, endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, roles []portainer.Role, userMemberships []portainer.TeamMembership) portainer.EndpointAuthorizations {
|
||||
endpointAuthorizations := make(portainer.EndpointAuthorizations)
|
||||
|
||||
groupUserAccessPolicies := map[portainer.EndpointGroupID]portainer.UserAccessPolicies{}
|
||||
groupTeamAccessPolicies := map[portainer.EndpointGroupID]portainer.TeamAccessPolicies{}
|
||||
for _, endpointGroup := range endpointGroups {
|
||||
groupUserAccessPolicies[endpointGroup.ID] = endpointGroup.UserAccessPolicies
|
||||
groupTeamAccessPolicies[endpointGroup.ID] = endpointGroup.TeamAccessPolicies
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
authorizations := getAuthorizationsFromUserEndpointPolicy(user, &endpoint, roles)
|
||||
if len(authorizations) > 0 {
|
||||
endpointAuthorizations[endpoint.ID] = authorizations
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
authorizations = getAuthorizationsFromUserEndpointGroupPolicy(user, &endpoint, roles, groupUserAccessPolicies)
|
||||
if len(authorizations) > 0 {
|
||||
endpointAuthorizations[endpoint.ID] = authorizations
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
authorizations = getAuthorizationsFromTeamEndpointPolicies(userMemberships, &endpoint, roles)
|
||||
if len(authorizations) > 0 {
|
||||
endpointAuthorizations[endpoint.ID] = authorizations
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
authorizations = getAuthorizationsFromTeamEndpointGroupPolicies(userMemberships, &endpoint, roles, groupTeamAccessPolicies)
|
||||
if len(authorizations) > 0 {
|
||||
endpointAuthorizations[endpoint.ID] = authorizations
|
||||
}
|
||||
}
|
||||
|
||||
return endpointAuthorizations
|
||||
}
|
||||
|
||||
func getAuthorizationsFromUserEndpointPolicy(user *portainer.User, endpoint *portainer.Endpoint, roles []portainer.Role) portainer.Authorizations {
|
||||
policyRoles := make([]portainer.RoleID, 0)
|
||||
|
||||
policy, ok := endpoint.UserAccessPolicies[user.ID]
|
||||
if ok {
|
||||
policyRoles = append(policyRoles, policy.RoleID)
|
||||
}
|
||||
|
||||
return getAuthorizationsFromRoles(policyRoles, roles)
|
||||
}
|
||||
|
||||
func getAuthorizationsFromUserEndpointGroupPolicy(user *portainer.User, endpoint *portainer.Endpoint, roles []portainer.Role, groupAccessPolicies map[portainer.EndpointGroupID]portainer.UserAccessPolicies) portainer.Authorizations {
|
||||
policyRoles := make([]portainer.RoleID, 0)
|
||||
|
||||
policy, ok := groupAccessPolicies[endpoint.GroupID][user.ID]
|
||||
if ok {
|
||||
policyRoles = append(policyRoles, policy.RoleID)
|
||||
}
|
||||
|
||||
return getAuthorizationsFromRoles(policyRoles, roles)
|
||||
}
|
||||
|
||||
func getAuthorizationsFromTeamEndpointPolicies(memberships []portainer.TeamMembership, endpoint *portainer.Endpoint, roles []portainer.Role) portainer.Authorizations {
|
||||
policyRoles := make([]portainer.RoleID, 0)
|
||||
|
||||
for _, membership := range memberships {
|
||||
policy, ok := endpoint.TeamAccessPolicies[membership.TeamID]
|
||||
if ok {
|
||||
policyRoles = append(policyRoles, policy.RoleID)
|
||||
}
|
||||
}
|
||||
|
||||
return getAuthorizationsFromRoles(policyRoles, roles)
|
||||
}
|
||||
|
||||
func getAuthorizationsFromTeamEndpointGroupPolicies(memberships []portainer.TeamMembership, endpoint *portainer.Endpoint, roles []portainer.Role, groupAccessPolicies map[portainer.EndpointGroupID]portainer.TeamAccessPolicies) portainer.Authorizations {
|
||||
policyRoles := make([]portainer.RoleID, 0)
|
||||
|
||||
for _, membership := range memberships {
|
||||
policy, ok := groupAccessPolicies[endpoint.GroupID][membership.TeamID]
|
||||
if ok {
|
||||
policyRoles = append(policyRoles, policy.RoleID)
|
||||
}
|
||||
}
|
||||
|
||||
return getAuthorizationsFromRoles(policyRoles, roles)
|
||||
}
|
||||
|
||||
func getAuthorizationsFromRoles(roleIdentifiers []portainer.RoleID, roles []portainer.Role) portainer.Authorizations {
|
||||
var associatedRoles []portainer.Role
|
||||
|
||||
for _, id := range roleIdentifiers {
|
||||
for _, role := range roles {
|
||||
if role.ID == id {
|
||||
associatedRoles = append(associatedRoles, role)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var authorizations portainer.Authorizations
|
||||
highestPriority := 0
|
||||
for _, role := range associatedRoles {
|
||||
if role.Priority > highestPriority {
|
||||
highestPriority = role.Priority
|
||||
authorizations = role.Authorizations
|
||||
}
|
||||
}
|
||||
|
||||
return authorizations
|
||||
}
|
||||
|
||||
func (service *Service) UserIsAdminOrAuthorized(tx dataservices.DataStoreTx, userID portainer.UserID, endpointID portainer.EndpointID, authorizations []portainer.Authorization) (bool, error) {
|
||||
user, err := tx.User().Read(userID)
|
||||
if err != nil {
|
||||
|
||||
@@ -37,7 +37,10 @@ export function ItemView() {
|
||||
<>
|
||||
<PageHeader
|
||||
title="Team details"
|
||||
breadcrumbs={[{ label: 'Teams' }, { label: team.Name }]}
|
||||
breadcrumbs={[
|
||||
{ label: 'Teams', link: 'portainer.teams' },
|
||||
{ label: team.Name },
|
||||
]}
|
||||
reload
|
||||
/>
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ export function KubernetesSidebar({ environmentId }: Props) {
|
||||
to="kubernetes.moreResources.jobs"
|
||||
pathOptions={{
|
||||
includePaths: [
|
||||
'kubernetes.moreResources.jobs',
|
||||
'kubernetes.moreResources.serviceAccounts',
|
||||
'kubernetes.moreResources.clusterRoles',
|
||||
'kubernetes.moreResources.roles',
|
||||
|
||||
175
pkg/authorization/resolver.go
Normal file
175
pkg/authorization/resolver.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package authorization
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// ResolvedAccess represents the result of dynamic authorization resolution.
|
||||
// It contains both the computed role and its authorizations for convenience.
|
||||
type ResolvedAccess struct {
|
||||
Role *portainer.Role
|
||||
Authorizations portainer.Authorizations
|
||||
}
|
||||
|
||||
// ResolverInput contains all the data needed to resolve user access to an endpoint.
|
||||
// This struct is used to pass data to the resolution functions without requiring
|
||||
// database access, making it easier to test and allowing callers to control data fetching.
|
||||
type ResolverInput struct {
|
||||
User *portainer.User
|
||||
Endpoint *portainer.Endpoint
|
||||
EndpointGroup portainer.EndpointGroup
|
||||
UserMemberships []portainer.TeamMembership
|
||||
Roles []portainer.Role
|
||||
}
|
||||
|
||||
// ComputeBaseRole computes the user's role on an endpoint from base access settings.
|
||||
// It checks access in precedence order:
|
||||
// 1. User → Endpoint direct access
|
||||
// 2. User → Endpoint Group access (inherited)
|
||||
// 3. User's Teams → Endpoint access
|
||||
// 4. User's Teams → Endpoint Group access (inherited)
|
||||
//
|
||||
// Returns the first matching role, or nil if no access is configured.
|
||||
func ComputeBaseRole(input ResolverInput) *portainer.Role {
|
||||
group := input.EndpointGroup
|
||||
|
||||
// 1. Check user → endpoint direct access
|
||||
if role := GetRoleFromUserAccessPolicies(
|
||||
input.User.ID,
|
||||
input.Endpoint.UserAccessPolicies,
|
||||
input.Roles,
|
||||
); role != nil {
|
||||
return role
|
||||
}
|
||||
|
||||
// 2. Check user → endpoint group access (inherited)
|
||||
if role := GetRoleFromUserAccessPolicies(
|
||||
input.User.ID,
|
||||
group.UserAccessPolicies,
|
||||
input.Roles,
|
||||
); role != nil {
|
||||
return role
|
||||
}
|
||||
|
||||
// 3. Check user's teams → endpoint access
|
||||
if role := GetRoleFromTeamAccessPolicies(
|
||||
input.UserMemberships,
|
||||
input.Endpoint.TeamAccessPolicies,
|
||||
input.Roles,
|
||||
); role != nil {
|
||||
return role
|
||||
}
|
||||
|
||||
// 4. Check user's teams → endpoint group access (inherited)
|
||||
if role := GetRoleFromTeamAccessPolicies(
|
||||
input.UserMemberships,
|
||||
group.TeamAccessPolicies,
|
||||
input.Roles,
|
||||
); role != nil {
|
||||
return role
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveUserEndpointAccess resolves a user's effective access to an endpoint.
|
||||
// In CE, this returns the base role computed from endpoint/group access settings.
|
||||
// EE extends this to also consider applied RBAC policies.
|
||||
//
|
||||
// Returns nil if the user has no access to the endpoint.
|
||||
func ResolveUserEndpointAccess(input ResolverInput) *ResolvedAccess {
|
||||
role := ComputeBaseRole(input)
|
||||
if role == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ResolvedAccess{
|
||||
Role: role,
|
||||
Authorizations: role.Authorizations,
|
||||
}
|
||||
}
|
||||
|
||||
// GetRoleFromUserAccessPolicies returns the role for a user from user access policies.
|
||||
// Returns nil if the user is not in the policies.
|
||||
func GetRoleFromUserAccessPolicies(
|
||||
userID portainer.UserID,
|
||||
policies portainer.UserAccessPolicies,
|
||||
roles []portainer.Role,
|
||||
) *portainer.Role {
|
||||
if policies == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
policy, ok := policies[userID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return FindRoleByID(policy.RoleID, roles)
|
||||
}
|
||||
|
||||
// GetRoleFromTeamAccessPolicies returns the highest priority role for a user
|
||||
// based on their team memberships and the team access policies.
|
||||
// If a user belongs to multiple teams with access, the role with highest priority wins.
|
||||
// Returns nil if none of the user's teams have access.
|
||||
func GetRoleFromTeamAccessPolicies(
|
||||
memberships []portainer.TeamMembership,
|
||||
policies portainer.TeamAccessPolicies,
|
||||
roles []portainer.Role,
|
||||
) *portainer.Role {
|
||||
if policies == nil || len(memberships) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect all roles from team memberships
|
||||
var matchingRoles []*portainer.Role
|
||||
for _, membership := range memberships {
|
||||
policy, ok := policies[membership.TeamID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
role := FindRoleByID(policy.RoleID, roles)
|
||||
if role != nil {
|
||||
matchingRoles = append(matchingRoles, role)
|
||||
}
|
||||
}
|
||||
|
||||
if len(matchingRoles) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return the role with highest priority
|
||||
return GetHighestPriorityRole(matchingRoles)
|
||||
}
|
||||
|
||||
// GetHighestPriorityRole returns the role with the highest priority from a slice.
|
||||
// In Portainer's role system, higher priority numbers = higher priority (lower access usually gives higher priority).
|
||||
// Current role priorities from highest to lowest: Read-only User (6), Standard User (5),
|
||||
// Namespace Operator (4), Helpdesk (3), Operator (2), Environment Administrator (1).
|
||||
// Returns nil if the slice is empty.
|
||||
func GetHighestPriorityRole(roles []*portainer.Role) *portainer.Role {
|
||||
if len(roles) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
highest := roles[0]
|
||||
for _, role := range roles[1:] {
|
||||
if role.Priority > highest.Priority {
|
||||
highest = role
|
||||
}
|
||||
}
|
||||
|
||||
return highest
|
||||
}
|
||||
|
||||
// FindRoleByID finds a role by its ID in a slice of roles.
|
||||
// Returns nil if the role is not found.
|
||||
func FindRoleByID(roleID portainer.RoleID, roles []portainer.Role) *portainer.Role {
|
||||
for i := range roles {
|
||||
if roles[i].ID == roleID {
|
||||
return &roles[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
405
pkg/authorization/resolver_test.go
Normal file
405
pkg/authorization/resolver_test.go
Normal file
@@ -0,0 +1,405 @@
|
||||
package authorization
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test role fixtures
|
||||
// In Portainer's role system, higher priority numbers = higher priority (more powerful).
|
||||
// Order from highest to lowest: Read-only (4), Helpdesk (3), Operator (2), Admin (1).
|
||||
var (
|
||||
roleAdmin = portainer.Role{
|
||||
ID: 1,
|
||||
Name: "Environment Administrator",
|
||||
Priority: 1,
|
||||
Authorizations: portainer.Authorizations{"admin": true},
|
||||
}
|
||||
roleOperator = portainer.Role{
|
||||
ID: 2,
|
||||
Name: "Operator",
|
||||
Priority: 2,
|
||||
Authorizations: portainer.Authorizations{"operator": true},
|
||||
}
|
||||
roleHelpdesk = portainer.Role{
|
||||
ID: 3,
|
||||
Name: "Helpdesk",
|
||||
Priority: 3,
|
||||
Authorizations: portainer.Authorizations{"helpdesk": true},
|
||||
}
|
||||
roleReadOnly = portainer.Role{
|
||||
ID: 4,
|
||||
Name: "Read-only",
|
||||
Priority: 4,
|
||||
Authorizations: portainer.Authorizations{"readonly": true},
|
||||
}
|
||||
|
||||
allRoles = []portainer.Role{roleAdmin, roleOperator, roleHelpdesk, roleReadOnly}
|
||||
)
|
||||
|
||||
func TestComputeBaseRole_UserEndpointAccess(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
user := &portainer.User{ID: 1}
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: 1,
|
||||
GroupID: 1,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{
|
||||
1: {RoleID: roleOperator.ID},
|
||||
},
|
||||
}
|
||||
|
||||
input := ResolverInput{
|
||||
User: user,
|
||||
Endpoint: endpoint,
|
||||
EndpointGroup: portainer.EndpointGroup{},
|
||||
UserMemberships: []portainer.TeamMembership{},
|
||||
Roles: allRoles,
|
||||
}
|
||||
|
||||
role := ComputeBaseRole(input)
|
||||
|
||||
is.NotNil(role)
|
||||
is.Equal(roleOperator.ID, role.ID)
|
||||
is.Equal("Operator", role.Name)
|
||||
}
|
||||
|
||||
func TestComputeBaseRole_UserGroupAccess(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
user := &portainer.User{ID: 1}
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: 1,
|
||||
GroupID: 10,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{}, // No direct access
|
||||
}
|
||||
groups := []portainer.EndpointGroup{
|
||||
{
|
||||
ID: 10,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{
|
||||
1: {RoleID: roleHelpdesk.ID}, // User has access via group
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
input := ResolverInput{
|
||||
User: user,
|
||||
Endpoint: endpoint,
|
||||
EndpointGroup: groups[0],
|
||||
UserMemberships: []portainer.TeamMembership{},
|
||||
Roles: allRoles,
|
||||
}
|
||||
|
||||
role := ComputeBaseRole(input)
|
||||
|
||||
is.NotNil(role)
|
||||
is.Equal(roleHelpdesk.ID, role.ID)
|
||||
is.Equal("Helpdesk", role.Name)
|
||||
}
|
||||
|
||||
func TestComputeBaseRole_TeamEndpointAccess(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
user := &portainer.User{ID: 1}
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: 1,
|
||||
GroupID: 1,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{}, // No user access
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{
|
||||
100: {RoleID: roleReadOnly.ID}, // Team 100 has access
|
||||
},
|
||||
}
|
||||
memberships := []portainer.TeamMembership{
|
||||
{UserID: 1, TeamID: 100}, // User is in team 100
|
||||
}
|
||||
|
||||
input := ResolverInput{
|
||||
User: user,
|
||||
Endpoint: endpoint,
|
||||
EndpointGroup: portainer.EndpointGroup{},
|
||||
UserMemberships: memberships,
|
||||
Roles: allRoles,
|
||||
}
|
||||
|
||||
role := ComputeBaseRole(input)
|
||||
|
||||
is.NotNil(role)
|
||||
is.Equal(roleReadOnly.ID, role.ID)
|
||||
}
|
||||
|
||||
func TestComputeBaseRole_TeamGroupAccess(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
user := &portainer.User{ID: 1}
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: 1,
|
||||
GroupID: 10,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{}, // No direct team access
|
||||
}
|
||||
groups := []portainer.EndpointGroup{
|
||||
{
|
||||
ID: 10,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{
|
||||
100: {RoleID: roleOperator.ID}, // Team 100 has group access
|
||||
},
|
||||
},
|
||||
}
|
||||
memberships := []portainer.TeamMembership{
|
||||
{UserID: 1, TeamID: 100},
|
||||
}
|
||||
|
||||
input := ResolverInput{
|
||||
User: user,
|
||||
Endpoint: endpoint,
|
||||
EndpointGroup: groups[0],
|
||||
UserMemberships: memberships,
|
||||
Roles: allRoles,
|
||||
}
|
||||
|
||||
role := ComputeBaseRole(input)
|
||||
|
||||
is.NotNil(role)
|
||||
is.Equal(roleOperator.ID, role.ID)
|
||||
}
|
||||
|
||||
func TestComputeBaseRole_Precedence(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
t.Run("User endpoint access takes precedence over group access", func(t *testing.T) {
|
||||
user := &portainer.User{ID: 1}
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: 1,
|
||||
GroupID: 10,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{
|
||||
1: {RoleID: roleOperator.ID}, // Direct access
|
||||
},
|
||||
}
|
||||
groups := []portainer.EndpointGroup{
|
||||
{
|
||||
ID: 10,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{
|
||||
1: {RoleID: roleAdmin.ID}, // Group access (higher role, but lower precedence)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
input := ResolverInput{
|
||||
User: user,
|
||||
Endpoint: endpoint,
|
||||
EndpointGroup: groups[0],
|
||||
Roles: allRoles,
|
||||
}
|
||||
|
||||
role := ComputeBaseRole(input)
|
||||
is.NotNil(role)
|
||||
is.Equal(roleOperator.ID, role.ID, "Direct endpoint access should take precedence")
|
||||
})
|
||||
|
||||
t.Run("User access takes precedence over team access", func(t *testing.T) {
|
||||
user := &portainer.User{ID: 1}
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: 1,
|
||||
GroupID: 1,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{
|
||||
1: {RoleID: roleHelpdesk.ID},
|
||||
},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{
|
||||
100: {RoleID: roleAdmin.ID}, // Team has higher role
|
||||
},
|
||||
}
|
||||
memberships := []portainer.TeamMembership{
|
||||
{UserID: 1, TeamID: 100},
|
||||
}
|
||||
|
||||
input := ResolverInput{
|
||||
User: user,
|
||||
Endpoint: endpoint,
|
||||
UserMemberships: memberships,
|
||||
Roles: allRoles,
|
||||
}
|
||||
|
||||
role := ComputeBaseRole(input)
|
||||
is.NotNil(role)
|
||||
is.Equal(roleHelpdesk.ID, role.ID, "User access should take precedence over team access")
|
||||
})
|
||||
|
||||
t.Run("Team endpoint access takes precedence over team group access", func(t *testing.T) {
|
||||
user := &portainer.User{ID: 1}
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: 1,
|
||||
GroupID: 10,
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{
|
||||
100: {RoleID: roleReadOnly.ID}, // Direct team endpoint access
|
||||
},
|
||||
}
|
||||
groups := []portainer.EndpointGroup{
|
||||
{
|
||||
ID: 10,
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{
|
||||
100: {RoleID: roleAdmin.ID}, // Team group access (higher role)
|
||||
},
|
||||
},
|
||||
}
|
||||
memberships := []portainer.TeamMembership{
|
||||
{UserID: 1, TeamID: 100},
|
||||
}
|
||||
|
||||
input := ResolverInput{
|
||||
User: user,
|
||||
Endpoint: endpoint,
|
||||
EndpointGroup: groups[0],
|
||||
UserMemberships: memberships,
|
||||
Roles: allRoles,
|
||||
}
|
||||
|
||||
role := ComputeBaseRole(input)
|
||||
is.NotNil(role)
|
||||
is.Equal(roleReadOnly.ID, role.ID, "Team endpoint access should take precedence over team group access")
|
||||
})
|
||||
}
|
||||
|
||||
func TestComputeBaseRole_NoAccess(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
user := &portainer.User{ID: 1}
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: 1,
|
||||
GroupID: 10,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
}
|
||||
groups := []portainer.EndpointGroup{
|
||||
{
|
||||
ID: 10,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
},
|
||||
}
|
||||
|
||||
input := ResolverInput{
|
||||
User: user,
|
||||
Endpoint: endpoint,
|
||||
EndpointGroup: groups[0],
|
||||
UserMemberships: []portainer.TeamMembership{},
|
||||
Roles: allRoles,
|
||||
}
|
||||
|
||||
role := ComputeBaseRole(input)
|
||||
is.Nil(role)
|
||||
}
|
||||
|
||||
func TestComputeBaseRole_MultipleTeams_HighestPriorityWins(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
user := &portainer.User{ID: 1}
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: 1,
|
||||
GroupID: 1,
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{
|
||||
100: {RoleID: roleReadOnly.ID}, // Highest priority (4)
|
||||
200: {RoleID: roleAdmin.ID}, // Lowest priority (1)
|
||||
300: {RoleID: roleOperator.ID}, // Medium priority (2)
|
||||
},
|
||||
}
|
||||
memberships := []portainer.TeamMembership{
|
||||
{UserID: 1, TeamID: 100},
|
||||
{UserID: 1, TeamID: 200},
|
||||
{UserID: 1, TeamID: 300},
|
||||
}
|
||||
|
||||
input := ResolverInput{
|
||||
User: user,
|
||||
Endpoint: endpoint,
|
||||
EndpointGroup: portainer.EndpointGroup{},
|
||||
UserMemberships: memberships,
|
||||
Roles: allRoles,
|
||||
}
|
||||
|
||||
role := ComputeBaseRole(input)
|
||||
|
||||
is.NotNil(role)
|
||||
is.Equal(roleReadOnly.ID, role.ID, "Highest priority role should be selected when user is in multiple teams")
|
||||
}
|
||||
|
||||
func TestResolveUserEndpointAccess(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
t.Run("Returns resolved access with role and authorizations", func(t *testing.T) {
|
||||
user := &portainer.User{ID: 1}
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: 1,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{
|
||||
1: {RoleID: roleOperator.ID},
|
||||
},
|
||||
}
|
||||
|
||||
input := ResolverInput{
|
||||
User: user,
|
||||
Endpoint: endpoint,
|
||||
Roles: allRoles,
|
||||
}
|
||||
|
||||
access := ResolveUserEndpointAccess(input)
|
||||
|
||||
is.NotNil(access)
|
||||
is.Equal(roleOperator.ID, access.Role.ID)
|
||||
is.True(access.Authorizations["operator"])
|
||||
})
|
||||
|
||||
t.Run("Returns nil when no access", func(t *testing.T) {
|
||||
user := &portainer.User{ID: 1}
|
||||
endpoint := &portainer.Endpoint{ID: 1}
|
||||
|
||||
input := ResolverInput{
|
||||
User: user,
|
||||
Endpoint: endpoint,
|
||||
Roles: allRoles,
|
||||
}
|
||||
|
||||
access := ResolveUserEndpointAccess(input)
|
||||
is.Nil(access)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFindRoleByID(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
t.Run("Finds existing role", func(t *testing.T) {
|
||||
role := FindRoleByID(roleOperator.ID, allRoles)
|
||||
is.NotNil(role)
|
||||
is.Equal(roleOperator.ID, role.ID)
|
||||
})
|
||||
|
||||
t.Run("Returns nil for non-existent role", func(t *testing.T) {
|
||||
role := FindRoleByID(999, allRoles)
|
||||
is.Nil(role)
|
||||
})
|
||||
|
||||
t.Run("Returns nil for empty roles slice", func(t *testing.T) {
|
||||
role := FindRoleByID(1, []portainer.Role{})
|
||||
is.Nil(role)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetHighestPriorityRole(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
t.Run("Returns nil for empty slice", func(t *testing.T) {
|
||||
result := GetHighestPriorityRole([]*portainer.Role{})
|
||||
is.Nil(result)
|
||||
})
|
||||
|
||||
t.Run("Returns single role", func(t *testing.T) {
|
||||
result := GetHighestPriorityRole([]*portainer.Role{&roleOperator})
|
||||
is.Equal(roleOperator.ID, result.ID)
|
||||
})
|
||||
|
||||
t.Run("Returns highest priority from multiple roles", func(t *testing.T) {
|
||||
result := GetHighestPriorityRole([]*portainer.Role{&roleReadOnly, &roleAdmin, &roleOperator})
|
||||
is.Equal(roleReadOnly.ID, result.ID)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user