LDAP user synchronization (#1478)

This commit is contained in:
Lauris BH 2017-05-10 16:10:18 +03:00 committed by Kim "BKC" Carlbäcker
parent fd76f090a2
commit 524885dd65
15 changed files with 356 additions and 52 deletions

View file

@ -28,6 +28,7 @@ type AuthenticationForm struct {
Filter string
AdminFilter string
IsActive bool
IsSyncEnabled bool
SMTPAuth string
SMTPHost string
SMTPPort int

View file

@ -47,6 +47,15 @@ type Source struct {
Enabled bool // if this source is disabled
}
// SearchResult : user data
type SearchResult struct {
Username string // Username
Name string // Name
Surname string // Surname
Mail string // E-mail address
IsAdmin bool // if user is administrator
}
func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
// See http://tools.ietf.org/search/rfc4515
badCharacters := "\x00()*\\"
@ -149,18 +158,39 @@ func bindUser(l *ldap.Conn, userDN, passwd string) error {
return err
}
func checkAdmin(l *ldap.Conn, ls *Source, userDN string) bool {
if len(ls.AdminFilter) > 0 {
log.Trace("Checking admin with filter %s and base %s", ls.AdminFilter, userDN)
search := ldap.NewSearchRequest(
userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ls.AdminFilter,
[]string{ls.AttributeName},
nil)
sr, err := l.Search(search)
if err != nil {
log.Error(4, "LDAP Admin Search failed unexpectedly! (%v)", err)
} else if len(sr.Entries) < 1 {
log.Error(4, "LDAP Admin Search failed")
} else {
return true
}
}
return false
}
// SearchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter
func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, string, string, string, bool, bool) {
func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResult {
// See https://tools.ietf.org/search/rfc4513#section-5.1.2
if len(passwd) == 0 {
log.Debug("Auth. failed for %s, password cannot be empty")
return "", "", "", "", false, false
return nil
}
l, err := dial(ls)
if err != nil {
log.Error(4, "LDAP Connect error, %s:%v", ls.Host, err)
ls.Enabled = false
return "", "", "", "", false, false
return nil
}
defer l.Close()
@ -171,7 +201,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
var ok bool
userDN, ok = ls.sanitizedUserDN(name)
if !ok {
return "", "", "", "", false, false
return nil
}
} else {
log.Trace("LDAP will use BindDN.")
@ -179,7 +209,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
var found bool
userDN, found = ls.findUserDN(l, name)
if !found {
return "", "", "", "", false, false
return nil
}
}
@ -187,13 +217,13 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
// binds user (checking password) before looking-up attributes in user context
err = bindUser(l, userDN, passwd)
if err != nil {
return "", "", "", "", false, false
return nil
}
}
userFilter, ok := ls.sanitizedUserQuery(name)
if !ok {
return "", "", "", "", false, false
return nil
}
log.Trace("Fetching attributes '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, userFilter, userDN)
@ -205,7 +235,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
sr, err := l.Search(search)
if err != nil {
log.Error(4, "LDAP Search failed unexpectedly! (%v)", err)
return "", "", "", "", false, false
return nil
} else if len(sr.Entries) < 1 {
if directBind {
log.Error(4, "User filter inhibited user login.")
@ -213,39 +243,78 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
log.Error(4, "LDAP Search failed unexpectedly! (0 entries)")
}
return "", "", "", "", false, false
return nil
}
username := sr.Entries[0].GetAttributeValue(ls.AttributeUsername)
firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName)
surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail)
isAdmin := false
if len(ls.AdminFilter) > 0 {
log.Trace("Checking admin with filter %s and base %s", ls.AdminFilter, userDN)
search = ldap.NewSearchRequest(
userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ls.AdminFilter,
[]string{ls.AttributeName},
nil)
sr, err = l.Search(search)
if err != nil {
log.Error(4, "LDAP Admin Search failed unexpectedly! (%v)", err)
} else if len(sr.Entries) < 1 {
log.Error(4, "LDAP Admin Search failed")
} else {
isAdmin = true
}
}
isAdmin := checkAdmin(l, ls, userDN)
if !directBind && ls.AttributesInBind {
// binds user (checking password) after looking-up attributes in BindDN context
err = bindUser(l, userDN, passwd)
if err != nil {
return "", "", "", "", false, false
return nil
}
}
return username, firstname, surname, mail, isAdmin, true
return &SearchResult{
Username: username,
Name: firstname,
Surname: surname,
Mail: mail,
IsAdmin: isAdmin,
}
}
// SearchEntries : search an LDAP source for all users matching userFilter
func (ls *Source) SearchEntries() []*SearchResult {
l, err := dial(ls)
if err != nil {
log.Error(4, "LDAP Connect error, %s:%v", ls.Host, err)
ls.Enabled = false
return nil
}
defer l.Close()
if ls.BindDN != "" && ls.BindPassword != "" {
err := l.Bind(ls.BindDN, ls.BindPassword)
if err != nil {
log.Debug("Failed to bind as BindDN[%s]: %v", ls.BindDN, err)
return nil
}
log.Trace("Bound as BindDN %s", ls.BindDN)
} else {
log.Trace("Proceeding with anonymous LDAP search.")
}
userFilter := fmt.Sprintf(ls.Filter, "*")
log.Trace("Fetching attributes '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, userFilter, ls.UserBase)
search := ldap.NewSearchRequest(
ls.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
[]string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail},
nil)
sr, err := l.Search(search)
if err != nil {
log.Error(4, "LDAP Search failed unexpectedly! (%v)", err)
return nil
}
result := make([]*SearchResult, len(sr.Entries))
for i, v := range sr.Entries {
result[i] = &SearchResult{
Username: v.GetAttributeValue(ls.AttributeUsername),
Name: v.GetAttributeValue(ls.AttributeName),
Surname: v.GetAttributeValue(ls.AttributeSurname),
Mail: v.GetAttributeValue(ls.AttributeMail),
IsAdmin: checkAdmin(l, ls, v.DN),
}
}
return result
}

View file

@ -66,6 +66,17 @@ func NewContext() {
go models.DeleteOldRepositoryArchives()
}
}
if setting.Cron.SyncExternalUsers.Enabled {
entry, err = c.AddFunc("Synchronize external users", setting.Cron.SyncExternalUsers.Schedule, models.SyncExternalUsers)
if err != nil {
log.Fatal(4, "Cron[Synchronize external users]: %v", err)
}
if setting.Cron.SyncExternalUsers.RunAtStart {
entry.Prev = time.Now()
entry.ExecTimes++
go models.SyncExternalUsers()
}
}
c.Start()
}

View file

@ -336,6 +336,12 @@ var (
Schedule string
OlderThan time.Duration
} `ini:"cron.archive_cleanup"`
SyncExternalUsers struct {
Enabled bool
RunAtStart bool
Schedule string
UpdateExisting bool
} `ini:"cron.sync_external_users"`
}{
UpdateMirror: struct {
Enabled bool
@ -379,6 +385,17 @@ var (
Schedule: "@every 24h",
OlderThan: 24 * time.Hour,
},
SyncExternalUsers: struct {
Enabled bool
RunAtStart bool
Schedule string
UpdateExisting bool
}{
Enabled: true,
RunAtStart: false,
Schedule: "@every 24h",
UpdateExisting: true,
},
}
// Git settings