package auth import ( "context" "encoding/json" "fmt" "strings" "github.com/go-ldap/ldap/v3" "github.com/h44z/wg-portal/internal" "github.com/h44z/wg-portal/internal/config" "github.com/h44z/wg-portal/internal/domain" "github.com/sirupsen/logrus" ) type LdapAuthenticator struct { cfg *config.LdapProvider } func newLdapAuthenticator(_ context.Context, cfg *config.LdapProvider) (*LdapAuthenticator, error) { var provider = &LdapAuthenticator{} provider.cfg = cfg dn, err := ldap.ParseDN(cfg.AdminGroupDN) if err != nil { return nil, fmt.Errorf("failed to parse admin group DN: %w", err) } provider.cfg.FieldMap = provider.getLdapFieldMapping(cfg.FieldMap) provider.cfg.ParsedAdminGroupDN = dn return provider, nil } func (l LdapAuthenticator) GetName() string { return l.cfg.ProviderName } func (l LdapAuthenticator) RegistrationEnabled() bool { return l.cfg.RegistrationEnabled } func (l LdapAuthenticator) PlaintextAuthentication(userId domain.UserIdentifier, plainPassword string) error { conn, err := internal.LdapConnect(l.cfg) if err != nil { return fmt.Errorf("failed to setup connection: %w", err) } defer internal.LdapDisconnect(conn) attrs := []string{"dn"} loginFilter := strings.Replace(l.cfg.LoginFilter, "{{login_identifier}}", string(userId), -1) searchRequest := ldap.NewSearchRequest( l.cfg.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 20, false, // 20 second time limit loginFilter, attrs, nil, ) sr, err := conn.Search(searchRequest) if err != nil { return fmt.Errorf("failed to search in ldap: %w", err) } if len(sr.Entries) == 0 { return domain.ErrNotFound } if len(sr.Entries) > 1 { return domain.ErrNotUnique } // Bind as the user to verify their password userDN := sr.Entries[0].DN err = conn.Bind(userDN, plainPassword) if err != nil { return fmt.Errorf("invalid credentials: %w", err) } _ = conn.Unbind() return nil } func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIdentifier) ( map[string]interface{}, error, ) { conn, err := internal.LdapConnect(l.cfg) if err != nil { return nil, fmt.Errorf("failed to setup connection: %w", err) } defer internal.LdapDisconnect(conn) attrs := internal.LdapSearchAttributes(&l.cfg.FieldMap) loginFilter := strings.Replace(l.cfg.LoginFilter, "{{login_identifier}}", string(userId), -1) searchRequest := ldap.NewSearchRequest( l.cfg.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 20, false, // 20 second time limit loginFilter, attrs, nil, ) sr, err := conn.Search(searchRequest) if err != nil { return nil, fmt.Errorf("failed to search in ldap: %w", err) } if len(sr.Entries) == 0 { return nil, domain.ErrNotFound } if len(sr.Entries) > 1 { return nil, domain.ErrNotUnique } users := internal.LdapConvertEntries(sr, &l.cfg.FieldMap) if l.cfg.LogUserInfo { contents, _ := json.Marshal(users[0]) logrus.Tracef("User info from LDAP source %s for %s: %v", l.GetName(), userId, string(contents)) } return users[0], nil } func (l LdapAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) { isAdmin, err := internal.LdapIsMemberOf(raw[l.cfg.FieldMap.GroupMembership].([][]byte), l.cfg.ParsedAdminGroupDN) if err != nil { return nil, fmt.Errorf("failed to check admin group: %w", err) } userInfo := &domain.AuthenticatorUserInfo{ Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, l.cfg.FieldMap.UserIdentifier, "")), Email: internal.MapDefaultString(raw, l.cfg.FieldMap.Email, ""), Firstname: internal.MapDefaultString(raw, l.cfg.FieldMap.Firstname, ""), Lastname: internal.MapDefaultString(raw, l.cfg.FieldMap.Lastname, ""), Phone: internal.MapDefaultString(raw, l.cfg.FieldMap.Phone, ""), Department: internal.MapDefaultString(raw, l.cfg.FieldMap.Department, ""), IsAdmin: isAdmin, } return userInfo, nil } func (l LdapAuthenticator) getLdapFieldMapping(f config.LdapFields) config.LdapFields { defaultMap := config.LdapFields{ BaseFields: config.BaseFields{ UserIdentifier: "mail", Email: "mail", Firstname: "givenName", Lastname: "sn", Phone: "telephoneNumber", Department: "department", }, GroupMembership: "memberOf", } if f.UserIdentifier != "" { defaultMap.UserIdentifier = f.UserIdentifier } if f.Email != "" { defaultMap.Email = f.Email } if f.Firstname != "" { defaultMap.Firstname = f.Firstname } if f.Lastname != "" { defaultMap.Lastname = f.Lastname } if f.Phone != "" { defaultMap.Phone = f.Phone } if f.Department != "" { defaultMap.Department = f.Department } if f.GroupMembership != "" { defaultMap.GroupMembership = f.GroupMembership } return defaultMap }