1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-08 15:25:22 +02:00

feat(api): rewrite access control management in Docker (#3337)

* feat(api): decorate Docker resource creation response with resource control

* fix(api): fix a potential resource control conflict between stacks/volumes

* feat(api): generate a default private resource control instead of admin only

* fix(api): fix default RC value

* fix(api): update RC authorizations check to support admin only flag

* refactor(api): relocate access control related methods

* fix(api): fix a potential conflict when fetching RC from database

* refactor(api): refactor access control logic

* refactor(api): remove the concept of DecoratedStack

* feat(api): automatically remove RC when removing a Docker resource

* refactor(api): update filter resource methods documentation

* refactor(api): update proxy package structure

* refactor(api): renamed proxy/misc package

* feat(api): re-introduce ResourceControlDelete operation as admin restricted

* refactor(api): relocate default endpoint authorizations

* feat(api): migrate RBAC data

* feat(app): ResourceControl management refactor

* fix(api): fix access control issue on stack deletion and automatically delete RC

* fix(api): fix stack filtering

* fix(api): fix UpdateResourceControl operation checks

* refactor(api): introduce a NewTransport builder method

* refactor(api): inject endpoint in Docker transport

* refactor(api): introduce Docker client into Docker transport

* refactor(api): refactor http/proxy package

* feat(api): inspect a Docker resource labels during access control validation

* fix(api): only apply automatic resource control creation on success response

* fix(api): fix stack access control check

* fix(api): use StatusCreated instead of StatusOK for automatic resource control creation

* fix(app): resource control fixes

* fix(api): fix an issue preventing administrator to inspect a resource with a RC

* refactor(api): remove useless error return

* refactor(api): document DecorateStacks function

* fix(api): fix invalid resource control type for container deletion

* feat(api): support Docker system networks

* feat(api): update Swagger docs

* refactor(api): rename transport variable

* refactor(api): rename transport variable

* feat(networks): add system tag for system networks

* feat(api): add support for resource control labels

* feat(api): upgrade to DBVersion 22

* refactor(api): refactor access control management in Docker proxy

* refactor(api): re-implement docker proxy taskListOperation

* refactor(api): review parameters declaration

* refactor(api): remove extra blank line

* refactor(api): review method comments

* fix(api): fix invalid ServerAddress property and review method visibility

* feat(api): update error message

* feat(api): update restrictedVolumeBrowserOperation method

* refactor(api): refactor method parameters

* refactor(api): minor refactor

* refactor(api): change Azure transport visibility

* refactor(api): update struct documentation

* refactor(api): update struct documentation

* feat(api): review restrictedResourceOperation method

* refactor(api): remove unused authorization methods

* feat(api): apply RBAC when enabled on stack operations

* fix(api): fix invalid data migration procedure for DBVersion = 22

* fix(app): RC duplicate on private resource

* feat(api): change Docker API version logic for libcompose/client factory

* fix(api): update access denied error message to be Docker API compliant

* fix(api): update volume browsing authorizations data migration

* fix(api): fix an issue with access control in multi-node agent Swarm cluster
This commit is contained in:
Anthony Lapenna 2019-11-13 12:41:42 +13:00 committed by GitHub
parent 198e92c734
commit 19d4db13be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
118 changed files with 3600 additions and 3020 deletions

View file

@ -71,7 +71,7 @@
</div>
<!-- restricted-access -->
<!-- authorized-teams -->
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled && $ctrl.formData.Ownership === 'restricted' && ($ctrl.isAdmin || (!$ctrl.isAdmin && $ctrl.availableTeams.length > 1))" >
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled && $ctrl.formData.Ownership === $ctrl.RCO.RESTRICTED && ($ctrl.isAdmin || (!$ctrl.isAdmin && $ctrl.availableTeams.length > 1))" >
<div class="col-sm-12">
<label for="group-access" class="control-label text-left">
Authorized teams
@ -97,7 +97,7 @@
</div>
<!-- !authorized-teams -->
<!-- authorized-users -->
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled && $ctrl.formData.Ownership === 'restricted' && $ctrl.isAdmin">
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled && $ctrl.formData.Ownership === $ctrl.RCO.RESTRICTED && $ctrl.isAdmin">
<div class="col-sm-12">
<label for="group-access" class="control-label text-left">
Authorized users

View file

@ -1,16 +1,19 @@
import _ from 'lodash-es';
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
angular.module('portainer.app')
.controller('porAccessControlFormController', ['$q', 'UserService', 'TeamService', 'Notifications', 'Authentication', 'ResourceControlService',
function ($q, UserService, TeamService, Notifications, Authentication, ResourceControlService) {
var ctrl = this;
ctrl.RCO = RCO;
ctrl.availableTeams = [];
ctrl.availableUsers = [];
function setOwnership(resourceControl, isAdmin) {
if (isAdmin && resourceControl.Ownership === 'private') {
ctrl.formData.Ownership = 'restricted';
if (isAdmin && resourceControl.Ownership === RCO.PRIVATE) {
ctrl.formData.Ownership = RCO.RESTRICTED;
} else {
ctrl.formData.Ownership = resourceControl.Ownership;
}
@ -37,7 +40,7 @@ function ($q, UserService, TeamService, Notifications, Authentication, ResourceC
ctrl.isAdmin = isAdmin;
if (isAdmin) {
ctrl.formData.Ownership = 'administrators';
ctrl.formData.Ownership = ctrl.RCO.ADMINISTRATORS;
}
$q.all({

View file

@ -1,6 +1,8 @@
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
export function AccessControlFormData() {
this.AccessControlEnabled = true;
this.Ownership = 'private';
this.Ownership = RCO.PRIVATE;
this.AuthorizedUsers = [];
this.AuthorizedTeams = [];
}

View file

@ -9,6 +9,8 @@ angular.module('portainer.app').component('porAccessControlPanel', {
// This component is usually displayed inside a resource-details view.
// This variable specifies the type of the associated resource.
// Accepted values: 'container', 'service' or 'volume'.
resourceType: '<'
resourceType: '<',
// Allow to disable the Ownership edition based on non resource control data
disableOwnershipChange: '<'
}
});

View file

@ -16,28 +16,28 @@
</span>
<span ng-if="$ctrl.resourceControl">
{{ $ctrl.resourceControl.Ownership }}
<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>
<portainer-tooltip ng-if="$ctrl.resourceControl.Ownership === $ctrl.RCO.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 === $ctrl.RCO.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 === $ctrl.RCO.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>
</td>
</tr>
<!-- !ownership -->
<tr ng-if="$ctrl.resourceControl.Type === 2 && $ctrl.resourceType === 'container'">
<tr ng-if="$ctrl.resourceControl.Type === $ctrl.RCTI.SERVICE && $ctrl.resourceType === $ctrl.RCTS.CONTAINER">
<td colspan="2">
<i class="fa fa-info-circle" aria-hidden="true" style="margin-right: 2px;"></i>
Access control on this resource is inherited from the following service: <a ui-sref="docker.services.service({ id: $ctrl.resourceControl.ResourceId })">{{ $ctrl.resourceControl.ResourceId | truncate }}</a>
<portainer-tooltip message="Access control applied on a service is also applied on each container of that service." position="bottom" style="margin-left: 2px;"></portainer-tooltip>
</td>
</tr>
<tr ng-if="$ctrl.resourceControl.Type === 1 && $ctrl.resourceType === 'volume'">
<tr ng-if="$ctrl.resourceControl.Type === $ctrl.RCTI.CONTAINER && $ctrl.resourceType === $ctrl.RCTS.VOLUME">
<td colspan="2">
<i class="fa fa-info-circle" aria-hidden="true" style="margin-right: 2px;"></i>
Access control on this resource is inherited from the following container: <a ui-sref="docker.containers.container({ id: $ctrl.resourceControl.ResourceId })">{{ $ctrl.resourceControl.ResourceId | truncate }}</a>
<portainer-tooltip message="Access control applied on a container created using a template is also applied on each volume associated to the container." position="bottom" style="margin-left: 2px;"></portainer-tooltip>
</td>
</tr>
<tr ng-if="$ctrl.resourceControl.Type === 6 && $ctrl.resourceType !== 'stack'">
<tr ng-if="$ctrl.resourceControl.Type === $ctrl.RCTI.STACK && $ctrl.resourceType !== $ctrl.RCTS.STACK">
<td colspan="2">
<i class="fa fa-info-circle" aria-hidden="true" style="margin-right: 2px;"></i>
Access control on this resource is inherited from the following stack: {{ $ctrl.resourceControl.ResourceId }}
@ -61,11 +61,7 @@
</tr>
<!-- !authorized-teams -->
<!-- edit-ownership -->
<tr ng-if="!($ctrl.resourceControl.Type === 1 && $ctrl.resourceType === 'volume')
&& !($ctrl.resourceControl.Type === 2 && $ctrl.resourceType === 'container')
&& !($ctrl.resourceControl.Type === 6 && $ctrl.resourceType !== 'stack')
&& !$ctrl.state.editOwnership
&& ($ctrl.isAdmin || $ctrl.state.canEditOwnership)">
<tr ng-if="$ctrl.canEditOwnership();">
<td colspan="2">
<a ng-click="$ctrl.state.editOwnership = true"><i class="fa fa-edit space-right" aria-hidden="true"></i>Change ownership</a>
</td>
@ -76,7 +72,7 @@
<td colspan="2" style="white-space: inherit;">
<div class="boxselector_wrapper">
<div ng-if="$ctrl.isAdmin">
<input type="radio" id="access_administrators" ng-model="$ctrl.formValues.Ownership" value="administrators">
<input type="radio" id="access_administrators" ng-model="$ctrl.formValues.Ownership" ng-value="$ctrl.RCO.ADMINISTRATORS">
<label for="access_administrators">
<div class="boxselector_header">
<i ng-class="'administrators' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
@ -86,7 +82,7 @@
</label>
</div>
<div ng-if="$ctrl.isAdmin">
<input type="radio" id="access_restricted" ng-model="$ctrl.formValues.Ownership" value="restricted">
<input type="radio" id="access_restricted" ng-model="$ctrl.formValues.Ownership" ng-value="$ctrl.RCO.RESTRICTED">
<label for="access_restricted">
<div class="boxselector_header">
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
@ -98,7 +94,7 @@
</label>
</div>
<div ng-if="!$ctrl.isAdmin && $ctrl.state.canChangeOwnershipToTeam && $ctrl.availableTeams.length > 0">
<input type="radio" id="access_restricted" ng-model="$ctrl.formValues.Ownership" value="restricted">
<input type="radio" id="access_restricted" ng-model="$ctrl.formValues.Ownership" ng-value="$ctrl.RCO.RESTRICTED">
<label for="access_restricted">
<div class="boxselector_header">
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
@ -113,7 +109,7 @@
</label>
</div>
<div>
<input type="radio" id="access_public" ng-model="$ctrl.formValues.Ownership" value="public">
<input type="radio" id="access_public" ng-model="$ctrl.formValues.Ownership" ng-value="$ctrl.RCO.PUBLIC">
<label for="access_public">
<div class="boxselector_header">
<i ng-class="'public' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
@ -127,7 +123,7 @@
</tr>
<!-- edit-ownership-choices -->
<!-- select-teams -->
<tr ng-if="$ctrl.state.editOwnership && $ctrl.formValues.Ownership === 'restricted' && ($ctrl.isAdmin || !$ctrl.isAdmin && $ctrl.availableTeams.length > 1)">
<tr ng-if="$ctrl.state.editOwnership && $ctrl.formValues.Ownership === $ctrl.RCO.RESTRICTED && ($ctrl.isAdmin || !$ctrl.isAdmin && $ctrl.availableTeams.length > 1)">
<td colspan="2">
<span>Teams</span>
<span ng-if="$ctrl.isAdmin && $ctrl.availableTeams.length === 0" class="small text-muted" style="margin-left: 10px;">
@ -149,7 +145,7 @@
</tr>
<!-- !select-teams -->
<!-- select-users -->
<tr ng-if="$ctrl.isAdmin && $ctrl.state.editOwnership && $ctrl.formValues.Ownership === 'restricted'">
<tr ng-if="$ctrl.isAdmin && $ctrl.state.editOwnership && $ctrl.formValues.Ownership === $ctrl.RCO.RESTRICTED">
<td colspan="2">
<span>Users</span>
<span ng-if="$ctrl.availableUsers.length === 0" class="small text-muted" style="margin-left: 10px;">

View file

@ -1,11 +1,17 @@
import _ from 'lodash-es';
import { AccessControlPanelData } from './porAccessControlPanelModel';
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
import { ResourceControlTypeString as RCTS, ResourceControlTypeInt as RCTI} from 'Portainer/models/resourceControl/resourceControlTypes';
angular.module('portainer.app')
.controller('porAccessControlPanelController', ['$q', '$state', 'UserService', 'TeamService', 'ResourceControlService', 'Notifications', 'Authentication', 'ModalService', 'FormValidator',
function ($q, $state, UserService, TeamService, ResourceControlService, Notifications, Authentication, ModalService, FormValidator) {
.controller('porAccessControlPanelController', ['$q', '$state', 'UserService', 'TeamService', 'ResourceControlHelper', 'ResourceControlService', 'Notifications', 'Authentication', 'ModalService', 'FormValidator',
function ($q, $state, UserService, TeamService, ResourceControlHelper, ResourceControlService, Notifications, Authentication, ModalService, FormValidator) {
var ctrl = this;
ctrl.RCO = RCO;
ctrl.RCTS = RCTS;
ctrl.RCTI = RCTI;
ctrl.state = {
displayAccessControlPanel: false,
canEditOwnership: false,
@ -13,17 +19,24 @@ function ($q, $state, UserService, TeamService, ResourceControlService, Notifica
formValidationError: ''
};
ctrl.formValues = {
Ownership: 'administrators',
Ownership_Users: [],
Ownership_Teams: []
};
ctrl.formValues = new AccessControlPanelData();
ctrl.authorizedUsers = [];
ctrl.availableUsers = [];
ctrl.authorizedTeams = [];
ctrl.availableTeams = [];
ctrl.canEditOwnership = function() {
const hasRC = ctrl.resourceControl;
const inheritedVolume = hasRC && ctrl.resourceControl.Type === RCTI.CONTAINER && ctrl.resourceType === RCTS.VOLUME;
const inheritedContainer = hasRC && ctrl.resourceControl.Type === RCTI.SERVICE && ctrl.resourceType === RCTS.CONTAINER;
const inheritedFromStack = hasRC && ctrl.resourceControl.Type === RCTI.STACK && ctrl.resourceType !== RCTS.STACK;
const hasSpecialDisable = ctrl.disableOwnershipChange;
return !inheritedVolume && !inheritedContainer && !inheritedFromStack && !hasSpecialDisable
&& !ctrl.state.editOwnership && (ctrl.isAdmin || ctrl.state.canEditOwnership);
}
ctrl.confirmUpdateOwnership = function () {
if (!validateForm()) {
return;
@ -39,7 +52,7 @@ function ($q, $state, UserService, TeamService, ResourceControlService, Notifica
var error = '';
var accessControlData = {
AccessControlEnabled: ctrl.formValues.Ownership === 'public' ? false : true,
AccessControlEnabled: ctrl.formValues.Ownership === RCO.PUBLIC ? false : true,
Ownership: ctrl.formValues.Ownership,
AuthorizedUsers: ctrl.formValues.Ownership_Users,
AuthorizedTeams: ctrl.formValues.Ownership_Teams
@ -53,32 +66,8 @@ function ($q, $state, UserService, TeamService, ResourceControlService, Notifica
return true;
}
function processOwnershipFormValues() {
var userIds = [];
angular.forEach(ctrl.formValues.Ownership_Users, function(user) {
userIds.push(user.Id);
});
var teamIds = [];
angular.forEach(ctrl.formValues.Ownership_Teams, function(team) {
teamIds.push(team.Id);
});
var publicOnly = ctrl.formValues.Ownership === 'public' ? true : false;
return {
ownership: ctrl.formValues.Ownership,
authorizedUserIds: publicOnly ? [] : userIds,
authorizedTeamIds: publicOnly ? [] : teamIds,
publicOnly: publicOnly
};
}
function updateOwnership() {
var resourceId = ctrl.resourceId;
var ownershipParameters = processOwnershipFormValues();
ResourceControlService.applyResourceControlChange(ctrl.resourceType, resourceId,
ctrl.resourceControl, ownershipParameters)
ResourceControlService.applyResourceControlChange(ctrl.resourceType, ctrl.resourceId, ctrl.resourceControl, ctrl.formValues)
.then(function success() {
Notifications.success('Access control successfully updated');
$state.reload();
@ -95,17 +84,12 @@ function ($q, $state, UserService, TeamService, ResourceControlService, Notifica
ctrl.isAdmin = isAdmin;
var resourceControl = ctrl.resourceControl;
if (isAdmin) {
if (resourceControl) {
ctrl.formValues.Ownership = resourceControl.Ownership === 'private' ? 'restricted' : resourceControl.Ownership;
} else {
ctrl.formValues.Ownership = 'administrators';
}
if (isAdmin && resourceControl) {
ctrl.formValues.Ownership = resourceControl.Ownership === RCO.PRIVATE ? RCO.RESTRICTED : resourceControl.Ownership;
} else {
ctrl.formValues.Ownership = 'administrators';
ctrl.formValues.Ownership = RCO.ADMINISTRATORS;
}
ResourceControlService.retrieveOwnershipDetails(resourceControl)
.then(function success(data) {
ctrl.authorizedUsers = data.authorizedUsers;

View file

@ -0,0 +1,7 @@
import { ResourceControlOwnership } from 'Portainer/models/resourceControl/resourceControlOwnership';
export function AccessControlPanelData() {
this.Ownership = ResourceControlOwnership.ADMINISTRATORS;
this.Ownership_Users = [];
this.Ownership_Teams = [];
}

View file

@ -1,5 +1,6 @@
import _ from 'lodash-es';
import './datatable.css';
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
function isBetween(value, a, b) {
return (value >= a && value <= b) || (value >= b && value <= a) ;
@ -9,6 +10,8 @@ angular.module('portainer.app')
.controller('GenericDatatableController', ['$interval', 'PaginationService', 'DatatableService', 'PAGINATION_MAX_ITEMS',
function ($interval, PaginationService, DatatableService, PAGINATION_MAX_ITEMS) {
this.RCO = RCO;
this.state = {
selectAll: false,
orderBy: this.orderBy,

View file

@ -109,7 +109,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 = 'administrators' }}
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }}
</span>
</td>
</tr>

View file

@ -2,6 +2,8 @@ import moment from 'moment';
import _ from 'lodash-es';
import filesize from 'filesize';
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
angular.module('portainer.app')
.filter('truncate', function () {
'use strict';
@ -145,11 +147,11 @@ angular.module('portainer.app')
'use strict';
return function (ownership) {
switch (ownership) {
case 'private':
case RCO.PRIVATE:
return 'fa fa-eye-slash';
case 'administrators':
case RCO.ADMINISTRATORS:
return 'fa fa-eye-slash';
case 'restricted':
case RCO.RESTRICTED:
return 'fa fa-users';
default:
return 'fa fa-eye';

View file

@ -0,0 +1,15 @@
import _ from 'lodash-es';
import angular from 'angular';
class NetworkHelper {
constructor(PREDEFINED_NETWORKS) {
this.PREDEFINED_NETWORKS = PREDEFINED_NETWORKS;
}
isSystemNetwork(item) {
return _.includes(this.PREDEFINED_NETWORKS, item.Name);
}
}
export default NetworkHelper;
angular.module('portainer.app').service('NetworkHelper', NetworkHelper);

View file

@ -1,44 +1,121 @@
import _ from 'lodash-es';
import angular from 'angular';
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
import { ResourceControlOwnershipParameters } from '../models/resourceControl/resourceControlOwnershipParameters';
angular.module('portainer.app')
.factory('ResourceControlHelper', [function ResourceControlHelperFactory() {
'use strict';
var helper = {};
class ResourceControlHelper {
helper.retrieveAuthorizedUsers = function(resourceControl, users) {
var authorizedUsers = [];
angular.forEach(resourceControl.UserAccesses, function(access) {
var user = _.find(users, { Id: access.UserId });
/**
* Transform ResourceControlViewModel to ResourceControlOwnershipParameters
* @param {int} userId ID of User performing the action
* @param {ResourceControlViewModel} resourceControl ResourceControl view model
*/
RCViewModelToOwnershipParameters(userId, resourceControl) {
if (!resourceControl) {
return new ResourceControlOwnershipParameters(true);
}
let adminOnly = false;
let publicOnly = false;
let users = [];
let teams = [];
switch (resourceControl.Ownership) {
case RCO.PUBLIC:
publicOnly = true;
break;
case RCO.PRIVATE:
users = [userId];
break;
case RCO.RESTRICTED:
users = _.map(resourceControl.UserAccesses, (user) => user.UserId);
teams = _.map(resourceControl.TeamAccesses, (team) => team.TeamId);
break;
default:
adminOnly = true;
break;
}
return new ResourceControlOwnershipParameters(adminOnly, publicOnly, users, teams);
}
/**
* Transform AccessControlFormData to ResourceControlOwnershipParameters
* @param {int} userId ID of user performing the operation
* @param {AccessControlFormData} formValues Form data (generated by AccessControlForm)
* @param {int[]} subResources Sub Resources restricted by the ResourceControl
*/
RCFormDataToOwnershipParameters(userId, formValues, subResources=[]) {
if (!formValues.AccessControlEnabled) {
formValues.Ownership = RCO.PUBLIC;
}
let adminOnly = false;
let publicOnly = false;
let users = [];
let teams = [];
switch (formValues.Ownership) {
case RCO.PUBLIC:
publicOnly = true;
break;
case RCO.PRIVATE:
users.push(userId);
break;
case RCO.RESTRICTED:
users = _.map(formValues.AuthorizedUsers, (user) => user.Id);
teams = _.map(formValues.AuthorizedTeams, (team) => team.Id);
break;
default:
adminOnly = true;
break;
}
return new ResourceControlOwnershipParameters(adminOnly, publicOnly, users, teams, subResources);
}
/**
* Transform AccessControlPanelData to ResourceControlOwnershipParameters
* @param {AccessControlPanelData} formValues Form data (generated by AccessControlPanel)
*/
RCPanelDataToOwnershipParameters(formValues) {
const adminOnly = formValues.Ownership === RCO.ADMINISTRATORS;
const publicOnly = formValues.Ownership === RCO.PUBLIC;
const userIds = publicOnly || adminOnly ? [] : _.map(formValues.Ownership_Users, (user) => user.Id);
const teamIds = publicOnly || adminOnly ? [] : _.map(formValues.Ownership_Teams, (team) => team.Id);
return new ResourceControlOwnershipParameters(adminOnly, publicOnly, userIds, teamIds);
}
retrieveAuthorizedUsers(resourceControl, users) {
const authorizedUsers = [];
_.forEach(resourceControl.UserAccesses, (access) => {
const user = _.find(users, { Id: access.UserId });
if (user) {
authorizedUsers.push(user);
}
});
return authorizedUsers;
};
}
helper.retrieveAuthorizedTeams = function(resourceControl, teams) {
var authorizedTeams = [];
angular.forEach(resourceControl.TeamAccesses, function(access) {
var team = _.find(teams, { Id: access.TeamId });
retrieveAuthorizedTeams(resourceControl, teams) {
const authorizedTeams = [];
_.forEach(resourceControl.TeamAccesses, (access) => {
const team = _.find(teams, { Id: access.TeamId });
if (team) {
authorizedTeams.push(team);
}
});
return authorizedTeams;
};
}
helper.isLeaderOfAnyRestrictedTeams = function(userMemberships, resourceControl) {
var isTeamLeader = false;
for (var i = 0; i < userMemberships.length; i++) {
var membership = userMemberships[i];
var found = _.find(resourceControl.TeamAccesses, { TeamId :membership.TeamId });
isLeaderOfAnyRestrictedTeams(userMemberships, resourceControl) {
let isTeamLeader = false;
_.forEach(userMemberships, (membership) => {
const found = _.find(resourceControl.TeamAccesses, { TeamId: membership.TeamId });
if (found && membership.Role === 1) {
isTeamLeader = true;
break;
return false;
}
}
});
return isTeamLeader;
};
}
}
return helper;
}]);
export default ResourceControlHelper;
angular.module('portainer.app').service('ResourceControlHelper', ResourceControlHelper);

View file

@ -1,3 +1,5 @@
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
export function ResourceControlViewModel(data) {
this.Id = data.Id;
this.Type = data.Type;
@ -5,17 +7,18 @@ export function ResourceControlViewModel(data) {
this.UserAccesses = data.UserAccesses;
this.TeamAccesses = data.TeamAccesses;
this.Public = data.Public;
this.System = data.System;
this.Ownership = determineOwnership(this);
}
function determineOwnership(resourceControl) {
if (resourceControl.Public) {
return 'public';
return RCO.PUBLIC;
} else if (resourceControl.UserAccesses.length === 1 && resourceControl.TeamAccesses.length === 0) {
return 'private';
return RCO.PRIVATE;
} else if (resourceControl.UserAccesses.length > 1 || resourceControl.TeamAccesses.length > 0) {
return 'restricted';
return RCO.RESTRICTED;
} else {
return 'administrators';
return RCO.ADMINISTRATORS;
}
}

View file

@ -0,0 +1,17 @@
/**
* Payload for resourceControleCreate operation
* @param {string} resourceId ID of involved Resource
* @param {ResourceControlResourceType} resourceType Type of involved Resource
* @param {ResourceControlOwnershipParameters} data Transcient type from view data to payload
*/
export function ResourceControlCreatePayload(resourceId, resourceType, data) {
void data;
this.ResourceID = resourceId;
this.Type = resourceType;
this.Public = data.Public;
this.AdministratorsOnly = data.AdministratorsOnly;
this.Users = data.Users;
this.Teams = data.Teams;
this.SubResourceIDs = data.SubResourceIDs;
}

View file

@ -0,0 +1,6 @@
export const ResourceControlOwnership = Object.freeze({
PUBLIC: 'public',
PRIVATE: 'private',
RESTRICTED: 'restricted',
ADMINISTRATORS: 'administrators'
});

View file

@ -0,0 +1,15 @@
/**
* Transcient type from view data to payload
* @param {bool} adminOnly is ResourceControl restricted to admin only
* @param {bool} publicOnly is ResourceControl exposed to public
* @param {[]int} users Authorized UserIDs array
* @param {[]int} teams Authorized TeamIDs array
* @param {[]int} subResources subResourceIDs array
*/
export function ResourceControlOwnershipParameters(adminOnly=false, publicOnly=false, users=[], teams=[], subResources=[]) {
this.AdministratorsOnly = adminOnly;
this.Public = publicOnly;
this.Users = users;
this.Teams = teams;
this.SubResourceIDs = subResources;
}

View file

@ -0,0 +1,22 @@
export const ResourceControlTypeString = Object.freeze({
CONFIG: 'config',
CONTAINER: 'container',
NETWORK:'network',
SECRET:'secret',
SERVICE:'service',
STACK:'stack',
VOLUME:'volume'
});
/**
* ResourceType int defined in portainer.go as ResourceControlType
*/
export const ResourceControlTypeInt = Object.freeze({
CONTAINER: 1,
SERVICE: 2,
VOLUME: 3,
NETWORK: 4,
SECRET: 5,
STACK: 6,
CONFIG: 7
});

View file

@ -0,0 +1,10 @@
/**
* Payload for resourceControlUpdate operation
* @param {ResourceControlOwnershipParameters} data
*/
export function ResourceControlUpdatePayload(data) {
this.Public = data.Public;
this.AdministratorsOnly = data.AdministratorsOnly;
this.Users = data.Users;
this.Teams = data.Teams;
}

View file

@ -1,4 +1,4 @@
import { ResourceControlViewModel } from '../../portainer/models/resourceControl';
import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl';
export function StackViewModel(data) {
this.Id = data.Id;

View file

@ -1,80 +1,108 @@
import _ from 'lodash-es';
angular.module('portainer.app')
.factory('ResourceControlService', ['$q', 'ResourceControl', 'UserService', 'TeamService', 'ResourceControlHelper', function ResourceControlServiceFactory($q, ResourceControl, UserService, TeamService, ResourceControlHelper) {
.factory('ResourceControlService', ['$q', 'ResourceControl', 'UserService', 'TeamService', 'ResourceControlHelper',
function ResourceControlServiceFactory($q, ResourceControl, UserService, TeamService, ResourceControlHelper) {
'use strict';
var service = {};
const service = {};
service.createResourceControl = function(publicOnly, userIDs, teamIDs, resourceID, type, subResourceIDs) {
service.duplicateResourceControl = duplicateResourceControl;
service.applyResourceControlChange = applyResourceControlChange;
service.applyResourceControl = applyResourceControl;
service.retrieveOwnershipDetails = retrieveOwnershipDetails;
service.retrieveUserPermissionsOnResource =retrieveUserPermissionsOnResource;
/**
* PRIVATE SECTION
*/
/**
* Create a ResourceControl
* @param {ResourceControlTypeString} rcType Type of ResourceControl
* @param {string} rcID ID of involved resource
* @param {ResourceControlOwnershipParameters} ownershipParameters Transcient type from view data to payload
*/
function createResourceControl(rcType, resourceID, ownershipParameters) {
var payload = {
Type: type,
Public: publicOnly,
Type: rcType,
Public: ownershipParameters.Public,
AdministratorsOnly: ownershipParameters.AdministratorsOnly,
ResourceID: resourceID,
Users: userIDs,
Teams: teamIDs,
SubResourceIDs: subResourceIDs
Users: ownershipParameters.Users,
Teams: ownershipParameters.Teams,
SubResourceIds: ownershipParameters.SubResourceIDs
};
return ResourceControl.create({}, payload).$promise;
};
}
service.deleteResourceControl = function(rcID) {
return ResourceControl.remove({id: rcID}).$promise;
};
service.updateResourceControl = function(publicOnly, userIDs, teamIDs, resourceControlId) {
var payload = {
Public: publicOnly,
Users: userIDs,
Teams: teamIDs
/**
* Update a ResourceControl
* @param {String} rcID ID of involved resource
* @param {ResourceControlOwnershipParameters} ownershipParameters Transcient type from view data to payload
*/
function updateResourceControl(rcID, ownershipParameters) {
const payload = {
AdministratorsOnly: ownershipParameters.AdministratorsOnly,
Public: ownershipParameters.Public,
Users: ownershipParameters.Users,
Teams: ownershipParameters.Teams
};
return ResourceControl.update({id: resourceControlId}, payload).$promise;
};
service.applyResourceControl = function(resourceControlType, resourceIdentifier, userId, accessControlData, subResources) {
if (!accessControlData.AccessControlEnabled) {
accessControlData.Ownership = 'public';
}
return ResourceControl.update({id: rcID}, payload).$promise;
}
var authorizedUserIds = [];
var authorizedTeamIds = [];
var publicOnly = false;
switch (accessControlData.Ownership) {
case 'public':
publicOnly = true;
break;
case 'private':
authorizedUserIds.push(userId);
break;
case 'restricted':
angular.forEach(accessControlData.AuthorizedUsers, function(user) {
authorizedUserIds.push(user.Id);
});
angular.forEach(accessControlData.AuthorizedTeams, function(team) {
authorizedTeamIds.push(team.Id);
});
break;
default:
return;
}
return service.createResourceControl(publicOnly, authorizedUserIds,
authorizedTeamIds, resourceIdentifier, resourceControlType, subResources);
};
/**
* END PRIVATE SECTION
*/
service.applyResourceControlChange = function(resourceControlType, resourceId, resourceControl, ownershipParameters) {
/**
* PUBLIC SECTION
*/
/**
* Apply a ResourceControl after Resource creation
* @param {int} userId ID of User performing the action
* @param {AccessControlFormData} accessControlData ResourceControl to apply
* @param {ResourceControlViewModel} resourceControl ResourceControl to update
* @param {[]int} subResources SubResources managed by the ResourceControl
*/
function applyResourceControl(userId, accessControlData, resourceControl, subResources=[]) {
const ownershipParameters = ResourceControlHelper.RCFormDataToOwnershipParameters(userId, accessControlData, subResources);
return updateResourceControl(resourceControl.Id, ownershipParameters);
}
/**
* Duplicate an existing ResourceControl (default to AdministratorsOnly if undefined)
* @param {int} userId ID of User performing the action
* @param {ResourceControlViewModel} oldResourceControl ResourceControl to duplicate
* @param {ResourceControlViewModel} newResourceControl ResourceControl to apply duplication to
*/
function duplicateResourceControl(userId, oldResourceControl, newResourceControl) {
const ownershipParameters = ResourceControlHelper.RCViewModelToOwnershipParameters(userId, oldResourceControl);
return updateResourceControl(newResourceControl.Id, ownershipParameters);
}
/**
* Update an existing ResourceControl or create a new one on existing resource without RC
* @param {ResourceControlTypeString} rcType Type of ResourceControl
* @param {String} resourceId ID of involved Resource
* @param {ResourceControlViewModel} resourceControl Previous ResourceControl (can be undefined)
* @param {AccessControlPanelData} formValues View data generated by AccessControlPanel
*/
function applyResourceControlChange(rcType, resourceId, resourceControl, formValues) {
const ownershipParameters = ResourceControlHelper.RCPanelDataToOwnershipParameters(formValues);
if (resourceControl) {
if (ownershipParameters.ownership === 'administrators') {
return service.deleteResourceControl(resourceControl.Id);
} else {
return service.updateResourceControl(ownershipParameters.publicOnly, ownershipParameters.authorizedUserIds,
ownershipParameters.authorizedTeamIds, resourceControl.Id);
}
return updateResourceControl(resourceControl.Id, ownershipParameters);
} else {
return service.createResourceControl(ownershipParameters.publicOnly, ownershipParameters.authorizedUserIds,
ownershipParameters.authorizedTeamIds, resourceId, resourceControlType);
return createResourceControl(rcType, resourceId, ownershipParameters);
}
};
}
service.retrieveOwnershipDetails = function(resourceControl) {
/**
* Retrive users and team details for ResourceControlViewModel
* @param {ResourceControlViewModel} resourceControl ResourceControl view model
*/
function retrieveOwnershipDetails(resourceControl) {
var deferred = $q.defer();
if (!resourceControl) {
@ -96,9 +124,9 @@ angular.module('portainer.app')
});
return deferred.promise;
};
}
service.retrieveUserPermissionsOnResource = function(userID, isAdministrator, resourceControl) {
function retrieveUserPermissionsOnResource(userID, isAdministrator, resourceControl) {
var deferred = $q.defer();
if (!resourceControl || isAdministrator) {
@ -123,7 +151,11 @@ angular.module('portainer.app')
}
return deferred.promise;
};
}
/**
* END PUBLIC SECTION
*/
return service;
}]);

View file

@ -208,11 +208,6 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
var deferred = $q.defer();
Stack.remove({ id: stack.Id ? stack.Id : stack.Name, external: external, endpointId: endpointId }).$promise
.then(function success() {
if (stack.ResourceControl && stack.ResourceControl.Id) {
return ResourceControlService.deleteResourceControl(stack.ResourceControl.Id);
}
})
.then(function success() {
deferred.resolve();
})

View file

@ -1,3 +1,5 @@
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
angular.module('portainer.app')
.factory('FormValidator', [function FormValidatorFactory() {
'use strict';
@ -9,11 +11,11 @@ angular.module('portainer.app')
return '';
}
if (isAdmin && accessControlData.Ownership === 'restricted' &&
if (isAdmin && accessControlData.Ownership === RCO.RESTRICTED &&
accessControlData.AuthorizedUsers.length === 0 &&
accessControlData.AuthorizedTeams.length === 0) {
return 'You must specify at least one team or user.';
} else if (!isAdmin && accessControlData.Ownership === 'restricted' &&
} else if (!isAdmin && accessControlData.Ownership === RCO.RESTRICTED &&
accessControlData.AuthorizedTeams.length === 0) {
return 'You must specify at least a team.';
}

View file

@ -98,7 +98,6 @@ function ($scope, $state, StackService, Authentication, Notifications, FormValid
var accessControlData = $scope.formValues.AccessControlData;
var userDetails = Authentication.getUserDetails();
var isAdmin = Authentication.isAdmin();
var userId = userDetails.ID;
if (method === 'editor' && $scope.formValues.StackFileContent === '') {
$scope.state.formValidationError = 'Stack file content must not be empty';
@ -116,8 +115,13 @@ function ($scope, $state, StackService, Authentication, Notifications, FormValid
}
$scope.state.actionInProgress = true;
action(name, method)
.then(function success() {
return ResourceControlService.applyResourceControl('stack', name, userId, accessControlData, []);
.then(function success(data) {
if (data.data) {
data = data.data;
}
const userId = userDetails.ID;
const resourceControl = data.ResourceControl;
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
})
.then(function success() {
Notifications.success('Stack successfully deployed');

View file

@ -79,8 +79,8 @@ function ($scope, $q, $state, $transition$, $anchorScroll, ContainerService, Ima
return ContainerService.createAndStartContainer(templateConfiguration);
})
.then(function success(data) {
var containerIdentifier = data.Id;
return ResourceControlService.applyResourceControl('container', containerIdentifier, userId, accessControlData, generatedVolumeIds);
const resourceControl = data.Portainer.ResourceControl;
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl, generatedVolumeIds);
})
.then(function success() {
Notifications.success('Container successfully created');
@ -111,8 +111,9 @@ function ($scope, $q, $state, $transition$, $anchorScroll, ContainerService, Ima
var endpointId = EndpointProvider.endpointID();
StackService.createComposeStackFromGitRepository(stackName, repositoryOptions, template.Env, endpointId)
.then(function success() {
return ResourceControlService.applyResourceControl('stack', stackName, userId, accessControlData, []);
.then(function success(data) {
const resourceControl = data.Portainer.ResourceControl;
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
})
.then(function success() {
Notifications.success('Stack successfully deployed');
@ -148,8 +149,9 @@ function ($scope, $q, $state, $transition$, $anchorScroll, ContainerService, Ima
var endpointId = EndpointProvider.endpointID();
StackService.createSwarmStackFromGitRepository(stackName, repositoryOptions, env, endpointId)
.then(function success() {
return ResourceControlService.applyResourceControl('stack', stackName, userId, accessControlData, []);
.then(function success(data) {
const resourceControl = data.Portainer.ResourceControl;
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
})
.then(function success() {
Notifications.success('Stack successfully deployed');