fix(api/container): standard users cannot connect or disconnect containers to networks (#1118)

pull/12598/merge
LP B 2025-09-09 22:07:19 +02:00 committed by GitHub
parent 8b73ad3b6f
commit 7ebb52ec6d
7 changed files with 508 additions and 63 deletions

View File

@ -5,11 +5,13 @@ import (
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/docker/consts"
"github.com/portainer/portainer/api/http/proxy/factory/utils"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/slicesx"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/docker/docker/client"
"github.com/rs/zerolog/log"
)
@ -17,9 +19,6 @@ const (
resourceLabelForPortainerTeamResourceControl = "io.portainer.accesscontrol.teams"
resourceLabelForPortainerUserResourceControl = "io.portainer.accesscontrol.users"
resourceLabelForPortainerPublicResourceControl = "io.portainer.accesscontrol.public"
resourceLabelForDockerSwarmStackName = "com.docker.stack.namespace"
resourceLabelForDockerServiceID = "com.docker.swarm.service.id"
resourceLabelForDockerComposeStackName = "com.docker.compose.project"
)
type (
@ -123,13 +122,7 @@ func (transport *Transport) createPrivateResourceControl(resourceIdentifier stri
return resourceControl, nil
}
func (transport *Transport) getInheritedResourceControlFromServiceOrStack(resourceIdentifier, nodeName string, resourceType portainer.ResourceControlType, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
client, err := transport.dockerClientFactory.CreateClient(transport.endpoint, nodeName, nil)
if err != nil {
return nil, err
}
defer client.Close()
func (transport *Transport) getInheritedResourceControlFromServiceOrStack(client *client.Client, resourceIdentifier string, resourceType portainer.ResourceControlType, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
switch resourceType {
case portainer.ContainerResourceControl:
return getInheritedResourceControlFromContainerLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
@ -295,8 +288,8 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou
return nil, nil
}
if resourceLabelsObject[resourceLabelForDockerServiceID] != nil {
inheritedServiceIdentifier := resourceLabelsObject[resourceLabelForDockerServiceID].(string)
if resourceLabelsObject[consts.SwarmServiceIDLabel] != nil {
inheritedServiceIdentifier := resourceLabelsObject[consts.SwarmServiceIDLabel].(string)
resourceControl = authorization.GetResourceControlByResourceIDAndType(inheritedServiceIdentifier, portainer.ServiceResourceControl, resourceControls)
if resourceControl != nil {
@ -304,8 +297,8 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou
}
}
if resourceLabelsObject[resourceLabelForDockerSwarmStackName] != nil {
stackName := resourceLabelsObject[resourceLabelForDockerSwarmStackName].(string)
if resourceLabelsObject[consts.SwarmStackNameLabel] != nil {
stackName := resourceLabelsObject[consts.SwarmStackNameLabel].(string)
stackResourceID := stackutils.ResourceControlID(transport.endpoint.ID, stackName)
resourceControl = authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls)
@ -314,8 +307,8 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou
}
}
if resourceLabelsObject[resourceLabelForDockerComposeStackName] != nil {
stackName := resourceLabelsObject[resourceLabelForDockerComposeStackName].(string)
if resourceLabelsObject[consts.ComposeStackNameLabel] != nil {
stackName := resourceLabelsObject[consts.ComposeStackNameLabel].(string)
stackResourceID := stackutils.ResourceControlID(transport.endpoint.ID, stackName)
resourceControl = authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls)
@ -328,14 +321,14 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou
}
func getStackResourceIDFromLabels(resourceLabelsObject map[string]string, endpointID portainer.EndpointID) string {
if resourceLabelsObject[resourceLabelForDockerSwarmStackName] != "" {
stackName := resourceLabelsObject[resourceLabelForDockerSwarmStackName]
if resourceLabelsObject[consts.SwarmStackNameLabel] != "" {
stackName := resourceLabelsObject[consts.SwarmStackNameLabel]
return stackutils.ResourceControlID(endpointID, stackName)
}
if resourceLabelsObject[resourceLabelForDockerComposeStackName] != "" {
stackName := resourceLabelsObject[resourceLabelForDockerComposeStackName]
if resourceLabelsObject[consts.ComposeStackNameLabel] != "" {
stackName := resourceLabelsObject[consts.ComposeStackNameLabel]
return stackutils.ResourceControlID(endpointID, stackName)
}

View File

@ -9,6 +9,7 @@ import (
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/docker/consts"
"github.com/portainer/portainer/api/http/proxy/factory/utils"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
@ -34,7 +35,7 @@ func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client,
return nil, err
}
serviceName := container.Config.Labels[resourceLabelForDockerServiceID]
serviceName := container.Config.Labels[consts.SwarmServiceIDLabel]
if serviceName != "" {
serviceResourceControl := authorization.GetResourceControlByResourceIDAndType(serviceName, portainer.ServiceResourceControl, resourceControls)
if serviceResourceControl != nil {

View File

@ -11,7 +11,7 @@ import (
"github.com/portainer/portainer/api/http/proxy/factory/utils"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/segmentio/encoding/json"
)
@ -19,7 +19,7 @@ import (
const serviceObjectIdentifier = "ID"
func getInheritedResourceControlFromServiceLabels(dockerClient *client.Client, endpointID portainer.EndpointID, serviceID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
service, _, err := dockerClient.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{})
service, _, err := dockerClient.ServiceInspectWithRaw(context.Background(), serviceID, swarm.ServiceInspectOptions{})
if err != nil {
return nil, err
}

View File

@ -2,6 +2,7 @@ package docker
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
@ -15,12 +16,15 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/docker/client"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/http/proxy/factory/utils"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
dockerclient "github.com/portainer/portainer/api/docker/client"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
dockerclient "github.com/docker/docker/client"
"github.com/rs/zerolog/log"
"github.com/segmentio/encoding/json"
)
@ -36,7 +40,7 @@ type (
dataStore dataservices.DataStore
signatureService portainer.DigitalSignatureService
reverseTunnelService portainer.ReverseTunnelService
dockerClientFactory *dockerclient.ClientFactory
dockerClientFactory *client.ClientFactory
gitService portainer.GitService
snapshotService portainer.SnapshotService
dockerID string
@ -49,7 +53,7 @@ type (
DataStore dataservices.DataStore
SignatureService portainer.DigitalSignatureService
ReverseTunnelService portainer.ReverseTunnelService
DockerClientFactory *dockerclient.ClientFactory
DockerClientFactory *client.ClientFactory
}
restrictedDockerOperationContext struct {
@ -107,6 +111,9 @@ var prefixProxyFuncMap = map[string]func(*Transport, *http.Request, string) (*ht
// ProxyDockerRequest intercepts a Docker API request and apply logic based
// on the requested operation.
func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Response, error) {
// from : /v1.41/containers/{id}/json
// or : /containers/{id}/json
// to : /containers/{id}/json
unversionedPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")
if transport.endpoint.Type == portainer.AgentOnDockerEnvironment || transport.endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
@ -119,6 +126,10 @@ func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Res
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
}
// from : /containers/{id}/json
// trim to : containers/{id}/json
// pick : [ containers, {id}, json ][0]
// prefix : containers
prefix := strings.Split(strings.TrimPrefix(unversionedPath, "/"), "/")[0]
if proxyFunc := prefixProxyFuncMap[prefix]; proxyFunc != nil {
@ -215,9 +226,10 @@ func (transport *Transport) proxyConfigRequest(request *http.Request, unversione
// Assume /configs/{id}
configID := path.Base(requestPath)
if request.Method == http.MethodGet {
switch request.Method {
case http.MethodGet:
return transport.rewriteOperation(request, transport.configInspectOperation)
} else if request.Method == http.MethodDelete {
case http.MethodDelete:
return transport.executeGenericResourceDeletionOperation(request, configID, configID, portainer.ConfigResourceControl)
}
@ -248,7 +260,6 @@ func (transport *Transport) proxyContainerRequest(request *http.Request, unversi
if action == "json" {
return transport.rewriteOperation(request, transport.containerInspectOperation)
}
return transport.restrictedResourceOperation(request, containerID, containerID, portainer.ContainerResourceControl, false)
} else if match, _ := path.Match("/containers/*", requestPath); match {
// Handle /containers/{id} requests
@ -280,7 +291,10 @@ func (transport *Transport) proxyServiceRequest(request *http.Request, unversion
if match, _ := path.Match("/services/*/*", requestPath); match {
// Handle /services/{id}/{action} requests
serviceID := path.Base(path.Dir(requestPath))
transport.decorateRegistryAuthenticationHeader(request)
if err := transport.decorateRegistryAuthenticationHeader(request); err != nil {
return nil, err
}
return transport.restrictedResourceOperation(request, serviceID, serviceID, portainer.ServiceResourceControl, false)
} else if match, _ := path.Match("/services/*", requestPath); match {
@ -320,28 +334,38 @@ func (transport *Transport) proxyVolumeRequest(request *http.Request, unversione
}
}
func match(requestPath string, pattern string) bool {
ok, err := path.Match(pattern, requestPath)
return err == nil && ok
}
func (transport *Transport) proxyNetworkRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath
switch requestPath {
case "/networks/create":
switch {
case requestPath == "/networks/create":
return transport.decorateGenericResourceCreationOperation(request, networkObjectIdentifier, portainer.NetworkResourceControl)
case "/networks":
case requestPath == "/networks":
return transport.rewriteOperation(request, transport.networkListOperation)
default:
// Assume /networks/{id}
networkID := path.Base(requestPath)
if request.Method == http.MethodGet {
return transport.rewriteOperation(request, transport.networkInspectOperation)
} else if request.Method == http.MethodDelete {
return transport.executeGenericResourceDeletionOperation(request, networkID, networkID, portainer.NetworkResourceControl)
}
case request.Method == http.MethodPost && match(requestPath, "/networks/*/connect"),
request.Method == http.MethodPost && match(requestPath, "/networks/*/disconnect"):
networkID := path.Base(path.Dir(requestPath))
return transport.restrictedResourceOperation(request, networkID, networkID, portainer.NetworkResourceControl, false)
case request.Method == http.MethodGet && match(requestPath, "/networks/*"):
return transport.rewriteOperation(request, transport.networkInspectOperation)
case request.Method == http.MethodDelete && match(requestPath, "/networks/*"):
networkID := path.Base(requestPath)
return transport.executeGenericResourceDeletionOperation(request, networkID, networkID, portainer.NetworkResourceControl)
}
// Assume /networks/{id}
networkID := path.Base(requestPath)
return transport.restrictedResourceOperation(request, networkID, networkID, portainer.NetworkResourceControl, false)
}
func (transport *Transport) proxySecretRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
@ -358,9 +382,10 @@ func (transport *Transport) proxySecretRequest(request *http.Request, unversione
// Assume /secrets/{id}
secretID := path.Base(requestPath)
if request.Method == http.MethodGet {
switch request.Method {
case http.MethodGet:
return transport.rewriteOperation(request, transport.secretInspectOperation)
} else if request.Method == http.MethodDelete {
case http.MethodDelete:
return transport.executeGenericResourceDeletionOperation(request, secretID, secretID, portainer.SecretResourceControl)
}
@ -413,7 +438,6 @@ func (transport *Transport) proxyBuildRequest(request *http.Request, _ string) (
func (transport *Transport) updateDefaultGitBranch(request *http.Request) error {
remote := request.URL.Query().Get("remote")
if !strings.HasSuffix(remote, ".git") {
return nil
}
@ -549,32 +573,101 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
}
resourceControl := authorization.GetResourceControlByResourceIDAndType(resourceID, resourceType, resourceControls)
if resourceControl == nil {
agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader)
if dockerResourceID == "" {
dockerResourceID = resourceID
}
// This resource was created outside of portainer,
// is part of a Docker service or part of a Docker Swarm/Compose stack.
inheritedResourceControl, err := transport.getInheritedResourceControlFromServiceOrStack(dockerResourceID, agentTargetHeader, resourceType, resourceControls)
if err != nil {
return nil, err
}
if inheritedResourceControl == nil || !authorization.UserCanAccessResource(tokenData.ID, userTeamIDs, inheritedResourceControl) {
if resourceControl != nil {
if !authorization.UserCanAccessResource(tokenData.ID, userTeamIDs, resourceControl) {
return utils.WriteAccessDeniedResponse()
}
return transport.executeDockerRequest(request)
}
if resourceControl != nil && !authorization.UserCanAccessResource(tokenData.ID, userTeamIDs, resourceControl) {
client, err := transport.dockerClientFactory.CreateClient(transport.endpoint, request.Header.Get(portainer.PortainerAgentTargetHeader), nil)
if err != nil {
return nil, err
}
defer client.Close()
// the resourceID may be the resource name (as it's a valid proxy call to use the name and not the UUID)
// so get the real resource ID and retry with it
resourceID, err = getRealResourceID(client, resourceType, resourceID)
if err != nil {
return nil, err
}
resourceControl = authorization.GetResourceControlByResourceIDAndType(resourceID, resourceType, resourceControls)
if resourceControl != nil {
if !authorization.UserCanAccessResource(tokenData.ID, userTeamIDs, resourceControl) {
return utils.WriteAccessDeniedResponse()
}
return transport.executeDockerRequest(request)
}
// If we still can't find the RC by provided ID or "real" (docker-extracted) ID
// it means this resource was created outside of portainer,
// is part of a Docker service or part of a Docker Swarm/Compose stack.
if dockerResourceID == "" {
dockerResourceID = resourceID
}
inheritedResourceControl, err := transport.getInheritedResourceControlFromServiceOrStack(client, dockerResourceID, resourceType, resourceControls)
if err != nil {
return nil, err
}
if inheritedResourceControl == nil || !authorization.UserCanAccessResource(tokenData.ID, userTeamIDs, inheritedResourceControl) {
return utils.WriteAccessDeniedResponse()
}
return transport.executeDockerRequest(request)
}
func getRealResourceID(client *dockerclient.Client, resourceType portainer.ResourceControlType, resourceId string) (string, error) {
switch resourceType {
case portainer.NetworkResourceControl:
network, err := client.NetworkInspect(context.Background(), resourceId, network.InspectOptions{})
if err != nil {
return "", err
}
return network.ID, nil
case portainer.ContainerResourceControl:
container, err := client.ContainerInspect(context.Background(), resourceId)
if err != nil {
return "", err
}
return container.ID, nil
case portainer.VolumeResourceControl:
// volumes don't have an UUID and their UACresourceID has a particular construct that makes them unique
// e.g. fmt.Sprintf("%s_%s", volumeName, dockerID)
// see transport.getVolumeResourceID() / FetchDockerID()
// FetchDockerID fetches info.Swarm.Cluster.ID if environment(endpoint) is swarm and info.ID otherwise
// So: return empty ID but without error
return "", nil
case portainer.ServiceResourceControl:
service, _, err := client.ServiceInspectWithRaw(context.Background(), resourceId, swarm.ServiceInspectOptions{})
if err != nil {
return "", err
}
return service.ID, nil
case portainer.ConfigResourceControl:
config, _, err := client.ConfigInspectWithRaw(context.Background(), resourceId)
if err != nil {
return "", err
}
return config.ID, nil
case portainer.SecretResourceControl:
secret, _, err := client.SecretInspectWithRaw(context.Background(), resourceId)
if err != nil {
return "", err
}
return secret.ID, nil
}
return "", fmt.Errorf("Unknown resource type %v", resourceType)
}
// rewriteOperationWithLabelFiltering will create a new operation context with data that will be used
// to decorate the original request's response as well as retrieve all the black listed labels
// to filter the resources.

View File

@ -6,9 +6,19 @@ import (
"net/http/httptest"
"testing"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"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/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTransport_updateDefaultGitBranch(t *testing.T) {
@ -21,7 +31,6 @@ func TestTransport_updateDefaultGitBranch(t *testing.T) {
}
commitId := "my-latest-commit-id"
defaultFields := fields{
gitService: testhelpers.NewGitService(nil, commitId),
}
@ -67,3 +76,331 @@ func TestTransport_updateDefaultGitBranch(t *testing.T) {
})
}
}
type RoutesDefinition map[[2]string]any
func mockDockerAPIServer(t *testing.T, routes RoutesDefinition) (*httptest.Server, string) {
version := "1.51"
v := func(path string) string {
return "/v" + version + path
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodHead && r.URL.Path == "/_ping" {
w.Header().Add("Api-Version", version)
_, _ = w.Write([]byte{})
return
}
for defs, rValue := range routes {
method, path := defs[0], defs[1]
if r.Method == method && r.URL.Path == v(path) {
_ = response.JSON(w, rValue)
return
}
}
http.NotFound(w, r)
}))
require.NotNil(t, srv)
return srv, version
}
func TestTransport_getRealResourceID(t *testing.T) {
srv, _ := mockDockerAPIServer(t, RoutesDefinition{
{http.MethodGet, "/networks"}: []network.Summary{{ID: "16e37c629e88694663791dc738fd37affb908d7b85ce00a20680675d10554fd4", Name: "mynetwork"}},
{http.MethodGet, "/networks/mynetwork"}: network.Inspect{ID: "16e37c629e88694663791dc738fd37affb908d7b85ce00a20680675d10554fd4", Name: "mynetwork"},
{http.MethodGet, "/networks/16e37c629e88694663791dc738fd37affb908d7b85ce00a20680675d10554fd4"}: network.Inspect{ID: "16e37c629e88694663791dc738fd37affb908d7b85ce00a20680675d10554fd4", Name: "mynetwork"},
{http.MethodGet, "/containers/mycontainer/json"}: container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ID: "545fc03ed1fd5008c3bfa2441209ff024e21e396acbeb58b2355930ad1295aa6", Name: "mycontainer"}},
{http.MethodGet, "/containers/545fc03ed1fd5008c3bfa2441209ff024e21e396acbeb58b2355930ad1295aa6/json"}: container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ID: "545fc03ed1fd5008c3bfa2441209ff024e21e396acbeb58b2355930ad1295aa6", Name: "mycontainer"}},
{http.MethodGet, "/services/myservice"}: swarm.Service{ID: "ibt43uf5awhg06bxp8rkd7bhi", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "myservice"}}},
{http.MethodGet, "/services/ibt43uf5awhg06bxp8rkd7bhi"}: swarm.Service{ID: "ibt43uf5awhg06bxp8rkd7bhi", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "myservice"}}},
{http.MethodGet, "/configs/myconfig"}: swarm.Config{ID: "3mlqqza0k413ecebk0mfa11em", Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "myconfig"}}},
{http.MethodGet, "/configs/3mlqqza0k413ecebk0mfa11em"}: swarm.Config{ID: "3mlqqza0k413ecebk0mfa11em", Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "myconfig"}}},
{http.MethodGet, "/secrets/mysecret"}: swarm.Secret{ID: "v9i7o4ivg33u4z3jfyxto162d", Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "mysecret"}}},
{http.MethodGet, "/secrets/v9i7o4ivg33u4z3jfyxto162d"}: swarm.Secret{ID: "v9i7o4ivg33u4z3jfyxto162d", Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "mysecret"}}},
})
defer srv.Close()
transport := &Transport{
endpoint: &portainer.Endpoint{URL: srv.URL},
}
client, err := transport.dockerClientFactory.CreateClient(transport.endpoint, "", nil)
require.NoError(t, err)
require.NotNil(t, client)
test := func(rctype portainer.ResourceControlType, name string, id string, errOnUnknown bool) {
// by id
got, err := getRealResourceID(client, rctype, id)
require.NoError(t, err)
require.Equal(t, id, got)
// by name
got, err = getRealResourceID(client, rctype, name)
require.NoError(t, err)
require.Equal(t, id, got)
// unknown for this type
_, err = getRealResourceID(client, rctype, "unknown")
if errOnUnknown {
require.Error(t, err)
} else {
require.NoError(t, err)
}
}
test(portainer.NetworkResourceControl, "mynetwork", "16e37c629e88694663791dc738fd37affb908d7b85ce00a20680675d10554fd4", true)
test(portainer.ContainerResourceControl, "mycontainer", "545fc03ed1fd5008c3bfa2441209ff024e21e396acbeb58b2355930ad1295aa6", true)
test(portainer.VolumeResourceControl, "anything", "", false)
test(portainer.ServiceResourceControl, "myservice", "ibt43uf5awhg06bxp8rkd7bhi", true)
test(portainer.ConfigResourceControl, "myconfig", "3mlqqza0k413ecebk0mfa11em", true)
test(portainer.SecretResourceControl, "mysecret", "v9i7o4ivg33u4z3jfyxto162d", true)
// validate that other types are not supported
_, err = getRealResourceID(client, portainer.ContainerGroupResourceControl, "")
require.Error(t, err)
}
func TestTransport_proxyNetworkRequest(t *testing.T) {
admin := portainer.User{ID: 1, Username: "admin", Role: portainer.AdministratorRole}
std1 := portainer.User{ID: 2, Username: "std1", Role: portainer.StandardUserRole}
std2 := portainer.User{ID: 3, Username: "std2", Role: portainer.StandardUserRole}
_, ds := datastore.MustNewTestStore(t, true, false)
require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
require.NoError(t, tx.User().Create(&admin))
require.NoError(t, tx.User().Create(&std1))
require.NoError(t, tx.User().Create(&std2))
require.NoError(t, tx.Endpoint().Create(&portainer.Endpoint{ID: 1, Name: "env",
UserAccessPolicies: portainer.UserAccessPolicies{std1.ID: portainer.AccessPolicy{RoleID: 1}},
}))
require.NoError(t, tx.ResourceControl().Create(authorization.NewPrivateResourceControl("16e37c629e88694663791dc738fd37affb908d7b85ce00a20680675d10554fd4", portainer.NetworkResourceControl, std1.ID)))
return nil
}))
srv, version := mockDockerAPIServer(t, RoutesDefinition{
{http.MethodGet, "/networks"}: []network.Summary{{ID: "16e37c629e88694663791dc738fd37affb908d7b85ce00a20680675d10554fd4", Name: "mynetwork"}},
{http.MethodGet, "/networks/mynetwork"}: network.Inspect{ID: "16e37c629e88694663791dc738fd37affb908d7b85ce00a20680675d10554fd4", Name: "mynetwork"},
{http.MethodGet, "/networks/16e37c629e88694663791dc738fd37affb908d7b85ce00a20680675d10554fd4"}: network.Inspect{ID: "16e37c629e88694663791dc738fd37affb908d7b85ce00a20680675d10554fd4", Name: "mynetwork"},
{http.MethodPost, "/networks/mynetwork/connect"}: struct{}{},
{http.MethodPost, "/networks/16e37c629e88694663791dc738fd37affb908d7b85ce00a20680675d10554fd4/connect"}: struct{}{},
{http.MethodPost, "/networks/mynetwork/disconnect"}: struct{}{},
{http.MethodPost, "/networks/16e37c629e88694663791dc738fd37affb908d7b85ce00a20680675d10554fd4/disconnect"}: struct{}{},
{http.MethodDelete, "/networks/mynetwork"}: struct{}{},
{http.MethodDelete, "/networks/16e37c629e88694663791dc738fd37affb908d7b85ce00a20680675d10554fd4"}: struct{}{},
{http.MethodPost, "/networks/create"}: network.CreateResponse{ID: "16e37c629e88694663791dc738fd37affb908d7b85ce00a20680675d10554fd4"},
{http.MethodPost, "/networks/prune"}: struct{}{},
})
defer srv.Close()
transport := &Transport{
endpoint: &portainer.Endpoint{URL: srv.URL},
dataStore: ds,
HTTPTransport: &http.Transport{},
}
test := func(method string, url string, token portainer.TokenData) (*http.Response, error) {
req := httptest.NewRequest(method, srv.URL+"/v"+version+url, nil)
req = req.WithContext(security.StoreTokenData(req, &token))
require.NotNil(t, req)
return transport.proxyNetworkRequest(req, url)
}
adminToken := portainer.TokenData{ID: admin.ID, Username: admin.Username, Role: admin.Role}
std1Token := portainer.TokenData{ID: std1.ID, Username: std1.Username, Role: std1.Role}
std2Token := portainer.TokenData{ID: std2.ID, Username: std2.Username, Role: std2.Role}
{
r, err := test(http.MethodGet, "/networks", adminToken)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusOK, r.StatusCode)
var resp []network.Summary
require.NoError(t, json.NewDecoder(r.Body).Decode(&resp))
require.Len(t, resp, 1)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodGet, "/networks", std1Token)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusOK, r.StatusCode)
var resp []network.Summary
require.NoError(t, json.NewDecoder(r.Body).Decode(&resp))
require.Len(t, resp, 1)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodGet, "/networks", std2Token)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusOK, r.StatusCode)
var resp []network.Summary
require.NoError(t, json.NewDecoder(r.Body).Decode(&resp))
require.Empty(t, resp)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodGet, "/networks/mynetwork", adminToken)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusOK, r.StatusCode)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodGet, "/networks/mynetwork", std1Token)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusOK, r.StatusCode)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodGet, "/networks/mynetwork", std2Token)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusForbidden, r.StatusCode)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodGet, "/networks/unknown", adminToken)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusNotFound, r.StatusCode)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodPost, "/networks/mynetwork/connect", adminToken)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusOK, r.StatusCode)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodPost, "/networks/mynetwork/connect", std1Token)
require.NoError(t, err)
require.NotNil(t, r)
require.NoError(t, r.Body.Close())
require.Equal(t, http.StatusOK, r.StatusCode)
}
{
r, err := test(http.MethodPost, "/networks/mynetwork/connect", std2Token)
require.NoError(t, err)
require.NotNil(t, r)
require.NoError(t, r.Body.Close())
require.Equal(t, http.StatusForbidden, r.StatusCode)
}
{
r, err := test(http.MethodPost, "/networks/mynetwork/disconnect", adminToken)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusOK, r.StatusCode)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodPost, "/networks/mynetwork/disconnect", std1Token)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusOK, r.StatusCode)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodPost, "/networks/mynetwork/disconnect", std2Token)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusForbidden, r.StatusCode)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodDelete, "/networks/mynetwork", adminToken)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusOK, r.StatusCode)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodDelete, "/networks/mynetwork", std1Token)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusOK, r.StatusCode)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodDelete, "/networks/mynetwork", std2Token)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusForbidden, r.StatusCode)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodPost, "/networks/create", adminToken)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusOK, r.StatusCode)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodPost, "/networks/create", std1Token)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusOK, r.StatusCode)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodPost, "/networks/create", std2Token)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusOK, r.StatusCode)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodPost, "/networks/prune", adminToken)
require.NoError(t, err)
require.NotNil(t, r)
require.Equal(t, http.StatusOK, r.StatusCode)
require.NoError(t, r.Body.Close())
}
{
r, err := test(http.MethodPost, "/networks/prune", std1Token)
require.Error(t, err)
require.Nil(t, r)
if r != nil {
r.Body.Close()
}
}
{
r, err := test(http.MethodPost, "/networks/prune", std2Token)
require.Error(t, err)
require.Nil(t, r)
if r != nil {
r.Body.Close()
}
}
}

View File

@ -63,7 +63,10 @@ type errorResponse struct {
// WriteAccessDeniedResponse will create a new access denied response
func WriteAccessDeniedResponse() (*http.Response, error) {
response := &http.Response{}
header := http.Header{}
header.Add("Content-Type", "application/json")
response := &http.Response{Header: header}
err := RewriteResponse(response, errorResponse{Message: "access denied to resource"}, http.StatusForbidden)
return response, err

View File

@ -0,0 +1,18 @@
package utils
import (
"net/http"
"testing"
"github.com/stretchr/testify/require"
)
func TestWriteAccessDeniedResponse(t *testing.T) {
r, err := WriteAccessDeniedResponse()
require.NoError(t, err)
defer r.Body.Close()
require.NotNil(t, r)
require.Equal(t, "application/json", r.Header.Get("content-type"))
require.Equal(t, http.StatusForbidden, r.StatusCode)
}