mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 13:55:21 +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
20
api/http/proxy/factory/azure.go
Normal file
20
api/http/proxy/factory/azure.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package factory
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/azure"
|
||||
)
|
||||
|
||||
func newAzureProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
remoteURL, err := url.Parse(azureAPIBaseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := newSingleHostReverseProxyWithHostHeader(remoteURL)
|
||||
proxy.Transport = azure.NewTransport(&endpoint.AzureCredentials)
|
||||
return proxy, nil
|
||||
}
|
80
api/http/proxy/factory/azure/transport.go
Normal file
80
api/http/proxy/factory/azure/transport.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
)
|
||||
|
||||
type (
|
||||
azureAPIToken struct {
|
||||
value string
|
||||
expirationTime time.Time
|
||||
}
|
||||
|
||||
Transport struct {
|
||||
credentials *portainer.AzureCredentials
|
||||
client *client.HTTPClient
|
||||
token *azureAPIToken
|
||||
mutex sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
// NewTransport returns a pointer to a new instance of Transport that implements the HTTP Transport
|
||||
// interface for proxying requests to the Azure API.
|
||||
func NewTransport(credentials *portainer.AzureCredentials) *Transport {
|
||||
return &Transport{
|
||||
credentials: credentials,
|
||||
client: client.NewHTTPClient(),
|
||||
}
|
||||
}
|
||||
|
||||
// RoundTrip is the implementation of the Transport interface.
|
||||
func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
err := transport.retrieveAuthenticationToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("Authorization", "Bearer "+transport.token.value)
|
||||
return http.DefaultTransport.RoundTrip(request)
|
||||
}
|
||||
|
||||
func (transport *Transport) authenticate() error {
|
||||
token, err := transport.client.ExecuteAzureAuthenticationRequest(transport.credentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expiresOn, err := strconv.ParseInt(token.ExpiresOn, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transport.token = &azureAPIToken{
|
||||
value: token.AccessToken,
|
||||
expirationTime: time.Unix(expiresOn, 0),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (transport *Transport) retrieveAuthenticationToken() error {
|
||||
transport.mutex.Lock()
|
||||
defer transport.mutex.Unlock()
|
||||
|
||||
if transport.token == nil {
|
||||
return transport.authenticate()
|
||||
}
|
||||
|
||||
timeLimit := time.Now().Add(-5 * time.Minute)
|
||||
if timeLimit.After(transport.token.expirationTime) {
|
||||
return transport.authenticate()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
118
api/http/proxy/factory/docker.go
Normal file
118
api/http/proxy/factory/docker.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package factory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/docker"
|
||||
)
|
||||
|
||||
func (factory *ProxyFactory) newDockerProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
|
||||
return factory.newDockerLocalProxy(endpoint)
|
||||
}
|
||||
|
||||
return factory.newDockerHTTPProxy(endpoint)
|
||||
}
|
||||
|
||||
func (factory *ProxyFactory) newDockerLocalProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return factory.newOSBasedLocalProxy(endpointURL.Path, endpoint)
|
||||
}
|
||||
|
||||
func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||
tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
endpoint.URL = fmt.Sprintf("http://localhost:%d", tunnel.Port)
|
||||
}
|
||||
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpointURL.Scheme = "http"
|
||||
httpTransport := &http.Transport{}
|
||||
|
||||
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
|
||||
config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpTransport.TLSClientConfig = config
|
||||
endpointURL.Scheme = "https"
|
||||
}
|
||||
|
||||
transportParameters := &docker.TransportParameters{
|
||||
Endpoint: endpoint,
|
||||
ResourceControlService: factory.resourceControlService,
|
||||
UserService: factory.userService,
|
||||
TeamService: factory.teamService,
|
||||
TeamMembershipService: factory.teamMembershipService,
|
||||
RegistryService: factory.registryService,
|
||||
DockerHubService: factory.dockerHubService,
|
||||
SettingsService: factory.settingsService,
|
||||
ReverseTunnelService: factory.reverseTunnelService,
|
||||
ExtensionService: factory.extensionService,
|
||||
SignatureService: factory.signatureService,
|
||||
DockerClientFactory: factory.dockerClientFactory,
|
||||
}
|
||||
|
||||
dockerTransport, err := docker.NewTransport(transportParameters, httpTransport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
|
||||
proxy.Transport = dockerTransport
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
type dockerLocalProxy struct {
|
||||
transport *docker.Transport
|
||||
}
|
||||
|
||||
// ServeHTTP is the http.Handler interface implementation
|
||||
// for a local (Unix socket or Windows named pipe) Docker proxy.
|
||||
func (proxy *dockerLocalProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Force URL/domain to http/unixsocket to be able to
|
||||
// use http.transport RoundTrip to do the requests via the socket
|
||||
r.URL.Scheme = "http"
|
||||
r.URL.Host = "unixsocket"
|
||||
|
||||
res, err := proxy.transport.ProxyDockerRequest(r)
|
||||
if err != nil {
|
||||
code := http.StatusInternalServerError
|
||||
if res != nil && res.StatusCode != 0 {
|
||||
code = res.StatusCode
|
||||
}
|
||||
|
||||
httperror.WriteError(w, code, "Unable to proxy the request via the Docker socket", err)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
for k, vv := range res.Header {
|
||||
for _, v := range vv {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(res.StatusCode)
|
||||
|
||||
if _, err := io.Copy(w, res.Body); err != nil {
|
||||
log.Printf("proxy error: %s\n", err)
|
||||
}
|
||||
}
|
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")
|
||||
}
|
46
api/http/proxy/factory/docker_unix.go
Normal file
46
api/http/proxy/factory/docker_unix.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
package factory
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/docker"
|
||||
)
|
||||
|
||||
func (factory ProxyFactory) newOSBasedLocalProxy(path string, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
transportParameters := &docker.TransportParameters{
|
||||
Endpoint: endpoint,
|
||||
ResourceControlService: factory.resourceControlService,
|
||||
UserService: factory.userService,
|
||||
TeamService: factory.teamService,
|
||||
TeamMembershipService: factory.teamMembershipService,
|
||||
RegistryService: factory.registryService,
|
||||
DockerHubService: factory.dockerHubService,
|
||||
SettingsService: factory.settingsService,
|
||||
ReverseTunnelService: factory.reverseTunnelService,
|
||||
ExtensionService: factory.extensionService,
|
||||
SignatureService: factory.signatureService,
|
||||
DockerClientFactory: factory.dockerClientFactory,
|
||||
}
|
||||
|
||||
proxy := &dockerLocalProxy{}
|
||||
|
||||
dockerTransport, err := docker.NewTransport(transportParameters, newSocketTransport(path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy.transport = dockerTransport
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
func newSocketTransport(socketPath string) *http.Transport {
|
||||
return &http.Transport{
|
||||
Dial: func(proto, addr string) (conn net.Conn, err error) {
|
||||
return net.Dial("unix", socketPath)
|
||||
},
|
||||
}
|
||||
}
|
47
api/http/proxy/factory/docker_windows.go
Normal file
47
api/http/proxy/factory/docker_windows.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
// +build windows
|
||||
|
||||
package factory
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/Microsoft/go-winio"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/docker"
|
||||
)
|
||||
|
||||
func (factory ProxyFactory) newOSBasedLocalProxy(path string, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
transportParameters := &docker.TransportParameters{
|
||||
Endpoint: endpoint,
|
||||
ResourceControlService: factory.resourceControlService,
|
||||
UserService: factory.userService,
|
||||
TeamService: factory.teamService,
|
||||
TeamMembershipService: factory.teamMembershipService,
|
||||
RegistryService: factory.registryService,
|
||||
DockerHubService: factory.dockerHubService,
|
||||
SettingsService: factory.settingsService,
|
||||
ReverseTunnelService: factory.reverseTunnelService,
|
||||
ExtensionService: factory.extensionService,
|
||||
SignatureService: factory.signatureService,
|
||||
DockerClientFactory: factory.dockerClientFactory,
|
||||
}
|
||||
|
||||
proxy := &dockerLocalProxy{}
|
||||
|
||||
dockerTransport, err := docker.NewTransport(transportParameters, newSocketTransport(path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy.transport = dockerTransport
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
func newNamedPipeTransport(namedPipePath string) *http.Transport {
|
||||
return &http.Transport{
|
||||
Dial: func(proto, addr string) (conn net.Conn, err error) {
|
||||
return winio.DialPipe(namedPipePath, nil)
|
||||
},
|
||||
}
|
||||
}
|
109
api/http/proxy/factory/factory.go
Normal file
109
api/http/proxy/factory/factory.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
package factory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
)
|
||||
|
||||
const azureAPIBaseURL = "https://management.azure.com"
|
||||
|
||||
var extensionPorts = map[portainer.ExtensionID]string{
|
||||
portainer.RegistryManagementExtension: "7001",
|
||||
portainer.OAuthAuthenticationExtension: "7002",
|
||||
portainer.RBACExtension: "7003",
|
||||
}
|
||||
|
||||
type (
|
||||
// ProxyFactory is a factory to create reverse proxies to Docker endpoints and extensions
|
||||
ProxyFactory struct {
|
||||
resourceControlService portainer.ResourceControlService
|
||||
userService portainer.UserService
|
||||
teamService portainer.TeamService
|
||||
teamMembershipService portainer.TeamMembershipService
|
||||
settingsService portainer.SettingsService
|
||||
registryService portainer.RegistryService
|
||||
dockerHubService portainer.DockerHubService
|
||||
signatureService portainer.DigitalSignatureService
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
extensionService portainer.ExtensionService
|
||||
dockerClientFactory *docker.ClientFactory
|
||||
}
|
||||
|
||||
// ProxyFactoryParameters is used to create a new ProxyFactory
|
||||
ProxyFactoryParameters struct {
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
UserService portainer.UserService
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
SettingsService portainer.SettingsService
|
||||
RegistryService portainer.RegistryService
|
||||
DockerHubService portainer.DockerHubService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
ExtensionService portainer.ExtensionService
|
||||
DockerClientFactory *docker.ClientFactory
|
||||
}
|
||||
)
|
||||
|
||||
// NewProxyFactory returns a pointer to a new instance of a ProxyFactory
|
||||
func NewProxyFactory(parameters *ProxyFactoryParameters) *ProxyFactory {
|
||||
return &ProxyFactory{
|
||||
resourceControlService: parameters.ResourceControlService,
|
||||
userService: parameters.UserService,
|
||||
teamService: parameters.TeamService,
|
||||
teamMembershipService: parameters.TeamMembershipService,
|
||||
settingsService: parameters.SettingsService,
|
||||
registryService: parameters.RegistryService,
|
||||
dockerHubService: parameters.DockerHubService,
|
||||
signatureService: parameters.SignatureService,
|
||||
reverseTunnelService: parameters.ReverseTunnelService,
|
||||
extensionService: parameters.ExtensionService,
|
||||
dockerClientFactory: parameters.DockerClientFactory,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildExtensionURL returns the URL to an extension server
|
||||
func BuildExtensionURL(extensionID portainer.ExtensionID) string {
|
||||
return fmt.Sprintf("http://%s:%s", portainer.ExtensionServer, extensionPorts[extensionID])
|
||||
}
|
||||
|
||||
// NewExtensionProxy returns a new HTTP proxy to an extension server
|
||||
func (factory *ProxyFactory) NewExtensionProxy(extensionID portainer.ExtensionID) (http.Handler, error) {
|
||||
address := "http://" + portainer.ExtensionServer + ":" + extensionPorts[extensionID]
|
||||
|
||||
extensionURL, err := url.Parse(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extensionURL.Scheme = "http"
|
||||
proxy := httputil.NewSingleHostReverseProxy(extensionURL)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// NewLegacyExtensionProxy returns a new HTTP proxy to a legacy extension server (Storidge)
|
||||
func (factory *ProxyFactory) NewLegacyExtensionProxy(extensionAPIURL string) (http.Handler, error) {
|
||||
extensionURL, err := url.Parse(extensionAPIURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extensionURL.Scheme = "http"
|
||||
proxy := httputil.NewSingleHostReverseProxy(extensionURL)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// NewEndpointProxy returns a new reverse proxy (filesystem based or HTTP) to an endpoint API server
|
||||
func (factory *ProxyFactory) NewEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
switch endpoint.Type {
|
||||
case portainer.AzureEnvironment:
|
||||
return newAzureProxy(endpoint)
|
||||
}
|
||||
|
||||
return factory.newDockerProxy(endpoint)
|
||||
}
|
11
api/http/proxy/factory/responseutils/json.go
Normal file
11
api/http/proxy/factory/responseutils/json.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package responseutils
|
||||
|
||||
// GetJSONObject will extract an object from a specific property of another JSON object.
|
||||
// Returns nil if nothing is associated to the specified key.
|
||||
func GetJSONObject(jsonObject map[string]interface{}, property string) map[string]interface{} {
|
||||
object := jsonObject[property]
|
||||
if object != nil {
|
||||
return object.(map[string]interface{})
|
||||
}
|
||||
return nil
|
||||
}
|
106
api/http/proxy/factory/responseutils/response.go
Normal file
106
api/http/proxy/factory/responseutils/response.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package responseutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// GetResponseAsJSONOBject returns the response content as a generic JSON object
|
||||
func GetResponseAsJSONOBject(response *http.Response) (map[string]interface{}, error) {
|
||||
responseData, err := getResponseBodyAsGenericJSON(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseObject := responseData.(map[string]interface{})
|
||||
return responseObject, nil
|
||||
}
|
||||
|
||||
// GetResponseAsJSONArray returns the response content as an array of generic JSON object
|
||||
func GetResponseAsJSONArray(response *http.Response) ([]interface{}, error) {
|
||||
responseData, err := getResponseBodyAsGenericJSON(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch responseObject := responseData.(type) {
|
||||
case []interface{}:
|
||||
return responseObject, nil
|
||||
case map[string]interface{}:
|
||||
if responseObject["message"] != nil {
|
||||
return nil, errors.New(responseObject["message"].(string))
|
||||
}
|
||||
log.Printf("[ERROR] [http,proxy,response] [message: invalid response format, expecting JSON array] [response: %+v]", responseObject)
|
||||
return nil, errors.New("unable to parse response: expected JSON array, got JSON object")
|
||||
default:
|
||||
log.Printf("[ERROR] [http,proxy,response] [message: invalid response format, expecting JSON array] [response: %+v]", responseObject)
|
||||
return nil, errors.New("unable to parse response: expected JSON array")
|
||||
}
|
||||
}
|
||||
|
||||
func getResponseBodyAsGenericJSON(response *http.Response) (interface{}, error) {
|
||||
if response.Body == nil {
|
||||
return nil, errors.New("unable to parse response: empty response body")
|
||||
}
|
||||
|
||||
var data interface{}
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = response.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
type dockerErrorResponse struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// WriteAccessDeniedResponse will create a new access denied response
|
||||
func WriteAccessDeniedResponse() (*http.Response, error) {
|
||||
response := &http.Response{}
|
||||
err := RewriteResponse(response, dockerErrorResponse{Message: "access denied to resource"}, http.StatusForbidden)
|
||||
return response, err
|
||||
}
|
||||
|
||||
// RewriteAccessDeniedResponse will overwrite the existing response with an access denied response
|
||||
func RewriteAccessDeniedResponse(response *http.Response) error {
|
||||
return RewriteResponse(response, dockerErrorResponse{Message: "access denied to resource"}, http.StatusForbidden)
|
||||
}
|
||||
|
||||
// RewriteResponse will replace the existing response body and status code with the one specified
|
||||
// in parameters
|
||||
func RewriteResponse(response *http.Response, newResponseData interface{}, statusCode int) error {
|
||||
jsonData, err := json.Marshal(newResponseData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body := ioutil.NopCloser(bytes.NewReader(jsonData))
|
||||
|
||||
response.StatusCode = statusCode
|
||||
response.Body = body
|
||||
response.ContentLength = int64(len(jsonData))
|
||||
|
||||
if response.Header == nil {
|
||||
response.Header = make(http.Header)
|
||||
}
|
||||
response.Header.Set("Content-Length", strconv.Itoa(len(jsonData)))
|
||||
|
||||
return nil
|
||||
}
|
46
api/http/proxy/factory/reverse_proxy.go
Normal file
46
api/http/proxy/factory/reverse_proxy.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package factory
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// newSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
|
||||
// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
|
||||
// HTTP header, which NewSingleHostReverseProxy deliberately preserves.
|
||||
func newSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
|
||||
targetQuery := target.RawQuery
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
|
||||
req.Host = req.URL.Host
|
||||
if targetQuery == "" || req.URL.RawQuery == "" {
|
||||
req.URL.RawQuery = targetQuery + req.URL.RawQuery
|
||||
} else {
|
||||
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
||||
}
|
||||
if _, ok := req.Header["User-Agent"]; !ok {
|
||||
// explicitly disable User-Agent so it's not set to default value
|
||||
req.Header.Set("User-Agent", "")
|
||||
}
|
||||
}
|
||||
return &httputil.ReverseProxy{Director: director}
|
||||
}
|
||||
|
||||
// singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
// included here for use in NewSingleHostReverseProxyWithHostHeader
|
||||
// because its used in NewSingleHostReverseProxy from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
func singleJoiningSlash(a, b string) string {
|
||||
aslash := strings.HasSuffix(a, "/")
|
||||
bslash := strings.HasPrefix(b, "/")
|
||||
switch {
|
||||
case aslash && bslash:
|
||||
return a + b[1:]
|
||||
case !aslash && !bslash:
|
||||
return a + "/" + b
|
||||
}
|
||||
return a + b
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue