mirror of https://github.com/portainer/portainer
feat(stacks): prevent external stack removal by a non-administrator user (#3800)
* fix(stacks): prevent external stacks removal by non admin * feat(stacks): add RBAC checks for external stack removals Co-authored-by: Maxime Bajeux <max.bajeux@gmail.com>pull/3826/head
parent
29c0584454
commit
9dcd223134
|
@ -9,7 +9,7 @@ import (
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DELETE request on /api/stacks/:id?external=<external>&endpointId=<endpointId>
|
// DELETE request on /api/stacks/:id?external=<external>&endpointId=<endpointId>
|
||||||
|
@ -21,9 +21,14 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||||
|
}
|
||||||
|
|
||||||
externalStack, _ := request.RetrieveBooleanQueryParameter(r, "external", true)
|
externalStack, _ := request.RetrieveBooleanQueryParameter(r, "external", true)
|
||||||
if externalStack {
|
if externalStack {
|
||||||
return handler.deleteExternalStack(r, w, stackID)
|
return handler.deleteExternalStack(r, w, stackID, securityContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := strconv.Atoi(stackID)
|
id, err := strconv.Atoi(stackID)
|
||||||
|
@ -68,11 +73,6 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||||
|
@ -106,7 +106,38 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return response.Empty(w)
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWriter, stackName string) *httperror.HandlerError {
|
func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWriter, stackName string, securityContext *security.RestrictedRequestContext) *httperror.HandlerError {
|
||||||
|
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := handler.UserService.User(securityContext.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
rbacExtension, err := handler.ExtensionService.Extension(portainer.RBACExtension)
|
||||||
|
if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify if RBAC extension is loaded", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointResourceAccess := false
|
||||||
|
_, ok := user.EndpointAuthorizations[portainer.EndpointID(endpointID)][portainer.EndpointResourcesAccess]
|
||||||
|
if ok {
|
||||||
|
endpointResourceAccess = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if rbacExtension != nil {
|
||||||
|
if !securityContext.IsAdmin && !endpointResourceAccess {
|
||||||
|
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", portainer.ErrUnauthorized}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !securityContext.IsAdmin {
|
||||||
|
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", portainer.ErrUnauthorized}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stack, err := handler.StackService.StackByName(stackName)
|
stack, err := handler.StackService.StackByName(stackName)
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for stack existence inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for stack existence inside the database", err}
|
||||||
|
@ -115,11 +146,6 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "A stack with this name exists inside the database. Cannot use external delete method", portainer.ErrStackNotExternal}
|
return &httperror.HandlerError{http.StatusBadRequest, "A stack with this name exists inside the database. Cannot use external delete method", portainer.ErrStackNotExternal}
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||||
if err == portainer.ErrObjectNotFound {
|
if err == portainer.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
|
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||||
|
|
|
@ -2,17 +2,24 @@ angular.module('portainer.app').controller('StacksDatatableController', [
|
||||||
'$scope',
|
'$scope',
|
||||||
'$controller',
|
'$controller',
|
||||||
'DatatableService',
|
'DatatableService',
|
||||||
function ($scope, $controller, DatatableService) {
|
'Authentication',
|
||||||
|
function ($scope, $controller, DatatableService, Authentication) {
|
||||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do not allow external items
|
* Do not allow external items
|
||||||
*/
|
*/
|
||||||
this.allowSelection = function (item) {
|
this.allowSelection = function (item) {
|
||||||
return !(item.External && item.Type === 2);
|
if (item.External && item.Type === 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !(item.External && !this.isAdmin && !this.isEndpointAdmin);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$onInit = function () {
|
this.$onInit = function () {
|
||||||
|
this.isAdmin = Authentication.isAdmin();
|
||||||
|
this.isEndpointAdmin = Authentication.hasAuthorizations(['EndpointResourcesAccess']);
|
||||||
this.setDefaults();
|
this.setDefaults();
|
||||||
this.prepareTableFromDataset();
|
this.prepareTableFromDataset();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue