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:
parent
27a0188949
commit
8057aa45c4
196 changed files with 3321 additions and 1316 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue