mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-10-27 20:25:41 +00:00
feat: add sqlite database for storing sessions (#326)
* feat: add sqlite database for storing sessions * refactor: use db instance instead of service in auth service * fix: coderabbit suggestions
This commit is contained in:
@@ -4,7 +4,12 @@ import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
// Frontend assets
|
||||
// Frontend
|
||||
//
|
||||
//go:embed dist
|
||||
var FrontendAssets embed.FS
|
||||
|
||||
// Migrations
|
||||
//
|
||||
//go:embed migrations/*.sql
|
||||
var Migrations embed.FS
|
||||
|
||||
1
internal/assets/migrations/000001_init_sqlite.down.sql
Normal file
1
internal/assets/migrations/000001_init_sqlite.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS "sessions";
|
||||
10
internal/assets/migrations/000001_init_sqlite.up.sql
Normal file
10
internal/assets/migrations/000001_init_sqlite.up.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS "sessions" (
|
||||
"uuid" TEXT NOT NULL PRIMARY KEY UNIQUE,
|
||||
"username" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"totp_pending" BOOLEAN NOT NULL,
|
||||
"oauth_groups" TEXT NULL,
|
||||
"expiry" INTEGER NOT NULL
|
||||
);
|
||||
@@ -57,19 +57,6 @@ func (app *BootstrapApp) Setup() error {
|
||||
csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId)
|
||||
redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
|
||||
|
||||
// Secrets
|
||||
encryptionSecret, err := utils.DeriveKey(app.Config.Secret, "encryption")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hmacSecret, err := utils.DeriveKey(app.Config.Secret, "hmac")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create configs
|
||||
authConfig := service.AuthServiceConfig{
|
||||
Users: users,
|
||||
@@ -80,8 +67,6 @@ func (app *BootstrapApp) Setup() error {
|
||||
LoginTimeout: app.Config.LoginTimeout,
|
||||
LoginMaxRetries: app.Config.LoginMaxRetries,
|
||||
SessionCookieName: sessionCookieName,
|
||||
HMACSecret: hmacSecret,
|
||||
EncryptionSecret: encryptionSecret,
|
||||
}
|
||||
|
||||
// Setup services
|
||||
@@ -107,8 +92,24 @@ func (app *BootstrapApp) Setup() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap database
|
||||
databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{
|
||||
DatabasePath: app.Config.DatabasePath,
|
||||
})
|
||||
|
||||
log.Debug().Str("service", fmt.Sprintf("%T", databaseService)).Msg("Initializing service")
|
||||
|
||||
err = databaseService.Init()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize database service: %w", err)
|
||||
}
|
||||
|
||||
database := databaseService.GetDatabase()
|
||||
|
||||
// Create services
|
||||
dockerService := service.NewDockerService()
|
||||
authService := service.NewAuthService(authConfig, dockerService, ldapService)
|
||||
authService := service.NewAuthService(authConfig, dockerService, ldapService, database)
|
||||
oauthBrokerService := service.NewOAuthBrokerService(app.getOAuthBrokerConfig())
|
||||
|
||||
// Initialize services
|
||||
|
||||
@@ -18,8 +18,6 @@ var RedirectCookieName = "tinyauth-redirect"
|
||||
type Config struct {
|
||||
Port int `mapstructure:"port" validate:"required"`
|
||||
Address string `validate:"required,ip4_addr" mapstructure:"address"`
|
||||
Secret string `validate:"required,len=32" mapstructure:"secret"`
|
||||
SecretFile string `mapstructure:"secret-file"`
|
||||
AppURL string `validate:"required,url" mapstructure:"app-url"`
|
||||
Users string `mapstructure:"users"`
|
||||
UsersFile string `mapstructure:"users-file"`
|
||||
@@ -56,6 +54,7 @@ type Config struct {
|
||||
LdapInsecure bool `mapstructure:"ldap-insecure"`
|
||||
LdapSearchFilter string `mapstructure:"ldap-search-filter"`
|
||||
ResourcesDir string `mapstructure:"resources-dir"`
|
||||
DatabasePath string `mapstructure:"database-path" validate:"required"`
|
||||
}
|
||||
|
||||
type OAuthLabels struct {
|
||||
@@ -112,6 +111,7 @@ type UserSearch struct {
|
||||
}
|
||||
|
||||
type SessionCookie struct {
|
||||
UUID string
|
||||
Username string
|
||||
Name string
|
||||
Email string
|
||||
|
||||
@@ -144,7 +144,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !controller.Auth.EmailWhitelisted(user.Email) {
|
||||
if !controller.Auth.IsEmailWhitelisted(user.Email) {
|
||||
queries, err := query.Values(config.UnauthorizedQuery{
|
||||
Username: user.Email,
|
||||
})
|
||||
@@ -169,8 +169,18 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1])
|
||||
}
|
||||
|
||||
var usename string
|
||||
|
||||
if user.PreferredUsername != "" {
|
||||
log.Debug().Msg("Using preferred username from OAuth provider")
|
||||
usename = user.PreferredUsername
|
||||
} else {
|
||||
log.Debug().Msg("No preferred username from OAuth provider, using pseudo username")
|
||||
usename = strings.Replace(user.Email, "@", "_", -1)
|
||||
}
|
||||
|
||||
controller.Auth.CreateSessionCookie(c, &config.SessionCookie{
|
||||
Username: user.Email,
|
||||
Username: usename,
|
||||
Name: name,
|
||||
Email: user.Email,
|
||||
Provider: req.Provider,
|
||||
|
||||
@@ -89,7 +89,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
if controller.Auth.BypassedIP(labels, clientIP) {
|
||||
if controller.Auth.IsBypassedIP(labels, clientIP) {
|
||||
c.Header("Authorization", c.Request.Header.Get("Authorization"))
|
||||
|
||||
headers := utils.ParseHeaders(labels.Headers)
|
||||
@@ -135,7 +135,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
authEnabled, err := controller.Auth.AuthEnabled(uri, labels)
|
||||
authEnabled, err := controller.Auth.IsAuthEnabled(uri, labels)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
|
||||
@@ -195,7 +195,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
if userContext.IsLoggedIn {
|
||||
appAllowed := controller.Auth.ResourceAllowed(c, userContext, labels)
|
||||
appAllowed := controller.Auth.IsResourceAllowed(c, userContext, labels)
|
||||
|
||||
if !appAllowed {
|
||||
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource")
|
||||
@@ -229,7 +229,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
if userContext.OAuth {
|
||||
groupOK := controller.Auth.OAuthGroup(c, userContext, labels)
|
||||
groupOK := controller.Auth.IsInOAuthGroup(c, userContext, labels)
|
||||
|
||||
if !groupOK {
|
||||
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements")
|
||||
|
||||
@@ -83,7 +83,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
goto basic
|
||||
}
|
||||
|
||||
if !m.Auth.EmailWhitelisted(cookie.Email) {
|
||||
if !m.Auth.IsEmailWhitelisted(cookie.Email) {
|
||||
log.Debug().Msg("Email from session cookie not whitelisted")
|
||||
m.Auth.DeleteSessionCookie(c)
|
||||
goto basic
|
||||
|
||||
@@ -49,18 +49,24 @@ func (m *ZerologMiddleware) Middleware() gin.HandlerFunc {
|
||||
|
||||
latency := time.Since(tStart).String()
|
||||
|
||||
// logPath check if the path should be logged normally or with debug
|
||||
subLogger := log.With().Str("method", method).
|
||||
Str("path", path).
|
||||
Str("address", address).
|
||||
Str("client_ip", clientIP).
|
||||
Int("status", code).
|
||||
Str("latency", latency).Logger()
|
||||
|
||||
if m.logPath(method + " " + path) {
|
||||
switch {
|
||||
case code >= 200 && code < 300:
|
||||
log.Info().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request")
|
||||
case code >= 300 && code < 400:
|
||||
log.Warn().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request")
|
||||
case code >= 400:
|
||||
log.Error().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request")
|
||||
case code >= 400 && code < 500:
|
||||
subLogger.Warn().Msg("Client Error")
|
||||
case code >= 500:
|
||||
subLogger.Error().Msg("Server Error")
|
||||
default:
|
||||
subLogger.Info().Msg("Request")
|
||||
}
|
||||
} else {
|
||||
log.Debug().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request")
|
||||
subLogger.Debug().Msg("Request")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
internal/model/session_model.go
Normal file
12
internal/model/session_model.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
type Session struct {
|
||||
UUID string `gorm:"column:uuid;primaryKey"`
|
||||
Username string `gorm:"column:username"`
|
||||
Email string `gorm:"column:email"`
|
||||
Name string `gorm:"column:name"`
|
||||
Provider string `gorm:"column:provider"`
|
||||
TOTPPending bool `gorm:"column:totp_pending"`
|
||||
OAuthGroups string `gorm:"column:oauth_groups"`
|
||||
Expiry int64 `gorm:"column:expiry"`
|
||||
}
|
||||
@@ -7,12 +7,14 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
"tinyauth/internal/config"
|
||||
"tinyauth/internal/model"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type LoginAttempt struct {
|
||||
@@ -30,8 +32,6 @@ type AuthServiceConfig struct {
|
||||
LoginTimeout int
|
||||
LoginMaxRetries int
|
||||
SessionCookieName string
|
||||
HMACSecret string
|
||||
EncryptionSecret string
|
||||
}
|
||||
|
||||
type AuthService struct {
|
||||
@@ -39,49 +39,24 @@ type AuthService struct {
|
||||
Docker *DockerService
|
||||
LoginAttempts map[string]*LoginAttempt
|
||||
LoginMutex sync.RWMutex
|
||||
Store *sessions.CookieStore
|
||||
LDAP *LdapService
|
||||
Database *gorm.DB
|
||||
}
|
||||
|
||||
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService) *AuthService {
|
||||
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, database *gorm.DB) *AuthService {
|
||||
return &AuthService{
|
||||
Config: config,
|
||||
Docker: docker,
|
||||
LoginAttempts: make(map[string]*LoginAttempt),
|
||||
LDAP: ldap,
|
||||
Database: database,
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *AuthService) Init() error {
|
||||
store := sessions.NewCookieStore([]byte(auth.Config.HMACSecret), []byte(auth.Config.EncryptionSecret))
|
||||
store.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: auth.Config.SessionExpiry,
|
||||
Secure: auth.Config.SecureCookie,
|
||||
HttpOnly: true,
|
||||
Domain: fmt.Sprintf(".%s", auth.Config.Domain),
|
||||
}
|
||||
|
||||
auth.Store = store
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *AuthService) GetSession(c *gin.Context) (*sessions.Session, error) {
|
||||
session, err := auth.Store.Get(c.Request, auth.Config.SessionCookieName)
|
||||
|
||||
// If there was an error getting the session, it might be invalid so let's clear it and retry
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("Error getting session, creating a new one")
|
||||
c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true)
|
||||
session, err = auth.Store.New(c.Request, auth.Config.SessionCookieName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (auth *AuthService) SearchUser(username string) config.UserSearch {
|
||||
if auth.GetLocalUser(username).Username != "" {
|
||||
return config.UserSearch{
|
||||
@@ -158,30 +133,24 @@ func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) {
|
||||
auth.LoginMutex.RLock()
|
||||
defer auth.LoginMutex.RUnlock()
|
||||
|
||||
// Return false if rate limiting is not configured
|
||||
if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
// Check if the identifier exists in the map
|
||||
attempt, exists := auth.LoginAttempts[identifier]
|
||||
if !exists {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
// If account is locked, check if lock time has expired
|
||||
if attempt.LockedUntil.After(time.Now()) {
|
||||
// Calculate remaining lockout time in seconds
|
||||
remaining := int(time.Until(attempt.LockedUntil).Seconds())
|
||||
return true, remaining
|
||||
}
|
||||
|
||||
// Lock has expired
|
||||
return false, 0
|
||||
}
|
||||
|
||||
func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
|
||||
// Skip if rate limiting is not configured
|
||||
if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
|
||||
return
|
||||
}
|
||||
@@ -189,133 +158,132 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
|
||||
auth.LoginMutex.Lock()
|
||||
defer auth.LoginMutex.Unlock()
|
||||
|
||||
// Get current attempt record or create a new one
|
||||
attempt, exists := auth.LoginAttempts[identifier]
|
||||
if !exists {
|
||||
attempt = &LoginAttempt{}
|
||||
auth.LoginAttempts[identifier] = attempt
|
||||
}
|
||||
|
||||
// Update last attempt time
|
||||
attempt.LastAttempt = time.Now()
|
||||
|
||||
// If successful login, reset failed attempts
|
||||
if success {
|
||||
attempt.FailedAttempts = 0
|
||||
attempt.LockedUntil = time.Time{} // Reset lock time
|
||||
return
|
||||
}
|
||||
|
||||
// Increment failed attempts
|
||||
attempt.FailedAttempts++
|
||||
|
||||
// If max retries reached, lock the account
|
||||
if attempt.FailedAttempts >= auth.Config.LoginMaxRetries {
|
||||
attempt.LockedUntil = time.Now().Add(time.Duration(auth.Config.LoginTimeout) * time.Second)
|
||||
log.Warn().Str("identifier", identifier).Int("timeout", auth.Config.LoginTimeout).Msg("Account locked due to too many failed login attempts")
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *AuthService) EmailWhitelisted(email string) bool {
|
||||
func (auth *AuthService) IsEmailWhitelisted(email string) bool {
|
||||
return utils.CheckFilter(auth.Config.OauthWhitelist, email)
|
||||
}
|
||||
|
||||
func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.SessionCookie) error {
|
||||
session, err := auth.GetSession(c)
|
||||
uuid, err := uuid.NewRandom()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var sessionExpiry int
|
||||
var expiry int
|
||||
|
||||
if data.TotpPending {
|
||||
sessionExpiry = 3600
|
||||
expiry = 3600
|
||||
} else {
|
||||
sessionExpiry = auth.Config.SessionExpiry
|
||||
expiry = auth.Config.SessionExpiry
|
||||
}
|
||||
|
||||
session.Values["username"] = data.Username
|
||||
session.Values["name"] = data.Name
|
||||
session.Values["email"] = data.Email
|
||||
session.Values["provider"] = data.Provider
|
||||
session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
|
||||
session.Values["totpPending"] = data.TotpPending
|
||||
session.Values["oauthGroups"] = data.OAuthGroups
|
||||
session := model.Session{
|
||||
UUID: uuid.String(),
|
||||
Username: data.Username,
|
||||
Email: data.Email,
|
||||
Name: data.Name,
|
||||
Provider: data.Provider,
|
||||
TOTPPending: data.TotpPending,
|
||||
OAuthGroups: data.OAuthGroups,
|
||||
Expiry: time.Now().Add(time.Duration(expiry) * time.Second).Unix(),
|
||||
}
|
||||
|
||||
err = auth.Database.Create(&session).Error
|
||||
|
||||
err = session.Save(c.Request, c.Writer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.SetCookie(auth.Config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error {
|
||||
session, err := auth.GetSession(c)
|
||||
cookie, err := c.Cookie(auth.Config.SessionCookieName)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete all values in the session
|
||||
for key := range session.Values {
|
||||
delete(session.Values, key)
|
||||
res := auth.Database.Unscoped().Where("uuid = ?", cookie).Delete(&model.Session{})
|
||||
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
|
||||
err = session.Save(c.Request, c.Writer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear the cookie in the browser
|
||||
c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, error) {
|
||||
session, err := auth.GetSession(c)
|
||||
cookie, err := c.Cookie(auth.Config.SessionCookieName)
|
||||
|
||||
if err != nil {
|
||||
return config.SessionCookie{}, err
|
||||
}
|
||||
|
||||
username, usernameOk := session.Values["username"].(string)
|
||||
email, emailOk := session.Values["email"].(string)
|
||||
name, nameOk := session.Values["name"].(string)
|
||||
provider, providerOK := session.Values["provider"].(string)
|
||||
expiry, expiryOk := session.Values["expiry"].(int64)
|
||||
totpPending, totpPendingOk := session.Values["totpPending"].(bool)
|
||||
oauthGroups, oauthGroupsOk := session.Values["oauthGroups"].(string)
|
||||
var session model.Session
|
||||
|
||||
// If any data is missing, delete the session cookie
|
||||
if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk || !oauthGroupsOk {
|
||||
log.Warn().Msg("Session cookie is invalid")
|
||||
auth.DeleteSessionCookie(c)
|
||||
return config.SessionCookie{}, nil
|
||||
res := auth.Database.Unscoped().Where("uuid = ?", cookie).First(&session)
|
||||
|
||||
if res.Error != nil {
|
||||
return config.SessionCookie{}, res.Error
|
||||
}
|
||||
|
||||
// If the session cookie has expired, delete it
|
||||
if time.Now().Unix() > expiry {
|
||||
log.Warn().Msg("Session cookie expired")
|
||||
auth.DeleteSessionCookie(c)
|
||||
return config.SessionCookie{}, nil
|
||||
if res.RowsAffected == 0 {
|
||||
return config.SessionCookie{}, fmt.Errorf("session not found")
|
||||
}
|
||||
|
||||
currentTime := time.Now().Unix()
|
||||
|
||||
if currentTime > session.Expiry {
|
||||
res := auth.Database.Unscoped().Where("uuid = ?", session.UUID).Delete(&model.Session{})
|
||||
if res.Error != nil {
|
||||
log.Error().Err(res.Error).Msg("Failed to delete expired session")
|
||||
}
|
||||
return config.SessionCookie{}, fmt.Errorf("session expired")
|
||||
}
|
||||
|
||||
return config.SessionCookie{
|
||||
Username: username,
|
||||
Name: name,
|
||||
Email: email,
|
||||
Provider: provider,
|
||||
TotpPending: totpPending,
|
||||
OAuthGroups: oauthGroups,
|
||||
UUID: session.UUID,
|
||||
Username: session.Username,
|
||||
Email: session.Email,
|
||||
Name: session.Name,
|
||||
Provider: session.Provider,
|
||||
TotpPending: session.TOTPPending,
|
||||
OAuthGroups: session.OAuthGroups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (auth *AuthService) UserAuthConfigured() bool {
|
||||
// If there are users or LDAP is configured, return true
|
||||
return len(auth.Config.Users) > 0 || auth.LDAP != nil
|
||||
}
|
||||
|
||||
func (auth *AuthService) ResourceAllowed(c *gin.Context, context config.UserContext, labels config.Labels) bool {
|
||||
func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.Labels) bool {
|
||||
if context.OAuth {
|
||||
log.Debug().Msg("Checking OAuth whitelist")
|
||||
return utils.CheckFilter(labels.OAuth.Whitelist, context.Email)
|
||||
@@ -325,7 +293,7 @@ func (auth *AuthService) ResourceAllowed(c *gin.Context, context config.UserCont
|
||||
return utils.CheckFilter(labels.Users, context.Username)
|
||||
}
|
||||
|
||||
func (auth *AuthService) OAuthGroup(c *gin.Context, context config.UserContext, labels config.Labels) bool {
|
||||
func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserContext, labels config.Labels) bool {
|
||||
if labels.OAuth.Groups == "" {
|
||||
return true
|
||||
}
|
||||
@@ -335,23 +303,20 @@ func (auth *AuthService) OAuthGroup(c *gin.Context, context config.UserContext,
|
||||
return true
|
||||
}
|
||||
|
||||
// Split the groups by comma (no need to parse since they are from the API response)
|
||||
// No need to parse since they are from the API response
|
||||
oauthGroups := strings.Split(context.OAuthGroups, ",")
|
||||
|
||||
// For every group check if it is in the required groups
|
||||
for _, group := range oauthGroups {
|
||||
if utils.CheckFilter(labels.OAuth.Groups, group) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// No groups matched
|
||||
log.Debug().Msg("No groups matched")
|
||||
return false
|
||||
}
|
||||
|
||||
func (auth *AuthService) AuthEnabled(uri string, labels config.Labels) (bool, error) {
|
||||
// If the label is empty, auth is enabled
|
||||
func (auth *AuthService) IsAuthEnabled(uri string, labels config.Labels) (bool, error) {
|
||||
if labels.Allowed == "" {
|
||||
return true, nil
|
||||
}
|
||||
@@ -362,12 +327,10 @@ func (auth *AuthService) AuthEnabled(uri string, labels config.Labels) (bool, er
|
||||
return true, err
|
||||
}
|
||||
|
||||
// If the regex matches the URI, auth is not enabled
|
||||
if regex.MatchString(uri) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Auth enabled
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -384,7 +347,6 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User {
|
||||
}
|
||||
|
||||
func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool {
|
||||
// Check if the IP is in block list
|
||||
for _, blocked := range labels.IP.Block {
|
||||
res, err := utils.FilterIP(blocked, ip)
|
||||
if err != nil {
|
||||
@@ -397,7 +359,6 @@ func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// For every IP in the allow list, check if the IP matches
|
||||
for _, allowed := range labels.IP.Allow {
|
||||
res, err := utils.FilterIP(allowed, ip)
|
||||
if err != nil {
|
||||
@@ -410,7 +371,6 @@ func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// If not in allowed range and allowed range is not empty, deny access
|
||||
if len(labels.IP.Allow) > 0 {
|
||||
log.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
|
||||
return false
|
||||
@@ -420,8 +380,7 @@ func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (auth *AuthService) BypassedIP(labels config.Labels, ip string) bool {
|
||||
// For every IP in the bypass list, check if the IP matches
|
||||
func (auth *AuthService) IsBypassedIP(labels config.Labels, ip string) bool {
|
||||
for _, bypassed := range labels.IP.Bypass {
|
||||
res, err := utils.FilterIP(bypassed, ip)
|
||||
if err != nil {
|
||||
|
||||
78
internal/service/database_service.go
Normal file
78
internal/service/database_service.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"tinyauth/internal/assets"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DatabaseServiceConfig struct {
|
||||
DatabasePath string
|
||||
}
|
||||
|
||||
type DatabaseService struct {
|
||||
Config DatabaseServiceConfig
|
||||
Database *gorm.DB
|
||||
}
|
||||
|
||||
func NewDatabaseService(config DatabaseServiceConfig) *DatabaseService {
|
||||
return &DatabaseService{
|
||||
Config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DatabaseService) Init() error {
|
||||
gormDB, err := gorm.Open(sqlite.Open(ds.Config.DatabasePath), &gorm.Config{})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sqlDB, err := gormDB.DB()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
|
||||
err = ds.migrateDatabase(sqlDB)
|
||||
|
||||
if err != nil && err != migrate.ErrNoChange {
|
||||
return err
|
||||
}
|
||||
|
||||
ds.Database = gormDB
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *DatabaseService) migrateDatabase(sqlDB *sql.DB) error {
|
||||
data, err := iofs.New(assets.Migrations, "migrations")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target, err := sqliteMigrate.WithInstance(sqlDB, &sqliteMigrate.Config{})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrator, err := migrate.NewWithInstance("iofs", data, "tinyauth", target)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return migrator.Up()
|
||||
}
|
||||
|
||||
func (ds *DatabaseService) GetDatabase() *gorm.DB {
|
||||
return ds.Database
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
func GetSecret(conf string, file string) string {
|
||||
@@ -49,24 +45,6 @@ func GetBasicAuth(username string, password string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(auth))
|
||||
}
|
||||
|
||||
func DeriveKey(secret string, info string) (string, error) {
|
||||
hash := sha256.New
|
||||
hkdf := hkdf.New(hash, []byte(secret), nil, []byte(info)) // I am not using a salt because I just want two different keys from one secret, maybe bad practice
|
||||
key := make([]byte, 24)
|
||||
|
||||
_, err := io.ReadFull(hkdf, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if bytes.Equal(key, make([]byte, 24)) {
|
||||
return "", errors.New("derived key is empty")
|
||||
}
|
||||
|
||||
encodedKey := base64.StdEncoding.EncodeToString(key)
|
||||
return encodedKey, nil
|
||||
}
|
||||
|
||||
func FilterIP(filter string, ip string) (bool, error) {
|
||||
ipAddr := net.ParseIP(ip)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user