mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-01-17 04:42:27 +00:00
Compare commits
10 Commits
main
...
feat/ldap-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5093200023 | ||
|
|
95ed36db8b | ||
|
|
4fc18325a0 | ||
|
|
4917202879 | ||
|
|
6597bea1b2 | ||
|
|
3f69509622 | ||
|
|
1b2bf3902c | ||
|
|
467c580ec4 | ||
|
|
98c0d7be24 | ||
|
|
caf993a738 |
13
.zed/debug.json
Normal file
13
.zed/debug.json
Normal file
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"label": "Attach to remote Delve",
|
||||
"adapter": "Delve",
|
||||
"mode": "remote",
|
||||
"remotePath": "/tinyauth",
|
||||
"request": "attach",
|
||||
"tcp_connection": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 4000,
|
||||
},
|
||||
},
|
||||
]
|
||||
5
Makefile
5
Makefile
@@ -62,3 +62,8 @@ develop:
|
||||
# Production
|
||||
prod:
|
||||
docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
||||
|
||||
# SQL
|
||||
.PHONY: sql
|
||||
sql:
|
||||
sqlc generate
|
||||
|
||||
@@ -21,9 +21,9 @@ func NewTinyauthCmdConfiguration() *config.Config {
|
||||
Address: "0.0.0.0",
|
||||
},
|
||||
Auth: config.AuthConfig{
|
||||
SessionExpiry: 3600,
|
||||
SessionMaxLifetime: 0,
|
||||
LoginTimeout: 300,
|
||||
SessionExpiry: 86400, // 1 day
|
||||
SessionMaxLifetime: 0, // disabled
|
||||
LoginTimeout: 300, // 5 minutes
|
||||
LoginMaxRetries: 3,
|
||||
},
|
||||
UI: config.UIConfig{
|
||||
@@ -34,6 +34,7 @@ func NewTinyauthCmdConfiguration() *config.Config {
|
||||
Ldap: config.LdapConfig{
|
||||
Insecure: false,
|
||||
SearchFilter: "(uid=%s)",
|
||||
GroupCacheTTL: 900, // 15 minutes
|
||||
},
|
||||
Log: config.LogConfig{
|
||||
Level: "info",
|
||||
|
||||
@@ -50,10 +50,12 @@ export const LoginPage = () => {
|
||||
const redirectUri = searchParams.get("redirect_uri");
|
||||
|
||||
const oauthProviders = providers.filter(
|
||||
(provider) => provider.id !== "username",
|
||||
(provider) => provider.id !== "local" && provider.id !== "ldap",
|
||||
);
|
||||
const userAuthConfigured =
|
||||
providers.find((provider) => provider.id === "username") !== undefined;
|
||||
providers.find(
|
||||
(provider) => provider.id === "local" || provider.id === "ldap",
|
||||
) !== undefined;
|
||||
|
||||
const oauthMutation = useMutation({
|
||||
mutationFn: (provider: string) =>
|
||||
|
||||
@@ -144,10 +144,18 @@ func (app *BootstrapApp) Setup() error {
|
||||
return configuredProviders[i].Name < configuredProviders[j].Name
|
||||
})
|
||||
|
||||
if services.authService.UserAuthConfigured() {
|
||||
if services.authService.LocalAuthConfigured() {
|
||||
configuredProviders = append(configuredProviders, controller.Provider{
|
||||
Name: "Username",
|
||||
ID: "username",
|
||||
Name: "Local",
|
||||
ID: "local",
|
||||
OAuth: false,
|
||||
})
|
||||
}
|
||||
|
||||
if services.authService.LdapAuthConfigured() {
|
||||
configuredProviders = append(configuredProviders, controller.Provider{
|
||||
Name: "LDAP",
|
||||
ID: "ldap",
|
||||
OAuth: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
||||
LoginMaxRetries: app.config.Auth.LoginMaxRetries,
|
||||
SessionCookieName: app.context.sessionCookieName,
|
||||
IP: app.config.Auth.IP,
|
||||
LDAPGroupsCacheTTL: app.config.Ldap.GroupCacheTTL,
|
||||
}, dockerService, services.ldapService, queries)
|
||||
|
||||
err = authService.Init()
|
||||
|
||||
@@ -75,6 +75,7 @@ type LdapConfig struct {
|
||||
SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"`
|
||||
AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert"`
|
||||
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 {
|
||||
@@ -138,28 +139,22 @@ type User struct {
|
||||
TotpSecret string
|
||||
}
|
||||
|
||||
type LdapUser struct {
|
||||
DN string
|
||||
Groups []string
|
||||
}
|
||||
|
||||
type UserSearch struct {
|
||||
Username string
|
||||
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 {
|
||||
Username string
|
||||
Name string
|
||||
Email string
|
||||
IsLoggedIn bool
|
||||
IsBasicAuth bool
|
||||
OAuth bool
|
||||
Provider string
|
||||
TotpPending bool
|
||||
@@ -167,6 +162,7 @@ type UserContext struct {
|
||||
TotpEnabled bool
|
||||
OAuthName string
|
||||
OAuthSub string
|
||||
LdapGroups string
|
||||
}
|
||||
|
||||
// API responses and queries
|
||||
@@ -195,6 +191,7 @@ type App struct {
|
||||
IP AppIP `description:"IP access configuration." yaml:"ip"`
|
||||
Response AppResponse `description:"Response customization." yaml:"response"`
|
||||
Path AppPath `description:"Path access configuration." yaml:"path"`
|
||||
LDAP AppLDAP `description:"LDAP access configuration." yaml:"ldap"`
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
@@ -211,6 +208,10 @@ type AppOAuth struct {
|
||||
Groups string `description:"Comma-separated list of required OAuth groups." yaml:"groups"`
|
||||
}
|
||||
|
||||
type AppLDAP struct {
|
||||
Groups string `description:"Comma-separated list of required LDAP groups." yaml:"groups"`
|
||||
}
|
||||
|
||||
type AppIP struct {
|
||||
Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"`
|
||||
Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"`
|
||||
|
||||
@@ -21,7 +21,6 @@ type UserContextResponse struct {
|
||||
OAuth bool `json:"oauth"`
|
||||
TotpPending bool `json:"totpPending"`
|
||||
OAuthName string `json:"oauthName"`
|
||||
OAuthSub string `json:"oauthSub"`
|
||||
}
|
||||
|
||||
type AppContextResponse struct {
|
||||
@@ -90,7 +89,6 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
||||
OAuth: context.OAuth,
|
||||
TotpPending: context.TotpPending,
|
||||
OAuthName: context.OAuthName,
|
||||
OAuthSub: context.OAuthSub,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
var controllerCfg = controller.ContextControllerConfig{
|
||||
Providers: []controller.Provider{
|
||||
{
|
||||
Name: "Username",
|
||||
ID: "username",
|
||||
Name: "Local",
|
||||
ID: "local",
|
||||
OAuth: false,
|
||||
},
|
||||
{
|
||||
@@ -40,8 +40,9 @@ var userContext = config.UserContext{
|
||||
Name: "testuser",
|
||||
Email: "test@example.com",
|
||||
IsLoggedIn: true,
|
||||
IsBasicAuth: false,
|
||||
OAuth: false,
|
||||
Provider: "username",
|
||||
Provider: "local",
|
||||
TotpPending: false,
|
||||
OAuthGroups: "",
|
||||
TotpEnabled: false,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
@@ -188,10 +189,10 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
username = user.PreferredUsername
|
||||
} else {
|
||||
tlog.App.Debug().Msg("No preferred username from OAuth provider, using pseudo username")
|
||||
username = strings.Replace(user.Email, "@", "_", -1)
|
||||
username = strings.Replace(user.Email, "@", "_", 1)
|
||||
}
|
||||
|
||||
sessionCookie := config.SessionCookie{
|
||||
sessionCookie := repository.Session{
|
||||
Username: username,
|
||||
Name: name,
|
||||
Email: user.Email,
|
||||
|
||||
@@ -173,7 +173,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
|
||||
tlog.App.Trace().Interface("context", userContext).Msg("User context from request")
|
||||
|
||||
if userContext.Provider == "basic" && userContext.TotpEnabled {
|
||||
if userContext.IsBasicAuth && userContext.TotpEnabled {
|
||||
tlog.App.Debug().Msg("User has TOTP enabled, denying basic auth access")
|
||||
userContext.IsLoggedIn = false
|
||||
}
|
||||
@@ -212,11 +212,17 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if userContext.OAuth || userContext.Provider == "ldap" {
|
||||
var groupOK bool
|
||||
|
||||
if userContext.OAuth {
|
||||
groupOK := controller.auth.IsInOAuthGroup(c, userContext, acls.OAuth.Groups)
|
||||
groupOK = controller.auth.IsInOAuthGroup(c, userContext, acls.OAuth.Groups)
|
||||
} else {
|
||||
groupOK = controller.auth.IsInLdapGroup(c, userContext, acls.LDAP.Groups)
|
||||
}
|
||||
|
||||
if !groupOK {
|
||||
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements")
|
||||
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User groups do not match resource requirements")
|
||||
|
||||
if req.Proxy == "nginx" || !isBrowser {
|
||||
c.JSON(403, gin.H{
|
||||
@@ -251,7 +257,13 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
c.Header("Remote-User", utils.SanitizeHeader(userContext.Username))
|
||||
c.Header("Remote-Name", utils.SanitizeHeader(userContext.Name))
|
||||
c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email))
|
||||
|
||||
if userContext.Provider == "ldap" {
|
||||
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.LdapGroups))
|
||||
} else if userContext.Provider != "local" {
|
||||
c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups))
|
||||
}
|
||||
|
||||
c.Header("Remote-Sub", utils.SanitizeHeader(userContext.OAuthSub))
|
||||
|
||||
controller.setHeaders(c, acls)
|
||||
|
||||
@@ -143,11 +143,11 @@ func TestProxyHandler(t *testing.T) {
|
||||
// Test logged in user
|
||||
c := gin.CreateTestContextOnly(recorder, router)
|
||||
|
||||
err := authService.CreateSessionCookie(c, &config.SessionCookie{
|
||||
err := authService.CreateSessionCookie(c, &repository.Session{
|
||||
Username: "testuser",
|
||||
Name: "testuser",
|
||||
Email: "testuser@example.com",
|
||||
Provider: "username",
|
||||
Provider: "local",
|
||||
TotpPending: false,
|
||||
OAuthGroups: "",
|
||||
})
|
||||
@@ -164,7 +164,7 @@ func TestProxyHandler(t *testing.T) {
|
||||
Email: "testuser@example.com",
|
||||
IsLoggedIn: true,
|
||||
OAuth: false,
|
||||
Provider: "username",
|
||||
Provider: "local",
|
||||
TotpPending: false,
|
||||
OAuthGroups: "",
|
||||
TotpEnabled: false,
|
||||
@@ -192,8 +192,9 @@ func TestProxyHandler(t *testing.T) {
|
||||
Name: "testuser",
|
||||
Email: "testuser@example.com",
|
||||
IsLoggedIn: true,
|
||||
IsBasicAuth: true,
|
||||
OAuth: false,
|
||||
Provider: "basic",
|
||||
Provider: "local",
|
||||
TotpPending: false,
|
||||
OAuthGroups: "",
|
||||
TotpEnabled: true,
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
@@ -112,11 +112,11 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
if user.TotpSecret != "" {
|
||||
tlog.App.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,
|
||||
Name: utils.Capitalize(req.Username),
|
||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain),
|
||||
Provider: "username",
|
||||
Provider: "local",
|
||||
TotpPending: true,
|
||||
})
|
||||
|
||||
@@ -138,11 +138,15 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
sessionCookie := config.SessionCookie{
|
||||
sessionCookie := repository.Session{
|
||||
Username: req.Username,
|
||||
Name: utils.Capitalize(req.Username),
|
||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain),
|
||||
Provider: "username",
|
||||
Provider: "local",
|
||||
}
|
||||
|
||||
if userSearch.Type == "ldap" {
|
||||
sessionCookie.Provider = "ldap"
|
||||
}
|
||||
|
||||
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||
@@ -248,11 +252,11 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
||||
|
||||
controller.auth.RecordLoginAttempt(context.Username, true)
|
||||
|
||||
sessionCookie := config.SessionCookie{
|
||||
sessionCookie := repository.Session{
|
||||
Username: user.Username,
|
||||
Name: utils.Capitalize(user.Username),
|
||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.config.CookieDomain),
|
||||
Provider: "username",
|
||||
Provider: "local",
|
||||
}
|
||||
|
||||
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||
|
||||
@@ -204,7 +204,7 @@ func TestTotpHandler(t *testing.T) {
|
||||
Email: "totpuser@example.com",
|
||||
IsLoggedIn: false,
|
||||
OAuth: false,
|
||||
Provider: "username",
|
||||
Provider: "local",
|
||||
TotpPending: true,
|
||||
OAuthGroups: "",
|
||||
TotpEnabled: true,
|
||||
@@ -267,7 +267,7 @@ func TestTotpHandler(t *testing.T) {
|
||||
Email: "totpuser@example.com",
|
||||
IsLoggedIn: false,
|
||||
OAuth: false,
|
||||
Provider: "username",
|
||||
Provider: "local",
|
||||
TotpPending: true,
|
||||
OAuthGroups: "",
|
||||
TotpEnabled: true,
|
||||
@@ -290,7 +290,7 @@ func TestTotpHandler(t *testing.T) {
|
||||
Email: "totpuser@example.com",
|
||||
IsLoggedIn: false,
|
||||
OAuth: false,
|
||||
Provider: "username",
|
||||
Provider: "local",
|
||||
TotpPending: false,
|
||||
OAuthGroups: "",
|
||||
TotpEnabled: false,
|
||||
|
||||
@@ -49,7 +49,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
Username: cookie.Username,
|
||||
Name: cookie.Name,
|
||||
Email: cookie.Email,
|
||||
Provider: "username",
|
||||
Provider: "local",
|
||||
TotpPending: true,
|
||||
TotpEnabled: true,
|
||||
})
|
||||
@@ -58,22 +58,44 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
switch cookie.Provider {
|
||||
case "username":
|
||||
case "local", "ldap":
|
||||
userSearch := m.auth.SearchUser(cookie.Username)
|
||||
|
||||
if userSearch.Type == "unknown" || userSearch.Type == "error" {
|
||||
if userSearch.Type == "unknown" {
|
||||
tlog.App.Debug().Msg("User from session cookie not found")
|
||||
m.auth.DeleteSessionCookie(c)
|
||||
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
|
||||
|
||||
if cookie.Provider == "ldap" {
|
||||
ldapUser, err := m.auth.GetLdapUser(userSearch.Username)
|
||||
|
||||
if err != nil {
|
||||
tlog.App.Error().Err(err).Msg("Error retrieving LDAP user details")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
ldapGroups = ldapUser.Groups
|
||||
}
|
||||
|
||||
m.auth.RefreshSessionCookie(c)
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: cookie.Username,
|
||||
Name: cookie.Name,
|
||||
Email: cookie.Email,
|
||||
Provider: "username",
|
||||
Provider: cookie.Provider,
|
||||
IsLoggedIn: true,
|
||||
LdapGroups: strings.Join(ldapGroups, ","),
|
||||
})
|
||||
c.Next()
|
||||
return
|
||||
@@ -155,20 +177,32 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
Username: user.Username,
|
||||
Name: utils.Capitalize(user.Username),
|
||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.config.CookieDomain),
|
||||
Provider: "basic",
|
||||
Provider: "local",
|
||||
IsLoggedIn: true,
|
||||
TotpEnabled: user.TotpSecret != "",
|
||||
IsBasicAuth: true,
|
||||
})
|
||||
c.Next()
|
||||
return
|
||||
case "ldap":
|
||||
tlog.App.Debug().Msg("Basic auth user is LDAP")
|
||||
|
||||
ldapUser, err := m.auth.GetLdapUser(basic.Username)
|
||||
|
||||
if err != nil {
|
||||
tlog.App.Debug().Err(err).Msg("Error retrieving LDAP user details")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: basic.Username,
|
||||
Name: utils.Capitalize(basic.Username),
|
||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.config.CookieDomain),
|
||||
Provider: "basic",
|
||||
Provider: "ldap",
|
||||
IsLoggedIn: true,
|
||||
LdapGroups: strings.Join(ldapUser.Groups, ","),
|
||||
IsBasicAuth: true,
|
||||
})
|
||||
c.Next()
|
||||
return
|
||||
|
||||
@@ -19,6 +19,11 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type LdapGroupsCache struct {
|
||||
Groups []string
|
||||
Expires time.Time
|
||||
}
|
||||
|
||||
type LoginAttempt struct {
|
||||
FailedAttempts int
|
||||
LastAttempt time.Time
|
||||
@@ -36,13 +41,16 @@ type AuthServiceConfig struct {
|
||||
LoginMaxRetries int
|
||||
SessionCookieName string
|
||||
IP config.IPConfig
|
||||
LDAPGroupsCacheTTL int
|
||||
}
|
||||
|
||||
type AuthService struct {
|
||||
config AuthServiceConfig
|
||||
docker *DockerService
|
||||
loginAttempts map[string]*LoginAttempt
|
||||
ldapGroupsCache map[string]*LdapGroupsCache
|
||||
loginMutex sync.RWMutex
|
||||
ldapGroupsMutex sync.RWMutex
|
||||
ldap *LdapService
|
||||
queries *repository.Queries
|
||||
}
|
||||
@@ -52,6 +60,7 @@ func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapS
|
||||
config: config,
|
||||
docker: docker,
|
||||
loginAttempts: make(map[string]*LoginAttempt),
|
||||
ldapGroupsCache: make(map[string]*LdapGroupsCache),
|
||||
ldap: ldap,
|
||||
queries: queries,
|
||||
}
|
||||
@@ -70,12 +79,12 @@ func (auth *AuthService) SearchUser(username string) config.UserSearch {
|
||||
}
|
||||
|
||||
if auth.ldap != nil {
|
||||
userDN, err := auth.ldap.Search(username)
|
||||
userDN, err := auth.ldap.GetUserDN(username)
|
||||
|
||||
if err != nil {
|
||||
tlog.App.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP")
|
||||
return config.UserSearch{
|
||||
Type: "error",
|
||||
Type: "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +140,37 @@ func (auth *AuthService) GetLocalUser(username string) config.User {
|
||||
return config.User{}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if err != nil {
|
||||
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{
|
||||
DN: userDN,
|
||||
Groups: groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (auth *AuthService) CheckPassword(user config.User, password string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
|
||||
}
|
||||
@@ -190,7 +230,7 @@ func (auth *AuthService) IsEmailWhitelisted(email string) bool {
|
||||
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()
|
||||
|
||||
if err != nil {
|
||||
@@ -300,20 +340,20 @@ func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error {
|
||||
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)
|
||||
|
||||
if err != nil {
|
||||
return config.SessionCookie{}, err
|
||||
return repository.Session{}, err
|
||||
}
|
||||
|
||||
session, err := auth.queries.GetSession(c, cookie)
|
||||
|
||||
if err != nil {
|
||||
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()
|
||||
@@ -324,7 +364,7 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
|
||||
if err != nil {
|
||||
tlog.App.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 +373,10 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
|
||||
if err != nil {
|
||||
tlog.App.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,
|
||||
Username: session.Username,
|
||||
Email: session.Email,
|
||||
@@ -349,8 +389,12 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (auth *AuthService) UserAuthConfigured() bool {
|
||||
return len(auth.config.Users) > 0 || auth.ldap != nil
|
||||
func (auth *AuthService) LocalAuthConfigured() bool {
|
||||
return len(auth.config.Users) > 0
|
||||
}
|
||||
|
||||
func (auth *AuthService) LdapAuthConfigured() bool {
|
||||
return auth.ldap != nil
|
||||
}
|
||||
|
||||
func (auth *AuthService) IsUserAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
|
||||
@@ -393,6 +437,22 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte
|
||||
return false
|
||||
}
|
||||
|
||||
func (auth *AuthService) IsInLdapGroup(c *gin.Context, context config.UserContext, requiredGroups string) bool {
|
||||
if requiredGroups == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
for userGroup := range strings.SplitSeq(context.LdapGroups, ",") {
|
||||
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
|
||||
tlog.App.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
tlog.App.Debug().Msg("No groups matched")
|
||||
return false
|
||||
}
|
||||
|
||||
func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, error) {
|
||||
// Check for block list
|
||||
if path.Block != "" {
|
||||
|
||||
@@ -116,7 +116,7 @@ func (ldap *LdapService) connect() (*ldapgo.Conn, error) {
|
||||
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
|
||||
escapedUsername := ldapgo.EscapeFilter(username)
|
||||
filter := fmt.Sprintf(ldap.config.SearchFilter, escapedUsername)
|
||||
@@ -145,6 +145,48 @@ func (ldap *LdapService) Search(username string) (string, error) {
|
||||
return userDN, nil
|
||||
}
|
||||
|
||||
func (ldap *LdapService) GetUserGroups(userDN string) ([]string, error) {
|
||||
escapedUserDN := ldapgo.EscapeFilter(userDN)
|
||||
|
||||
searchRequest := ldapgo.NewSearchRequest(
|
||||
ldap.config.BaseDN,
|
||||
ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false,
|
||||
fmt.Sprintf("(&(objectclass=groupOfUniqueNames)(uniquemember=%s))", escapedUserDN),
|
||||
[]string{"dn"},
|
||||
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 {
|
||||
groupDNs = append(groupDNs, entry.DN)
|
||||
}
|
||||
|
||||
groups := []string{}
|
||||
|
||||
// I guess it should work for most ldap providers
|
||||
for _, dn := range groupDNs {
|
||||
rdnParts, err := ldapgo.ParseDN(dn)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (ldap *LdapService) BindService(rebind bool) error {
|
||||
// Locks must not be used for initial binding attempt
|
||||
if rebind {
|
||||
|
||||
Reference in New Issue
Block a user