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 }}
-
+