feat(UAC): change default ownership to admininstrators (#2137)

* #960 feat(UAC): change ownership to admins for externally created ressources

* feat(UAC): change ownership to admins for externally created resources

Deprecated AdministratorsOnly js and go backend

* #960 feat(UAC): remove AdministratorsOnly property and minor GUI  fixes

Update swagger definition changing AdministratorsOnly to Public

* #960 feat(UAC): fix create resource with access control data

* #960 feat(UAC): authorization of non-admin users for restricted operations

On stacks, containers networks, services , tasks and volumes.

* #960 feat(UAC): database migration to version 14

 The administrator resources are deleted and Public resources are now managed by admins

* #960 feat(UAC):  small fixes from PR #2137

* #960 feat(UAC): improve the readability of the source code

* feat(UAC) fix displayed ownership for Swarm related  resources  (#960)
pull/2193/head
Ricardo Cardona Ramirez 2018-08-19 00:57:28 -05:00 committed by Anthony Lapenna
parent 31c2a6d9e7
commit e1e263d8c8
30 changed files with 206 additions and 179 deletions

View File

@ -0,0 +1,19 @@
package migrator
func (m *Migrator) updateResourceControlsToDBVersion14() error {
resourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return err
}
for _, resourceControl := range resourceControls {
if resourceControl.AdministratorsOnly == true {
err = m.resourceControlService.DeleteResourceControl(resourceControl.ID)
if err != nil {
return err
}
}
}
return nil
}

View File

@ -178,5 +178,13 @@ func (m *Migrator) Migrate() error {
}
}
// 1.19.2-dev
if m.currentDBVersion < 14 {
err := m.updateResourceControlsToDBVersion14()
if err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion)
}

View File

@ -12,12 +12,12 @@ import (
)
type resourceControlCreatePayload struct {
ResourceID string
Type string
AdministratorsOnly bool
Users []int
Teams []int
SubResourceIDs []string
ResourceID string
Type string
Public bool
Users []int
Teams []int
SubResourceIDs []string
}
func (payload *resourceControlCreatePayload) Validate(r *http.Request) error {
@ -29,8 +29,8 @@ func (payload *resourceControlCreatePayload) Validate(r *http.Request) error {
return portainer.Error("Invalid type")
}
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.AdministratorsOnly {
return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or AdministratorOnly")
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public {
return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or Public")
}
return nil
}
@ -90,12 +90,12 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req
}
resourceControl := portainer.ResourceControl{
ResourceID: payload.ResourceID,
SubResourceIDs: payload.SubResourceIDs,
Type: resourceControlType,
AdministratorsOnly: payload.AdministratorsOnly,
UserAccesses: userAccesses,
TeamAccesses: teamAccesses,
ResourceID: payload.ResourceID,
SubResourceIDs: payload.SubResourceIDs,
Type: resourceControlType,
Public: payload.Public,
UserAccesses: userAccesses,
TeamAccesses: teamAccesses,
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)

View File

