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

feat(extensions): remove rbac extension (#4157)

* feat(extensions): remove rbac extension client code

* feat(extensions): remove server rbac code

* remove extensions code

* fix(notifications): remove error

* feat(extensions): remove authorizations service

* feat(rbac): deprecate fields

* fix(portainer): revert change

* fix(bouncer): remove rbac authorization check

* feat(sidebar): remove roles link

* fix(portainer): remove portainer module
This commit is contained in:
Chaim Lev-Ari 2020-08-11 08:41:37 +03:00 committed by GitHub
parent 8629738e34
commit 9d18d47194
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
117 changed files with 98 additions and 3487 deletions

View file

@ -281,28 +281,6 @@ angular.module('portainer.app', ['portainer.oauth']).config([
},
};
var extensions = {
name: 'portainer.extensions',
url: '/extensions',
views: {
'content@': {
templateUrl: './views/extensions/extensions.html',
controller: 'ExtensionsController',
},
},
};
var extension = {
name: 'portainer.extensions.extension',
url: '/extension/:id',
views: {
'content@': {
templateUrl: './views/extensions/inspect/extension.html',
controller: 'ExtensionController',
},
},
};
var registries = {
name: 'portainer.registries',
url: '/registries',
@ -469,8 +447,6 @@ angular.module('portainer.app', ['portainer.oauth']).config([
$stateRegistryProvider.register(init);
$stateRegistryProvider.register(initEndpoint);
$stateRegistryProvider.register(initAdmin);
$stateRegistryProvider.register(extensions);
$stateRegistryProvider.register(extension);
$stateRegistryProvider.register(registries);
$stateRegistryProvider.register(registry);
$stateRegistryProvider.register(registryAccess);

View file

@ -10,15 +10,6 @@
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button
ng-if="$ctrl.rbacEnabled"
type="button"
class="btn btn-sm btn-primary"
ng-disabled="($ctrl.dataset | filter:{ Updated: true}).length === 0 "
ng-click="$ctrl.updateAction()"
>
<i class="fa fa-check space-right" aria-hidden="true"></i>Update
</button>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
@ -53,13 +44,6 @@
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.rbacEnabled">
<a ng-click="$ctrl.changeOrderBy('RoleName')">
Role
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RoleName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RoleName' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
@ -79,18 +63,6 @@
<span ng-if="$ctrl.inheritFrom && item.Override" class="text-muted small" style="margin-left: 2px;"><code style="font-size: 85% !important;">override</code></span>
</td>
<td>{{ item.Type }}</td>
<td ng-if="$ctrl.rbacEnabled">
<span ng-if="!item.Updated">
{{ item.Role.Name }}
<a ng-if="!item.Inherited" class="interactive" ng-click="item.Updated = true; item.OldRole = item.Role; $event.stopPropagation();">
<i class="fa fa-edit" aria-hidden="true"></i> Edit
</a>
</span>
<span ng-if="item.Updated">
<select ng-model="item.Role" ng-options="role.Name for role in $ctrl.roles"> </select>
<a class="interactive" ng-click="item.Updated = false; item.Role = item.OldRole; item.OldRole = null; $event.stopPropagation();"><i class="fa fa-times"></i></a>
</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="4" class="text-center text-muted">Loading...</td>

View file

@ -11,7 +11,6 @@ angular.module('portainer.app').component('accessDatatable', {
removeAction: '<',
updateAction: '<',
reverseOrder: '<',
rbacEnabled: '<',
inheritFrom: '<',
},
});

View file

@ -27,17 +27,7 @@
</span>
</div>
</div>
<div class="form-group" ng-if="ctrl.entityType !== 'registry'">
<label class="col-sm-3 col-lg-2 control-label text-left">
Role
</label>
<div class="col-sm-9 col-lg-4">
<select ng-if="ctrl.rbacEnabled" class="form-control" ng-model="ctrl.formValues.selectedRole" ng-options="role.Name for role in ctrl.roles"> </select>
<span class="small text-muted" ng-if="!ctrl.rbacEnabled">
The <a ui-sref="portainer.extensions.extension({id: 3})">Role-Based Access Control extension</a> is required to select a specific role.
</span>
</div>
</div>
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
@ -67,10 +57,8 @@
title-icon="fa-user-lock"
table-key="{{ 'access_' + ctrl.entityType }}"
order-by="Name"
rbac-enabled="ctrl.rbacEnabled && ctrl.entityType !== 'registry'"
inherit-from="ctrl.inheritFrom"
dataset="ctrl.authorizedUsersAndTeams"
roles="ctrl.roles"
update-action="ctrl.updateAction"
remove-action="ctrl.unauthorizeAccess"
>

View file

@ -4,11 +4,9 @@ import angular from 'angular';
class PorAccessManagementController {
/* @ngInject */
constructor(Notifications, ExtensionService, AccessService, RoleService) {
constructor(Notifications, AccessService) {
this.Notifications = Notifications;
this.ExtensionService = ExtensionService;
this.AccessService = AccessService;
this.RoleService = RoleService;
this.unauthorizeAccess = this.unauthorizeAccess.bind(this);
this.updateAction = this.updateAction.bind(this);
@ -31,11 +29,10 @@ class PorAccessManagementController {
const entity = this.accessControlledEntity;
const oldUserAccessPolicies = entity.UserAccessPolicies;
const oldTeamAccessPolicies = entity.TeamAccessPolicies;
const selectedRoleId = this.rbacEnabled ? this.formValues.selectedRole.Id : 0;
const selectedUserAccesses = _.filter(this.formValues.multiselectOutput, (access) => access.Type === 'user');
const selectedTeamAccesses = _.filter(this.formValues.multiselectOutput, (access) => access.Type === 'team');
const accessPolicies = this.AccessService.generateAccessPolicies(oldUserAccessPolicies, oldTeamAccessPolicies, selectedUserAccesses, selectedTeamAccesses, selectedRoleId);
const accessPolicies = this.AccessService.generateAccessPolicies(oldUserAccessPolicies, oldTeamAccessPolicies, selectedUserAccesses, selectedTeamAccesses, 0);
this.accessControlledEntity.UserAccessPolicies = accessPolicies.userAccessPolicies;
this.accessControlledEntity.TeamAccessPolicies = accessPolicies.teamAccessPolicies;
this.updateAccess();
@ -56,18 +53,7 @@ class PorAccessManagementController {
try {
const entity = this.accessControlledEntity;
const parent = this.inheritFrom;
// TODO: refactor
// extract this code and locate it in AccessService.accesses() function
// see resourcePoolAccessController for another usage of AccessService.accesses()
// which needs RBAC support
this.roles = [];
this.rbacEnabled = await this.ExtensionService.extensionEnabled(this.ExtensionService.EXTENSIONS.RBAC);
if (this.rbacEnabled) {
this.roles = await this.RoleService.roles();
this.formValues = {
selectedRole: this.roles[0],
};
}
const data = await this.AccessService.accesses(entity, parent, this.roles);
this.availableUsersAndTeams = _.orderBy(data.availableUsersAndTeams, 'Name', 'asc');
this.authorizedUsersAndTeams = data.authorizedUsersAndTeams;

View file

@ -1,8 +0,0 @@
angular.module('portainer.app').component('extensionItem', {
templateUrl: './extensionItem.html',
controller: 'ExtensionItemController',
bindings: {
model: '<',
currentDate: '<',
},
});

View file

@ -1,47 +0,0 @@
<!-- extension -->
<div class="blocklist-item" ng-click="$ctrl.goToExtensionView()" ng-class="{ 'blocklist-item--disabled': !$ctrl.model.Available }">
<div class="blocklist-item-box">
<!-- extension-image -->
<span ng-if="$ctrl.model.Logo" style="width: 75px; text-align: center;">
<!-- <img class="blocklist-item-logo" ng-src="{{ $ctrl.model.Logo }}" /> -->
<i class="{{ $ctrl.model.Logo }} fa fa-4x blue-icon" aria-hidden="true"></i>
</span>
<span class="blocklist-item-logo" ng-if="!$ctrl.model.Logo">
<i class="fa fa-bolt fa-4x blue-icon" style="margin-left: 14px;" aria-hidden="true"></i>
</span>
<!-- !extension-image -->
<!-- extension-details -->
<span class="col-sm-12">
<!-- blocklist-item-line1 -->
<div class="blocklist-item-line">
<span>
<span class="blocklist-item-title">
{{ $ctrl.model.Name }}
</span>
</span>
<span>
<span class="label label-primary" ng-if="!$ctrl.model.Enabled && !$ctrl.model.Available">coming soon</span>
<span class="label label-warning" ng-if="!$ctrl.model.Enabled && $ctrl.model.Deal && !$ctrl.model.License.Expiration">deal</span>
<span class="label label-danger" ng-if="!$ctrl.model.Enabled && $ctrl.model.License.Expiration && !$ctrl.model.License.Valid">expired</span>
<span class="label label-success" ng-if="$ctrl.model.Enabled && $ctrl.model.License.Valid">enabled</span>
<span class="label label-primary" ng-if="$ctrl.model.Enabled && $ctrl.model.License.Valid && $ctrl.model.UpdateAvailable">update available</span>
</span>
</div>
<!-- !blocklist-item-line1 -->
<!-- blocklist-item-line2 -->
<div class="blocklist-item-line">
<span>
<span class="blocklist-item-desc">
{{ $ctrl.model.ShortDescription }}
</span>
</span>
<span ng-if="$ctrl.model.License.Company">
<span class="small text-muted">Licensed to {{ $ctrl.model.License.Company }} - Expires on {{ $ctrl.model.License.Expiration }}</span>
</span>
</div>
<!-- !blocklist-item-line2 -->
</span>
<!-- !extension-details -->
</div>
<!-- !extension -->
</div>

View file

@ -1,13 +0,0 @@
angular.module('portainer.app').controller('ExtensionItemController', [
'$state',
function ($state) {
var ctrl = this;
ctrl.goToExtensionView = goToExtensionView;
function goToExtensionView() {
if (ctrl.model.Available) {
$state.go('portainer.extensions.extension', { id: ctrl.model.Id });
}
}
},
]);

View file

@ -1,7 +0,0 @@
angular.module('portainer.app').component('extensionList', {
templateUrl: './extensionList.html',
bindings: {
extensions: '<',
currentDate: '<',
},
});

View file

@ -1,13 +0,0 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa fa-bolt" aria-hidden="true" style="margin-right: 2px;"></i> Available extensions </div>
</div>
<div class="blocklist">
<extension-item ng-repeat="extension in $ctrl.extensions" model="extension" current-date="$ctrl.currentDate"></extension-item>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -1,17 +0,0 @@
export function ExtensionViewModel(data) {
this.Id = data.Id;
this.Name = data.Name;
this.Enabled = data.Enabled;
this.Description = data.Description;
this.Price = data.Price;
this.PriceDescription = data.PriceDescription;
this.Available = data.Available;
this.Deal = data.Deal;
this.ShortDescription = data.ShortDescription;
this.License = data.License;
this.Version = data.Version;
this.UpdateAvailable = data.UpdateAvailable;
this.ShopURL = data.ShopURL;
this.Images = data.Images;
this.Logo = data.Logo;
}

View file

@ -1,18 +0,0 @@
angular.module('portainer.app').factory('Extension', [
'$resource',
'API_ENDPOINT_EXTENSIONS',
function ExtensionFactory($resource, API_ENDPOINT_EXTENSIONS) {
'use strict';
return $resource(
API_ENDPOINT_EXTENSIONS + '/:id/:action',
{},
{
create: { method: 'POST' },
query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } },
delete: { method: 'DELETE', params: { id: '@id' } },
update: { method: 'POST', params: { id: '@id', action: 'update' } },
}
);
},
]);

View file

@ -11,15 +11,7 @@ angular.module('portainer.app').factory('AccessService', [
'use strict';
var service = {};
function _getRole(roles, roleId) {
if (roles.length) {
const role = _.find(roles, (role) => role.Id === roleId);
return role ? role : { Id: 0, Name: '-' };
}
return { Id: 0, Name: '-' };
}
function _mapAccessData(accesses, authorizedPolicies, inheritedPolicies, roles) {
function _mapAccessData(accesses, authorizedPolicies, inheritedPolicies) {
var availableAccesses = [];
var authorizedAccesses = [];
@ -30,14 +22,11 @@ angular.module('portainer.app').factory('AccessService', [
const inherited = inheritedPolicies && inheritedPolicies[access.Id];
if (authorized && inherited) {
access.Role = _getRole(roles, authorizedPolicies[access.Id].RoleId);
access.Override = true;
authorizedAccesses.push(access);
} else if (authorized && !inherited) {
access.Role = _getRole(roles, authorizedPolicies[access.Id].RoleId);
authorizedAccesses.push(access);
} else if (!authorized && inherited) {
access.Role = _getRole(roles, inheritedPolicies[access.Id].RoleId);
access.Inherited = true;
authorizedAccesses.push(access);
availableAccesses.push(access);
@ -52,7 +41,7 @@ angular.module('portainer.app').factory('AccessService', [
};
}
function getAccesses(authorizedUserPolicies, authorizedTeamPolicies, inheritedUserPolicies, inheritedTeamPolicies, roles) {
function getAccesses(authorizedUserPolicies, authorizedTeamPolicies, inheritedUserPolicies, inheritedTeamPolicies) {
var deferred = $q.defer();
$q.all({
@ -67,8 +56,8 @@ angular.module('portainer.app').factory('AccessService', [
return new TeamAccessViewModel(team);
});
var userAccessData = _mapAccessData(userAccesses, authorizedUserPolicies, inheritedUserPolicies, roles);
var teamAccessData = _mapAccessData(teamAccesses, authorizedTeamPolicies, inheritedTeamPolicies, roles);
var userAccessData = _mapAccessData(userAccesses, authorizedUserPolicies, inheritedUserPolicies);
var teamAccessData = _mapAccessData(teamAccesses, authorizedTeamPolicies, inheritedTeamPolicies);
var accessData = {
availableUsersAndTeams: userAccessData.available.concat(teamAccessData.available),
@ -84,7 +73,7 @@ angular.module('portainer.app').factory('AccessService', [
return deferred.promise;
}
async function accessesAsync(entity, parent, roles) {
async function accessesAsync(entity, parent) {
try {
if (!entity) {
throw { msg: 'Unable to retrieve accesses' };
@ -101,14 +90,14 @@ angular.module('portainer.app').factory('AccessService', [
if (parent && !parent.TeamAccessPolicies) {
parent.TeamAccessPolicies = {};
}
return await getAccesses(entity.UserAccessPolicies, entity.TeamAccessPolicies, parent ? parent.UserAccessPolicies : {}, parent ? parent.TeamAccessPolicies : {}, roles);
return await getAccesses(entity.UserAccessPolicies, entity.TeamAccessPolicies, parent ? parent.UserAccessPolicies : {}, parent ? parent.TeamAccessPolicies : {});
} catch (err) {
throw err;
}
}
function accesses(entity, parent, roles) {
return $async(accessesAsync, entity, parent, roles);
function accesses(entity, parent) {
return $async(accessesAsync, entity, parent);
}
service.accesses = accesses;

View file

@ -1,102 +0,0 @@
import _ from 'lodash-es';
import { ExtensionViewModel } from '../../models/extension';
angular.module('portainer.app').factory('ExtensionService', [
'$q',
'Extension',
'StateManager',
'$async',
'FileUploadService',
function ExtensionServiceFactory($q, Extension, StateManager, $async, FileUploadService) {
'use strict';
var service = {};
service.EXTENSIONS = Object.freeze({
REGISTRY_MANAGEMENT: 1,
OAUTH_AUTHENTICATION: 2,
RBAC: 3,
});
service.enable = enable;
service.update = update;
service.delete = _delete;
service.extensions = extensions;
service.extension = extension;
service.extensionEnabled = extensionEnabled;
service.retrieveAndSaveEnabledExtensions = retrieveAndSaveEnabledExtensions;
function enable(license, extensionFile) {
if (extensionFile) {
return FileUploadService.uploadExtension(license, extensionFile);
} else {
return Extension.create({ license: license }).$promise;
}
}
function update(id, version) {
return Extension.update({ id: id, version: version }).$promise;
}
function _delete(id) {
return Extension.delete({ id: id }).$promise;
}
function extensions(store) {
var deferred = $q.defer();
Extension.query({ store: store })
.$promise.then(function success(data) {
var extensions = data.map(function (item) {
return new ExtensionViewModel(item);
});
deferred.resolve(extensions);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve extensions', err: err });
});
return deferred.promise;
}
function extension(id) {
var deferred = $q.defer();
Extension.get({ id: id })
.$promise.then(function success(data) {
var extension = new ExtensionViewModel(data);
deferred.resolve(extension);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve extension details', err: err });
});
return deferred.promise;
}
function extensionEnabled(extensionId) {
return $async(extensionsEnabledAsync, extensionId);
}
async function extensionsEnabledAsync(extensionId) {
if (extensionId === service.EXTENSIONS.RBAC) {
return StateManager.getExtension(extensionId) ? true : false;
} else {
const extensions = await service.extensions(false);
const extension = _.find(extensions, (ext) => ext.Id === extensionId);
return extension ? extension.Enabled : false;
}
}
function retrieveAndSaveEnabledExtensions() {
return $async(retrieveAndSaveEnabledExtensionsAsync);
}
async function retrieveAndSaveEnabledExtensionsAsync() {
const extensions = await service.extensions(false);
_.forEach(extensions, (ext) => delete ext.License);
StateManager.saveExtensions(extensions);
}
return service;
},
]);

View file

@ -197,19 +197,6 @@ angular.module('portainer.app').factory('FileUploadService', [
return $q.all(queue);
};
service.uploadExtension = function (license, extensionFile) {
const payload = {
License: license,
file: extensionFile,
ArchiveFileName: extensionFile.name,
};
return Upload.upload({
url: 'api/extensions/upload',
data: payload,
ignoreLoadingBar: true,
});
};
return service;
},
]);

View file

@ -50,12 +50,6 @@ angular.module('portainer.app').factory('LocalStorage', [
getUIState: function () {
return localStorageService.get('UI_STATE');
},
storeExtensionState: function (state) {
localStorageService.set('EXTENSION_STATE', state);
},
getExtensionState: function () {
return localStorageService.get('EXTENSION_STATE');
},
storeJWT: function (jwt) {
localStorageService.set('JWT', jwt);
},
@ -145,7 +139,7 @@ angular.module('portainer.app').factory('LocalStorage', [
localStorageService.clearAll();
},
cleanAuthData() {
localStorageService.remove('JWT', 'EXTENSION_STATE', 'APPLICATION_STATE', 'LOGIN_STATE_UUID');
localStorageService.remove('JWT', 'APPLICATION_STATE', 'LOGIN_STATE_UUID');
},
cleanEndpointData() {
localStorageService.remove('ENDPOINT_ID', 'ENDPOINT_PUBLIC_URL', 'ENDPOINT_OFFLINE_MODE', 'ENDPOINTS_DATA', 'ENDPOINT_STATE');

View file

@ -1,4 +1,3 @@
import _ from 'lodash-es';
import moment from 'moment';
angular.module('portainer.app').factory('StateManager', [
@ -60,7 +59,6 @@ angular.module('portainer.app').factory('StateManager', [
manager.clean = function () {
state.endpoint = {};
state.extensions = [];
};
manager.updateLogo = function (logoURL) {
@ -174,11 +172,6 @@ angular.module('portainer.app').factory('StateManager', [
state.UI = UIState;
}
const extensionState = LocalStorage.getExtensionState();
if (extensionState) {
state.extensions = extensionState;
}
var endpointState = LocalStorage.getEndpointState();
if (endpointState) {
state.endpoint = endpointState;
@ -276,19 +269,6 @@ angular.module('portainer.app').factory('StateManager', [
return state.endpoint.agentApiVersion;
};
manager.saveExtensions = function (extensions) {
state.extensions = extensions;
LocalStorage.storeExtensionState(state.extensions);
};
manager.getExtensions = function () {
return state.extensions;
};
manager.getExtension = function (extensionId) {
return _.find(state.extensions, { Id: extensionId, Enabled: true });
};
return manager;
},
]);

View file

@ -12,7 +12,6 @@ class AuthenticationController {
Authentication,
UserService,
EndpointService,
ExtensionService,
StateManager,
Notifications,
SettingsService,
@ -28,7 +27,6 @@ class AuthenticationController {
this.Authentication = Authentication;
this.UserService = UserService;
this.EndpointService = EndpointService;
this.ExtensionService = ExtensionService;
this.StateManager = StateManager;
this.Notifications = Notifications;
this.SettingsService = SettingsService;
@ -47,7 +45,6 @@ class AuthenticationController {
OAuthProvider: '',
};
this.retrieveAndSaveEnabledExtensionsAsync = this.retrieveAndSaveEnabledExtensionsAsync.bind(this);
this.checkForEndpointsAsync = this.checkForEndpointsAsync.bind(this);
this.checkForLatestVersionAsync = this.checkForLatestVersionAsync.bind(this);
this.postLoginSteps = this.postLoginSteps.bind(this);
@ -117,14 +114,6 @@ class AuthenticationController {
* POST LOGIN STEPS SECTION
*/
async retrieveAndSaveEnabledExtensionsAsync() {
try {
await this.ExtensionService.retrieveAndSaveEnabledExtensions();
} catch (err) {
this.error(err, 'Unable to retrieve enabled extensions');
}
}
async checkForEndpointsAsync() {
try {
const endpoints = await this.EndpointService.endpoints(0, 1);
@ -158,7 +147,6 @@ class AuthenticationController {
}
async postLoginSteps() {
await this.retrieveAndSaveEnabledExtensionsAsync();
await this.checkForEndpointsAsync();
await this.checkForLatestVersionAsync();
}

View file

@ -1,129 +0,0 @@
<rd-header>
<rd-header-title title-text="Extensions"></rd-header-title>
<rd-header-content>Portainer extensions</rd-header-content>
</rd-header>
<information-panel title-text="Information">
<span class="text-muted" style="font-size: 90%;">
<p>
Portainer CE is a great way of managing clusters, provisioning containers and services and managing container environment lifecycles. To extend the benefit of Portainer CE
even more, and to address the needs of larger, complex or critical environments, the Portainer team provides a growing range of low-cost Extensions.
</p>
<p>
To ensure that Portainer remains the best choice for managing production container platforms, the Portainer team have chosen a modular, extensible design approach, where
additional capability can be added to the Portainer CE core as needed, and at very low cost.
</p>
<p>
Available through a simple subscription process from the list below, Portainer Extensions provide a simple way to enhance Portainer CEs core functionality through
incremental capability in important areas.
</p>
<p>
For additional information on Portainer Extensions, see our website
<a href="https://www.portainer.io/products-services/portainer-extension-software/" target="_blank">here</a>.
</p>
</span>
</information-panel>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="extensionEnableForm">
<div class="col-sm-12 form-section-title">
Enable extension
</div>
<div class="form-group">
<div class="col-sm-12">
<p class="small text-muted" ng-if="!state.offlineActivation">
Portainer will download the latest version of the extension. Ensure that you have a valid license.
</p>
<p class="small text-muted" ng-if="state.offlineActivation">
You will need to upload the extension archive manually. Ensure that you have a valid license.
</p>
<p class="small text-muted" ng-if="state.offlineActivation">
You can download the latest version of our extensions <a target="_blank" href="https://downloads.portainer.io/extensions.zip">here</a>.
</p>
<p>
<a class="small interactive" ng-if="!state.offlineActivation" ng-click="state.offlineActivation = true;">
<i class="fa fa-toggle-off space-right" aria-hidden="true"></i> Switch to offline activation
</a>
<a class="small interactive" ng-if="state.offlineActivation" ng-click="state.offlineActivation = false;">
<i class="fa fa-wifi space-right" aria-hidden="true"></i> Switch to online activation
</a>
</p>
</div>
</div>
<div class="form-group">
<label for="extension_license" class="col-sm-2 control-label text-left">License</label>
<div class="col-sm-10">
<input
type="text"
name="extension_license"
class="form-control"
ng-model="formValues.License"
ng-change="isValidLicenseFormat(extensionEnableForm)"
required
placeholder="Enter a license key here"
/>
</div>
</div>
<div class="form-group" ng-show="extensionEnableForm.extension_license.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="extensionEnableForm.extension_license.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
<p ng-message="invalidLicense"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Invalid license format.</p>
</div>
</div>
</div>
<div class="form-group" ng-if="state.offlineActivation">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ngf-select ng-model="formValues.ExtensionFile" style="margin-left: 0px;">Select file</button>
<span style="margin-left: 5px;">
{{ formValues.ExtensionFile.name }}
<i class="fa fa-times red-icon" ng-if="!formValues.ExtensionFile" aria-hidden="true"></i>
</span>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-click="enableExtension()"
ng-disabled="state.actionInProgress || !extensionEnableForm.$valid || (state.offlineActivation && !formValues.ExtensionFile)"
button-spinner="state.actionInProgress"
style="margin-left: 0px;"
>
<span ng-hide="state.actionInProgress">Enable extension</span>
<span ng-show="state.actionInProgress">Enabling extension...</span>
</button>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="extensions && extensions.length > 0">
<div class="col-sm-12">
<extension-list current-date="state.currentDate" extensions="extensions"></extension-list>
</div>
</div>
<information-panel title-text="Error" ng-if="extensions && extensions.length === 0">
<span class="small text-muted">
<p>
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i>
Portainer must be connected to the Internet to fetch the list of available extensions.
</p>
</span>
</information-panel>

View file

@ -1,67 +0,0 @@
import moment from 'moment';
angular.module('portainer.app').controller('ExtensionsController', [
'$scope',
'$state',
'ExtensionService',
'Notifications',
function ($scope, $state, ExtensionService, Notifications) {
$scope.state = {
actionInProgress: false,
currentDate: moment().format('YYYY-MM-dd'),
};
$scope.formValues = {
License: '',
ExtensionFile: null,
};
function initView() {
ExtensionService.extensions(true)
.then(function onSuccess(data) {
$scope.extensions = data;
})
.catch(function onError(err) {
$scope.extensions = [];
Notifications.error('Failure', err, 'Unable to access extension store');
});
}
$scope.enableExtension = function () {
const license = $scope.formValues.License;
const extensionFile = $scope.formValues.ExtensionFile;
$scope.state.actionInProgress = true;
ExtensionService.enable(license, extensionFile)
.then(function onSuccess() {
return ExtensionService.retrieveAndSaveEnabledExtensions();
})
.then(function () {
Notifications.success('Extension successfully enabled');
$state.reload();
})
.catch(function onError(err) {
Notifications.error('Failure', err, 'Unable to enable extension');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
};
$scope.isValidLicenseFormat = function (form) {
var valid = true;
if (!$scope.formValues.License) {
return;
}
if (isNaN($scope.formValues.License[0])) {
valid = false;
}
form.extension_license.$setValidity('invalidLicense', valid);
};
initView();
},
]);

View file

@ -1,196 +0,0 @@
<rd-header>
<rd-header-title title-text="Extension details"></rd-header-title>
<rd-header-content> <a ui-sref="portainer.extensions">Portainer extensions</a> &gt; {{ extension.Name }} </rd-header-content>
</rd-header>
<div class="row" ng-if="extension">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<div style="display: flex;">
<div style="flex-grow: 4; display: flex; flex-direction: column; justify-content: space-between;">
<div class="form-group">
<div class="text-muted" style="font-size: 150%;"> {{ extension.Name }} extension </div>
<div class="small text-muted" style="margin-top: 5px;"> By <a href="https://portainer.io" href="_blank">Portainer.io</a> </div>
</div>
<div class="form-group">
<div class="text-muted">
{{ extension.ShortDescription }}
</div>
</div>
</div>
<div style="flex-grow: 1; border-left: 1px solid #777;">
<div class="form-group" style="margin-left: 40px;">
<div style="font-size: 125%; border-bottom: 2px solid #2d3e63; padding-bottom: 5px;">
<span ng-if="extension.Enabled">Enabled</span>
<span ng-if="!extension.Enabled && extension.License.Expiration && !extension.License.Valid">Expired</span>
<span ng-if="!extension.Enabled && !extension.License.Expiration">{{ extension.Price }}</span>
</div>
<div class="small text-muted col-sm-12" style="margin: 15px 0 15px 0;" ng-if="!extension.Enabled">
{{ extension.PriceDescription }}
</div>
<div style="margin-top: 10px; margin-bottom: 95px;" ng-if="!extension.Enabled && extension.Available">
<label for="instances_qty" class="col-sm-7 control-label text-left" style="margin-top: 7px;">Instances</label>
<div class="col-sm-5">
<input type="number" class="form-control" ng-model="formValues.instances" id="instances_qty" placeholder="1" min="1" />
</div>
</div>
<div style="margin-top: 15px;" ng-if="!extension.Enabled && extension.Available">
<a href="{{ extension.ShopURL }}&quantity={{ formValues.instances }}" target="_blank" class="btn btn-primary btn-sm" style="width: 100%; margin-left: 0;">
Buy
</a>
</div>
<div style="margin-top: 10px;" ng-if="!extension.Enabled && extension.Available">
<a ui-sref="portainer.extensions" class="btn btn-primary btn-sm" style="width: 100%; margin-left: 0;">
Add license key
</a>
</div>
<div style="margin-top: 15px;" ng-if="!extension.Enabled && !extension.Available">
<btn class="btn btn-primary btn-sm" style="width: 100%; margin-left: 0;" disabled>Coming soon</btn>
</div>
<div style="margin-top: 15px;" ng-if="extension.Enabled && extension.UpdateAvailable && !state.offlineUpdate">
<button
type="button"
class="btn btn-primary btn-sm"
ng-click="updateExtensionOnline(extension)"
ng-disabled="state.onlineUpdateInProgress"
button-spinner="state.onlineUpdateInProgress"
style="width: 100%; margin-left: 0;"
>
<span ng-hide="state.onlineUpdateInProgress">Update via Internet</span>
<span ng-show="state.onlineUpdateInProgress">Updating extension...</span>
</button>
</div>
<div style="margin-top: 5px;" ng-if="extension.Enabled">
<button
type="button"
class="btn btn-danger btn-sm"
ng-click="deleteExtension(extension)"
ng-disabled="state.deleteInProgress"
button-spinner="state.deleteInProgress"
style="width: 100%; margin-left: 0;"
>
<span ng-hide="state.deleteInProgress">Delete</span>
<span ng-show="state.deleteInProgress">Deleting extension...</span>
</button>
</div>
<div style="margin-top: 15px;" ng-if="extension.Enabled && extension.UpdateAvailable">
<p>
<a class="small interactive" ng-if="!state.offlineUpdate" ng-click="state.offlineUpdate = true;">
<i class="fa fa-toggle-off space-right" aria-hidden="true"></i> Switch to offline update
</a>
<a class="small interactive" ng-if="state.offlineUpdate" ng-click="state.offlineUpdate = false;">
<i class="fa fa-wifi space-right" aria-hidden="true"></i> Switch to online update
</a>
</p>
</div>
</div>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="extension && state.offlineUpdate">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal">
<div class="col-sm-12 form-section-title">
<span>
Offline update
</span>
</div>
<div class="form-group">
<div class="col-sm-12">
<p class="small text-muted" ng-if="state.offlineUpdate">
You will need to upload the extension archive manually. You can download the latest version of our extensions
<a target="_blank" href="https://download.portainer.io/extensions.zip">here</a>.
</p>
</div>
</div>
<div class="form-group" ng-if="state.offlineUpdate">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ngf-select ng-model="formValues.ExtensionFile" style="margin-left: 0px;">Select file</button>
<span style="margin-left: 5px;">
{{ formValues.ExtensionFile.name }}
<i class="fa fa-times red-icon" ng-if="!formValues.ExtensionFile" aria-hidden="true"></i>
</span>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-click="updateExtensionOffline(extension)"
ng-disabled="state.offlineUpdateInProgress || !formValues.ExtensionFile"
button-spinner="state.offlineUpdateInProgress"
style="margin-left: 0px;"
>
<span ng-hide="state.offlineUpdateInProgress">Update extension</span>
<span ng-show="state.offlineUpdateInProgress">Updating extension...</span>
</button>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="extension">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<div class="col-sm-12 form-section-title">
<span>
Description
</span>
</div>
<div class="form-group" ng-if="extension.Description">
<div class="text-muted" style="font-size: 90%;" ng-bind-html="extension.Description"></div>
</div>
<div class="form-group" ng-if="!extension.Description">
<div class="small text-muted">
<p>
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i>
Unable to provide a description in an offline environment.
</p>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="extension.Description && extension.Images">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<div class="col-sm-12 form-section-title">
<span>
Screenshots
</span>
</div>
<div style="text-align: center;">
<div ng-repeat="image in extension.Images" style="margin-top: 25px; cursor: zoom-in;">
<img ng-src="{{ image }}" ng-click="enlargeImage(image)" style="max-width: 1024px;" />
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -1,90 +0,0 @@
angular.module('portainer.app').controller('ExtensionController', [
'$q',
'$scope',
'$transition$',
'$state',
'ExtensionService',
'Notifications',
'ModalService',
function ($q, $scope, $transition$, $state, ExtensionService, Notifications, ModalService) {
$scope.state = {
onlineUpdateInProgress: false,
offlineUpdateInProgress: false,
deleteInProgress: false,
offlineUpdate: false,
};
$scope.formValues = {
instances: 1,
extensionFile: null,
};
$scope.updateExtensionOnline = updateExtensionOnline;
$scope.updateExtensionOffline = updateExtensionOffline;
$scope.deleteExtension = deleteExtension;
$scope.enlargeImage = enlargeImage;
function enlargeImage(image) {
ModalService.enlargeImage(image);
}
function deleteExtension(extension) {
$scope.state.deleteInProgress = true;
ExtensionService.delete(extension.Id)
.then(function onSuccess() {
Notifications.success('Extension successfully deleted');
$state.go('portainer.extensions');
})
.catch(function onError(err) {
Notifications.error('Failure', err, 'Unable to delete extension');
})
.finally(function final() {
$scope.state.deleteInProgress = false;
});
}
function updateExtensionOnline(extension) {
$scope.state.onlineUpdateInProgress = true;
ExtensionService.update(extension.Id, extension.Version)
.then(function onSuccess() {
Notifications.success('Extension successfully updated');
$state.reload();
})
.catch(function onError(err) {
Notifications.error('Failure', err, 'Unable to update extension');
})
.finally(function final() {
$scope.state.onlineUpdateInProgress = false;
});
}
function updateExtensionOffline(extension) {
$scope.state.offlineUpdateInProgress = true;
const extensionFile = $scope.formValues.ExtensionFile;
ExtensionService.enable(extension.License.LicenseKey, extensionFile)
.then(function onSuccess() {
Notifications.success('Extension successfully updated');
$state.reload();
})
.catch(function onError(err) {
Notifications.error('Failure', err, 'Unable to update extension');
})
.finally(function final() {
$scope.state.offlineUpdateInProgress = false;
});
}
function initView() {
ExtensionService.extension($transition$.params().id)
.then(function onSuccess(extension) {
$scope.extension = extension;
})
.catch(function onError(err) {
Notifications.error('Failure', err, 'Unable to retrieve extension information');
});
}
initView();
},
]);

View file

@ -8,8 +8,7 @@ angular.module('portainer.app').controller('InitAdminController', [
'SettingsService',
'UserService',
'EndpointService',
'ExtensionService',
function ($async, $scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService, ExtensionService) {
function ($async, $scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService) {
$scope.logo = StateManager.getState().application.logo;
$scope.formValues = {
@ -23,18 +22,6 @@ angular.module('portainer.app').controller('InitAdminController', [
actionInProgress: false,
};
function retrieveAndSaveEnabledExtensions() {
return $async(retrieveAndSaveEnabledExtensionsAsync);
}
async function retrieveAndSaveEnabledExtensionsAsync() {
try {
await ExtensionService.retrieveAndSaveEnabledExtensions();
} catch (err) {
Notifications.error('Failure', err, 'Unable to retrieve enabled extensions');
}
}
$scope.createAdminUser = function () {
var username = $scope.formValues.Username;
var password = $scope.formValues.Password;
@ -44,9 +31,6 @@ angular.module('portainer.app').controller('InitAdminController', [
.then(function success() {
return Authentication.login(username, password);
})
.then(function success() {
return retrieveAndSaveEnabledExtensions();
})
.then(function success() {
StateManager.updateEnableTelemetry($scope.formValues.enableTelemetry);
return SettingsService.update({ enableTelemetry: $scope.formValues.enableTelemetry });

View file

@ -101,10 +101,7 @@
<div class="col-sm-12">
<label for="toggle_allowvolumebrowser" class="control-label text-left">
Enable volume management for non-administrators
<portainer-tooltip
position="bottom"
message="When enabled, non-admin users & users with helpdesk, standard and read-only roles from the RBAC extension will be able to use Portainer volume management features."
></portainer-tooltip>
<portainer-tooltip position="bottom" message="When enabled, regular users will be able to use Portainer volume management features."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="toggle_allowvolumebrowser" ng-model="formValues.enableVolumeBrowser" /><i></i> </label>
</div>

View file

@ -111,9 +111,6 @@
<li class="sidebar-title">
<span>Settings</span>
</li>
<li class="sidebar-list" ng-if="isAdmin">
<a ui-sref="portainer.extensions" ui-sref-active="active">Extensions <span class="menu-icon fa fa-bolt fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="isAdmin || isTeamLeader">
<a ui-sref="portainer.users" ui-sref-active="active">Users <span class="menu-icon fa fa-users fa-fw"></span></a>
<div
@ -123,29 +120,11 @@
($state.current.name === 'portainer.users' ||
$state.current.name === 'portainer.users.user' ||
$state.current.name === 'portainer.teams' ||
$state.current.name === 'portainer.teams.team' ||
$state.current.name === 'portainer.roles' ||
$state.current.name === 'portainer.roles.role' ||
$state.current.name === 'portainer.roles.new')
$state.current.name === 'portainer.teams.team')
"
>
<a ui-sref="portainer.teams" ui-sref-active="active">Teams</a>
</div>
<div
class="sidebar-sublist"
ng-if="
toggle &&
($state.current.name === 'portainer.users' ||
$state.current.name === 'portainer.users.user' ||
$state.current.name === 'portainer.teams' ||
$state.current.name === 'portainer.teams.team' ||
$state.current.name === 'portainer.roles' ||
$state.current.name === 'portainer.roles.role' ||
$state.current.name === 'portainer.roles.new')
"
>
<a ui-sref="portainer.roles" ui-sref-active="active">Roles</a>
</div>
</li>
<li class="sidebar-list" ng-if="isAdmin">
<a ui-sref="portainer.endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug fa-fw"></span></a>

View file

@ -7,8 +7,7 @@ angular.module('portainer.app').controller('SidebarController', [
'Authentication',
'UserService',
'EndpointProvider',
'ExtensionService',
function ($q, $scope, $transitions, StateManager, Notifications, Authentication, UserService, EndpointProvider, ExtensionService) {
function ($q, $scope, $transitions, StateManager, Notifications, Authentication, UserService, EndpointProvider) {
function checkPermissions(memberships) {
var isLeader = false;
angular.forEach(memberships, function (membership) {
@ -48,15 +47,7 @@ angular.module('portainer.app').controller('SidebarController', [
const isAdmin = Authentication.isAdmin();
const { allowStackManagementForRegularUsers } = $scope.applicationState.application;
if (isAdmin || allowStackManagementForRegularUsers) {
return true;
}
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
if (rbacEnabled) {
return Authentication.hasAuthorizations(['EndpointResourcesAccess']);
}
return false;
return isAdmin || allowStackManagementForRegularUsers;
}
$transitions.onEnter({}, async () => {

View file

@ -1,7 +1,7 @@
angular.module('portainer.app').controller('StacksController', StacksController);
/* @ngInject */
function StacksController($scope, $state, Notifications, StackService, ModalService, EndpointProvider, Authentication, StateManager, ExtensionService) {
function StacksController($scope, $state, Notifications, StackService, ModalService, EndpointProvider, Authentication, StateManager) {
$scope.removeAction = function (selectedItems) {
ModalService.confirmDeletion('Do you want to remove the selected stack(s)? Associated services will be removed as well.', function onConfirm(confirmed) {
if (!confirmed) {
@ -56,24 +56,7 @@ function StacksController($scope, $state, Notifications, StackService, ModalServ
async function loadCreateEnabled() {
const appState = StateManager.getState().application;
if (appState.allowStackManagementForRegularUsers) {
return true;
}
let isAdmin = true;
if (appState.authentication) {
isAdmin = Authentication.isAdmin();
}
if (isAdmin) {
return true;
}
const RBACExtensionEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
if (!RBACExtensionEnabled) {
return false;
}
return Authentication.hasAuthorizations(['EndpointResourcesAccess']);
return appState.allowStackManagementForRegularUsers || Authentication.isAdmin();
}
async function initView() {