feat: add ldap support (#232)

* feat: add ldap support

* feat: add insecure option for self-signed certificates

* fix: recognize ldap as a username provider

* test: fix tests

* feat: add configurable search filter

* fix: fix error message in ldap search result

* refactor: bot suggestions
This commit is contained in:
Stavros
2025-07-05 18:17:39 +03:00
committed by GitHub
parent 4524e3322c
commit 6ec8c9766c
12 changed files with 342 additions and 72 deletions

View File

@@ -7,6 +7,7 @@ import (
"sync"
"time"
"tinyauth/internal/docker"
"tinyauth/internal/ldap"
"tinyauth/internal/types"
"tinyauth/internal/utils"
@@ -22,9 +23,10 @@ type Auth struct {
LoginAttempts map[string]*types.LoginAttempt
LoginMutex sync.RWMutex
Store *sessions.CookieStore
LDAP *ldap.LDAP
}
func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth {
func NewAuth(config types.AuthConfig, docker *docker.Docker, ldap *ldap.LDAP) *Auth {
// Create cookie store
store := sessions.NewCookieStore([]byte(config.HMACSecret), []byte(config.EncryptionSecret))
@@ -42,6 +44,7 @@ func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth {
Docker: docker,
LoginAttempts: make(map[string]*types.LoginAttempt),
Store: store,
LDAP: ldap,
}
}
@@ -68,14 +71,97 @@ func (auth *Auth) GetSession(c *gin.Context) (*sessions.Session, error) {
return session, nil
}
func (auth *Auth) GetUser(username string) *types.User {
func (auth *Auth) SearchUser(username string) types.UserSearch {
// Loop through users and return the user if the username matches
for _, user := range auth.Config.Users {
if user.Username == username {
return &user
log.Debug().Str("username", username).Msg("Searching for user")
if auth.GetLocalUser(username).Username != "" {
log.Debug().Str("username", username).Msg("Found local user")
// If user found, return a user with the username and type "local"
return types.UserSearch{
Username: username,
Type: "local",
}
}
return nil
// If no user found, check LDAP
if auth.LDAP != nil {
log.Debug().Str("username", username).Msg("Checking LDAP for user")
userDN, err := auth.LDAP.Search(username)
if err != nil {
log.Warn().Err(err).Str("username", username).Msg("Failed to find user in LDAP")
return types.UserSearch{}
}
// If user found in LDAP, return a user with the DN as username
return types.UserSearch{
Username: userDN,
Type: "ldap",
}
}
return types.UserSearch{}
}
func (auth *Auth) VerifyUser(search types.UserSearch, password string) bool {
// Authenticate the user based on the type
switch search.Type {
case "local":
// Get local user
user := auth.GetLocalUser(search.Username)
// Check if password is correct
return auth.CheckPassword(user, password)
case "ldap":
// If LDAP is configured, bind to the LDAP server with the user DN and password
if auth.LDAP != nil {
log.Debug().Str("username", search.Username).Msg("Binding to LDAP for user authentication")
// Bind to the LDAP server
err := auth.LDAP.Bind(search.Username, password)
if err != nil {
log.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP")
return false
}
// If bind is successful, rebind with the LDAP bind user
err = auth.LDAP.Bind(auth.LDAP.Config.BindDN, auth.LDAP.Config.BindPassword)
if err != nil {
log.Error().Err(err).Msg("Failed to rebind with service account after user authentication")
// Consider closing the connection or creating a new one
return false
}
log.Debug().Str("username", search.Username).Msg("LDAP authentication successful")
// Return true if the bind was successful
return true
}
default:
log.Warn().Str("type", search.Type).Msg("Unknown user type for authentication")
return false
}
// If no user found or authentication failed, return false
log.Warn().Str("username", search.Username).Msg("User authentication failed")
return false
}
func (auth *Auth) GetLocalUser(username string) types.User {
// Loop through users and return the user if the username matches
log.Debug().Str("username", username).Msg("Searching for local user")
for _, user := range auth.Config.Users {
if user.Username == username {
return user
}
}
// If no user found, return an empty user
log.Warn().Str("username", username).Msg("Local user not found")
return types.User{}
}
func (auth *Auth) CheckPassword(user types.User, password string) bool {
@@ -275,7 +361,7 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error)
func (auth *Auth) UserAuthConfigured() bool {
// If there are users, return true
return len(auth.Config.Users) > 0
return len(auth.Config.Users) > 0 || auth.LDAP != nil
}
func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, labels types.Labels) bool {

View File

@@ -18,7 +18,7 @@ func TestLoginRateLimiting(t *testing.T) {
// Initialize a new auth service with 3 max retries and 5 seconds timeout
config.LoginMaxRetries = 3
config.LoginTimeout = 5
authService := auth.NewAuth(config, &docker.Docker{})
authService := auth.NewAuth(config, &docker.Docker{}, nil)
// Test identifier
identifier := "test_user"
@@ -62,7 +62,7 @@ func TestLoginRateLimiting(t *testing.T) {
// Reinitialize auth service with a shorter timeout for testing
config.LoginTimeout = 1
config.LoginMaxRetries = 3
authService = auth.NewAuth(config, &docker.Docker{})
authService = auth.NewAuth(config, &docker.Docker{}, nil)
// Add enough failed attempts to lock the account
for i := 0; i < 3; i++ {
@@ -87,7 +87,7 @@ func TestLoginRateLimiting(t *testing.T) {
t.Log("Testing disabled rate limiting")
config.LoginMaxRetries = 0
config.LoginTimeout = 0
authService = auth.NewAuth(config, &docker.Docker{})
authService = auth.NewAuth(config, &docker.Docker{}, nil)
for i := 0; i < 10; i++ {
authService.RecordLoginAttempt(identifier, false)
@@ -103,7 +103,7 @@ func TestConcurrentLoginAttempts(t *testing.T) {
// Initialize a new auth service with 2 max retries and 5 seconds timeout
config.LoginMaxRetries = 2
config.LoginTimeout = 5
authService := auth.NewAuth(config, &docker.Docker{})
authService := auth.NewAuth(config, &docker.Docker{}, nil)
// Test multiple identifiers
identifiers := []string{"user1", "user2", "user3"}