@ -11,14 +11,14 @@ import (
)
type resourceControlUpdatePayload struct {
AdministratorsOnly bool
Users []int
Teams []int
Public bool
Users []int
Teams []int
}
func (payload *resourceControlUpdatePayload) Validate(r *http.Request) error {
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.AdministratorsOnly {
return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or AdministratorOnly")
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public {
return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or Public")
}
return nil
}
@ -52,7 +52,7 @@ func (handler *Handler) resourceControlUpdate(w http.ResponseWriter, r *http.Req
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the resource control", portainer.ErrResourceAccessDenied}
}
resourceControl.AdministratorsOnly = payload.AdministratorsOnly
resourceControl.Public = payload.Public
var userAccesses = make([]portainer.UserResourceAccess, 0)
for _, v := range payload.Users {

View File

@ -48,8 +48,8 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
if resourceControl != nil {
if !securityContext.IsAdmin && !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
if !securityContext.IsAdmin {
if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
}
}

View File

@ -41,6 +41,10 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
}
extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}}
if !securityContext.IsAdmin && resourceControl == nil {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
}
if resourceControl != nil {
if securityContext.IsAdmin || proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
extendedStack.ResourceControl = *resourceControl

View File

@ -36,6 +36,10 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
}
extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}}
if !securityContext.IsAdmin && resourceControl == nil {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
}
if resourceControl != nil {
if securityContext.IsAdmin || proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
extendedStack.ResourceControl = *resourceControl

View File

@ -53,8 +53,8 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
if resourceControl != nil {
if !securityContext.IsAdmin && !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
if !securityContext.IsAdmin {
if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
}
}

View File

@ -62,8 +62,8 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
if resourceControl != nil {
if !securityContext.IsAdmin && !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
if !securityContext.IsAdmin {
if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
}
}

View File

@ -1,6 +1,8 @@
package proxy
import "github.com/portainer/portainer"
import (
"github.com/portainer/portainer"
)
type (
// ExtendedStack represents a stack combined with its associated access control
@ -15,7 +17,7 @@ type (
// It will retrieve an identifier from the labels object. If an identifier exists, it will check for
// an existing resource control associated to it.
// Returns a decorated object and authorized access (true) when a resource control is found and the user can access the resource.
// Returns the original object and authorized access (true) when no resource control is found.
// 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) {
@ -24,32 +26,31 @@ func applyResourceAccessControlFromLabel(labelsObject, resourceObject map[string
resourceIdentifier := labelsObject[labelIdentifier].(string)
return applyResourceAccessControl(resourceObject, resourceIdentifier, context)
}
return resourceObject, true
return resourceObject, false
}
// applyResourceAccessControl returns an optionally decorated object as the first return value and the
// access level for the user (granted or denied) as the second return value.
// Returns a decorated object and authorized access (true) when a resource control is found to the specified resource
// identifier and the user can access the resource.
// Returns the original object and authorized access (true) when no resource control is found for the specified
// Returns the original object and authorized access (false) when no resource control is found for the specified
// resource identifier.
// 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) {
authorizedAccess := true
resourceControl := getResourceControlByResourceID(resourceIdentifier, context.resourceControls)
if resourceControl != nil {
if context.isAdmin || canUserAccessResource(context.userID, context.userTeamIDs, resourceControl) {
resourceObject = decorateObject(resourceObject, resourceControl)
} else {
authorizedAccess = false
}
if resourceControl == nil {
return resourceObject, context.isAdmin
}
return resourceObject, authorizedAccess
if context.isAdmin || resourceControl.Public || canUserAccessResource(context.userID, context.userTeamIDs, resourceControl) {
resourceObject = decorateObject(resourceObject, resourceControl)
return resourceObject, true
}
return resourceObject, false
}
// decorateResourceWithAccessControlFromLabel will retrieve an identifier from the labels object. If an identifier exists,
@ -94,7 +95,7 @@ func canUserAccessResource(userID portainer.UserID, userTeamIDs []portainer.Team
}
}
return false
return resourceControl.Public
}
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
@ -123,6 +124,10 @@ func getResourceControlByResourceID(resourceID string, resourceControls []portai
// CanAccessStack checks if a user can access a stack
func CanAccessStack(stack *portainer.Stack, resourceControl *portainer.ResourceControl, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
if resourceControl == nil {
return false
}
userTeamIDs := make([]portainer.TeamID, 0)
for _, membership := range memberships {
userTeamIDs = append(userTeamIDs, membership.TeamID)
@ -132,7 +137,7 @@ func CanAccessStack(stack *portainer.Stack, resourceControl *portainer.ResourceC
return true
}
return false
return resourceControl.Public
}
// FilterStacks filters stacks based on user role and resource controls.
@ -149,9 +154,9 @@ func FilterStacks(stacks []portainer.Stack, resourceControls []portainer.Resourc
for _, stack := range stacks {
extendedStack := ExtendedStack{stack, portainer.ResourceControl{}}
resourceControl := getResourceControlByResourceID(stack.Name, resourceControls)
if resourceControl == nil {
if resourceControl == nil && isAdmin {
filteredStacks = append(filteredStacks, extendedStack)
} else if resourceControl != nil && (isAdmin || canUserAccessResource(userID, userTeamIDs, resourceControl)) {
} else if resourceControl != nil && (isAdmin || resourceControl.Public || canUserAccessResource(userID, userTeamIDs, resourceControl)) {
extendedStack.ResourceControl = *resourceControl
filteredStacks = append(filteredStacks, extendedStack)
}

View File

@ -62,27 +62,27 @@ func containerInspectOperation(response *http.Response, executor *operationExecu
containerID := responseObject[containerIdentifier].(string)
responseObject, access := applyResourceAccessControl(responseObject, containerID, executor.operationContext)
if !access {
return rewriteAccessDeniedResponse(response)
if access {
return rewriteResponse(response, responseObject, http.StatusOK)
}
containerLabels := extractContainerLabelsFromContainerInspectObject(responseObject)
responseObject, access = applyResourceAccessControlFromLabel(containerLabels, responseObject, containerLabelForServiceIdentifier, executor.operationContext)
if !access {
return rewriteAccessDeniedResponse(response)
if access {
return rewriteResponse(response, responseObject, http.StatusOK)
}
responseObject, access = applyResourceAccessControlFromLabel(containerLabels, responseObject, containerLabelForSwarmStackIdentifier, executor.operationContext)
if !access {
return rewriteAccessDeniedResponse(response)
if access {
return rewriteResponse(response, responseObject, http.StatusOK)
}
responseObject, access = applyResourceAccessControlFromLabel(containerLabels, responseObject, containerLabelForComposeStackIdentifier, executor.operationContext)
if !access {
return rewriteAccessDeniedResponse(response)
if access {
return rewriteResponse(response, responseObject, http.StatusOK)
}
return rewriteResponse(response, responseObject, http.StatusOK)
return rewriteAccessDeniedResponse(response)
}
// extractContainerLabelsFromContainerInspectObject retrieve the Labels of the container if present.
@ -148,19 +148,20 @@ func filterContainerList(containerData []interface{}, context *restrictedOperati
containerID := containerObject[containerIdentifier].(string)
containerObject, access := applyResourceAccessControl(containerObject, containerID, context)
if access {
if !access {
containerLabels := extractContainerLabelsFromContainerListObject(containerObject)
containerObject, access = applyResourceAccessControlFromLabel(containerLabels, containerObject, containerLabelForComposeStackIdentifier, context)
if access {
if !access {
containerObject, access = applyResourceAccessControlFromLabel(containerLabels, containerObject, containerLabelForServiceIdentifier, context)
if access {
if !access {
containerObject, access = applyResourceAccessControlFromLabel(containerLabels, containerObject, containerLabelForSwarmStackIdentifier, context)
if access {
filteredContainerData = append(filteredContainerData, containerObject)
}
}
}
}
if access {
filteredContainerData = append(filteredContainerData, containerObject)
}
}
return filteredContainerData, nil

View File

@ -53,17 +53,17 @@ func networkInspectOperation(response *http.Response, executor *operationExecuto
networkID := responseObject[networkIdentifier].(string)
responseObject, access := applyResourceAccessControl(responseObject, networkID, executor.operationContext)
if !access {
return rewriteAccessDeniedResponse(response)
if access {
return rewriteResponse(response, responseObject, http.StatusOK)
}
networkLabels := extractNetworkLabelsFromNetworkInspectObject(responseObject)
responseObject, access = applyResourceAccessControlFromLabel(networkLabels, responseObject, networkLabelForStackIdentifier, executor.operationContext)
if !access {
return rewriteAccessDeniedResponse(response)
if access {
return rewriteResponse(response, responseObject, http.StatusOK)
}
return rewriteResponse(response, responseObject, http.StatusOK)
return rewriteAccessDeniedResponse(response)
}
// extractNetworkLabelsFromNetworkInspectObject retrieve the Labels of the network if present.
@ -121,12 +121,13 @@ func filterNetworkList(networkData []interface{}, context *restrictedOperationCo
networkID := networkObject[networkIdentifier].(string)
networkObject, access := applyResourceAccessControl(networkObject, networkID, context)
if access {
if !access {
networkLabels := extractNetworkLabelsFromNetworkListObject(networkObject)
networkObject, access = applyResourceAccessControlFromLabel(networkLabels, networkObject, networkLabelForStackIdentifier, context)
if access {
filteredNetworkData = append(filteredNetworkData, networkObject)
}
}
if access {
filteredNetworkData = append(filteredNetworkData, networkObject)
}
}

View File

@ -53,17 +53,17 @@ func serviceInspectOperation(response *http.Response, executor *operationExecuto
serviceID := responseObject[serviceIdentifier].(string)
responseObject, access := applyResourceAccessControl(responseObject, serviceID, executor.operationContext)
if !access {
return rewriteAccessDeniedResponse(response)
if access {
return rewriteResponse(response, responseObject, http.StatusOK)
}
serviceLabels := extractServiceLabelsFromServiceInspectObject(responseObject)
responseObject, access = applyResourceAccessControlFromLabel(serviceLabels, responseObject, serviceLabelForStackIdentifier, executor.operationContext)
if !access {
return rewriteAccessDeniedResponse(response)
if access {
return rewriteResponse(response, responseObject, http.StatusOK)
}
return rewriteResponse(response, responseObject, http.StatusOK)
return rewriteAccessDeniedResponse(response)
}
// extractServiceLabelsFromServiceInspectObject retrieve the Labels of the service if present.
@ -129,12 +129,13 @@ func filterServiceList(serviceData []interface{}, context *restrictedOperationCo
serviceID := serviceObject[serviceIdentifier].(string)
serviceObject, access := applyResourceAccessControl(serviceObject, serviceID, context)
if access {
if !access {
serviceLabels := extractServiceLabelsFromServiceListObject(serviceObject)
serviceObject, access = applyResourceAccessControlFromLabel(serviceLabels, serviceObject, serviceLabelForStackIdentifier, context)
if access {
filteredServiceData = append(filteredServiceData, serviceObject)
}
}
if access {
filteredServiceData = append(filteredServiceData, serviceObject)
}
}

View File

@ -65,12 +65,13 @@ func filterTaskList(taskData []interface{}, context *restrictedOperationContext)
serviceID := taskObject[taskServiceIdentifier].(string)
taskObject, access := applyResourceAccessControl(taskObject, serviceID, context)
if access {
if !access {
taskLabels := extractTaskLabelsFromTaskListObject(taskObject)
taskObject, access = applyResourceAccessControlFromLabel(taskLabels, taskObject, taskLabelForStackIdentifier, context)
if access {
filteredTaskData = append(filteredTaskData, taskObject)
}
}
if access {
filteredTaskData = append(filteredTaskData, taskObject)
}
}

View File

@ -62,17 +62,17 @@ func volumeInspectOperation(response *http.Response, executor *operationExecutor
volumeID := responseObject[volumeIdentifier].(string)
responseObject, access := applyResourceAccessControl(responseObject, volumeID, executor.operationContext)
if !access {
return rewriteAccessDeniedResponse(response)
if access {
return rewriteResponse(response, responseObject, http.StatusOK)
}
volumeLabels := extractVolumeLabelsFromVolumeInspectObject(responseObject)
responseObject, access = applyResourceAccessControlFromLabel(volumeLabels, responseObject, volumeLabelForStackIdentifier, executor.operationContext)
if !access {
return rewriteAccessDeniedResponse(response)
if access {
return rewriteResponse(response, responseObject, http.StatusOK)
}
return rewriteResponse(response, responseObject, http.StatusOK)
return rewriteAccessDeniedResponse(response)
}
// extractVolumeLabelsFromVolumeInspectObject retrieve the Labels of the volume if present.
@ -130,12 +130,13 @@ func filterVolumeList(volumeData []interface{}, context *restrictedOperationCont
volumeID := volumeObject[volumeIdentifier].(string)
volumeObject, access := applyResourceAccessControl(volumeObject, volumeID, context)
if access {
if !access {
volumeLabels := extractVolumeLabelsFromVolumeListObject(volumeObject)
volumeObject, access = applyResourceAccessControlFromLabel(volumeLabels, volumeObject, volumeLabelForStackIdentifier, context)
if access {
filteredVolumeData = append(filteredVolumeData, volumeObject)
}
}
if access {
filteredVolumeData = append(filteredVolumeData, volumeObject)
}
}

View File

@ -1,21 +1,19 @@
package security
import "github.com/portainer/portainer"
import (
"github.com/portainer/portainer"
)
// AuthorizedResourceControlDeletion ensure that the user can delete a resource control object.
// A non-administrator user cannot delete a resource control where:
// * the AdministratorsOnly flag is set
// * the Public flag is false
// * he is not one of the users in the user accesses
// * he is not a member of any team within the team accesses
func AuthorizedResourceControlDeletion(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
if context.IsAdmin {
if context.IsAdmin || resourceControl.Public {
return true
}
if resourceControl.AdministratorsOnly {
return false
}
userAccessesCount := len(resourceControl.UserAccesses)
teamAccessesCount := len(resourceControl.TeamAccesses)
@ -42,39 +40,25 @@ func AuthorizedResourceControlDeletion(resourceControl *portainer.ResourceContro
// AuthorizedResourceControlAccess checks whether the user can alter an existing resource control.
func AuthorizedResourceControlAccess(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
if context.IsAdmin {
if context.IsAdmin || resourceControl.Public {
return true
}
if resourceControl.AdministratorsOnly {
return false
}
authorizedTeamAccess := false
for _, access := range resourceControl.TeamAccesses {
for _, membership := range context.UserMemberships {
if membership.TeamID == access.TeamID {
authorizedTeamAccess = true
break
return true
}
}
}
if !authorizedTeamAccess {
return false
}
authorizedUserAccess := false
for _, access := range resourceControl.UserAccesses {
if context.UserID == access.UserID {
authorizedUserAccess = true
break
return true
}
}
if !authorizedUserAccess {
return false
}
return true
return false
}
// AuthorizedResourceControlUpdate ensure that the user can update a resource control object.
@ -92,20 +76,16 @@ func AuthorizedResourceControlUpdate(resourceControl *portainer.ResourceControl,
// AuthorizedResourceControlCreation ensure that the user can create a resource control object.
// A non-administrator user cannot create a resource control where:
// * the AdministratorsOnly flag is set
// * the Public flag is set false
// * he wants to create a resource control without any user/team accesses
// * he wants to add more than one user in the user accesses
// * he wants tp add a user in the user accesses that is not corresponding to its id
// * he wants to add a team he is not a member of
func AuthorizedResourceControlCreation(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
if context.IsAdmin {
if context.IsAdmin || resourceControl.Public {
return true
}
if resourceControl.AdministratorsOnly {
return false
}
userAccessesCount := len(resourceControl.UserAccesses)
teamAccessesCount := len(resourceControl.TeamAccesses)
@ -126,19 +106,15 @@ func AuthorizedResourceControlCreation(resourceControl *portainer.ResourceContro
if teamAccessesCount > 0 {
for _, access := range resourceControl.TeamAccesses {
isMember := false
for _, membership := range context.UserMemberships {
if membership.TeamID == access.TeamID {
isMember = true
return true
}
}
if !isMember {
return false
}
}
}
return true
return false
}
// AuthorizedTeamManagement ensure that access to the management of the specified team is granted.

View File

@ -274,18 +274,21 @@ type (
// ResourceControl represent a reference to a Docker resource with specific access controls
ResourceControl struct {
ID ResourceControlID `json:"Id"`
ResourceID string `json:"ResourceId"`
SubResourceIDs []string `json:"SubResourceIds"`
Type ResourceControlType `json:"Type"`
AdministratorsOnly bool `json:"AdministratorsOnly"`
UserAccesses []UserResourceAccess `json:"UserAccesses"`
TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
ID ResourceControlID `json:"Id"`
ResourceID string `json:"ResourceId"`
SubResourceIDs []string `json:"SubResourceIds"`
Type ResourceControlType `json:"Type"`
UserAccesses []UserResourceAccess `json:"UserAccesses"`
TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
Public bool `json:"Public"`
// Deprecated fields
// Deprecated in DBVersion == 2
OwnerID UserID `json:"OwnerId,omitempty"`
AccessLevel ResourceAccessLevel `json:"AccessLevel,omitempty"`
// Deprecated in DBVersion == 14
AdministratorsOnly bool `json:"AdministratorsOnly,omitempty"`
}
// ResourceControlType represents the type of resource associated to the resource control (volume, container, service...).
@ -613,7 +616,7 @@ const (
// APIVersion is the version number of the Portainer API.
APIVersion = "1.19.2-dev"
// DBVersion is the version number of the Portainer database.
DBVersion = 13
DBVersion = 14
// PortainerAgentHeader represents the name of the header available in any agent response
PortainerAgentHeader = "Portainer-Agent"
// PortainerAgentTargetHeader represent the name of the header containing the target node name.

View File

@ -3268,11 +3268,10 @@ definitions:
example: "container"
description: "Type of Docker resource. Valid values are: container, volume\
\ service, secret, config or stack"
AdministratorsOnly:
Public:
type: "boolean"
example: true
description: "Restrict access to the associated resource to administrators\
\ only"
description: "Permit access to the associated resource to any user"
Users:
type: "array"
description: "List of user identifiers with access to the associated resource"
@ -3491,11 +3490,10 @@ definitions:
example: "container"
description: "Type of Docker resource. Valid values are: container, volume\
\ service, secret, config or stack"
AdministratorsOnly:
Public:
type: "boolean"
example: true
description: "Restrict access to the associated resource to administrators\
\ only"
description: "Permit access to the associated resource to any user"
Users:
type: "array"
description: "List of user identifiers with access to the associated resource"
@ -3520,11 +3518,10 @@ definitions:
ResourceControlUpdateRequest:
type: "object"
properties:
AdministratorsOnly:
Public:
type: "boolean"
example: false
description: "Restrict access to the associated resource to administrators\
\ only"
description: "Permit access to the associated resource to any user"
Users:
type: "array"
description: "List of user identifiers with access to the associated resource"

View File

@ -63,7 +63,7 @@
<td ng-if="$ctrl.showOwnershipColumn">
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }}
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
</span>
</td>
</tr>

View File

@ -244,7 +244,7 @@
<td ng-if="$ctrl.showOwnershipColumn" ng-show="$ctrl.columnVisibility.columns.ownership.display">
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }}
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
</span>
</td>
</tr>

View File

@ -111,7 +111,7 @@
<td ng-if="$ctrl.showOwnershipColumn">
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }}
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
</span>
</td>
</tr>

View File

@ -63,7 +63,7 @@
<td ng-if="$ctrl.showOwnershipColumn">
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }}
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
</span>
</td>
</tr>

View File

@ -118,7 +118,7 @@
<td ng-if="$ctrl.showOwnershipColumn">
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }}
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
</span>
</td>
</tr>

View File

@ -115,7 +115,7 @@
<td ng-if="$ctrl.showOwnershipColumn">
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }}
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
</span>
</td>
</tr>

View File

@ -285,7 +285,7 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co
var teams = resourceControl.TeamAccesses.map(function(t) {
return t.TeamId;
});
return ResourceControlService.createResourceControl(resourceControl.AdministratorsOnly, users, teams, containerIdentifier, 'container', []);
return ResourceControlService.createResourceControl(resourceControl.Public, users, teams, containerIdentifier, 'container', []);
}
function notifyAndChangeView() {

View File

@ -11,12 +11,12 @@
<td>
<i ng-class="$ctrl.resourceControl.Ownership | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
<span ng-if="!$ctrl.resourceControl">
public
<portainer-tooltip message="This resource can be managed by any user with access to this endpoint." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
administrators
<portainer-tooltip message="This resource can only be managed by administrators." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
</span>
<span ng-if="$ctrl.resourceControl">
{{ $ctrl.resourceControl.Ownership }}
<portainer-tooltip ng-if="$ctrl.resourceControl.Ownership === 'administrators'" message="This resource can only be managed by administrators." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
<portainer-tooltip ng-if="$ctrl.resourceControl.Ownership === 'public'" message="This resource can be managed by any user with access to this endpoint." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
<portainer-tooltip ng-if="$ctrl.resourceControl.Ownership === 'private'" message="Management of this resource is restricted to a single user." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
<portainer-tooltip ng-if="$ctrl.resourceControl.Ownership === 'restricted'" message="This resource can be managed by a restricted set of users and/or teams." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
</span>

View File

@ -12,7 +12,7 @@ function ($q, $state, UserService, TeamService, ResourceControlService, Notifica
};
ctrl.formValues = {
Ownership: 'public',
Ownership: 'administrators',
Ownership_Users: [],
Ownership_Teams: []
};
@ -51,7 +51,7 @@ function ($q, $state, UserService, TeamService, ResourceControlService, Notifica
return true;
}
function processOwnershipFormValues() {
function processOwnershipFormValues() {
var userIds = [];
angular.forEach(ctrl.formValues.Ownership_Users, function(user) {
userIds.push(user.Id);
@ -60,13 +60,14 @@ function ($q, $state, UserService, TeamService, ResourceControlService, Notifica
angular.forEach(ctrl.formValues.Ownership_Teams, function(team) {
teamIds.push(team.Id);
});
var administratorsOnly = ctrl.formValues.Ownership === 'administrators' ? true : false;
var publicOnly = ctrl.formValues.Ownership === 'public' ? true : false;
return {
ownership: ctrl.formValues.Ownership,
authorizedUserIds: administratorsOnly ? [] : userIds,
authorizedTeamIds: administratorsOnly ? [] : teamIds,
administratorsOnly: administratorsOnly
authorizedUserIds: publicOnly ? [] : userIds,
authorizedTeamIds: publicOnly ? [] : teamIds,
publicOnly: publicOnly
};
}
@ -96,12 +97,13 @@ function ($q, $state, UserService, TeamService, ResourceControlService, Notifica
if (resourceControl) {
ctrl.formValues.Ownership = resourceControl.Ownership === 'private' ? 'restricted' : resourceControl.Ownership;
} else {
ctrl.formValues.Ownership = 'public';
ctrl.formValues.Ownership = 'administrators';
}
} else {
ctrl.formValues.Ownership = 'public';
ctrl.formValues.Ownership = 'administrators';
}
ResourceControlService.retrieveOwnershipDetails(resourceControl)
.then(function success(data) {
ctrl.authorizedUsers = data.authorizedUsers;

View File

@ -70,7 +70,7 @@
<td ng-if="$ctrl.showOwnershipColumn">
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }}
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
</span>
</td>
</tr>

View File

@ -4,16 +4,18 @@ function ResourceControlViewModel(data) {
this.ResourceId = data.ResourceId;
this.UserAccesses = data.UserAccesses;
this.TeamAccesses = data.TeamAccesses;
this.AdministratorsOnly = data.AdministratorsOnly;
this.Public = data.Public;
this.Ownership = determineOwnership(this);
}
function determineOwnership(resourceControl) {
if (resourceControl.AdministratorsOnly) {
return 'administrators';
if (resourceControl.Public) {
return 'public';
} else if (resourceControl.UserAccesses.length === 1 && resourceControl.TeamAccesses.length === 0) {
return 'private';
} else if (resourceControl.UserAccesses.length > 1 || resourceControl.TeamAccesses.length > 0) {
return 'restricted';
} else {
return 'administrators';
}
}

View File

@ -3,10 +3,10 @@ angular.module('portainer.app')
'use strict';
var service = {};
service.createResourceControl = function(administratorsOnly, userIDs, teamIDs, resourceID, type, subResourceIDs) {
service.createResourceControl = function(publicOnly, userIDs, teamIDs, resourceID, type, subResourceIDs) {
var payload = {
Type: type,
AdministratorsOnly: administratorsOnly,
Public: publicOnly,
ResourceID: resourceID,
Users: userIDs,
Teams: teamIDs,
@ -19,9 +19,9 @@ angular.module('portainer.app')
return ResourceControl.remove({id: rcID}).$promise;
};
service.updateResourceControl = function(admin, userIDs, teamIDs, resourceControlId) {
service.updateResourceControl = function(publicOnly, userIDs, teamIDs, resourceControlId) {
var payload = {
AdministratorsOnly: admin,
Public: publicOnly,
Users: userIDs,
Teams: teamIDs
};
@ -30,15 +30,15 @@ angular.module('portainer.app')
service.applyResourceControl = function(resourceControlType, resourceIdentifier, userId, accessControlData, subResources) {
if (!accessControlData.AccessControlEnabled) {
return;
accessControlData.Ownership = 'public';
}
var authorizedUserIds = [];
var authorizedTeamIds = [];
var administratorsOnly = false;
var publicOnly = false;
switch (accessControlData.Ownership) {
case 'administrators':
administratorsOnly = true;
case 'public':
publicOnly = true;
break;
case 'private':
authorizedUserIds.push(userId);
@ -51,21 +51,23 @@ angular.module('portainer.app')
authorizedTeamIds.push(team.Id);
});
break;
}
return service.createResourceControl(administratorsOnly, authorizedUserIds,
default:
return;
}
return service.createResourceControl(publicOnly, authorizedUserIds,
authorizedTeamIds, resourceIdentifier, resourceControlType, subResources);
};
service.applyResourceControlChange = function(resourceControlType, resourceId, resourceControl, ownershipParameters) {
service.applyResourceControlChange = function(resourceControlType, resourceId, resourceControl, ownershipParameters) {
if (resourceControl) {
if (ownershipParameters.ownership === 'public') {
if (ownershipParameters.ownership === 'administrators') {
return service.deleteResourceControl(resourceControl.Id);
} else {
return service.updateResourceControl(ownershipParameters.administratorsOnly, ownershipParameters.authorizedUserIds,
return service.updateResourceControl(ownershipParameters.publicOnly, ownershipParameters.authorizedUserIds,
ownershipParameters.authorizedTeamIds, resourceControl.Id);
}
} else {
return service.createResourceControl(ownershipParameters.administratorsOnly, ownershipParameters.authorizedUserIds,
return service.createResourceControl(ownershipParameters.publicOnly, ownershipParameters.authorizedUserIds,
ownershipParameters.authorizedTeamIds, resourceId, resourceControlType);
}
};