diff --git a/api/http/handler/docker/utils/get_stacks.go b/api/http/handler/docker/utils/get_stacks.go index fd924fb68..e44a8ff4c 100644 --- a/api/http/handler/docker/utils/get_stacks.go +++ b/api/http/handler/docker/utils/get_stacks.go @@ -19,6 +19,7 @@ type StackViewModel struct { Name string IsExternal bool Type portainer.StackType + Labels map[string]string } // GetDockerStacks retrieves all the stacks associated to a specific environment filtered by the user's access. @@ -56,6 +57,7 @@ func GetDockerStacks(tx dataservices.DataStoreTx, securityContext *security.Rest Name: name, IsExternal: true, Type: portainer.DockerComposeStack, + Labels: container.Labels, } } } @@ -68,6 +70,7 @@ func GetDockerStacks(tx dataservices.DataStoreTx, securityContext *security.Rest Name: name, IsExternal: true, Type: portainer.DockerSwarmStack, + Labels: service.Spec.Labels, } } } @@ -79,7 +82,10 @@ func GetDockerStacks(tx dataservices.DataStoreTx, securityContext *security.Rest return uac.FilterByResourceControl(stacksList, user, securityContext.UserMemberships, func(item StackViewModel) (*portainer.ResourceControl, error) { - return uac.StackResourceControlGetter(tx, environmentID)(*item.InternalStack) + if item.InternalStack != nil { + return uac.StackResourceControlGetter(tx, environmentID)(*item.InternalStack) + } + return uac.ExternalStackResourceControlGetter(tx, environmentID)(uac.ExternalStack{Labels: item.Labels}) }, ) } diff --git a/api/http/handler/docker/utils/get_stacks_test.go b/api/http/handler/docker/utils/get_stacks_test.go index e6ca1e059..e6ab5f6cc 100644 --- a/api/http/handler/docker/utils/get_stacks_test.go +++ b/api/http/handler/docker/utils/get_stacks_test.go @@ -8,7 +8,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/datastore" - dockerconsts "github.com/portainer/portainer/api/docker/consts" + "github.com/portainer/portainer/api/docker/consts" "github.com/portainer/portainer/api/http/security" "github.com/stretchr/testify/assert" @@ -28,12 +28,13 @@ func TestHandler_getDockerStacks(t *testing.T) { containers := []types.Container{ { Labels: map[string]string{ - dockerconsts.ComposeStackNameLabel: "stack1", + consts.ComposeStackNameLabel: "stack1", }, }, { Labels: map[string]string{ - dockerconsts.ComposeStackNameLabel: "stack2", + consts.ComposeStackNameLabel: "stack2", + "io.portainer.accesscontrol.public": "true", }, }, } @@ -43,7 +44,7 @@ func TestHandler_getDockerStacks(t *testing.T) { Spec: swarm.ServiceSpec{ Annotations: swarm.Annotations{ Labels: map[string]string{ - dockerconsts.SwarmStackNameLabel: "stack3", + consts.SwarmStackNameLabel: "stack3", }, }, }, @@ -65,14 +66,16 @@ func TestHandler_getDockerStacks(t *testing.T) { is.NoError(tx.Stack().Create(&stack1)) is.NoError(tx.Stack().Create(&portainer.Stack{ ID: 2, - Name: "stack2", + Name: "stack2", // stack 2 on env 2 EndpointID: 2, Type: portainer.DockerSwarmStack, })) is.NoError(tx.User().Create(&portainer.User{ID: 1, Role: portainer.AdministratorRole})) + is.NoError(tx.User().Create(&portainer.User{ID: 2, Role: portainer.StandardUserRole})) return nil })) + // testing admin user is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error { stacksList, err := GetDockerStacks(tx, &security.RestrictedRequestContext{ IsAdmin: true, @@ -93,11 +96,43 @@ func TestHandler_getDockerStacks(t *testing.T) { Name: "stack2", IsExternal: true, Type: portainer.DockerComposeStack, + Labels: map[string]string{ + consts.ComposeStackNameLabel: "stack2", + "io.portainer.accesscontrol.public": "true", + }, }, { Name: "stack3", IsExternal: true, Type: portainer.DockerSwarmStack, + Labels: map[string]string{ + consts.SwarmStackNameLabel: "stack3", + }, + }, + } + + assert.ElementsMatch(t, expectedStacks, stacksList) + return nil + })) + + // testing standard user + is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error { + stacksList, err := GetDockerStacks(tx, &security.RestrictedRequestContext{ + IsAdmin: false, + UserID: 2, + }, environment.ID, containers, services) + require.NoError(t, err) + assert.Len(t, stacksList, 1) + + expectedStacks := []StackViewModel{ + { + Name: "stack2", + IsExternal: true, + Type: portainer.DockerComposeStack, + Labels: map[string]string{ + consts.ComposeStackNameLabel: "stack2", + "io.portainer.accesscontrol.public": "true", + }, }, } diff --git a/api/uac/stacks.go b/api/uac/stacks.go index 956db612a..d9c34f67d 100644 --- a/api/uac/stacks.go +++ b/api/uac/stacks.go @@ -25,3 +25,24 @@ func StackResourceControlGetter[ func StackResourceControlID(endpointID portainer.EndpointID, name string) string { return stackutils.ResourceControlID(endpointID, name) } + +type ExternalStack struct { + Labels map[string]string +} + +// External stacks are indirectly detected either via containers or services labels +// Any UAC applied to them can only be fetched from containers'/services' labels +func ExternalStackResourceControlGetter[ + TX txLike[RCS, TS, US], + RCS rcServiceLike, + TS teamServiceLike, + US userServiceLike, +]( + tx TX, + endpointID portainer.EndpointID) func(item ExternalStack) (*portainer.ResourceControl, error) { + return genericResourcControlGetter(tx, endpointID, ResourceContext[ExternalStack]{ + RCType: portainer.StackResourceControl, + IDGetter: func(s ExternalStack) string { return "0" }, + LabelsGetter: func(es ExternalStack) map[string]string { return es.Labels }, + }) +}