chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:08:39 +08:00
commit a0df89c693
5252 changed files with 523444 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
package uac
import (
"github.com/docker/docker/api/types/swarm"
portainer "github.com/portainer/portainer/api"
)
func ConfigResourceControlGetter[
TX txLike[RCS, TS, US],
RCS rcServiceLike,
TS teamServiceLike,
US userServiceLike,
](
tx TX,
endpointID portainer.EndpointID,
) func(item swarm.Config) (*portainer.ResourceControl, error) {
return genericResourcControlGetter(tx, endpointID, ResourceContext[swarm.Config]{
RCType: portainer.ConfigResourceControl,
IDGetter: ConfigResourceControlID,
LabelsGetter: ConfigLabels,
})
}
func ConfigResourceControlID(item swarm.Config) string {
return item.ID
}
func ConfigLabels(item swarm.Config) map[string]string {
return item.Spec.Labels
}
+71
View File
@@ -0,0 +1,71 @@
package uac
import (
"testing"
"github.com/docker/docker/api/types/swarm"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/docker/consts"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/stretchr/testify/require"
)
func TestConfigResourceControlGetter(t *testing.T) {
t.Parallel()
is := require.New(t)
ok, store := datastore.MustNewTestStore(t, true, false)
is.True(ok)
is.NotNil(store)
envID := portainer.EndpointID(1)
configID := "config"
stackName := "stack"
stackRCID := stackutils.ResourceControlID(envID, stackName)
serviceID := "service"
is.NoError(store.UpdateTx(func(tx dataservices.DataStoreTx) error {
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(configID, portainer.ConfigResourceControl)))
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(stackRCID, portainer.StackResourceControl)))
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(serviceID, portainer.ServiceResourceControl)))
return nil
}))
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
// by direct ID
rc, err := ConfigResourceControlGetter(tx, envID)(swarm.Config{ID: configID})
is.NoError(err)
is.NotNil(rc)
is.Equal(configID, rc.ResourceID)
// by compose stack label
rc, err = ConfigResourceControlGetter(tx, envID)(
swarm.Config{ID: "unknown", Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Labels: map[string]string{consts.ComposeStackNameLabel: stackName}}}},
)
is.NoError(err)
is.NotNil(rc)
is.Equal(stackRCID, rc.ResourceID)
// by swarm stack label
rc, err = ConfigResourceControlGetter(tx, envID)(
swarm.Config{ID: "unknown", Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Labels: map[string]string{consts.SwarmStackNameLabel: stackName}}}},
)
is.NoError(err)
is.NotNil(rc)
is.Equal(stackRCID, rc.ResourceID)
// by service ID
rc, err = ConfigResourceControlGetter(tx, envID)(
swarm.Config{ID: "unknown", Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Labels: map[string]string{consts.SwarmServiceIDLabel: serviceID}}}},
)
is.NoError(err)
is.NotNil(rc)
is.Equal(serviceID, rc.ResourceID)
return nil
}))
}
+4
View File
@@ -0,0 +1,4 @@
package uac
// TODO: implement UAC rules for Container groups
// See usage of portainer.ContainerGroupResourceControl
+30
View File
@@ -0,0 +1,30 @@
package uac
import (
"github.com/docker/docker/api/types/container"
portainer "github.com/portainer/portainer/api"
)
func ContainerResourceControlGetter[
TX txLike[RCS, TS, US],
RCS rcServiceLike,
TS teamServiceLike,
US userServiceLike,
](
tx TX,
endpointID portainer.EndpointID,
) func(item container.Summary) (*portainer.ResourceControl, error) {
return genericResourcControlGetter(tx, endpointID, ResourceContext[container.Summary]{
RCType: portainer.ContainerResourceControl,
IDGetter: ContainerResourceControlID,
LabelsGetter: ContainerLabels,
})
}
func ContainerResourceControlID(item container.Summary) string {
return item.ID
}
func ContainerLabels(item container.Summary) map[string]string {
return item.Labels
}
+71
View File
@@ -0,0 +1,71 @@
package uac
import (
"testing"
"github.com/docker/docker/api/types/container"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/docker/consts"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/stretchr/testify/require"
)
func TestContainerResourceControlGetter(t *testing.T) {
t.Parallel()
is := require.New(t)
ok, store := datastore.MustNewTestStore(t, true, false)
is.True(ok)
is.NotNil(store)
envID := portainer.EndpointID(1)
containerID := "container"
stackName := "stack"
stackRCID := stackutils.ResourceControlID(envID, stackName)
serviceID := "service"
is.NoError(store.UpdateTx(func(tx dataservices.DataStoreTx) error {
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(containerID, portainer.ContainerResourceControl)))
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(stackRCID, portainer.StackResourceControl)))
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(serviceID, portainer.ServiceResourceControl)))
return nil
}))
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
// by direct ID
rc, err := ContainerResourceControlGetter(tx, envID)(container.Summary{ID: containerID})
is.NoError(err)
is.NotNil(rc)
is.Equal(containerID, rc.ResourceID)
// by compose stack label
rc, err = ContainerResourceControlGetter(tx, envID)(
container.Summary{ID: "unknown", Labels: map[string]string{consts.ComposeStackNameLabel: stackName}},
)
is.NoError(err)
is.NotNil(rc)
is.Equal(stackRCID, rc.ResourceID)
// by swarm stack label
rc, err = ContainerResourceControlGetter(tx, envID)(
container.Summary{ID: "unknown", Labels: map[string]string{consts.SwarmStackNameLabel: stackName}},
)
is.NoError(err)
is.NotNil(rc)
is.Equal(stackRCID, rc.ResourceID)
// by service ID
rc, err = ContainerResourceControlGetter(tx, envID)(
container.Summary{ID: "unknown", Labels: map[string]string{consts.SwarmServiceIDLabel: serviceID}},
)
is.NoError(err)
is.NotNil(rc)
is.Equal(serviceID, rc.ResourceID)
return nil
}))
}
+4
View File
@@ -0,0 +1,4 @@
package uac
// TODO: implement UAC rules for Custom templates
// See usage of portainer.CustomTemplateResourceControl
+184
View File
@@ -0,0 +1,184 @@
package uac
import (
"strings"
"github.com/docker/docker/api/types/swarm"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/docker/consts"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/slicesx"
"github.com/rs/zerolog/log"
)
type rcServiceLike interface {
ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error)
}
type teamServiceLike interface {
TeamByName(name string) (*portainer.Team, error)
}
type userServiceLike interface {
UserIDByUsername(name string) (portainer.UserID, error)
}
type txLike[
RCS rcServiceLike,
TS teamServiceLike,
US userServiceLike,
] interface {
ResourceControl() RCS
Team() TS
User() US
}
type ResourceContext[T any] struct {
RCType portainer.ResourceControlType
IDGetter func(T) string
LabelsGetter func(T) map[string]string
}
func genericResourcControlGetter[
T any,
TX txLike[RCS, TS, US],
RCS rcServiceLike,
TS teamServiceLike,
US userServiceLike,
](
tx TX,
endpointID portainer.EndpointID,
context ResourceContext[T],
) func(T) (*portainer.ResourceControl, error) {
return func(item T) (*portainer.ResourceControl, error) {
resourceID := context.IDGetter(item)
resourceType := context.RCType
if rc, err := tx.ResourceControl().ResourceControlByResourceIDAndType(
resourceID, resourceType,
); err != nil && !dataservices.IsErrObjectNotFound(err) {
return nil, err
} else if rc != nil {
return rc, nil
}
if context.LabelsGetter == nil {
return authorization.NewEmptyRestrictedResourceControl(resourceID, resourceType), nil
}
labels := context.LabelsGetter(item)
if labels == nil {
return authorization.NewEmptyRestrictedResourceControl(resourceID, resourceType), nil
}
if serviceId, ok := labels[consts.SwarmServiceIDLabel]; ok {
if rc, err := tx.ResourceControl().ResourceControlByResourceIDAndType(
ServiceResourceControlID(swarm.Service{ID: serviceId}),
portainer.ServiceResourceControl,
); err != nil && !dataservices.IsErrObjectNotFound(err) {
return nil, err
} else if rc != nil {
return rc, nil
}
}
if stackName, ok := labels[consts.SwarmStackNameLabel]; ok {
if rc, err := tx.ResourceControl().ResourceControlByResourceIDAndType(
StackResourceControlID(endpointID, stackName),
portainer.StackResourceControl,
); err != nil && !dataservices.IsErrObjectNotFound(err) {
return nil, err
} else if rc != nil {
return rc, nil
}
}
if stackName, ok := labels[consts.ComposeStackNameLabel]; ok {
if rc, err := tx.ResourceControl().ResourceControlByResourceIDAndType(
StackResourceControlID(endpointID, stackName),
portainer.StackResourceControl,
); err != nil && !dataservices.IsErrObjectNotFound(err) {
return nil, err
} else if rc != nil {
return rc, nil
}
}
return rcFromPortainerLabels(tx, labels, resourceID, resourceType)
}
}
const (
publicRCLabel = "io.portainer.accesscontrol.public"
userRCLabel = "io.portainer.accesscontrol.users"
teamRCLabel = "io.portainer.accesscontrol.teams"
)
// translation of rules from transport.newResourceControlFromPortainerLabels(resourceLabelsObject, resourceIdentifier, resourceType)
func rcFromPortainerLabels[
TX txLike[RCS, TS, US],
RCS rcServiceLike,
TS teamServiceLike,
US userServiceLike,
](
tx TX, labels map[string]string, resourceID string, resourceType portainer.ResourceControlType,
) (*portainer.ResourceControl, error) {
if _, ok := labels[publicRCLabel]; ok {
return authorization.NewPublicResourceControl(resourceID, resourceType), nil
}
teamNames := make([]string, 0)
userNames := make([]string, 0)
if teams, ok := labels[teamRCLabel]; ok {
teamNames = getUniqueElements(teams)
}
if users, ok := labels[userRCLabel]; ok {
userNames = getUniqueElements(users)
}
if len(teamNames) == 0 && len(userNames) == 0 {
return authorization.NewEmptyRestrictedResourceControl(resourceID, resourceType), nil
}
teamIDs := make([]portainer.TeamID, 0)
userIDs := make([]portainer.UserID, 0)
for _, name := range teamNames {
team, err := tx.Team().TeamByName(name)
if err != nil {
log.Warn().
Str("name", name).
Str("resource_id", resourceID).
Msg("unknown team name in access control label, ignoring access control rule for this team")
continue
}
teamIDs = append(teamIDs, team.ID)
}
for _, name := range userNames {
userID, err := tx.User().UserIDByUsername(name)
if err != nil {
log.Warn().
Str("name", name).
Str("resource_id", resourceID).
Msg("unknown user name in access control label, ignoring access control rule for this user")
continue
}
userIDs = append(userIDs, userID)
}
return authorization.NewRestrictedResourceControl(resourceID, resourceType, userIDs, teamIDs), nil
}
func getUniqueElements(items string) []string {
xs := strings.Split(items, ",")
xs = slicesx.Map(xs, strings.TrimSpace)
xs = slicesx.FilterInPlace(xs, func(x string) bool { return len(x) > 0 })
return slicesx.Unique(xs)
}
+172
View File
@@ -0,0 +1,172 @@
package uac
import (
"testing"
"github.com/docker/docker/api/types/container"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/docker/consts"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/stretchr/testify/require"
)
func TestGenericResourcControlGetter(t *testing.T) {
t.Parallel()
is := require.New(t)
ok, store := datastore.MustNewTestStore(t, true, false)
is.True(ok)
is.NotNil(store)
endpointID := portainer.EndpointID(1)
composeStackName := "compose-stack"
composeStackRCID := StackResourceControlID(endpointID, composeStackName)
swarmStackName := "swarm-stack"
swarmStackRCID := StackResourceControlID(endpointID, swarmStackName)
serviceID := "service"
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
// created on container create
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl("container", portainer.ContainerResourceControl)))
// created on compose stack create
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(composeStackRCID, portainer.StackResourceControl)))
// created on swarm stack create
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(swarmStackRCID, portainer.StackResourceControl)))
// created a swarm service create
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(serviceID, portainer.ServiceResourceControl)))
return nil
})
is.NoError(err)
err = store.ViewTx(func(tx dataservices.DataStoreTx) error {
context := ResourceContext[container.Summary]{RCType: portainer.ContainerResourceControl, IDGetter: ContainerResourceControlID, LabelsGetter: ContainerLabels}
// trying to get UAC for a container created through Portainer
rc, err := genericResourcControlGetter(tx, 1, context)(container.Summary{ID: "container"})
is.NoError(err)
is.NotNil(rc)
// trying to get UAC for an external container
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{ID: "unknown"})
is.NoError(err)
is.NotNil(rc)
is.Empty(rc.UserAccesses)
is.Empty(rc.TeamAccesses)
// trying to get UAC for a container from a compose stack
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{ID: "by-compose-stack-name-label", Labels: map[string]string{consts.ComposeStackNameLabel: composeStackName}})
is.NoError(err)
is.NotNil(rc)
is.Equal(composeStackRCID, rc.ResourceID)
// trying to get UAC for a container from a swarm stack
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{ID: "by-swarm-stack-name-label", Labels: map[string]string{consts.SwarmStackNameLabel: swarmStackName}})
is.NoError(err)
is.NotNil(rc)
is.Equal(swarmStackRCID, rc.ResourceID)
// trying to get UAC for a container from a swarm service
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{ID: "by-swarm-service-id-label", Labels: map[string]string{consts.SwarmServiceIDLabel: serviceID}})
is.NoError(err)
is.NotNil(rc)
is.Equal(serviceID, rc.ResourceID)
return nil
})
is.NoError(err)
}
func TestGenericResourcControlGetterWithPortainerLabels(t *testing.T) {
t.Parallel()
is := require.New(t)
ok, store := datastore.MustNewTestStore(t, true, false)
is.True(ok)
is.NotNil(store)
is.NoError(store.UpdateTx(func(tx dataservices.DataStoreTx) error {
is.NoError(tx.User().Create(&portainer.User{Role: portainer.AdministratorRole, Username: "admin"}))
is.NoError(tx.User().Create(&portainer.User{Role: portainer.AdministratorRole, Username: "user"}))
is.NoError(tx.Team().Create(&portainer.Team{Name: "team"}))
is.NoError(tx.TeamMembership().Create(&portainer.TeamMembership{UserID: 2, TeamID: 1}))
return nil
}))
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
context := ResourceContext[container.Summary]{RCType: portainer.ContainerResourceControl, IDGetter: ContainerResourceControlID, LabelsGetter: ContainerLabels}
rc, err := genericResourcControlGetter(tx, 1, context)(container.Summary{
ID: "public",
Labels: map[string]string{publicRCLabel: "true"},
})
is.NoError(err)
is.NotNil(rc)
is.True(rc.Public)
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{
ID: "team",
Labels: map[string]string{teamRCLabel: "team"},
})
is.NoError(err)
is.NotNil(rc)
is.Contains(rc.TeamAccesses, portainer.TeamResourceAccess{TeamID: 1, AccessLevel: portainer.ReadWriteAccessLevel})
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{
ID: "no-team",
Labels: map[string]string{teamRCLabel: "team2"},
})
is.NoError(err)
is.NotNil(rc)
is.Empty(rc.UserAccesses)
is.Empty(rc.TeamAccesses)
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{
ID: "user",
Labels: map[string]string{userRCLabel: "user"},
})
is.NoError(err)
is.NotNil(rc)
is.Contains(rc.UserAccesses, portainer.UserResourceAccess{UserID: 2, AccessLevel: portainer.ReadWriteAccessLevel})
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{
ID: "no-user",
Labels: map[string]string{userRCLabel: "user2"},
})
is.NoError(err)
is.NotNil(rc)
is.Empty(rc.UserAccesses)
is.Empty(rc.TeamAccesses)
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{
ID: "warn-on-unknown",
Labels: map[string]string{userRCLabel: "user2,user3", teamRCLabel: "team2,team3"},
})
is.NoError(err)
is.NotNil(rc)
is.Empty(rc.UserAccesses)
is.Empty(rc.TeamAccesses)
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{
ID: "empty-when-no-label",
})
is.NoError(err)
is.NotNil(rc)
is.Empty(rc.UserAccesses)
is.Empty(rc.TeamAccesses)
rc, err = genericResourcControlGetter(tx, 1, context)(container.Summary{
ID: "empty-when-empty-labels",
Labels: map[string]string{},
})
is.NoError(err)
is.NotNil(rc)
is.Empty(rc.UserAccesses)
is.Empty(rc.TeamAccesses)
return nil
}))
}
+30
View File
@@ -0,0 +1,30 @@
package uac
import (
"github.com/docker/docker/api/types/network"
portainer "github.com/portainer/portainer/api"
)
func NetworkResourceControlGetter[
TX txLike[RCS, TS, US],
RCS rcServiceLike,
TS teamServiceLike,
US userServiceLike,
](
tx TX,
endpointID portainer.EndpointID,
) func(item network.Summary) (*portainer.ResourceControl, error) {
return genericResourcControlGetter(tx, endpointID, ResourceContext[network.Summary]{
RCType: portainer.NetworkResourceControl,
IDGetter: NetworkResourceControlID,
LabelsGetter: NetworkLabels,
})
}
func NetworkResourceControlID(item network.Summary) string {
return item.ID
}
func NetworkLabels(item network.Summary) map[string]string {
return item.Labels
}
+71
View File
@@ -0,0 +1,71 @@
package uac
import (
"testing"
"github.com/docker/docker/api/types/network"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/docker/consts"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/stretchr/testify/require"
)
func TestNetworkResourceControlGetter(t *testing.T) {
t.Parallel()
is := require.New(t)
ok, store := datastore.MustNewTestStore(t, true, false)
is.True(ok)
is.NotNil(store)
envID := portainer.EndpointID(1)
networkID := "network"
stackName := "stack"
stackRCID := stackutils.ResourceControlID(envID, stackName)
serviceID := "service"
is.NoError(store.UpdateTx(func(tx dataservices.DataStoreTx) error {
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(networkID, portainer.NetworkResourceControl)))
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(stackRCID, portainer.StackResourceControl)))
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(serviceID, portainer.ServiceResourceControl)))
return nil
}))
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
// by direct ID
rc, err := NetworkResourceControlGetter(tx, envID)(network.Inspect{ID: networkID})
is.NoError(err)
is.NotNil(rc)
is.Equal(networkID, rc.ResourceID)
// by compose stack label
rc, err = NetworkResourceControlGetter(tx, envID)(
network.Inspect{ID: "unknown", Labels: map[string]string{consts.ComposeStackNameLabel: stackName}},
)
is.NoError(err)
is.NotNil(rc)
is.Equal(stackRCID, rc.ResourceID)
// by swarm stack label
rc, err = NetworkResourceControlGetter(tx, envID)(
network.Inspect{ID: "unknown", Labels: map[string]string{consts.SwarmStackNameLabel: stackName}},
)
is.NoError(err)
is.NotNil(rc)
is.Equal(stackRCID, rc.ResourceID)
// by service ID
rc, err = NetworkResourceControlGetter(tx, envID)(
network.Inspect{ID: "unknown", Labels: map[string]string{consts.SwarmServiceIDLabel: serviceID}},
)
is.NoError(err)
is.NotNil(rc)
is.Equal(serviceID, rc.ResourceID)
return nil
}))
}
+30
View File
@@ -0,0 +1,30 @@
package uac
import (
"github.com/docker/docker/api/types/swarm"
portainer "github.com/portainer/portainer/api"
)
func SecretResourceControlGetter[
TX txLike[RCS, TS, US],
RCS rcServiceLike,
TS teamServiceLike,
US userServiceLike,
](
tx TX,
endpointID portainer.EndpointID,
) func(item swarm.Secret) (*portainer.ResourceControl, error) {
return genericResourcControlGetter(tx, endpointID, ResourceContext[swarm.Secret]{
RCType: portainer.SecretResourceControl,
IDGetter: SecretResourceControlID,
LabelsGetter: SecretLabels,
})
}
func SecretResourceControlID(item swarm.Secret) string {
return item.ID
}
func SecretLabels(item swarm.Secret) map[string]string {
return item.Spec.Labels
}
+71
View File
@@ -0,0 +1,71 @@
package uac
import (
"testing"
"github.com/docker/docker/api/types/swarm"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/docker/consts"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/stretchr/testify/require"
)
func TestSecretResourceControlGetter(t *testing.T) {
t.Parallel()
is := require.New(t)
ok, store := datastore.MustNewTestStore(t, true, false)
is.True(ok)
is.NotNil(store)
envID := portainer.EndpointID(1)
secretID := "secret"
stackName := "stack"
stackRCID := stackutils.ResourceControlID(envID, stackName)
serviceID := "service"
is.NoError(store.UpdateTx(func(tx dataservices.DataStoreTx) error {
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(secretID, portainer.SecretResourceControl)))
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(stackRCID, portainer.StackResourceControl)))
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(serviceID, portainer.ServiceResourceControl)))
return nil
}))
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
// by direct ID
rc, err := SecretResourceControlGetter(tx, envID)(swarm.Secret{ID: secretID})
is.NoError(err)
is.NotNil(rc)
is.Equal(secretID, rc.ResourceID)
// by compose stack label
rc, err = SecretResourceControlGetter(tx, envID)(
swarm.Secret{ID: "unknown", Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Labels: map[string]string{consts.ComposeStackNameLabel: stackName}}}},
)
is.NoError(err)
is.NotNil(rc)
is.Equal(stackRCID, rc.ResourceID)
// by swarm stack label
rc, err = SecretResourceControlGetter(tx, envID)(
swarm.Secret{ID: "unknown", Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Labels: map[string]string{consts.SwarmStackNameLabel: stackName}}}},
)
is.NoError(err)
is.NotNil(rc)
is.Equal(stackRCID, rc.ResourceID)
// by service ID
rc, err = SecretResourceControlGetter(tx, envID)(
swarm.Secret{ID: "unknown", Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Labels: map[string]string{consts.SwarmServiceIDLabel: serviceID}}}},
)
is.NoError(err)
is.NotNil(rc)
is.Equal(serviceID, rc.ResourceID)
return nil
}))
}
+30
View File
@@ -0,0 +1,30 @@
package uac
import (
"github.com/docker/docker/api/types/swarm"
portainer "github.com/portainer/portainer/api"
)
func ServiceResourceControlGetter[
TX txLike[RCS, TS, US],
RCS rcServiceLike,
TS teamServiceLike,
US userServiceLike,
](
tx TX,
endpointID portainer.EndpointID,
) func(item swarm.Service) (*portainer.ResourceControl, error) {
return genericResourcControlGetter(tx, endpointID, ResourceContext[swarm.Service]{
RCType: portainer.ServiceResourceControl,
IDGetter: ServiceResourceControlID,
LabelsGetter: ServiceLabels,
})
}
func ServiceResourceControlID(item swarm.Service) string {
return item.ID
}
func ServiceLabels(item swarm.Service) map[string]string {
return item.Spec.Labels
}
+48
View File
@@ -0,0 +1,48 @@
package uac
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/stacks/stackutils"
)
func StackResourceControlGetter[
TX txLike[RCS, TS, US],
RCS rcServiceLike,
TS teamServiceLike,
US userServiceLike,
](
tx TX,
endpointID portainer.EndpointID,
) func(item portainer.Stack) (*portainer.ResourceControl, error) {
return genericResourcControlGetter(tx, endpointID, ResourceContext[portainer.Stack]{
RCType: portainer.StackResourceControl,
IDGetter: func(s portainer.Stack) string { return StackResourceControlID(s.EndpointID, s.Name) },
// stacks don't have labels so we don't pass a getter
})
}
// TODO: replace usage of stackutils function to this package
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 },
})
}
+53
View File
@@ -0,0 +1,53 @@
package uac
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/slicesx"
)
// FilterByResourceControl filters a list of items based on the user's role and the resource control associated to the item.
// UAC rules
// * UAC in DB (direct)
// * UAC inherited (from swarm service | swarm stack | compose stack)
// * UAC defined in labels (UAC on external resources)
func FilterByResourceControl[T any](
items []T,
user *portainer.User,
userMemberships []portainer.TeamMembership,
resourceControlGetter func(item T) (*portainer.ResourceControl, error),
) ([]T, error) {
filteredItems := make([]T, 0)
if user == nil {
return filteredItems, nil
}
if canBypassUAC(user) {
return items, nil
}
userTeamIDs := slicesx.Map(userMemberships, func(membership portainer.TeamMembership) portainer.TeamID {
return membership.TeamID
})
for _, item := range items {
rc, err := resourceControlGetter(item)
if err != nil {
return nil, fmt.Errorf("Unable to retrieve resource control: %w", err)
}
// TODO: move UserCanAccessResource function to the UAC package
if authorization.UserCanAccessResource(user.ID, userTeamIDs, rc) {
filteredItems = append(filteredItems, item)
}
}
return filteredItems, nil
}
func canBypassUAC(user *portainer.User) bool {
return user != nil && user.Role == portainer.AdministratorRole
}
+78
View File
@@ -0,0 +1,78 @@
package uac
import (
"testing"
"github.com/docker/docker/api/types/container"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/docker/consts"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/stretchr/testify/require"
)
func TestCanBypassUAC(t *testing.T) {
t.Parallel()
is := require.New(t)
admin := &portainer.User{Role: portainer.AdministratorRole}
user := &portainer.User{Role: portainer.StandardUserRole}
is.False(canBypassUAC(nil))
is.False(canBypassUAC(user))
is.True(canBypassUAC(admin))
}
func TestFilterByResourceControl(t *testing.T) {
t.Parallel()
is := require.New(t)
ok, store := datastore.MustNewTestStore(t, true, false)
is.True(ok)
is.NotNil(store)
endpointID := portainer.EndpointID(1)
stackRCID := StackResourceControlID(endpointID, "stack")
is.NoError(store.UpdateTx(func(tx dataservices.DataStoreTx) error {
is.NoError(tx.ResourceControl().Create(authorization.NewPrivateResourceControl(stackRCID, portainer.StackResourceControl, 2)))
is.NoError(tx.ResourceControl().Create(authorization.NewAdministratorsOnlyResourceControl("admin", portainer.ContainerResourceControl)))
is.NoError(tx.ResourceControl().Create(authorization.NewPrivateResourceControl("private", portainer.ContainerResourceControl, 2)))
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl("public", portainer.ContainerResourceControl)))
is.NoError(tx.ResourceControl().Create(authorization.NewSystemResourceControl("system", portainer.ContainerResourceControl)))
return nil
}))
admin := &portainer.User{ID: 1, Role: portainer.AdministratorRole}
std := &portainer.User{ID: 2, Role: portainer.StandardUserRole}
std2 := &portainer.User{ID: 3, Role: portainer.StandardUserRole}
containers := []container.Summary{{ID: "admin"}, {ID: "private"}, {ID: "public"}, {ID: "system"},
{ID: "bylabel", Labels: map[string]string{consts.ComposeStackNameLabel: "stack"}},
}
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
c, err := FilterByResourceControl(containers, admin, []portainer.TeamMembership{}, ContainerResourceControlGetter(tx, endpointID))
is.NoError(err)
is.Len(c, len(containers))
c, err = FilterByResourceControl(containers, std, []portainer.TeamMembership{}, ContainerResourceControlGetter(tx, endpointID))
is.NoError(err)
is.Len(c, 4)
is.Contains(c, container.Summary{ID: "private"})
is.Contains(c, container.Summary{ID: "public"})
is.Contains(c, container.Summary{ID: "system"})
is.Contains(c, container.Summary{ID: "bylabel", Labels: map[string]string{consts.ComposeStackNameLabel: "stack"}})
c, err = FilterByResourceControl(containers, std2, []portainer.TeamMembership{}, ContainerResourceControlGetter(tx, endpointID))
is.NoError(err)
is.Len(c, 2)
is.Contains(c, container.Summary{ID: "public"})
is.Contains(c, container.Summary{ID: "system"})
return nil
}))
}
+32
View File
@@ -0,0 +1,32 @@
package uac
import (
"github.com/docker/docker/api/types/volume"
portainer "github.com/portainer/portainer/api"
)
func VolumeResourceControlGetter[
TX txLike[RCS, TS, US],
RCS rcServiceLike,
TS teamServiceLike,
US userServiceLike,
](
tx TX,
endpointID portainer.EndpointID,
) func(item volume.Volume) (*portainer.ResourceControl, error) {
return genericResourcControlGetter(tx, endpointID, ResourceContext[volume.Volume]{
RCType: portainer.VolumeResourceControl,
IDGetter: VolumeResourceControlID,
LabelsGetter: VolumeLabels,
})
}
// TODO: use a copy of getVolumeResourceID()
// or change the key (+ migrate) to not require using the dockerID/swarm nodeID
func VolumeResourceControlID(item volume.Volume) string {
return item.Name
}
func VolumeLabels(item volume.Volume) map[string]string {
return item.Labels
}
+71
View File
@@ -0,0 +1,71 @@
package uac
import (
"testing"
"github.com/docker/docker/api/types/volume"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/docker/consts"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/stretchr/testify/require"
)
func TestVolumeResourceControlGetter(t *testing.T) {
t.Parallel()
is := require.New(t)
ok, store := datastore.MustNewTestStore(t, true, false)
is.True(ok)
is.NotNil(store)
envID := portainer.EndpointID(1)
volumeName := "volume"
stackName := "stack"
stackRCID := stackutils.ResourceControlID(envID, stackName)
serviceID := "service"
is.NoError(store.UpdateTx(func(tx dataservices.DataStoreTx) error {
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(volumeName, portainer.VolumeResourceControl)))
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(stackRCID, portainer.StackResourceControl)))
is.NoError(tx.ResourceControl().Create(authorization.NewPublicResourceControl(serviceID, portainer.ServiceResourceControl)))
return nil
}))
is.NoError(store.ViewTx(func(tx dataservices.DataStoreTx) error {
// by direct ID
rc, err := VolumeResourceControlGetter(tx, envID)(volume.Volume{Name: volumeName})
is.NoError(err)
is.NotNil(rc)
is.Equal(volumeName, rc.ResourceID)
// by compose stack label
rc, err = VolumeResourceControlGetter(tx, envID)(
volume.Volume{Name: "unknown", Labels: map[string]string{consts.ComposeStackNameLabel: stackName}},
)
is.NoError(err)
is.NotNil(rc)
is.Equal(stackRCID, rc.ResourceID)
// by swarm stack label
rc, err = VolumeResourceControlGetter(tx, envID)(
volume.Volume{Name: "unknown", Labels: map[string]string{consts.SwarmStackNameLabel: stackName}},
)
is.NoError(err)
is.NotNil(rc)
is.Equal(stackRCID, rc.ResourceID)
// by service ID
rc, err = VolumeResourceControlGetter(tx, envID)(
volume.Volume{Name: "unknown", Labels: map[string]string{consts.SwarmServiceIDLabel: serviceID}},
)
is.NoError(err)
is.NotNil(rc)
is.Equal(serviceID, rc.ResourceID)
return nil
}))
}