mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-05-17 17:50:14 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9fd457e28 | |||
| e38c4710d4 | |||
| f8b0188776 | |||
| 7b5d882ee8 | |||
| b9abab2f17 | |||
| 3fd56272d2 |
@@ -34,6 +34,7 @@ type Services struct {
|
||||
ldapService *service.LdapService
|
||||
oauthBrokerService *service.OAuthBrokerService
|
||||
oidcService *service.OIDCService
|
||||
policyEngine *service.PolicyEngine
|
||||
}
|
||||
|
||||
type BootstrapApp struct {
|
||||
|
||||
@@ -44,7 +44,7 @@ func (app *BootstrapApp) setupRouter() error {
|
||||
controller.NewContextController(app.log, app.config, app.runtime, apiRouter)
|
||||
controller.NewOAuthController(app.log, app.config, app.runtime, apiRouter, app.services.authService)
|
||||
controller.NewOIDCController(app.log, app.services.oidcService, app.runtime, apiRouter)
|
||||
controller.NewProxyController(app.log, app.runtime, apiRouter, app.services.accessControlService, app.services.authService)
|
||||
controller.NewProxyController(app.log, app.runtime, apiRouter, app.services.accessControlService, app.services.authService, app.services.policyEngine)
|
||||
controller.NewUserController(app.log, app.runtime, apiRouter, app.services.authService)
|
||||
controller.NewResourcesController(app.config, &engine.RouterGroup)
|
||||
controller.NewHealthController(apiRouter)
|
||||
|
||||
@@ -16,38 +16,21 @@ func (app *BootstrapApp) setupServices() error {
|
||||
|
||||
app.services.ldapService = ldapService
|
||||
|
||||
useKubernetes := app.config.LabelProvider == "kubernetes" ||
|
||||
(app.config.LabelProvider == "auto" && os.Getenv("KUBERNETES_SERVICE_HOST") != "")
|
||||
|
||||
var labelProvider service.LabelProvider
|
||||
|
||||
if useKubernetes {
|
||||
app.log.App.Debug().Msg("Using Kubernetes label provider")
|
||||
|
||||
kubernetesService, err := service.NewKubernetesService(app.log, app.ctx, &app.wg)
|
||||
labelProvider, err := app.getLabelProvider()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize kubernetes service: %w", err)
|
||||
return fmt.Errorf("failed to initialize label provider: %w", err)
|
||||
}
|
||||
|
||||
app.services.kubernetesService = kubernetesService
|
||||
labelProvider = kubernetesService
|
||||
} else {
|
||||
app.log.App.Debug().Msg("Using Docker label provider")
|
||||
|
||||
dockerService, err := service.NewDockerService(app.log, app.ctx, &app.wg)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize docker service: %w", err)
|
||||
}
|
||||
|
||||
app.services.dockerService = dockerService
|
||||
labelProvider = dockerService
|
||||
}
|
||||
|
||||
accessControlsService := service.NewAccessControlsService(app.log, &labelProvider, app.config.Apps)
|
||||
accessControlsService := service.NewAccessControlsService(app.log, app.config, &labelProvider)
|
||||
app.services.accessControlService = accessControlsService
|
||||
|
||||
err = app.setupPolicyEngine()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize policy engine: %w", err)
|
||||
}
|
||||
|
||||
oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx)
|
||||
app.services.oauthBrokerService = oauthBrokerService
|
||||
|
||||
@@ -64,3 +47,67 @@ func (app *BootstrapApp) setupServices() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) {
|
||||
if app.config.LabelProvider == "none" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
useKubernetes := app.config.LabelProvider == "kubernetes" ||
|
||||
(app.config.LabelProvider == "auto" && os.Getenv("KUBERNETES_SERVICE_HOST") != "")
|
||||
|
||||
if useKubernetes {
|
||||
app.log.App.Debug().Msg("Using Kubernetes label provider")
|
||||
|
||||
kubernetesService, err := service.NewKubernetesService(app.log, app.ctx, &app.wg)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize kubernetes service: %w", err)
|
||||
}
|
||||
|
||||
app.services.kubernetesService = kubernetesService
|
||||
return kubernetesService, nil
|
||||
}
|
||||
|
||||
app.log.App.Debug().Msg("Using Docker label provider")
|
||||
|
||||
dockerService, err := service.NewDockerService(app.log, app.ctx, &app.wg)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize docker service: %w", err)
|
||||
}
|
||||
|
||||
app.services.dockerService = dockerService
|
||||
return dockerService, nil
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) setupPolicyEngine() error {
|
||||
policyEngine, err := service.NewPolicyEngine(app.config, app.log)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize policy engine: %w", err)
|
||||
}
|
||||
|
||||
policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{
|
||||
Log: app.log,
|
||||
})
|
||||
policyEngine.RegisterRule(service.RuleOAuthGroup, &service.OAuthGroupRule{
|
||||
Log: app.log,
|
||||
})
|
||||
policyEngine.RegisterRule(service.RuleLDAPGroup, &service.LDAPGroupRule{
|
||||
Log: app.log,
|
||||
})
|
||||
policyEngine.RegisterRule(service.RuleAuthEnabled, &service.AuthEnabledRule{
|
||||
Log: app.log,
|
||||
})
|
||||
policyEngine.RegisterRule(service.RuleIPAllowed, &service.IPAllowedRule{
|
||||
Log: app.log,
|
||||
Config: app.config,
|
||||
})
|
||||
policyEngine.RegisterRule(service.RuleIPBypassed, &service.IPBypassedRule{
|
||||
Log: app.log,
|
||||
})
|
||||
|
||||
app.services.policyEngine = policyEngine
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package controller
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
@@ -55,6 +56,7 @@ type ProxyController struct {
|
||||
runtime model.RuntimeConfig
|
||||
acls *service.AccessControlsService
|
||||
auth *service.AuthService
|
||||
policyEngine *service.PolicyEngine
|
||||
}
|
||||
|
||||
func NewProxyController(
|
||||
@@ -63,12 +65,14 @@ func NewProxyController(
|
||||
router *gin.RouterGroup,
|
||||
acls *service.AccessControlsService,
|
||||
auth *service.AuthService,
|
||||
policyEngine *service.PolicyEngine,
|
||||
) *ProxyController {
|
||||
controller := &ProxyController{
|
||||
log: log,
|
||||
runtime: runtime,
|
||||
acls: acls,
|
||||
auth: auth,
|
||||
policyEngine: policyEngine,
|
||||
}
|
||||
|
||||
proxyGroup := router.Group("/auth")
|
||||
@@ -101,7 +105,13 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
if controller.auth.IsBypassedIP(clientIP, acls) {
|
||||
aclsCtx := &service.ACLContext{
|
||||
ACLs: acls,
|
||||
IP: net.ParseIP(clientIP),
|
||||
Path: proxyCtx.Path,
|
||||
}
|
||||
|
||||
if controller.policyEngine.Evaluate(service.RuleIPBypassed, aclsCtx) {
|
||||
controller.setHeaders(c, acls)
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
@@ -110,15 +120,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
authEnabled, err := controller.auth.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 controller.policyEngine.Evaluate(service.RuleAuthEnabled, aclsCtx) {
|
||||
controller.log.App.Debug().Msg("Authentication is disabled for this resource, allowing access without authentication")
|
||||
controller.setHeaders(c, acls)
|
||||
c.JSON(200, gin.H{
|
||||
@@ -128,7 +130,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !controller.auth.CheckIP(clientIP, acls) {
|
||||
if !controller.policyEngine.Evaluate(service.RuleIPAllowed, aclsCtx) {
|
||||
queries, err := query.Values(UnauthorizedQuery{
|
||||
Resource: strings.Split(proxyCtx.Host, ".")[0],
|
||||
IP: clientIP,
|
||||
@@ -164,10 +166,10 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if userContext.Authenticated {
|
||||
userAllowed := controller.auth.IsUserAllowed(c, *userContext, acls)
|
||||
aclsCtx.UserContext = userContext
|
||||
|
||||
if !userAllowed {
|
||||
if userContext.Authenticated {
|
||||
if !controller.policyEngine.Evaluate(service.RuleUserAllowed, aclsCtx) {
|
||||
controller.log.App.Warn().Str("user", userContext.GetUsername()).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User is not allowed to access resource")
|
||||
|
||||
queries, err := query.Values(UnauthorizedQuery{
|
||||
@@ -205,9 +207,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
||||
var groupOK bool
|
||||
|
||||
if userContext.IsOAuth() {
|
||||
groupOK = controller.auth.IsInOAuthGroup(c, *userContext, acls)
|
||||
groupOK = controller.policyEngine.Evaluate(service.RuleOAuthGroup, aclsCtx)
|
||||
} else {
|
||||
groupOK = controller.auth.IsInLDAPGroup(c, *userContext, acls)
|
||||
groupOK = controller.policyEngine.Evaluate(service.RuleLDAPGroup, aclsCtx)
|
||||
}
|
||||
|
||||
if !groupOK {
|
||||
|
||||
@@ -24,33 +24,6 @@ func TestProxyController(t *testing.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 = `
|
||||
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,29 @@ func TestProxyController(t *testing.T) {
|
||||
|
||||
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
|
||||
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, queries, broker)
|
||||
aclsService := service.NewAccessControlsService(log, nil, acls)
|
||||
aclsService := service.NewAccessControlsService(log, cfg, nil)
|
||||
policyEngine, err := service.NewPolicyEngine(cfg, log)
|
||||
require.NoError(t, err)
|
||||
|
||||
policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{
|
||||
Log: log,
|
||||
})
|
||||
policyEngine.RegisterRule(service.RuleOAuthGroup, &service.OAuthGroupRule{
|
||||
Log: log,
|
||||
})
|
||||
policyEngine.RegisterRule(service.RuleLDAPGroup, &service.LDAPGroupRule{
|
||||
Log: log,
|
||||
})
|
||||
policyEngine.RegisterRule(service.RuleAuthEnabled, &service.AuthEnabledRule{
|
||||
Log: log,
|
||||
})
|
||||
policyEngine.RegisterRule(service.RuleIPAllowed, &service.IPAllowedRule{
|
||||
Log: log,
|
||||
Config: cfg,
|
||||
})
|
||||
policyEngine.RegisterRule(service.RuleIPBypassed, &service.IPBypassedRule{
|
||||
Log: log,
|
||||
})
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
@@ -406,7 +401,7 @@ func TestProxyController(t *testing.T) {
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
controller.NewProxyController(log, runtime, group, aclsService, authService)
|
||||
controller.NewProxyController(log, runtime, group, aclsService, authService, policyEngine)
|
||||
|
||||
test.run(t, router, recorder)
|
||||
})
|
||||
|
||||
@@ -24,6 +24,9 @@ func NewDefaultConfiguration() *Config {
|
||||
SessionMaxLifetime: 0, // disabled
|
||||
LoginTimeout: 300, // 5 minutes
|
||||
LoginMaxRetries: 3,
|
||||
ACLs: ACLsConfig{
|
||||
Policy: "allow",
|
||||
},
|
||||
},
|
||||
UI: UIConfig{
|
||||
Title: "Tinyauth",
|
||||
@@ -78,7 +81,7 @@ type Config struct {
|
||||
UI UIConfig `description:"UI customization." yaml:"ui"`
|
||||
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap"`
|
||||
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
||||
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, or kubernetes). auto detects the environment." yaml:"labelProvider"`
|
||||
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider"`
|
||||
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
||||
}
|
||||
|
||||
@@ -114,6 +117,7 @@ type AuthConfig struct {
|
||||
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
|
||||
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
|
||||
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
|
||||
ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls"`
|
||||
}
|
||||
|
||||
type UserAttributes struct {
|
||||
@@ -223,6 +227,10 @@ type OIDCClientConfig struct {
|
||||
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
|
||||
|
||||
type Apps struct {
|
||||
|
||||
@@ -0,0 +1,702 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
)
|
||||
|
||||
func TestUserAllowedRule(t *testing.T) {
|
||||
log := logger.NewLogger().WithTestConfig()
|
||||
log.Init()
|
||||
|
||||
rule := &UserAllowedRule{Log: log}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx *ACLContext
|
||||
expected Effect
|
||||
}{
|
||||
{
|
||||
name: "abstains when ACLs are nil",
|
||||
ctx: &ACLContext{
|
||||
ACLs: nil,
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderLocal,
|
||||
Local: &model.LocalContext{
|
||||
BaseContext: model.BaseContext{Username: "alice"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectAbstain,
|
||||
},
|
||||
{
|
||||
name: "allows OAuth user when email matches whitelist",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
OAuth: model.AppOAuth{Whitelist: "allowed@example.com"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderOAuth,
|
||||
OAuth: &model.OAuthContext{
|
||||
BaseContext: model.BaseContext{
|
||||
Username: "different-username",
|
||||
Email: "allowed@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectAllow,
|
||||
},
|
||||
{
|
||||
name: "denies OAuth user when email does not match whitelist",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
OAuth: model.AppOAuth{Whitelist: "allowed@example.com"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderOAuth,
|
||||
OAuth: &model.OAuthContext{
|
||||
BaseContext: model.BaseContext{Email: "denied@example.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "abstains for OAuth user when whitelist filter is invalid",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
OAuth: model.AppOAuth{Whitelist: "/[/"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderOAuth,
|
||||
OAuth: &model.OAuthContext{
|
||||
BaseContext: model.BaseContext{Email: "allowed@example.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectAbstain,
|
||||
},
|
||||
{
|
||||
name: "denies local user when username matches block list",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
Users: model.AppUsers{Block: "alice,bob"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderLocal,
|
||||
Local: &model.LocalContext{
|
||||
BaseContext: model.BaseContext{Username: "alice"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "allows local user when username does not match block list",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
Users: model.AppUsers{Block: "alice,bob"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderLocal,
|
||||
Local: &model.LocalContext{
|
||||
BaseContext: model.BaseContext{Username: "charlie"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectAllow,
|
||||
},
|
||||
{
|
||||
name: "abstains when block list filter is invalid",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
Users: model.AppUsers{Block: "/[/"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderLocal,
|
||||
Local: &model.LocalContext{
|
||||
BaseContext: model.BaseContext{Username: "alice"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectAbstain,
|
||||
},
|
||||
{
|
||||
name: "allows local user when username matches allow list",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
Users: model.AppUsers{Allow: "alice,bob"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderLocal,
|
||||
Local: &model.LocalContext{
|
||||
BaseContext: model.BaseContext{Username: "alice"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectAllow,
|
||||
},
|
||||
{
|
||||
name: "denies local user when username does not match allow list",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
Users: model.AppUsers{Allow: "alice,bob"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderLocal,
|
||||
Local: &model.LocalContext{
|
||||
BaseContext: model.BaseContext{Username: "charlie"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "abstains when allow list filter is invalid",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
Users: model.AppUsers{Allow: "/[/"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderLocal,
|
||||
Local: &model.LocalContext{
|
||||
BaseContext: model.BaseContext{Username: "alice"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectAbstain,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, rule.Evaluate(tt.ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOAuthGroupRule(t *testing.T) {
|
||||
log := logger.NewLogger().WithTestConfig()
|
||||
log.Init()
|
||||
|
||||
rule := &OAuthGroupRule{Log: log}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx *ACLContext
|
||||
expected Effect
|
||||
}{
|
||||
{
|
||||
name: "abstains when ACLs are nil",
|
||||
ctx: &ACLContext{
|
||||
ACLs: nil,
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderOAuth,
|
||||
OAuth: &model.OAuthContext{
|
||||
Groups: []string{"admins"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectAbstain,
|
||||
},
|
||||
{
|
||||
name: "abstains when user is not OAuth",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
OAuth: model.AppOAuth{Groups: "admins"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderLocal,
|
||||
Local: &model.LocalContext{
|
||||
BaseContext: model.BaseContext{Username: "alice"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectAbstain,
|
||||
},
|
||||
{
|
||||
name: "allows when provider is an override provider regardless of groups",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
OAuth: model.AppOAuth{Groups: "admins"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderOAuth,
|
||||
OAuth: &model.OAuthContext{
|
||||
ID: "google",
|
||||
Groups: []string{"unrelated"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectAllow,
|
||||
},
|
||||
{
|
||||
name: "allows OAuth user when a group matches",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
OAuth: model.AppOAuth{Groups: "admins,users"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderOAuth,
|
||||
OAuth: &model.OAuthContext{
|
||||
ID: "custom",
|
||||
Groups: []string{"users"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectAllow,
|
||||
},
|
||||
{
|
||||
name: "denies OAuth user when no group matches",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
OAuth: model.AppOAuth{Groups: "admins"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderOAuth,
|
||||
OAuth: &model.OAuthContext{
|
||||
ID: "custom",
|
||||
Groups: []string{"users", "guests"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "denies OAuth user when user has no groups",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
OAuth: model.AppOAuth{Groups: "admins"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderOAuth,
|
||||
OAuth: &model.OAuthContext{
|
||||
ID: "custom",
|
||||
Groups: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "abstains when groups filter is invalid",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
OAuth: model.AppOAuth{Groups: "/[/"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderOAuth,
|
||||
OAuth: &model.OAuthContext{
|
||||
ID: "custom",
|
||||
Groups: []string{"admins"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectAbstain,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, rule.Evaluate(tt.ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLDAPGroupRule(t *testing.T) {
|
||||
log := logger.NewLogger().WithTestConfig()
|
||||
log.Init()
|
||||
|
||||
rule := &LDAPGroupRule{Log: log}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx *ACLContext
|
||||
expected Effect
|
||||
}{
|
||||
{
|
||||
name: "abstains when context is nil",
|
||||
ctx: nil,
|
||||
expected: EffectAbstain,
|
||||
},
|
||||
{
|
||||
name: "abstains when user is not LDAP",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
LDAP: model.AppLDAP{Groups: "admins"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderLocal,
|
||||
Local: &model.LocalContext{
|
||||
BaseContext: model.BaseContext{Username: "alice"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectAbstain,
|
||||
},
|
||||
{
|
||||
name: "allows LDAP user when a group matches",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
LDAP: model.AppLDAP{Groups: "admins,users"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderLDAP,
|
||||
LDAP: &model.LDAPContext{
|
||||
Groups: []string{"users"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectAllow,
|
||||
},
|
||||
{
|
||||
name: "denies LDAP user when no group matches",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
LDAP: model.AppLDAP{Groups: "admins"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderLDAP,
|
||||
LDAP: &model.LDAPContext{
|
||||
Groups: []string{"users", "guests"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "denies LDAP user when user has no groups",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
LDAP: model.AppLDAP{Groups: "admins"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderLDAP,
|
||||
LDAP: &model.LDAPContext{
|
||||
Groups: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "abstains when groups filter is invalid",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
LDAP: model.AppLDAP{Groups: "/[/"},
|
||||
},
|
||||
UserContext: &model.UserContext{
|
||||
Provider: model.ProviderLDAP,
|
||||
LDAP: &model.LDAPContext{
|
||||
Groups: []string{"admins"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EffectAbstain,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, rule.Evaluate(tt.ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthEnabledRule(t *testing.T) {
|
||||
log := logger.NewLogger().WithTestConfig()
|
||||
log.Init()
|
||||
|
||||
rule := &AuthEnabledRule{Log: log}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx *ACLContext
|
||||
expected Effect
|
||||
}{
|
||||
{
|
||||
name: "deny when ACLs are nil",
|
||||
ctx: &ACLContext{
|
||||
ACLs: nil,
|
||||
Path: "/anything",
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "allows when path does not match block regex",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
Path: model.AppPath{Block: "^/admin"},
|
||||
},
|
||||
Path: "/public",
|
||||
},
|
||||
expected: EffectAllow,
|
||||
},
|
||||
{
|
||||
name: "denies when path matches block regex and no allow regex",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
Path: model.AppPath{Block: "^/admin"},
|
||||
},
|
||||
Path: "/admin/users",
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "allows when path matches allow regex",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
Path: model.AppPath{Allow: "^/public"},
|
||||
},
|
||||
Path: "/public/index",
|
||||
},
|
||||
expected: EffectAllow,
|
||||
},
|
||||
{
|
||||
name: "denies when path does not match allow regex",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
Path: model.AppPath{Allow: "^/public"},
|
||||
},
|
||||
Path: "/private",
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "allows when blocked path is also explicitly allowed",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
Path: model.AppPath{
|
||||
Block: "^/admin",
|
||||
Allow: "^/admin/public",
|
||||
},
|
||||
},
|
||||
Path: "/admin/public/page",
|
||||
},
|
||||
expected: EffectAllow,
|
||||
},
|
||||
{
|
||||
name: "denies when block regex fails to compile",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
Path: model.AppPath{Block: "[invalid"},
|
||||
},
|
||||
Path: "/anything",
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "denies when allow regex fails to compile",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
Path: model.AppPath{Allow: "[invalid"},
|
||||
},
|
||||
Path: "/anything",
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "denies when no path rules are configured",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{},
|
||||
Path: "/anything",
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, rule.Evaluate(tt.ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPAllowedRule(t *testing.T) {
|
||||
log := logger.NewLogger().WithTestConfig()
|
||||
log.Init()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config model.Config
|
||||
ctx *ACLContext
|
||||
expected Effect
|
||||
}{
|
||||
{
|
||||
name: "abstains when ACLs are nil",
|
||||
ctx: &ACLContext{
|
||||
ACLs: nil,
|
||||
IP: net.ParseIP("10.0.0.1"),
|
||||
},
|
||||
expected: EffectAbstain,
|
||||
},
|
||||
{
|
||||
name: "denies when IP matches app block list",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
IP: model.AppIP{Block: []string{"10.0.0.1"}},
|
||||
},
|
||||
IP: net.ParseIP("10.0.0.1"),
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "denies when IP matches global block list",
|
||||
config: model.Config{
|
||||
Auth: model.AuthConfig{
|
||||
IP: model.IPConfig{Block: []string{"10.0.0.0/24"}},
|
||||
},
|
||||
},
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{},
|
||||
IP: net.ParseIP("10.0.0.5"),
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "allows when IP matches app allow list",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
IP: model.AppIP{Allow: []string{"192.168.1.0/24"}},
|
||||
},
|
||||
IP: net.ParseIP("192.168.1.10"),
|
||||
},
|
||||
expected: EffectAllow,
|
||||
},
|
||||
{
|
||||
name: "allows when IP matches global allow list",
|
||||
config: model.Config{
|
||||
Auth: model.AuthConfig{
|
||||
IP: model.IPConfig{Allow: []string{"192.168.1.10"}},
|
||||
},
|
||||
},
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{},
|
||||
IP: net.ParseIP("192.168.1.10"),
|
||||
},
|
||||
expected: EffectAllow,
|
||||
},
|
||||
{
|
||||
name: "denies when allow list is set and IP does not match",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
IP: model.AppIP{Allow: []string{"192.168.1.0/24"}},
|
||||
},
|
||||
IP: net.ParseIP("10.0.0.1"),
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "allows when no block or allow lists are configured",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{},
|
||||
IP: net.ParseIP("10.0.0.1"),
|
||||
},
|
||||
expected: EffectAllow,
|
||||
},
|
||||
{
|
||||
name: "block list takes precedence over allow list",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
IP: model.AppIP{
|
||||
Block: []string{"10.0.0.1"},
|
||||
Allow: []string{"10.0.0.1"},
|
||||
},
|
||||
},
|
||||
IP: net.ParseIP("10.0.0.1"),
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "skips invalid block entries and continues evaluation",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
IP: model.AppIP{
|
||||
Block: []string{"not-an-ip"},
|
||||
Allow: []string{"10.0.0.1"},
|
||||
},
|
||||
},
|
||||
IP: net.ParseIP("10.0.0.1"),
|
||||
},
|
||||
expected: EffectAllow,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rule := &IPAllowedRule{Log: log, Config: tt.config}
|
||||
assert.Equal(t, tt.expected, rule.Evaluate(tt.ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPBypassedRule(t *testing.T) {
|
||||
log := logger.NewLogger().WithTestConfig()
|
||||
log.Init()
|
||||
|
||||
rule := &IPBypassedRule{Log: log}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx *ACLContext
|
||||
expected Effect
|
||||
}{
|
||||
{
|
||||
name: "deny when ACLs are nil",
|
||||
ctx: &ACLContext{
|
||||
ACLs: nil,
|
||||
IP: net.ParseIP("10.0.0.1"),
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "allows when IP matches bypass list",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}},
|
||||
},
|
||||
IP: net.ParseIP("10.0.0.5"),
|
||||
},
|
||||
expected: EffectAllow,
|
||||
},
|
||||
{
|
||||
name: "denies when IP does not match bypass list",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
IP: model.AppIP{Bypass: []string{"10.0.0.0/24"}},
|
||||
},
|
||||
IP: net.ParseIP("192.168.1.1"),
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "denies when bypass list is empty",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{},
|
||||
IP: net.ParseIP("10.0.0.1"),
|
||||
},
|
||||
expected: EffectDeny,
|
||||
},
|
||||
{
|
||||
name: "skips invalid bypass entries and allows on later match",
|
||||
ctx: &ACLContext{
|
||||
ACLs: &model.App{
|
||||
IP: model.AppIP{Bypass: []string{"not-an-ip", "10.0.0.1"}},
|
||||
},
|
||||
IP: net.ParseIP("10.0.0.1"),
|
||||
},
|
||||
expected: EffectAllow,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, rule.Evaluate(tt.ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
)
|
||||
|
||||
type RuleName string
|
||||
|
||||
const (
|
||||
RuleUserAllowed RuleName = "rule-user-allowed"
|
||||
RuleOAuthGroup RuleName = "rule-oauth-group"
|
||||
RuleLDAPGroup RuleName = "rule-ldap-group"
|
||||
RuleAuthEnabled RuleName = "rule-auth-enabled"
|
||||
RuleIPAllowed RuleName = "rule-ip-allowed"
|
||||
RuleIPBypassed RuleName = "rule-ip-bypassed"
|
||||
)
|
||||
|
||||
type UserAllowedRule struct {
|
||||
Log *logger.Logger
|
||||
}
|
||||
|
||||
func (rule *UserAllowedRule) Evaluate(ctx *ACLContext) Effect {
|
||||
if ctx.ACLs == nil {
|
||||
return EffectAbstain
|
||||
}
|
||||
|
||||
if ctx.UserContext.Provider == model.ProviderOAuth {
|
||||
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)
|
||||
if err != nil {
|
||||
rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.OAuth.Email).Msg("Invalid entry in OAuth whitelist")
|
||||
return EffectAbstain
|
||||
}
|
||||
if match {
|
||||
rule.Log.App.Debug().Str("email", ctx.UserContext.OAuth.Email).Msg("User is in OAuth whitelist, allowing access")
|
||||
return EffectAllow
|
||||
}
|
||||
return EffectDeny
|
||||
}
|
||||
|
||||
if ctx.ACLs.Users.Block != "" {
|
||||
rule.Log.App.Debug().Msg("Checking users block list")
|
||||
match, err := utils.CheckFilter(ctx.ACLs.Users.Block, ctx.UserContext.GetUsername())
|
||||
if err != nil {
|
||||
rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.GetUsername()).Msg("Invalid entry in users block list")
|
||||
return EffectAbstain
|
||||
}
|
||||
if match {
|
||||
rule.Log.App.Debug().Str("username", ctx.UserContext.GetUsername()).Msg("User is in users block list, denying access")
|
||||
return EffectDeny
|
||||
}
|
||||
return EffectAllow
|
||||
}
|
||||
|
||||
rule.Log.App.Debug().Msg("Checking users allow list")
|
||||
|
||||
match, err := utils.CheckFilter(ctx.ACLs.Users.Allow, ctx.UserContext.GetUsername())
|
||||
|
||||
if err != nil {
|
||||
rule.Log.App.Warn().Err(err).Str("item", ctx.UserContext.GetUsername()).Msg("Invalid entry in users allow list")
|
||||
return EffectAbstain
|
||||
}
|
||||
|
||||
if match {
|
||||
rule.Log.App.Debug().Str("username", ctx.UserContext.GetUsername()).Msg("User is in users allow list, allowing access")
|
||||
return EffectAllow
|
||||
}
|
||||
|
||||
rule.Log.App.Debug().Str("username", ctx.UserContext.GetUsername()).Msg("User is not in users allow list, denying access")
|
||||
return EffectDeny
|
||||
}
|
||||
|
||||
type OAuthGroupRule struct {
|
||||
Log *logger.Logger
|
||||
}
|
||||
|
||||
func (rule *OAuthGroupRule) Evaluate(ctx *ACLContext) Effect {
|
||||
if ctx.ACLs == nil {
|
||||
return EffectAbstain
|
||||
}
|
||||
|
||||
if !ctx.UserContext.IsOAuth() {
|
||||
rule.Log.App.Debug().Msg("User is not an OAuth user, skipping OAuth group check")
|
||||
return EffectAbstain
|
||||
}
|
||||
|
||||
if _, ok := model.OverrideProviders[ctx.UserContext.OAuth.ID]; ok {
|
||||
rule.Log.App.Debug().Str("provider", ctx.UserContext.OAuth.ID).Msg("Provider override detected, skipping group check")
|
||||
return EffectAllow
|
||||
}
|
||||
|
||||
for _, group := range ctx.UserContext.OAuth.Groups {
|
||||
match, err := utils.CheckFilter(ctx.ACLs.OAuth.Groups, strings.TrimSpace(group))
|
||||
if err != nil {
|
||||
return EffectAbstain
|
||||
}
|
||||
if match {
|
||||
rule.Log.App.Trace().Str("group", group).Str("required", ctx.ACLs.OAuth.Groups).Msg("User group matched, allowing access")
|
||||
return EffectAllow
|
||||
}
|
||||
}
|
||||
|
||||
rule.Log.App.Debug().Msg("No groups matched")
|
||||
return EffectDeny
|
||||
}
|
||||
|
||||
type LDAPGroupRule struct {
|
||||
Log *logger.Logger
|
||||
}
|
||||
|
||||
func (rule *LDAPGroupRule) Evaluate(ctx *ACLContext) Effect {
|
||||
if ctx == nil {
|
||||
return EffectAbstain
|
||||
}
|
||||
|
||||
if !ctx.UserContext.IsLDAP() {
|
||||
rule.Log.App.Debug().Msg("User is not an LDAP user, skipping LDAP group check")
|
||||
return EffectAbstain
|
||||
}
|
||||
|
||||
for _, group := range ctx.UserContext.LDAP.Groups {
|
||||
match, err := utils.CheckFilter(ctx.ACLs.LDAP.Groups, strings.TrimSpace(group))
|
||||
if err != nil {
|
||||
return EffectAbstain
|
||||
}
|
||||
if match {
|
||||
rule.Log.App.Trace().Str("group", group).Str("required", ctx.ACLs.LDAP.Groups).Msg("User group matched, allowing access")
|
||||
return EffectAllow
|
||||
}
|
||||
}
|
||||
|
||||
rule.Log.App.Debug().Msg("No groups matched")
|
||||
return EffectDeny
|
||||
}
|
||||
|
||||
type AuthEnabledRule struct {
|
||||
Log *logger.Logger
|
||||
}
|
||||
|
||||
func (rule *AuthEnabledRule) Evaluate(ctx *ACLContext) Effect {
|
||||
if ctx.ACLs == nil {
|
||||
return EffectDeny
|
||||
}
|
||||
|
||||
if ctx.ACLs.Path.Block != "" {
|
||||
regex, err := regexp.Compile(ctx.ACLs.Path.Block)
|
||||
|
||||
if err != nil {
|
||||
rule.Log.App.Error().Err(err).Msg("Failed to compile block regex")
|
||||
return EffectDeny
|
||||
}
|
||||
|
||||
if !regex.MatchString(ctx.Path) {
|
||||
return EffectAllow
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.ACLs.Path.Allow != "" {
|
||||
regex, err := regexp.Compile(ctx.ACLs.Path.Allow)
|
||||
|
||||
if err != nil {
|
||||
rule.Log.App.Error().Err(err).Msg("Failed to compile allow regex")
|
||||
return EffectDeny
|
||||
}
|
||||
|
||||
if regex.MatchString(ctx.Path) {
|
||||
return EffectAllow
|
||||
}
|
||||
}
|
||||
|
||||
return EffectDeny
|
||||
}
|
||||
|
||||
type IPAllowedRule struct {
|
||||
Log *logger.Logger
|
||||
Config model.Config
|
||||
}
|
||||
|
||||
func (rule *IPAllowedRule) Evaluate(ctx *ACLContext) Effect {
|
||||
if ctx.ACLs == nil {
|
||||
return EffectAbstain
|
||||
}
|
||||
|
||||
// Merge the global and app IP filter
|
||||
blockedIps := append(ctx.ACLs.IP.Block, rule.Config.Auth.IP.Block...)
|
||||
allowedIPs := append(ctx.ACLs.IP.Allow, rule.Config.Auth.IP.Allow...)
|
||||
|
||||
for _, blocked := range blockedIps {
|
||||
match, err := utils.CheckIPFilter(blocked, ctx.IP.String())
|
||||
if err != nil {
|
||||
rule.Log.App.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
|
||||
continue
|
||||
}
|
||||
if match {
|
||||
rule.Log.App.Debug().Str("ip", ctx.IP.String()).Str("item", blocked).Msg("IP is in block list, denying access")
|
||||
return EffectDeny
|
||||
}
|
||||
}
|
||||
|
||||
for _, allowed := range allowedIPs {
|
||||
match, err := utils.CheckIPFilter(allowed, ctx.IP.String())
|
||||
if err != nil {
|
||||
rule.Log.App.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
|
||||
continue
|
||||
}
|
||||
if match {
|
||||
rule.Log.App.Debug().Str("ip", ctx.IP.String()).Str("item", allowed).Msg("IP is in allow list, allowing access")
|
||||
return EffectAllow
|
||||
}
|
||||
}
|
||||
|
||||
if len(allowedIPs) > 0 {
|
||||
rule.Log.App.Debug().Str("ip", ctx.IP.String()).Msg("IP not in allow list, denying access")
|
||||
return EffectDeny
|
||||
}
|
||||
|
||||
rule.Log.App.Debug().Str("ip", ctx.IP.String()).Msg("IP not in block or allow list, allowing access")
|
||||
return EffectAllow
|
||||
}
|
||||
|
||||
type IPBypassedRule struct {
|
||||
Log *logger.Logger
|
||||
}
|
||||
|
||||
func (rule *IPBypassedRule) Evaluate(ctx *ACLContext) Effect {
|
||||
if ctx.ACLs == nil {
|
||||
return EffectDeny
|
||||
}
|
||||
|
||||
for _, bypassed := range ctx.ACLs.IP.Bypass {
|
||||
match, err := utils.CheckIPFilter(bypassed, ctx.IP.String())
|
||||
if err != nil {
|
||||
rule.Log.App.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
|
||||
continue
|
||||
}
|
||||
if match {
|
||||
rule.Log.App.Debug().Str("ip", ctx.IP.String()).Str("item", bypassed).Msg("IP is in bypass list, skipping authentication")
|
||||
return EffectAllow
|
||||
}
|
||||
}
|
||||
|
||||
rule.Log.App.Debug().Str("ip", ctx.IP.String()).Msg("IP not in bypass list, proceeding with authentication")
|
||||
return EffectDeny
|
||||
}
|
||||
@@ -13,32 +13,33 @@ type LabelProvider interface {
|
||||
|
||||
type AccessControlsService struct {
|
||||
log *logger.Logger
|
||||
config model.Config
|
||||
labelProvider *LabelProvider
|
||||
static map[string]model.App
|
||||
}
|
||||
|
||||
func NewAccessControlsService(
|
||||
log *logger.Logger,
|
||||
labelProvider *LabelProvider,
|
||||
static map[string]model.App) *AccessControlsService {
|
||||
config model.Config,
|
||||
labelProvider *LabelProvider) *AccessControlsService {
|
||||
|
||||
return &AccessControlsService{
|
||||
log: log,
|
||||
config: config,
|
||||
labelProvider: labelProvider,
|
||||
static: static,
|
||||
}
|
||||
}
|
||||
|
||||
func (acls *AccessControlsService) lookupStaticACLs(domain string) *model.App {
|
||||
func (service *AccessControlsService) lookupStaticACLs(domain string) *model.App {
|
||||
var appAcls *model.App
|
||||
for app, config := range acls.static {
|
||||
for app, config := range service.config.Apps {
|
||||
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
|
||||
break // If we find a match by domain, we can stop searching
|
||||
}
|
||||
|
||||
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
|
||||
break // If we find a match by app name, we can stop searching
|
||||
}
|
||||
@@ -46,18 +47,18 @@ func (acls *AccessControlsService) lookupStaticACLs(domain string) *model.App {
|
||||
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
|
||||
app := acls.lookupStaticACLs(domain)
|
||||
app := service.lookupStaticACLs(domain)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// If we have a label provider configured, try to get ACLs from it
|
||||
if acls.labelProvider != nil {
|
||||
return (*acls.labelProvider).GetLabels(domain)
|
||||
if service.labelProvider != nil {
|
||||
return (*service.labelProvider).GetLabels(domain)
|
||||
}
|
||||
|
||||
// no labels
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -18,7 +17,6 @@ import (
|
||||
|
||||
"slices"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/oauth2"
|
||||
@@ -286,7 +284,12 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
|
||||
}
|
||||
|
||||
func (auth *AuthService) IsEmailWhitelisted(email string) bool {
|
||||
return utils.CheckFilter(strings.Join(auth.runtime.OAuthWhitelist, ","), email)
|
||||
match, err := utils.CheckFilter(strings.Join(auth.runtime.OAuthWhitelist, ","), email)
|
||||
if err != nil {
|
||||
auth.log.App.Warn().Err(err).Str("email", email).Msg("Invalid email filter pattern")
|
||||
return false
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
func (auth *AuthService) CreateSession(ctx context.Context, data repository.Session) (*http.Cookie, error) {
|
||||
@@ -454,171 +457,6 @@ func (auth *AuthService) LDAPAuthConfigured() bool {
|
||||
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) {
|
||||
auth.ensureOAuthSessionLimit()
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/tinyauthapp/tinyauth/internal/model"
|
||||
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
|
||||
)
|
||||
|
||||
type Policy string
|
||||
|
||||
const (
|
||||
PolicyAllow Policy = "allow"
|
||||
PolicyDeny Policy = "deny"
|
||||
)
|
||||
|
||||
type Effect int
|
||||
|
||||
const (
|
||||
EffectAbstain Effect = iota
|
||||
EffectAllow
|
||||
EffectDeny
|
||||
)
|
||||
|
||||
type Rule interface {
|
||||
Evaluate(ctx *ACLContext) Effect
|
||||
}
|
||||
|
||||
type ACLContext struct {
|
||||
ACLs *model.App
|
||||
UserContext *model.UserContext
|
||||
IP net.IP
|
||||
Path string
|
||||
}
|
||||
|
||||
type PolicyEngine struct {
|
||||
log *logger.Logger
|
||||
rules map[RuleName]Rule
|
||||
policy Policy
|
||||
}
|
||||
|
||||
func NewPolicyEngine(config model.Config, log *logger.Logger) (*PolicyEngine, error) {
|
||||
engine := PolicyEngine{
|
||||
log: log,
|
||||
rules: make(map[RuleName]Rule),
|
||||
}
|
||||
|
||||
switch config.Auth.ACLs.Policy {
|
||||
case string(PolicyAllow):
|
||||
log.App.Debug().Msg("Using 'allow' ACL policy: access to apps will be allowed by default unless explicitly blocked")
|
||||
engine.policy = PolicyAllow
|
||||
case string(PolicyDeny):
|
||||
log.App.Debug().Msg("Using 'deny' ACL policy: access to apps will be blocked by default unless explicitly allowed")
|
||||
engine.policy = PolicyDeny
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid acl policy: %s", config.Auth.ACLs.Policy)
|
||||
}
|
||||
|
||||
return &engine, nil
|
||||
}
|
||||
|
||||
func (engine *PolicyEngine) RegisterRule(name RuleName, rule Rule) {
|
||||
engine.rules[name] = rule
|
||||
}
|
||||
|
||||
func (engine *PolicyEngine) evaluateRuleByName(name RuleName, ctx *ACLContext) Effect {
|
||||
rule, exists := engine.rules[name]
|
||||
|
||||
if !exists {
|
||||
engine.log.App.Warn().Str("rule", string(name)).Msg("Rule not found in policy engine, defaulting to deny")
|
||||
return EffectDeny
|
||||
}
|
||||
|
||||
return rule.Evaluate(ctx)
|
||||
}
|
||||
|
||||
func (engine *PolicyEngine) effectToAccess(effect Effect) bool {
|
||||
switch effect {
|
||||
case EffectAllow:
|
||||
return true
|
||||
case EffectDeny:
|
||||
return false
|
||||
default:
|
||||
// If the effect is abstain, we fall back to the default policy
|
||||
return engine.policy == PolicyAllow
|
||||
}
|
||||
}
|
||||
|
||||
func (engine *PolicyEngine) Evaluate(name RuleName, ctx *ACLContext) bool {
|
||||
effect := engine.evaluateRuleByName(name, ctx)
|
||||
access := engine.effectToAccess(effect)
|
||||
|
||||
engine.log.App.Debug().
|
||||
Str("rule", string(name)).
|
||||
Int("effect", int(effect)).
|
||||
Bool("access", access).
|
||||
Msg("Evaluated ACL rule")
|
||||
|
||||
return access
|
||||
}
|
||||
@@ -40,6 +40,9 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
|
||||
SessionExpiry: 10,
|
||||
LoginTimeout: 10,
|
||||
LoginMaxRetries: 3,
|
||||
ACLs: model.ACLsConfig{
|
||||
Policy: "allow",
|
||||
},
|
||||
},
|
||||
Database: model.DatabaseConfig{
|
||||
Path: filepath.Join(tempDir, "test.db"),
|
||||
@@ -48,6 +51,32 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) {
|
||||
Enabled: true,
|
||||
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)
|
||||
|
||||
@@ -3,7 +3,7 @@ package utils
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -46,26 +46,27 @@ func EncodeBasicAuth(username string, password string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(auth))
|
||||
}
|
||||
|
||||
func FilterIP(filter string, ip string) (bool, error) {
|
||||
func CheckIPFilter(filter string, ip string) (bool, error) {
|
||||
ipAddr := net.ParseIP(ip)
|
||||
|
||||
if ipAddr == nil {
|
||||
return false, errors.New("invalid IP address")
|
||||
return false, fmt.Errorf("invalid ip address")
|
||||
}
|
||||
|
||||
filter = strings.Replace(filter, "-", "/", -1)
|
||||
filter = strings.ReplaceAll(filter, "-", "/")
|
||||
|
||||
if strings.Contains(filter, "/") {
|
||||
_, cidr, err := net.ParseCIDR(filter)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("invalid cidr notation: %w", err)
|
||||
}
|
||||
return cidr.Contains(ipAddr), nil
|
||||
}
|
||||
|
||||
ipFilter := net.ParseIP(filter)
|
||||
|
||||
if ipFilter == nil {
|
||||
return false, errors.New("invalid IP address in filter")
|
||||
return false, fmt.Errorf("invalid ip address")
|
||||
}
|
||||
|
||||
if ipFilter.Equal(ipAddr) {
|
||||
@@ -75,31 +76,29 @@ func FilterIP(filter string, ip string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func CheckFilter(filter string, str string) bool {
|
||||
func CheckFilter(filter string, input string) (bool, error) {
|
||||
if len(strings.TrimSpace(filter)) == 0 {
|
||||
return true
|
||||
return false, fmt.Errorf("filter is empty")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(filter, "/") && strings.HasSuffix(filter, "/") {
|
||||
re, err := regexp.Compile(filter[1 : len(filter)-1])
|
||||
if err != nil {
|
||||
return false
|
||||
return false, fmt.Errorf("invalid regex filter: %w", err)
|
||||
}
|
||||
|
||||
if re.MatchString(strings.TrimSpace(str)) {
|
||||
return true
|
||||
if re.MatchString(input) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
filterSplit := strings.Split(filter, ",")
|
||||
|
||||
for _, item := range filterSplit {
|
||||
if strings.TrimSpace(item) == strings.TrimSpace(str) {
|
||||
return true
|
||||
for item := range strings.SplitSeq(filter, ",") {
|
||||
if strings.TrimSpace(item) == input {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func GenerateUUID(str string) string {
|
||||
|
||||
@@ -75,66 +75,77 @@ func TestEncodeBasicAuth(t *testing.T) {
|
||||
assert.Equal(t, expected, utils.EncodeBasicAuth(username, password))
|
||||
}
|
||||
|
||||
func TestFilterIP(t *testing.T) {
|
||||
func TestCheckIPFilter(t *testing.T) {
|
||||
// Exact match IPv4
|
||||
ok, err := utils.FilterIP("10.10.0.1", "10.10.0.1")
|
||||
ok, err := utils.CheckIPFilter("10.10.0.1", "10.10.0.1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, ok)
|
||||
|
||||
// Non-match IPv4
|
||||
ok, err = utils.FilterIP("10.10.0.1", "10.10.0.2")
|
||||
ok, err = utils.CheckIPFilter("10.10.0.1", "10.10.0.2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, false, ok)
|
||||
|
||||
// CIDR match IPv4
|
||||
ok, err = utils.FilterIP("10.10.0.0/24", "10.10.0.2")
|
||||
ok, err = utils.CheckIPFilter("10.10.0.0/24", "10.10.0.2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, ok)
|
||||
|
||||
// CIDR match IPv4 with '-' instead of '/'
|
||||
ok, err = utils.FilterIP("10.10.10.0-24", "10.10.10.5")
|
||||
ok, err = utils.CheckIPFilter("10.10.10.0-24", "10.10.10.5")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, ok)
|
||||
|
||||
// CIDR non-match IPv4
|
||||
ok, err = utils.FilterIP("10.10.0.0/24", "10.5.0.1")
|
||||
ok, err = utils.CheckIPFilter("10.10.0.0/24", "10.5.0.1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, false, ok)
|
||||
|
||||
// Invalid CIDR
|
||||
ok, err = utils.FilterIP("10.10.0.0/222", "10.0.0.1")
|
||||
assert.ErrorContains(t, err, "invalid CIDR address")
|
||||
ok, err = utils.CheckIPFilter("10.10.0.0/222", "10.0.0.1")
|
||||
assert.ErrorContains(t, err, "invalid cidr notation: invalid CIDR address: 10.10.0.0/222")
|
||||
assert.Equal(t, false, ok)
|
||||
|
||||
// Invalid IP in filter
|
||||
ok, err = utils.FilterIP("invalid_ip", "10.5.5.5")
|
||||
assert.ErrorContains(t, err, "invalid IP address in filter")
|
||||
ok, err = utils.CheckIPFilter("invalid_ip", "10.5.5.5")
|
||||
assert.ErrorContains(t, err, "invalid ip address")
|
||||
assert.Equal(t, false, ok)
|
||||
|
||||
// Invalid IP to check
|
||||
ok, err = utils.FilterIP("10.10.10.10", "invalid_ip")
|
||||
assert.ErrorContains(t, err, "invalid IP address")
|
||||
ok, err = utils.CheckIPFilter("10.10.10.10", "invalid_ip")
|
||||
assert.ErrorContains(t, err, "invalid ip address")
|
||||
assert.Equal(t, false, ok)
|
||||
}
|
||||
|
||||
func TestCheckFilter(t *testing.T) {
|
||||
// Empty filter
|
||||
assert.Equal(t, true, utils.CheckFilter("", "anystring"))
|
||||
_, err := utils.CheckFilter("", "anystring")
|
||||
assert.ErrorContains(t, err, "filter is empty")
|
||||
|
||||
// Exact match
|
||||
assert.Equal(t, true, utils.CheckFilter("hello", "hello"))
|
||||
ok, err := utils.CheckFilter("hello", "hello")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, ok)
|
||||
|
||||
// Regex match
|
||||
assert.Equal(t, true, utils.CheckFilter("/^h.*o$/", "hello"))
|
||||
ok, err = utils.CheckFilter("/^h.*o$/", "hello")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, ok)
|
||||
|
||||
// Invalid regex
|
||||
assert.Equal(t, false, utils.CheckFilter("/[unclosed", "test"))
|
||||
ok, err = utils.CheckFilter("/[unclosed/", "test")
|
||||
assert.ErrorContains(t, err, "invalid regex")
|
||||
assert.Equal(t, false, ok)
|
||||
|
||||
// Comma-separated values
|
||||
assert.Equal(t, true, utils.CheckFilter("apple, banana, cherry", "banana"))
|
||||
ok, err = utils.CheckFilter("apple, banana, cherry", "banana")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, ok)
|
||||
|
||||
// No match
|
||||
assert.Equal(t, false, utils.CheckFilter("apple, banana, cherry", "grape"))
|
||||
ok, err = utils.CheckFilter("apple, banana, cherry", "grape")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, false, ok)
|
||||
}
|
||||
|
||||
func TestGenerateUUID(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user