mirror of https://github.com/portainer/portainer
feat(teamleader) EE-294 redesign team leader (#6973)
feat(teamleader) EE-294 redesign team leader (#6973)pull/7014/head
parent
bca1c6b9cf
commit
0522032515
|
@ -34,7 +34,7 @@ var endpointGroupNames map[portainer.EndpointGroupID]string
|
|||
// @id EndpointList
|
||||
// @summary List environments(endpoints)
|
||||
// @description List all environments(endpoints) based on the current user authorizations. Will
|
||||
// @description return all environments(endpoints) if using an administrator account otherwise it will
|
||||
// @description return all environments(endpoints) if using an administrator or team leader account otherwise it will
|
||||
// @description only return authorized environments(endpoints).
|
||||
// @description **Access policy**: restricted
|
||||
// @tags endpoints
|
||||
|
|
|
@ -28,6 +28,8 @@ type publicSettingsResponse struct {
|
|||
EnableTelemetry bool `json:"EnableTelemetry" example:"true"`
|
||||
// The expiry of a Kubeconfig
|
||||
KubeconfigExpiry string `example:"24h" default:"0"`
|
||||
// Whether team sync is enabled
|
||||
TeamSync bool `json:"TeamSync" example:"true"`
|
||||
}
|
||||
|
||||
// @id SettingsPublic
|
||||
|
@ -72,5 +74,9 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
|
|||
publicSettings.OAuthLoginURI += "&prompt=login"
|
||||
}
|
||||
}
|
||||
//if LDAP authentication is on, compose the related fields from application settings
|
||||
if publicSettings.AuthenticationMethod == portainer.AuthenticationLDAP {
|
||||
publicSettings.TeamSync = len(appSettings.LDAPSettings.GroupSearchSettings) > 0
|
||||
}
|
||||
return publicSettings
|
||||
}
|
||||
|
|
|
@ -21,14 +21,13 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/team_memberships",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/team_memberships",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipList))).Methods(http.MethodGet)
|
||||
h.Handle("/team_memberships/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/team_memberships/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipDelete))).Methods(http.MethodDelete)
|
||||
|
||||
h.Use(bouncer.TeamLeaderAccess)
|
||||
|
||||
h.Handle("/team_memberships", httperror.LoggerHandler(h.teamMembershipCreate)).Methods(http.MethodPost)
|
||||
h.Handle("/team_memberships", httperror.LoggerHandler(h.teamMembershipList)).Methods(http.MethodGet)
|
||||
h.Handle("/team_memberships/{id}", httperror.LoggerHandler(h.teamMembershipUpdate)).Methods(http.MethodPut)
|
||||
h.Handle("/team_memberships/{id}", httperror.LoggerHandler(h.teamMembershipDelete)).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -5,8 +5,6 @@ import (
|
|||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// @id TeamMembershipList
|
||||
|
@ -23,15 +21,6 @@ import (
|
|||
// @failure 500 "Server error"
|
||||
// @router /team_memberships [get]
|
||||
func (handler *Handler) teamMembershipList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin && !securityContext.IsTeamLeader {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to list team memberships", errors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
memberships, err := handler.DataStore.TeamMembership().TeamMemberships()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve team memberships from the database", err}
|
||||
|
|
|
@ -36,8 +36,8 @@ func (payload *teamMembershipUpdatePayload) Validate(r *http.Request) error {
|
|||
|
||||
// @id TeamMembershipUpdate
|
||||
// @summary Update a team membership
|
||||
// @description Update a team membership. Access is only available to administrators leaders of the associated team.
|
||||
// @description **Access policy**: administrator
|
||||
// @description Update a team membership. Access is only available to administrators or leaders of the associated team.
|
||||
// @description **Access policy**: administrator or leaders of the associated team
|
||||
// @tags team_memberships
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
|
@ -63,15 +63,6 @@ func (handler *Handler) teamMembershipUpdate(w http.ResponseWriter, r *http.Requ
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !security.AuthorizedTeamManagement(portainer.TeamID(payload.TeamID), securityContext) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the membership", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
membership, err := handler.DataStore.TeamMembership().TeamMembership(portainer.TeamMembershipID(membershipID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team membership with the specified identifier inside the database", err}
|
||||
|
@ -79,8 +70,15 @@ func (handler *Handler) teamMembershipUpdate(w http.ResponseWriter, r *http.Requ
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a team membership with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if securityContext.IsTeamLeader && membership.Role != portainer.MembershipRole(payload.Role) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the role of membership", httperrors.ErrResourceAccessDenied}
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
isLeadingBothTeam := security.AuthorizedTeamManagement(portainer.TeamID(payload.TeamID), securityContext) &&
|
||||
security.AuthorizedTeamManagement(membership.TeamID, securityContext)
|
||||
if !(securityContext.IsAdmin || isLeadingBothTeam) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the membership", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
membership.UserID = portainer.UserID(payload.UserID)
|
||||
|
|
|
@ -20,18 +20,19 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/teams",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/teams",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamList))).Methods(http.MethodGet)
|
||||
h.Handle("/teams/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/teams/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/teams/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/teams/{id}/memberships",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMemberships))).Methods(http.MethodGet)
|
||||
|
||||
adminRouter := h.NewRoute().Subrouter()
|
||||
adminRouter.Use(bouncer.AdminAccess)
|
||||
|
||||
teamLeaderRouter := h.NewRoute().Subrouter()
|
||||
teamLeaderRouter.Use(bouncer.TeamLeaderAccess)
|
||||
|
||||
adminRouter.Handle("/teams", httperror.LoggerHandler(h.teamCreate)).Methods(http.MethodPost)
|
||||
teamLeaderRouter.Handle("/teams", httperror.LoggerHandler(h.teamList)).Methods(http.MethodGet)
|
||||
teamLeaderRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamInspect)).Methods(http.MethodGet)
|
||||
adminRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamUpdate)).Methods(http.MethodPut)
|
||||
adminRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamDelete)).Methods(http.MethodDelete)
|
||||
teamLeaderRouter.Handle("/teams/{id}/memberships", httperror.LoggerHandler(h.teamMemberships)).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ import (
|
|||
type teamCreatePayload struct {
|
||||
// Name
|
||||
Name string `example:"developers" validate:"required"`
|
||||
// TeamLeaders
|
||||
TeamLeaders []portainer.UserID `example:"3,5"`
|
||||
}
|
||||
|
||||
func (payload *teamCreatePayload) Validate(r *http.Request) error {
|
||||
|
@ -62,5 +64,18 @@ func (handler *Handler) teamCreate(w http.ResponseWriter, r *http.Request) *http
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the team inside the database", err}
|
||||
}
|
||||
|
||||
for _, teamLeader := range payload.TeamLeaders {
|
||||
membership := &portainer.TeamMembership{
|
||||
UserID: teamLeader,
|
||||
TeamID: team.ID,
|
||||
Role: portainer.TeamLeader,
|
||||
}
|
||||
|
||||
err = handler.DataStore.TeamMembership().Create(membership)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team leadership inside the database", err}
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, team)
|
||||
}
|
||||
|
|
|
@ -48,30 +48,34 @@ func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimi
|
|||
demoService: demoService,
|
||||
passwordStrengthChecker: passwordStrengthChecker,
|
||||
}
|
||||
h.Handle("/users",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/users",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userList))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/users/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.userDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/users/{id}/tokens",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userGetAccessTokens))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}/tokens",
|
||||
rateLimiter.LimitAccess(bouncer.RestrictedAccess(httperror.LoggerHandler(h.userCreateAccessToken)))).Methods(http.MethodPost)
|
||||
h.Handle("/users/{id}/tokens/{keyID}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userRemoveAccessToken))).Methods(http.MethodDelete)
|
||||
h.Handle("/users/{id}/memberships",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userMemberships))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}/passwd",
|
||||
rateLimiter.LimitAccess(bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userUpdatePassword)))).Methods(http.MethodPut)
|
||||
h.Handle("/users/admin/check",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.adminCheck))).Methods(http.MethodGet)
|
||||
h.Handle("/users/admin/init",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.adminInit))).Methods(http.MethodPost)
|
||||
|
||||
adminRouter := h.NewRoute().Subrouter()
|
||||
adminRouter.Use(bouncer.AdminAccess)
|
||||
|
||||
teamLeaderRouter := h.NewRoute().Subrouter()
|
||||
teamLeaderRouter.Use(bouncer.TeamLeaderAccess)
|
||||
|
||||
restrictedRouter := h.NewRoute().Subrouter()
|
||||
restrictedRouter.Use(bouncer.RestrictedAccess)
|
||||
|
||||
authenticatedRouter := h.NewRoute().Subrouter()
|
||||
authenticatedRouter.Use(bouncer.AuthenticatedAccess)
|
||||
|
||||
publicRouter := h.NewRoute().Subrouter()
|
||||
publicRouter.Use(bouncer.PublicAccess)
|
||||
|
||||
adminRouter.Handle("/users", httperror.LoggerHandler(h.userCreate)).Methods(http.MethodPost)
|
||||
restrictedRouter.Handle("/users", httperror.LoggerHandler(h.userList)).Methods(http.MethodGet)
|
||||
restrictedRouter.Handle("/users/{id}", httperror.LoggerHandler(h.userInspect)).Methods(http.MethodGet)
|
||||
authenticatedRouter.Handle("/users/{id}", httperror.LoggerHandler(h.userUpdate)).Methods(http.MethodPut)
|
||||
adminRouter.Handle("/users/{id}", httperror.LoggerHandler(h.userDelete)).Methods(http.MethodDelete)
|
||||
restrictedRouter.Handle("/users/{id}/tokens", httperror.LoggerHandler(h.userGetAccessTokens)).Methods(http.MethodGet)
|
||||
restrictedRouter.Handle("/users/{id}/tokens", rateLimiter.LimitAccess(httperror.LoggerHandler(h.userCreateAccessToken))).Methods(http.MethodPost)
|
||||
restrictedRouter.Handle("/users/{id}/tokens/{keyID}", httperror.LoggerHandler(h.userRemoveAccessToken)).Methods(http.MethodDelete)
|
||||
restrictedRouter.Handle("/users/{id}/memberships", httperror.LoggerHandler(h.userMemberships)).Methods(http.MethodGet)
|
||||
authenticatedRouter.Handle("/users/{id}/passwd", rateLimiter.LimitAccess(httperror.LoggerHandler(h.userUpdatePassword))).Methods(http.MethodPut)
|
||||
publicRouter.Handle("/users/admin/check", httperror.LoggerHandler(h.adminCheck)).Methods(http.MethodGet)
|
||||
publicRouter.Handle("/users/admin/init", httperror.LoggerHandler(h.adminInit)).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -34,8 +34,7 @@ func (payload *userCreatePayload) Validate(r *http.Request) error {
|
|||
// @id UserCreate
|
||||
// @summary Create a new user
|
||||
// @description Create a new Portainer user.
|
||||
// @description Only team leaders and administrators can create users.
|
||||
// @description Only administrators can create an administrator user account.
|
||||
// @description Only administrators can create users.
|
||||
// @description **Access policy**: restricted
|
||||
// @tags users
|
||||
// @security ApiKeyAuth
|
||||
|
@ -56,19 +55,6 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin && !securityContext.IsTeamLeader {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create user", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
if securityContext.IsTeamLeader && payload.Role == 1 {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create administrator user", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
user, err := handler.DataStore.User().UserByUsername(payload.Username)
|
||||
if err != nil && !handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err}
|
||||
|
|
|
@ -103,6 +103,16 @@ func AuthorizedTeamManagement(teamID portainer.TeamID, context *RestrictedReques
|
|||
return false
|
||||
}
|
||||
|
||||
// AuthorizedIsTeamLeader ensure that the user is an admin or a team leader
|
||||
func AuthorizedIsTeamLeader(context *RestrictedRequestContext) bool {
|
||||
return context.IsAdmin || context.IsTeamLeader
|
||||
}
|
||||
|
||||
// AuthorizedIsAdmin ensure that the user is an admin
|
||||
func AuthorizedIsAdmin(context *RestrictedRequestContext) bool {
|
||||
return context.IsAdmin
|
||||
}
|
||||
|
||||
// authorizedEndpointAccess ensure that the user can access the specified environment(endpoint).
|
||||
// It will check if the user is part of the authorized users or part of a team that is
|
||||
// listed in the authorized teams of the environment(endpoint) and the associated group.
|
||||
|
|
|
@ -78,6 +78,19 @@ func (bouncer *RequestBouncer) RestrictedAccess(h http.Handler) http.Handler {
|
|||
return h
|
||||
}
|
||||
|
||||
// TeamLeaderAccess defines a security check for APIs require team leader privilege
|
||||
//
|
||||
// Bouncer operations are applied backwards:
|
||||
// - Parse the JWT from the request and stored in context, user has to be authenticated
|
||||
// - Upgrade to the restricted request
|
||||
// - User is admin or team leader
|
||||
func (bouncer *RequestBouncer) TeamLeaderAccess(h http.Handler) http.Handler {
|
||||
h = bouncer.mwIsTeamLeader(h)
|
||||
h = bouncer.mwUpgradeToRestrictedRequest(h)
|
||||
h = bouncer.mwAuthenticatedUser(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// AuthenticatedAccess defines a security check for restricted API environments(endpoints).
|
||||
// Authentication is required to access these environments(endpoints).
|
||||
// The request context will be enhanced with a RestrictedRequestContext object
|
||||
|
@ -219,6 +232,24 @@ func (bouncer *RequestBouncer) mwUpgradeToRestrictedRequest(next http.Handler) h
|
|||
})
|
||||
}
|
||||
|
||||
// mwIsTeamLeader will verify that the user is an admin or a team leader
|
||||
func (bouncer *RequestBouncer) mwIsTeamLeader(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteError(w, http.StatusInternalServerError, "Unable to retrieve restricted request context ", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin && !securityContext.IsTeamLeader {
|
||||
httperror.WriteError(w, http.StatusForbidden, "Access denied", httperrors.ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// mwAuthenticateFirst authenticates a request an auth token.
|
||||
// A result of a first succeded token lookup would be used for the authentication.
|
||||
func (bouncer *RequestBouncer) mwAuthenticateFirst(tokenLookups []tokenLookup, next http.Handler) http.Handler {
|
||||
|
|
|
@ -81,11 +81,11 @@ func FilterRegistries(registries []portainer.Registry, user *portainer.User, tea
|
|||
}
|
||||
|
||||
// FilterEndpoints filters environments(endpoints) based on user role and team memberships.
|
||||
// Non administrator users only have access to authorized environments(endpoints) (can be inherited via endpoint groups).
|
||||
// Non administrator and non-team-leader only have access to authorized environments(endpoints) (can be inherited via endpoint groups).
|
||||
func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.Endpoint {
|
||||
filteredEndpoints := endpoints
|
||||
|
||||
if !context.IsAdmin {
|
||||
if !context.IsAdmin && !context.IsTeamLeader {
|
||||
filteredEndpoints = make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
|
@ -101,11 +101,11 @@ func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.Endpoint
|
|||
}
|
||||
|
||||
// FilterEndpointGroups filters environment(endpoint) groups based on user role and team memberships.
|
||||
// Non administrator users only have access to authorized environment(endpoint) groups.
|
||||
// Non administrator users and Non-team-leaders only have access to authorized environment(endpoint) groups.
|
||||
func FilterEndpointGroups(endpointGroups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.EndpointGroup {
|
||||
filteredEndpointGroups := endpointGroups
|
||||
|
||||
if !context.IsAdmin {
|
||||
if !context.IsAdmin && !context.IsTeamLeader {
|
||||
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
|
||||
|
||||
for _, group := range endpointGroups {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="toolBar">
|
||||
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px"></i> {{ $ctrl.titleText }} </div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<div class="actionBar" ng-show="$ctrl.isAdmin">
|
||||
<button type="button" class="btn btn-sm btn-danger" 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
|
||||
</button>
|
||||
|
@ -25,7 +25,7 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<span class="md-checkbox" ng-show="$ctrl.isAdmin">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
|
@ -43,7 +43,7 @@
|
|||
ng-class="{ active: item.Checked }"
|
||||
>
|
||||
<td>
|
||||
<span class="md-checkbox">
|
||||
<span class="md-checkbox" ng-show="$ctrl.isAdmin">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" />
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
|
|
|
@ -9,5 +9,6 @@ angular.module('portainer.app').component('teamsDatatable', {
|
|||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
removeAction: '<',
|
||||
isAdmin: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="toolBar">
|
||||
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px"></i> {{ $ctrl.titleText }} </div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<div class="actionBar" ng-show="$ctrl.isAdmin">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger"
|
||||
|
@ -31,7 +31,7 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<span class="md-checkbox" ng-show="$ctrl.isAdmin">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
|
@ -63,11 +63,12 @@
|
|||
ng-class="{ active: item.Checked }"
|
||||
>
|
||||
<td>
|
||||
<span class="md-checkbox">
|
||||
<span class="md-checkbox" ng-show="$ctrl.isAdmin">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="!$ctrl.allowSelection(item)" />
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<a ui-sref="portainer.users.user({id: item.Id})">{{ item.Username }}</a>
|
||||
<a ui-sref="portainer.users.user({id: item.Id})" ng-show="$ctrl.isAdmin">{{ item.Username }}</a>
|
||||
<span ng-show="!$ctrl.isAdmin">{{ item.Username }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
|
|
|
@ -13,5 +13,6 @@ angular.module('portainer.app').component('usersDatatable', {
|
|||
reverseOrder: '<',
|
||||
removeAction: '<',
|
||||
authenticationMethod: '<',
|
||||
isAdmin: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -24,6 +24,7 @@ export function SettingsViewModel(data) {
|
|||
|
||||
export function PublicSettingsViewModel(settings) {
|
||||
this.AuthenticationMethod = settings.AuthenticationMethod;
|
||||
this.TeamSync = settings.TeamSync;
|
||||
this.RequiredPasswordLength = settings.RequiredPasswordLength;
|
||||
this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
||||
this.EnforceEdgeID = settings.EnforceEdgeID;
|
||||
|
|
|
@ -33,10 +33,10 @@
|
|||
<td
|
||||
>{{ item.TeamName ? 'Team' : 'User' }} <code ng-if="item.TeamName">{{ item.TeamName }}</code> access defined on {{ item.AccessLocation }}
|
||||
<code ng-if="item.GroupName">{{ item.GroupName }}</code>
|
||||
<a ng-if="!item.GroupName" ui-sref="portainer.endpoints.endpoint.access({id: item.EndpointId})"
|
||||
<a ng-if="!item.GroupName && $ctrl.isAdmin" ui-sref="portainer.endpoints.endpoint.access({id: item.EndpointId})"
|
||||
><i style="margin-left: 5px" class="fa fa-users" aria-hidden="true"></i> Manage access
|
||||
</a>
|
||||
<a ng-if="item.GroupName" ui-sref="portainer.groups.group.access({id: item.GroupId})"
|
||||
<a ng-if="item.GroupName && $ctrl.isAdmin" ui-sref="portainer.groups.group.access({id: item.GroupId})"
|
||||
><i style="margin-left: 5px" class="fa fa-users" aria-hidden="true"></i> Manage access
|
||||
</a>
|
||||
</td>
|
||||
|
|
|
@ -7,5 +7,6 @@ export const accessViewerDatatable = {
|
|||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
dataset: '<',
|
||||
isAdmin: '<',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@ import AccessViewerPolicyModel from '../../models/access';
|
|||
|
||||
export default class AccessViewerController {
|
||||
/* @ngInject */
|
||||
constructor(Notifications, RoleService, UserService, EndpointService, GroupService, TeamService, TeamMembershipService) {
|
||||
constructor(Notifications, RoleService, UserService, EndpointService, GroupService, TeamService, TeamMembershipService, Authentication) {
|
||||
this.Notifications = Notifications;
|
||||
this.RoleService = RoleService;
|
||||
this.UserService = UserService;
|
||||
|
@ -13,6 +13,7 @@ export default class AccessViewerController {
|
|||
this.GroupService = GroupService;
|
||||
this.TeamService = TeamService;
|
||||
this.TeamMembershipService = TeamMembershipService;
|
||||
this.Authentication = Authentication;
|
||||
|
||||
this.limitedFeature = 'rbac-roles';
|
||||
this.users = [];
|
||||
|
@ -100,6 +101,33 @@ export default class AccessViewerController {
|
|||
return this.findLowestRole(policyRoles);
|
||||
}
|
||||
|
||||
// for admin, returns all users
|
||||
// for team leader, only return all his/her team member users
|
||||
async teamMemberUsers(users, teamMemberships) {
|
||||
if (this.isAdmin) {
|
||||
return users;
|
||||
}
|
||||
|
||||
const filteredUsers = [];
|
||||
const userId = this.Authentication.getUserDetails().ID;
|
||||
const leadingTeams = await this.UserService.userLeadingTeams(userId);
|
||||
|
||||
const isMember = (userId, teamId) => {
|
||||
return !!_.find(teamMemberships, { UserId: userId, TeamId: teamId });
|
||||
};
|
||||
|
||||
for (const user of users) {
|
||||
for (const leadingTeam of leadingTeams) {
|
||||
if (isMember(user.Id, leadingTeam.Id)) {
|
||||
filteredUsers.push(user);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredUsers;
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
try {
|
||||
const limitedToBE = isLimitedToBE(this.limitedFeature);
|
||||
|
@ -108,7 +136,8 @@ export default class AccessViewerController {
|
|||
return;
|
||||
}
|
||||
|
||||
this.users = await this.UserService.users();
|
||||
this.isAdmin = this.Authentication.isAdmin();
|
||||
this.allUsers = await this.UserService.users();
|
||||
this.endpoints = _.keyBy((await this.EndpointService.endpoints()).value, 'Id');
|
||||
const groups = await this.GroupService.groups();
|
||||
this.groupUserAccessPolicies = {};
|
||||
|
@ -121,6 +150,7 @@ export default class AccessViewerController {
|
|||
this.roles = _.keyBy(await this.RoleService.roles(), 'Id');
|
||||
this.teams = _.keyBy(await this.TeamService.teams(), 'Id');
|
||||
this.teamMemberships = await this.TeamMembershipService.memberships();
|
||||
this.users = await this.teamMemberUsers(this.allUsers, this.teamMemberships);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve accesses');
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
Effective role for each environment will be displayed for the selected user
|
||||
</div>
|
||||
</div>
|
||||
<access-viewer-datatable table-key="access_viewer" dataset="$ctrl.userRoles" order-by="EndpointName"> </access-viewer-datatable>
|
||||
<access-viewer-datatable table-key="access_viewer" dataset="$ctrl.userRoles" order-by="EndpointName" is-admin="$ctrl.isAdmin"> </access-viewer-datatable>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
|
|
@ -4,8 +4,7 @@ import { TeamMembershipModel } from '../../models/teamMembership';
|
|||
angular.module('portainer.app').factory('TeamService', [
|
||||
'$q',
|
||||
'Teams',
|
||||
'TeamMembershipService',
|
||||
function TeamServiceFactory($q, Teams, TeamMembershipService) {
|
||||
function TeamServiceFactory($q, Teams) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
|
@ -41,17 +40,11 @@ angular.module('portainer.app').factory('TeamService', [
|
|||
var deferred = $q.defer();
|
||||
var payload = {
|
||||
Name: name,
|
||||
TeamLeaders: leaderIds,
|
||||
};
|
||||
Teams.create({}, payload)
|
||||
.$promise.then(function success(data) {
|
||||
var teamId = data.Id;
|
||||
var teamMembershipQueries = [];
|
||||
angular.forEach(leaderIds, function (userId) {
|
||||
teamMembershipQueries.push(TeamMembershipService.createMembership(userId, teamId, 1));
|
||||
});
|
||||
$q.all(teamMembershipQueries).then(function success() {
|
||||
deferred.resolve();
|
||||
});
|
||||
.$promise.then(function success() {
|
||||
deferred.resolve();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to create team', err: err });
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
>
|
||||
</sidebar-section>
|
||||
|
||||
<sidebar-section ng-if="isAdmin || isTeamLeader" title="Settings">
|
||||
<sidebar-section ng-if="showUsersSection" title="Settings">
|
||||
<sidebar-menu
|
||||
ng-show="display"
|
||||
icon-class="fa-users fa-fw"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.app').controller('SidebarController', SidebarController);
|
||||
|
||||
function SidebarController($rootScope, $scope, $transitions, StateManager, Notifications, Authentication, UserService, EndpointProvider) {
|
||||
function SidebarController($rootScope, $scope, $transitions, StateManager, Notifications, Authentication, UserService, EndpointProvider, SettingsService) {
|
||||
$scope.applicationState = StateManager.getState();
|
||||
$scope.endpointState = EndpointProvider.endpoint();
|
||||
$scope.display = !window.ddExtension;
|
||||
|
@ -29,11 +29,14 @@ function SidebarController($rootScope, $scope, $transitions, StateManager, Notif
|
|||
const userDetails = Authentication.getUserDetails();
|
||||
const isAdmin = isClusterAdmin();
|
||||
$scope.isAdmin = isAdmin;
|
||||
$scope.showUsersSection = isAdmin;
|
||||
|
||||
if (!isAdmin) {
|
||||
try {
|
||||
const memberships = await UserService.userMemberships(userDetails.ID);
|
||||
checkPermissions(memberships);
|
||||
const settings = await SettingsService.publicSettings();
|
||||
$scope.showUsersSection = $scope.isTeamLeader && !settings.TeamSync;
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve user memberships');
|
||||
}
|
||||
|
|
|
@ -36,6 +36,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="team && settings.TeamSync">
|
||||
<div class="col-sm-12 text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
The team leader feature is disabled as external authentication is currently enabled with team sync.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="team">
|
||||
<div class="col-sm-6">
|
||||
<rd-widget>
|
||||
|
@ -53,7 +60,7 @@
|
|||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-sm-12 nopadding">
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<button class="btn btn-primary btn-sm" ng-click="addAllUsers()" ng-if="isAdmin" ng-disabled="users.length === 0 || filteredUsers.length === 0"
|
||||
<button class="btn btn-primary btn-sm" ng-click="addAllUsers()" ng-if="isAdmin" ng-disabled="users.length === 0 || filteredUsers.length === 0 || settings.TeamSync"
|
||||
><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Add all users</button
|
||||
>
|
||||
</div>
|
||||
|
@ -83,7 +90,7 @@
|
|||
<td>
|
||||
{{ user.Username }}
|
||||
<span style="margin-left: 5px">
|
||||
<a ng-click="addUser(user)"><i class="fa fa-plus-circle space-right" aria-hidden="true"></i>Add</a>
|
||||
<a ng-click="addUser(user)" ng-class="{ 'btn disabled py-0': settings.TeamSync }"> <i class="fa fa-plus-circle space-right" aria-hidden="true"></i>Add </a>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -118,7 +125,11 @@
|
|||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-sm-12 nopadding">
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<button class="btn btn-primary btn-sm" ng-click="removeAllUsers()" ng-if="isAdmin" ng-disabled="teamMembers.length === 0 || filteredGroupMembers.length === 0"
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-click="removeAllUsers()"
|
||||
ng-if="isAdmin"
|
||||
ng-disabled="teamMembers.length === 0 || filteredGroupMembers.length === 0 || settings.TeamSync"
|
||||
><i class="fa fa-user-times space-right" aria-hidden="true"></i>Remove all users</button
|
||||
>
|
||||
</div>
|
||||
|
@ -155,7 +166,9 @@
|
|||
<td>
|
||||
{{ user.Username }}
|
||||
<span style="margin-left: 5px" ng-if="isAdmin || user.TeamRole === 'Member'">
|
||||
<a ng-click="removeUser(user)"><i class="fa fa-minus-circle space-right" aria-hidden="true"></i>Remove</a>
|
||||
<a ng-click="removeUser(user)" ng-class="{ 'btn disabled py-0': settings.TeamSync }">
|
||||
<i class="fa fa-minus-circle space-right" aria-hidden="true"></i>Remove
|
||||
</a>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -163,10 +176,10 @@
|
|||
<i ng-if="user.TeamRole === 'Member'" class="fa fa-user" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
{{ user.TeamRole }}
|
||||
<span style="margin-left: 5px" ng-if="isAdmin">
|
||||
<a style="margin-left: 5px" ng-click="promoteToLeader(user)" ng-if="user.TeamRole === 'Member'"
|
||||
<a style="margin-left: 5px" ng-click="promoteToLeader(user)" ng-if="user.TeamRole === 'Member'" ng-class="{ 'btn disabled py-0': settings.TeamSync }"
|
||||
><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Leader</a
|
||||
>
|
||||
<a style="margin-left: 5px" ng-click="demoteToMember(user)" ng-if="user.TeamRole === 'Leader'"
|
||||
<a style="margin-left: 5px" ng-click="demoteToMember(user)" ng-if="user.TeamRole === 'Leader'" ng-class="{ 'btn disabled py-0': settings.TeamSync }"
|
||||
><i class="fa fa-user-times space-right" aria-hidden="true"></i>Member</a
|
||||
>
|
||||
</span>
|
||||
|
|
|
@ -10,7 +10,8 @@ angular.module('portainer.app').controller('TeamController', [
|
|||
'Notifications',
|
||||
'PaginationService',
|
||||
'Authentication',
|
||||
function ($q, $scope, $state, $transition$, TeamService, UserService, TeamMembershipService, ModalService, Notifications, PaginationService, Authentication) {
|
||||
'SettingsService',
|
||||
function ($q, $scope, $state, $transition$, TeamService, UserService, TeamMembershipService, ModalService, Notifications, PaginationService, Authentication, SettingsService) {
|
||||
$scope.state = {
|
||||
pagination_count_users: PaginationService.getPaginationLimit('team_available_users'),
|
||||
pagination_count_members: PaginationService.getPaginationLimit('team_members'),
|
||||
|
@ -189,21 +190,23 @@ angular.module('portainer.app').controller('TeamController', [
|
|||
}
|
||||
}
|
||||
|
||||
function initView() {
|
||||
async function initView() {
|
||||
$scope.isAdmin = Authentication.isAdmin();
|
||||
$q.all({
|
||||
team: TeamService.team($transition$.params().id),
|
||||
users: UserService.users(false),
|
||||
memberships: TeamService.userMemberships($transition$.params().id),
|
||||
})
|
||||
.then(function success(data) {
|
||||
var users = data.users;
|
||||
$scope.team = data.team;
|
||||
assignUsersAndMembers(users, data.memberships);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve team details');
|
||||
|
||||
try {
|
||||
$scope.settings = await SettingsService.publicSettings();
|
||||
|
||||
const data = await $q.all({
|
||||
team: TeamService.team($transition$.params().id),
|
||||
users: UserService.users($scope.isAdmin && $scope.settings.TeamSync),
|
||||
memberships: TeamService.userMemberships($transition$.params().id),
|
||||
});
|
||||
|
||||
$scope.team = data.team;
|
||||
assignUsersAndMembers(data.users, data.memberships);
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve team details');
|
||||
}
|
||||
}
|
||||
|
||||
initView();
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<teams-datatable title-text="Teams" title-icon="fa-users" dataset="teams" table-key="teams" order-by="Name" remove-action="removeAction"></teams-datatable>
|
||||
<teams-datatable title-text="Teams" title-icon="fa-users" dataset="teams" table-key="teams" order-by="Name" remove-action="removeAction" is-admin="isAdmin"></teams-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -89,6 +89,7 @@ angular.module('portainer.app').controller('TeamsController', [
|
|||
var teams = data.teams;
|
||||
$scope.teams = teams;
|
||||
$scope.users = _.orderBy(data.users, 'Username', 'asc');
|
||||
$scope.isTeamLeader = !!teams.length;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.teams = [];
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<rd-header-content>User management</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="row" ng-if="isAdmin">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plus" title-text="Add a new user"> </rd-widget-header>
|
||||
|
@ -169,6 +169,7 @@
|
|||
order-by="Username"
|
||||
authentication-method="AuthenticationMethod"
|
||||
remove-action="removeAction"
|
||||
is-admin="isAdmin"
|
||||
></users-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue