package docker

import (
	"bytes"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"net/http"
	"path"
	"regexp"
	"strconv"
	"strings"
	"sync"

	portainer "github.com/portainer/portainer/api"
	"github.com/portainer/portainer/api/dataservices"
	"github.com/portainer/portainer/api/http/proxy/factory/utils"
	"github.com/portainer/portainer/api/http/security"
	"github.com/portainer/portainer/api/internal/authorization"

	dockerclient "github.com/portainer/portainer/api/docker/client"
	"github.com/rs/zerolog/log"
	"github.com/segmentio/encoding/json"
)

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            dataservices.DataStore
		signatureService     portainer.DigitalSignatureService
		reverseTunnelService portainer.ReverseTunnelService
		dockerClientFactory  *dockerclient.ClientFactory
		gitService           portainer.GitService
		snapshotService      portainer.SnapshotService
		dockerID             string
		mu                   sync.Mutex
	}

	// TransportParameters is used to create a new Transport
	TransportParameters struct {
		Endpoint             *portainer.Endpoint
		DataStore            dataservices.DataStore
		SignatureService     portainer.DigitalSignatureService
		ReverseTunnelService portainer.ReverseTunnelService
		DockerClientFactory  *dockerclient.ClientFactory
	}

	restrictedDockerOperationContext struct {
		isAdmin          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, gitService portainer.GitService, snapshotService portainer.SnapshotService) (*Transport, error) {
	transport := &Transport{
		endpoint:             parameters.Endpoint,
		dataStore:            parameters.DataStore,
		signatureService:     parameters.SignatureService,
		reverseTunnelService: parameters.ReverseTunnelService,
		dockerClientFactory:  parameters.DockerClientFactory,
		HTTPTransport:        httpTransport,
		gitService:           gitService,
		snapshotService:      snapshotService,
	}

	return transport, nil
}

// RoundTrip is the implementation of the http.RoundTripper interface
func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
	return transport.ProxyDockerRequest(request)
}

var prefixProxyFuncMap = map[string]func(*Transport, *http.Request, string) (*http.Response, error){
	"build":      (*Transport).proxyBuildRequest,
	"configs":    (*Transport).proxyConfigRequest,
	"containers": (*Transport).proxyContainerRequest,
	"images":     (*Transport).proxyImageRequest,
	"networks":   (*Transport).proxyNetworkRequest,
	"nodes":      (*Transport).proxyNodeRequest,
	"secrets":    (*Transport).proxySecretRequest,
	"services":   (*Transport).proxyServiceRequest,
	"swarm":      (*Transport).proxySwarmRequest,
	"tasks":      (*Transport).proxyTaskRequest,
	"v2":         (*Transport).proxyAgentRequest,
	"volumes":    (*Transport).proxyVolumeRequest,
}

// ProxyDockerRequest intercepts a Docker API request and apply logic based
// on the requested operation.
func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Response, error) {
	unversionedPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")

	if transport.endpoint.Type == portainer.AgentOnDockerEnvironment || transport.endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
		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)
	}

	prefix := strings.Split(strings.TrimPrefix(unversionedPath, "/"), "/")[0]

	if proxyFunc := prefixProxyFuncMap[prefix]; proxyFunc != nil {
		return proxyFunc(transport, request, unversionedPath)
	}

	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.EdgeAgentOnDockerEnvironment {
		return response, err
	}

	if err == nil {
		transport.reverseTunnelService.UpdateLastActivity(transport.endpoint.ID)
	}

	return response, err
}

func (transport *Transport) proxyAgentRequest(r *http.Request, unversionedPath string) (*http.Response, error) {
	requestPath := strings.TrimPrefix(unversionedPath, "/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)
		}

		volumeName := volumeIDParameter[0]

		resourceID, err := transport.getVolumeResourceID(volumeName)
		if err != nil {
			return nil, err
		}

		// Volume browser request
		return transport.restrictedResourceOperation(r, resourceID, volumeName, portainer.VolumeResourceControl, true)
	case strings.HasPrefix(requestPath, "/dockerhub"):
		requestPath, registryIdString := path.Split(r.URL.Path)

		registryID, err := strconv.Atoi(registryIdString)
		if err != nil {
			return nil, fmt.Errorf("missing registry id: %w", err)
		}

		r.URL.Path = strings.TrimSuffix(requestPath, "/")

		registry := &portainer.Registry{
			Type: portainer.DockerHubRegistry,
		}

		if registryID != 0 {
			registry, err = transport.dataStore.Registry().Read(portainer.RegistryID(registryID))
			if err != nil {
				return nil, fmt.Errorf("failed fetching registry: %w", err)
			}
		}

		if registry.Type != portainer.DockerHubRegistry {
			return nil, errors.New("Invalid registry type")
		}

		newBody, err := json.Marshal(registry)
		if err != nil {
			return nil, err
		}

		r.Method = http.MethodPost

		r.Body = io.NopCloser(bytes.NewReader(newBody))
		r.ContentLength = int64(len(newBody))
	}

	return transport.executeDockerRequest(r)
}

