mirror of https://github.com/portainer/portainer
feat(authentication/ldap): Auto create and assign LDAP users (#2042)
parent
ea7615d71c
commit
cec878b01d
|
@ -0,0 +1,16 @@
|
|||
package migrator
|
||||
|
||||
import "github.com/portainer/portainer"
|
||||
|
||||
func (m *Migrator) updateSettingsToVersion13() error {
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
legacySettings.LDAPSettings.GroupSearchSettings = []portainer.LDAPGroupSearchSettings{
|
||||
portainer.LDAPGroupSearchSettings{},
|
||||
}
|
||||
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
}
|
|
@ -170,5 +170,12 @@ func (m *Migrator) Migrate() error {
|
|||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 13 {
|
||||
err := m.updateSettingsToVersion13()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
|
|
@ -164,6 +164,9 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
|||
SearchSettings: []portainer.LDAPSearchSettings{
|
||||
portainer.LDAPSearchSettings{},
|
||||
},
|
||||
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
|
||||
portainer.LDAPGroupSearchSettings{},
|
||||
},
|
||||
},
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
|
|
|
@ -10,10 +10,11 @@ const (
|
|||
|
||||
// User errors.
|
||||
const (
|
||||
ErrUserAlreadyExists = Error("User already exists")
|
||||
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed")
|
||||
ErrAdminAlreadyInitialized = Error("An administrator user already exists")
|
||||
ErrAdminCannotRemoveSelf = Error("Cannot remove your own user account. Contact another administrator")
|
||||
ErrUserAlreadyExists = Error("User already exists")
|
||||
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed")
|
||||
ErrAdminAlreadyInitialized = Error("An administrator user already exists")
|
||||
ErrAdminCannotRemoveSelf = Error("Cannot remove your own user account. Contact another administrator")
|
||||
ErrCannotRemoveLastLocalAdmin = Error("Cannot remove the last local administrator account")
|
||||
)
|
||||
|
||||
// Team errors.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -102,12 +102,65 @@ func (*Service) AuthenticateUser(username, password string, settings *portainer.
|
|||
|
||||
err = connection.Bind(userDN, password)
|
||||
if err != nil {
|
||||
return err
|
||||
return portainer.ErrUnauthorized
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserGroups is used to retrieve user groups from LDAP/AD.
|
||||
func (*Service) GetUserGroups(username string, settings *portainer.LDAPSettings) ([]string, error) {
|
||||
connection, err := createConnection(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
err = connection.Bind(settings.ReaderDN, settings.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userDN, err := searchUser(username, connection, settings.SearchSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userGroups := getGroups(userDN, connection, settings.GroupSearchSettings)
|
||||
|
||||
return userGroups, nil
|
||||
}
|
||||
|
||||
// Get a list of group names for specified user from LDAP/AD
|
||||
func getGroups(userDN string, conn *ldap.Conn, settings []portainer.LDAPGroupSearchSettings) []string {
|
||||
groups := make([]string, 0)
|
||||
|
||||
for _, searchSettings := range settings {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
searchSettings.GroupBaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
fmt.Sprintf("(&%s(%s=%s))", searchSettings.GroupFilter, searchSettings.GroupAttribute, userDN),
|
||||
[]string{"cn"},
|
||||
nil,
|
||||
)
|
||||
|
||||
// Deliberately skip errors on the search request so that we can jump to other search settings
|
||||
// if any issue arise with the current one.
|
||||
sr, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range sr.Entries {
|
||||
for _, attr := range entry.Attributes {
|
||||
groups = append(groups, attr.Values[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
// TestConnectivity is used to test a connection against the LDAP server using the credentials
|
||||
// specified in the LDAPSettings.
|
||||
func (*Service) TestConnectivity(settings *portainer.LDAPSettings) error {
|
||||
|
|
|
@ -46,12 +46,13 @@ type (
|
|||
|
||||
// LDAPSettings represents the settings used to connect to a LDAP server.
|
||||
LDAPSettings struct {
|
||||
ReaderDN string `json:"ReaderDN"`
|
||||
Password string `json:"Password"`
|
||||
URL string `json:"URL"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
StartTLS bool `json:"StartTLS"`
|
||||
SearchSettings []LDAPSearchSettings `json:"SearchSettings"`
|
||||
ReaderDN string `json:"ReaderDN"`
|
||||
Password string `json:"Password"`
|
||||
URL string `json:"URL"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
StartTLS bool `json:"StartTLS"`
|
||||
SearchSettings []LDAPSearchSettings `json:"SearchSettings"`
|
||||
GroupSearchSettings []LDAPGroupSearchSettings `json:"GroupSearchSettings"`
|
||||
}
|
||||
|
||||
// TLSConfiguration represents a TLS configuration.
|
||||
|
@ -70,6 +71,13 @@ type (
|
|||
UserNameAttribute string `json:"UserNameAttribute"`
|
||||
}
|
||||
|
||||
// LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server.
|
||||
LDAPGroupSearchSettings struct {
|
||||
GroupBaseDN string `json:"GroupBaseDN"`
|
||||
GroupFilter string `json:"GroupFilter"`
|
||||
GroupAttribute string `json:"GroupAttribute"`
|
||||
}
|
||||
|
||||
// Settings represents the application settings.
|
||||
Settings struct {
|
||||
LogoURL string `json:"LogoURL"`
|
||||
|
@ -581,6 +589,7 @@ type (
|
|||
LDAPService interface {
|
||||
AuthenticateUser(username, password string, settings *LDAPSettings) error
|
||||
TestConnectivity(settings *LDAPSettings) error
|
||||
GetUserGroups(username string, settings *LDAPSettings) ([]string, error)
|
||||
}
|
||||
|
||||
// SwarmStackManager represents a service to manage Swarm stacks.
|
||||
|
@ -602,7 +611,7 @@ const (
|
|||
// APIVersion is the version number of the Portainer API.
|
||||
APIVersion = "1.18.2-dev"
|
||||
// DBVersion is the version number of the Portainer database.
|
||||
DBVersion = 12
|
||||
DBVersion = 13
|
||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
||||
PortainerAgentHeader = "Portainer-Agent"
|
||||
// PortainerAgentTargetHeader represent the name of the header containing the target node name.
|
||||
|
|
|
@ -2897,6 +2897,21 @@ definitions:
|
|||
type: "string"
|
||||
example: "uid"
|
||||
description: "LDAP attribute which denotes the username"
|
||||
LDAPGroupSearchSettings:
|
||||
type: "object"
|
||||
properties:
|
||||
GroupBaseDN:
|
||||
type: "string"
|
||||
example: "dc=ldap,dc=domain,dc=tld"
|
||||
description: "The distinguished name of the element from which the LDAP server will search for groups."
|
||||
GroupFilter:
|
||||
type: "string"
|
||||
example: "(objectClass=account)"
|
||||
description: "The LDAP search filter used to select group elements, optional."
|
||||
GroupAttribute:
|
||||
type: "string"
|
||||
example: "member"
|
||||
description: "LDAP attribute which denotes the group membership."
|
||||
|
||||
LDAPSettings:
|
||||
type: "object"
|
||||
|
@ -2923,6 +2938,10 @@ definitions:
|
|||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/LDAPSearchSettings"
|
||||
GroupSearchSettings:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/LDAPGroupSearchSettings"
|
||||
|
||||
Settings:
|
||||
type: "object"
|
||||
|
|
|
@ -3,6 +3,7 @@ function LDAPSettingsViewModel(data) {
|
|||
this.Password = data.Password;
|
||||
this.URL = data.URL;
|
||||
this.SearchSettings = data.SearchSettings;
|
||||
this.GroupSearchSettings = data.GroupSearchSettings;
|
||||
}
|
||||
|
||||
function LDAPSearchSettings(BaseDN, UsernameAttribute, Filter) {
|
||||
|
@ -10,3 +11,9 @@ function LDAPSearchSettings(BaseDN, UsernameAttribute, Filter) {
|
|||
this.UsernameAttribute = UsernameAttribute;
|
||||
this.Filter = Filter;
|
||||
}
|
||||
|
||||
function LDAPGroupSearchSettings(GroupBaseDN, GroupAttribute, GroupFilter) {
|
||||
this.GroupBaseDN = GroupBaseDN;
|
||||
this.GroupAttribute = GroupAttribute;
|
||||
this.GroupFilter = GroupFilter;
|
||||
}
|
||||
|
|
|
@ -49,10 +49,10 @@
|
|||
</div>
|
||||
<div class="form-group" ng-if="settings.AuthenticationMethod === 2">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
When using LDAP authentication, Portainer will delegate user authentication to a LDAP server (exception for the <b>admin</b> user that always uses internal authentication).
|
||||
When using LDAP authentication, Portainer will delegate user authentication to a LDAP server and fallback to internal authentication if LDAP authentication fails.
|
||||
<p style="margin-top:5px;">
|
||||
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
<u>Users still need to be created in Portainer beforehand.</u>
|
||||
<u>Portainer will create user(s) automatically with standard user role and assign them to team(s) which matches to LDAP group name(s).</u>
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -229,12 +229,66 @@
|
|||
|
||||
<div class="form-group">
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addSearchConfiguration()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add search configuration
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add user search configuration
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- !search-settings -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Group search configurations
|
||||
</div>
|
||||
|
||||
<!-- group-search-settings -->
|
||||
<div ng-repeat="groupConfig in LDAPSettings.GroupSearchSettings | limitTo: (1 - LDAPSettings.GroupSearchSettings)" style="margin-top: 5px;">
|
||||
|
||||
<div class="form-group" ng-if="$index > 0">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
Extra search configuration
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ldap_group_basedn_{{$index}}" class="col-sm-4 col-md-2 control-label text-left">
|
||||
Group Base DN
|
||||
<portainer-tooltip position="bottom" message="The distinguished name of the element from which the LDAP server will search for groups."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-8 col-md-4">
|
||||
<input type="text" class="form-control" id="ldap_group_basedn_{{$index}}" ng-model="groupConfig.GroupBaseDN" placeholder="dc=ldap,dc=domain,dc=tld">
|
||||
</div>
|
||||
|
||||
<label for="ldap_group_att_{{$index}}" class="col-sm-4 col-md-3 col-lg-2 margin-sm-top control-label text-left">
|
||||
Group Membership Attribute
|
||||
<portainer-tooltip position="bottom" message="LDAP attribute which denotes the group membership."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-8 col-md-3 col-lg-4 margin-sm-top">
|
||||
<input type="text" class="form-control" id="ldap_group_att_{{$index}}" ng-model="groupConfig.GroupAttribute" placeholder="member">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ldap_group_filter_{{$index}}" class="col-sm-4 col-md-2 control-label text-left">
|
||||
Group Filter
|
||||
<portainer-tooltip position="bottom" message="The LDAP search filter used to select group elements, optional."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-7 col-md-9">
|
||||
<input type="text" class="form-control" id="ldap_group_filter_{{$index}}" ng-model="groupConfig.GroupFilter" placeholder="(objectClass=account)">
|
||||
</div>
|
||||
<div class="col-sm-1" ng-if="$index > 0">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeGroupSearchConfiguration($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addGroupSearchConfiguration()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add group search configuration
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- !group-search-settings -->
|
||||
</div>
|
||||
|
||||
<!-- actions -->
|
||||
|
|
|
@ -21,6 +21,14 @@ function ($q, $scope, Notifications, SettingsService, FileUploadService) {
|
|||
$scope.removeSearchConfiguration = function(index) {
|
||||
$scope.LDAPSettings.SearchSettings.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addGroupSearchConfiguration = function() {
|
||||
$scope.LDAPSettings.GroupSearchSettings.push({ GroupBaseDN: '', GroupAttribute: '', GroupFilter: '' });
|
||||
};
|
||||
|
||||
$scope.removeGroupSearchConfiguration = function(index) {
|
||||
$scope.LDAPSettings.GroupSearchSettings.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.LDAPConnectivityCheck = function() {
|
||||
var settings = $scope.settings;
|
||||
|
|
Loading…
Reference in New Issue