feat(ACI): EE-261 Add RBAC to ACI (#226)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
31
api/bolt/migrator/migrate_dbversion29.go
Normal file
31
api/bolt/migrator/migrate_dbversion29.go
Normal file
@@ -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()
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
103
api/http/security/rabc_azure.go
Normal file
103
api/http/security/rabc_azure.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ func authorizedOperation(operation *portainer.APIOperationAuthorizationRequest)
|
||||
var dockerRule = regexp.MustCompile(`/(?P<identifier>\d+)/docker(?P<operation>/.*)`)
|
||||
var storidgeRule = regexp.MustCompile(`/(?P<identifier>\d+)/storidge(?P<operation>/.*)`)
|
||||
var k8sRule = regexp.MustCompile(`/(?P<identifier>\d+)/kubernetes(?P<operation>/.*)`)
|
||||
var azureRule = regexp.MustCompile(`/(?P<identifier>\d+)/azure(?P<operation>/.*)`)
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
60
api/internal/authorization/azure_authorizations_default.go
Normal file
60
api/internal/authorization/azure_authorizations_default.go
Normal file
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -5,10 +5,16 @@
|
||||
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
authorization="AzureContainerGroupDelete"
|
||||
>
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="azure.containerinstances.new">
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="azure.containerinstances.new" authorization="AzureContainerGroupCreate">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add container
|
||||
</button>
|
||||
</div>
|
||||
@@ -29,7 +35,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<span class="md-checkbox" authorization="AzureContainerGroupDelete">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
@@ -64,7 +70,7 @@
|
||||
ng-class="{ active: item.Checked }"
|
||||
>
|
||||
<td>
|
||||
<span class="md-checkbox">
|
||||
<span class="md-checkbox" authorization="AzureContainerGroupDelete">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" />
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
@@ -85,10 +91,10 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="3" class="text-center text-muted">Loading...</td>
|
||||
<td colspan="4" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="3" class="text-center text-muted">No container available.</td>
|
||||
<td colspan="4" class="text-center text-muted">No container available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user