mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-05-14 08:11:28 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9abab2f17 | |||
| 3fd56272d2 |
@@ -45,7 +45,7 @@ func (app *BootstrapApp) setupServices() error {
|
|||||||
labelProvider = dockerService
|
labelProvider = dockerService
|
||||||
}
|
}
|
||||||
|
|
||||||
accessControlsService := service.NewAccessControlsService(app.log, &labelProvider, app.config.Apps)
|
accessControlsService := service.NewAccessControlsService(app.log, app.config, &labelProvider)
|
||||||
app.services.accessControlService = accessControlsService
|
app.services.accessControlService = accessControlsService
|
||||||
|
|
||||||
oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx)
|
oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx)
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
clientIP := c.ClientIP()
|
clientIP := c.ClientIP()
|
||||||
|
|
||||||
if controller.auth.IsBypassedIP(clientIP, acls) {
|
if controller.acls.IsIPBypassed(clientIP, acls) {
|
||||||
controller.setHeaders(c, acls)
|
controller.setHeaders(c, acls)
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
@@ -110,13 +110,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
authEnabled, err := controller.auth.IsAuthEnabled(proxyCtx.Path, acls)
|
authEnabled := controller.acls.IsAuthEnabled(proxyCtx.Path, acls)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
controller.log.App.Error().Err(err).Msg("Failed to determine if authentication is enabled for resource")
|
|
||||||
controller.handleError(c, proxyCtx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !authEnabled {
|
if !authEnabled {
|
||||||
controller.log.App.Debug().Msg("Authentication is disabled for this resource, allowing access without authentication")
|
controller.log.App.Debug().Msg("Authentication is disabled for this resource, allowing access without authentication")
|
||||||
@@ -128,7 +122,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !controller.auth.CheckIP(clientIP, acls) {
|
if !controller.acls.IsIPAllowed(clientIP, acls) {
|
||||||
queries, err := query.Values(UnauthorizedQuery{
|
queries, err := query.Values(UnauthorizedQuery{
|
||||||
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
||||||
IP: clientIP,
|
IP: clientIP,
|
||||||
@@ -165,7 +159,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if userContext.Authenticated {
|
if userContext.Authenticated {
|
||||||
userAllowed := controller.auth.IsUserAllowed(c, *userContext, acls)
|
userAllowed := controller.acls.IsUserAllowed(*userContext, acls)
|
||||||
|
|
||||||
if !userAllowed {
|
if !userAllowed {
|
||||||
controller.log.App.Warn().Str("user", userContext.GetUsername()).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User is not allowed to access resource")
|
controller.log.App.Warn().Str("user", userContext.GetUsername()).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User is not allowed to access resource")
|
||||||
@@ -205,9 +199,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
var groupOK bool
|
var groupOK bool
|
||||||
|
|
||||||
if userContext.IsOAuth() {
|
if userContext.IsOAuth() {
|
||||||
groupOK = controller.auth.IsInOAuthGroup(c, *userContext, acls)
|
groupOK = controller.acls.IsInOAuthGroup(*userContext, acls)
|
||||||
} else {
|
} else {
|
||||||
groupOK = controller.auth.IsInLDAPGroup(c, *userContext, acls)
|
groupOK = controller.acls.IsInLDAPGroup(*userContext, acls)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !groupOK {
|
if !groupOK {
|
||||||
|
|||||||
@@ -24,33 +24,6 @@ func TestProxyController(t *testing.T) {
|
|||||||
|
|
||||||
cfg, runtime := test.CreateTestConfigs(t)
|
cfg, runtime := test.CreateTestConfigs(t)
|
||||||
|
|
||||||
acls := map[string]model.App{
|
|
||||||
"app_path_allow": {
|
|
||||||
Config: model.AppConfig{
|
|
||||||
Domain: "path-allow.example.com",
|
|
||||||
},
|
|
||||||
Path: model.AppPath{
|
|
||||||
Allow: "/allowed",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"app_user_allow": {
|
|
||||||
Config: model.AppConfig{
|
|
||||||
Domain: "user-allow.example.com",
|
|
||||||
},
|
|
||||||
Users: model.AppUsers{
|
|
||||||
Allow: "testuser",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"ip_bypass": {
|
|
||||||
Config: model.AppConfig{
|
|
||||||
Domain: "ip-bypass.example.com",
|
|
||||||
},
|
|
||||||
IP: model.AppIP{
|
|
||||||
Bypass: []string{"10.10.10.10"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const browserUserAgent = `
|
const browserUserAgent = `
|
||||||
Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36`
|
Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36`
|
||||||
|
|
||||||
@@ -391,7 +364,7 @@ func TestProxyController(t *testing.T) {
|
|||||||
|
|
||||||
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
|
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
|
||||||
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, queries, broker)
|
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, queries, broker)
|
||||||
aclsService := service.NewAccessControlsService(log, nil, acls)
|
aclsService := service.NewAccessControlsService(log, cfg, nil)
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.description, func(t *testing.T) {
|
t.Run(test.description, func(t *testing.T) {
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ func NewDefaultConfiguration() *Config {
|
|||||||
SessionMaxLifetime: 0, // disabled
|
SessionMaxLifetime: 0, // disabled
|
||||||
LoginTimeout: 300, // 5 minutes
|
LoginTimeout: 300, // 5 minutes
|
||||||
LoginMaxRetries: 3,
|
LoginMaxRetries: 3,
|
||||||
|
ACLs: ACLsConfig{
|
||||||
|
Policy: "allow",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
UI: UIConfig{
|
UI: UIConfig{
|
||||||
Title: "Tinyauth",
|
Title: "Tinyauth",
|
||||||
@@ -114,6 +117,7 @@ type AuthConfig struct {
|
|||||||
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
||||||
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
||||||
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
||||||
|
ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserAttributes struct {
|
type UserAttributes struct {
|
||||||
@@ -223,6 +227,10 @@ type OIDCClientConfig struct {
|
|||||||
Name string `description:"Client name in UI." yaml:"name"`
|
Name string `description:"Client name in UI." yaml:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ACLsConfig struct {
|
||||||
|
Policy string `description:"ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow." yaml:"policy"`
|
||||||
|
}
|
||||||
|
|
||||||
// ACLs
|
// ACLs
|
||||||
|
|
||||||
type Apps struct {
|
type Apps struct {
|
||||||
|
|||||||
@@ -1,44 +1,82 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||||
|
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type AccessControlPolicy string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PolicyAllow AccessControlPolicy = "allow"
|
||||||
|
PolicyDeny AccessControlPolicy = "deny"
|
||||||
|
)
|
||||||
|
|
||||||
|
func accessControlPolicyFromString(s string) (AccessControlPolicy, bool) {
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case "allow":
|
||||||
|
return PolicyAllow, true
|
||||||
|
case "deny":
|
||||||
|
return PolicyDeny, true
|
||||||
|
default:
|
||||||
|
return PolicyAllow, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type LabelProvider interface {
|
type LabelProvider interface {
|
||||||
GetLabels(appDomain string) (*model.App, error)
|
GetLabels(appDomain string) (*model.App, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccessControlsService struct {
|
type AccessControlsService struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
|
config model.Config
|
||||||
labelProvider *LabelProvider
|
labelProvider *LabelProvider
|
||||||
static map[string]model.App
|
policy AccessControlPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAccessControlsService(
|
func NewAccessControlsService(
|
||||||
log *logger.Logger,
|
log *logger.Logger,
|
||||||
labelProvider *LabelProvider,
|
config model.Config,
|
||||||
static map[string]model.App) *AccessControlsService {
|
labelProvider *LabelProvider) *AccessControlsService {
|
||||||
return &AccessControlsService{
|
|
||||||
|
service := AccessControlsService{
|
||||||
log: log,
|
log: log,
|
||||||
|
config: config,
|
||||||
labelProvider: labelProvider,
|
labelProvider: labelProvider,
|
||||||
static: static,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
policy, ok := accessControlPolicyFromString(config.Auth.ACLs.Policy)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
log.App.Warn().Str("policy", config.Auth.ACLs.Policy).Msg("Invalid ACL policy in config, defaulting to 'allow'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if policy == PolicyAllow {
|
||||||
|
log.App.Debug().Msg("Using 'allow' ACL policy: access to apps will be allowed by default unless explicitly blocked")
|
||||||
|
} else {
|
||||||
|
log.App.Debug().Msg("Using 'deny' ACL policy: access to apps will be blocked by default unless explicitly allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
service.policy = policy
|
||||||
|
|
||||||
|
return &service
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acls *AccessControlsService) lookupStaticACLs(domain string) *model.App {
|
func (service *AccessControlsService) lookupStaticACLs(domain string) *model.App {
|
||||||
var appAcls *model.App
|
var appAcls *model.App
|
||||||
for app, config := range acls.static {
|
for app, config := range service.config.Apps {
|
||||||
if config.Config.Domain == domain {
|
if config.Config.Domain == domain {
|
||||||
acls.log.App.Debug().Str("name", app).Msg("Found matching container by domain")
|
service.log.App.Debug().Str("name", app).Msg("Found matching container by domain")
|
||||||
appAcls = &config
|
appAcls = &config
|
||||||
break // If we find a match by domain, we can stop searching
|
break // If we find a match by domain, we can stop searching
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.SplitN(domain, ".", 2)[0] == app {
|
if strings.SplitN(domain, ".", 2)[0] == app {
|
||||||
acls.log.App.Debug().Str("name", app).Msg("Found matching container by app name")
|
service.log.App.Debug().Str("name", app).Msg("Found matching container by app name")
|
||||||
appAcls = &config
|
appAcls = &config
|
||||||
break // If we find a match by app name, we can stop searching
|
break // If we find a match by app name, we can stop searching
|
||||||
}
|
}
|
||||||
@@ -46,20 +84,193 @@ func (acls *AccessControlsService) lookupStaticACLs(domain string) *model.App {
|
|||||||
return appAcls
|
return appAcls
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acls *AccessControlsService) GetAccessControls(domain string) (*model.App, error) {
|
func (service *AccessControlsService) GetAccessControls(domain string) (*model.App, error) {
|
||||||
// First check in the static config
|
// First check in the static config
|
||||||
app := acls.lookupStaticACLs(domain)
|
app := service.lookupStaticACLs(domain)
|
||||||
|
|
||||||
if app != nil {
|
if app != nil {
|
||||||
acls.log.App.Debug().Msg("Using static ACLs for app")
|
service.log.App.Debug().Msg("Using static ACLs for app")
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a label provider configured, try to get ACLs from it
|
// If we have a label provider configured, try to get ACLs from it
|
||||||
if acls.labelProvider != nil {
|
if service.labelProvider != nil {
|
||||||
return (*acls.labelProvider).GetLabels(domain)
|
return (*service.labelProvider).GetLabels(domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// no labels
|
// no labels
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service *AccessControlsService) IsUserAllowed(context model.UserContext, acls *model.App) bool {
|
||||||
|
if acls == nil {
|
||||||
|
return service.policyResult(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if context.Provider == model.ProviderOAuth {
|
||||||
|
service.log.App.Debug().Msg("User is an OAuth user, checking OAuth whitelist")
|
||||||
|
return utils.CheckFilter(acls.OAuth.Whitelist, context.OAuth.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
if acls.Users.Block != "" {
|
||||||
|
service.log.App.Debug().Msg("Checking users block list")
|
||||||
|
if utils.CheckFilter(acls.Users.Block, context.GetUsername()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service.log.App.Debug().Msg("Checking users allow list")
|
||||||
|
return service.policyResult(utils.CheckFilter(acls.Users.Allow, context.GetUsername()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *AccessControlsService) IsInOAuthGroup(context model.UserContext, acls *model.App) bool {
|
||||||
|
if acls == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !context.IsOAuth() {
|
||||||
|
service.log.App.Debug().Msg("User is not an OAuth user, skipping OAuth group check")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := model.OverrideProviders[context.OAuth.ID]; ok {
|
||||||
|
service.log.App.Debug().Str("provider", context.OAuth.ID).Msg("Provider override detected, skipping group check")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, userGroup := range context.OAuth.Groups {
|
||||||
|
if utils.CheckFilter(acls.OAuth.Groups, strings.TrimSpace(userGroup)) {
|
||||||
|
service.log.App.Trace().Str("group", userGroup).Str("required", acls.OAuth.Groups).Msg("User group matched")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service.log.App.Debug().Msg("No groups matched")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *AccessControlsService) IsInLDAPGroup(context model.UserContext, acls *model.App) bool {
|
||||||
|
if acls == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !context.IsLDAP() {
|
||||||
|
service.log.App.Debug().Msg("User is not an LDAP user, skipping LDAP group check")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, userGroup := range context.LDAP.Groups {
|
||||||
|
if utils.CheckFilter(acls.LDAP.Groups, strings.TrimSpace(userGroup)) {
|
||||||
|
service.log.App.Trace().Str("group", userGroup).Str("required", acls.LDAP.Groups).Msg("User group matched")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service.log.App.Debug().Msg("No groups matched")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *AccessControlsService) IsAuthEnabled(uri string, acls *model.App) bool {
|
||||||
|
if acls == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if acls.Path.Block != "" {
|
||||||
|
regex, err := regexp.Compile(acls.Path.Block)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
service.log.App.Error().Err(err).Msg("Failed to compile block regex")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !regex.MatchString(uri) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if acls.Path.Allow != "" {
|
||||||
|
regex, err := regexp.Compile(acls.Path.Allow)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
service.log.App.Error().Err(err).Msg("Failed to compile allow regex")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if regex.MatchString(uri) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *AccessControlsService) IsIPAllowed(ip string, acls *model.App) bool {
|
||||||
|
if acls == nil {
|
||||||
|
return service.policyResult(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge the global and app IP filter
|
||||||
|
blockedIps := append(acls.IP.Block, service.config.Auth.IP.Block...)
|
||||||
|
allowedIPs := append(acls.IP.Allow, service.config.Auth.IP.Allow...)
|
||||||
|
|
||||||
|
for _, blocked := range blockedIps {
|
||||||
|
res, err := utils.FilterIP(blocked, ip)
|
||||||
|
if err != nil {
|
||||||
|
service.log.App.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if res {
|
||||||
|
service.log.App.Debug().Str("ip", ip).Str("item", blocked).Msg("IP is in block list, denying access")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, allowed := range allowedIPs {
|
||||||
|
res, err := utils.FilterIP(allowed, ip)
|
||||||
|
if err != nil {
|
||||||
|
service.log.App.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if res {
|
||||||
|
service.log.App.Debug().Str("ip", ip).Str("item", allowed).Msg("IP is in allow list, allowing access")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allowedIPs) > 0 {
|
||||||
|
service.log.App.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
service.log.App.Debug().Str("ip", ip).Msg("IP not in block or allow list, allowing access")
|
||||||
|
return service.policyResult(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *AccessControlsService) IsIPBypassed(ip string, acls *model.App) bool {
|
||||||
|
if acls == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, bypassed := range acls.IP.Bypass {
|
||||||
|
res, err := utils.FilterIP(bypassed, ip)
|
||||||
|
if err != nil {
|
||||||
|
service.log.App.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if res {
|
||||||
|
service.log.App.Debug().Str("ip", ip).Str("item", bypassed).Msg("IP is in bypass list, skipping authentication")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service.log.App.Debug().Str("ip", ip).Msg("IP not in bypass list, proceeding with authentication")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *AccessControlsService) policyResult(result bool) bool {
|
||||||
|
if service.policy == PolicyAllow {
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
return !result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -18,7 +17,6 @@ import (
|
|||||||
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
@@ -454,171 +452,6 @@ func (auth *AuthService) LDAPAuthConfigured() bool {
|
|||||||
return auth.ldap != nil
|
return auth.ldap != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) IsUserAllowed(c *gin.Context, context model.UserContext, acls *model.App) bool {
|
|
||||||
if acls == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if context.Provider == model.ProviderOAuth {
|
|
||||||
auth.log.App.Debug().Msg("User is an OAuth user, checking OAuth whitelist")
|
|
||||||
return utils.CheckFilter(acls.OAuth.Whitelist, context.OAuth.Email)
|
|
||||||
}
|
|
||||||
|
|
||||||
if acls.Users.Block != "" {
|
|
||||||
auth.log.App.Debug().Msg("Checking users block list")
|
|
||||||
if utils.CheckFilter(acls.Users.Block, context.GetUsername()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auth.log.App.Debug().Msg("Checking users allow list")
|
|
||||||
return utils.CheckFilter(acls.Users.Allow, context.GetUsername())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context model.UserContext, acls *model.App) bool {
|
|
||||||
if acls == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !context.IsOAuth() {
|
|
||||||
auth.log.App.Debug().Msg("User is not an OAuth user, skipping OAuth group check")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := model.OverrideProviders[context.OAuth.ID]; ok {
|
|
||||||
auth.log.App.Debug().Str("provider", context.OAuth.ID).Msg("Provider override detected, skipping group check")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, userGroup := range context.OAuth.Groups {
|
|
||||||
if utils.CheckFilter(acls.OAuth.Groups, strings.TrimSpace(userGroup)) {
|
|
||||||
auth.log.App.Trace().Str("group", userGroup).Str("required", acls.OAuth.Groups).Msg("User group matched")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auth.log.App.Debug().Msg("No groups matched")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *AuthService) IsInLDAPGroup(c *gin.Context, context model.UserContext, acls *model.App) bool {
|
|
||||||
if acls == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !context.IsLDAP() {
|
|
||||||
auth.log.App.Debug().Msg("User is not an LDAP user, skipping LDAP group check")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, userGroup := range context.LDAP.Groups {
|
|
||||||
if utils.CheckFilter(acls.LDAP.Groups, strings.TrimSpace(userGroup)) {
|
|
||||||
auth.log.App.Trace().Str("group", userGroup).Str("required", acls.LDAP.Groups).Msg("User group matched")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auth.log.App.Debug().Msg("No groups matched")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *AuthService) IsAuthEnabled(uri string, acls *model.App) (bool, error) {
|
|
||||||
if acls == nil {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for block list
|
|
||||||
if acls.Path.Block != "" {
|
|
||||||
regex, err := regexp.Compile(acls.Path.Block)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !regex.MatchString(uri) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for allow list
|
|
||||||
if acls.Path.Allow != "" {
|
|
||||||
regex, err := regexp.Compile(acls.Path.Allow)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if regex.MatchString(uri) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *AuthService) CheckIP(ip string, acls *model.App) bool {
|
|
||||||
if acls == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge the global and app IP filter
|
|
||||||
blockedIps := append(auth.config.Auth.IP.Block, acls.IP.Block...)
|
|
||||||
allowedIPs := append(auth.config.Auth.IP.Allow, acls.IP.Allow...)
|
|
||||||
|
|
||||||
for _, blocked := range blockedIps {
|
|
||||||
res, err := utils.FilterIP(blocked, ip)
|
|
||||||
if err != nil {
|
|
||||||
auth.log.App.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if res {
|
|
||||||
auth.log.App.Debug().Str("ip", ip).Str("item", blocked).Msg("IP is in block list, denying access")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, allowed := range allowedIPs {
|
|
||||||
res, err := utils.FilterIP(allowed, ip)
|
|
||||||
if err != nil {
|
|
||||||
auth.log.App.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if res {
|
|
||||||
auth.log.App.Debug().Str("ip", ip).Str("item", allowed).Msg("IP is in allow list, allowing access")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(allowedIPs) > 0 {
|
|
||||||
auth.log.App.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
auth.log.App.Debug().Str("ip", ip).Msg("IP not in any block or allow list, allowing access by default")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *AuthService) IsBypassedIP(ip string, acls *model.App) bool {
|
|
||||||
if acls == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, bypassed := range acls.IP.Bypass {
|
|
||||||
res, err := utils.FilterIP(bypassed, ip)
|
|
||||||
if err != nil {
|
|
||||||
auth.log.App.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if res {
|
|
||||||
auth.log.App.Debug().Str("ip", ip).Str("item", bypassed).Msg("IP is in bypass list, skipping authentication")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auth.log.App.Debug().Str("ip", ip).Msg("IP not in bypass list, proceeding with authentication")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *AuthService) NewOAuthSession(serviceName string, params OAuthURLParams) (string, OAuthPendingSession, error) {
|
func (auth *AuthService) NewOAuthSession(serviceName string, params OAuthURLParams) (string, OAuthPendingSession, error) {
|
||||||
auth.ensureOAuthSessionLimit()
|
auth.ensureOAuthSessionLimit()
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
|
|||||||
SessionExpiry: 10,
|
SessionExpiry: 10,
|
||||||
LoginTimeout: 10,
|
LoginTimeout: 10,
|
||||||
LoginMaxRetries: 3,
|
LoginMaxRetries: 3,
|
||||||
|
ACLs: model.ACLsConfig{
|
||||||
|
Policy: "allow",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Database: model.DatabaseConfig{
|
Database: model.DatabaseConfig{
|
||||||
Path: filepath.Join(tempDir, "test.db"),
|
Path: filepath.Join(tempDir, "test.db"),
|
||||||
@@ -48,6 +51,32 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
Path: filepath.Join(tempDir, "resources"),
|
Path: filepath.Join(tempDir, "resources"),
|
||||||
},
|
},
|
||||||
|
Apps: map[string]model.App{
|
||||||
|
"app_path_allow": {
|
||||||
|
Config: model.AppConfig{
|
||||||
|
Domain: "path-allow.example.com",
|
||||||
|
},
|
||||||
|
Path: model.AppPath{
|
||||||
|
Allow: "/allowed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"app_user_allow": {
|
||||||
|
Config: model.AppConfig{
|
||||||
|
Domain: "user-allow.example.com",
|
||||||
|
},
|
||||||
|
Users: model.AppUsers{
|
||||||
|
Allow: "testuser",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ip_bypass": {
|
||||||
|
Config: model.AppConfig{
|
||||||
|
Domain: "ip-bypass.example.com",
|
||||||
|
},
|
||||||
|
IP: model.AppIP{
|
||||||
|
Bypass: []string{"10.10.10.10"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
passwd, err := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost)
|
passwd, err := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost)
|
||||||
|
|||||||
Reference in New Issue
Block a user