mirror of https://github.com/portainer/portainer
238 lines
6.8 KiB
Go
238 lines
6.8 KiB
Go
package proxy
|
|
|
|
import (
|
|
"net"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/portainer/portainer"
|
|
"github.com/portainer/portainer/http/security"
|
|
)
|
|
|
|
type (
|
|
proxyTransport struct {
|
|
dockerTransport *http.Transport
|
|
ResourceControlService portainer.ResourceControlService
|
|
TeamMembershipService portainer.TeamMembershipService
|
|
}
|
|
restrictedOperationContext struct {
|
|
isAdmin bool
|
|
userID portainer.UserID
|
|
userTeamIDs []portainer.TeamID
|
|
resourceControls []portainer.ResourceControl
|
|
}
|
|
restrictedOperationRequest func(*http.Request, *http.Response, *restrictedOperationContext) error
|
|
)
|
|
|
|
func newSocketTransport(socketPath string) *http.Transport {
|
|
return &http.Transport{
|
|
Dial: func(proto, addr string) (conn net.Conn, err error) {
|
|
return net.Dial("unix", socketPath)
|
|
},
|
|
}
|
|
}
|
|
|
|
func newHTTPTransport() *http.Transport {
|
|
return &http.Transport{}
|
|
}
|
|
|
|
func (p *proxyTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
return p.proxyDockerRequest(request)
|
|
}
|
|
|
|
func (p *proxyTransport) executeDockerRequest(request *http.Request) (*http.Response, error) {
|
|
return p.dockerTransport.RoundTrip(request)
|
|
}
|
|
|
|
func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) {
|
|
path := request.URL.Path
|
|
|
|
if strings.HasPrefix(path, "/containers") {
|
|
return p.proxyContainerRequest(request)
|
|
} else if strings.HasPrefix(path, "/services") {
|
|
return p.proxyServiceRequest(request)
|
|
} else if strings.HasPrefix(path, "/volumes") {
|
|
return p.proxyVolumeRequest(request)
|
|
}
|
|
|
|
return p.executeDockerRequest(request)
|
|
}
|
|
|
|
func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
|
|
// return p.executeDockerRequest(request)
|
|
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.rewriteOperation(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.executeDockerRequest(request)
|
|
|
|
case "/volumes/prune":
|
|
return p.administratorOperation(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)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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) {
|
|
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 := &restrictedOperationContext{
|
|
isAdmin: true,
|
|
userID: tokenData.ID,
|
|
resourceControls: resourceControls,
|
|
}
|
|
|
|
if tokenData.Role != portainer.AdministratorRole {
|
|
operationContext.isAdmin = false
|
|
|
|
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
|
|
}
|
|
|
|
response, err := p.executeDockerRequest(request)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
err = operation(request, response, operationContext)
|
|
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)
|
|
}
|