Skip to content
1 change: 1 addition & 0 deletions internal/bootstrap/app_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type Services struct {
ldapService *service.LdapService
oauthBrokerService *service.OAuthBrokerService
oidcService *service.OIDCService
policyEngine *service.PolicyEngine
}

type BootstrapApp struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/bootstrap/router_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
111 changes: 85 additions & 26 deletions internal/bootstrap/service_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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") != "")
labelProvider, err := app.getLabelProvider()

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)

if err != nil {
return fmt.Errorf("failed to initialize kubernetes service: %w", err)
}
if err != nil {
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")
accessControlsService := service.NewAccessControlsService(app.log, app.config, &labelProvider)
Comment thread
steveiliop56 marked this conversation as resolved.
app.services.accessControlService = accessControlsService

dockerService, err := service.NewDockerService(app.log, app.ctx, &app.wg)
err = app.setupPolicyEngine()

if err != nil {
return fmt.Errorf("failed to initialize docker service: %w", err)
}

app.services.dockerService = dockerService
labelProvider = dockerService
if err != nil {
return fmt.Errorf("failed to initialize policy engine: %w", err)
}

accessControlsService := service.NewAccessControlsService(app.log, &labelProvider, app.config.Apps)
app.services.accessControlService = accessControlsService

oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx)
app.services.oauthBrokerService = oauthBrokerService

Expand All @@ -64,3 +47,79 @@ func (app *BootstrapApp) setupServices() error {

return nil
}

func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) {
switch app.config.LabelProvider {
case "none", "docker", "kubernetes", "auto":
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)
}

if dockerService == nil {
if app.config.LabelProvider == "docker" {
app.log.App.Warn().Msg("Docker label provider selected but Docker is not available, will continue without it")
}
return nil, nil
}

app.services.dockerService = dockerService
return dockerService, nil
default:
return nil, fmt.Errorf("invalid label provider: %s", app.config.LabelProvider)
}
}
Comment thread
steveiliop56 marked this conversation as resolved.

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
}
50 changes: 26 additions & 24 deletions internal/controller/proxy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package controller
import (
"errors"
"fmt"
"net"
"net/http"
"net/url"
"regexp"
Expand Down Expand Up @@ -51,10 +52,11 @@ type ProxyContext struct {
}

type ProxyController struct {
log *logger.Logger
runtime model.RuntimeConfig
acls *service.AccessControlsService
auth *service.AuthService
log *logger.Logger
runtime model.RuntimeConfig
acls *service.AccessControlsService
auth *service.AuthService
policyEngine *service.PolicyEngine
}

func NewProxyController(
Expand All @@ -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,
log: log,
runtime: runtime,
acls: acls,
auth: auth,
policyEngine: policyEngine,
}

proxyGroup := router.Group("/auth")
Expand Down Expand Up @@ -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,
Expand All @@ -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{
Expand All @@ -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,
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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 {
Expand Down
55 changes: 26 additions & 29 deletions internal/controller/proxy_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tinyauthapp/tinyauth/internal/controller"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
Expand All @@ -22,33 +23,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`

Expand Down Expand Up @@ -384,7 +358,30 @@ func TestProxyController(t *testing.T) {

broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, store, 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) {
Expand All @@ -399,7 +396,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)
})
Expand Down
10 changes: 9 additions & 1 deletion internal/model/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ func NewDefaultConfiguration() *Config {
SessionMaxLifetime: 0, // disabled
LoginTimeout: 300, // 5 minutes
LoginMaxRetries: 3,
ACLs: ACLsConfig{
Policy: "allow",
},
},
UI: UIConfig{
Title: "Tinyauth",
Expand Down Expand Up @@ -79,7 +82,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"`
}

Expand Down Expand Up @@ -116,6 +119,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 {
Expand Down Expand Up @@ -225,6 +229,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 {
Expand Down
Loading
Loading