func (transport *Transport) proxyConfigRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
	requestPath := unversionedPath

	switch 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, configID, portainer.ConfigResourceControl)
		}

		return transport.restrictedResourceOperation(request, configID, configID, portainer.ConfigResourceControl, false)
	}
}

func (transport *Transport) proxyContainerRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
	requestPath := unversionedPath

	switch 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, 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, containerID, portainer.ContainerResourceControl)
			}

			return transport.restrictedResourceOperation(request, containerID, containerID, portainer.ContainerResourceControl, false)
		}

		return transport.executeDockerRequest(request)
	}
}

func (transport *Transport) proxyServiceRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
	requestPath := unversionedPath

	switch 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))
			transport.decorateRegistryAuthenticationHeader(request)

			return transport.restrictedResourceOperation(request, serviceID, 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, serviceID, portainer.ServiceResourceControl)
			}

			return transport.restrictedResourceOperation(request, serviceID, serviceID, portainer.ServiceResourceControl, false)
		}

		return transport.executeDockerRequest(request)
	}
}

func (transport *Transport) proxyVolumeRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
	requestPath := unversionedPath

	switch requestPath {
	case "/volumes/create":
		return transport.decorateVolumeResourceCreationOperation(request, 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, unversionedPath string) (*http.Response, error) {
	requestPath := unversionedPath

	switch 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, networkID, portainer.NetworkResourceControl)
		}

		return transport.restrictedResourceOperation(request, networkID, networkID, portainer.NetworkResourceControl, false)
	}
}

func (transport *Transport) proxySecretRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
	requestPath := unversionedPath

	switch 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, secretID, portainer.SecretResourceControl)
		}

		return transport.restrictedResourceOperation(request, secretID, secretID, portainer.SecretResourceControl, false)
	}
}

func (transport *Transport) proxyNodeRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
	requestPath := unversionedPath

	// Assume /nodes/{id}
	if path.Base(requestPath) != "nodes" {
		return transport.administratorOperation(request)
	}

	return transport.executeDockerRequest(request)
}

func (transport *Transport) proxySwarmRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
	requestPath := unversionedPath

	switch requestPath {
	case "/swarm":
		return transport.rewriteOperation(request, swarmInspectOperation)
	default:
		// Assume /swarm/{action}
		return transport.administratorOperation(request)
	}
}

func (transport *Transport) proxyTaskRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
	requestPath := unversionedPath

	switch requestPath {
	case "/tasks":
		return transport.rewriteOperation(request, transport.taskListOperation)
	default:
		// Assume /tasks/{id}
		return transport.executeDockerRequest(request)
	}
}

func (transport *Transport) proxyBuildRequest(request *http.Request, _ string) (*http.Response, error) {
	if err := transport.updateDefaultGitBranch(request); err != nil {
		return nil, err
	}

	return transport.interceptAndRewriteRequest(request, buildOperation)
}

func (transport *Transport) updateDefaultGitBranch(request *http.Request) error {
	remote := request.URL.Query().Get("remote")

	if !strings.HasSuffix(remote, ".git") {
		return nil
	}

	repositoryURL := remote[:len(remote)-4]
	latestCommitID, err := transport.gitService.LatestCommitID(repositoryURL, "", "", "", false)
	if err != nil {
		return err
	}

	newRemote := fmt.Sprintf("%s#%s", remote, latestCommitID)

	q := request.URL.Query()
	q.Set("remote", newRemote)
	request.URL.RawQuery = q.Encode()

	return nil
}

func (transport *Transport) proxyImageRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
	requestPath := unversionedPath

	switch 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) {
	transport.decorateRegistryAuthenticationHeader(request)

	return transport.decorateGenericResourceCreationOperation(request, serviceObjectIdentifier, portainer.ServiceResourceControl)
}

