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
|
// @id EndpointList
|
||||||
// @summary List environments(endpoints)
|
// @summary List environments(endpoints)
|
||||||
// @description List all environments(endpoints) based on the current user authorizations. Will
|
// @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 only return authorized environments(endpoints).
|
||||||
// @description **Access policy**: restricted
|
// @description **Access policy**: restricted
|
||||||
// @tags endpoints
|
// @tags endpoints
|
||||||
|
|
|
@ -28,6 +28,8 @@ type publicSettingsResponse struct {
|
||||||
EnableTelemetry bool `json:"EnableTelemetry" example:"true"`
|
EnableTelemetry bool `json:"EnableTelemetry" example:"true"`
|
||||||
// The expiry of a Kubeconfig
|
// The expiry of a Kubeconfig
|
||||||
KubeconfigExpiry string `example:"24h" default:"0"`
|
KubeconfigExpiry string `example:"24h" default:"0"`
|
||||||
|
// Whether team sync is enabled
|
||||||
|
TeamSync bool `json:"TeamSync" example:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id SettingsPublic
|
// @id SettingsPublic
|
||||||
|
@ -72,5 +74,9 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
|
||||||
publicSettings.OAuthLoginURI += "&prompt=login"
|
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
|
return publicSettings
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,14 +21,13 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
}
|
}
|
||||||
h.Handle("/team_memberships",
|
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipCreate))).Methods(http.MethodPost)
|
h.Use(bouncer.TeamLeaderAccess)
|
||||||
h.Handle("/team_memberships",
|
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipList))).Methods(http.MethodGet)
|
h.Handle("/team_memberships", httperror.LoggerHandler(h.teamMembershipCreate)).Methods(http.MethodPost)
|
||||||
h.Handle("/team_memberships/{id}",
|
h.Handle("/team_memberships", httperror.LoggerHandler(h.teamMembershipList)).Methods(http.MethodGet)
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipUpdate))).Methods(http.MethodPut)
|
h.Handle("/team_memberships/{id}", httperror.LoggerHandler(h.teamMembershipUpdate)).Methods(http.MethodPut)
|
||||||
h.Handle("/team_memberships/{id}",
|
h.Handle("/team_memberships/{id}", httperror.LoggerHandler(h.teamMembershipDelete)).Methods(http.MethodDelete)
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMembershipDelete))).Methods(http.MethodDelete)
|
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,6 @@ import (
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api/http/errors"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// @id TeamMembershipList
|
// @id TeamMembershipList
|
||||||
|
@ -23,15 +21,6 @@ import (
|
||||||
// @failure 500 "Server error"
|
// @failure 500 "Server error"
|
||||||
// @router /team_memberships [get]
|
// @router /team_memberships [get]
|
||||||
func (handler *Handler) teamMembershipList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
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()
|
memberships, err := handler.DataStore.TeamMembership().TeamMemberships()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve team memberships from the database", err}
|
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
|
// @id TeamMembershipUpdate
|
||||||
// @summary Update a team membership
|
// @summary Update a team membership
|
||||||
// @description Update a team membership. Access is only available to administrators leaders of the associated team.
|
// @description Update a team membership. Access is only available to administrators or leaders of the associated team.
|
||||||
// @description **Access policy**: administrator
|
// @description **Access policy**: administrator or leaders of the associated team
|
||||||
// @tags team_memberships
|
// @tags team_memberships
|
||||||
// @security ApiKeyAuth
|
// @security ApiKeyAuth
|
||||||
// @security jwt
|
// @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}
|
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))
|
membership, err := handler.DataStore.TeamMembership().TeamMembership(portainer.TeamMembershipID(membershipID))
|
||||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team membership with the specified identifier inside the database", 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}
|
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) {
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the role of membership", httperrors.ErrResourceAccessDenied}
|
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)
|
membership.UserID = portainer.UserID(payload.UserID)
|
||||||
|
|
|
@ -20,18 +20,19 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
}
|
}
|
||||||
h.Handle("/teams",
|
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamCreate))).Methods(http.MethodPost)
|
adminRouter := h.NewRoute().Subrouter()
|
||||||
h.Handle("/teams",
|
adminRouter.Use(bouncer.AdminAccess)
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamList))).Methods(http.MethodGet)
|
|
||||||
h.Handle("/teams/{id}",
|
teamLeaderRouter := h.NewRoute().Subrouter()
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamInspect))).Methods(http.MethodGet)
|
teamLeaderRouter.Use(bouncer.TeamLeaderAccess)
|
||||||
h.Handle("/teams/{id}",
|
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamUpdate))).Methods(http.MethodPut)
|
adminRouter.Handle("/teams", httperror.LoggerHandler(h.teamCreate)).Methods(http.MethodPost)
|
||||||
h.Handle("/teams/{id}",
|
teamLeaderRouter.Handle("/teams", httperror.LoggerHandler(h.teamList)).Methods(http.MethodGet)
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamDelete))).Methods(http.MethodDelete)
|
teamLeaderRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamInspect)).Methods(http.MethodGet)
|
||||||
h.Handle("/teams/{id}/memberships",
|
adminRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamUpdate)).Methods(http.MethodPut)
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.teamMemberships))).Methods(http.MethodGet)
|
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
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@ import (
|
||||||
type teamCreatePayload struct {
|
type teamCreatePayload struct {
|
||||||
// Name
|
// Name
|
||||||
Name string `example:"developers" validate:"required"`
|
Name string `example:"developers" validate:"required"`
|
||||||
|
// TeamLeaders
|
||||||
|
TeamLeaders []portainer.UserID `example:"3,5"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *teamCreatePayload) Validate(r *http.Request) error {
|
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}
|
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)
|
return response.JSON(w, team)
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,30 +48,34 @@ func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimi
|
||||||
demoService: demoService,
|
demoService: demoService,
|
||||||
passwordStrengthChecker: passwordStrengthChecker,
|
passwordStrengthChecker: passwordStrengthChecker,
|
||||||
}
|
}
|
||||||
h.Handle("/users",
|
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost)
|
adminRouter := h.NewRoute().Subrouter()
|
||||||
h.Handle("/users",
|
adminRouter.Use(bouncer.AdminAccess)
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userList))).Methods(http.MethodGet)
|
|
||||||
h.Handle("/users/{id}",
|
teamLeaderRouter := h.NewRoute().Subrouter()
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userInspect))).Methods(http.MethodGet)
|
teamLeaderRouter.Use(bouncer.TeamLeaderAccess)
|
||||||
h.Handle("/users/{id}",
|
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userUpdate))).Methods(http.MethodPut)
|
restrictedRouter := h.NewRoute().Subrouter()
|
||||||
h.Handle("/users/{id}",
|
restrictedRouter.Use(bouncer.RestrictedAccess)
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.userDelete))).Methods(http.MethodDelete)
|
|
||||||
h.Handle("/users/{id}/tokens",
|
authenticatedRouter := h.NewRoute().Subrouter()
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userGetAccessTokens))).Methods(http.MethodGet)
|
authenticatedRouter.Use(bouncer.AuthenticatedAccess)
|
||||||
h.Handle("/users/{id}/tokens",
|
|
||||||
rateLimiter.LimitAccess(bouncer.RestrictedAccess(httperror.LoggerHandler(h.userCreateAccessToken)))).Methods(http.MethodPost)
|
publicRouter := h.NewRoute().Subrouter()
|
||||||
h.Handle("/users/{id}/tokens/{keyID}",
|
publicRouter.Use(bouncer.PublicAccess)
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userRemoveAccessToken))).Methods(http.MethodDelete)
|
|
||||||
h.Handle("/users/{id}/memberships",
|
adminRouter.Handle("/users", httperror.LoggerHandler(h.userCreate)).Methods(http.MethodPost)
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userMemberships))).Methods(http.MethodGet)
|
restrictedRouter.Handle("/users", httperror.LoggerHandler(h.userList)).Methods(http.MethodGet)
|
||||||
h.Handle("/users/{id}/passwd",
|
restrictedRouter.Handle("/users/{id}", httperror.LoggerHandler(h.userInspect)).Methods(http.MethodGet)
|
||||||
rateLimiter.LimitAccess(bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userUpdatePassword)))).Methods(http.MethodPut)
|
authenticatedRouter.Handle("/users/{id}", httperror.LoggerHandler(h.userUpdate)).Methods(http.MethodPut)
|
||||||
h.Handle("/users/admin/check",
|
adminRouter.Handle("/users/{id}", httperror.LoggerHandler(h.userDelete)).Methods(http.MethodDelete)
|
||||||
bouncer.PublicAccess(httperror.LoggerHandler(h.adminCheck))).Methods(http.MethodGet)
|
restrictedRouter.Handle("/users/{id}/tokens", httperror.LoggerHandler(h.userGetAccessTokens)).Methods(http.MethodGet)
|
||||||
h.Handle("/users/admin/init",
|
restrictedRouter.Handle("/users/{id}/tokens", rateLimiter.LimitAccess(httperror.LoggerHandler(h.userCreateAccessToken))).Methods(http.MethodPost)
|
||||||
bouncer.PublicAccess(httperror.LoggerHandler(h.adminInit))).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
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,8 +34,7 @@ func (payload *userCreatePayload) Validate(r *http.Request) error {
|
||||||
// @id UserCreate
|
// @id UserCreate
|
||||||
// @summary Create a new user
|
// @summary Create a new user
|
||||||
// @description Create a new Portainer user.
|
// @description Create a new Portainer user.
|
||||||
// @description Only team leaders and administrators can create users.
|
// @description Only administrators can create users.
|
||||||
// @description Only administrators can create an administrator user account.
|
|
||||||
// @description **Access policy**: restricted
|
// @description **Access policy**: restricted
|
||||||
// @tags users
|
// @tags users
|
||||||
// @security ApiKeyAuth
|
// @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}
|
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)
|
user, err := handler.DataStore.User().UserByUsername(payload.Username)
|
||||||
if err != nil && !handler.DataStore.IsErrObjectNotFound(err) {
|
if err != nil && !handler.DataStore.IsErrObjectNotFound(err) {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", 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
|
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).
|
// 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
|
// 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.
|
// 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
|
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).
|
// AuthenticatedAccess defines a security check for restricted API environments(endpoints).
|
||||||
// Authentication is required to access these environments(endpoints).
|
// Authentication is required to access these environments(endpoints).
|
||||||
// The request context will be enhanced with a RestrictedRequestContext object
|
// 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.
|
// mwAuthenticateFirst authenticates a request an auth token.
|
||||||
// A result of a first succeded token lookup would be used for the authentication.
|
// 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 {
|
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.
|
// 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 {
|
func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.Endpoint {
|
||||||
filteredEndpoints := endpoints
|
filteredEndpoints := endpoints
|
||||||
|
|
||||||
if !context.IsAdmin {
|
if !context.IsAdmin && !context.IsTeamLeader {
|
||||||
filteredEndpoints = make([]portainer.Endpoint, 0)
|
filteredEndpoints = make([]portainer.Endpoint, 0)
|
||||||
|
|
||||||
for _, endpoint := range endpoints {
|
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.
|
// 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 {
|
func FilterEndpointGroups(endpointGroups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.EndpointGroup {
|
||||||
filteredEndpointGroups := endpointGroups
|
filteredEndpointGroups := endpointGroups
|
||||||
|
|
||||||
if !context.IsAdmin {
|
if !context.IsAdmin && !context.IsTeamLeader {
|
||||||
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
|
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
|
||||||
|
|
||||||
for _, group := range endpointGroups {
|
for _, group := range endpointGroups {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="toolBar">
|
<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 class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px"></i> {{ $ctrl.titleText }} </div>
|
||||||
</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)">
|
<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
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
</button>
|
</button>
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<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()" />
|
<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>
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
ng-class="{ active: item.Checked }"
|
ng-class="{ active: item.Checked }"
|
||||||
>
|
>
|
||||||
<td>
|
<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)" />
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" />
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -9,5 +9,6 @@ angular.module('portainer.app').component('teamsDatatable', {
|
||||||
orderBy: '@',
|
orderBy: '@',
|
||||||
reverseOrder: '<',
|
reverseOrder: '<',
|
||||||
removeAction: '<',
|
removeAction: '<',
|
||||||
|
isAdmin: '<',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="toolBar">
|
<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 class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px"></i> {{ $ctrl.titleText }} </div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar">
|
<div class="actionBar" ng-show="$ctrl.isAdmin">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-danger"
|
class="btn btn-sm btn-danger"
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<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()" />
|
<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>
|
||||||
|
@ -63,11 +63,12 @@
|
||||||
ng-class="{ active: item.Checked }"
|
ng-class="{ active: item.Checked }"
|
||||||
>
|
>
|
||||||
<td>
|
<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)" />
|
<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>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</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>
|
||||||
<td>
|
<td>
|
||||||
<span>
|
<span>
|
||||||
|
|
|
@ -13,5 +13,6 @@ angular.module('portainer.app').component('usersDatatable', {
|
||||||
reverseOrder: '<',
|
reverseOrder: '<',
|
||||||
removeAction: '<',
|
removeAction: '<',
|
||||||
authenticationMethod: '<',
|
authenticationMethod: '<',
|
||||||
|
isAdmin: '<',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,6 +24,7 @@ export function SettingsViewModel(data) {
|
||||||
|
|
||||||
export function PublicSettingsViewModel(settings) {
|
export function PublicSettingsViewModel(settings) {
|
||||||
this.AuthenticationMethod = settings.AuthenticationMethod;
|
this.AuthenticationMethod = settings.AuthenticationMethod;
|
||||||
|
this.TeamSync = settings.TeamSync;
|
||||||
this.RequiredPasswordLength = settings.RequiredPasswordLength;
|
this.RequiredPasswordLength = settings.RequiredPasswordLength;
|
||||||
this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
||||||
this.EnforceEdgeID = settings.EnforceEdgeID;
|
this.EnforceEdgeID = settings.EnforceEdgeID;
|
||||||
|
|
|
@ -33,10 +33,10 @@
|
||||||
<td
|
<td
|
||||||
>{{ item.TeamName ? 'Team' : 'User' }} <code ng-if="item.TeamName">{{ item.TeamName }}</code> access defined on {{ item.AccessLocation }}
|
>{{ 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>
|
<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
|
><i style="margin-left: 5px" class="fa fa-users" aria-hidden="true"></i> Manage access
|
||||||
</a>
|
</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
|
><i style="margin-left: 5px" class="fa fa-users" aria-hidden="true"></i> Manage access
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -7,5 +7,6 @@ export const accessViewerDatatable = {
|
||||||
tableKey: '@',
|
tableKey: '@',
|
||||||
orderBy: '@',
|
orderBy: '@',
|
||||||
dataset: '<',
|
dataset: '<',
|
||||||
|
isAdmin: '<',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@ import AccessViewerPolicyModel from '../../models/access';
|
||||||
|
|
||||||
export default class AccessViewerController {
|
export default class AccessViewerController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor(Notifications, RoleService, UserService, EndpointService, GroupService, TeamService, TeamMembershipService) {
|
constructor(Notifications, RoleService, UserService, EndpointService, GroupService, TeamService, TeamMembershipService, Authentication) {
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
this.RoleService = RoleService;
|
this.RoleService = RoleService;
|
||||||
this.UserService = UserService;
|
this.UserService = UserService;
|
||||||
|
@ -13,6 +13,7 @@ export default class AccessViewerController {
|
||||||
this.GroupService = GroupService;
|
this.GroupService = GroupService;
|
||||||
this.TeamService = TeamService;
|
this.TeamService = TeamService;
|
||||||
this.TeamMembershipService = TeamMembershipService;
|
this.TeamMembershipService = TeamMembershipService;
|
||||||
|
this.Authentication = Authentication;
|
||||||
|
|
||||||
this.limitedFeature = 'rbac-roles';
|
this.limitedFeature = 'rbac-roles';
|
||||||
this.users = [];
|
this.users = [];
|
||||||
|
@ -100,6 +101,33 @@ export default class AccessViewerController {
|
||||||
return this.findLowestRole(policyRoles);
|
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() {
|
async $onInit() {
|
||||||
try {
|
try {
|
||||||
const limitedToBE = isLimitedToBE(this.limitedFeature);
|
const limitedToBE = isLimitedToBE(this.limitedFeature);
|
||||||
|
@ -108,7 +136,8 @@ export default class AccessViewerController {
|
||||||
return;
|
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');
|
this.endpoints = _.keyBy((await this.EndpointService.endpoints()).value, 'Id');
|
||||||
const groups = await this.GroupService.groups();
|
const groups = await this.GroupService.groups();
|
||||||
this.groupUserAccessPolicies = {};
|
this.groupUserAccessPolicies = {};
|
||||||
|
@ -121,6 +150,7 @@ export default class AccessViewerController {
|
||||||
this.roles = _.keyBy(await this.RoleService.roles(), 'Id');
|
this.roles = _.keyBy(await this.RoleService.roles(), 'Id');
|
||||||
this.teams = _.keyBy(await this.TeamService.teams(), 'Id');
|
this.teams = _.keyBy(await this.TeamService.teams(), 'Id');
|
||||||
this.teamMemberships = await this.TeamMembershipService.memberships();
|
this.teamMemberships = await this.TeamMembershipService.memberships();
|
||||||
|
this.users = await this.teamMemberUsers(this.allUsers, this.teamMemberships);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve accesses');
|
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
|
Effective role for each environment will be displayed for the selected user
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</form>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
|
|
|
@ -4,8 +4,7 @@ import { TeamMembershipModel } from '../../models/teamMembership';
|
||||||
angular.module('portainer.app').factory('TeamService', [
|
angular.module('portainer.app').factory('TeamService', [
|
||||||
'$q',
|
'$q',
|
||||||
'Teams',
|
'Teams',
|
||||||
'TeamMembershipService',
|
function TeamServiceFactory($q, Teams) {
|
||||||
function TeamServiceFactory($q, Teams, TeamMembershipService) {
|
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
|
@ -41,17 +40,11 @@ angular.module('portainer.app').factory('TeamService', [
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
var payload = {
|
var payload = {
|
||||||
Name: name,
|
Name: name,
|
||||||
|
TeamLeaders: leaderIds,
|
||||||
};
|
};
|
||||||
Teams.create({}, payload)
|
Teams.create({}, payload)
|
||||||
.$promise.then(function success(data) {
|
.$promise.then(function success() {
|
||||||
var teamId = data.Id;
|
deferred.resolve();
|
||||||
var teamMembershipQueries = [];
|
|
||||||
angular.forEach(leaderIds, function (userId) {
|
|
||||||
teamMembershipQueries.push(TeamMembershipService.createMembership(userId, teamId, 1));
|
|
||||||
});
|
|
||||||
$q.all(teamMembershipQueries).then(function success() {
|
|
||||||
deferred.resolve();
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
deferred.reject({ msg: 'Unable to create team', err: err });
|
deferred.reject({ msg: 'Unable to create team', err: err });
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
>
|
>
|
||||||
</sidebar-section>
|
</sidebar-section>
|
||||||
|
|
||||||
<sidebar-section ng-if="isAdmin || isTeamLeader" title="Settings">
|
<sidebar-section ng-if="showUsersSection" title="Settings">
|
||||||
<sidebar-menu
|
<sidebar-menu
|
||||||
ng-show="display"
|
ng-show="display"
|
||||||
icon-class="fa-users fa-fw"
|
icon-class="fa-users fa-fw"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.app').controller('SidebarController', SidebarController);
|
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.applicationState = StateManager.getState();
|
||||||
$scope.endpointState = EndpointProvider.endpoint();
|
$scope.endpointState = EndpointProvider.endpoint();
|
||||||
$scope.display = !window.ddExtension;
|
$scope.display = !window.ddExtension;
|
||||||
|
@ -29,11 +29,14 @@ function SidebarController($rootScope, $scope, $transitions, StateManager, Notif
|
||||||
const userDetails = Authentication.getUserDetails();
|
const userDetails = Authentication.getUserDetails();
|
||||||
const isAdmin = isClusterAdmin();
|
const isAdmin = isClusterAdmin();
|
||||||
$scope.isAdmin = isAdmin;
|
$scope.isAdmin = isAdmin;
|
||||||
|
$scope.showUsersSection = isAdmin;
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
try {
|
try {
|
||||||
const memberships = await UserService.userMemberships(userDetails.ID);
|
const memberships = await UserService.userMemberships(userDetails.ID);
|
||||||
checkPermissions(memberships);
|
checkPermissions(memberships);
|
||||||
|
const settings = await SettingsService.publicSettings();
|
||||||
|
$scope.showUsersSection = $scope.isTeamLeader && !settings.TeamSync;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve user memberships');
|
Notifications.error('Failure', err, 'Unable to retrieve user memberships');
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,13 @@
|
||||||
</div>
|
</div>
|
||||||
</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="row" ng-if="team">
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
|
@ -53,7 +60,7 @@
|
||||||
</rd-widget-header>
|
</rd-widget-header>
|
||||||
<rd-widget-taskbar classes="col-sm-12 nopadding">
|
<rd-widget-taskbar classes="col-sm-12 nopadding">
|
||||||
<div class="col-sm-12 col-md-6 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
|
><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Add all users</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -83,7 +90,7 @@
|
||||||
<td>
|
<td>
|
||||||
{{ user.Username }}
|
{{ user.Username }}
|
||||||
<span style="margin-left: 5px">
|
<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>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -118,7 +125,11 @@
|
||||||
</rd-widget-header>
|
</rd-widget-header>
|
||||||
<rd-widget-taskbar classes="col-sm-12 nopadding">
|
<rd-widget-taskbar classes="col-sm-12 nopadding">
|
||||||
<div class="col-sm-12 col-md-6 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
|
><i class="fa fa-user-times space-right" aria-hidden="true"></i>Remove all users</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -155,7 +166,9 @@
|
||||||
<td>
|
<td>
|
||||||
{{ user.Username }}
|
{{ user.Username }}
|
||||||
<span style="margin-left: 5px" ng-if="isAdmin || user.TeamRole === 'Member'">
|
<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>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -163,10 +176,10 @@
|
||||||
<i ng-if="user.TeamRole === 'Member'" class="fa fa-user" aria-hidden="true" style="margin-right: 2px"></i>
|
<i ng-if="user.TeamRole === 'Member'" class="fa fa-user" aria-hidden="true" style="margin-right: 2px"></i>
|
||||||
{{ user.TeamRole }}
|
{{ user.TeamRole }}
|
||||||
<span style="margin-left: 5px" ng-if="isAdmin">
|
<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
|
><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
|
><i class="fa fa-user-times space-right" aria-hidden="true"></i>Member</a
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -10,7 +10,8 @@ angular.module('portainer.app').controller('TeamController', [
|
||||||
'Notifications',
|
'Notifications',
|
||||||
'PaginationService',
|
'PaginationService',
|
||||||
'Authentication',
|
'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 = {
|
$scope.state = {
|
||||||
pagination_count_users: PaginationService.getPaginationLimit('team_available_users'),
|
pagination_count_users: PaginationService.getPaginationLimit('team_available_users'),
|
||||||
pagination_count_members: PaginationService.getPaginationLimit('team_members'),
|
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();
|
$scope.isAdmin = Authentication.isAdmin();
|
||||||
$q.all({
|
|
||||||
team: TeamService.team($transition$.params().id),
|
try {
|
||||||
users: UserService.users(false),
|
$scope.settings = await SettingsService.publicSettings();
|
||||||
memberships: TeamService.userMemberships($transition$.params().id),
|
|
||||||
})
|
const data = await $q.all({
|
||||||
.then(function success(data) {
|
team: TeamService.team($transition$.params().id),
|
||||||
var users = data.users;
|
users: UserService.users($scope.isAdmin && $scope.settings.TeamSync),
|
||||||
$scope.team = data.team;
|
memberships: TeamService.userMemberships($transition$.params().id),
|
||||||
assignUsersAndMembers(users, data.memberships);
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve team details');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$scope.team = data.team;
|
||||||
|
assignUsersAndMembers(data.users, data.memberships);
|
||||||
|
} catch (err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve team details');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
|
|
|
@ -11,6 +11,6 @@
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -89,6 +89,7 @@ angular.module('portainer.app').controller('TeamsController', [
|
||||||
var teams = data.teams;
|
var teams = data.teams;
|
||||||
$scope.teams = teams;
|
$scope.teams = teams;
|
||||||
$scope.users = _.orderBy(data.users, 'Username', 'asc');
|
$scope.users = _.orderBy(data.users, 'Username', 'asc');
|
||||||
|
$scope.isTeamLeader = !!teams.length;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
$scope.teams = [];
|
$scope.teams = [];
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<rd-header-content>User management</rd-header-content>
|
<rd-header-content>User management</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row" ng-if="isAdmin">
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-plus" title-text="Add a new user"> </rd-widget-header>
|
<rd-widget-header icon="fa-plus" title-text="Add a new user"> </rd-widget-header>
|
||||||
|
@ -169,6 +169,7 @@
|
||||||
order-by="Username"
|
order-by="Username"
|
||||||
authentication-method="AuthenticationMethod"
|
authentication-method="AuthenticationMethod"
|
||||||
remove-action="removeAction"
|
remove-action="removeAction"
|
||||||
|
is-admin="isAdmin"
|
||||||
></users-datatable>
|
></users-datatable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue