feat: enhance LDAP integration with new configuration options and browsing capabilities

This commit is contained in:
Björn Blomberg
2026-04-14 16:07:52 +02:00
parent d6fd50e511
commit 61f33d350a
6 changed files with 631 additions and 163 deletions

View File

@@ -73,16 +73,12 @@ func (m *Manager) Login(database *db.DB, username, password string) (token, role
cfg := m.cfg
m.mu.RUnlock()
if cfg == nil || cfg.LDAP.Host == "" {
return "", "", errors.New("invalid credentials")
}
if err := ldapBind(cfg.LDAP.Host, cfg.LDAP.Port, cfg.LDAP.BindDN, cfg.LDAP.BindPassword); err != nil {
if cfg == nil || cfg.LDAP.Url == "" {
return "", "", errors.New("invalid credentials")
}
if err := ldapUserBind(cfg, username, password); err != nil {
return "", "", errors.New("invalid credentials")
return "", "", err
}
tok, err := m.createSession(username, "user")
@@ -90,44 +86,104 @@ func (m *Manager) Login(database *db.DB, username, password string) (token, role
}
// TestLDAP tests an LDAP configuration by performing a service-account bind.
func TestLDAP(host string, port int, bindDN, bindPassword string) error {
return ldapBind(host, port, bindDN, bindPassword)
}
func ldapBind(host string, port int, bindDN, bindPassword string) error {
l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
func TestLDAP(url string, adminUser, adminPassword string) error {
l, err := ldap.DialURL(url)
if err != nil {
return err
}
defer l.Close()
return l.Bind(bindDN, bindPassword)
if adminUser != "" {
return l.Bind(adminUser, adminPassword)
}
return nil
}
func ldapUserBind(cfg *config.Config, username, password string) error {
l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", cfg.LDAP.Host, cfg.LDAP.Port))
l, err := ldap.DialURL(cfg.LDAP.Url)
if err != nil {
return err
}
defer l.Close()
if err := l.Bind(cfg.LDAP.BindDN, cfg.LDAP.BindPassword); err != nil {
return err
if cfg.LDAP.AdminUser != "" {
if err := l.Bind(cfg.LDAP.AdminUser, cfg.LDAP.AdminPass); err != nil {
return err
}
}
sr, err := l.Search(&ldap.SearchRequest{
BaseDN: cfg.LDAP.BaseDN,
Filter: fmt.Sprintf("(uid=%s)", ldap.EscapeFilter(username)),
Scope: ldap.ScopeWholeSubtree,
})
searchRequest := ldap.NewSearchRequest(
"", // Search from root or a specific BaseDN if needed. Assuming root here since URL can contain it, or we rely on LDAP configured properly
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(|(uid=%s)(sAMAccountName=%s))", ldap.EscapeFilter(username), ldap.EscapeFilter(username)),
[]string{"dn"},
nil,
)
sr, err := l.Search(searchRequest)
if err != nil {
return err
}
if len(sr.Entries) != 1 {
if len(sr.Entries) == 0 {
return errors.New("user not found in LDAP")
}
// Bind to the exact DN of the user discovered
return l.Bind(sr.Entries[0].DN, password)
}
// BrowseLDAP returns users and groups from the LDAP server.
func BrowseLDAP(url string, adminUser, adminPassword string) ([]string, []string, error) {
l, err := ldap.DialURL(url)
if err != nil {
return nil, nil, err
}
defer l.Close()
if adminUser != "" {
if err := l.Bind(adminUser, adminPassword); err != nil {
return nil, nil, err
}
}
// Search for Users
userReq := ldap.NewSearchRequest(
"", ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
"(|(objectClass=person)(objectClass=user))",
[]string{"uid", "sAMAccountName", "cn"},
nil,
)
var users []string
if sr, err := l.Search(userReq); err == nil {
for _, e := range sr.Entries {
if v := e.GetAttributeValue("sAMAccountName"); v != "" {
users = append(users, v)
} else if v := e.GetAttributeValue("uid"); v != "" {
users = append(users, v)
} else if v := e.GetAttributeValue("cn"); v != "" {
users = append(users, v)
}
}
}
// Search for Groups
groupReq := ldap.NewSearchRequest(
"", ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
"(|(objectClass=groupOfNames)(objectClass=group))",
[]string{"cn"},
nil,
)
var groups []string
if sr, err := l.Search(groupReq); err == nil {
for _, e := range sr.Entries {
if v := e.GetAttributeValue("cn"); v != "" {
groups = append(groups, v)
}
}
}
return users, groups, nil
}
func (m *Manager) createSession(username, role string) (string, error) {
token, err := generateToken()
if err != nil {