feat(ACI): EE-261 Add RBAC to ACI (#226)

Co-authored-by: Simon Meng <simon.meng@portainer.io>
This commit is contained in:
cong meng
2021-04-09 12:20:33 +12:00
committed by GitHub
parent 4682056058
commit 6eb3dfd3c2
11 changed files with 270 additions and 23 deletions

View File

@@ -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)
}

View 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()
}

View File

@@ -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}
}

View File

@@ -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)
}
}

View File

@@ -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
}
)

View 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
}
}

View File

@@ -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)

View File

@@ -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
}

View 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,
},
}
}

View File

@@ -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"

View File

@@ -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>