Files
portainer/api/http/proxy/factory/docker/transport.go
T
Chaim Lev-Ari 99a372fb88 feat(useractivity): log user activity for write actions (#229)
* feat(useractivity): introduce backend for useractivity logging (#213)

* refactor(useractivity): move query and logs to base type

* feat(useractivity): cleanup user activity logs

* feat(useractivity): log an activity

* refactor(useractivity): create generic get logs function

* fix(api): hide unused function

* refactor(useractivity): create generic get logs function

* feat(useractivity): get user activity logs

* feat(http/ua): add http get logs handler

* refactor(http/ua): rename logs_list file

* feat(useractivity): fetch logs as csv

* feat(useractivity): save payload as bytes

* style(useractivity): doc the count parameter

* feat(useractivity): introduce UI for user activity logs (#220)

* feat(useractivity): add useractivity page

* feat(useractivity): get logs from server

* feat(useractivity): show logs in datatable

* fix(useractivity): save logs as csv

* feat(useractivity): show logs payload

* feat(useractivity): sort desc by default

* feat(useractivity): parse object

* fix(useractivity): expect base64 payload

* feat(useractivity): show message when missing logs

* feat(useractivity): log api (#215)

* feat(templates): log write methods

* refactor(useractivity): move middleware

* feat(dockerhub): log update docker settings

* feat(edgegroup): log write

* feat(edgejobs): log write request

* feat(useractivity): return bytes to user

* fix(customtemplates): set activity context

* feat(edgestacks): log activities

* feat(endpointgroup): log activities

* feat(endpoint): log write activities

* feat(licenses): log write activities

* feat(registries): log activitites

* feat(resource_control): log user activity

* feat(settings): log update

* feat(stacks): log activity

* feat(tags): log user activitiy

* feat(teammembership): log user activity

* feat(teams): log write activities

* feat(useractivity): get default context

* feat(http/upload): log upload tls

* feat(users): log user activities

* fix(settings): clean payload

* feat(webhook): log user activities

* feat(websocket): log activities

* feat(docker): log write activities

* refactor(useractivity): move log proxy

* feat(azure): log write activity

* refactor(kube): use basic transport for all transports

* feat(kube): log kube activity

* fix(useractivity): parse body

* refactor(kuberenetes): log requests only if success

* refactor(docker): log requests only if success

* refactor(azure): log requests only if success

* feat(gitlab): log activity

* feat(registries): log proxy request

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>

* feat(activity-logs): save pagination limit

* feat(useractivity): remove config payload

* fix(docker): log request after success

* refactor(http): move copy body to utils

* feat(kuberentes): remove config values

* feat(useractivity): copy body before request

* fix(useractivity): fix column size

* feat(useractivity): filter json payloads

* refactor(useractivity): log with same logic

* fix(useractivity/csv): export same columns as datatable

* fix(useractivity): replace context with endpoint

* fix(user-activity): rename tables

* feat(endpoint): clear azure key

* feat(stacks): omit empty migrate values

* fix(stacks): add back import

* feat(endpoints): log update settings

* fix(registry): clear password value

* feat(registry): omit update empty value

* fix(registries): don't return from unauthorized azure request

* fix(useractivity): log any payload similar to json

* feat(useractivity): ignoer binary upload

* fix(useractivity): refresh user activity logs

* feat(useractivity): use [REDACTED] for cleared credential (#265)

* feat(docker/services): log force update service

* feat(useractivity): log username when available

* feat(webhooks): remove logging of execute

* refactor(http): replace redacted values

* style(kube): remove commented code

* feat(http/kube): proxy local requests

* feat(useractivity): log patch method

* fix(datatables): use unique filter id

* fix kube settings update

* fix: EE-527 set payload to [REDACTED] when update kube config

* refactor(http/k8s): rename proxy function

* EE-530: a dummy fix of exec activity log for a local kube setup

Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>
Co-authored-by: Hui <arris_li@hotmail.com>
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-04-15 20:37:29 +12:00

754 lines
26 KiB
Go

package docker
import (
"encoding/base64"
"encoding/json"
"log"
"net/http"
"path"
"regexp"
"strings"
"github.com/pkg/errors"
"github.com/docker/docker/client"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/http/useractivity"
"github.com/portainer/portainer/api/http/utils"
"github.com/portainer/portainer/api/internal/authorization"
)
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
dataStore portainer.DataStore
signatureService portainer.DigitalSignatureService
reverseTunnelService portainer.ReverseTunnelService
dockerClient *client.Client
dockerClientFactory *docker.ClientFactory
userActivityStore portainer.UserActivityStore
}
// TransportParameters is used to create a new Transport
TransportParameters struct {
Endpoint *portainer.Endpoint
DataStore portainer.DataStore
SignatureService portainer.DigitalSignatureService
ReverseTunnelService portainer.ReverseTunnelService
DockerClientFactory *docker.ClientFactory
UserActivityStore portainer.UserActivityStore
}
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,
dataStore: parameters.DataStore,
signatureService: parameters.SignatureService,
reverseTunnelService: parameters.ReverseTunnelService,
dockerClientFactory: parameters.DockerClientFactory,
HTTPTransport: httpTransport,
dockerClient: dockerClient,
userActivityStore: parameters.UserActivityStore,
}
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, true)
}
}
func (transport *Transport) executeDockerRequest(request *http.Request, shouldLog bool) (*http.Response, error) {
var body []byte
if shouldLog {
bodyBytes, err := utils.CopyBody(request)
if err != nil {
return nil, err
}
body = bodyBytes
}
response, err := transport.HTTPTransport.RoundTrip(request)
// log if request is success
if shouldLog && err == nil && (200 <= response.StatusCode && response.StatusCode < 300) {
useractivity.LogProxyActivity(transport.userActivityStore, transport.endpoint.Name, request, body)
}
if transport.endpoint.Type != portainer.EdgeAgentOnDockerEnvironment {
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"):
// host file browser request
volumeIDParameter, found := r.URL.Query()["volumeID"]
if !found || len(volumeIDParameter) < 1 {
return transport.administratorOperation(r)
}
agentTargetHeader := r.Header.Get(portainer.PortainerAgentTargetHeader)
resourceID, err := transport.getVolumeResourceID(agentTargetHeader, volumeIDParameter[0])
if err != nil {
return nil, err
}
// volume browser request
return transport.restrictedResourceOperation(r, resourceID, portainer.VolumeResourceControl, true)
}
return transport.executeDockerRequest(r, true)
}
func (transport *Transport) proxyConfigRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/configs/create":
return transport.decorateConfigCreationOperation(request)
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.decorateContainerCreationOperation(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, true)
}
}
func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/services/create":
return transport.decorateServiceCreationOperation(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, true)
}
}
func (transport *Transport) proxyVolumeRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/volumes/create":
return transport.decorateVolumeResourceCreationOperation(request, volumeObjectIdentifier, portainer.VolumeResourceControl)
case "/volumes/prune":
return transport.administratorOperation(request)
case "/volumes":
return transport.rewriteOperation(request, transport.volumeListOperation)
default:
// assume /volumes/{name}
return transport.restrictedVolumeOperation(requestPath, request)
}
}
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, true)
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.decorateSecretCreationOperation(request)
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, true)
}
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, true)
}
}
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, true)
}
}
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, true)
}
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 {
user, err := transport.dataStore.User().User(tokenData.ID)
if err != nil {
return nil, err
}
if volumeBrowseRestrictionCheck {
securitySettings, err := transport.fetchEndpointSecuritySettings()
if err != nil {
return nil, err
}
if !securitySettings.AllowVolumeBrowserForRegularUsers {
// Return access denied for all roles except endpoint-administrator
_, userCanBrowse := user.EndpointAuthorizations[transport.endpoint.ID][portainer.OperationDockerAgentBrowseList]
if !userCanBrowse {
return responseutils.WriteAccessDeniedResponse()
}
}
}
_, endpointResourceAccess := user.EndpointAuthorizations[transport.endpoint.ID][portainer.EndpointResourcesAccess]
if endpointResourceAccess {
return transport.executeDockerRequest(request, true)
}
teamMemberships, err := transport.dataStore.TeamMembership().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.dataStore.ResourceControl().ResourceControls()
if err != nil {
return nil, err
}
resourceControl := authorization.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 || !authorization.UserCanAccessResource(tokenData.ID, userTeamIDs, inheritedResourceControl) {
return responseutils.WriteAccessDeniedResponse()
}
}
if resourceControl != nil && !authorization.UserCanAccessResource(tokenData.ID, userTeamIDs, resourceControl) {
return responseutils.WriteAccessDeniedResponse()
}
}
return transport.executeDockerRequest(request, true)
}
// 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.dataStore.Settings().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, true)
}
// 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.37/#operation/ContainerCreate
// https://docs.docker.com/engine/api/v1.37/#operation/NetworkCreate
// https://docs.docker.com/engine/api/v1.37/#operation/VolumeCreate
// https://docs.docker.com/engine/api/v1.37/#operation/ServiceCreate
// https://docs.docker.com/engine/api/v1.37/#operation/SecretCreate
// https://docs.docker.com/engine/api/v1.37/#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, log bool) (*http.Response, error) {
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
response, err := transport.executeDockerRequest(request, log)
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
}
if response.StatusCode == http.StatusNoContent || response.StatusCode == http.StatusOK {
resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType)
if err != nil {
return response, err
}
if resourceControl != nil {
err = transport.dataStore.ResourceControl().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, true)
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, true)
}
func (transport *Transport) createRegistryAccessContext(request *http.Request) (*registryAccessContext, error) {
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
accessContext := &registryAccessContext{
isAdmin: true,
userID: tokenData.ID,
}
hub, err := transport.dataStore.DockerHub().DockerHub()
if err != nil {
return nil, err
}
accessContext.dockerHub = hub
registries, err := transport.dataStore.Registry().Registries()
if err != nil {
return nil, err
}
accessContext.registries = registries
if tokenData.Role != portainer.AdministratorRole {
accessContext.isAdmin = false
teamMemberships, err := transport.dataStore.TeamMembership().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.dataStore.ResourceControl().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.dataStore.User().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.dataStore.TeamMembership().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
}
func (transport *Transport) isAdminOrEndpointAdmin(request *http.Request) (bool, error) {
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return false, err
}
if tokenData.Role == portainer.AdministratorRole {
return true, nil
}
user, err := transport.dataStore.User().User(tokenData.ID)
if err != nil {
return false, err
}
_, endpointResourceAccess := user.EndpointAuthorizations[portainer.EndpointID(transport.endpoint.ID)][portainer.EndpointResourcesAccess]
return endpointResourceAccess, nil
}
func (transport *Transport) fetchEndpointSecuritySettings() (*portainer.EndpointSecuritySettings, error) {
endpoint, err := transport.dataStore.Endpoint().Endpoint(portainer.EndpointID(transport.endpoint.ID))
if err != nil {
return nil, err
}
return &endpoint.SecuritySettings, nil
}