Compare commits

..
7 Commits
13 changed files with 81 additions and 57 deletions
-4
View File
@@ -83,7 +83,3 @@ for their support for Tinyauth.
- **Freepik** for providing the police hat and badge. - **Freepik** for providing the police hat and badge.
- **Renee French** for the original gopher logo. - **Renee French** for the original gopher logo.
- **Syrhu** for providing the background image of the app. - **Syrhu** for providing the background image of the app.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=tinyauthapp/tinyauth&type=Date)](https://www.star-history.com/#tinyauthapp/tinyauth&Date)
+2 -2
View File
@@ -116,11 +116,11 @@ func generateTotpCmd() *cli.Command {
userStr := fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TOTPSecret) userStr := fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TOTPSecret)
fmt.Print("\nOr add the following TOTP secret to your authenticator app: ") fmt.Print("\nOr add the following TOTP secret to your authenticator app: ")
fmt.Print(colors.green.Render(secret)) fmt.Print(colors.blue.Render(secret))
fmt.Print("\n\n") fmt.Print("\n\n")
fmt.Printf("Finally, add your user '%s' back to your configuration: ", user.Username) fmt.Printf("Finally, add your user '%s' back to your configuration: ", user.Username)
fmt.Print(colors.green.Render(userStr)) fmt.Print(colors.blue.Render(userStr))
fmt.Print("\n") fmt.Print("\n")
return nil return nil
+5 -5
View File
@@ -3,7 +3,6 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"reflect"
"strings" "strings"
"charm.land/huh/v2" "charm.land/huh/v2"
@@ -32,10 +31,11 @@ func main() {
Configuration: tConfig, Configuration: tConfig,
Resources: loaders, Resources: loaders,
Run: func(_ []string) error { Run: func(_ []string) error {
if !reflect.DeepEqual(model.NewDefaultConfiguration(env).Experimental, tConfig.Experimental) { // enable this on experimental features
colors := getColors() //if !reflect.DeepEqual(model.NewDefaultConfiguration(env).Experimental, tConfig.Experimental) {
fmt.Println(colors.yellow.Render("⚠") + " Experimental features are enabled, use with caution. Experimental features may change with each release.") // colors := getColors()
} // fmt.Println(colors.yellow.Render("⚠") + " Experimental features are enabled, use with caution. Experimental features may change with each release.")
//}
return runCmd(*tConfig) return runCmd(*tConfig)
}, },
} }
+8 -4
View File
@@ -85,7 +85,8 @@ func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) {
err := app.dig.Provide(service.NewKubernetesService) err := app.dig.Provide(service.NewKubernetesService)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to provide kubernetes service: %w", err) app.log.App.Warn().Err(err).Msg("Failed to provide kubernetes service")
return nil, nil
} }
err = app.dig.Invoke(func(k *service.KubernetesService) error { err = app.dig.Invoke(func(k *service.KubernetesService) error {
@@ -94,7 +95,8 @@ func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) {
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to invoke kubernetes service: %w", err) app.log.App.Warn().Err(err).Msg("Failed to invoke kubernetes service")
return nil, nil
} }
// Kubernetes will fail to initialize with an error if it cannot connect to the cluster // Kubernetes will fail to initialize with an error if it cannot connect to the cluster
@@ -114,7 +116,8 @@ func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) {
err := app.dig.Provide(service.NewDockerService) err := app.dig.Provide(service.NewDockerService)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to provide docker service: %w", err) app.log.App.Warn().Err(err).Msg("Failed to provide docker service")
return nil, nil
} }
err = app.dig.Invoke(func(d *service.DockerService) error { err = app.dig.Invoke(func(d *service.DockerService) error {
@@ -123,7 +126,8 @@ func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) {
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to invoke docker service: %w", err) app.log.App.Warn().Err(err).Msg("Failed to invoke docker service")
return nil, nil
} }
if app.services.dockerService == nil { if app.services.dockerService == nil {
+1 -1
View File
@@ -282,7 +282,7 @@ func (m *ContextMiddleware) basicAuth(username string, password string) (*model.
} }
userContext.Provider = model.ProviderLocal userContext.Provider = model.ProviderLocal
case model.UserLDAP: case model.UserLDAP:
user, err := m.auth.GetLDAPUser(username) user, err := m.auth.GetLDAPUser(search.Username)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("error retrieving ldap user details: %w", err) return nil, nil, fmt.Errorf("error retrieving ldap user details: %w", err)
+18 -17
View File
@@ -101,22 +101,23 @@ func NewDefaultConfiguration(runtimeEnv RuntimeEnv) *Config {
} }
type Config struct { type Config struct {
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl,omitempty"` AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl,omitempty"`
ConfigFile string `description:"Path to config file." yaml:"-" gen:"include"` ConfigFile string `description:"Path to config file." yaml:"-" gen:"include"`
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider,omitempty"` LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider,omitempty"`
Database DatabaseConfig `description:"Database configuration." yaml:"database,omitempty"` Database DatabaseConfig `description:"Database configuration." yaml:"database,omitempty"`
Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics,omitempty"` Analytics AnalyticsConfig `description:"Analytics configuration." yaml:"analytics,omitempty"`
Resources ResourcesConfig `description:"Resources configuration." yaml:"resources,omitempty"` Resources ResourcesConfig `description:"Resources configuration." yaml:"resources,omitempty"`
Server ServerConfig `description:"Server configuration." yaml:"server,omitempty"` Server ServerConfig `description:"Server configuration." yaml:"server,omitempty"`
Auth AuthConfig `description:"Authentication configuration." yaml:"auth,omitempty"` Auth AuthConfig `description:"Authentication configuration." yaml:"auth,omitempty"`
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps,omitempty"` Apps map[string]App `description:"Application ACLs configuration." yaml:"apps,omitempty"`
OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth,omitempty"` OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth,omitempty"`
OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc,omitempty"` OIDC OIDCConfig `description:"OIDC configuration." yaml:"oidc,omitempty"`
UI UIConfig `description:"UI customization." yaml:"ui,omitempty"` UI UIConfig `description:"UI customization." yaml:"ui,omitempty"`
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap,omitempty"` LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap,omitempty"`
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental,omitempty"` // enable the cli warning on experimental features
Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale,omitempty"` //Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental,omitempty"`
Log LogConfig `description:"Logging configuration." yaml:"log,omitempty"` Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale,omitempty"`
Log LogConfig `description:"Logging configuration." yaml:"log,omitempty"`
} }
type DatabaseConfig struct { type DatabaseConfig struct {
@@ -237,7 +238,7 @@ type LogStreamConfig struct {
Level string `description:"Log level for this stream. Use global if empty." yaml:"level,omitempty"` Level string `description:"Log level for this stream. Use global if empty." yaml:"level,omitempty"`
} }
type ExperimentalConfig struct{} //type ExperimentalConfig struct{}
type TailscaleConfig struct { type TailscaleConfig struct {
Enabled bool `description:"Enable Tailscale integration." yaml:"enabled,omitempty"` Enabled bool `description:"Enable Tailscale integration." yaml:"enabled,omitempty"`
+6 -1
View File
@@ -1,6 +1,7 @@
package service package service
import ( import (
"errors"
"regexp" "regexp"
"strings" "strings"
@@ -43,6 +44,10 @@ func (rule *UserAllowedRule) Evaluate(ctx *ACLContext) Effect {
rule.Log.App.Debug().Msg("User is an OAuth user, checking OAuth whitelist") rule.Log.App.Debug().Msg("User is an OAuth user, checking OAuth whitelist")
match, err := utils.CheckFilter(ctx.ACLs.OAuth.Whitelist, ctx.UserContext.OAuth.Email) match, err := utils.CheckFilter(ctx.ACLs.OAuth.Whitelist, ctx.UserContext.OAuth.Email)
if err != nil { if err != nil {
if errors.Is(err, utils.ErrFilterEmpty) {
rule.Log.App.Debug().Msg("OAuth whitelist is empty, abstaining")
return EffectAbstain
}
rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.OAuth.Email).Msg("Invalid entry in OAuth whitelist") rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.OAuth.Email).Msg("Invalid entry in OAuth whitelist")
return EffectDeny return EffectDeny
} }
@@ -72,7 +77,7 @@ func (rule *UserAllowedRule) Evaluate(ctx *ACLContext) Effect {
match, err := utils.CheckFilter(ctx.ACLs.Users.Allow, ctx.UserContext.GetUsername()) match, err := utils.CheckFilter(ctx.ACLs.Users.Allow, ctx.UserContext.GetUsername())
if err != nil { if err != nil {
if err == utils.ErrFilterEmpty { if errors.Is(err, utils.ErrFilterEmpty) {
return EffectAbstain return EffectAbstain
} }
rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.GetUsername()).Msg("Invalid entry in users allow list") rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.GetUsername()).Msg("Invalid entry in users allow list")
@@ -44,6 +44,21 @@ func TestUserAllowedRule(t *testing.T) {
}, },
expected: EffectAbstain, expected: EffectAbstain,
}, },
{
name: "abstains when filter is empty",
ctx: &ACLContext{
ACLs: &model.App{
OAuth: model.AppOAuth{Whitelist: ""},
},
UserContext: &model.UserContext{
Provider: model.ProviderOAuth,
OAuth: &model.OAuthContext{
BaseContext: model.BaseContext{Username: "alice"},
},
},
},
expected: EffectAbstain,
},
{ {
name: "allows OAuth user when email matches whitelist", name: "allows OAuth user when email matches whitelist",
ctx: &ACLContext{ ctx: &ACLContext{
+8 -7
View File
@@ -15,6 +15,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/repository" "github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
"github.com/tinyauthapp/tinyauth/pkg/cache"
"go.uber.org/dig" "go.uber.org/dig"
"github.com/google/uuid" "github.com/google/uuid"
@@ -71,9 +72,9 @@ type AuthService struct {
dummyHash string dummyHash string
caches struct { caches struct {
login *CacheStore[LoginAttempt] login *cache.CacheStore[LoginAttempt]
oauth *CacheStore[OAuthPendingSession] oauth *cache.CacheStore[OAuthPendingSession]
ldap *CacheStore[[]string] ldap *cache.CacheStore[[]string]
} }
} }
@@ -115,9 +116,9 @@ func NewAuthService(i AuthServiceInput) (*AuthService, error) {
service.dummyHash = string(dummyHash) service.dummyHash = string(dummyHash)
// caches setup // caches setup
oauthCache := NewCacheStore[OAuthPendingSession](256) oauthCache := cache.NewCacheStore[OAuthPendingSession](256)
loginCache := NewCacheStore[LoginAttempt](service.calculateLockdownLimit()) loginCache := cache.NewCacheStore[LoginAttempt](service.calculateLockdownLimit())
ldapCache := NewCacheStore[[]string](1024) ldapCache := cache.NewCacheStore[[]string](1024)
service.caches.oauth = oauthCache service.caches.oauth = oauthCache
service.caches.login = loginCache service.caches.login = loginCache
@@ -279,7 +280,7 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
return return
} }
auth.caches.login.WithLock(func(actions CacheStoreActions[LoginAttempt]) { auth.caches.login.WithLock(func(actions cache.CacheStoreActions[LoginAttempt]) {
entry, ok := actions.Get(identifier) entry, ok := actions.Get(identifier)
if !ok { if !ok {
+9 -8
View File
@@ -27,6 +27,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/repository" "github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
"github.com/tinyauthapp/tinyauth/pkg/cache"
"go.uber.org/dig" "go.uber.org/dig"
) )
@@ -158,9 +159,9 @@ type OIDCService struct {
issuer string issuer string
caches struct { caches struct {
code *CacheStore[AuthorizeCodeEntry] code *cache.CacheStore[AuthorizeCodeEntry]
usedCode *CacheStore[UsedCodeEntry] usedCode *cache.CacheStore[UsedCodeEntry]
authorize *CacheStore[AuthorizeRequest] authorize *cache.CacheStore[AuthorizeRequest]
} }
} }
@@ -339,11 +340,11 @@ func NewOIDCService(i OIDCServiceInput) (*OIDCService, error) {
i.Ding.Go(service.cleanupRoutine, ding.RingMinor) i.Ding.Go(service.cleanupRoutine, ding.RingMinor)
// Create caches // Create caches
codeCash := NewCacheStore[AuthorizeCodeEntry](256) codeCache := cache.NewCacheStore[AuthorizeCodeEntry](256)
usedCode := NewCacheStore[UsedCodeEntry](256) usedCode := cache.NewCacheStore[UsedCodeEntry](256)
authorize := NewCacheStore[AuthorizeRequest](256) authorize := cache.NewCacheStore[AuthorizeRequest](256)
service.caches.code = codeCash service.caches.code = codeCache
service.caches.usedCode = usedCode service.caches.usedCode = usedCode
service.caches.authorize = authorize service.caches.authorize = authorize
@@ -503,7 +504,7 @@ func (service *OIDCService) GetCodeEntry(codeHash string, clientId string) (*Aut
var entry AuthorizeCodeEntry var entry AuthorizeCodeEntry
var ok bool var ok bool
service.caches.code.WithLock(func(actions CacheStoreActions[AuthorizeCodeEntry]) { service.caches.code.WithLock(func(actions cache.CacheStoreActions[AuthorizeCodeEntry]) {
entry, ok = actions.Get(codeHash) entry, ok = actions.Get(codeHash)
if !ok { if !ok {
+5 -4
View File
@@ -10,6 +10,7 @@ import (
"github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/logger" "github.com/tinyauthapp/tinyauth/internal/utils/logger"
"github.com/tinyauthapp/tinyauth/pkg/cache"
"go.uber.org/dig" "go.uber.org/dig"
) )
@@ -59,8 +60,8 @@ type TailscaleService struct {
apiToken string apiToken string
caches struct { caches struct {
devices *CacheStore[tailscaleAPIDevices] devices *cache.CacheStore[tailscaleAPIDevices]
users *CacheStore[tailscaleAPIUsers] users *cache.CacheStore[tailscaleAPIUsers]
} }
urls struct { urls struct {
@@ -100,8 +101,8 @@ func NewTailscaleService(i TailscaleServiceInput) (*TailscaleService, error) {
apiToken: apiToken, apiToken: apiToken,
} }
devicesCache := NewCacheStore[tailscaleAPIDevices](0) devicesCache := cache.NewCacheStore[tailscaleAPIDevices](0)
usersCache := NewCacheStore[tailscaleAPIUsers](0) usersCache := cache.NewCacheStore[tailscaleAPIUsers](0)
s.caches.devices = devicesCache s.caches.devices = devicesCache
s.caches.users = usersCache s.caches.users = usersCache
@@ -1,4 +1,4 @@
package service package cache
import ( import (
"slices" "slices"
@@ -33,8 +33,8 @@ func NewCacheStore[T any](maxSize int) *CacheStore[T] {
} }
} }
// With lock allows performing multiple operations on the cache store atomically. // WithLock allows performing multiple operations on a single lock.
// The provided mutate function receives a set of actions (Set, Get, Delete) that // The provided mutate function receives a set of actions (Set, Get, Delete, Update) that
// can be used to manipulate the cache store within the locked context. // can be used to manipulate the cache store within the locked context.
func (cs *CacheStore[T]) WithLock(mutate func(actions CacheStoreActions[T])) { func (cs *CacheStore[T]) WithLock(mutate func(actions CacheStoreActions[T])) {
cs.mu.Lock() cs.mu.Lock()
@@ -1,4 +1,4 @@
package service package cache
import ( import (
"strconv" "strconv"