mirror of https://github.com/portainer/portainer
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
parent
31c2a6d9e7
commit
e1e263d8c8
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue