feat(registries): Registry browser for non-admins [EE-2459] (#6549)

* feat(registries): allow non-admin users to see environment registries

* remove unused function

* fix error message

* fix test

* fix imports order

* feat(registry): check access first, add parameters name

* use registryID

* fix(sidebar): allow standard users to see endpoint registries view

Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
pull/6735/head
Marcelo Rydel 2022-04-07 10:22:31 -03:00 committed by GitHub
parent 77e48bfb74
commit f9f937f844
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 226 additions and 202 deletions

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

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

View File

@ -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)
}

View File

@ -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.

View File

@ -102,27 +102,20 @@
is-sidebar-open="$ctrl.isSidebarOpen"
children-paths="['docker.registries', 'docker.registries.access', 'docker.featuresConfiguration']"
>
<div ng-if="$ctrl.adminAccess">
<sidebar-menu-item
authorization="PortainerEndpointUpdateSettings"
path="docker.featuresConfiguration"
path-params="{ endpointId: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="dockerSidebar-setup"
>
Setup
</sidebar-menu-item>
<sidebar-menu-item
ng-if="$ctrl.adminAccess"
authorization="PortainerEndpointUpdateSettings"
path="docker.featuresConfiguration"
path-params="{ endpointId: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="dockerSidebar-setup"
>
Setup
</sidebar-menu-item>
<sidebar-menu-item
authorization="PortainerRegistryList"
path="docker.registries"
path-params="{ endpointId: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="dockerSidebar-registries"
>
Registries
</sidebar-menu-item>
</div>
<sidebar-menu-item path="docker.registries" path-params="{ endpointId: $ctrl.endpointId }" class-name="sidebar-sublist" data-cy="dockerSidebar-registries">
Registries
</sidebar-menu-item>
</sidebar-menu>
<sidebar-menu
@ -134,25 +127,18 @@
is-sidebar-open="$ctrl.isSidebarOpen"
children-paths="['docker.registries', 'docker.registries.access', 'docker.featuresConfiguration']"
>
<div ng-if="$ctrl.adminAccess">
<sidebar-menu-item
authorization="PortainerEndpointUpdateSettings"
path="docker.featuresConfiguration"
path-params="{ endpointId: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="swarmSidebar-setup"
>
Setup
</sidebar-menu-item>
<sidebar-menu-item
ng-if="$ctrl.adminAccess"
authorization="PortainerEndpointUpdateSettings"
path="docker.featuresConfiguration"
path-params="{ endpointId: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="swarmSidebar-setup"
>
Setup
</sidebar-menu-item>
<sidebar-menu-item
authorization="PortainerRegistryList"
path="docker.registries"
path-params="{ endpointId: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="swarmSidebar-registries"
>
Registries
</sidebar-menu-item>
</div>
<sidebar-menu-item path="docker.registries" path-params="{ endpointId: $ctrl.endpointId }" class-name="sidebar-sublist" data-cy="swarmSidebar-registries">
Registries
</sidebar-menu-item>
</sidebar-menu>

View File

