Compare commits

...

5 Commits

Author SHA1 Message Date
Stavros
1b2bf3902c feat: retrieve and store groups from ldap provider 2026-01-09 23:23:36 +02:00
Stavros
467c580ec4 refactor: remove useless session struct abstraction 2026-01-09 22:43:30 +02:00
Stavros
98c0d7be24 Merge branch 'main' into feat/ldap-groups 2026-01-09 22:35:20 +02:00
Stavros
e3f92ce4fc refactor: simplify user parsing (#571) 2026-01-08 16:03:37 +02:00
Stavros
caf993a738 wip 2025-12-31 19:47:53 +02:00
18 changed files with 196 additions and 101 deletions

View File

@@ -0,0 +1 @@
ALTER TABLE "sessions" DROP COLUMN "ldap_groups";

View File

@@ -0,0 +1 @@
ALTER TABLE "sessions" ADD COLUMN "ldap_groups" TEXT;

View File

@@ -2,7 +2,6 @@ package bootstrap
import ( import (
"fmt" "fmt"
"strings"
"github.com/steveiliop56/tinyauth/internal/controller" "github.com/steveiliop56/tinyauth/internal/controller"
"github.com/steveiliop56/tinyauth/internal/middleware" "github.com/steveiliop56/tinyauth/internal/middleware"
@@ -15,7 +14,7 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
engine.Use(gin.Recovery()) engine.Use(gin.Recovery())
if len(app.config.Server.TrustedProxies) > 0 { if len(app.config.Server.TrustedProxies) > 0 {
err := engine.SetTrustedProxies(strings.Split(app.config.Server.TrustedProxies, ",")) err := engine.SetTrustedProxies(app.config.Server.TrustedProxies)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to set trusted proxies: %w", err) return nil, fmt.Errorf("failed to set trusted proxies: %w", err)

View File

@@ -36,12 +36,12 @@ type ServerConfig struct {
Port int `description:"The port on which the server listens." yaml:"port"` Port int `description:"The port on which the server listens." yaml:"port"`
Address string `description:"The address on which the server listens." yaml:"address"` Address string `description:"The address on which the server listens." yaml:"address"`
SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"` SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"`
TrustedProxies string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"` TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
} }
type AuthConfig struct { type AuthConfig struct {
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"` IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
Users string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"` Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
UsersFile string `description:"Path to the users file." yaml:"usersFile"` UsersFile string `description:"Path to the users file." yaml:"usersFile"`
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"` SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"` SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"`
@@ -56,7 +56,7 @@ type IPConfig struct {
} }
type OAuthConfig struct { type OAuthConfig struct {
Whitelist string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"` Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"`
AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"` AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"`
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"` Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"`
} }
@@ -122,23 +122,16 @@ type User struct {
TotpSecret string TotpSecret string
} }
type LdapUser struct {
DN string
Groups []string
}
type UserSearch struct { type UserSearch struct {
Username string Username string
Type string // local, ldap or unknown Type string // local, ldap or unknown
} }
type SessionCookie struct {
UUID string
Username string
Name string
Email string
Provider string
TotpPending bool
OAuthGroups string
OAuthName string
OAuthSub string
}
type UserContext struct { type UserContext struct {
Username string Username string
Name string Name string
@@ -151,6 +144,7 @@ type UserContext struct {
TotpEnabled bool TotpEnabled bool
OAuthName string OAuthName string
OAuthSub string OAuthSub string
LdapGroups string
} }
// API responses and queries // API responses and queries

View File

@@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/steveiliop56/tinyauth/internal/config" "github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/service" "github.com/steveiliop56/tinyauth/internal/service"
"github.com/steveiliop56/tinyauth/internal/utils" "github.com/steveiliop56/tinyauth/internal/utils"
@@ -190,7 +191,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
username = strings.Replace(user.Email, "@", "_", -1) username = strings.Replace(user.Email, "@", "_", -1)
} }
sessionCookie := config.SessionCookie{ sessionCookie := repository.Session{
Username: username, Username: username,
Name: name, Name: name,
Email: user.Email, Email: user.Email,

View File

@@ -57,7 +57,7 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test
}, },
}, },
OauthWhitelist: "", OauthWhitelist: []string{},
SessionExpiry: 3600, SessionExpiry: 3600,
SessionMaxLifetime: 0, SessionMaxLifetime: 0,
SecureCookie: false, SecureCookie: false,
@@ -140,7 +140,7 @@ func TestProxyHandler(t *testing.T) {
// Test logged in user // Test logged in user
c := gin.CreateTestContextOnly(recorder, router) c := gin.CreateTestContextOnly(recorder, router)
err := authService.CreateSessionCookie(c, &config.SessionCookie{ err := authService.CreateSessionCookie(c, &repository.Session{
Username: "testuser", Username: "testuser",
Name: "testuser", Name: "testuser",
Email: "testuser@example.com", Email: "testuser@example.com",

View File

@@ -5,7 +5,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/steveiliop56/tinyauth/internal/config" "github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/service" "github.com/steveiliop56/tinyauth/internal/service"
"github.com/steveiliop56/tinyauth/internal/utils" "github.com/steveiliop56/tinyauth/internal/utils"
@@ -108,7 +108,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
if user.TotpSecret != "" { if user.TotpSecret != "" {
log.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification") log.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
err := controller.auth.CreateSessionCookie(c, &config.SessionCookie{ err := controller.auth.CreateSessionCookie(c, &repository.Session{
Username: user.Username, Username: user.Username,
Name: utils.Capitalize(req.Username), Name: utils.Capitalize(req.Username),
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain), Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain),
@@ -134,13 +134,28 @@ func (controller *UserController) loginHandler(c *gin.Context) {
} }
} }
sessionCookie := config.SessionCookie{ sessionCookie := repository.Session{
Username: req.Username, Username: req.Username,
Name: utils.Capitalize(req.Username), Name: utils.Capitalize(req.Username),
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain), Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain),
Provider: "username", Provider: "username",
} }
if userSearch.Type == "ldap" {
ldapUser, err := controller.auth.GetLdapUser(userSearch.Username)
if err != nil {
log.Error().Err(err).Str("username", req.Username).Msg("Failed to get LDAP user details")
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
})
return
}
sessionCookie.LdapGroups = strings.Join(ldapUser.Groups, ",")
}
log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie") log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
err = controller.auth.CreateSessionCookie(c, &sessionCookie) err = controller.auth.CreateSessionCookie(c, &sessionCookie)
@@ -237,7 +252,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
controller.auth.RecordLoginAttempt(context.Username, true) controller.auth.RecordLoginAttempt(context.Username, true)
sessionCookie := config.SessionCookie{ sessionCookie := repository.Session{
Username: user.Username, Username: user.Username,
Name: utils.Capitalize(user.Username), Name: utils.Capitalize(user.Username),
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.config.CookieDomain), Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.config.CookieDomain),

View File

@@ -60,7 +60,7 @@ func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Eng
TotpSecret: totpSecret, TotpSecret: totpSecret,
}, },
}, },
OauthWhitelist: "", OauthWhitelist: []string{},
SessionExpiry: 3600, SessionExpiry: 3600,
SessionMaxLifetime: 0, SessionMaxLifetime: 0,
SecureCookie: false, SecureCookie: false,

View File

@@ -74,6 +74,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
Email: cookie.Email, Email: cookie.Email,
Provider: "username", Provider: "username",
IsLoggedIn: true, IsLoggedIn: true,
LdapGroups: cookie.LdapGroups,
}) })
c.Next() c.Next()
return return
@@ -155,7 +156,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
Username: user.Username, Username: user.Username,
Name: utils.Capitalize(user.Username), Name: utils.Capitalize(user.Username),
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.config.CookieDomain), Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.config.CookieDomain),
Provider: "basic", Provider: "username",
IsLoggedIn: true, IsLoggedIn: true,
TotpEnabled: user.TotpSecret != "", TotpEnabled: user.TotpSecret != "",
}) })
@@ -163,12 +164,22 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
return return
case "ldap": case "ldap":
log.Debug().Msg("Basic auth user is LDAP") log.Debug().Msg("Basic auth user is LDAP")
ldapUser, err := m.auth.GetLdapUser(basic.Username)
if err != nil {
log.Debug().Err(err).Msg("Error retrieving LDAP user details")
c.Next()
return
}
c.Set("context", &config.UserContext{ c.Set("context", &config.UserContext{
Username: basic.Username, Username: basic.Username,
Name: utils.Capitalize(basic.Username), Name: utils.Capitalize(basic.Username),
Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.config.CookieDomain), Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.config.CookieDomain),
Provider: "basic", Provider: "ldap",
IsLoggedIn: true, IsLoggedIn: true,
LdapGroups: strings.Join(ldapUser.Groups, ","),
}) })
c.Next() c.Next()
return return

View File

@@ -16,4 +16,5 @@ type Session struct {
CreatedAt int64 CreatedAt int64
OAuthName string OAuthName string
OAuthSub string OAuthSub string
LdapGroups string
} }

View File

@@ -21,11 +21,12 @@ INSERT INTO sessions (
"expiry", "expiry",
"created_at", "created_at",
"oauth_name", "oauth_name",
"oauth_sub" "oauth_sub",
"ldap_groups"
) VALUES ( ) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
) )
RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub, ldap_groups
` `
type CreateSessionParams struct { type CreateSessionParams struct {
@@ -40,6 +41,7 @@ type CreateSessionParams struct {
CreatedAt int64 CreatedAt int64
OAuthName string OAuthName string
OAuthSub string OAuthSub string
LdapGroups string
} }
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) { func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) {
@@ -55,6 +57,7 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S
arg.CreatedAt, arg.CreatedAt,
arg.OAuthName, arg.OAuthName,
arg.OAuthSub, arg.OAuthSub,
arg.LdapGroups,
) )
var i Session var i Session
err := row.Scan( err := row.Scan(
@@ -69,6 +72,7 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S
&i.CreatedAt, &i.CreatedAt,
&i.OAuthName, &i.OAuthName,
&i.OAuthSub, &i.OAuthSub,
&i.LdapGroups,
) )
return i, err return i, err
} }
@@ -94,7 +98,7 @@ func (q *Queries) DeleteSession(ctx context.Context, uuid string) error {
} }
const getSession = `-- name: GetSession :one const getSession = `-- name: GetSession :one
SELECT uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub FROM "sessions" SELECT uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub, ldap_groups FROM "sessions"
WHERE "uuid" = ? WHERE "uuid" = ?
` `
@@ -113,6 +117,7 @@ func (q *Queries) GetSession(ctx context.Context, uuid string) (Session, error)
&i.CreatedAt, &i.CreatedAt,
&i.OAuthName, &i.OAuthName,
&i.OAuthSub, &i.OAuthSub,
&i.LdapGroups,
) )
return i, err return i, err
} }
@@ -127,9 +132,10 @@ UPDATE "sessions" SET
"oauth_groups" = ?, "oauth_groups" = ?,
"expiry" = ?, "expiry" = ?,
"oauth_name" = ?, "oauth_name" = ?,
"oauth_sub" = ? "oauth_sub" = ?,
"ldap_groups" = ?
WHERE "uuid" = ? WHERE "uuid" = ?
RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub, ldap_groups
` `
type UpdateSessionParams struct { type UpdateSessionParams struct {
@@ -142,6 +148,7 @@ type UpdateSessionParams struct {
Expiry int64 Expiry int64
OAuthName string OAuthName string
OAuthSub string OAuthSub string
LdapGroups string
UUID string UUID string
} }
@@ -156,6 +163,7 @@ func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (S
arg.Expiry, arg.Expiry,
arg.OAuthName, arg.OAuthName,
arg.OAuthSub, arg.OAuthSub,
arg.LdapGroups,
arg.UUID, arg.UUID,
) )
var i Session var i Session
@@ -171,6 +179,7 @@ func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (S
&i.CreatedAt, &i.CreatedAt,
&i.OAuthName, &i.OAuthName,
&i.OAuthSub, &i.OAuthSub,
&i.LdapGroups,
) )
return i, err return i, err
} }

View File

@@ -27,7 +27,7 @@ type LoginAttempt struct {
type AuthServiceConfig struct { type AuthServiceConfig struct {
Users []config.User Users []config.User
OauthWhitelist string OauthWhitelist []string
SessionExpiry int SessionExpiry int
SessionMaxLifetime int SessionMaxLifetime int
SecureCookie bool SecureCookie bool
@@ -70,7 +70,7 @@ func (auth *AuthService) SearchUser(username string) config.UserSearch {
} }
if auth.ldap != nil { if auth.ldap != nil {
userDN, err := auth.ldap.Search(username) userDN, err := auth.ldap.GetUserDN(username)
if err != nil { if err != nil {
log.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP") log.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP")
@@ -131,6 +131,19 @@ func (auth *AuthService) GetLocalUser(username string) config.User {
return config.User{} return config.User{}
} }
func (auth *AuthService) GetLdapUser(userDN string) (config.LdapUser, error) {
groups, err := auth.ldap.GetUserGroups(userDN)
if err != nil {
return config.LdapUser{}, err
}
return config.LdapUser{
DN: userDN,
Groups: groups,
}, nil
}
func (auth *AuthService) CheckPassword(user config.User, password string) bool { func (auth *AuthService) CheckPassword(user config.User, password string) bool {
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
} }
@@ -187,10 +200,10 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
} }
func (auth *AuthService) IsEmailWhitelisted(email string) bool { func (auth *AuthService) IsEmailWhitelisted(email string) bool {
return utils.CheckFilter(auth.config.OauthWhitelist, email) return utils.CheckFilter(strings.Join(auth.config.OauthWhitelist, ","), email)
} }
func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.SessionCookie) error { func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *repository.Session) error {
uuid, err := uuid.NewRandom() uuid, err := uuid.NewRandom()
if err != nil { if err != nil {
@@ -217,6 +230,7 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio
CreatedAt: time.Now().Unix(), CreatedAt: time.Now().Unix(),
OAuthName: data.OAuthName, OAuthName: data.OAuthName,
OAuthSub: data.OAuthSub, OAuthSub: data.OAuthSub,
LdapGroups: data.LdapGroups,
} }
_, err = auth.queries.CreateSession(c, session) _, err = auth.queries.CreateSession(c, session)
@@ -270,6 +284,7 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
OAuthName: session.OAuthName, OAuthName: session.OAuthName,
OAuthSub: session.OAuthSub, OAuthSub: session.OAuthSub,
UUID: session.UUID, UUID: session.UUID,
LdapGroups: session.LdapGroups,
}) })
if err != nil { if err != nil {
@@ -300,20 +315,20 @@ func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error {
return nil return nil
} }
func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, error) { func (auth *AuthService) GetSessionCookie(c *gin.Context) (repository.Session, error) {
cookie, err := c.Cookie(auth.config.SessionCookieName) cookie, err := c.Cookie(auth.config.SessionCookieName)
if err != nil { if err != nil {
return config.SessionCookie{}, err return repository.Session{}, err
} }
session, err := auth.queries.GetSession(c, cookie) session, err := auth.queries.GetSession(c, cookie)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return config.SessionCookie{}, fmt.Errorf("session not found") return repository.Session{}, fmt.Errorf("session not found")
} }
return config.SessionCookie{}, err return repository.Session{}, err
} }
currentTime := time.Now().Unix() currentTime := time.Now().Unix()
@@ -324,7 +339,7 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to delete session exceeding max lifetime") log.Error().Err(err).Msg("Failed to delete session exceeding max lifetime")
} }
return config.SessionCookie{}, fmt.Errorf("session expired due to max lifetime exceeded") return repository.Session{}, fmt.Errorf("session expired due to max lifetime exceeded")
} }
} }
@@ -333,10 +348,10 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to delete expired session") log.Error().Err(err).Msg("Failed to delete expired session")
} }
return config.SessionCookie{}, fmt.Errorf("session expired") return repository.Session{}, fmt.Errorf("session expired")
} }
return config.SessionCookie{ return repository.Session{
UUID: session.UUID, UUID: session.UUID,
Username: session.Username, Username: session.Username,
Email: session.Email, Email: session.Email,
@@ -346,6 +361,7 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
OAuthGroups: session.OAuthGroups, OAuthGroups: session.OAuthGroups,
OAuthName: session.OAuthName, OAuthName: session.OAuthName,
OAuthSub: session.OAuthSub, OAuthSub: session.OAuthSub,
LdapGroups: session.LdapGroups,
}, nil }, nil
} }

View File

@@ -4,6 +4,8 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"slices"
"strings"
"sync" "sync"
"time" "time"
@@ -116,7 +118,7 @@ func (ldap *LdapService) connect() (*ldapgo.Conn, error) {
return ldap.conn, nil return ldap.conn, nil
} }
func (ldap *LdapService) Search(username string) (string, error) { func (ldap *LdapService) GetUserDN(username string) (string, error) {
// Escape the username to prevent LDAP injection // Escape the username to prevent LDAP injection
escapedUsername := ldapgo.EscapeFilter(username) escapedUsername := ldapgo.EscapeFilter(username)
filter := fmt.Sprintf(ldap.config.SearchFilter, escapedUsername) filter := fmt.Sprintf(ldap.config.SearchFilter, escapedUsername)
@@ -145,6 +147,47 @@ func (ldap *LdapService) Search(username string) (string, error) {
return userDN, nil return userDN, nil
} }
func (ldap *LdapService) GetUserGroups(userDN string) ([]string, error) {
searchRequest := ldapgo.NewSearchRequest(
ldap.config.BaseDN,
ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false,
"(objectclass=groupOfUniqueNames)",
[]string{"uniquemember"},
nil,
)
ldap.mutex.Lock()
defer ldap.mutex.Unlock()
searchResult, err := ldap.conn.Search(searchRequest)
if err != nil {
return []string{}, err
}
groupDNs := []string{}
for _, entry := range searchResult.Entries {
memberAttributes := entry.GetAttributeValues("uniquemember")
// 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{}
for _, groupDN := range groupDNs {
groupDN = strings.TrimPrefix(groupDN, "cn=")
parts := strings.SplitN(groupDN, ",", 2)
if len(parts) > 0 {
groups = append(groups, parts[0])
}
}
return groups, nil
}
func (ldap *LdapService) BindService(rebind bool) error { func (ldap *LdapService) BindService(rebind bool) error {
// Locks must not be used for initial binding attempt // Locks must not be used for initial binding attempt
if rebind { if rebind {

View File

@@ -7,22 +7,14 @@ import (
"github.com/steveiliop56/tinyauth/internal/config" "github.com/steveiliop56/tinyauth/internal/config"
) )
func ParseUsers(users string) ([]config.User, error) { func ParseUsers(usersStr []string) ([]config.User, error) {
var usersParsed []config.User var users []config.User
users = strings.TrimSpace(users) if len(usersStr) == 0 {
if users == "" {
return []config.User{}, nil return []config.User{}, nil
} }
userList := strings.Split(users, ",") for _, user := range usersStr {
if len(userList) == 0 {
return []config.User{}, errors.New("invalid user format")
}
for _, user := range userList {
if strings.TrimSpace(user) == "" { if strings.TrimSpace(user) == "" {
continue continue
} }
@@ -30,64 +22,71 @@ func ParseUsers(users string) ([]config.User, error) {
if err != nil { if err != nil {
return []config.User{}, err return []config.User{}, err
} }
usersParsed = append(usersParsed, parsed) users = append(users, parsed)
} }
return usersParsed, nil return users, nil
} }
func GetUsers(conf string, file string) ([]config.User, error) { func GetUsers(usersCfg []string, usersPath string) ([]config.User, error) {
var users string var usersStr []string
if conf == "" && file == "" { if len(usersCfg) == 0 && usersPath == "" {
return []config.User{}, nil return []config.User{}, nil
} }
if conf != "" { if len(usersCfg) > 0 {
users += conf usersStr = append(usersStr, usersCfg...)
} }
if file != "" { if usersPath != "" {
contents, err := ReadFile(file) contents, err := ReadFile(usersPath)
if err != nil { if err != nil {
return []config.User{}, err return []config.User{}, err
} }
if users != "" {
users += "," lines := strings.SplitSeq(contents, "\n")
for line := range lines {
lineTrimmed := strings.TrimSpace(line)
if lineTrimmed == "" {
continue
}
usersStr = append(usersStr, lineTrimmed)
} }
users += ParseFileToLine(contents)
} }
return ParseUsers(users) return ParseUsers(usersStr)
} }
func ParseUser(user string) (config.User, error) { func ParseUser(userStr string) (config.User, error) {
if strings.Contains(user, "$$") { if strings.Contains(userStr, "$$") {
user = strings.ReplaceAll(user, "$$", "$") userStr = strings.ReplaceAll(userStr, "$$", "$")
} }
userSplit := strings.Split(user, ":") parts := strings.SplitN(userStr, ":", 4)
if len(userSplit) < 2 || len(userSplit) > 3 { if len(parts) < 2 || len(parts) > 3 {
return config.User{}, errors.New("invalid user format") return config.User{}, errors.New("invalid user format")
} }
for _, userPart := range userSplit { for i, part := range parts {
if strings.TrimSpace(userPart) == "" { trimmed := strings.TrimSpace(part)
if trimmed == "" {
return config.User{}, errors.New("invalid user format") return config.User{}, errors.New("invalid user format")
} }
parts[i] = trimmed
} }
if len(userSplit) == 2 { user := config.User{
return config.User{ Username: parts[0],
Username: strings.TrimSpace(userSplit[0]), Password: parts[1],
Password: strings.TrimSpace(userSplit[1]),
}, nil
} }
return config.User{ if len(parts) == 3 {
Username: strings.TrimSpace(userSplit[0]), user.TotpSecret = parts[2]
Password: strings.TrimSpace(userSplit[1]), }
TotpSecret: strings.TrimSpace(userSplit[2]),
}, nil return user, nil
} }

View File

@@ -22,7 +22,7 @@ func TestGetUsers(t *testing.T) {
defer os.Remove("/tmp/tinyauth_users_test.txt") defer os.Remove("/tmp/tinyauth_users_test.txt")
// Test file // Test file
users, err := utils.GetUsers("", "/tmp/tinyauth_users_test.txt") users, err := utils.GetUsers([]string{}, "/tmp/tinyauth_users_test.txt")
assert.NilError(t, err) assert.NilError(t, err)
@@ -34,7 +34,7 @@ func TestGetUsers(t *testing.T) {
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password) assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
// Test config // Test config
users, err = utils.GetUsers("user3:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G,user4:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "") users, err = utils.GetUsers([]string{"user3:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "user4:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G"}, "")
assert.NilError(t, err) assert.NilError(t, err)
@@ -46,7 +46,7 @@ func TestGetUsers(t *testing.T) {
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password) assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password)
// Test both // Test both
users, err = utils.GetUsers("user5:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "/tmp/tinyauth_users_test.txt") users, err = utils.GetUsers([]string{"user5:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G"}, "/tmp/tinyauth_users_test.txt")
assert.NilError(t, err) assert.NilError(t, err)
@@ -60,14 +60,14 @@ func TestGetUsers(t *testing.T) {
assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[2].Password) assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[2].Password)
// Test empty // Test empty
users, err = utils.GetUsers("", "") users, err = utils.GetUsers([]string{}, "")
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, 0, len(users)) assert.Equal(t, 0, len(users))
// Test non-existent file // Test non-existent file
users, err = utils.GetUsers("", "/tmp/non_existent_file.txt") users, err = utils.GetUsers([]string{}, "/tmp/non_existent_file.txt")
assert.ErrorContains(t, err, "no such file or directory") assert.ErrorContains(t, err, "no such file or directory")
@@ -76,7 +76,7 @@ func TestGetUsers(t *testing.T) {
func TestParseUsers(t *testing.T) { func TestParseUsers(t *testing.T) {
// Valid users // Valid users
users, err := utils.ParseUsers("user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G,user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF") // user2 has TOTP users, err := utils.ParseUsers([]string{"user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF"}) // user2 has TOTP
assert.NilError(t, err) assert.NilError(t, err)
@@ -90,7 +90,7 @@ func TestParseUsers(t *testing.T) {
assert.Equal(t, "ABCDEF", users[1].TotpSecret) assert.Equal(t, "ABCDEF", users[1].TotpSecret)
// Valid weirdly spaced users // Valid weirdly spaced users
users, err = utils.ParseUsers(" user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G , user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF ") // Spacing is on purpose users, err = utils.ParseUsers([]string{" user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G ", " user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF "}) // Spacing is on purpose
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, 2, len(users)) assert.Equal(t, 2, len(users))

View File

@@ -10,9 +10,10 @@ INSERT INTO sessions (
"expiry", "expiry",
"created_at", "created_at",
"oauth_name", "oauth_name",
"oauth_sub" "oauth_sub",
"ldap_groups"
) VALUES ( ) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
) )
RETURNING *; RETURNING *;
@@ -34,7 +35,8 @@ UPDATE "sessions" SET
"oauth_groups" = ?, "oauth_groups" = ?,
"expiry" = ?, "expiry" = ?,
"oauth_name" = ?, "oauth_name" = ?,
"oauth_sub" = ? "oauth_sub" = ?,
"ldap_groups" = ?
WHERE "uuid" = ? WHERE "uuid" = ?
RETURNING *; RETURNING *;

View File

@@ -9,5 +9,6 @@ CREATE TABLE IF NOT EXISTS "sessions" (
"expiry" INTEGER NOT NULL, "expiry" INTEGER NOT NULL,
"created_at" INTEGER NOT NULL, "created_at" INTEGER NOT NULL,
"oauth_name" TEXT NULL, "oauth_name" TEXT NULL,
"oauth_sub" TEXT NULL "oauth_sub" TEXT NULL,
"ldap_groups" TEXT NULL
); );

View File

@@ -19,3 +19,5 @@ sql:
go_type: "string" go_type: "string"
- column: "sessions.oauth_sub" - column: "sessions.oauth_sub"
go_type: "string" go_type: "string"
- column: "sessions.ldap_groups"
go_type: "string"