func (transport *Transport) decorateRegistryAuthenticationHeader(request *http.Request) error {
	accessContext, err := transport.createRegistryAccessContext(request)
	if err != nil {
		return err
	}

	originalHeader := request.Header.Get("X-Registry-Auth")

	if originalHeader == "" {
		return nil
	}

	decodedHeaderData, err := base64.StdEncoding.DecodeString(originalHeader)
	if err != nil {
		return err
	}

	var originalHeaderData portainerRegistryAuthenticationHeader
	if err := json.Unmarshal(decodedHeaderData, &originalHeaderData); err != nil {
		return err
	}

	// Delete header and exist function without error if Front End
	// passes empty json. This is to restore original behavior which
	// never originally passed this header
	if string(decodedHeaderData) == "{}" {
		request.Header.Del("X-Registry-Auth")

		return nil
	}

	// Only set X-Registry-Auth if registryId is defined
	if originalHeaderData.RegistryId == nil {
		return nil
	}

	authenticationHeader, err := createRegistryAuthenticationHeader(transport.dataStore, *originalHeaderData.RegistryId, accessContext)
	if err != nil {
		return err
	}

	headerData, err := json.Marshal(authenticationHeader)
	if err != nil {
		return err
	}

	request.Header.Set("X-Registry-Auth", base64.URLEncoding.EncodeToString(headerData))

	return nil
}

func (transport *Transport) restrictedResourceOperation(request *http.Request, resourceID string, dockerResourceID string, resourceType portainer.ResourceControlType, volumeBrowseRestrictionCheck bool) (*http.Response, error) {
	tokenData, err := security.RetrieveTokenData(request)
	if err != nil {
		return nil, err
	}

	if tokenData.Role == portainer.AdministratorRole {
		return transport.executeDockerRequest(request)
	}

	if volumeBrowseRestrictionCheck {
		securitySettings, err := transport.fetchEndpointSecuritySettings()
		if err != nil {
			return nil, err
		}

		if !securitySettings.AllowVolumeBrowserForRegularUsers {
			return utils.WriteAccessDeniedResponse()
		}
	}

	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().ReadAll()
	if err != nil {
		return nil, err
	}

	resourceControl := authorization.GetResourceControlByResourceIDAndType(resourceID, resourceType, resourceControls)
	if resourceControl == nil {
		agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader)

		if dockerResourceID == "" {
			dockerResourceID = resourceID
		}

		// This resource was created outside of portainer,
		// is part of a Docker service or part of a Docker Swarm/Compose stack.
		inheritedResourceControl, err := transport.getInheritedResourceControlFromServiceOrStack(dockerResourceID, agentTargetHeader, resourceType, resourceControls)
		if err != nil {
			return nil, err
		}

		if inheritedResourceControl == nil || !authorization.UserCanAccessResource(tokenData.ID, userTeamIDs, inheritedResourceControl) {
			return utils.WriteAccessDeniedResponse()
		}
	}

	if resourceControl != nil && !authorization.UserCanAccessResource(tokenData.ID, userTeamIDs, resourceControl) {
		return utils.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.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) {
	if err := operation(request); 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.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 := utils.GetResponseAsJSONObject(response)
	if err != nil {
		return err
	}

	if responseObject[resourceIdentifierAttribute] == nil {
		log.Error().Msg("missing identifier in Docker resource creation response")

		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 utils.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, volumeName string, resourceType portainer.ResourceControlType) (*http.Response, error) {
	response, err := transport.restrictedResourceOperation(request, resourceIdentifierAttribute, volumeName, resourceType, false)
	if err != nil {
		return response, err
	}

	if response.StatusCode != http.StatusNoContent && response.StatusCode != http.StatusOK {
		return response, nil
	}

	resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType)
	if dataservices.IsErrObjectNotFound(err) {
		return response, nil
	} else if err != nil {
		return response, err
	}

	if resourceControl != nil {
		err = transport.dataStore.ResourceControl().Delete(resourceControl.ID)
	}

	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
	}

	if response.StatusCode == http.StatusOK {
		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 utils.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 := &registryAccessContext{
		isAdmin:    true,
		endpointID: transport.endpoint.ID,
	}

	user, err := transport.dataStore.User().Read(tokenData.ID)
	if err != nil {
		return nil, err
	}
	accessContext.user = user

	registries, err := transport.dataStore.Registry().ReadAll()
	if err != nil {
		return nil, err
	}
	accessContext.registries = registries

	if user.Role == portainer.AdministratorRole {
		return accessContext, nil
	}

	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().ReadAll()
	if err != nil {
		return nil, err
	}

	operationContext := &restrictedDockerOperationContext{
		isAdmin:          true,
		userID:           tokenData.ID,
		resourceControls: resourceControls,
	}

	if tokenData.Role == portainer.AdministratorRole {
		return operationContext, nil
	}

	operationContext.isAdmin = false

	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
	}

	return tokenData.Role == portainer.AdministratorRole, nil
}

func (transport *Transport) fetchEndpointSecuritySettings() (*portainer.EndpointSecuritySettings, error) {
	endpoint, err := transport.dataStore.Endpoint().Endpoint(transport.endpoint.ID)
	if err != nil {
		return nil, err
	}

	return &endpoint.SecuritySettings, nil
}
