mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-01-18 13:22:30 +00:00
feat: store ldap group results in cache
This commit is contained in:
@@ -21,9 +21,9 @@ func NewTinyauthCmdConfiguration() *config.Config {
|
|||||||
Address: "0.0.0.0",
|
Address: "0.0.0.0",
|
||||||
},
|
},
|
||||||
Auth: config.AuthConfig{
|
Auth: config.AuthConfig{
|
||||||
SessionExpiry: 3600,
|
SessionExpiry: 86400, // 1 day
|
||||||
SessionMaxLifetime: 0,
|
SessionMaxLifetime: 0, // disabled
|
||||||
LoginTimeout: 300,
|
LoginTimeout: 300, // 5 minutes
|
||||||
LoginMaxRetries: 3,
|
LoginMaxRetries: 3,
|
||||||
},
|
},
|
||||||
UI: config.UIConfig{
|
UI: config.UIConfig{
|
||||||
@@ -32,8 +32,9 @@ func NewTinyauthCmdConfiguration() *config.Config {
|
|||||||
BackgroundImage: "/background.jpg",
|
BackgroundImage: "/background.jpg",
|
||||||
},
|
},
|
||||||
Ldap: config.LdapConfig{
|
Ldap: config.LdapConfig{
|
||||||
Insecure: false,
|
Insecure: false,
|
||||||
SearchFilter: "(uid=%s)",
|
SearchFilter: "(uid=%s)",
|
||||||
|
GroupCacheTTL: 900, // 15 minutes
|
||||||
},
|
},
|
||||||
Log: config.LogConfig{
|
Log: config.LogConfig{
|
||||||
Level: "info",
|
Level: "info",
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
LoginMaxRetries: app.config.Auth.LoginMaxRetries,
|
LoginMaxRetries: app.config.Auth.LoginMaxRetries,
|
||||||
SessionCookieName: app.context.sessionCookieName,
|
SessionCookieName: app.context.sessionCookieName,
|
||||||
IP: app.config.Auth.IP,
|
IP: app.config.Auth.IP,
|
||||||
|
LDAPGroupsCacheTTL: app.config.Ldap.GroupCacheTTL,
|
||||||
}, dockerService, services.ldapService, queries)
|
}, dockerService, services.ldapService, queries)
|
||||||
|
|
||||||
err = authService.Init()
|
err = authService.Init()
|
||||||
|
|||||||
@@ -67,14 +67,15 @@ type UIConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LdapConfig struct {
|
type LdapConfig struct {
|
||||||
Address string `description:"LDAP server address." yaml:"address"`
|
Address string `description:"LDAP server address." yaml:"address"`
|
||||||
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"`
|
BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"`
|
||||||
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"`
|
BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"`
|
||||||
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"`
|
BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"`
|
||||||
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"`
|
Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"`
|
||||||
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
|
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
|
||||||
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert"`
|
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert"`
|
||||||
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"`
|
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"`
|
||||||
|
GroupCacheTTL int `description:"Cache duration for LDAP group membership in seconds." yaml:"groupCacheTTL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogConfig struct {
|
type LogConfig struct {
|
||||||
|
|||||||
@@ -67,9 +67,16 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
goto basic
|
goto basic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if userSearch.Type != cookie.Provider {
|
||||||
|
tlog.App.Warn().Msg("User type from session cookie does not match user search type")
|
||||||
|
m.auth.DeleteSessionCookie(c)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var ldapGroups []string
|
var ldapGroups []string
|
||||||
|
|
||||||
if userSearch.Type == "ldap" {
|
if cookie.Provider == "ldap" {
|
||||||
ldapUser, err := m.auth.GetLdapUser(userSearch.Username)
|
ldapUser, err := m.auth.GetLdapUser(userSearch.Username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -86,7 +93,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
Username: cookie.Username,
|
Username: cookie.Username,
|
||||||
Name: cookie.Name,
|
Name: cookie.Name,
|
||||||
Email: cookie.Email,
|
Email: cookie.Email,
|
||||||
Provider: userSearch.Type,
|
Provider: cookie.Provider,
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
LdapGroups: strings.Join(ldapGroups, ","),
|
LdapGroups: strings.Join(ldapGroups, ","),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type LdapGroupsCache struct {
|
||||||
|
Groups []string
|
||||||
|
Expires time.Time
|
||||||
|
}
|
||||||
|
|
||||||
type LoginAttempt struct {
|
type LoginAttempt struct {
|
||||||
FailedAttempts int
|
FailedAttempts int
|
||||||
LastAttempt time.Time
|
LastAttempt time.Time
|
||||||
@@ -36,24 +41,28 @@ type AuthServiceConfig struct {
|
|||||||
LoginMaxRetries int
|
LoginMaxRetries int
|
||||||
SessionCookieName string
|
SessionCookieName string
|
||||||
IP config.IPConfig
|
IP config.IPConfig
|
||||||
|
LDAPGroupsCacheTTL int
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthService struct {
|
type AuthService struct {
|
||||||
config AuthServiceConfig
|
config AuthServiceConfig
|
||||||
docker *DockerService
|
docker *DockerService
|
||||||
loginAttempts map[string]*LoginAttempt
|
loginAttempts map[string]*LoginAttempt
|
||||||
loginMutex sync.RWMutex
|
ldapGroupsCache map[string]*LdapGroupsCache
|
||||||
ldap *LdapService
|
loginMutex sync.RWMutex
|
||||||
queries *repository.Queries
|
ldapGroupsMutex sync.RWMutex
|
||||||
|
ldap *LdapService
|
||||||
|
queries *repository.Queries
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries) *AuthService {
|
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries) *AuthService {
|
||||||
return &AuthService{
|
return &AuthService{
|
||||||
config: config,
|
config: config,
|
||||||
docker: docker,
|
docker: docker,
|
||||||
loginAttempts: make(map[string]*LoginAttempt),
|
loginAttempts: make(map[string]*LoginAttempt),
|
||||||
ldap: ldap,
|
ldapGroupsCache: make(map[string]*LdapGroupsCache),
|
||||||
queries: queries,
|
ldap: ldap,
|
||||||
|
queries: queries,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,12 +141,30 @@ func (auth *AuthService) GetLocalUser(username string) config.User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) GetLdapUser(userDN string) (config.LdapUser, error) {
|
func (auth *AuthService) GetLdapUser(userDN string) (config.LdapUser, error) {
|
||||||
|
auth.ldapGroupsMutex.Lock()
|
||||||
|
entry, exists := auth.ldapGroupsCache[userDN]
|
||||||
|
auth.ldapGroupsMutex.Unlock()
|
||||||
|
|
||||||
|
if exists && time.Now().Before(entry.Expires) {
|
||||||
|
return config.LdapUser{
|
||||||
|
DN: userDN,
|
||||||
|
Groups: entry.Groups,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
groups, err := auth.ldap.GetUserGroups(userDN)
|
groups, err := auth.ldap.GetUserGroups(userDN)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config.LdapUser{}, err
|
return config.LdapUser{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auth.ldapGroupsMutex.Lock()
|
||||||
|
auth.ldapGroupsCache[userDN] = &LdapGroupsCache{
|
||||||
|
Groups: groups,
|
||||||
|
Expires: time.Now().Add(time.Duration(auth.config.LDAPGroupsCacheTTL) * time.Second),
|
||||||
|
}
|
||||||
|
auth.ldapGroupsMutex.Unlock()
|
||||||
|
|
||||||
return config.LdapUser{
|
return config.LdapUser{
|
||||||
DN: userDN,
|
DN: userDN,
|
||||||
Groups: groups,
|
Groups: groups,
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -148,11 +146,13 @@ func (ldap *LdapService) GetUserDN(username string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ldap *LdapService) GetUserGroups(userDN string) ([]string, error) {
|
func (ldap *LdapService) GetUserGroups(userDN string) ([]string, error) {
|
||||||
|
escapedUserDN := ldapgo.EscapeFilter(userDN)
|
||||||
|
|
||||||
searchRequest := ldapgo.NewSearchRequest(
|
searchRequest := ldapgo.NewSearchRequest(
|
||||||
ldap.config.BaseDN,
|
ldap.config.BaseDN,
|
||||||
ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false,
|
ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false,
|
||||||
"(objectclass=groupOfUniqueNames)",
|
fmt.Sprintf("(&(objectclass=groupOfUniqueNames)(uniquemember=%s))", escapedUserDN),
|
||||||
[]string{"uniquemember"},
|
[]string{"dn"},
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -167,22 +167,21 @@ func (ldap *LdapService) GetUserGroups(userDN string) ([]string, error) {
|
|||||||
groupDNs := []string{}
|
groupDNs := []string{}
|
||||||
|
|
||||||
for _, entry := range searchResult.Entries {
|
for _, entry := range searchResult.Entries {
|
||||||
memberAttributes := entry.GetAttributeValues("uniquemember")
|
groupDNs = append(groupDNs, entry.DN)
|
||||||
// no need to escape username here, if it's malicious it won't match anything
|
|
||||||
if slices.Contains(memberAttributes, userDN) {
|
|
||||||
groupDNs = append(groupDNs, entry.DN)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should work for most ldap providers?
|
|
||||||
groups := []string{}
|
groups := []string{}
|
||||||
|
|
||||||
for _, groupDN := range groupDNs {
|
// I guess it should work for most ldap providers
|
||||||
groupDN = strings.TrimPrefix(groupDN, "cn=")
|
for _, dn := range groupDNs {
|
||||||
parts := strings.SplitN(groupDN, ",", 2)
|
rdnParts, err := ldapgo.ParseDN(dn)
|
||||||
if len(parts) > 0 {
|
if err != nil {
|
||||||
groups = append(groups, parts[0])
|
return []string{}, err
|
||||||
}
|
}
|
||||||
|
if len(rdnParts.RDNs) == 0 || len(rdnParts.RDNs[0].Attributes) == 0 {
|
||||||
|
return []string{}, fmt.Errorf("invalid DN format: %s", dn)
|
||||||
|
}
|
||||||
|
groups = append(groups, rdnParts.RDNs[0].Attributes[0].Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups, nil
|
return groups, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user