mirror of https://github.com/portainer/portainer
feat(registry-manager): allow regular users to use the registry browse feature (#2664)
* feat(registries): registries accessibility to all authorized people and not only admins * feat(registry): dockerhub settings for admin only * feat(registry): remove registry config access for non admin users * feat(api): use AuthenticatedAccess policy instead of RestrictedAccess for extensionList operation * refactor(api): minor update to security package * refactor(api): revert unexporting function changes * refactor(api): apply gofmtpull/2745/head
parent
99e50370bd
commit
7aa6a30614
|
@ -23,7 +23,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
h.Handle("/extensions",
|
h.Handle("/extensions",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
|
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
|
||||||
h.Handle("/extensions",
|
h.Handle("/extensions",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost)
|
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost)
|
||||||
h.Handle("/extensions/{id}",
|
h.Handle("/extensions/{id}",
|
||||||
|
|
|
@ -18,6 +18,7 @@ func hideFields(registry *portainer.Registry) {
|
||||||
// Handler is the HTTP handler used to handle registry operations.
|
// Handler is the HTTP handler used to handle registry operations.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
|
requestBouncer *security.RequestBouncer
|
||||||
RegistryService portainer.RegistryService
|
RegistryService portainer.RegistryService
|
||||||
ExtensionService portainer.ExtensionService
|
ExtensionService portainer.ExtensionService
|
||||||
FileService portainer.FileService
|
FileService portainer.FileService
|
||||||
|
@ -27,7 +28,8 @@ type Handler struct {
|
||||||
// NewHandler creates a handler to manage registry operations.
|
// NewHandler creates a handler to manage registry operations.
|
||||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
|
requestBouncer: bouncer,
|
||||||
}
|
}
|
||||||
|
|
||||||
h.Handle("/registries",
|
h.Handle("/registries",
|
||||||
|
@ -35,7 +37,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
h.Handle("/registries",
|
h.Handle("/registries",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryList))).Methods(http.MethodGet)
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryList))).Methods(http.MethodGet)
|
||||||
h.Handle("/registries/{id}",
|
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}",
|
h.Handle("/registries/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut)
|
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut)
|
||||||
h.Handle("/registries/{id}/access",
|
h.Handle("/registries/{id}/access",
|
||||||
|
@ -45,7 +47,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
h.Handle("/registries/{id}",
|
h.Handle("/registries/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
|
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
|
||||||
h.PathPrefix("/registries/{id}/v2").Handler(
|
h.PathPrefix("/registries/{id}/v2").Handler(
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
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)
|
extension, err := handler.ExtensionService.Extension(portainer.RegistryManagementExtension)
|
||||||
if err == portainer.ErrObjectNotFound {
|
if err == portainer.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err}
|
return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err}
|
||||||
|
|
|
@ -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}
|
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)
|
hideFields(registry)
|
||||||
return response.JSON(w, registry)
|
return response.JSON(w, registry)
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,10 +153,10 @@ func authorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *porta
|
||||||
return true
|
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
|
// It will check if the user is part of the authorized users or part of a team that is
|
||||||
// listed in the authorized teams.
|
// 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)
|
return authorizedAccess(userID, memberships, endpointGroup.AuthorizedUsers, endpointGroup.AuthorizedTeams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -111,6 +111,31 @@ func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portain
|
||||||
return nil
|
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.
|
// mwSecureHeaders provides secure headers middleware for handlers.
|
||||||
func mwSecureHeaders(next http.Handler) http.Handler {
|
func mwSecureHeaders(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -124,7 +124,7 @@ func FilterEndpointGroups(endpointGroups []portainer.EndpointGroup, context *Res
|
||||||
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
|
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
|
||||||
|
|
||||||
for _, group := range endpointGroups {
|
for _, group := range endpointGroups {
|
||||||
if AuthorizedEndpointGroupAccess(&group, context.UserID, context.UserMemberships) {
|
if authorizedEndpointGroupAccess(&group, context.UserID, context.UserMemberships) {
|
||||||
filteredEndpointGroups = append(filteredEndpointGroups, group)
|
filteredEndpointGroups = append(filteredEndpointGroups, group)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
</a>
|
</a>
|
||||||
</rd-header-title>
|
</rd-header-title>
|
||||||
<rd-header-content>
|
<rd-header-content>
|
||||||
<a ui-sref="portainer.registries">Registries</a> > <a ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a> > Repositories
|
<a ui-sref="portainer.registries">Registries</a> > <a ng-if="isAdmin" ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a><span ng-if="!isAdmin">{{ registry.Name}}</span> > Repositories
|
||||||
</rd-header-content>
|
</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.extensions.registrymanagement')
|
angular.module('portainer.extensions.registrymanagement')
|
||||||
.controller('RegistryRepositoriesController', ['$transition$', '$scope', 'RegistryService', 'RegistryV2Service', 'Notifications',
|
.controller('RegistryRepositoriesController', ['$transition$', '$scope', 'RegistryService', 'RegistryV2Service', 'Notifications', 'Authentication',
|
||||||
function ($transition$, $scope, RegistryService, RegistryV2Service, Notifications) {
|
function ($transition$, $scope, RegistryService, RegistryV2Service, Notifications, Authentication) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
displayInvalidConfigurationMessage: false
|
displayInvalidConfigurationMessage: false
|
||||||
|
@ -9,6 +9,13 @@ function ($transition$, $scope, RegistryService, RegistryV2Service, Notification
|
||||||
function initView() {
|
function initView() {
|
||||||
var registryId = $transition$.params().id;
|
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)
|
RegistryService.registry(registryId)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.registry = data;
|
$scope.registry = data;
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar">
|
<div class="actionBar" ng-if="$ctrl.accessManagement">
|
||||||
<button type="button" class="btn btn-sm btn-danger"
|
<button type="button" class="btn btn-sm btn-danger"
|
||||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" ng-if="$ctrl.accessManagement">
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
<label for="select_all"></label>
|
<label for="select_all"></label>
|
||||||
</span>
|
</span>
|
||||||
|
@ -47,11 +47,12 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" ng-if="$ctrl.accessManagement">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a ui-sref="portainer.registries.registry({id: item.Id})">{{ item.Name }}</a>
|
<a ui-sref="portainer.registries.registry({id: item.Id})" ng-if="$ctrl.accessManagement">{{ item.Name }}</a>
|
||||||
|
<span ng-if="!$ctrl.accessManagement">{{ item.Name }}</span>
|
||||||
<span ng-if="item.Authentication" style="margin-left: 5px;" class="label label-info image-tag">authentication-enabled</span>
|
<span ng-if="item.Authentication" style="margin-left: 5px;" class="label label-info image-tag">authentication-enabled</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<rd-header-content>Registry management</rd-header-content>
|
<rd-header-content>Registry management</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
<div class="row" ng-if="dockerhub">
|
<div class="row" ng-if="dockerhub && isAdmin">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-database" title-text="DockerHub">
|
<rd-widget-header icon="fa-database" title-text="DockerHub">
|
||||||
|
@ -74,7 +74,7 @@
|
||||||
title-text="Registries" title-icon="fa-database"
|
title-text="Registries" title-icon="fa-database"
|
||||||
dataset="registries" table-key="registries"
|
dataset="registries" table-key="registries"
|
||||||
order-by="Name"
|
order-by="Name"
|
||||||
access-management="applicationState.application.authentication"
|
access-management="applicationState.application.authentication && isAdmin"
|
||||||
remove-action="removeAction"
|
remove-action="removeAction"
|
||||||
registry-management="registryManagementAvailable"
|
registry-management="registryManagementAvailable"
|
||||||
></registries-datatable>
|
></registries-datatable>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('RegistriesController', ['$q', '$scope', '$state', 'RegistryService', 'DockerHubService', 'ModalService', 'Notifications', 'ExtensionService',
|
.controller('RegistriesController', ['$q', '$scope', '$state', 'RegistryService', 'DockerHubService', 'ModalService', 'Notifications', 'ExtensionService', 'Authentication',
|
||||||
function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, ExtensionService) {
|
function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, ExtensionService, Authentication) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
actionInProgress: false
|
actionInProgress: false
|
||||||
|
@ -67,6 +67,12 @@ function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, N
|
||||||
$scope.registries = data.registries;
|
$scope.registries = data.registries;
|
||||||
$scope.dockerhub = data.dockerhub;
|
$scope.dockerhub = data.dockerhub;
|
||||||
$scope.registryManagementAvailable = data.registryManagement;
|
$scope.registryManagementAvailable = data.registryManagement;
|
||||||
|
var authenticationEnabled = $scope.applicationState.application.authentication;
|
||||||
|
if (authenticationEnabled) {
|
||||||
|
var userDetails = Authentication.getUserDetails();
|
||||||
|
var isAdmin = userDetails.role === 1;
|
||||||
|
$scope.isAdmin = isAdmin;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
$scope.registries = [];
|
$scope.registries = [];
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
<a ui-sref="portainer.tags" ui-sref-active="active">Tags</a>
|
<a ui-sref="portainer.tags" ui-sref-active="active">Tags</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
<li class="sidebar-list" ng-if="applicationState.application.authentication">
|
||||||
<a ui-sref="portainer.registries" ui-sref-active="active">Registries <span class="menu-icon fa fa-database fa-fw"></span></a>
|
<a ui-sref="portainer.registries" ui-sref-active="active">Registries <span class="menu-icon fa fa-database fa-fw"></span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
||||||
|
|
Loading…
Reference in New Issue