1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-05 13:55:21 +02:00

feat(extensions): introduce RBAC extension (#2900)

This commit is contained in:
Anthony Lapenna 2019-05-24 18:04:58 +12:00 committed by GitHub
parent 27a0188949
commit 8057aa45c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
196 changed files with 3321 additions and 1316 deletions

View file

@ -20,7 +20,7 @@ type (
// Returns the original object and denied access (false) when no resource control is found.
// Returns the original object and denied access (false) when a resource control is found and the user cannot access the resource.
func applyResourceAccessControlFromLabel(labelsObject, resourceObject map[string]interface{}, labelIdentifier string,
context *restrictedOperationContext) (map[string]interface{}, bool) {
context *restrictedDockerOperationContext) (map[string]interface{}, bool) {
if labelsObject != nil && labelsObject[labelIdentifier] != nil {
resourceIdentifier := labelsObject[labelIdentifier].(string)
@ -38,14 +38,14 @@ func applyResourceAccessControlFromLabel(labelsObject, resourceObject map[string
// Returns the original object and denied access (false) when a resource control is associated to the resource
// and the user cannot access the resource.
func applyResourceAccessControl(resourceObject map[string]interface{}, resourceIdentifier string,
context *restrictedOperationContext) (map[string]interface{}, bool) {
context *restrictedDockerOperationContext) (map[string]interface{}, bool) {
resourceControl := getResourceControlByResourceID(resourceIdentifier, context.resourceControls)
if resourceControl == nil {
return resourceObject, context.isAdmin
return resourceObject, context.isAdmin || context.endpointResourceAccess
}
if context.isAdmin || resourceControl.Public || canUserAccessResource(context.userID, context.userTeamIDs, resourceControl) {
if context.isAdmin || context.endpointResourceAccess || resourceControl.Public || canUserAccessResource(context.userID, context.userTeamIDs, resourceControl) {
resourceObject = decorateObject(resourceObject, resourceControl)
return resourceObject, true
}

View file

@ -24,7 +24,7 @@ func configListOperation(response *http.Response, executor *operationExecutor) e
return err
}
if executor.operationContext.isAdmin {
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
responseArray, err = decorateConfigList(responseArray, executor.operationContext.resourceControls)
} else {
responseArray, err = filterConfigList(responseArray, executor.operationContext)
@ -87,7 +87,7 @@ func decorateConfigList(configData []interface{}, resourceControls []portainer.R
// Authorized configs are decorated during the process.
// Resource controls checks are based on: resource identifier.
// Config object schema reference: https://docs.docker.com/engine/api/v1.30/#operation/ConfigList
func filterConfigList(configData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
func filterConfigList(configData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
filteredConfigData := make([]interface{}, 0)
for _, config := range configData {

View file

@ -26,7 +26,7 @@ func containerListOperation(response *http.Response, executor *operationExecutor
return err
}
if executor.operationContext.isAdmin {
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
responseArray, err = decorateContainerList(responseArray, executor.operationContext.resourceControls)
} else {
responseArray, err = filterContainerList(responseArray, executor.operationContext)
@ -137,7 +137,7 @@ func decorateContainerList(containerData []interface{}, resourceControls []porta
// Authorized containers are decorated during the process.
// Resource controls checks are based on: resource identifier, service identifier (from label), stack identifier (from label).
// Container object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
func filterContainerList(containerData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
func filterContainerList(containerData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
filteredContainerData := make([]interface{}, 0)
for _, container := range containerData {

View file

@ -24,12 +24,14 @@ type (
DockerHubService portainer.DockerHubService
SettingsService portainer.SettingsService
SignatureService portainer.DigitalSignatureService
endpointIdentifier portainer.EndpointID
}
restrictedOperationContext struct {
isAdmin bool
userID portainer.UserID
userTeamIDs []portainer.TeamID
resourceControls []portainer.ResourceControl
restrictedDockerOperationContext struct {
isAdmin bool
endpointResourceAccess bool
userID portainer.UserID
userTeamIDs []portainer.TeamID
resourceControls []portainer.ResourceControl
}
registryAccessContext struct {
isAdmin bool
@ -44,7 +46,7 @@ type (
Serveraddress string `json:"serveraddress"`
}
operationExecutor struct {
operationContext *restrictedOperationContext
operationContext *restrictedDockerOperationContext
labelBlackList []portainer.Pair
}
restrictedOperationRequest func(*http.Response, *operationExecutor) error
@ -460,7 +462,7 @@ func (p *proxyTransport) createRegistryAccessContext(request *http.Request) (*re
return accessContext, nil
}
func (p *proxyTransport) createOperationContext(request *http.Request) (*restrictedOperationContext, error) {
func (p *proxyTransport) createOperationContext(request *http.Request) (*restrictedDockerOperationContext, error) {
var err error
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
@ -472,15 +474,21 @@ func (p *proxyTransport) createOperationContext(request *http.Request) (*restric
return nil, err
}
operationContext := &restrictedOperationContext{
isAdmin: true,
userID: tokenData.ID,
resourceControls: resourceControls,
operationContext := &restrictedDockerOperationContext{
isAdmin: true,
userID: tokenData.ID,
resourceControls: resourceControls,
endpointResourceAccess: false,
}
if tokenData.Role != portainer.AdministratorRole {
operationContext.isAdmin = false
_, ok := tokenData.EndpointAuthorizations[p.endpointIdentifier][portainer.EndpointResourcesAccess]
if ok {
operationContext.endpointResourceAccess = true
}
teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
if err != nil {
return nil, err

View file

@ -40,10 +40,10 @@ func newAzureProxy(credentials *portainer.AzureCredentials) (http.Handler, error
return proxy, nil
}
func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, enableSignature bool) (http.Handler, error) {
func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, enableSignature bool, endpointID portainer.EndpointID) (http.Handler, error) {
u.Scheme = "https"
proxy := factory.createDockerReverseProxy(u, enableSignature)
proxy := factory.createDockerReverseProxy(u, enableSignature, endpointID)
config, err := crypto.CreateTLSConfigurationFromDisk(tlsConfig.TLSCACertPath, tlsConfig.TLSCertPath, tlsConfig.TLSKeyPath, tlsConfig.TLSSkipVerify)
if err != nil {
return nil, err
@ -53,12 +53,12 @@ func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portaine
return proxy, nil
}
func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, enableSignature bool) http.Handler {
func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, enableSignature bool, endpointID portainer.EndpointID) http.Handler {
u.Scheme = "http"
return factory.createDockerReverseProxy(u, enableSignature)
return factory.createDockerReverseProxy(u, enableSignature, endpointID)
}
func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignature bool) *httputil.ReverseProxy {
func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignature bool, endpointID portainer.EndpointID) *httputil.ReverseProxy {
proxy := newSingleHostReverseProxyWithHostHeader(u)
transport := &proxyTransport{
enableSignature: enableSignature,
@ -68,6 +68,7 @@ func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignatur
RegistryService: factory.RegistryService,
DockerHubService: factory.DockerHubService,
dockerTransport: &http.Transport{},
endpointIdentifier: endpointID,
}
if enableSignature {

View file

@ -4,9 +4,11 @@ package proxy
import (
"net/http"
portainer "github.com/portainer/portainer/api"
)
func (factory *proxyFactory) newLocalProxy(path string) http.Handler {
func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.EndpointID) http.Handler {
proxy := &localProxy{}
transport := &proxyTransport{
enableSignature: false,
@ -16,6 +18,7 @@ func (factory *proxyFactory) newLocalProxy(path string) http.Handler {
RegistryService: factory.RegistryService,
DockerHubService: factory.DockerHubService,
dockerTransport: newSocketTransport(path),
endpointIdentifier: endpointID,
}
proxy.Transport = transport
return proxy

View file

@ -6,10 +6,10 @@ import (
"net"
"net/http"
"github.com/Microsoft/go-winio"
portainer "github.com/portainer/portainer/api"
)
func (factory *proxyFactory) newLocalProxy(path string) http.Handler {
func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.EndpointID) http.Handler {
proxy := &localProxy{}
transport := &proxyTransport{
enableSignature: false,
@ -19,6 +19,7 @@ func (factory *proxyFactory) newLocalProxy(path string) http.Handler {
RegistryService: factory.RegistryService,
DockerHubService: factory.DockerHubService,
dockerTransport: newNamedPipeTransport(path),
endpointIdentifier: endpointID,
}
proxy.Transport = transport
return proxy

View file

@ -14,6 +14,7 @@ import (
var extensionPorts = map[portainer.ExtensionID]string{
portainer.RegistryManagementExtension: "7001",
portainer.OAuthAuthenticationExtension: "7002",
portainer.RBACExtension: "7003",
}
type (
@ -135,14 +136,14 @@ func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string)
return proxy, nil
}
func (manager *Manager) createDockerProxy(endpointURL *url.URL, tlsConfig *portainer.TLSConfiguration) (http.Handler, error) {
func (manager *Manager) createDockerProxy(endpointURL *url.URL, tlsConfig *portainer.TLSConfiguration, endpointID portainer.EndpointID) (http.Handler, error) {
if endpointURL.Scheme == "tcp" {
if tlsConfig.TLS || tlsConfig.TLSSkipVerify {
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, tlsConfig, false)
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, tlsConfig, false, endpointID)
}
return manager.proxyFactory.newDockerHTTPProxy(endpointURL, false), nil
return manager.proxyFactory.newDockerHTTPProxy(endpointURL, false, endpointID), nil
}
return manager.proxyFactory.newLocalProxy(endpointURL.Path), nil
return manager.proxyFactory.newLocalProxy(endpointURL.Path, endpointID), nil
}
func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
@ -153,10 +154,10 @@ func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler,
switch endpoint.Type {
case portainer.AgentOnDockerEnvironment:
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, true)
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, true, endpoint.ID)
case portainer.AzureEnvironment:
return newAzureProxy(&endpoint.AzureCredentials)
default:
return manager.createDockerProxy(endpointURL, &endpoint.TLSConfig)
return manager.createDockerProxy(endpointURL, &endpoint.TLSConfig, endpoint.ID)
}
}

View file

@ -24,7 +24,7 @@ func networkListOperation(response *http.Response, executor *operationExecutor)
return err
}
if executor.operationContext.isAdmin {
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
responseArray, err = decorateNetworkList(responseArray, executor.operationContext.resourceControls)
} else {
responseArray, err = filterNetworkList(responseArray, executor.operationContext)
@ -110,7 +110,7 @@ func decorateNetworkList(networkData []interface{}, resourceControls []portainer
// Authorized networks are decorated during the process.
// Resource controls checks are based on: resource identifier, stack identifier (from label).
// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
func filterNetworkList(networkData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
func filterNetworkList(networkData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
filteredNetworkData := make([]interface{}, 0)
for _, network := range networkData {

View file

@ -24,7 +24,7 @@ func secretListOperation(response *http.Response, executor *operationExecutor) e
return err
}
if executor.operationContext.isAdmin {
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
responseArray, err = decorateSecretList(responseArray, executor.operationContext.resourceControls)
} else {
responseArray, err = filterSecretList(responseArray, executor.operationContext)
@ -87,7 +87,7 @@ func decorateSecretList(secretData []interface{}, resourceControls []portainer.R
// Authorized secrets are decorated during the process.
// Resource controls checks are based on: resource identifier.
// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
func filterSecretList(secretData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
func filterSecretList(secretData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
filteredSecretData := make([]interface{}, 0)
for _, secret := range secretData {

View file

@ -24,7 +24,7 @@ func serviceListOperation(response *http.Response, executor *operationExecutor)
return err
}
if executor.operationContext.isAdmin {
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
responseArray, err = decorateServiceList(responseArray, executor.operationContext.resourceControls)
} else {
responseArray, err = filterServiceList(responseArray, executor.operationContext)
@ -118,7 +118,7 @@ func decorateServiceList(serviceData []interface{}, resourceControls []portainer
// Authorized services are decorated during the process.
// Resource controls checks are based on: resource identifier, stack identifier (from label).
// Service object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
func filterServiceList(serviceData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
func filterServiceList(serviceData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
filteredServiceData := make([]interface{}, 0)
for _, service := range serviceData {

View file

@ -25,7 +25,7 @@ func taskListOperation(response *http.Response, executor *operationExecutor) err
return err
}
if !executor.operationContext.isAdmin {
if !executor.operationContext.isAdmin && !executor.operationContext.endpointResourceAccess {
responseArray, err = filterTaskList(responseArray, executor.operationContext)
if err != nil {
return err
@ -54,7 +54,7 @@ func extractTaskLabelsFromTaskListObject(responseObject map[string]interface{})
// Resource controls checks are based on: service identifier, stack identifier (from label).
// Task object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList
// any resource control giving access to the user based on the associated service identifier.
func filterTaskList(taskData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
func filterTaskList(taskData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
filteredTaskData := make([]interface{}, 0)
for _, task := range taskData {

View file

@ -29,7 +29,7 @@ func volumeListOperation(response *http.Response, executor *operationExecutor) e
if responseObject["Volumes"] != nil {
volumeData := responseObject["Volumes"].([]interface{})
if executor.operationContext.isAdmin {
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
volumeData, err = decorateVolumeList(volumeData, executor.operationContext.resourceControls)
} else {
volumeData, err = filterVolumeList(volumeData, executor.operationContext)
@ -119,7 +119,7 @@ func decorateVolumeList(volumeData []interface{}, resourceControls []portainer.R
// Authorized volumes are decorated during the process.
// Resource controls checks are based on: resource identifier, stack identifier (from label).
// Volume object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
func filterVolumeList(volumeData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
func filterVolumeList(volumeData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
filteredVolumeData := make([]interface{}, 0)
for _, volume := range volumeData {