You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
portainer/api/http/proxy/docker_transport.go

601 lines
18 KiB

package proxy
import (
"encoding/base64"
"encoding/json"
"net/http"
"path"
"regexp"
"strings"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
var apiVersionRe = regexp.MustCompile(`(/v[0-9]\.[0-9]*)?`)
type (
proxyTransport struct {
dockerTransport *http.Transport
enableSignature bool
ResourceControlService portainer.ResourceControlService
UserService portainer.UserService
TeamMembershipService portainer.TeamMembershipService
RegistryService portainer.RegistryService
DockerHubService portainer.DockerHubService
SettingsService portainer.SettingsService
SignatureService portainer.DigitalSignatureService
ReverseTunnelService portainer.ReverseTunnelService
ExtensionService portainer.ExtensionService
endpointIdentifier portainer.EndpointID
endpointType portainer.EndpointType
}
restrictedDockerOperationContext struct {
isAdmin bool
endpointResourceAccess bool
userID portainer.UserID
userTeamIDs []portainer.TeamID
resourceControls []portainer.ResourceControl
}
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"`
}
operationExecutor struct {
operationContext *restrictedDockerOperationContext
labelBlackList []portainer.Pair
}
restrictedOperationRequest func(*http.Response, *operationExecutor) error
operationRequest func(*http.Request) error
)
func (p *proxyTransport) RoundTrip(request *http.Request) (*http.Response, error) {
return p.proxyDockerRequest(request)
}
func (p *proxyTransport) executeDockerRequest(request *http.Request) (*http.Response, error) {
response, err := p.dockerTransport.RoundTrip(request)
if p.endpointType != portainer.EdgeAgentEnvironment {
return response, err
}
if err == nil {
p.ReverseTunnelService.SetTunnelStatusToActive(p.endpointIdentifier)
} else {
p.ReverseTunnelService.SetTunnelStatusToIdle(p.endpointIdentifier)
}
return response, err
}
func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) {
path := apiVersionRe.ReplaceAllString(request.URL.Path, "")
request.URL.Path = path
if p.enableSignature {
signature, err := p.SignatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return nil, err
}
request.Header.Set(portainer.PortainerAgentPublicKeyHeader, p.SignatureService.EncodedPublicKey())
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
}
switch {
case strings.HasPrefix(path, "/configs"):
return p.proxyConfigRequest(request)
case strings.HasPrefix(path, "/containers"):
return p.proxyContainerRequest(request)
case strings.HasPrefix(path, "/services"):
return p.proxyServiceRequest(request)
case strings.HasPrefix(path, "/volumes"):
return p.proxyVolumeRequest(request)
case strings.HasPrefix(path, "/networks"):
return p.proxyNetworkRequest(request)
case strings.HasPrefix(path, "/secrets"):
return p.proxySecretRequest(request)
case strings.HasPrefix(path, "/swarm"):
return p.proxySwarmRequest(request)
case strings.HasPrefix(path, "/nodes"):
return p.proxyNodeRequest(request)
case strings.HasPrefix(path, "/tasks"):
return p.proxyTaskRequest(request)
case strings.HasPrefix(path, "/build"):
return p.proxyBuildRequest(request)
case strings.HasPrefix(path, "/images"):
return p.proxyImageRequest(request)
case strings.HasPrefix(path, "/v2"):
return p.proxyAgentRequest(request)
default:
return p.executeDockerRequest(request)
}
}
func (p *proxyTransport) 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 p.administratorOperation(r)
}
return p.restrictedVolumeBrowserOperation(r, volumeIDParameter[0])
}
return p.executeDockerRequest(r)
}
func (p *proxyTransport) proxyConfigRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/configs/create":
return p.executeDockerRequest(request)
case "/configs":
return p.rewriteOperation(request, configListOperation)
default:
// assume /configs/{id}
if request.Method == http.MethodGet {
return p.rewriteOperation(request, configInspectOperation)
}
configID := path.Base(requestPath)
return p.restrictedOperation(request, configID)
}
}
func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/containers/create":
return p.executeDockerRequest(request)
case "/containers/prune":
return p.administratorOperation(request)
case "/containers/json":
return p.rewriteOperationWithLabelFiltering(request, 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 p.rewriteOperation(request, containerInspectOperation)
}
return p.restrictedOperation(request, containerID)
} else if match, _ := path.Match("/containers/*", requestPath); match {
// Handle /containers/{id} requests
containerID := path.Base(requestPath)
return p.restrictedOperation(request, containerID)
}
return p.executeDockerRequest(request)
}
}
func (p *proxyTransport) proxyServiceRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/services/create":
return p.replaceRegistryAuthenticationHeader(request)
case "/services":
return p.rewriteOperation(request, 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 p.restrictedOperation(request, serviceID)
} else if match, _ := path.Match("/services/*", requestPath); match {
// Handle /services/{id} requests
serviceID := path.Base(requestPath)
if request.Method == http.MethodGet {
return p.rewriteOperation(request, serviceInspectOperation)
}
return p.restrictedOperation(request, serviceID)
}
return p.executeDockerRequest(request)
}
}
func (p *proxyTransport) proxyVolumeRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/volumes/create":
return p.executeDockerRequest(request)
case "/volumes/prune":
return p.administratorOperation(request)
case "/volumes":
return p.rewriteOperation(request, volumeListOperation)
default:
// assume /volumes/{name}
if request.Method == http.MethodGet {
return p.rewriteOperation(request, volumeInspectOperation)
}
volumeID := path.Base(requestPath)
return p.restrictedOperation(request, volumeID)
}
}
func (p *proxyTransport) proxyNetworkRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/networks/create":
return p.executeDockerRequest(request)
case "/networks":
return p.rewriteOperation(request, networkListOperation)
default:
// assume /networks/{id}
if request.Method == http.MethodGet {
return p.rewriteOperation(request, networkInspectOperation)
}
networkID := path.Base(requestPath)
return p.restrictedOperation(request, networkID)
}
}
func (p *proxyTransport) proxySecretRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/secrets/create":
return p.executeDockerRequest(request)
case "/secrets":
return p.rewriteOperation(request, secretListOperation)
default:
// assume /secrets/{id}
if request.Method == http.MethodGet {
return p.rewriteOperation(request, secretInspectOperation)
}
secretID := path.Base(requestPath)
return p.restrictedOperation(request, secretID)
}
}
func (p *proxyTransport) proxyNodeRequest(request *http.Request) (*http.Response, error) {
requestPath := request.URL.Path
// assume /nodes/{id}
if path.Base(requestPath) != "nodes" {
return p.administratorOperation(request)
}
return p.executeDockerRequest(request)
}
func (p *proxyTransport) proxySwarmRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/swarm":
return p.rewriteOperation(request, swarmInspectOperation)
default:
// assume /swarm/{action}
return p.administratorOperation(request)
}
}
func (p *proxyTransport) proxyTaskRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/tasks":
return p.rewriteOperation(request, taskListOperation)
default:
// assume /tasks/{id}
return p.executeDockerRequest(request)
}
}
func (p *proxyTransport) proxyBuildRequest(request *http.Request) (*http.Response, error) {
return p.interceptAndRewriteRequest(request, buildOperation)
}
func (p *proxyTransport) proxyImageRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/images/create":
return p.replaceRegistryAuthenticationHeader(request)
default:
if path.Base(requestPath) == "push" && request.Method == http.MethodPost {
return p.replaceRegistryAuthenticationHeader(request)
}
return p.executeDockerRequest(request)
}
}
func (p *proxyTransport) replaceRegistryAuthenticationHeader(request *http.Request) (*http.Response, error) {
accessContext, err := p.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 p.executeDockerRequest(request)
}
// restrictedOperation ensures that the current user has the required authorizations
// before executing the original request.
func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {
var err error
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
if tokenData.Role != portainer.AdministratorRole {
teamMemberships, err := p.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 := p.ResourceControlService.ResourceControls()
if err != nil {
return nil, err
}
resourceControl := getResourceControlByResourceID(resourceID, resourceControls)
if resourceControl == nil || !canUserAccessResource(tokenData.ID, userTeamIDs, resourceControl) {
return writeAccessDeniedResponse()
}
}
return p.executeDockerRequest(request)
}
// restrictedVolumeBrowserOperation is similar to restrictedOperation but adds an extra check on a specific setting
func (p *proxyTransport) restrictedVolumeBrowserOperation(request *http.Request, resourceID string) (*http.Response, error) {
var err error
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
if tokenData.Role != portainer.AdministratorRole {
settings, err := p.SettingsService.Settings()
if err != nil {
return nil, err
}
_, err = p.ExtensionService.Extension(portainer.RBACExtension)
if err == portainer.ErrObjectNotFound && !settings.AllowVolumeBrowserForRegularUsers {
return writeAccessDeniedResponse()
} else if err != nil && err != portainer.ErrObjectNotFound {
return nil, err
}
user, err := p.UserService.User(tokenData.ID)
if err != nil {
return nil, err
}
endpointResourceAccess := false
_, ok := user.EndpointAuthorizations[p.endpointIdentifier][portainer.EndpointResourcesAccess]
if ok {
endpointResourceAccess = true
}
teamMemberships, err := p.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 := p.ResourceControlService.ResourceControls()
if err != nil {
return nil, err
}
resourceControl := getResourceControlByResourceID(resourceID, resourceControls)
if !endpointResourceAccess && (resourceControl == nil || !canUserAccessResource(tokenData.ID, userTeamIDs, resourceControl)) {
return writeAccessDeniedResponse()
}
}
return p.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 (p *proxyTransport) rewriteOperationWithLabelFiltering(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
operationContext, err := p.createOperationContext(request)
if err != nil {
return nil, err
}
settings, err := p.SettingsService.Settings()
if err != nil {
return nil, err
}
executor := &operationExecutor{
operationContext: operationContext,
labelBlackList: settings.BlackListedLabels,
}
return p.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 (p *proxyTransport) rewriteOperation(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
operationContext, err := p.createOperationContext(request)
if err != nil {
return nil, err
}
executor := &operationExecutor{
operationContext: operationContext,
}
return p.executeRequestAndRewriteResponse(request, operation, executor)
}
func (p *proxyTransport) interceptAndRewriteRequest(request *http.Request, operation operationRequest) (*http.Response, error) {
err := operation(request)
if err != nil {
return nil, err
}
return p.executeDockerRequest(request)
}
func (p *proxyTransport) executeRequestAndRewriteResponse(request *http.Request, operation restrictedOperationRequest, executor *operationExecutor) (*http.Response, error) {
response, err := p.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 (p *proxyTransport) administratorOperation(request *http.Request) (*http.Response, error) {
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
if tokenData.Role != portainer.AdministratorRole {
return writeAccessDeniedResponse()
}
return p.executeDockerRequest(request)
}
func (p *proxyTransport) 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 := p.DockerHubService.DockerHub()
if err != nil {
return nil, err
}
accessContext.dockerHub = hub
registries, err := p.RegistryService.Registries()
if err != nil {
return nil, err
}
accessContext.registries = registries
if tokenData.Role != portainer.AdministratorRole {
accessContext.isAdmin = false
teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
if err != nil {
return nil, err
}
accessContext.teamMemberships = teamMemberships
}
return accessContext, nil
}
func (p *proxyTransport) createOperationContext(request *http.Request) (*restrictedDockerOperationContext, error) {
var err error
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
resourceControls, err := p.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 := p.UserService.User(operationContext.userID)
if err != nil {
return nil, err
}
_, ok := user.EndpointAuthorizations[p.endpointIdentifier][portainer.EndpointResourcesAccess]
if ok {
operationContext.endpointResourceAccess = true
}
teamMemberships, err := p.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
}