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