diff --git a/api/http/handler/extensions/handler.go b/api/http/handler/extensions/handler.go
index b7ebbd06b..bba32f389 100644
--- a/api/http/handler/extensions/handler.go
+++ b/api/http/handler/extensions/handler.go
@@ -23,7 +23,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
}
h.Handle("/extensions",
- bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
+ bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
h.Handle("/extensions",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost)
h.Handle("/extensions/{id}",
diff --git a/api/http/handler/registries/handler.go b/api/http/handler/registries/handler.go
index a8f3ae24c..26edebd15 100644
--- a/api/http/handler/registries/handler.go
+++ b/api/http/handler/registries/handler.go
@@ -18,6 +18,7 @@ func hideFields(registry *portainer.Registry) {
// Handler is the HTTP handler used to handle registry operations.
type Handler struct {
*mux.Router
+ requestBouncer *security.RequestBouncer
RegistryService portainer.RegistryService
ExtensionService portainer.ExtensionService
FileService portainer.FileService
@@ -27,7 +28,8 @@ type Handler struct {
// NewHandler creates a handler to manage registry operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
- Router: mux.NewRouter(),
+ Router: mux.NewRouter(),
+ requestBouncer: bouncer,
}
h.Handle("/registries",
@@ -35,7 +37,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
h.Handle("/registries",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryList))).Methods(http.MethodGet)
h.Handle("/registries/{id}",
- bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet)
+ bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet)
h.Handle("/registries/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut)
h.Handle("/registries/{id}/access",
@@ -45,7 +47,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
h.Handle("/registries/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
h.PathPrefix("/registries/{id}/v2").Handler(
- bouncer.AdministratorAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
+ bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
return h
}
diff --git a/api/http/handler/registries/proxy.go b/api/http/handler/registries/proxy.go
index b83bcc549..844dc7e79 100644
--- a/api/http/handler/registries/proxy.go
+++ b/api/http/handler/registries/proxy.go
@@ -24,6 +24,11 @@ func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *htt
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
}
+ err = handler.requestBouncer.RegistryAccess(r, registry)
+ if err != nil {
+ return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied}
+ }
+
extension, err := handler.ExtensionService.Extension(portainer.RegistryManagementExtension)
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err}
diff --git a/api/http/handler/registries/registry_inspect.go b/api/http/handler/registries/registry_inspect.go
index 96cdf84ac..1dd9a3ffb 100644
--- a/api/http/handler/registries/registry_inspect.go
+++ b/api/http/handler/registries/registry_inspect.go
@@ -23,6 +23,11 @@ func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
}
+ err = handler.requestBouncer.RegistryAccess(r, registry)
+ if err != nil {
+ return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied}
+ }
+
hideFields(registry)
return response.JSON(w, registry)
}
diff --git a/api/http/security/authorization.go b/api/http/security/authorization.go
index 6ffa3a0e4..4a0bac587 100644
--- a/api/http/security/authorization.go
+++ b/api/http/security/authorization.go
@@ -153,10 +153,10 @@ func authorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *porta
return true
}
-// AuthorizedEndpointGroupAccess ensure that the user can access the specified endpoint group.
+// authorizedEndpointGroupAccess ensure that the user can access the specified endpoint group.
// It will check if the user is part of the authorized users or part of a team that is
// listed in the authorized teams.
-func AuthorizedEndpointGroupAccess(endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
+func authorizedEndpointGroupAccess(endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
return authorizedAccess(userID, memberships, endpointGroup.AuthorizedUsers, endpointGroup.AuthorizedTeams)
}
diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go
index de0c75523..fa88612a8 100644
--- a/api/http/security/bouncer.go
+++ b/api/http/security/bouncer.go
@@ -111,6 +111,31 @@ func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portain
return nil
}
+// RegistryAccess retrieves the JWT token from the request context and verifies
+// that the user can access the specified registry.
+// An error is returned when access is denied.
+func (bouncer *RequestBouncer) RegistryAccess(r *http.Request, registry *portainer.Registry) error {
+ tokenData, err := RetrieveTokenData(r)
+ if err != nil {
+ return err
+ }
+
+ if tokenData.Role == portainer.AdministratorRole {
+ return nil
+ }
+
+ memberships, err := bouncer.teamMembershipService.TeamMembershipsByUserID(tokenData.ID)
+ if err != nil {
+ return err
+ }
+
+ if !AuthorizedRegistryAccess(registry, tokenData.ID, memberships) {
+ return portainer.ErrEndpointAccessDenied
+ }
+
+ return nil
+}
+
// mwSecureHeaders provides secure headers middleware for handlers.
func mwSecureHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
diff --git a/api/http/security/filter.go b/api/http/security/filter.go
index 878a66689..4d44043b3 100644
--- a/api/http/security/filter.go
+++ b/api/http/security/filter.go
@@ -124,7 +124,7 @@ func FilterEndpointGroups(endpointGroups []portainer.EndpointGroup, context *Res
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
for _, group := range endpointGroups {
- if AuthorizedEndpointGroupAccess(&group, context.UserID, context.UserMemberships) {
+ if authorizedEndpointGroupAccess(&group, context.UserID, context.UserMemberships) {
filteredEndpointGroups = append(filteredEndpointGroups, group)
}
}
diff --git a/app/app.js b/app/app.js
index 8a05b219b..f1d2c14ca 100644
--- a/app/app.js
+++ b/app/app.js
@@ -45,7 +45,7 @@ function initAuthentication(authManager, Authentication, $rootScope, $state) {
// to have more controls on which URL should trigger the unauthenticated state.
$rootScope.$on('unauthenticated', function (event, data) {
if (!_.includes(data.config.url, '/v2/')) {
- $state.go('portainer.auth', {error: 'Your session has expired', redirect: $state.current.name});
+ $state.go('portainer.auth', { error: 'Your session has expired' });
}
});
}
diff --git a/app/constants.js b/app/constants.js
index 464089158..d741e312d 100644
--- a/app/constants.js
+++ b/app/constants.js
@@ -20,4 +20,5 @@ angular.module('portainer')
.constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json')
.constant('PAGINATION_MAX_ITEMS', 10)
.constant('APPLICATION_CACHE_VALIDITY', 3600)
-.constant('CONSOLE_COMMANDS_LABEL_PREFIX', 'io.portainer.commands.');
+.constant('CONSOLE_COMMANDS_LABEL_PREFIX', 'io.portainer.commands.')
+.constant('PREDEFINED_NETWORKS', ['host', 'bridge', 'none']);
diff --git a/app/docker/components/datatables/networks-datatable/networksDatatable.html b/app/docker/components/datatables/networks-datatable/networksDatatable.html
index 29b82b6a7..032de038b 100644
--- a/app/docker/components/datatables/networks-datatable/networksDatatable.html
+++ b/app/docker/components/datatables/networks-datatable/networksDatatable.html
@@ -110,7 +110,7 @@
-
+
{{ item.Name | truncate:40 }}
diff --git a/app/docker/components/datatables/networks-datatable/networksDatatable.js b/app/docker/components/datatables/networks-datatable/networksDatatable.js
index a6383af97..ca9112f98 100644
--- a/app/docker/components/datatables/networks-datatable/networksDatatable.js
+++ b/app/docker/components/datatables/networks-datatable/networksDatatable.js
@@ -1,6 +1,6 @@
angular.module('portainer.docker').component('networksDatatable', {
templateUrl: 'app/docker/components/datatables/networks-datatable/networksDatatable.html',
- controller: 'GenericDatatableController',
+ controller: 'NetworksDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
diff --git a/app/docker/components/datatables/networks-datatable/networksDatatableController.js b/app/docker/components/datatables/networks-datatable/networksDatatableController.js
new file mode 100644
index 000000000..8c9afd635
--- /dev/null
+++ b/app/docker/components/datatables/networks-datatable/networksDatatableController.js
@@ -0,0 +1,20 @@
+angular.module('portainer.docker')
+ .controller('NetworksDatatableController', ['$scope', '$controller', 'PREDEFINED_NETWORKS',
+ function ($scope, $controller, PREDEFINED_NETWORKS) {
+ angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
+
+ this.disableRemove = function(item) {
+ return PREDEFINED_NETWORKS.includes(item.Name);
+ };
+
+ this.selectAll = function() {
+ for (var i = 0; i < this.state.filteredDataSet.length; i++) {
+ var item = this.state.filteredDataSet[i];
+ if (!this.disableRemove(item) && item.Checked !== this.state.selectAll) {
+ item.Checked = this.state.selectAll;
+ this.selectItem(item);
+ }
+ }
+ };
+ }
+]);
\ No newline at end of file
diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js
index 6dd6acd9f..dfcfa928f 100644
--- a/app/docker/views/containers/create/createContainerController.js
+++ b/app/docker/views/containers/create/createContainerController.js
@@ -50,6 +50,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
PortBindings: [],
PublishAllPorts: false,
Binds: [],
+ AutoRemove: false,
NetworkMode: 'bridge',
Privileged: false,
Runtime: '',
diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html
index da8521a1c..83091a978 100644
--- a/app/docker/views/containers/create/createcontainer.html
+++ b/app/docker/views/containers/create/createcontainer.html
@@ -124,6 +124,19 @@
Actions
+
+
+
|
diff --git a/app/docker/views/networks/edit/networkController.js b/app/docker/views/networks/edit/networkController.js
index 9ca04092e..c26800ec1 100644
--- a/app/docker/views/networks/edit/networkController.js
+++ b/app/docker/views/networks/edit/networkController.js
@@ -1,6 +1,6 @@
angular.module('portainer.docker')
-.controller('NetworkController', ['$scope', '$state', '$transition$', '$filter', 'NetworkService', 'Container', 'Notifications', 'HttpRequestHelper',
-function ($scope, $state, $transition$, $filter, NetworkService, Container, Notifications, HttpRequestHelper) {
+.controller('NetworkController', ['$scope', '$state', '$transition$', '$filter', 'NetworkService', 'Container', 'Notifications', 'HttpRequestHelper', 'PREDEFINED_NETWORKS',
+function ($scope, $state, $transition$, $filter, NetworkService, Container, Notifications, HttpRequestHelper, PREDEFINED_NETWORKS) {
$scope.removeNetwork = function removeNetwork() {
NetworkService.remove($transition$.params().id, $transition$.params().id)
@@ -25,6 +25,10 @@ function ($scope, $state, $transition$, $filter, NetworkService, Container, Noti
});
};
+ $scope.allowRemove = function allowRemove(item) {
+ return !PREDEFINED_NETWORKS.includes(item.Name);
+ };
+
function filterContainersInNetwork(network, containers) {
var containersInNetwork = [];
containers.forEach(function(container) {
diff --git a/app/extensions/registry-management/views/repositories/registryRepositories.html b/app/extensions/registry-management/views/repositories/registryRepositories.html
index 6ca3664c7..5e3210ca7 100644
--- a/app/extensions/registry-management/views/repositories/registryRepositories.html
+++ b/app/extensions/registry-management/views/repositories/registryRepositories.html
@@ -5,7 +5,7 @@
- Registries > {{ registry.Name }} > Repositories
+ Registries > {{ registry.Name }}{{ registry.Name}} > Repositories
diff --git a/app/extensions/registry-management/views/repositories/registryRepositoriesController.js b/app/extensions/registry-management/views/repositories/registryRepositoriesController.js
index ccf4fb3de..5d463e722 100644
--- a/app/extensions/registry-management/views/repositories/registryRepositoriesController.js
+++ b/app/extensions/registry-management/views/repositories/registryRepositoriesController.js
@@ -1,6 +1,6 @@
angular.module('portainer.extensions.registrymanagement')
-.controller('RegistryRepositoriesController', ['$transition$', '$scope', 'RegistryService', 'RegistryV2Service', 'Notifications',
-function ($transition$, $scope, RegistryService, RegistryV2Service, Notifications) {
+.controller('RegistryRepositoriesController', ['$transition$', '$scope', 'RegistryService', 'RegistryV2Service', 'Notifications', 'Authentication',
+function ($transition$, $scope, RegistryService, RegistryV2Service, Notifications, Authentication) {
$scope.state = {
displayInvalidConfigurationMessage: false
@@ -9,6 +9,13 @@ function ($transition$, $scope, RegistryService, RegistryV2Service, Notification
function initView() {
var registryId = $transition$.params().id;
+ var authenticationEnabled = $scope.applicationState.application.authentication;
+ if (authenticationEnabled) {
+ var userDetails = Authentication.getUserDetails();
+ var isAdmin = userDetails.role === 1;
+ $scope.isAdmin = isAdmin;
+ }
+
RegistryService.registry(registryId)
.then(function success(data) {
$scope.registry = data;
diff --git a/app/portainer/__module.js b/app/portainer/__module.js
index d3cb2b46c..01adba1ab 100644
--- a/app/portainer/__module.js
+++ b/app/portainer/__module.js
@@ -48,7 +48,7 @@ angular.module('portainer.app', [])
var authentication = {
name: 'portainer.auth',
- url: '/auth?redirect',
+ url: '/auth',
params: {
logout: false,
error: ''
@@ -329,7 +329,7 @@ angular.module('portainer.app', [])
}
},
resolve: {
- endpointID: ['EndpointProvider', '$state',
+ endpointID: ['EndpointProvider', '$state',
function (EndpointProvider, $state) {
var id = EndpointProvider.endpointID();
if (!id) {
diff --git a/app/portainer/components/datatables/registries-datatable/registriesDatatable.html b/app/portainer/components/datatables/registries-datatable/registriesDatatable.html
index 9bc262c40..58377e2c3 100644
--- a/app/portainer/components/datatables/registries-datatable/registriesDatatable.html
+++ b/app/portainer/components/datatables/registries-datatable/registriesDatatable.html
@@ -6,7 +6,7 @@
{{ $ctrl.titleText }}
-
+