@ -2,12 +2,13 @@ import { TeamAccessViewModel, UserAccessViewModel } from 'Portainer/models/acces
class DockerRegistryAccessController {
/* @ngInject */
constructor($async, $state, Notifications, EndpointService, GroupService) {
constructor($async, $state, Notifications, EndpointService, GroupService, RegistryService) {
this.$async = $async;
this.$state = $state;
this.Notifications = Notifications;
this.EndpointService = EndpointService;
this.GroupService = GroupService;
this.RegistryService = RegistryService;
this.updateAccess = this.updateAccess.bind(this);
this.filterUsers = this.filterUsers.bind(this);
@ -35,10 +36,10 @@ class DockerRegistryAccessController {
const endpointGroupTeams = this.endpointGroup.TeamAccessPolicies;
return users.filter((userOrTeam) => {
const userRole = userOrTeam instanceof UserAccessViewModel && (endpointUsers[userOrTeam.Id] || endpointGroupUsers[userOrTeam.Id]);
const teamRole = userOrTeam instanceof TeamAccessViewModel && (endpointTeams[userOrTeam.Id] || endpointGroupTeams[userOrTeam.Id]);
const userAccess = userOrTeam instanceof UserAccessViewModel && (endpointUsers[userOrTeam.Id] || endpointGroupUsers[userOrTeam.Id]);
const teamAccess = userOrTeam instanceof TeamAccessViewModel && (endpointTeams[userOrTeam.Id] || endpointGroupTeams[userOrTeam.Id]);
return userRole || teamRole;
return userAccess || teamAccess;
});
}
@ -51,7 +52,7 @@ class DockerRegistryAccessController {
endpointId: this.$state.params.endpointId,
registryId: this.$state.params.id,
};
this.registry = await this.EndpointService.registry(this.state.endpointId, this.state.registryId);
this.registry = await this.RegistryService.registry(this.state.registryId, this.state.endpointId);
this.registryEndpointAccesses = this.registry.RegistryAccesses[this.state.endpointId] || {};
this.endpointGroup = await this.GroupService.group(this.endpoint.GroupId);
} catch (err) {

View File

@ -71,25 +71,24 @@
children-paths="['kubernetes.cluster', 'portainer.k8sendpoint.kubernetesConfig', 'kubernetes.registries', 'kubernetes.registries.access']"
data-cy="k8sSidebar-cluster"
>
<div ng-if="$ctrl.adminAccess">
<sidebar-menu-item
authorization="K8sClusterSetupRW"
path="portainer.k8sendpoint.kubernetesConfig"
path-params="{ id: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="k8sSidebar-setup"
>
Setup
</sidebar-menu-item>
<sidebar-menu-item
ng-if="$ctrl.adminAccess"
authorization="K8sClusterSetupRW"
path="portainer.k8sendpoint.kubernetesConfig"
path-params="{ id: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="k8sSidebar-setup"
>
Setup
</sidebar-menu-item>
<sidebar-menu-item
authorization="PortainerRegistryList"
path="kubernetes.registries"
path-params="{ endpointId: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="k8sSidebar-registries"
>
Registries
</sidebar-menu-item>
</div>
<sidebar-menu-item
authorization="PortainerRegistryList"
path="kubernetes.registries"
path-params="{ endpointId: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="k8sSidebar-registries"
>
Registries
</sidebar-menu-item>
</sidebar-menu>

View File

@ -2,12 +2,13 @@ import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
export default class KubernetesRegistryAccessController {
/* @ngInject */
constructor($async, $state, ModalService, EndpointService, Notifications, KubernetesResourcePoolService) {
constructor($async, $state, ModalService, EndpointService, Notifications, RegistryService, KubernetesResourcePoolService) {
this.$async = $async;
this.$state = $state;
this.ModalService = ModalService;
this.Notifications = Notifications;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.RegistryService = RegistryService;
this.EndpointService = EndpointService;
this.state = {
@ -57,7 +58,7 @@ export default class KubernetesRegistryAccessController {
this.state = {
registryId: this.$state.params.id,
};
this.registry = await this.EndpointService.registry(this.endpoint.Id, this.state.registryId);
this.registry = await this.RegistryService.registry(this.state.registryId, this.endpoint.Id);
if (this.registry.RegistryAccesses && this.registry.RegistryAccesses[this.endpoint.Id]) {
this.savedResourcePools = this.registry.RegistryAccesses[this.endpoint.Id].Namespaces.map((value) => ({ value }));
}

View File

@ -33,11 +33,6 @@ angular.module('portainer.app').factory('Endpoints', [
params: { id: '@id', namespace: '@namespace' },
isArray: true,
},
registry: {
url: `${API_ENDPOINT_ENDPOINTS}/:id/registries/:registryId`,
method: 'GET',
params: { id: '@id', namespace: '@namespace', registryId: '@registryId' },
},
updateRegistryAccess: {
method: 'PUT',
url: `${API_ENDPOINT_ENDPOINTS}/:id/registries/:registryId`,

View File

@ -9,7 +9,6 @@ angular.module('portainer.app').factory('EndpointService', [
var service = {
updateSecuritySettings,
registries,
registry,
updateRegistryAccess,
};
@ -184,9 +183,5 @@ angular.module('portainer.app').factory('EndpointService', [
function updateSecuritySettings(id, securitySettings) {
return Endpoints.updateSecuritySettings({ id }, securitySettings).$promise;
}
function registry(endpointId, registryId) {
return Endpoints.registry({ registryId, id: endpointId }).$promise;
}
},
]);

View File

@ -50,10 +50,11 @@ angular.module('portainer.app').factory('RegistryGitlabService', [
return repositories;
}
async function repositoriesAsync(registry) {
async function repositoriesAsync(registry, endpointId) {
try {
const params = {
id: registry.Id,
endpointId: endpointId,
projectId: registry.Gitlab.ProjectId,
page: 1,
};
@ -76,8 +77,8 @@ angular.module('portainer.app').factory('RegistryGitlabService', [
return $async(projectsAsync, url, token);
}
function repositories(registry) {
return $async(repositoriesAsync, registry);
function repositories(registry, endpointId) {
return $async(repositoriesAsync, registry, endpointId);
}
service.projects = projects;

View File

@ -4,7 +4,7 @@
<i class="fa fa-sync" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Manage registry access inside this environment</rd-header-content>
<rd-header-content>Registry management</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
@ -16,6 +16,7 @@
order-by="Name"
endpoint-type="$ctrl.endpointType"
can-manage-access="$ctrl.canManageAccess"
can-browse="$ctrl.canBrowse"
></registries-datatable>
</div>
</div>

View File

@ -1,24 +1,30 @@
import _ from 'lodash-es';
import { RegistryTypes } from 'Portainer/models/registryTypes';
class EndpointRegistriesController {
/* @ngInject */
constructor($async, Notifications, EndpointService) {
constructor($async, Notifications, EndpointService, Authentication) {
this.$async = $async;
this.Notifications = Notifications;
this.EndpointService = EndpointService;
this.Authentication = Authentication;
this.canManageAccess = this.canManageAccess.bind(this);
this.canBrowse = this.canBrowse.bind(this);
}
canManageAccess(item) {
return item.Type !== RegistryTypes.ANONYMOUS;
return item.Type !== RegistryTypes.ANONYMOUS && this.Authentication.isAdmin();
}
canBrowse(item) {
return !_.includes([RegistryTypes.ANONYMOUS, RegistryTypes.DOCKERHUB, RegistryTypes.QUAY], item.Type);
}
getRegistries() {
return this.$async(async () => {
try {
const registries = await this.EndpointService.registries(this.endpointId);
this.registries = registries;
this.registries = await this.EndpointService.registries(this.endpointId);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve registries');
}