mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 05:45:22 +02:00
feat(api): rewrite access control management in Docker (#3337)
* feat(api): decorate Docker resource creation response with resource control * fix(api): fix a potential resource control conflict between stacks/volumes * feat(api): generate a default private resource control instead of admin only * fix(api): fix default RC value * fix(api): update RC authorizations check to support admin only flag * refactor(api): relocate access control related methods * fix(api): fix a potential conflict when fetching RC from database * refactor(api): refactor access control logic * refactor(api): remove the concept of DecoratedStack * feat(api): automatically remove RC when removing a Docker resource * refactor(api): update filter resource methods documentation * refactor(api): update proxy package structure * refactor(api): renamed proxy/misc package * feat(api): re-introduce ResourceControlDelete operation as admin restricted * refactor(api): relocate default endpoint authorizations * feat(api): migrate RBAC data * feat(app): ResourceControl management refactor * fix(api): fix access control issue on stack deletion and automatically delete RC * fix(api): fix stack filtering * fix(api): fix UpdateResourceControl operation checks * refactor(api): introduce a NewTransport builder method * refactor(api): inject endpoint in Docker transport * refactor(api): introduce Docker client into Docker transport * refactor(api): refactor http/proxy package * feat(api): inspect a Docker resource labels during access control validation * fix(api): only apply automatic resource control creation on success response * fix(api): fix stack access control check * fix(api): use StatusCreated instead of StatusOK for automatic resource control creation * fix(app): resource control fixes * fix(api): fix an issue preventing administrator to inspect a resource with a RC * refactor(api): remove useless error return * refactor(api): document DecorateStacks function * fix(api): fix invalid resource control type for container deletion * feat(api): support Docker system networks * feat(api): update Swagger docs * refactor(api): rename transport variable * refactor(api): rename transport variable * feat(networks): add system tag for system networks * feat(api): add support for resource control labels * feat(api): upgrade to DBVersion 22 * refactor(api): refactor access control management in Docker proxy * refactor(api): re-implement docker proxy taskListOperation * refactor(api): review parameters declaration * refactor(api): remove extra blank line * refactor(api): review method comments * fix(api): fix invalid ServerAddress property and review method visibility * feat(api): update error message * feat(api): update restrictedVolumeBrowserOperation method * refactor(api): refactor method parameters * refactor(api): minor refactor * refactor(api): change Azure transport visibility * refactor(api): update struct documentation * refactor(api): update struct documentation * feat(api): review restrictedResourceOperation method * refactor(api): remove unused authorization methods * feat(api): apply RBAC when enabled on stack operations * fix(api): fix invalid data migration procedure for DBVersion = 22 * fix(app): RC duplicate on private resource * feat(api): change Docker API version logic for libcompose/client factory * fix(api): update access denied error message to be Docker API compliant * fix(api): update volume browsing authorizations data migration * fix(api): fix an issue with access control in multi-node agent Swarm cluster
This commit is contained in:
parent
198e92c734
commit
19d4db13be
118 changed files with 3600 additions and 3020 deletions
306
api/http/proxy/factory/docker/access_control.go
Normal file
306
api/http/proxy/factory/docker/access_control.go
Normal file
|
@ -0,0 +1,306 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
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 (
|
||||
resourceLabelsObjectSelector func(map[string]interface{}) map[string]interface{}
|
||||
resourceOperationParameters struct {
|
||||
resourceIdentifierAttribute string
|
||||
resourceType portainer.ResourceControlType
|
||||
labelsObjectSelector resourceLabelsObjectSelector
|
||||
}
|
||||
)
|
||||
|
||||
func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject map[string]interface{}, resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
|
||||
if labelsObject[resourceLabelForPortainerPublicResourceControl] != nil {
|
||||
resourceControl := portainer.NewPublicResourceControl(resourceID, resourceType)
|
||||
|
||||
err := transport.resourceControlService.CreateResourceControl(resourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resourceControl, nil
|
||||
}
|
||||
|
||||
teamNames := make([]string, 0)
|
||||
userNames := make([]string, 0)
|
||||
if labelsObject[resourceLabelForPortainerTeamResourceControl] != nil {
|
||||
concatenatedTeamNames := labelsObject[resourceLabelForPortainerTeamResourceControl].(string)
|
||||
teamNames = strings.Split(concatenatedTeamNames, ",")
|
||||
}
|
||||
|
||||
if labelsObject[resourceLabelForPortainerUserResourceControl] != nil {
|
||||
concatenatedUserNames := labelsObject[resourceLabelForPortainerUserResourceControl].(string)
|
||||
userNames = strings.Split(concatenatedUserNames, ",")
|
||||
}
|
||||
|
||||
if len(teamNames) > 0 || len(userNames) > 0 {
|
||||
teamIDs := make([]portainer.TeamID, 0)
|
||||
userIDs := make([]portainer.UserID, 0)
|
||||
|
||||
for _, name := range teamNames {
|
||||
team, err := transport.teamService.TeamByName(name)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [http,proxy,docker] [message: unknown team name in access control label, ignoring access control rule for this team] [name: %s] [resource_id: %s]", name, resourceID)
|
||||
continue
|
||||
}
|
||||
|
||||
teamIDs = append(teamIDs, team.ID)
|
||||
}
|
||||
|
||||
for _, name := range userNames {
|
||||
user, err := transport.userService.UserByUsername(name)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [http,proxy,docker] [message: unknown user name in access control label, ignoring access control rule for this user] [name: %s] [resource_id: %s]", name, resourceID)
|
||||
continue
|
||||
}
|
||||
|
||||
userIDs = append(userIDs, user.ID)
|
||||
}
|
||||
|
||||
resourceControl := portainer.NewRestrictedResourceControl(resourceID, resourceType, userIDs, teamIDs)
|
||||
|
||||
err := transport.resourceControlService.CreateResourceControl(resourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resourceControl, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (transport *Transport) createPrivateResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userID portainer.UserID) (*portainer.ResourceControl, error) {
|
||||
resourceControl := portainer.NewPrivateResourceControl(resourceIdentifier, resourceType, userID)
|
||||
|
||||
err := transport.resourceControlService.CreateResourceControl(resourceControl)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [http,proxy,docker,transport] [message: unable to persist resource control] [resource: %s] [err: %s]", resourceIdentifier, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resourceControl, nil
|
||||
}
|
||||
|
||||
func (transport *Transport) getInheritedResourceControlFromServiceOrStack(resourceIdentifier, nodeName string, resourceType portainer.ResourceControlType, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
client := transport.dockerClient
|
||||
|
||||
if nodeName != "" {
|
||||
dockerClient, err := transport.dockerClientFactory.CreateClient(transport.endpoint, nodeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
client = dockerClient
|
||||
}
|
||||
|
||||
switch resourceType {
|
||||
case portainer.ContainerResourceControl:
|
||||
return getInheritedResourceControlFromContainerLabels(client, resourceIdentifier, resourceControls)
|
||||
case portainer.NetworkResourceControl:
|
||||
return getInheritedResourceControlFromNetworkLabels(client, resourceIdentifier, resourceControls)
|
||||
case portainer.VolumeResourceControl:
|
||||
return getInheritedResourceControlFromVolumeLabels(client, resourceIdentifier, resourceControls)
|
||||
case portainer.ServiceResourceControl:
|
||||
return getInheritedResourceControlFromServiceLabels(client, resourceIdentifier, resourceControls)
|
||||
case portainer.ConfigResourceControl:
|
||||
return getInheritedResourceControlFromConfigLabels(client, resourceIdentifier, resourceControls)
|
||||
case portainer.SecretResourceControl:
|
||||
return getInheritedResourceControlFromSecretLabels(client, resourceIdentifier, resourceControls)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (transport *Transport) applyAccessControlOnResource(parameters *resourceOperationParameters, responseObject map[string]interface{}, response *http.Response, executor *operationExecutor) error {
|
||||
if responseObject[parameters.resourceIdentifierAttribute] == nil {
|
||||
log.Printf("[WARN] [message: unable to find resource identifier property in resource object] [identifier_attribute: %s]", parameters.resourceIdentifierAttribute)
|
||||
return nil
|
||||
}
|
||||
|
||||
if parameters.resourceType == portainer.NetworkResourceControl {
|
||||
systemResourceControl := findSystemNetworkResourceControl(responseObject)
|
||||
if systemResourceControl != nil {
|
||||
responseObject = decorateObject(responseObject, systemResourceControl)
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
resourceIdentifier := responseObject[parameters.resourceIdentifierAttribute].(string)
|
||||
resourceLabelsObject := parameters.labelsObjectSelector(responseObject)
|
||||
|
||||
resourceControl, err := transport.findResourceControl(resourceIdentifier, parameters.resourceType, resourceLabelsObject, executor.operationContext.resourceControls)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resourceControl == nil && (executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess) {
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess || portainer.UserCanAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl) {
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
return responseutils.RewriteAccessDeniedResponse(response)
|
||||
}
|
||||
|
||||
func (transport *Transport) applyAccessControlOnResourceList(parameters *resourceOperationParameters, resourceData []interface{}, executor *operationExecutor) ([]interface{}, error) {
|
||||
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
|
||||
return transport.decorateResourceList(parameters, resourceData, executor.operationContext.resourceControls)
|
||||
}
|
||||
|
||||
return transport.filterResourceList(parameters, resourceData, executor.operationContext)
|
||||
}
|
||||
|
||||
func (transport *Transport) decorateResourceList(parameters *resourceOperationParameters, resourceData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
||||
decoratedResourceData := make([]interface{}, 0)
|
||||
|
||||
for _, resource := range resourceData {
|
||||
resourceObject := resource.(map[string]interface{})
|
||||
|
||||
if resourceObject[parameters.resourceIdentifierAttribute] == nil {
|
||||
log.Printf("[WARN] [http,proxy,docker,decorate] [message: unable to find resource identifier property in resource list element] [identifier_attribute: %s]", parameters.resourceIdentifierAttribute)
|
||||
continue
|
||||
}
|
||||
|
||||
if parameters.resourceType == portainer.NetworkResourceControl {
|
||||
systemResourceControl := findSystemNetworkResourceControl(resourceObject)
|
||||
if systemResourceControl != nil {
|
||||
resourceObject = decorateObject(resourceObject, systemResourceControl)
|
||||
decoratedResourceData = append(decoratedResourceData, resourceObject)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
resourceIdentifier := resourceObject[parameters.resourceIdentifierAttribute].(string)
|
||||
resourceLabelsObject := parameters.labelsObjectSelector(resourceObject)
|
||||
|
||||
resourceControl, err := transport.findResourceControl(resourceIdentifier, parameters.resourceType, resourceLabelsObject, resourceControls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
resourceObject = decorateObject(resourceObject, resourceControl)
|
||||
}
|
||||
|
||||
decoratedResourceData = append(decoratedResourceData, resourceObject)
|
||||
}
|
||||
|
||||
return decoratedResourceData, nil
|
||||
}
|
||||
|
||||
func (transport *Transport) filterResourceList(parameters *resourceOperationParameters, resourceData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
|
||||
filteredResourceData := make([]interface{}, 0)
|
||||
|
||||
for _, resource := range resourceData {
|
||||
resourceObject := resource.(map[string]interface{})
|
||||
if resourceObject[parameters.resourceIdentifierAttribute] == nil {
|
||||
log.Printf("[WARN] [http,proxy,docker,filter] [message: unable to find resource identifier property in resource list element] [identifier_attribute: %s]", parameters.resourceIdentifierAttribute)
|
||||
continue
|
||||
}
|
||||
|
||||
resourceIdentifier := resourceObject[parameters.resourceIdentifierAttribute].(string)
|
||||
resourceLabelsObject := parameters.labelsObjectSelector(resourceObject)
|
||||
|
||||
if parameters.resourceType == portainer.NetworkResourceControl {
|
||||
systemResourceControl := findSystemNetworkResourceControl(resourceObject)
|
||||
if systemResourceControl != nil {
|
||||
resourceObject = decorateObject(resourceObject, systemResourceControl)
|
||||
filteredResourceData = append(filteredResourceData, resourceObject)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
resourceControl, err := transport.findResourceControl(resourceIdentifier, parameters.resourceType, resourceLabelsObject, context.resourceControls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resourceControl == nil {
|
||||
if context.isAdmin || context.endpointResourceAccess {
|
||||
filteredResourceData = append(filteredResourceData, resourceObject)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if context.isAdmin || context.endpointResourceAccess || portainer.UserCanAccessResource(context.userID, context.userTeamIDs, resourceControl) {
|
||||
resourceObject = decorateObject(resourceObject, resourceControl)
|
||||
filteredResourceData = append(filteredResourceData, resourceObject)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredResourceData, nil
|
||||
}
|
||||
|
||||
func (transport *Transport) findResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, resourceLabelsObject map[string]interface{}, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
resourceControl := portainer.GetResourceControlByResourceIDAndType(resourceIdentifier, resourceType, resourceControls)
|
||||
if resourceControl != nil {
|
||||
return resourceControl, nil
|
||||
}
|
||||
|
||||
if resourceLabelsObject != nil {
|
||||
if resourceLabelsObject[resourceLabelForDockerServiceID] != nil {
|
||||
inheritedServiceIdentifier := resourceLabelsObject[resourceLabelForDockerServiceID].(string)
|
||||
resourceControl = portainer.GetResourceControlByResourceIDAndType(inheritedServiceIdentifier, portainer.ServiceResourceControl, resourceControls)
|
||||
|
||||
if resourceControl != nil {
|
||||
return resourceControl, nil
|
||||
}
|
||||
}
|
||||
|
||||
if resourceLabelsObject[resourceLabelForDockerSwarmStackName] != nil {
|
||||
inheritedSwarmStackIdentifier := resourceLabelsObject[resourceLabelForDockerSwarmStackName].(string)
|
||||
resourceControl = portainer.GetResourceControlByResourceIDAndType(inheritedSwarmStackIdentifier, portainer.StackResourceControl, resourceControls)
|
||||
|
||||
if resourceControl != nil {
|
||||
return resourceControl, nil
|
||||
}
|
||||
}
|
||||
|
||||
if resourceLabelsObject[resourceLabelForDockerComposeStackName] != nil {
|
||||
inheritedComposeStackIdentifier := resourceLabelsObject[resourceLabelForDockerComposeStackName].(string)
|
||||
resourceControl = portainer.GetResourceControlByResourceIDAndType(inheritedComposeStackIdentifier, portainer.StackResourceControl, resourceControls)
|
||||
|
||||
if resourceControl != nil {
|
||||
return resourceControl, nil
|
||||
}
|
||||
}
|
||||
|
||||
return transport.newResourceControlFromPortainerLabels(resourceLabelsObject, resourceIdentifier, resourceType)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
|
||||
if object["Portainer"] == nil {
|
||||
object["Portainer"] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
portainerMetadata := object["Portainer"].(map[string]interface{})
|
||||
portainerMetadata["ResourceControl"] = resourceControl
|
||||
return object
|
||||
}
|
56
api/http/proxy/factory/docker/build.go
Normal file
56
api/http/proxy/factory/docker/build.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/api/archive"
|
||||
)
|
||||
|
||||
type postDockerfileRequest struct {
|
||||
Content string
|
||||
}
|
||||
|
||||
// buildOperation inspects the "Content-Type" header to determine if it needs to alter the request.
|
||||
// If the value of the header is empty, it means that a Dockerfile is posted via upload, the function
|
||||
// will extract the file content from the request body, tar it, and rewrite the body.
|
||||
// If the value of the header contains "application/json", it means that the content of a Dockerfile is posted
|
||||
// in the request payload as JSON, the function will create a new file called Dockerfile inside a tar archive and
|
||||
// rewrite the body of the request.
|
||||
// In any other case, it will leave the request unaltered.
|
||||
func buildOperation(request *http.Request) error {
|
||||
contentTypeHeader := request.Header.Get("Content-Type")
|
||||
if contentTypeHeader != "" && !strings.Contains(contentTypeHeader, "application/json") {
|
||||
return nil
|
||||
}
|
||||
|
||||
var dockerfileContent []byte
|
||||
|
||||
if contentTypeHeader == "" {
|
||||
body, err := ioutil.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dockerfileContent = body
|
||||
} else {
|
||||
var req postDockerfileRequest
|
||||
if err := json.NewDecoder(request.Body).Decode(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
dockerfileContent = []byte(req.Content)
|
||||
}
|
||||
|
||||
buffer, err := archive.TarFileInBuffer(dockerfileContent, "Dockerfile", 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request.Body = ioutil.NopCloser(bytes.NewReader(buffer))
|
||||
request.ContentLength = int64(len(buffer))
|
||||
request.Header.Set("Content-Type", "application/x-tar")
|
||||
|
||||
return nil
|
||||
}
|
86
api/http/proxy/factory/docker/configs.go
Normal file
86
api/http/proxy/factory/docker/configs.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
)
|
||||
|
||||
const (
|
||||
configObjectIdentifier = "ID"
|
||||
)
|
||||
|
||||
func getInheritedResourceControlFromConfigLabels(dockerClient *client.Client, configID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
config, _, err := dockerClient.ConfigInspectWithRaw(context.Background(), configID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swarmStackName := config.Spec.Labels[resourceLabelForDockerSwarmStackName]
|
||||
if swarmStackName != "" {
|
||||
return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// configListOperation extracts the response as a JSON object, loop through the configs array
|
||||
// decorate and/or filter the configs based on resource controls before rewriting the response.
|
||||
func (transport *Transport) configListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// ConfigList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.30/#operation/ConfigList
|
||||
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourceOperationParameters := &resourceOperationParameters{
|
||||
resourceIdentifierAttribute: configObjectIdentifier,
|
||||
resourceType: portainer.ConfigResourceControl,
|
||||
labelsObjectSelector: selectorConfigLabels,
|
||||
}
|
||||
|
||||
responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// configInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the config based on resource control and either rewrite an access denied response or a decorated config.
|
||||
func (transport *Transport) configInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// ConfigInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.30/#operation/ConfigInspect
|
||||
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourceOperationParameters := &resourceOperationParameters{
|
||||
resourceIdentifierAttribute: configObjectIdentifier,
|
||||
resourceType: portainer.ConfigResourceControl,
|
||||
labelsObjectSelector: selectorConfigLabels,
|
||||
}
|
||||
|
||||
return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor)
|
||||
}
|
||||
|
||||
// selectorConfigLabels retrieve the labels object associated to the config object.
|
||||
// Labels are available under the "Spec.Labels" property.
|
||||
// API schema references:
|
||||
// https://docs.docker.com/engine/api/v1.40/#operation/ConfigList
|
||||
// https://docs.docker.com/engine/api/v1.40/#operation/ConfigInspect
|
||||
func selectorConfigLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||
secretSpec := responseutils.GetJSONObject(responseObject, "Spec")
|
||||
if secretSpec != nil {
|
||||
secretLabelsObject := responseutils.GetJSONObject(secretSpec, "Labels")
|
||||
return secretLabelsObject
|
||||
}
|
||||
return nil
|
||||
}
|
146
api/http/proxy/factory/docker/containers.go
Normal file
146
api/http/proxy/factory/docker/containers.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
)
|
||||
|
||||
const (
|
||||
containerObjectIdentifier = "Id"
|
||||
)
|
||||
|
||||
func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client, containerID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
container, err := dockerClient.ContainerInspect(context.Background(), containerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swarmStackName := container.Config.Labels[resourceLabelForDockerSwarmStackName]
|
||||
if swarmStackName != "" {
|
||||
return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||
}
|
||||
|
||||
serviceName := container.Config.Labels[resourceLabelForDockerServiceID]
|
||||
if serviceName != "" {
|
||||
return portainer.GetResourceControlByResourceIDAndType(serviceName, portainer.ServiceResourceControl, resourceControls), nil
|
||||
}
|
||||
|
||||
composeStackName := container.Config.Labels[resourceLabelForDockerComposeStackName]
|
||||
if composeStackName != "" {
|
||||
return portainer.GetResourceControlByResourceIDAndType(composeStackName, portainer.StackResourceControl, resourceControls), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// containerListOperation extracts the response as a JSON array, loop through the containers array
|
||||
// decorate and/or filter the containers based on resource controls before rewriting the response.
|
||||
func (transport *Transport) containerListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// ContainerList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
||||
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourceOperationParameters := &resourceOperationParameters{
|
||||
resourceIdentifierAttribute: containerObjectIdentifier,
|
||||
resourceType: portainer.ContainerResourceControl,
|
||||
labelsObjectSelector: selectorContainerLabelsFromContainerListOperation,
|
||||
}
|
||||
|
||||
responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if executor.labelBlackList != nil {
|
||||
responseArray, err = filterContainersWithBlackListedLabels(responseArray, executor.labelBlackList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// containerInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the container based on resource control and either rewrite an access denied response or a decorated container.
|
||||
func (transport *Transport) containerInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
//ContainerInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
|
||||
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourceOperationParameters := &resourceOperationParameters{
|
||||
resourceIdentifierAttribute: containerObjectIdentifier,
|
||||
resourceType: portainer.ContainerResourceControl,
|
||||
labelsObjectSelector: selectorContainerLabelsFromContainerInspectOperation,
|
||||
}
|
||||
|
||||
return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor)
|
||||
}
|
||||
|
||||
// selectorContainerLabelsFromContainerInspectOperation retrieve the labels object associated to the container object.
|
||||
// This selector is specific to the containerInspect Docker operation.
|
||||
// Labels are available under the "Config.Labels" property.
|
||||
// API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
|
||||
func selectorContainerLabelsFromContainerInspectOperation(responseObject map[string]interface{}) map[string]interface{} {
|
||||
containerConfigObject := responseutils.GetJSONObject(responseObject, "Config")
|
||||
if containerConfigObject != nil {
|
||||
containerLabelsObject := responseutils.GetJSONObject(containerConfigObject, "Labels")
|
||||
return containerLabelsObject
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectorContainerLabelsFromContainerListOperation retrieve the labels object associated to the container object.
|
||||
// This selector is specific to the containerList Docker operation.
|
||||
// Labels are available under the "Labels" property.
|
||||
// API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
||||
func selectorContainerLabelsFromContainerListOperation(responseObject map[string]interface{}) map[string]interface{} {
|
||||
containerLabelsObject := responseutils.GetJSONObject(responseObject, "Labels")
|
||||
return containerLabelsObject
|
||||
}
|
||||
|
||||
// filterContainersWithLabels loops through a list of containers, and filters containers that do not contains
|
||||
// any labels in the labels black list.
|
||||
func filterContainersWithBlackListedLabels(containerData []interface{}, labelBlackList []portainer.Pair) ([]interface{}, error) {
|
||||
filteredContainerData := make([]interface{}, 0)
|
||||
|
||||
for _, container := range containerData {
|
||||
containerObject := container.(map[string]interface{})
|
||||
|
||||
containerLabels := selectorContainerLabelsFromContainerListOperation(containerObject)
|
||||
if containerLabels != nil {
|
||||
if !containerHasBlackListedLabel(containerLabels, labelBlackList) {
|
||||
filteredContainerData = append(filteredContainerData, containerObject)
|
||||
}
|
||||
} else {
|
||||
filteredContainerData = append(filteredContainerData, containerObject)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredContainerData, nil
|
||||
}
|
||||
|
||||
func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelBlackList []portainer.Pair) bool {
|
||||
for key, value := range containerLabels {
|
||||
labelName := key
|
||||
labelValue := value.(string)
|
||||
|
||||
for _, blackListedLabel := range labelBlackList {
|
||||
if blackListedLabel.Name == labelName && blackListedLabel.Value == labelValue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
101
api/http/proxy/factory/docker/networks.go
Normal file
101
api/http/proxy/factory/docker/networks.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
)
|
||||
|
||||
const (
|
||||
networkObjectIdentifier = "Id"
|
||||
networkObjectName = "Name"
|
||||
)
|
||||
|
||||
func getInheritedResourceControlFromNetworkLabels(dockerClient *client.Client, networkID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
network, err := dockerClient.NetworkInspect(context.Background(), networkID, types.NetworkInspectOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swarmStackName := network.Labels[resourceLabelForDockerSwarmStackName]
|
||||
if swarmStackName != "" {
|
||||
return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// networkListOperation extracts the response as a JSON object, loop through the networks array
|
||||
// decorate and/or filter the networks based on resource controls before rewriting the response.
|
||||
func (transport *Transport) networkListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// NetworkList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourceOperationParameters := &resourceOperationParameters{
|
||||
resourceIdentifierAttribute: networkObjectIdentifier,
|
||||
resourceType: portainer.NetworkResourceControl,
|
||||
labelsObjectSelector: selectorNetworkLabels,
|
||||
}
|
||||
|
||||
responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// networkInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the network based on resource control and either rewrite an access denied response or a decorated network.
|
||||
func (transport *Transport) networkInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// NetworkInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
|
||||
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourceOperationParameters := &resourceOperationParameters{
|
||||
resourceIdentifierAttribute: networkObjectIdentifier,
|
||||
resourceType: portainer.NetworkResourceControl,
|
||||
labelsObjectSelector: selectorNetworkLabels,
|
||||
}
|
||||
|
||||
return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor)
|
||||
}
|
||||
|
||||
// findSystemNetworkResourceControl will check if the network object is a system network
|
||||
// and will return a system resource control if that's the case.
|
||||
func findSystemNetworkResourceControl(networkObject map[string]interface{}) *portainer.ResourceControl {
|
||||
if networkObject[networkObjectName] == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
networkID := networkObject[networkObjectIdentifier].(string)
|
||||
networkName := networkObject[networkObjectName].(string)
|
||||
|
||||
if networkName == "bridge" || networkName == "host" || networkName == "none" {
|
||||
return portainer.NewSystemResourceControl(networkID, portainer.NetworkResourceControl)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectorNetworkLabels retrieve the labels object associated to the network object.
|
||||
// Labels are available under the "Labels" property.
|
||||
// API schema references:
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||
func selectorNetworkLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||
return responseutils.GetJSONObject(responseObject, "Labels")
|
||||
}
|
52
api/http/proxy/factory/docker/registry.go
Normal file
52
api/http/proxy/factory/docker/registry.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
type (
|
||||
registryAccessContext struct {
|
||||
isAdmin bool
|
||||
userID portainer.UserID
|
||||
teamMemberships []portainer.TeamMembership
|
||||
registries []portainer.Registry
|
||||
dockerHub *portainer.DockerHub
|
||||
}
|
||||
registryAuthenticationHeader struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Serveraddress string `json:"serveraddress"`
|
||||
}
|
||||
)
|
||||
|
||||
func createRegistryAuthenticationHeader(serverAddress string, accessContext *registryAccessContext) *registryAuthenticationHeader {
|
||||
var authenticationHeader *registryAuthenticationHeader
|
||||
|
||||
if serverAddress == "" {
|
||||
authenticationHeader = ®istryAuthenticationHeader{
|
||||
Username: accessContext.dockerHub.Username,
|
||||
Password: accessContext.dockerHub.Password,
|
||||
Serveraddress: "docker.io",
|
||||
}
|
||||
} else {
|
||||
var matchingRegistry *portainer.Registry
|
||||
for _, registry := range accessContext.registries {
|
||||
if registry.URL == serverAddress &&
|
||||
(accessContext.isAdmin || (!accessContext.isAdmin && security.AuthorizedRegistryAccess(®istry, accessContext.userID, accessContext.teamMemberships))) {
|
||||
matchingRegistry = ®istry
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matchingRegistry != nil {
|
||||
authenticationHeader = ®istryAuthenticationHeader{
|
||||
Username: matchingRegistry.Username,
|
||||
Password: matchingRegistry.Password,
|
||||
Serveraddress: matchingRegistry.URL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return authenticationHeader
|
||||
}
|
87
api/http/proxy/factory/docker/secrets.go
Normal file
87
api/http/proxy/factory/docker/secrets.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
const (
|
||||
secretObjectIdentifier = "ID"
|
||||
)
|
||||
|
||||
func getInheritedResourceControlFromSecretLabels(dockerClient *client.Client, secretID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
secret, _, err := dockerClient.SecretInspectWithRaw(context.Background(), secretID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swarmStackName := secret.Spec.Labels[resourceLabelForDockerSwarmStackName]
|
||||
if swarmStackName != "" {
|
||||
return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// secretListOperation extracts the response as a JSON object, loop through the secrets array
|
||||
// decorate and/or filter the secrets based on resource controls before rewriting the response.
|
||||
func (transport *Transport) secretListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// SecretList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/SecretList
|
||||
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourceOperationParameters := &resourceOperationParameters{
|
||||
resourceIdentifierAttribute: secretObjectIdentifier,
|
||||
resourceType: portainer.SecretResourceControl,
|
||||
labelsObjectSelector: selectorSecretLabels,
|
||||
}
|
||||
|
||||
responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// secretInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the secret based on resource control and either rewrite an access denied response or a decorated secret.
|
||||
func (transport *Transport) secretInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// SecretInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/SecretInspect
|
||||
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourceOperationParameters := &resourceOperationParameters{
|
||||
resourceIdentifierAttribute: secretObjectIdentifier,
|
||||
resourceType: portainer.SecretResourceControl,
|
||||
labelsObjectSelector: selectorSecretLabels,
|
||||
}
|
||||
|
||||
return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor)
|
||||
}
|
||||
|
||||
// selectorSecretLabels retrieve the labels object associated to the secret object.
|
||||
// Labels are available under the "Spec.Labels" property.
|
||||
// API schema references:
|
||||
// https://docs.docker.com/engine/api/v1.40/#operation/SecretList
|
||||
// https://docs.docker.com/engine/api/v1.40/#operation/SecretInspect
|
||||
func selectorSecretLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||
secretSpec := responseutils.GetJSONObject(responseObject, "Spec")
|
||||
if secretSpec != nil {
|
||||
secretLabelsObject := responseutils.GetJSONObject(secretSpec, "Labels")
|
||||
return secretLabelsObject
|
||||
}
|
||||
return nil
|
||||
}
|
86
api/http/proxy/factory/docker/services.go
Normal file
86
api/http/proxy/factory/docker/services.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
)
|
||||
|
||||
const (
|
||||
serviceObjectIdentifier = "ID"
|
||||
)
|
||||
|
||||
func getInheritedResourceControlFromServiceLabels(dockerClient *client.Client, serviceID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
service, _, err := dockerClient.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swarmStackName := service.Spec.Labels[resourceLabelForDockerSwarmStackName]
|
||||
if swarmStackName != "" {
|
||||
return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// serviceListOperation extracts the response as a JSON array, loop through the service array
|
||||
// decorate and/or filter the services based on resource controls before rewriting the response.
|
||||
func (transport *Transport) serviceListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// ServiceList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
||||
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourceOperationParameters := &resourceOperationParameters{
|
||||
resourceIdentifierAttribute: serviceObjectIdentifier,
|
||||
resourceType: portainer.ServiceResourceControl,
|
||||
labelsObjectSelector: selectorServiceLabels,
|
||||
}
|
||||
|
||||
responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// serviceInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the service based on resource control and either rewrite an access denied response or a decorated service.
|
||||
func (transport *Transport) serviceInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
//ServiceInspect response is a JSON object
|
||||
//https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
|
||||
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourceOperationParameters := &resourceOperationParameters{
|
||||
resourceIdentifierAttribute: serviceObjectIdentifier,
|
||||
resourceType: portainer.ServiceResourceControl,
|
||||
labelsObjectSelector: selectorServiceLabels,
|
||||
}
|
||||
|
||||
return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor)
|
||||
}
|
||||
|
||||
// selectorServiceLabels retrieve the labels object associated to the service object.
|
||||
// Labels are available under the "Spec.Labels" property.
|
||||
// API schema references:
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
||||
func selectorServiceLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||
serviceSpecObject := responseutils.GetJSONObject(responseObject, "Spec")
|
||||
if serviceSpecObject != nil {
|
||||
return responseutils.GetJSONObject(serviceSpecObject, "Labels")
|
||||
}
|
||||
return nil
|
||||
}
|
25
api/http/proxy/factory/docker/swarm.go
Normal file
25
api/http/proxy/factory/docker/swarm.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
)
|
||||
|
||||
// swarmInspectOperation extracts the response as a JSON object and rewrites the response based
|
||||
// on the current user role. Sensitive fields are deleted from the response for non-administrator users.
|
||||
func swarmInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// SwarmInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.30/#operation/SwarmInspect
|
||||
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !executor.operationContext.isAdmin {
|
||||
delete(responseObject, "JoinTokens")
|
||||
delete(responseObject, "TLSInfo")
|
||||
}
|
||||
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
50
api/http/proxy/factory/docker/tasks.go
Normal file
50
api/http/proxy/factory/docker/tasks.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
)
|
||||
|
||||
const (
|
||||
taskServiceObjectIdentifier = "ServiceID"
|
||||
)
|
||||
|
||||
// taskListOperation extracts the response as a JSON array, loop through the tasks array
|
||||
// and filter the containers based on resource controls before rewriting the response.
|
||||
func (transport *Transport) taskListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// TaskList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
||||
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourceOperationParameters := &resourceOperationParameters{
|
||||
resourceIdentifierAttribute: taskServiceObjectIdentifier,
|
||||
resourceType: portainer.ServiceResourceControl,
|
||||
labelsObjectSelector: selectorTaskLabels,
|
||||
}
|
||||
|
||||
responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// selectorServiceLabels retrieve the labels object associated to the task object.
|
||||
// Labels are available under the "Spec.ContainerSpec.Labels" property.
|
||||
// API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
||||
func selectorTaskLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||
taskSpecObject := responseutils.GetJSONObject(responseObject, "Spec")
|
||||
if taskSpecObject != nil {
|
||||
containerSpecObject := responseutils.GetJSONObject(taskSpecObject, "ContainerSpec")
|
||||
if containerSpecObject != nil {
|
||||
return responseutils.GetJSONObject(containerSpecObject, "Labels")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
725
api/http/proxy/factory/docker/transport.go
Normal file
725
api/http/proxy/factory/docker/transport.go
Normal file
|
@ -0,0 +1,725 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
var apiVersionRe = regexp.MustCompile(`(/v[0-9]\.[0-9]*)?`)
|
||||
|
||||
type (
|
||||
// Transport is a custom transport for Docker API reverse proxy. It allows
|
||||
// interception of requests and rewriting of responses.
|
||||
Transport struct {
|
||||
HTTPTransport *http.Transport
|
||||
endpoint *portainer.Endpoint
|
||||
resourceControlService portainer.ResourceControlService
|
||||
userService portainer.UserService
|
||||
teamService portainer.TeamService
|
||||
teamMembershipService portainer.TeamMembershipService
|
||||
registryService portainer.RegistryService
|
||||
dockerHubService portainer.DockerHubService
|
||||
settingsService portainer.SettingsService
|
||||
signatureService portainer.DigitalSignatureService
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
extensionService portainer.ExtensionService
|
||||
dockerClient *client.Client
|
||||
dockerClientFactory *docker.ClientFactory
|
||||
}
|
||||
|
||||
// TransportParameters is used to create a new Transport
|
||||
TransportParameters struct {
|
||||
Endpoint *portainer.Endpoint
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
UserService portainer.UserService
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
RegistryService portainer.RegistryService
|
||||
DockerHubService portainer.DockerHubService
|
||||
SettingsService portainer.SettingsService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
ExtensionService portainer.ExtensionService
|
||||
DockerClientFactory *docker.ClientFactory
|
||||
}
|
||||
|
||||
restrictedDockerOperationContext struct {
|
||||
isAdmin bool
|
||||
endpointResourceAccess bool
|
||||
userID portainer.UserID
|
||||
userTeamIDs []portainer.TeamID
|
||||
resourceControls []portainer.ResourceControl
|
||||
}
|
||||
|
||||
operationExecutor struct {
|
||||
operationContext *restrictedDockerOperationContext
|
||||
labelBlackList []portainer.Pair
|
||||
}
|
||||
restrictedOperationRequest func(*http.Response, *operationExecutor) error
|
||||
operationRequest func(*http.Request) error
|
||||
)
|
||||
|
||||
// NewTransport returns a pointer to a new Transport instance.
|
||||
func NewTransport(parameters *TransportParameters, httpTransport *http.Transport) (*Transport, error) {
|
||||
dockerClient, err := parameters.DockerClientFactory.CreateClient(parameters.Endpoint, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transport := &Transport{
|
||||
endpoint: parameters.Endpoint,
|
||||
resourceControlService: parameters.ResourceControlService,
|
||||
userService: parameters.UserService,
|
||||
teamService: parameters.TeamService,
|
||||
teamMembershipService: parameters.TeamMembershipService,
|
||||
registryService: parameters.RegistryService,
|
||||
dockerHubService: parameters.DockerHubService,
|
||||
settingsService: parameters.SettingsService,
|
||||
signatureService: parameters.SignatureService,
|
||||
reverseTunnelService: parameters.ReverseTunnelService,
|
||||
extensionService: parameters.ExtensionService,
|
||||
dockerClientFactory: parameters.DockerClientFactory,
|
||||
HTTPTransport: httpTransport,
|
||||
dockerClient: dockerClient,
|
||||
}
|
||||
|
||||
return transport, nil
|
||||
}
|
||||
|
||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||
func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return transport.ProxyDockerRequest(request)
|
||||
}
|
||||
|
||||
// ProxyDockerRequest intercepts a Docker API request and apply logic based
|
||||
// on the requested operation.
|
||||
func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Response, error) {
|
||||
requestPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")
|
||||
request.URL.Path = requestPath
|
||||
|
||||
if transport.endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey())
|
||||
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(requestPath, "/configs"):
|
||||
return transport.proxyConfigRequest(request)
|
||||
case strings.HasPrefix(requestPath, "/containers"):
|
||||
return transport.proxyContainerRequest(request)
|
||||
case strings.HasPrefix(requestPath, "/services"):
|
||||
return transport.proxyServiceRequest(request)
|
||||
case strings.HasPrefix(requestPath, "/volumes"):
|
||||
return transport.proxyVolumeRequest(request)
|
||||
case strings.HasPrefix(requestPath, "/networks"):
|
||||
return transport.proxyNetworkRequest(request)
|
||||
case strings.HasPrefix(requestPath, "/secrets"):
|
||||
return transport.proxySecretRequest(request)
|
||||
case strings.HasPrefix(requestPath, "/swarm"):
|
||||
return transport.proxySwarmRequest(request)
|
||||
case strings.HasPrefix(requestPath, "/nodes"):
|
||||
return transport.proxyNodeRequest(request)
|
||||
case strings.HasPrefix(requestPath, "/tasks"):
|
||||
return transport.proxyTaskRequest(request)
|
||||
case strings.HasPrefix(requestPath, "/build"):
|
||||
return transport.proxyBuildRequest(request)
|
||||
case strings.HasPrefix(requestPath, "/images"):
|
||||
return transport.proxyImageRequest(request)
|
||||
case strings.HasPrefix(requestPath, "/v2"):
|
||||
return transport.proxyAgentRequest(request)
|
||||
default:
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
func (transport *Transport) executeDockerRequest(request *http.Request) (*http.Response, error) {
|
||||
response, err := transport.HTTPTransport.RoundTrip(request)
|
||||
|
||||
if transport.endpoint.Type != portainer.EdgeAgentEnvironment {
|
||||
return response, err
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpoint.ID)
|
||||
} else {
|
||||
transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpoint.ID)
|
||||
}
|
||||
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response, error) {
|
||||
requestPath := strings.TrimPrefix(r.URL.Path, "/v2")
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(requestPath, "/browse"):
|
||||
volumeIDParameter, found := r.URL.Query()["volumeID"]
|
||||
if !found || len(volumeIDParameter) < 1 {
|
||||
return transport.administratorOperation(r)
|
||||
}
|
||||
|
||||
return transport.restrictedResourceOperation(r, volumeIDParameter[0], portainer.VolumeResourceControl, true)
|
||||
}
|
||||
|
||||
return transport.executeDockerRequest(r)
|
||||
}
|
||||
|
||||
func (transport *Transport) proxyConfigRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/configs/create":
|
||||
return transport.decorateGenericResourceCreationOperation(request, configObjectIdentifier, portainer.ConfigResourceControl)
|
||||
|
||||
case "/configs":
|
||||
return transport.rewriteOperation(request, transport.configListOperation)
|
||||
|
||||
default:
|
||||
// assume /configs/{id}
|
||||
configID := path.Base(requestPath)
|
||||
|
||||
if request.Method == http.MethodGet {
|
||||
return transport.rewriteOperation(request, transport.configInspectOperation)
|
||||
} else if request.Method == http.MethodDelete {
|
||||
return transport.executeGenericResourceDeletionOperation(request, configID, portainer.ConfigResourceControl)
|
||||
}
|
||||
|
||||
return transport.restrictedResourceOperation(request, configID, portainer.ConfigResourceControl, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/containers/create":
|
||||
return transport.decorateGenericResourceCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl)
|
||||
|
||||
case "/containers/prune":
|
||||
return transport.administratorOperation(request)
|
||||
|
||||
case "/containers/json":
|
||||
return transport.rewriteOperationWithLabelFiltering(request, transport.containerListOperation)
|
||||
|
||||
default:
|
||||
// This section assumes /containers/**
|
||||
if match, _ := path.Match("/containers/*/*", requestPath); match {
|
||||
// Handle /containers/{id}/{action} requests
|
||||
containerID := path.Base(path.Dir(requestPath))
|
||||
action := path.Base(requestPath)
|
||||
|
||||
if action == "json" {
|
||||
return transport.rewriteOperation(request, transport.containerInspectOperation)
|
||||
}
|
||||
return transport.restrictedResourceOperation(request, containerID, portainer.ContainerResourceControl, false)
|
||||
} else if match, _ := path.Match("/containers/*", requestPath); match {
|
||||
// Handle /containers/{id} requests
|
||||
containerID := path.Base(requestPath)
|
||||
|
||||
if request.Method == http.MethodDelete {
|
||||
return transport.executeGenericResourceDeletionOperation(request, containerID, portainer.ContainerResourceControl)
|
||||
}
|
||||
|
||||
return transport.restrictedResourceOperation(request, containerID, portainer.ContainerResourceControl, false)
|
||||
}
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/services/create":
|
||||
return transport.replaceRegistryAuthenticationHeader(request)
|
||||
|
||||
case "/services":
|
||||
return transport.rewriteOperation(request, transport.serviceListOperation)
|
||||
|
||||
default:
|
||||
// This section assumes /services/**
|
||||
if match, _ := path.Match("/services/*/*", requestPath); match {
|
||||
// Handle /services/{id}/{action} requests
|
||||
serviceID := path.Base(path.Dir(requestPath))
|
||||
return transport.restrictedResourceOperation(request, serviceID, portainer.ServiceResourceControl, false)
|
||||
} else if match, _ := path.Match("/services/*", requestPath); match {
|
||||
// Handle /services/{id} requests
|
||||
serviceID := path.Base(requestPath)
|
||||
|
||||
switch request.Method {
|
||||
case http.MethodGet:
|
||||
return transport.rewriteOperation(request, transport.serviceInspectOperation)
|
||||
case http.MethodDelete:
|
||||
return transport.executeGenericResourceDeletionOperation(request, serviceID, portainer.ServiceResourceControl)
|
||||
}
|
||||
return transport.restrictedResourceOperation(request, serviceID, portainer.ServiceResourceControl, false)
|
||||
}
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
func (transport *Transport) proxyVolumeRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/volumes/create":
|
||||
return transport.decorateGenericResourceCreationOperation(request, volumeObjectIdentifier, portainer.VolumeResourceControl)
|
||||
|
||||
case "/volumes/prune":
|
||||
return transport.administratorOperation(request)
|
||||
|
||||
case "/volumes":
|
||||
return transport.rewriteOperation(request, transport.volumeListOperation)
|
||||
|
||||
default:
|
||||
// assume /volumes/{name}
|
||||
volumeID := path.Base(requestPath)
|
||||
|
||||
if request.Method == http.MethodGet {
|
||||
return transport.rewriteOperation(request, transport.volumeInspectOperation)
|
||||
} else if request.Method == http.MethodDelete {
|
||||
return transport.executeGenericResourceDeletionOperation(request, volumeID, portainer.VolumeResourceControl)
|
||||
}
|
||||
return transport.restrictedResourceOperation(request, volumeID, portainer.VolumeResourceControl, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (transport *Transport) proxyNetworkRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/networks/create":
|
||||
return transport.decorateGenericResourceCreationOperation(request, networkObjectIdentifier, portainer.NetworkResourceControl)
|
||||
|
||||
case "/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, portainer.NetworkResourceControl)
|
||||
}
|
||||
return transport.restrictedResourceOperation(request, networkID, portainer.NetworkResourceControl, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (transport *Transport) proxySecretRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/secrets/create":
|
||||
return transport.decorateGenericResourceCreationOperation(request, secretObjectIdentifier, portainer.SecretResourceControl)
|
||||
|
||||
case "/secrets":
|
||||
return transport.rewriteOperation(request, transport.secretListOperation)
|
||||
|
||||
default:
|
||||
// assume /secrets/{id}
|
||||
secretID := path.Base(requestPath)
|
||||
|
||||
if request.Method == http.MethodGet {
|
||||
return transport.rewriteOperation(request, transport.secretInspectOperation)
|
||||
} else if request.Method == http.MethodDelete {
|
||||
return transport.executeGenericResourceDeletionOperation(request, secretID, portainer.SecretResourceControl)
|
||||
}
|
||||
return transport.restrictedResourceOperation(request, secretID, portainer.SecretResourceControl, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (transport *Transport) proxyNodeRequest(request *http.Request) (*http.Response, error) {
|
||||
requestPath := request.URL.Path
|
||||
|
||||
// assume /nodes/{id}
|
||||
if path.Base(requestPath) != "nodes" {
|
||||
return transport.administratorOperation(request)
|
||||
}
|
||||
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
func (transport *Transport) proxySwarmRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/swarm":
|
||||
return transport.rewriteOperation(request, swarmInspectOperation)
|
||||
default:
|
||||
// assume /swarm/{action}
|
||||
return transport.administratorOperation(request)
|
||||
}
|
||||
}
|
||||
|
||||
func (transport *Transport) proxyTaskRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/tasks":
|
||||
return transport.rewriteOperation(request, transport.taskListOperation)
|
||||
default:
|
||||
// assume /tasks/{id}
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
func (transport *Transport) proxyBuildRequest(request *http.Request) (*http.Response, error) {
|
||||
return transport.interceptAndRewriteRequest(request, buildOperation)
|
||||
}
|
||||
|
||||
func (transport *Transport) proxyImageRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/images/create":
|
||||
return transport.replaceRegistryAuthenticationHeader(request)
|
||||
default:
|
||||
if path.Base(requestPath) == "push" && request.Method == http.MethodPost {
|
||||
return transport.replaceRegistryAuthenticationHeader(request)
|
||||
}
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
func (transport *Transport) replaceRegistryAuthenticationHeader(request *http.Request) (*http.Response, error) {
|
||||
accessContext, err := transport.createRegistryAccessContext(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
originalHeader := request.Header.Get("X-Registry-Auth")
|
||||
|
||||
if originalHeader != "" {
|
||||
|
||||
decodedHeaderData, err := base64.StdEncoding.DecodeString(originalHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var originalHeaderData registryAuthenticationHeader
|
||||
err = json.Unmarshal(decodedHeaderData, &originalHeaderData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authenticationHeader := createRegistryAuthenticationHeader(originalHeaderData.Serveraddress, accessContext)
|
||||
|
||||
headerData, err := json.Marshal(authenticationHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
header := base64.StdEncoding.EncodeToString(headerData)
|
||||
|
||||
request.Header.Set("X-Registry-Auth", header)
|
||||
}
|
||||
|
||||
return transport.decorateGenericResourceCreationOperation(request, serviceObjectIdentifier, portainer.ServiceResourceControl)
|
||||
}
|
||||
|
||||
func (transport *Transport) restrictedResourceOperation(request *http.Request, resourceID string, resourceType portainer.ResourceControlType, volumeBrowseRestrictionCheck bool) (*http.Response, error) {
|
||||
var err error
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
rbacExtension, err := transport.extensionService.Extension(portainer.RBACExtension)
|
||||
if err != nil && err != portainer.ErrObjectNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if volumeBrowseRestrictionCheck {
|
||||
settings, err := transport.settingsService.Settings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rbacExtension != nil && !settings.AllowVolumeBrowserForRegularUsers {
|
||||
return responseutils.WriteAccessDeniedResponse()
|
||||
}
|
||||
}
|
||||
|
||||
user, err := transport.userService.User(tokenData.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpointResourceAccess := false
|
||||
_, ok := user.EndpointAuthorizations[transport.endpoint.ID][portainer.EndpointResourcesAccess]
|
||||
if ok {
|
||||
endpointResourceAccess = true
|
||||
}
|
||||
|
||||
if rbacExtension != nil && endpointResourceAccess {
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
teamMemberships, err := transport.teamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userTeamIDs := make([]portainer.TeamID, 0)
|
||||
for _, membership := range teamMemberships {
|
||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||
}
|
||||
|
||||
resourceControls, err := transport.resourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resourceControl := portainer.GetResourceControlByResourceIDAndType(resourceID, resourceType, resourceControls)
|
||||
if resourceControl == nil {
|
||||
agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader)
|
||||
|
||||
// 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(resourceID, agentTargetHeader, resourceType, resourceControls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if inheritedResourceControl == nil || !portainer.UserCanAccessResource(tokenData.ID, userTeamIDs, inheritedResourceControl) {
|
||||
return responseutils.WriteAccessDeniedResponse()
|
||||
}
|
||||
}
|
||||
|
||||
if resourceControl != nil && !portainer.UserCanAccessResource(tokenData.ID, userTeamIDs, resourceControl) {
|
||||
return responseutils.WriteAccessDeniedResponse()
|
||||
}
|
||||
}
|
||||
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (transport *Transport) rewriteOperationWithLabelFiltering(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
|
||||
operationContext, err := transport.createOperationContext(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
settings, err := transport.settingsService.Settings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
executor := &operationExecutor{
|
||||
operationContext: operationContext,
|
||||
labelBlackList: settings.BlackListedLabels,
|
||||
}
|
||||
|
||||
return transport.executeRequestAndRewriteResponse(request, operation, executor)
|
||||
}
|
||||
|
||||
// rewriteOperation will create a new operation context with data that will be used
|
||||
// to decorate the original request's response.
|
||||
func (transport *Transport) rewriteOperation(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
|
||||
operationContext, err := transport.createOperationContext(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
executor := &operationExecutor{
|
||||
operationContext: operationContext,
|
||||
}
|
||||
|
||||
return transport.executeRequestAndRewriteResponse(request, operation, executor)
|
||||
}
|
||||
|
||||
func (transport *Transport) interceptAndRewriteRequest(request *http.Request, operation operationRequest) (*http.Response, error) {
|
||||
err := operation(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
// decorateGenericResourceCreationResponse extracts the response as a JSON object, extracts the resource identifier from that object based
|
||||
// on the resourceIdentifierAttribute parameter then generate a new resource control associated to that resource
|
||||
// with a random token and rewrites the response by decorating the original response with a ResourceControl object.
|
||||
// The generic Docker API response format is JSON object:
|
||||
// https://docs.docker.com/engine/api/v1.40/#operation/ContainerCreate
|
||||
// https://docs.docker.com/engine/api/v1.40/#operation/NetworkCreate
|
||||
// https://docs.docker.com/engine/api/v1.40/#operation/VolumeCreate
|
||||
// https://docs.docker.com/engine/api/v1.40/#operation/ServiceCreate
|
||||
// https://docs.docker.com/engine/api/v1.40/#operation/SecretCreate
|
||||
// https://docs.docker.com/engine/api/v1.40/#operation/ConfigCreate
|
||||
func (transport *Transport) decorateGenericResourceCreationResponse(response *http.Response, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType, userID portainer.UserID) error {
|
||||
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if responseObject[resourceIdentifierAttribute] == nil {
|
||||
log.Printf("[ERROR] [proxy,docker]")
|
||||
return errors.New("missing identifier in Docker resource creation response")
|
||||
}
|
||||
|
||||
resourceID := responseObject[resourceIdentifierAttribute].(string)
|
||||
|
||||
resourceControl, err := transport.createPrivateResourceControl(resourceID, resourceType, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
func (transport *Transport) decorateGenericResourceCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := transport.executeDockerRequest(request)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
if response.StatusCode == http.StatusCreated {
|
||||
err = transport.decorateGenericResourceCreationResponse(response, resourceIdentifierAttribute, resourceType, tokenData.ID)
|
||||
}
|
||||
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (transport *Transport) executeGenericResourceDeletionOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
|
||||
response, err := transport.restrictedResourceOperation(request, resourceIdentifierAttribute, resourceType, false)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
resourceControl, err := transport.resourceControlService.ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
err = transport.resourceControlService.DeleteResourceControl(resourceControl.ID)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
}
|
||||
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (transport *Transport) executeRequestAndRewriteResponse(request *http.Request, operation restrictedOperationRequest, executor *operationExecutor) (*http.Response, error) {
|
||||
response, err := transport.executeDockerRequest(request)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
err = operation(response, executor)
|
||||
return response, err
|
||||
}
|
||||
|
||||
// administratorOperation ensures that the user has administrator privileges
|
||||
// before executing the original request.
|
||||
func (transport *Transport) administratorOperation(request *http.Request) (*http.Response, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
return responseutils.WriteAccessDeniedResponse()
|
||||
}
|
||||
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
func (transport *Transport) createRegistryAccessContext(request *http.Request) (*registryAccessContext, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessContext := ®istryAccessContext{
|
||||
isAdmin: true,
|
||||
userID: tokenData.ID,
|
||||
}
|
||||
|
||||
hub, err := transport.dockerHubService.DockerHub()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessContext.dockerHub = hub
|
||||
|
||||
registries, err := transport.registryService.Registries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessContext.registries = registries
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
accessContext.isAdmin = false
|
||||
|
||||
teamMemberships, err := transport.teamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessContext.teamMemberships = teamMemberships
|
||||
}
|
||||
|
||||
return accessContext, nil
|
||||
}
|
||||
|
||||
func (transport *Transport) createOperationContext(request *http.Request) (*restrictedDockerOperationContext, error) {
|
||||
var err error
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resourceControls, err := transport.resourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
operationContext := &restrictedDockerOperationContext{
|
||||
isAdmin: true,
|
||||
userID: tokenData.ID,
|
||||
resourceControls: resourceControls,
|
||||
endpointResourceAccess: false,
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
operationContext.isAdmin = false
|
||||
|
||||
user, err := transport.userService.User(operationContext.userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, ok := user.EndpointAuthorizations[transport.endpoint.ID][portainer.EndpointResourcesAccess]
|
||||
if ok {
|
||||
operationContext.endpointResourceAccess = true
|
||||
}
|
||||
|
||||
teamMemberships, err := transport.teamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userTeamIDs := make([]portainer.TeamID, 0)
|
||||
for _, membership := range teamMemberships {
|
||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||
}
|
||||
operationContext.userTeamIDs = userTeamIDs
|
||||
}
|
||||
|
||||
return operationContext, nil
|
||||
}
|
89
api/http/proxy/factory/docker/volumes.go
Normal file
89
api/http/proxy/factory/docker/volumes.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
)
|
||||
|
||||
const (
|
||||
volumeObjectIdentifier = "Name"
|
||||
)
|
||||
|
||||
func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, volumeID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
volume, err := dockerClient.VolumeInspect(context.Background(), volumeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
swarmStackName := volume.Labels[resourceLabelForDockerSwarmStackName]
|
||||
if swarmStackName != "" {
|
||||
return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// volumeListOperation extracts the response as a JSON object, loop through the volume array
|
||||
// decorate and/or filter the volumes based on resource controls before rewriting the response.
|
||||
func (transport *Transport) volumeListOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// VolumeList response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
||||
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The "Volumes" field contains the list of volumes as an array of JSON objects
|
||||
if responseObject["Volumes"] != nil {
|
||||
volumeData := responseObject["Volumes"].([]interface{})
|
||||
|
||||
resourceOperationParameters := &resourceOperationParameters{
|
||||
resourceIdentifierAttribute: volumeObjectIdentifier,
|
||||
resourceType: portainer.VolumeResourceControl,
|
||||
labelsObjectSelector: selectorVolumeLabels,
|
||||
}
|
||||
|
||||
volumeData, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, volumeData, executor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Overwrite the original volume list
|
||||
responseObject["Volumes"] = volumeData
|
||||
}
|
||||
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
// volumeInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the volume based on any existing resource control and either rewrite an access denied response or a decorated volume.
|
||||
func (transport *Transport) volumeInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// VolumeInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
|
||||
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourceOperationParameters := &resourceOperationParameters{
|
||||
resourceIdentifierAttribute: volumeObjectIdentifier,
|
||||
resourceType: portainer.VolumeResourceControl,
|
||||
labelsObjectSelector: selectorVolumeLabels,
|
||||
}
|
||||
|
||||
return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor)
|
||||
}
|
||||
|
||||
// selectorVolumeLabels retrieve the labels object associated to the volume object.
|
||||
// Labels are available under the "Labels" property.
|
||||
// API schema references:
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
||||
func selectorVolumeLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||
return responseutils.GetJSONObject(responseObject, "Labels")
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue