Compare commits

..
4 Commits
16 changed files with 194 additions and 178 deletions
-2
View File
@@ -101,8 +101,6 @@ TINYAUTH_AUTH_SESSIONMAXLIFETIME=0
TINYAUTH_AUTH_LOGINTIMEOUT=300
# Maximum login retries.
TINYAUTH_AUTH_LOGINMAXRETRIES=3
# Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically.
TINYAUTH_AUTH_LOCKDOWNENABLED=true
# Comma-separated list of trusted proxy addresses.
TINYAUTH_AUTH_TRUSTEDPROXIES=
# ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow.
+11 -11
View File
@@ -62,18 +62,19 @@ If you like, you can help translate Tinyauth into more languages by visiting the
Tinyauth is licensed under the GNU Affero General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) AGPL-licensed code must also be made available under the AGPL along with build & install instructions. If you run a modified version over a network, you must also make the source available to the users of that service. For more information about the license check the [license](LICENSE) file.
## Hosting Partners
If you use one of our partners, you can help support us while getting a great hosting deal.
<div>
<a title="InstaPods" target="_blank" href="https://app.instapods.com/dashboard/pods/create?app=tinyauth&ref=tinyauth"><img src="https://instapods.com/deploy-button.svg"></a>
</div>
## Sponsors
A big thank you to the following people for providing me with more coffee:
A big thank-you to the following individuals and companies
for their support for Tinyauth.
### Companies and Organizations
- [JetBrains for Open-Source](https://jb.gg/OpenSource)
- [CodeRabbit AI](https://www.coderabbit.ai)
- [Dosu](https://dosu.dev/)
- [InstaPods](https://instapods.com) - [Deploy Tinyauth from 3$/month](https://app.instapods.com/dashboard/pods/create?app=tinyauth&ref=tinyauth)
### Individuals
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https:&#x2F;&#x2F;github.com&#x2F;erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a>&nbsp;&nbsp;<a href="https://github.com/nicotsx"><img src="https:&#x2F;&#x2F;github.com&#x2F;nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a>&nbsp;&nbsp;<a href="https://github.com/SimpleHomelab"><img src="https:&#x2F;&#x2F;github.com&#x2F;SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a>&nbsp;&nbsp;<a href="https://github.com/jmadden91"><img src="https:&#x2F;&#x2F;github.com&#x2F;jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a>&nbsp;&nbsp;<a href="https://github.com/tribor"><img src="https:&#x2F;&#x2F;github.com&#x2F;tribor.png" width="64px" alt="User avatar: tribor" /></a>&nbsp;&nbsp;<a href="https://github.com/eliasbenb"><img src="https:&#x2F;&#x2F;github.com&#x2F;eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a>&nbsp;&nbsp;<a href="https://github.com/afunworm"><img src="https:&#x2F;&#x2F;github.com&#x2F;afunworm.png" width="64px" alt="User avatar: afunworm" /></a>&nbsp;&nbsp;<a href="https://github.com/chip-well"><img src="https:&#x2F;&#x2F;github.com&#x2F;chip-well.png" width="64px" alt="User avatar: chip-well" /></a>&nbsp;&nbsp;<a href="https://github.com/Lancelot-Enguerrand"><img src="https:&#x2F;&#x2F;github.com&#x2F;Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a>&nbsp;&nbsp;<a href="https://github.com/allgoewer"><img src="https:&#x2F;&#x2F;github.com&#x2F;allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a>&nbsp;&nbsp;<a href="https://github.com/NEANC"><img src="https:&#x2F;&#x2F;github.com&#x2F;NEANC.png" width="64px" alt="User avatar: NEANC" /></a>&nbsp;&nbsp;<a href="https://github.com/axjab"><img src="https:&#x2F;&#x2F;github.com&#x2F;axjab.png" width="64px" alt="User avatar: axjab" /></a>&nbsp;&nbsp;<a href="https://github.com/stegratech"><img src="https:&#x2F;&#x2F;github.com&#x2F;stegratech.png" width="64px" alt="User avatar: stegratech" /></a>&nbsp;&nbsp;<a href="https://github.com/apearson"><img src="https:&#x2F;&#x2F;github.com&#x2F;apearson.png" width="64px" alt="User avatar: apearson" /></a>&nbsp;&nbsp;<a href="https://github.com/Micky5991"><img src="https:&#x2F;&#x2F;github.com&#x2F;Micky5991.png" width="64px" alt="User avatar: Micky5991" /></a>&nbsp;&nbsp;<!-- sponsors -->
@@ -81,7 +82,6 @@ A big thank you to the following people for providing me with more coffee:
- **Freepik** for providing the police hat and badge.
- **Renee French** for the original gopher logo.
- **Coderabbit AI** for providing free AI code reviews.
- **Syrhu** for providing the background image of the app.
## Star History
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

After

Width:  |  Height:  |  Size: 3.7 MiB

+10
View File
@@ -30,6 +30,16 @@ func (app *BootstrapApp) setupRouter() error {
if err != nil {
return fmt.Errorf("failed to set trusted proxies: %w", err)
}
app.runtime.TrustedProxiesConfigured = true
} else {
err := engine.SetTrustedProxies(nil)
if err != nil {
return fmt.Errorf("failed to set trusted proxies: %w", err)
}
app.log.App.Warn().Msg("Trusted proxies are not configured, IP access controls will NOT work")
}
middlewareProvideFor := []any{
+4 -3
View File
@@ -111,9 +111,10 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
clientIP := c.ClientIP()
aclsCtx := &service.ACLContext{
ACLs: acls,
IP: net.ParseIP(clientIP),
Path: proxyCtx.Path,
ACLs: acls,
IP: net.ParseIP(clientIP),
Path: proxyCtx.Path,
TrustedProxiesConfigured: controller.runtime.TrustedProxiesConfigured,
}
if controller.policyEngine.Evaluate(service.RuleIPBypassed, aclsCtx) {
+16 -25
View File
@@ -72,27 +72,12 @@ func (controller *UserController) loginHandler(c *gin.Context) {
controller.log.App.Debug().Str("username", req.Username).Msg("Login attempt")
isLocked, remaining := controller.auth.IsAccountLocked(req.Username)
if isLocked {
controller.log.App.Warn().Str("username", req.Username).Msg("Account is locked due to too many failed login attempts")
controller.log.AuditLoginFailure(req.Username, "local", c.ClientIP(), "account locked")
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
c.JSON(429, gin.H{
"status": 429,
"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remaining),
})
return
}
search, err := controller.auth.SearchUser(req.Username)
if err != nil {
if errors.Is(err, service.ErrUserNotFound) {
controller.auth.DummyPasswordCheck(req.Password)
controller.log.App.Warn().Str("username", req.Username).Msg("User not found during login attempt")
controller.auth.RecordLoginAttempt(req.Username, false)
controller.log.AuditLoginFailure(req.Username, "unknown", c.ClientIP(), "user not found")
c.JSON(401, gin.H{
"status": 401,
@@ -108,14 +93,24 @@ func (controller *UserController) loginHandler(c *gin.Context) {
return
}
isLocked, remaining := controller.auth.IsAccountLocked(req.Username)
if isLocked {
controller.log.App.Warn().Str("username", req.Username).Msg("Account is locked due to too many failed login attempts")
controller.log.AuditLoginFailure(req.Username, search.Type.String(), c.ClientIP(), "account locked")
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
c.JSON(429, gin.H{
"status": 429,
"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remaining),
})
return
}
if err := controller.auth.CheckUserPassword(*search, req.Password); err != nil {
controller.log.App.Warn().Str("username", req.Username).Msg("Invalid password during login attempt")
controller.auth.RecordLoginAttempt(req.Username, false)
if search.Type == model.UserLocal {
controller.log.AuditLoginFailure(req.Username, "local", c.ClientIP(), "invalid password")
} else {
controller.log.AuditLoginFailure(req.Username, "ldap", c.ClientIP(), "invalid password")
}
controller.log.AuditLoginFailure(req.Username, search.Type.String(), c.ClientIP(), "invalid password")
c.JSON(401, gin.H{
"status": 401,
"message": "Unauthorized",
@@ -216,11 +211,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
controller.log.App.Info().Str("username", req.Username).Msg("Login successful")
if search.Type == model.UserLocal {
controller.log.AuditLoginSuccess(req.Username, "local", c.ClientIP())
} else {
controller.log.AuditLoginSuccess(req.Username, "ldap", c.ClientIP())
}
controller.log.AuditLoginSuccess(req.Username, search.Type.String(), c.ClientIP())
controller.auth.RecordLoginAttempt(req.Username, true)
-2
View File
@@ -49,7 +49,6 @@ func NewDefaultConfiguration(runtimeEnv RuntimeEnv) *Config {
ACLs: ACLsConfig{
Policy: "allow",
},
LockdownEnabled: true,
},
UI: UIConfig{
Title: "Tinyauth",
@@ -151,7 +150,6 @@ type AuthConfig struct {
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime,omitempty"`
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout,omitempty"`
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries,omitempty"`
LockdownEnabled bool `description:"Enable lockdown mode after maximum login retries. Lockdown mode limit is calculated automatically." yaml:"lockdownEnabled,omitempty"`
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies,omitempty"`
ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls,omitempty"`
}
+12 -11
View File
@@ -1,17 +1,18 @@
package model
type RuntimeConfig struct {
AppURL string
UUID string
CookieDomain string
SessionCookieName string
CSRFCookieName string
RedirectCookieName string
OAuthSessionCookieName string
LocalUsers []LocalUser
OAuthProviders map[string]OAuthServiceConfig
OAuthWhitelist []string
ConfiguredProviders []Provider
AppURL string
UUID string
CookieDomain string
SessionCookieName string
CSRFCookieName string
RedirectCookieName string
OAuthSessionCookieName string
LocalUsers []LocalUser
OAuthProviders map[string]OAuthServiceConfig
OAuthWhitelist []string
ConfiguredProviders []Provider
TrustedProxiesConfigured bool
}
type Provider struct {
+10
View File
@@ -7,6 +7,16 @@ const (
UserLDAP
)
func (t UserSearchType) String() string {
switch t {
case UserLocal:
return "local"
case UserLDAP:
return "ldap"
}
return "unknown"
}
type LDAPUser struct {
DN string
Groups []string
@@ -215,6 +215,10 @@ type IPAllowedRule struct {
}
func (rule *IPAllowedRule) Evaluate(ctx *ACLContext) Effect {
if !ctx.TrustedProxiesConfigured {
return EffectAllow // We can't block the proxy
}
// merge global and per-app block/allow lists
blockedIps := append([]string{}, rule.Config.Auth.IP.Block...)
allowedIPs := append([]string{}, rule.Config.Auth.IP.Allow...)
@@ -263,6 +267,10 @@ type IPBypassedRule struct {
}
func (rule *IPBypassedRule) Evaluate(ctx *ACLContext) Effect {
if !ctx.TrustedProxiesConfigured {
return EffectDeny
}
// merge global and per-app bypass lists
bypassList := append([]string{}, rule.Config.Auth.IP.Bypass...)
if ctx.ACLs != nil {
+62 -26
View File
@@ -611,11 +611,20 @@ func TestIPAllowedRule(t *testing.T) {
ctx *ACLContext
expected Effect
}{
{
name: "when trusted proxies are not configured, IP is allowed",
ctx: &ACLContext{
ACLs: &model.App{},
IP: net.ParseIP("10.0.0.1"),
},
expected: EffectAllow,
},
{
name: "allows when ACLs are nil and no global lists configured",
ctx: &ACLContext{
ACLs: nil,
IP: net.ParseIP("10.0.0.1"),
ACLs: nil,
IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
},
expected: EffectAllow,
},
@@ -625,7 +634,8 @@ func TestIPAllowedRule(t *testing.T) {
ACLs: &model.App{
IP: model.AppIP{Block: []string{"10.0.0.1"}},
},
IP: net.ParseIP("10.0.0.1"),
IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
},
expected: EffectDeny,
},
@@ -637,8 +647,9 @@ func TestIPAllowedRule(t *testing.T) {
},
},
ctx: &ACLContext{
ACLs: &model.App{},
IP: net.ParseIP("10.0.0.5"),
ACLs: &model.App{},
IP: net.ParseIP("10.0.0.5"),
TrustedProxiesConfigured: true,
},
expected: EffectDeny,
},
@@ -648,7 +659,8 @@ func TestIPAllowedRule(t *testing.T) {
ACLs: &model.App{
IP: model.AppIP{Allow: []string{"192.168.1.0/24"}},
},
IP: net.ParseIP("192.168.1.10"),
IP: net.ParseIP("192.168.1.10"),
TrustedProxiesConfigured: true,
},
expected: EffectAllow,
},
@@ -660,8 +672,9 @@ func TestIPAllowedRule(t *testing.T) {
},
},
ctx: &ACLContext{
ACLs: &model.App{},
IP: net.ParseIP("192.168.1.10"),
ACLs: &model.App{},
IP: net.ParseIP("192.168.1.10"),
TrustedProxiesConfigured: true,
},
expected: EffectAllow,
},
@@ -671,15 +684,17 @@ func TestIPAllowedRule(t *testing.T) {
ACLs: &model.App{
IP: model.AppIP{Allow: []string{"192.168.1.0/24"}},
},
IP: net.ParseIP("10.0.0.1"),
IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
},
expected: EffectDeny,
},
{
name: "allows when no block or allow lists are configured",
ctx: &ACLContext{
ACLs: &model.App{},
IP: net.ParseIP("10.0.0.1"),
ACLs: &model.App{},
IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
},
expected: EffectAllow,
},
@@ -692,7 +707,8 @@ func TestIPAllowedRule(t *testing.T) {
Allow: []string{"10.0.0.1"},
},
},
IP: net.ParseIP("10.0.0.1"),
IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
},
expected: EffectDeny,
},
@@ -705,7 +721,8 @@ func TestIPAllowedRule(t *testing.T) {
Allow: []string{"10.0.0.1"},
},
},
IP: net.ParseIP("10.0.0.1"),
IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
},
expected: EffectAllow,
},
@@ -735,12 +752,23 @@ func TestIPBypassedRule(t *testing.T) {
ctx *ACLContext
expected Effect
}{
{
name: "when trusted proxies are not configured, IP is not bypassed",
rule: defaultIPBR,
ctx: &ACLContext{
ACLs: &model.App{},
IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: false,
},
expected: EffectDeny,
},
{
name: "deny when ACLs are nil and no global bypass",
rule: defaultIPBR,
ctx: &ACLContext{
ACLs: nil,
IP: net.ParseIP("10.0.0.1"),
ACLs: nil,
IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
},
expected: EffectDeny,
},
@@ -748,8 +776,9 @@ func TestIPBypassedRule(t *testing.T) {
name: "allows when ACLs are nil but IP matches global bypass",
rule: globBypassIPBR,
ctx: &ACLContext{
ACLs: nil,
IP: net.ParseIP("10.0.0.5"),
ACLs: nil,
IP: net.ParseIP("10.0.0.5"),
TrustedProxiesConfigured: true,
},
expected: EffectAllow,
},
@@ -757,8 +786,9 @@ func TestIPBypassedRule(t *testing.T) {
name: "denies when ACLs are nil and IP does not match global bypass",
rule: globBypassIPBR,
ctx: &ACLContext{
ACLs: nil,
IP: net.ParseIP("192.168.1.1"),
ACLs: nil,
IP: net.ParseIP("192.168.1.1"),
TrustedProxiesConfigured: true,
},
expected: EffectDeny,
},
@@ -769,7 +799,8 @@ func TestIPBypassedRule(t *testing.T) {
ACLs: &model.App{
IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}},
},
IP: net.ParseIP("10.0.0.5"),
IP: net.ParseIP("10.0.0.5"),
TrustedProxiesConfigured: true,
},
expected: EffectAllow,
},
@@ -780,7 +811,8 @@ func TestIPBypassedRule(t *testing.T) {
ACLs: &model.App{
IP: model.AppIP{Bypass: []string{"172.16.0.0/24"}},
},
IP: net.ParseIP("10.0.0.5"),
IP: net.ParseIP("10.0.0.5"),
TrustedProxiesConfigured: true,
},
expected: EffectAllow,
},
@@ -791,7 +823,8 @@ func TestIPBypassedRule(t *testing.T) {
ACLs: &model.App{
IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}},
},
IP: net.ParseIP("10.0.0.5"),
IP: net.ParseIP("10.0.0.5"),
TrustedProxiesConfigured: true,
},
expected: EffectAllow,
},
@@ -802,7 +835,8 @@ func TestIPBypassedRule(t *testing.T) {
ACLs: &model.App{
IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}},
},
IP: net.ParseIP("192.168.1.1"),
IP: net.ParseIP("192.168.1.1"),
TrustedProxiesConfigured: true,
},
expected: EffectDeny,
},
@@ -810,8 +844,9 @@ func TestIPBypassedRule(t *testing.T) {
name: "denies when bypass list is empty",
rule: defaultIPBR,
ctx: &ACLContext{
ACLs: &model.App{},
IP: net.ParseIP("10.0.0.1"),
ACLs: &model.App{},
IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
},
expected: EffectDeny,
},
@@ -822,7 +857,8 @@ func TestIPBypassedRule(t *testing.T) {
ACLs: &model.App{
IP: model.AppIP{Bypass: []string{"not-an-ip", "10.0.0.1"}},
},
IP: net.ParseIP("10.0.0.1"),
IP: net.ParseIP("10.0.0.1"),
TrustedProxiesConfigured: true,
},
expected: EffectAllow,
},
+21 -91
View File
@@ -8,7 +8,6 @@ import (
"math/big"
"net/http"
"strings"
"sync"
"time"
"github.com/steveiliop56/ding"
@@ -71,21 +70,11 @@ type AuthService struct {
dummyHash string
lockdown struct {
active bool
until time.Time
ctx context.Context
cancelFunc context.CancelFunc
mu sync.RWMutex
}
caches struct {
login *CacheStore[LoginAttempt]
oauth *CacheStore[OAuthPendingSession]
ldap *CacheStore[[]string]
}
maxLoginLimits int
}
type AuthServiceInput struct {
@@ -116,15 +105,6 @@ func NewAuthService(i AuthServiceInput) (*AuthService, error) {
policyEngine: i.PolicyEngine,
}
// get the max login limits based on the number of users and the configured max retries
service.maxLoginLimits = service.calculateLockdownLimit()
loginCacheSize := 0
if !service.config.Auth.LockdownEnabled {
loginCacheSize = service.maxLoginLimits
}
// dummy hash
dummyHash, err := bcrypt.GenerateFromPassword([]byte(utils.GenerateString(8)), bcrypt.DefaultCost)
@@ -136,7 +116,7 @@ func NewAuthService(i AuthServiceInput) (*AuthService, error) {
// caches setup
oauthCache := NewCacheStore[OAuthPendingSession](256)
loginCache := NewCacheStore[LoginAttempt](loginCacheSize)
loginCache := NewCacheStore[LoginAttempt](service.calculateLockdownLimit())
ldapCache := NewCacheStore[[]string](1024)
service.caches.oauth = oauthCache
@@ -159,6 +139,23 @@ func NewAuthService(i AuthServiceInput) (*AuthService, error) {
}
}, ding.RingMinor)
i.Ding.Go(func(ctx context.Context) {
ticker := time.NewTicker(15 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
service.log.App.Debug().Msg("Updating login cache limits")
service.caches.login.SetMaxSize(service.calculateLockdownLimit())
service.log.App.Debug().Msg("Login cache limits updated")
case <-ctx.Done():
return
}
}
}, ding.RingMinor)
return service, nil
}
@@ -260,10 +257,6 @@ func (auth *AuthService) GetLDAPUser(userDN string) (*model.LDAPUser, error) {
}
func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) {
if locked, remaining := auth.IsInLockdown(); locked {
return true, remaining
}
if auth.config.Auth.LoginMaxRetries <= 0 || auth.config.Auth.LoginTimeout <= 0 {
return false, 0
}
@@ -286,14 +279,6 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
return
}
if !success && auth.config.Auth.LockdownEnabled && auth.caches.login.Size() >= auth.maxLoginLimits {
if locked, _ := auth.IsInLockdown(); locked {
return
}
go auth.lockdownMode()
return
}
auth.caches.login.WithLock(func(actions CacheStoreActions[LoginAttempt]) {
entry, ok := actions.Get(identifier)
@@ -359,7 +344,7 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess
return nil, fmt.Errorf("tailscale service not configured, cannot create session for tailscale user")
}
uuid, err := uuid.NewRandom()
u, err := uuid.NewRandom()
if err != nil {
return nil, fmt.Errorf("failed to generate session uuid: %w", err)
@@ -376,7 +361,7 @@ func (auth *AuthService) CreateSession(ctx context.Context, data repository.Sess
expiresAt := time.Now().Add(time.Duration(expiry) * time.Second)
session := repository.CreateSessionParams{
UUID: uuid.String(),
UUID: u.String(),
Username: data.Username,
Email: data.Email,
Name: data.Name,
@@ -631,62 +616,7 @@ func (auth *AuthService) GetOAuthPendingSession(sessionId string) (*OAuthPending
return &session, nil
}
func (auth *AuthService) lockdownMode() {
auth.lockdown.mu.Lock()
if auth.lockdown.active {
auth.lockdown.mu.Unlock()
return
}
ctx, cancel := context.WithCancel(auth.ctx)
auth.log.App.Warn().Msg("Too many failed login attempts, entering lockdown mode")
auth.lockdown.active = true
auth.lockdown.ctx = ctx
auth.lockdown.cancelFunc = cancel
d := time.Duration(auth.config.Auth.LoginTimeout) * time.Second
auth.lockdown.until = time.Now().Add(d)
timer := time.NewTimer(d)
auth.lockdown.mu.Unlock()
defer cancel()
defer timer.Stop()
select {
case <-timer.C:
// Timer expired, end lockdown
case <-ctx.Done():
// Context cancelled, end lockdown
}
auth.lockdown.mu.Lock()
auth.log.App.Info().Msg("Exiting lockdown mode")
auth.caches.login.Clear()
auth.lockdown.active = false
auth.lockdown.until = time.Time{}
auth.lockdown.ctx = nil
auth.lockdown.cancelFunc = nil
auth.lockdown.mu.Unlock()
}
func (auth *AuthService) IsInLockdown() (bool, int) {
auth.lockdown.mu.RLock()
defer auth.lockdown.mu.RUnlock()
if auth.lockdown.active {
remaining := int(time.Until(auth.lockdown.until).Seconds())
return true, remaining
}
return false, 0
}
// mostly a testing function, not useful for anything else
// ClearLoginAttempts is a testing function, not useful for anything else
func (auth *AuthService) ClearLoginAttempts() {
auth.caches.login.Clear()
}
+16
View File
@@ -195,3 +195,19 @@ func (cs *CacheStore[T]) Clear() {
cs.cache = make(map[string]cacheEntry[T])
cs.order = make([]string, 0)
}
func (cs *CacheStore[T]) SetMaxSize(maxSize int) {
cs.mu.Lock()
defer cs.mu.Unlock()
cs.maxSize = maxSize
for len(cs.cache) > maxSize {
if !cs.evictOne() {
break
}
}
}
func (cs *CacheStore[T]) GetMaxSize() int {
cs.mu.Lock()
defer cs.mu.Unlock()
return cs.maxSize
}
+15
View File
@@ -316,6 +316,21 @@ func TestCacheStoreSizeAndClear(t *testing.T) {
assert.False(t, ok)
}
func TestCacheStoreWithMaxSize(t *testing.T) {
cs := NewCacheStore[string](0)
assert.Equal(t, 0, cs.Size())
for i := 0; i < 100; i++ {
cs.Set(strconv.Itoa(i), strconv.Itoa(i), 0)
}
assert.Equal(t, 100, cs.Size())
cs.SetMaxSize(10)
assert.Equal(t, 10, cs.Size())
}
func TestCacheStoreWithLock(t *testing.T) {
cs := NewCacheStore[int](0)
cs.Set("counter", 1, 0)
+5 -4
View File
@@ -29,10 +29,11 @@ type Rule interface {
}
type ACLContext struct {
ACLs *model.App
UserContext *model.UserContext
IP net.IP
Path string
ACLs *model.App
UserContext *model.UserContext
IP net.IP
Path string
TrustedProxiesConfigured bool
}
type PolicyEngine struct {
+4 -3
View File
@@ -163,9 +163,10 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
},
},
},
CookieDomain: "example.com",
AppURL: "https://tinyauth.example.com",
SessionCookieName: "tinyauth-session",
CookieDomain: "example.com",
AppURL: "https://tinyauth.example.com",
SessionCookieName: "tinyauth-session",
TrustedProxiesConfigured: true,
}
return config, runtime