mirror of
https://github.com/h44z/wg-portal.git
synced 2025-09-13 14:31:15 +00:00
WIP: new user management and authentication system, use go 1.16 embed
This commit is contained in:
@@ -1,94 +0,0 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
type Authentication struct {
|
||||
Cfg *Config
|
||||
}
|
||||
|
||||
func NewAuthentication(config Config) Authentication {
|
||||
a := Authentication{
|
||||
Cfg: &config,
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func (a Authentication) open() (*ldap.Conn, error) {
|
||||
conn, err := ldap.DialURL(a.Cfg.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if a.Cfg.StartTLS {
|
||||
// Reconnect with TLS
|
||||
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = conn.Bind(a.Cfg.BindUser, a.Cfg.BindPass)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (a Authentication) close(conn *ldap.Conn) {
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (a Authentication) CheckLogin(username, password string) bool {
|
||||
return a.CheckCustomLogin("sAMAccountName", username, password)
|
||||
}
|
||||
|
||||
func (a Authentication) CheckCustomLogin(userIdentifier, username, password string) bool {
|
||||
client, err := a.open()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer a.close(client)
|
||||
|
||||
// Search for the given username
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
a.Cfg.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
fmt.Sprintf("(&(objectClass=organizationalPerson)(%s=%s))", userIdentifier, username),
|
||||
[]string{"dn", "userAccountControl"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := client.Search(searchRequest)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(sr.Entries) != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
userDN := sr.Entries[0].DN
|
||||
|
||||
// Check if user is disabled, if so deny login
|
||||
uac := sr.Entries[0].GetAttributeValue("userAccountControl")
|
||||
if uac != "" && IsLdapUserDisabled(uac) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Bind as the user to verify their password
|
||||
err = client.Bind(userDN, password)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
27
internal/ldap/config.go
Normal file
27
internal/ldap/config.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package ldap
|
||||
|
||||
type Type string
|
||||
|
||||
const (
|
||||
TypeActiveDirectory Type = "AD"
|
||||
TypeOpenLDAP Type = "OpenLDAP"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
URL string `yaml:"url" envconfig:"LDAP_URL"`
|
||||
StartTLS bool `yaml:"startTLS" envconfig:"LDAP_STARTTLS"`
|
||||
BaseDN string `yaml:"dn" envconfig:"LDAP_BASEDN"`
|
||||
BindUser string `yaml:"user" envconfig:"LDAP_USER"`
|
||||
BindPass string `yaml:"pass" envconfig:"LDAP_PASSWORD"`
|
||||
|
||||
Type Type `yaml:"typ" envconfig:"LDAP_TYPE"` // AD for active directory, OpenLDAP for OpenLDAP
|
||||
UserClass string `yaml:"userClass" envconfig:"LDAP_USER_CLASS"`
|
||||
EmailAttribute string `yaml:"attrEmail" envconfig:"LDAP_ATTR_EMAIL"`
|
||||
FirstNameAttribute string `yaml:"attrFirstname" envconfig:"LDAP_ATTR_FIRSTNAME"`
|
||||
LastNameAttribute string `yaml:"attrLastname" envconfig:"LDAP_ATTR_LASTNAME"`
|
||||
PhoneAttribute string `yaml:"attrPhone" envconfig:"LDAP_ATTR_PHONE"`
|
||||
GroupMemberAttribute string `yaml:"attrGroups" envconfig:"LDAP_ATTR_GROUPS"`
|
||||
DisabledAttribute string `yaml:"attrDisabled" envconfig:"LDAP_ATTR_DISABLED"`
|
||||
|
||||
AdminLdapGroup string `yaml:"adminGroup" envconfig:"LDAP_ADMIN_GROUP"`
|
||||
}
|
@@ -1,9 +1,112 @@
|
||||
package ldap
|
||||
|
||||
type Config struct {
|
||||
URL string `yaml:"url" envconfig:"LDAP_URL"`
|
||||
StartTLS bool `yaml:"startTLS" envconfig:"LDAP_STARTTLS"`
|
||||
BaseDN string `yaml:"dn" envconfig:"LDAP_BASEDN"`
|
||||
BindUser string `yaml:"user" envconfig:"LDAP_USER"`
|
||||
BindPass string `yaml:"pass" envconfig:"LDAP_PASSWORD"`
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type RawLdapData struct {
|
||||
DN string
|
||||
Attributes map[string]string
|
||||
RawAttributes map[string][][]byte
|
||||
}
|
||||
|
||||
func Open(cfg *Config) (*ldap.Conn, error) {
|
||||
conn, err := ldap.DialURL(cfg.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cfg.StartTLS {
|
||||
// Reconnect with TLS
|
||||
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = conn.Bind(cfg.BindUser, cfg.BindPass)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func Close(conn *ldap.Conn) {
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func FindAllUsers(cfg *Config) ([]RawLdapData, error) {
|
||||
client, err := Open(cfg)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to open ldap connection")
|
||||
}
|
||||
defer Close(client)
|
||||
|
||||
// Search all users
|
||||
attrs := []string{"dn", cfg.EmailAttribute, cfg.EmailAttribute, cfg.FirstNameAttribute, cfg.LastNameAttribute,
|
||||
cfg.PhoneAttribute, cfg.GroupMemberAttribute}
|
||||
if cfg.DisabledAttribute != "" {
|
||||
attrs = append(attrs, cfg.DisabledAttribute)
|
||||
}
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
cfg.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
fmt.Sprintf("(objectClass=%s)", cfg.UserClass), attrs, nil,
|
||||
)
|
||||
|
||||
sr, err := client.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to search in ldap")
|
||||
}
|
||||
|
||||
tmpData := make([]RawLdapData, 0, len(sr.Entries))
|
||||
|
||||
for _, entry := range sr.Entries {
|
||||
tmp := RawLdapData{
|
||||
DN: entry.DN,
|
||||
Attributes: make(map[string]string, len(attrs)),
|
||||
RawAttributes: make(map[string][][]byte, len(attrs)),
|
||||
}
|
||||
|
||||
for _, field := range attrs {
|
||||
tmp.Attributes[field] = entry.GetAttributeValue(field)
|
||||
tmp.RawAttributes[field] = entry.GetRawAttributeValues(field)
|
||||
}
|
||||
|
||||
tmpData = append(tmpData, tmp)
|
||||
}
|
||||
|
||||
return tmpData, nil
|
||||
}
|
||||
|
||||
func IsActiveDirectoryUserDisabled(userAccountControl string) bool {
|
||||
if userAccountControl == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
uacInt, err := strconv.Atoi(userAccountControl)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
if int32(uacInt)&0x2 != 0 {
|
||||
return true // bit 2 set means account is disabled
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func IsOpenLdapUserDisabled(pwdAccountLockedTime string) bool {
|
||||
if pwdAccountLockedTime != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
@@ -1,338 +0,0 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var Fields = []string{"givenName", "sn", "mail", "department", "memberOf", "sAMAccountName", "telephoneNumber",
|
||||
"mobile", "displayName", "cn", "title", "company", "manager", "streetAddress", "employeeID", "memberOf", "l",
|
||||
"st", "postalCode", "co", "facsimileTelephoneNumber", "pager", "thumbnailPhoto", "otherMobile",
|
||||
"extensionAttribute2", "distinguishedName", "userAccountControl"}
|
||||
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// Cache Data Store
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
type UserCacheHolder interface {
|
||||
Clear()
|
||||
SetAllUsers(users []RawLdapData)
|
||||
GetUser(dn string) *RawLdapData
|
||||
GetUsers() []*RawLdapData
|
||||
}
|
||||
|
||||
type RawLdapData struct {
|
||||
DN string
|
||||
Attributes map[string]string
|
||||
RawAttributes map[string][][]byte
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// Sample Cache Data store
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
type UserCacheHolderEntry struct {
|
||||
RawLdapData
|
||||
Username string
|
||||
Mail string
|
||||
Firstname string
|
||||
Lastname string
|
||||
Groups []string
|
||||
}
|
||||
|
||||
func (e *UserCacheHolderEntry) CalcFieldsFromAttributes() {
|
||||
e.Username = strings.ToLower(e.Attributes["sAMAccountName"])
|
||||
e.Mail = e.Attributes["mail"]
|
||||
e.Firstname = e.Attributes["givenName"]
|
||||
e.Lastname = e.Attributes["sn"]
|
||||
e.Groups = make([]string, len(e.RawAttributes["memberOf"]))
|
||||
for i, group := range e.RawAttributes["memberOf"] {
|
||||
e.Groups[i] = string(group)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *UserCacheHolderEntry) GetUID() string {
|
||||
return fmt.Sprintf("u%x", md5.Sum([]byte(e.Attributes["distinguishedName"])))
|
||||
}
|
||||
|
||||
type SynchronizedUserCacheHolder struct {
|
||||
users map[string]*UserCacheHolderEntry
|
||||
mux sync.RWMutex
|
||||
}
|
||||
|
||||
func (h *SynchronizedUserCacheHolder) Init() {
|
||||
h.users = make(map[string]*UserCacheHolderEntry)
|
||||
}
|
||||
|
||||
func (h *SynchronizedUserCacheHolder) Clear() {
|
||||
h.mux.Lock()
|
||||
defer h.mux.Unlock()
|
||||
|
||||
h.users = make(map[string]*UserCacheHolderEntry)
|
||||
}
|
||||
|
||||
func (h *SynchronizedUserCacheHolder) SetAllUsers(users []RawLdapData) {
|
||||
h.mux.Lock()
|
||||
defer h.mux.Unlock()
|
||||
|
||||
h.users = make(map[string]*UserCacheHolderEntry)
|
||||
|
||||
for i := range users {
|
||||
h.users[users[i].DN] = &UserCacheHolderEntry{RawLdapData: users[i]}
|
||||
h.users[users[i].DN].CalcFieldsFromAttributes()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SynchronizedUserCacheHolder) GetUser(dn string) *RawLdapData {
|
||||
h.mux.RLock()
|
||||
defer h.mux.RUnlock()
|
||||
|
||||
return &h.users[dn].RawLdapData
|
||||
}
|
||||
|
||||
func (h *SynchronizedUserCacheHolder) GetUserData(dn string) *UserCacheHolderEntry {
|
||||
h.mux.RLock()
|
||||
defer h.mux.RUnlock()
|
||||
|
||||
return h.users[dn]
|
||||
}
|
||||
|
||||
func (h *SynchronizedUserCacheHolder) GetUsers() []*RawLdapData {
|
||||
h.mux.RLock()
|
||||
defer h.mux.RUnlock()
|
||||
|
||||
users := make([]*RawLdapData, 0, len(h.users))
|
||||
for _, user := range h.users {
|
||||
users = append(users, &user.RawLdapData)
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
func (h *SynchronizedUserCacheHolder) GetSortedUsers(sortKey string, sortDirection string) []*UserCacheHolderEntry {
|
||||
h.mux.RLock()
|
||||
defer h.mux.RUnlock()
|
||||
|
||||
sortedUsers := make([]*UserCacheHolderEntry, 0, len(h.users))
|
||||
|
||||
for _, user := range h.users {
|
||||
sortedUsers = append(sortedUsers, user)
|
||||
}
|
||||
|
||||
sort.Slice(sortedUsers, func(i, j int) bool {
|
||||
if sortDirection == "asc" {
|
||||
return sortedUsers[i].Attributes[sortKey] < sortedUsers[j].Attributes[sortKey]
|
||||
} else {
|
||||
return sortedUsers[i].Attributes[sortKey] > sortedUsers[j].Attributes[sortKey]
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return sortedUsers
|
||||
|
||||
}
|
||||
|
||||
func (h *SynchronizedUserCacheHolder) IsInGroup(username, gid string) bool {
|
||||
userDN := h.GetUserDN(username)
|
||||
if userDN == "" {
|
||||
return false // user not found -> not in group
|
||||
}
|
||||
|
||||
user := h.GetUserData(userDN)
|
||||
if user == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, group := range user.Groups {
|
||||
if group == gid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *SynchronizedUserCacheHolder) UserExists(username string) bool {
|
||||
userDN := h.GetUserDN(username)
|
||||
if userDN == "" {
|
||||
return false // user not found
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *SynchronizedUserCacheHolder) GetUserDN(username string) string {
|
||||
userDN := ""
|
||||
for dn, user := range h.users {
|
||||
accName := strings.ToLower(user.Attributes["sAMAccountName"])
|
||||
if accName == username {
|
||||
userDN = dn
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return userDN
|
||||
}
|
||||
|
||||
func (h *SynchronizedUserCacheHolder) GetUserDNByMail(mail string) string {
|
||||
userDN := ""
|
||||
for dn, user := range h.users {
|
||||
accMail := strings.ToLower(user.Attributes["mail"])
|
||||
if accMail == mail {
|
||||
userDN = dn
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return userDN
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
// Cache Handler, LDAP interaction
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
type UserCache struct {
|
||||
Cfg *Config
|
||||
LastError error
|
||||
UpdatedAt time.Time
|
||||
userData UserCacheHolder
|
||||
}
|
||||
|
||||
func NewUserCache(config Config, store UserCacheHolder) *UserCache {
|
||||
uc := &UserCache{
|
||||
Cfg: &config,
|
||||
UpdatedAt: time.Now(),
|
||||
userData: store,
|
||||
}
|
||||
|
||||
logrus.Infof("Filling user cache...")
|
||||
err := uc.Update(true, true)
|
||||
logrus.Infof("User cache filled!")
|
||||
uc.LastError = err
|
||||
|
||||
return uc
|
||||
}
|
||||
|
||||
func (u UserCache) open() (*ldap.Conn, error) {
|
||||
conn, err := ldap.DialURL(u.Cfg.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u.Cfg.StartTLS {
|
||||
// Reconnect with TLS
|
||||
err = conn.StartTLS(&tls.Config{InsecureSkipVerify: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = conn.Bind(u.Cfg.BindUser, u.Cfg.BindPass)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (u UserCache) close(conn *ldap.Conn) {
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Update updates the user cache in background, minimal locking will happen
|
||||
func (u *UserCache) Update(filter, withDisabledUsers bool) error {
|
||||
logrus.Debugf("Updating ldap cache...")
|
||||
client, err := u.open()
|
||||
if err != nil {
|
||||
u.LastError = err
|
||||
return err
|
||||
}
|
||||
defer u.close(client)
|
||||
|
||||
// Search for the given username
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
u.Cfg.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
"(objectClass=organizationalPerson)",
|
||||
Fields,
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := client.Search(searchRequest)
|
||||
if err != nil {
|
||||
u.LastError = err
|
||||
return err
|
||||
}
|
||||
|
||||
tmpData := make([]RawLdapData, 0, len(sr.Entries))
|
||||
|
||||
for _, entry := range sr.Entries {
|
||||
if filter {
|
||||
usernameAttr := strings.ToLower(entry.GetAttributeValue("sAMAccountName"))
|
||||
firstNameAttr := entry.GetAttributeValue("givenName")
|
||||
lastNameAttr := entry.GetAttributeValue("sn")
|
||||
mailAttr := entry.GetAttributeValue("mail")
|
||||
userAccountControl := entry.GetAttributeValue("userAccountControl")
|
||||
employeeID := entry.GetAttributeValue("employeeID")
|
||||
dn := entry.GetAttributeValue("distinguishedName")
|
||||
|
||||
if usernameAttr == "" || firstNameAttr == "" || lastNameAttr == "" || mailAttr == "" || employeeID == "" {
|
||||
continue // prefilter...
|
||||
}
|
||||
|
||||
if !withDisabledUsers && userAccountControl != "" && IsLdapUserDisabled(userAccountControl) {
|
||||
continue
|
||||
}
|
||||
|
||||
if entry.DN != dn {
|
||||
logrus.Errorf("LDAP inconsistent: '%s' != '%s'", entry.DN, dn)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
tmp := RawLdapData{
|
||||
DN: entry.DN,
|
||||
Attributes: make(map[string]string, len(Fields)),
|
||||
RawAttributes: make(map[string][][]byte, len(Fields)),
|
||||
}
|
||||
|
||||
for _, field := range Fields {
|
||||
tmp.Attributes[field] = entry.GetAttributeValue(field)
|
||||
tmp.RawAttributes[field] = entry.GetRawAttributeValues(field)
|
||||
}
|
||||
|
||||
tmpData = append(tmpData, tmp)
|
||||
}
|
||||
|
||||
// Copy to userdata
|
||||
u.userData.SetAllUsers(tmpData)
|
||||
u.UpdatedAt = time.Now()
|
||||
u.LastError = nil
|
||||
|
||||
logrus.Debug("Ldap cache updated...")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsLdapUserDisabled(userAccountControl string) bool {
|
||||
uacInt, err := strconv.Atoi(userAccountControl)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
if int32(uacInt)&0x2 != 0 {
|
||||
return true // bit 2 set means account is disabled
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
Reference in New Issue
Block a user