1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-02 20:35:25 +02:00

feat(teamleader) EE-294 redesign team leader (#6973)

feat(teamleader) EE-294 redesign team leader (#6973)
This commit is contained in:
congs 2022-06-03 16:44:42 +12:00 committed by GitHub
parent bca1c6b9cf
commit 0522032515
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 223 additions and 135 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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