diff --git a/api/bolt/migrator/migrate_ce.go b/api/bolt/migrator/migrate_ce.go index 53ed9442a..a7dbda016 100644 --- a/api/bolt/migrator/migrate_ce.go +++ b/api/bolt/migrator/migrate_ce.go @@ -287,6 +287,14 @@ func (m *Migrator) MigrateCE() error { } } + // Portainer EE-2.4.0 + if m.currentDBVersion < 30 { + err := m.updateRbacRolesToDB30() + if err != nil { + return err + } + } + log.Println("Update DB version to ", portainer.DBVersion) return m.versionService.StoreDBVersion(portainer.DBVersion) } diff --git a/api/bolt/migrator/migrate_dbversion29.go b/api/bolt/migrator/migrate_dbversion29.go new file mode 100644 index 000000000..eabc110a1 --- /dev/null +++ b/api/bolt/migrator/migrate_dbversion29.go @@ -0,0 +1,31 @@ +package migrator + +import ( + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/authorization" +) + +func (m *Migrator) updateRbacRolesToDB30() error { + defaultAuthorizationsOfRoles := map[portainer.RoleID]portainer.Authorizations{ + portainer.RoleIDEndpointAdmin: authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole(), + portainer.RoleIDHelpdesk: authorization.DefaultEndpointAuthorizationsForHelpDeskRole(), + portainer.RoleIDOperator: authorization.DefaultEndpointAuthorizationsForOperatorRole(), + portainer.RoleIDStandardUser: authorization.DefaultEndpointAuthorizationsForStandardUserRole(), + portainer.RoleIDReadonly: authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(), + } + + for roleID, defaultAuthorizations := range defaultAuthorizationsOfRoles { + role, err := m.roleService.Role(roleID) + if err != nil { + return err + } + role.Authorizations = defaultAuthorizations + + err = m.roleService.UpdateRole(role.ID, role) + if err != nil { + return err + } + } + + return m.authorizationService.UpdateUsersAuthorizations() +} diff --git a/api/http/handler/endpointproxy/proxy_azure.go b/api/http/handler/endpointproxy/proxy_azure.go index 9984b763f..28dc83a3c 100644 --- a/api/http/handler/endpointproxy/proxy_azure.go +++ b/api/http/handler/endpointproxy/proxy_azure.go @@ -24,7 +24,7 @@ func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.R return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} } - err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false) + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) if err != nil { return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } diff --git a/api/http/proxy/factory/azure/access_control.go b/api/http/proxy/factory/azure/access_control.go index 75f8e5aab..967dace89 100644 --- a/api/http/proxy/factory/azure/access_control.go +++ b/api/http/proxy/factory/azure/access_control.go @@ -22,14 +22,22 @@ func (transport *Transport) createAzureRequestContext(request *http.Request) (*a } context := &azureRequestContext{ - isAdmin: true, - userID: tokenData.ID, - resourceControls: resourceControls, + isAdmin: true, + userID: tokenData.ID, + resourceControls: resourceControls, + endpointResourceAccess: false, } if tokenData.Role != portainer.AdministratorRole { context.isAdmin = false + user, err := transport.dataStore.User().User(context.userID) + if err != nil { + return nil, err + } + + _, context.endpointResourceAccess = user.EndpointAuthorizations[transport.endpoint.ID][portainer.EndpointResourcesAccess] + teamMemberships, err := transport.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID) if err != nil { return nil, err @@ -72,7 +80,7 @@ func (transport *Transport) createPrivateResourceControl( } func (transport *Transport) userCanDeleteContainerGroup(request *http.Request, context *azureRequestContext) bool { - if context.isAdmin { + if context.isAdmin || context.endpointResourceAccess { return true } resourceIdentifier := request.URL.Path @@ -119,7 +127,7 @@ func (transport *Transport) filterContainerGroups(containerGroups []interface{}, } } - if context.isAdmin || userCanAccessResource { + if context.isAdmin || context.endpointResourceAccess || userCanAccessResource { filteredContainerGroups = append(filteredContainerGroups, containerGroup) } } diff --git a/api/http/proxy/factory/azure/transport.go b/api/http/proxy/factory/azure/transport.go index b8b0c3a1f..e90695e73 100644 --- a/api/http/proxy/factory/azure/transport.go +++ b/api/http/proxy/factory/azure/transport.go @@ -27,10 +27,11 @@ type ( } azureRequestContext struct { - isAdmin bool - userID portainer.UserID - userTeamIDs []portainer.TeamID - resourceControls []portainer.ResourceControl + isAdmin bool + endpointResourceAccess bool + userID portainer.UserID + userTeamIDs []portainer.TeamID + resourceControls []portainer.ResourceControl } ) diff --git a/api/http/security/rabc_azure.go b/api/http/security/rabc_azure.go new file mode 100644 index 000000000..b7bc53ea0 --- /dev/null +++ b/api/http/security/rabc_azure.go @@ -0,0 +1,103 @@ +package security + +import ( + portainer "github.com/portainer/portainer/api" + "net/http" + "path" + "strings" +) + +func getAzureOperationAuthorization(url, method string) portainer.Authorization { + url = strings.Split(url, "?")[0] + if matched, _ := path.Match("/subscriptions", url); matched { + return azureSubscriptionsOperationAuthorization(url, method) + } else if matched, _ := path.Match("/subscriptions/*", url); matched { + return azureSubscriptionOperationAuthorization(url, method) + } else if matched, _ := path.Match("/subscriptions/*/providers/*", url); matched { + return azureProviderOperationAuthorization(url, method) + } else if matched, _ := path.Match("/subscriptions/*/resourcegroups", url); matched { + return azureResourceGroupsOperationAuthorization(url, method) + } else if matched, _ := path.Match("/subscriptions/*/resourcegroups/*", url); matched { + return azureResourceGroupOperationAuthorization(url, method) + } else if matched, _ := path.Match("/subscriptions/*/providers/*/containerGroups", url); matched { + return azureContainerGroupsOperationAuthorization(url, method) + } else if matched, _ := path.Match("/subscriptions/*/resourceGroups/*/providers/*/containerGroups/*", url); matched { + return azureContainerGroupOperationAuthorization(url, method) + } + + return portainer.OperationAzureUndefined +} + +// /subscriptions +func azureSubscriptionsOperationAuthorization(url, method string) portainer.Authorization { + switch method { + case http.MethodGet: + return portainer.OperationAzureSubscriptionsList + default: + return portainer.OperationAzureUndefined + } +} + +// /subscriptions/* +func azureSubscriptionOperationAuthorization(url, method string) portainer.Authorization { + switch method { + case http.MethodGet: + return portainer.OperationAzureSubscriptionGet + default: + return portainer.OperationAzureUndefined + } +} + +// /subscriptions/*/resourcegroups +func azureResourceGroupsOperationAuthorization(url, method string) portainer.Authorization { + switch method { + case http.MethodGet: + return portainer.OperationAzureResourceGroupsList + default: + return portainer.OperationAzureUndefined + } +} + +// /subscriptions/*/resourcegroups/* +func azureResourceGroupOperationAuthorization(url, method string) portainer.Authorization { + switch method { + case http.MethodGet: + return portainer.OperationAzureResourceGroupGet + default: + return portainer.OperationAzureUndefined + } +} + +// /subscriptions/*/providers/* +func azureProviderOperationAuthorization(url, method string) portainer.Authorization { + switch method { + case http.MethodGet: + return portainer.OperationAzureProviderGet + default: + return portainer.OperationAzureUndefined + } +} + +// /subscriptions/*/providers/Microsoft.ContainerInstance/containerGroups +func azureContainerGroupsOperationAuthorization(url, method string) portainer.Authorization { + switch method { + case http.MethodGet: + return portainer.OperationAzureContainerGroupsList + default: + return portainer.OperationAzureUndefined + } +} + +// /subscriptions/*/resourceGroups/*/providers/Microsoft.ContainerInstance/containerGroups/* +func azureContainerGroupOperationAuthorization(url, method string) portainer.Authorization { + switch method { + case http.MethodPut: + return portainer.OperationAzureContainerGroupCreate + case http.MethodGet: + return portainer.OperationAzureContainerGroupGet + case http.MethodDelete: + return portainer.OperationAzureContainerGroupDelete + default: + return portainer.OperationAzureUndefined + } +} diff --git a/api/http/security/rbac.go b/api/http/security/rbac.go index df3ced27a..631eb91f0 100644 --- a/api/http/security/rbac.go +++ b/api/http/security/rbac.go @@ -17,6 +17,7 @@ func authorizedOperation(operation *portainer.APIOperationAuthorizationRequest) var dockerRule = regexp.MustCompile(`/(?P\d+)/docker(?P/.*)`) var storidgeRule = regexp.MustCompile(`/(?P\d+)/storidge(?P/.*)`) var k8sRule = regexp.MustCompile(`/(?P\d+)/kubernetes(?P/.*)`) +var azureRule = regexp.MustCompile(`/(?P\d+)/azure(?P/.*)`) func extractMatches(regex *regexp.Regexp, str string) map[string]string { match := regex.FindStringSubmatch(str) @@ -49,6 +50,9 @@ func getOperationAuthorization(url, method string) portainer.Authorization { // the current endpoint. The namespace + resource authorization // is done in the k8s level. return portainer.OperationK8sResourcePoolsR + } else if azureRule.MatchString(url) { + match := azureRule.FindStringSubmatch(url) + return getAzureOperationAuthorization(strings.TrimPrefix(url, "/"+match[1]+"/azure"), method) } return getPortainerOperationAuthorization(url, method) diff --git a/api/internal/authorization/authorizations.go b/api/internal/authorization/authorizations.go index 334128662..b24144ca7 100644 --- a/api/internal/authorization/authorizations.go +++ b/api/internal/authorization/authorizations.go @@ -157,7 +157,10 @@ func DefaultEndpointAuthorizationsForEndpointAdministratorRole() portainer.Autho portainer.OperationPortainerEndpointUpdateSettings: true, portainer.OperationIntegrationStoridgeAdmin: true, portainer.EndpointResourcesAccess: true, - }, DefaultK8sClusterAuthorizations()[portainer.RoleIDEndpointAdmin]) + }, + DefaultK8sClusterAuthorizations()[portainer.RoleIDEndpointAdmin], + DefaultAzureAuthorizations()[portainer.RoleIDEndpointAdmin], + ) } // DefaultEndpointAuthorizationsForHelpDeskRole returns the default endpoint authorizations @@ -209,7 +212,10 @@ func DefaultEndpointAuthorizationsForHelpDeskRole() portainer.Authorizations { portainer.OperationPortainerStackFile: true, portainer.OperationPortainerWebhookList: true, portainer.EndpointResourcesAccess: true, - }, DefaultK8sClusterAuthorizations()[portainer.RoleIDHelpdesk]) + }, + DefaultK8sClusterAuthorizations()[portainer.RoleIDHelpdesk], + DefaultAzureAuthorizations()[portainer.RoleIDHelpdesk], + ) return authorizations } @@ -276,7 +282,10 @@ func DefaultEndpointAuthorizationsForOperatorRole() portainer.Authorizations { portainer.OperationPortainerWebsocketExec: true, portainer.OperationPortainerWebhookList: true, portainer.EndpointResourcesAccess: true, - }, DefaultK8sClusterAuthorizations()[portainer.RoleIDOperator]) + }, + DefaultK8sClusterAuthorizations()[portainer.RoleIDOperator], + DefaultAzureAuthorizations()[portainer.RoleIDOperator], + ) return authorizations } @@ -403,7 +412,10 @@ func DefaultEndpointAuthorizationsForStandardUserRole() portainer.Authorizations portainer.OperationPortainerWebsocketExec: true, portainer.OperationPortainerWebhookList: true, portainer.OperationPortainerWebhookCreate: true, - }, DefaultK8sClusterAuthorizations()[portainer.RoleIDStandardUser]) + }, + DefaultK8sClusterAuthorizations()[portainer.RoleIDStandardUser], + DefaultAzureAuthorizations()[portainer.RoleIDStandardUser], + ) return authorizations } @@ -456,7 +468,10 @@ func DefaultEndpointAuthorizationsForReadOnlyUserRole() portainer.Authorizations portainer.OperationPortainerStackInspect: true, portainer.OperationPortainerStackFile: true, portainer.OperationPortainerWebhookList: true, - }, DefaultK8sClusterAuthorizations()[portainer.RoleIDReadonly]) + }, + DefaultK8sClusterAuthorizations()[portainer.RoleIDReadonly], + DefaultAzureAuthorizations()[portainer.RoleIDReadonly], + ) return authorizations } diff --git a/api/internal/authorization/azure_authorizations_default.go b/api/internal/authorization/azure_authorizations_default.go new file mode 100644 index 000000000..45813edc9 --- /dev/null +++ b/api/internal/authorization/azure_authorizations_default.go @@ -0,0 +1,60 @@ +package authorization + +import ( + portainer "github.com/portainer/portainer/api" +) + +// DefaultAzureAuthorizations returns a set of default azure authorizations based on user's role. +func DefaultAzureAuthorizations() map[portainer.RoleID]portainer.Authorizations { + return map[portainer.RoleID]portainer.Authorizations{ + portainer.RoleIDEndpointAdmin: { + portainer.OperationAzureSubscriptionsList: true, + portainer.OperationAzureSubscriptionGet: true, + portainer.OperationAzureProviderGet: true, + portainer.OperationAzureResourceGroupsList: true, + portainer.OperationAzureResourceGroupGet: true, + portainer.OperationAzureContainerGroupsList: true, + portainer.OperationAzureContainerGroupGet: true, + portainer.OperationAzureContainerGroupCreate: true, + portainer.OperationAzureContainerGroupDelete: true, + }, + portainer.RoleIDOperator: { + portainer.OperationAzureSubscriptionsList: true, + portainer.OperationAzureSubscriptionGet: true, + portainer.OperationAzureProviderGet: true, + portainer.OperationAzureResourceGroupsList: true, + portainer.OperationAzureResourceGroupGet: true, + portainer.OperationAzureContainerGroupsList: true, + portainer.OperationAzureContainerGroupGet: true, + }, + portainer.RoleIDHelpdesk: { + portainer.OperationAzureSubscriptionsList: true, + portainer.OperationAzureSubscriptionGet: true, + portainer.OperationAzureProviderGet: true, + portainer.OperationAzureResourceGroupsList: true, + portainer.OperationAzureResourceGroupGet: true, + portainer.OperationAzureContainerGroupsList: true, + portainer.OperationAzureContainerGroupGet: true, + }, + portainer.RoleIDStandardUser: { + portainer.OperationAzureSubscriptionsList: true, + portainer.OperationAzureSubscriptionGet: true, + portainer.OperationAzureProviderGet: true, + portainer.OperationAzureResourceGroupsList: true, + portainer.OperationAzureResourceGroupGet: true, + portainer.OperationAzureContainerGroupsList: true, + portainer.OperationAzureContainerGroupGet: true, + portainer.OperationAzureContainerGroupCreate: true, + portainer.OperationAzureContainerGroupDelete: true, + }, + portainer.RoleIDReadonly: { + portainer.OperationAzureSubscriptionsList: true, + portainer.OperationAzureSubscriptionGet: true, + portainer.OperationAzureProviderGet: true, + portainer.OperationAzureResourceGroupsList: true, + portainer.OperationAzureResourceGroupGet: true, + portainer.OperationAzureContainerGroupsList: true, + portainer.OperationAzureContainerGroupGet: true, + }, + } +} diff --git a/api/portainer.go b/api/portainer.go index ca18cd82d..bb1a0d6cd 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1283,9 +1283,9 @@ const ( // APIVersion is the version number of the Portainer API APIVersion = "2.0.2" // DBVersion is the version number of the Portainer CE database - DBVersion = 28 + DBVersion = 30 // DBVersionEE is the version number of the Portainer EE database - DBVersionEE = 28 + DBVersionEE = 30 // Edition is the edition of the Portainer API Edition = PortainerEE // ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax @@ -1685,6 +1685,16 @@ const ( OperationDockerAgentBrowsePut Authorization = "DockerAgentBrowsePut" OperationDockerAgentBrowseRename Authorization = "DockerAgentBrowseRename" + OperationAzureSubscriptionsList Authorization = "AzureSubscriptionsList" + OperationAzureSubscriptionGet Authorization = "AzureSubscriptionGet" + OperationAzureProviderGet Authorization = "AzureProviderGet" + OperationAzureResourceGroupsList Authorization = "AzureResourceGroupsList" + OperationAzureResourceGroupGet Authorization = "AzureResourceGroupGet" + OperationAzureContainerGroupsList Authorization = "AzureContainerGroupsList" + OperationAzureContainerGroupGet Authorization = "AzureContainerGroupGet" + OperationAzureContainerGroupCreate Authorization = "AzureContainerGroupCreate" + OperationAzureContainerGroupDelete Authorization = "AzureContainerGroupDelete" + OperationPortainerDockerHubInspect Authorization = "PortainerDockerHubInspect" OperationPortainerDockerHubUpdate Authorization = "PortainerDockerHubUpdate" OperationPortainerEndpointGroupCreate Authorization = "PortainerEndpointGroupCreate" @@ -1777,6 +1787,7 @@ const ( OperationIntegrationStoridgeAdmin Authorization = "IntegrationStoridgeAdmin" OperationDockerUndefined Authorization = "DockerUndefined" + OperationAzureUndefined Authorization = "AzureUndefined" OperationDockerAgentUndefined Authorization = "DockerAgentUndefined" OperationPortainerUndefined Authorization = "PortainerUndefined" diff --git a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html index 62fa95acf..d406e5cc7 100644 --- a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html +++ b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html @@ -5,10 +5,16 @@
{{ $ctrl.titleText }}
- -
@@ -29,7 +35,7 @@ - + @@ -64,7 +70,7 @@ ng-class="{ active: item.Checked }" > - + @@ -85,10 +91,10 @@ - Loading... + Loading... - No container available. + No container available.