diff --git a/api/http/handler/endpoints/endpoint_registries_inspect.go b/api/http/handler/endpoints/endpoint_registries_inspect.go deleted file mode 100644 index 50a2a1641..000000000 --- a/api/http/handler/endpoints/endpoint_registries_inspect.go +++ /dev/null @@ -1,63 +0,0 @@ -package endpoints - -import ( - "net/http" - - httperror "github.com/portainer/libhttp/error" - "github.com/portainer/libhttp/request" - "github.com/portainer/libhttp/response" - portainer "github.com/portainer/portainer/api" - httperrors "github.com/portainer/portainer/api/http/errors" - "github.com/portainer/portainer/api/http/security" -) - -// @id endpointRegistryInspect -// @summary get registry for environment -// @description **Access policy**: authenticated -// @tags endpoints -// @security ApiKeyAuth -// @security jwt -// @produce json -// @param id path int true "identifier" -// @param registryId path int true "Registry identifier" -// @success 200 {object} portainer.Registry "Success" -// @failure 400 "Invalid request" -// @failure 403 "Permission denied" -// @failure 404 "Registry not found" -// @failure 500 "Server error" -// @router /endpoints/{id}/registries/{registryId} [get] -func (handler *Handler) endpointRegistryInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") - if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid environment identifier route variable", Err: err} - } - - registryID, err := request.RetrieveNumericRouteVariableValue(r, "registryId") - if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid registry identifier route variable", Err: err} - } - - registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID)) - if handler.DataStore.IsErrObjectNotFound(err) { - return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a registry with the specified identifier inside the database", Err: err} - } else if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a registry with the specified identifier inside the database", Err: err} - } - - securityContext, err := security.RetrieveRestrictedRequestContext(r) - if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err} - } - - user, err := handler.DataStore.User().User(securityContext.UserID) - if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user from the database", Err: err} - } - - if !security.AuthorizedRegistryAccess(registry, user, securityContext.UserMemberships, portainer.EndpointID(endpointID)) { - return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied} - } - - hideRegistryFields(registry, !securityContext.IsAdmin) - return response.JSON(w, registry) -} diff --git a/api/http/handler/endpoints/endpoint_registries_list.go b/api/http/handler/endpoints/endpoint_registries_list.go index 049e4aa94..81b28b099 100644 --- a/api/http/handler/endpoints/endpoint_registries_list.go +++ b/api/http/handler/endpoints/endpoint_registries_list.go @@ -55,29 +55,9 @@ func (handler *Handler) endpointRegistriesList(w http.ResponseWriter, r *http.Re return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err} } - if endpointutils.IsKubernetesEndpoint(endpoint) { - namespace, _ := request.RetrieveQueryParameter(r, "namespace", true) - - if namespace == "" && !isAdmin { - return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Missing namespace query parameter", Err: errors.New("missing namespace query parameter")} - } - - if namespace != "" { - - authorized, err := handler.isNamespaceAuthorized(endpoint, namespace, user.ID, securityContext.UserMemberships, isAdmin) - if err != nil { - return &httperror.HandlerError{http.StatusNotFound, "Unable to check for namespace authorization", err} - } - - if !authorized { - return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "User is not authorized to use namespace", Err: errors.New("user is not authorized to use namespace")} - } - - registries = filterRegistriesByNamespace(registries, endpoint.ID, namespace) - } - - } else if !isAdmin { - registries = security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID) + registries, handleError := handler.filterRegistriesByAccess(r, registries, endpoint, user, securityContext.UserMemberships) + if handleError != nil { + return handleError } for idx := range registries { @@ -87,6 +67,40 @@ func (handler *Handler) endpointRegistriesList(w http.ResponseWriter, r *http.Re return response.JSON(w, registries) } +func (handler *Handler) filterRegistriesByAccess(r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User, memberships []portainer.TeamMembership) ([]portainer.Registry, *httperror.HandlerError) { + if !endpointutils.IsKubernetesEndpoint(endpoint) { + return security.FilterRegistries(registries, user, memberships, endpoint.ID), nil + } + + return handler.filterKubernetesEndpointRegistries(r, registries, endpoint, user, memberships) +} + +func (handler *Handler) filterKubernetesEndpointRegistries(r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User, memberships []portainer.TeamMembership) ([]portainer.Registry, *httperror.HandlerError) { + namespaceParam, _ := request.RetrieveQueryParameter(r, "namespace", true) + isAdmin, err := security.IsAdmin(r) + if err != nil { + return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check user role", Err: err} + } + + if namespaceParam != "" { + authorized, err := handler.isNamespaceAuthorized(endpoint, namespaceParam, user.ID, memberships, isAdmin) + if err != nil { + return nil, &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to check for namespace authorization", Err: err} + } + if !authorized { + return nil, &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "User is not authorized to use namespace", Err: errors.New("user is not authorized to use namespace")} + } + + return filterRegistriesByNamespaces(registries, endpoint.ID, []string{namespaceParam}), nil + } + + if isAdmin { + return registries, nil + } + + return handler.filterKubernetesRegistriesByUserRole(r, registries, endpoint, user) +} + func (handler *Handler) isNamespaceAuthorized(endpoint *portainer.Endpoint, namespace string, userId portainer.UserID, memberships []portainer.TeamMembership, isAdmin bool) (bool, error) { if isAdmin || namespace == "" { return true, nil @@ -114,24 +128,78 @@ func (handler *Handler) isNamespaceAuthorized(endpoint *portainer.Endpoint, name return security.AuthorizedAccess(userId, memberships, namespacePolicy.UserAccessPolicies, namespacePolicy.TeamAccessPolicies), nil } -func filterRegistriesByNamespace(registries []portainer.Registry, endpointId portainer.EndpointID, namespace string) []portainer.Registry { - if namespace == "" { - return registries - } - +func filterRegistriesByNamespaces(registries []portainer.Registry, endpointId portainer.EndpointID, namespaces []string) []portainer.Registry { filteredRegistries := []portainer.Registry{} for _, registry := range registries { - for _, authorizedNamespace := range registry.RegistryAccesses[endpointId].Namespaces { - if authorizedNamespace == namespace { - filteredRegistries = append(filteredRegistries, registry) - } + if registryAccessPoliciesContainsNamespace(registry.RegistryAccesses[endpointId], namespaces) { + filteredRegistries = append(filteredRegistries, registry) } } return filteredRegistries } +func registryAccessPoliciesContainsNamespace(registryAccess portainer.RegistryAccessPolicies, namespaces []string) bool { + for _, authorizedNamespace := range registryAccess.Namespaces { + for _, namespace := range namespaces { + if namespace == authorizedNamespace { + return true + } + } + } + return false +} + +func (handler *Handler) filterKubernetesRegistriesByUserRole(r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User) ([]portainer.Registry, *httperror.HandlerError) { + err := handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) + if err == security.ErrAuthorizationRequired { + return nil, &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "User is not authorized", Err: errors.New("missing namespace query parameter")} + } + if err != nil { + return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err} + } + + userNamespaces, err := handler.userNamespaces(endpoint, user) + if err != nil { + return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "unable to retrieve user namespaces", Err: err} + } + + return filterRegistriesByNamespaces(registries, endpoint.ID, userNamespaces), nil +} + +func (handler *Handler) userNamespaces(endpoint *portainer.Endpoint, user *portainer.User) ([]string, error) { + kcl, err := handler.K8sClientFactory.GetKubeClient(endpoint) + if err != nil { + return nil, err + } + + namespaceAuthorizations, err := kcl.GetNamespaceAccessPolicies() + if err != nil { + return nil, err + } + + userMemberships, err := handler.DataStore.TeamMembership().TeamMembershipsByUserID(user.ID) + if err != nil { + return nil, err + } + + var userNamespaces []string + for namespace, namespaceAuthorization := range namespaceAuthorizations { + if _, ok := namespaceAuthorization.UserAccessPolicies[user.ID]; ok { + userNamespaces = append(userNamespaces, namespace) + continue + } + for _, userTeam := range userMemberships { + if _, ok := namespaceAuthorization.TeamAccessPolicies[userTeam.TeamID]; ok { + userNamespaces = append(userNamespaces, namespace) + continue + } + } + } + return userNamespaces, nil +} + func hideRegistryFields(registry *portainer.Registry, hideAccesses bool) { registry.Password = "" registry.ManagementConfiguration = nil diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index 99de0e2de..e4de0bad3 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -66,8 +66,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost) h.Handle("/endpoints/{id}/registries", bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointRegistriesList))).Methods(http.MethodGet) - h.Handle("/endpoints/{id}/registries/{registryId}", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointRegistryInspect))).Methods(http.MethodGet) h.Handle("/endpoints/{id}/registries/{registryId}", bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointRegistryAccess))).Methods(http.MethodPut) return h diff --git a/api/http/handler/registries/handler.go b/api/http/handler/registries/handler.go index cfa414049..10760c34a 100644 --- a/api/http/handler/registries/handler.go +++ b/api/http/handler/registries/handler.go @@ -5,6 +5,7 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/http/proxy" @@ -45,27 +46,27 @@ func newHandler(bouncer *security.RequestBouncer) *Handler { } } -func (h *Handler) initRouter(bouncer accessGuard) { - h.Handle("/registries", - bouncer.AdminAccess(httperror.LoggerHandler(h.registryCreate))).Methods(http.MethodPost) - h.Handle("/registries", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryList))).Methods(http.MethodGet) - h.Handle("/registries/{id}", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet) - h.Handle("/registries/{id}", - bouncer.AdminAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut) - h.Handle("/registries/{id}/configure", - bouncer.AdminAccess(httperror.LoggerHandler(h.registryConfigure))).Methods(http.MethodPost) - h.Handle("/registries/{id}", - bouncer.AdminAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete) - h.PathPrefix("/registries/proxies/gitlab").Handler( - bouncer.AdminAccess(httperror.LoggerHandler(h.proxyRequestsToGitlabAPIWithoutRegistry))) +func (handler *Handler) initRouter(bouncer accessGuard) { + adminRouter := handler.NewRoute().Subrouter() + adminRouter.Use(bouncer.AdminAccess) + + authenticatedRouter := handler.NewRoute().Subrouter() + authenticatedRouter.Use(bouncer.AuthenticatedAccess) + + adminRouter.Handle("/registries", httperror.LoggerHandler(handler.registryList)).Methods(http.MethodGet) + adminRouter.Handle("/registries", httperror.LoggerHandler(handler.registryCreate)).Methods(http.MethodPost) + adminRouter.Handle("/registries/{id}", httperror.LoggerHandler(handler.registryUpdate)).Methods(http.MethodPut) + adminRouter.Handle("/registries/{id}/configure", httperror.LoggerHandler(handler.registryConfigure)).Methods(http.MethodPost) + adminRouter.Handle("/registries/{id}", httperror.LoggerHandler(handler.registryDelete)).Methods(http.MethodDelete) + + authenticatedRouter.Handle("/registries/{id}", httperror.LoggerHandler(handler.registryInspect)).Methods(http.MethodGet) + authenticatedRouter.PathPrefix("/registries/proxies/gitlab").Handler(httperror.LoggerHandler(handler.proxyRequestsToGitlabAPIWithoutRegistry)) } type accessGuard interface { AdminAccess(h http.Handler) http.Handler - RestrictedAccess(h http.Handler) http.Handler AuthenticatedAccess(h http.Handler) http.Handler + AuthorizedEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error } func (handler *Handler) registriesHaveSameURLAndCredentials(r1, r2 *portainer.Registry) bool { @@ -78,3 +79,30 @@ func (handler *Handler) registriesHaveSameURLAndCredentials(r1, r2 *portainer.Re return hasSameUrl && hasSameCredentials && r1.Gitlab.ProjectPath == r2.Gitlab.ProjectPath } + +func (handler *Handler) userHasRegistryAccess(r *http.Request) (hasAccess bool, isAdmin bool, err error) { + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + return false, false, err + } + + if securityContext.IsAdmin { + return true, true, nil + } + + endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false) + if err != nil { + return false, false, err + } + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) + if err != nil { + return false, false, err + } + + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) + if err != nil { + return false, false, err + } + + return true, false, nil +} diff --git a/api/http/handler/registries/registry_inspect.go b/api/http/handler/registries/registry_inspect.go index 99ba68ec7..600873f91 100644 --- a/api/http/handler/registries/registry_inspect.go +++ b/api/http/handler/registries/registry_inspect.go @@ -8,6 +8,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" + httperrors "github.com/portainer/portainer/api/http/errors" ) // @id RegistryInspect @@ -26,6 +27,13 @@ import ( // @failure 500 "Server error" // @router /registries/{id} [get] func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + hasAccess, isAdmin, err := handler.userHasRegistryAccess(r) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} + } + if !hasAccess { + return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied} + } registryID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { @@ -39,6 +47,6 @@ 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} } - hideFields(registry, false) + hideFields(registry, !isAdmin) return response.JSON(w, registry) } diff --git a/api/http/handler/registries/registry_update_test.go b/api/http/handler/registries/registry_update_test.go index 73bf65d64..2516b097b 100644 --- a/api/http/handler/registries/registry_update_test.go +++ b/api/http/handler/registries/registry_update_test.go @@ -26,12 +26,12 @@ func (t TestBouncer) AdminAccess(h http.Handler) http.Handler { return h } -func (t TestBouncer) RestrictedAccess(h http.Handler) http.Handler { +func (t TestBouncer) AuthenticatedAccess(h http.Handler) http.Handler { return h } -func (t TestBouncer) AuthenticatedAccess(h http.Handler) http.Handler { - return h +func (t TestBouncer) AuthorizedEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error { + return nil } // TODO, no i don't know what this is actually intended to test either. diff --git a/app/docker/components/docker-sidebar/docker-sidebar.html b/app/docker/components/docker-sidebar/docker-sidebar.html index 7992dd60c..ad5762461 100644 --- a/app/docker/components/docker-sidebar/docker-sidebar.html +++ b/app/docker/components/docker-sidebar/docker-sidebar.html @@ -102,27 +102,20 @@ is-sidebar-open="$ctrl.isSidebarOpen" children-paths="['docker.registries', 'docker.registries.access', 'docker.featuresConfiguration']" > -