mirror of
https://github.com/portainer/portainer.git
synced 2025-07-25 00:09:40 +02:00
feat(authentication/ldap): Auto create and assign LDAP users (#2042)
This commit is contained in:
parent
ea7615d71c
commit
cec878b01d
14 changed files with 358 additions and 44 deletions
|
@ -1,6 +1,7 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
|
@ -40,34 +41,82 @@ func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *ht
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
u, err := handler.UserService.UserByUsername(payload.Username)
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid credentials", ErrInvalidCredentials}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
|
||||
}
|
||||
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
if settings.AuthenticationMethod == portainer.AuthenticationLDAP && u.ID != 1 {
|
||||
err = handler.LDAPService.AuthenticateUser(payload.Username, payload.Password, &settings.LDAPSettings)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate user via LDAP/AD", err}
|
||||
}
|
||||
} else {
|
||||
err = handler.CryptoService.CompareHashAndData(u.Password, payload.Password)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", ErrInvalidCredentials}
|
||||
}
|
||||
u, err := handler.UserService.UserByUsername(payload.Username)
|
||||
if err != nil && err != portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
|
||||
}
|
||||
|
||||
if err == portainer.ErrObjectNotFound && settings.AuthenticationMethod == portainer.AuthenticationInternal {
|
||||
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", portainer.ErrUnauthorized}
|
||||
}
|
||||
|
||||
if settings.AuthenticationMethod == portainer.AuthenticationLDAP {
|
||||
if u == nil {
|
||||
return handler.authenticateLDAPAndCreateUser(w, payload.Username, payload.Password, &settings.LDAPSettings)
|
||||
}
|
||||
return handler.authenticateLDAP(w, u, payload.Password, &settings.LDAPSettings)
|
||||
}
|
||||
|
||||
return handler.authenticateInternal(w, u, payload.Password)
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
|
||||
err := handler.LDAPService.AuthenticateUser(user.Username, password, ldapSettings)
|
||||
if err != nil {
|
||||
return handler.authenticateInternal(w, user, password)
|
||||
}
|
||||
|
||||
err = handler.addUserIntoTeams(user, ldapSettings)
|
||||
if err != nil {
|
||||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portainer.User, password string) *httperror.HandlerError {
|
||||
err := handler.CryptoService.CompareHashAndData(user.Password, password)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", portainer.ErrUnauthorized}
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, username, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
|
||||
err := handler.LDAPService.AuthenticateUser(username, password, ldapSettings)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", err}
|
||||
}
|
||||
|
||||
user := &portainer.User{
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
}
|
||||
|
||||
err = handler.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.addUserIntoTeams(user, ldapSettings)
|
||||
if err != nil {
|
||||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
|
||||
tokenData := &portainer.TokenData{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
Role: u.Role,
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
}
|
||||
|
||||
token, err := handler.JWTService.GenerateToken(tokenData)
|
||||
|
@ -77,3 +126,59 @@ func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *ht
|
|||
|
||||
return response.JSON(w, &authenticateResponse{JWT: token})
|
||||
}
|
||||
|
||||
func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portainer.LDAPSettings) error {
|
||||
teams, err := handler.TeamService.Teams()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userGroups, err := handler.LDAPService.GetUserGroups(user.Username, settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userMemberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, team := range teams {
|
||||
if teamExists(team.Name, userGroups) {
|
||||
|
||||
if teamMembershipExists(team.ID, userMemberships) {
|
||||
continue
|
||||
}
|
||||
|
||||
membership := &portainer.TeamMembership{
|
||||
UserID: user.ID,
|
||||
TeamID: team.ID,
|
||||
Role: portainer.TeamMember,
|
||||
}
|
||||
|
||||
err := handler.TeamMembershipService.CreateTeamMembership(membership)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func teamExists(teamName string, ldapGroups []string) bool {
|
||||
for _, group := range ldapGroups {
|
||||
if group == teamName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func teamMembershipExists(teamID portainer.TeamID, memberships []portainer.TeamMembership) bool {
|
||||
for _, membership := range memberships {
|
||||
if membership.TeamID == teamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -20,12 +20,14 @@ const (
|
|||
// Handler is the HTTP handler used to handle authentication operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
authDisabled bool
|
||||
UserService portainer.UserService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
SettingsService portainer.SettingsService
|
||||
authDisabled bool
|
||||
UserService portainer.UserService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
SettingsService portainer.SettingsService
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage authentication operations.
|
||||
|
|
|
@ -26,19 +26,47 @@ func (handler *Handler) userDelete(w http.ResponseWriter, r *http.Request) *http
|
|||
return &httperror.HandlerError{http.StatusForbidden, "Cannot remove your own user account. Contact another administrator", portainer.ErrAdminCannotRemoveSelf}
|
||||
}
|
||||
|
||||
_, err = handler.UserService.User(portainer.UserID(userID))
|
||||
user, err := handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a user with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a user with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.UserService.DeleteUser(portainer.UserID(userID))
|
||||
if user.Role == portainer.AdministratorRole {
|
||||
return handler.deleteAdminUser(w, user)
|
||||
}
|
||||
|
||||
return handler.deleteUser(w, user)
|
||||
}
|
||||
|
||||
func (handler *Handler) deleteAdminUser(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
|
||||
users, err := handler.UserService.Users()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err}
|
||||
}
|
||||
|
||||
localAdminCount := 0
|
||||
for _, u := range users {
|
||||
if u.Role == portainer.AdministratorRole && u.Password != "" {
|
||||
localAdminCount++
|
||||
}
|
||||
}
|
||||
|
||||
if localAdminCount < 2 {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Cannot remove local administrator user", portainer.ErrCannotRemoveLastLocalAdmin}
|
||||
}
|
||||
|
||||
return handler.deleteUser(w, user)
|
||||
}
|
||||
|
||||
func (handler *Handler) deleteUser(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
|
||||
err := handler.UserService.DeleteUser(portainer.UserID(user.ID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove user from the database", err}
|
||||
}
|
||||
|
||||
err = handler.TeamMembershipService.DeleteTeamMembershipByUserID(portainer.UserID(userID))
|
||||
err = handler.TeamMembershipService.DeleteTeamMembershipByUserID(portainer.UserID(user.ID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove user memberships from the database", err}
|
||||
}
|
||||
|
|
|
@ -91,6 +91,8 @@ func (server *Server) Start() error {
|
|||
authHandler.JWTService = server.JWTService
|
||||
authHandler.LDAPService = server.LDAPService
|
||||
authHandler.SettingsService = server.SettingsService
|
||||
authHandler.TeamService = server.TeamService
|
||||
authHandler.TeamMembershipService = server.TeamMembershipService
|
||||
|
||||
var dockerHubHandler = dockerhub.NewHandler(requestBouncer)
|
||||
dockerHubHandler.DockerHubService = server.DockerHubService
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue