Skip to content

Commit c855f9b

Browse files
authored
feat: add support for deny by default access controls (#852)
1 parent a56c349 commit c855f9b

17 files changed

Lines changed: 1640 additions & 306 deletions

internal/bootstrap/app_bootstrap.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Services struct {
3535
ldapService *service.LdapService
3636
oauthBrokerService *service.OAuthBrokerService
3737
oidcService *service.OIDCService
38+
policyEngine *service.PolicyEngine
3839
}
3940

4041
type BootstrapApp struct {

internal/bootstrap/router_bootstrap.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func (app *BootstrapApp) setupRouter() error {
4444
controller.NewContextController(app.log, app.config, app.runtime, apiRouter)
4545
controller.NewOAuthController(app.log, app.config, app.runtime, apiRouter, app.services.authService)
4646
controller.NewOIDCController(app.log, app.services.oidcService, app.runtime, apiRouter)
47-
controller.NewProxyController(app.log, app.runtime, apiRouter, app.services.accessControlService, app.services.authService)
47+
controller.NewProxyController(app.log, app.runtime, apiRouter, app.services.accessControlService, app.services.authService, app.services.policyEngine)
4848
controller.NewUserController(app.log, app.runtime, apiRouter, app.services.authService)
4949
controller.NewResourcesController(app.config, &engine.RouterGroup)
5050
controller.NewHealthController(apiRouter)

internal/bootstrap/service_bootstrap.go

Lines changed: 85 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,38 +16,21 @@ func (app *BootstrapApp) setupServices() error {
1616

1717
app.services.ldapService = ldapService
1818

19-
useKubernetes := app.config.LabelProvider == "kubernetes" ||
20-
(app.config.LabelProvider == "auto" && os.Getenv("KUBERNETES_SERVICE_HOST") != "")
19+
labelProvider, err := app.getLabelProvider()
2120

22-
var labelProvider service.LabelProvider
23-
24-
if useKubernetes {
25-
app.log.App.Debug().Msg("Using Kubernetes label provider")
26-
27-
kubernetesService, err := service.NewKubernetesService(app.log, app.ctx, &app.wg)
28-
29-
if err != nil {
30-
return fmt.Errorf("failed to initialize kubernetes service: %w", err)
31-
}
21+
if err != nil {
22+
return fmt.Errorf("failed to initialize label provider: %w", err)
23+
}
3224

33-
app.services.kubernetesService = kubernetesService
34-
labelProvider = kubernetesService
35-
} else {
36-
app.log.App.Debug().Msg("Using Docker label provider")
25+
accessControlsService := service.NewAccessControlsService(app.log, app.config, &labelProvider)
26+
app.services.accessControlService = accessControlsService
3727

38-
dockerService, err := service.NewDockerService(app.log, app.ctx, &app.wg)
28+
err = app.setupPolicyEngine()
3929

40-
if err != nil {
41-
return fmt.Errorf("failed to initialize docker service: %w", err)
42-
}
43-
44-
app.services.dockerService = dockerService
45-
labelProvider = dockerService
30+
if err != nil {
31+
return fmt.Errorf("failed to initialize policy engine: %w", err)
4632
}
4733

48-
accessControlsService := service.NewAccessControlsService(app.log, &labelProvider, app.config.Apps)
49-
app.services.accessControlService = accessControlsService
50-
5134
oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx)
5235
app.services.oauthBrokerService = oauthBrokerService
5336

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

6548
return nil
6649
}
50+
51+
func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) {
52+
switch app.config.LabelProvider {
53+
case "none", "docker", "kubernetes", "auto":
54+
if app.config.LabelProvider == "none" {
55+
return nil, nil
56+
}
57+
58+
useKubernetes := app.config.LabelProvider == "kubernetes" ||
59+
(app.config.LabelProvider == "auto" && os.Getenv("KUBERNETES_SERVICE_HOST") != "")
60+
61+
if useKubernetes {
62+
app.log.App.Debug().Msg("Using Kubernetes label provider")
63+
64+
kubernetesService, err := service.NewKubernetesService(app.log, app.ctx, &app.wg)
65+
66+
if err != nil {
67+
return nil, fmt.Errorf("failed to initialize kubernetes service: %w", err)
68+
}
69+
70+
app.services.kubernetesService = kubernetesService
71+
return kubernetesService, nil
72+
}
73+
74+
app.log.App.Debug().Msg("Using Docker label provider")
75+
76+
dockerService, err := service.NewDockerService(app.log, app.ctx, &app.wg)
77+
78+
if err != nil {
79+
return nil, fmt.Errorf("failed to initialize docker service: %w", err)
80+
}
81+
82+
if dockerService == nil {
83+
if app.config.LabelProvider == "docker" {
84+
app.log.App.Warn().Msg("Docker label provider selected but Docker is not available, will continue without it")
85+
}
86+
return nil, nil
87+
}
88+
89+
app.services.dockerService = dockerService
90+
return dockerService, nil
91+
default:
92+
return nil, fmt.Errorf("invalid label provider: %s", app.config.LabelProvider)
93+
}
94+
}
95+
96+
func (app *BootstrapApp) setupPolicyEngine() error {
97+
policyEngine, err := service.NewPolicyEngine(app.config, app.log)
98+
99+
if err != nil {
100+
return fmt.Errorf("failed to initialize policy engine: %w", err)
101+
}
102+
103+
policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{
104+
Log: app.log,
105+
})
106+
policyEngine.RegisterRule(service.RuleOAuthGroup, &service.OAuthGroupRule{
107+
Log: app.log,
108+
})
109+
policyEngine.RegisterRule(service.RuleLDAPGroup, &service.LDAPGroupRule{
110+
Log: app.log,
111+
})
112+
policyEngine.RegisterRule(service.RuleAuthEnabled, &service.AuthEnabledRule{
113+
Log: app.log,
114+
})
115+
policyEngine.RegisterRule(service.RuleIPAllowed, &service.IPAllowedRule{
116+
Log: app.log,
117+
Config: app.config,
118+
})
119+
policyEngine.RegisterRule(service.RuleIPBypassed, &service.IPBypassedRule{
120+
Log: app.log,
121+
})
122+
123+
app.services.policyEngine = policyEngine
124+
return nil
125+
}

internal/controller/proxy_controller.go

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package controller
33
import (
44
"errors"
55
"fmt"
6+
"net"
67
"net/http"
78
"net/url"
89
"regexp"
@@ -51,10 +52,11 @@ type ProxyContext struct {
5152
}
5253

5354
type ProxyController struct {
54-
log *logger.Logger
55-
runtime model.RuntimeConfig
56-
acls *service.AccessControlsService
57-
auth *service.AuthService
55+
log *logger.Logger
56+
runtime model.RuntimeConfig
57+
acls *service.AccessControlsService
58+
auth *service.AuthService
59+
policyEngine *service.PolicyEngine
5860
}
5961

6062
func NewProxyController(
@@ -63,12 +65,14 @@ func NewProxyController(
6365
router *gin.RouterGroup,
6466
acls *service.AccessControlsService,
6567
auth *service.AuthService,
68+
policyEngine *service.PolicyEngine,
6669
) *ProxyController {
6770
controller := &ProxyController{
68-
log: log,
69-
runtime: runtime,
70-
acls: acls,
71-
auth: auth,
71+
log: log,
72+
runtime: runtime,
73+
acls: acls,
74+
auth: auth,
75+
policyEngine: policyEngine,
7276
}
7377

7478
proxyGroup := router.Group("/auth")
@@ -101,7 +105,13 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
101105

102106
clientIP := c.ClientIP()
103107

104-
if controller.auth.IsBypassedIP(clientIP, acls) {
108+
aclsCtx := &service.ACLContext{
109+
ACLs: acls,
110+
IP: net.ParseIP(clientIP),
111+
Path: proxyCtx.Path,
112+
}
113+
114+
if controller.policyEngine.Evaluate(service.RuleIPBypassed, aclsCtx) {
105115
controller.setHeaders(c, acls)
106116
c.JSON(200, gin.H{
107117
"status": 200,
@@ -110,15 +120,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
110120
return
111121
}
112122

113-
authEnabled, err := controller.auth.IsAuthEnabled(proxyCtx.Path, acls)
114-
115-
if err != nil {
116-
controller.log.App.Error().Err(err).Msg("Failed to determine if authentication is enabled for resource")
117-
controller.handleError(c, proxyCtx)
118-
return
119-
}
120-
121-
if !authEnabled {
123+
if controller.policyEngine.Evaluate(service.RuleAuthEnabled, aclsCtx) {
122124
controller.log.App.Debug().Msg("Authentication is disabled for this resource, allowing access without authentication")
123125
controller.setHeaders(c, acls)
124126
c.JSON(200, gin.H{
@@ -128,7 +130,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
128130
return
129131
}
130132

131-
if !controller.auth.CheckIP(clientIP, acls) {
133+
if !controller.policyEngine.Evaluate(service.RuleIPAllowed, aclsCtx) {
132134
queries, err := query.Values(UnauthorizedQuery{
133135
Resource: strings.Split(proxyCtx.Host, ".")[0],
134136
IP: clientIP,
@@ -164,10 +166,10 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
164166
}
165167
}
166168

167-
if userContext.Authenticated {
168-
userAllowed := controller.auth.IsUserAllowed(c, *userContext, acls)
169+
aclsCtx.UserContext = userContext
169170

170-
if !userAllowed {
171+
if userContext.Authenticated {
172+
if !controller.policyEngine.Evaluate(service.RuleUserAllowed, aclsCtx) {
171173
controller.log.App.Warn().Str("user", userContext.GetUsername()).Str("resource", strings.Split(proxyCtx.Host, ".")[0]).Msg("User is not allowed to access resource")
172174

173175
queries, err := query.Values(UnauthorizedQuery{
@@ -205,9 +207,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
205207
var groupOK bool
206208

207209
if userContext.IsOAuth() {
208-
groupOK = controller.auth.IsInOAuthGroup(c, *userContext, acls)
210+
groupOK = controller.policyEngine.Evaluate(service.RuleOAuthGroup, aclsCtx)
209211
} else {
210-
groupOK = controller.auth.IsInLDAPGroup(c, *userContext, acls)
212+
groupOK = controller.policyEngine.Evaluate(service.RuleLDAPGroup, aclsCtx)
211213
}
212214

213215
if !groupOK {

internal/controller/proxy_controller_test.go

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/gin-gonic/gin"
1010
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
1112
"github.com/tinyauthapp/tinyauth/internal/controller"
1213
"github.com/tinyauthapp/tinyauth/internal/model"
1314
"github.com/tinyauthapp/tinyauth/internal/repository/memory"
@@ -22,33 +23,6 @@ func TestProxyController(t *testing.T) {
2223

2324
cfg, runtime := test.CreateTestConfigs(t)
2425

25-
acls := map[string]model.App{
26-
"app_path_allow": {
27-
Config: model.AppConfig{
28-
Domain: "path-allow.example.com",
29-
},
30-
Path: model.AppPath{
31-
Allow: "/allowed",
32-
},
33-
},
34-
"app_user_allow": {
35-
Config: model.AppConfig{
36-
Domain: "user-allow.example.com",
37-
},
38-
Users: model.AppUsers{
39-
Allow: "testuser",
40-
},
41-
},
42-
"ip_bypass": {
43-
Config: model.AppConfig{
44-
Domain: "ip-bypass.example.com",
45-
},
46-
IP: model.AppIP{
47-
Bypass: []string{"10.10.10.10"},
48-
},
49-
},
50-
}
51-
5226
const browserUserAgent = `
5327
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`
5428

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

385359
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
386360
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, store, broker)
387-
aclsService := service.NewAccessControlsService(log, nil, acls)
361+
aclsService := service.NewAccessControlsService(log, cfg, nil)
362+
363+
policyEngine, err := service.NewPolicyEngine(cfg, log)
364+
require.NoError(t, err)
365+
366+
policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{
367+
Log: log,
368+
})
369+
policyEngine.RegisterRule(service.RuleOAuthGroup, &service.OAuthGroupRule{
370+
Log: log,
371+
})
372+
policyEngine.RegisterRule(service.RuleLDAPGroup, &service.LDAPGroupRule{
373+
Log: log,
374+
})
375+
policyEngine.RegisterRule(service.RuleAuthEnabled, &service.AuthEnabledRule{
376+
Log: log,
377+
})
378+
policyEngine.RegisterRule(service.RuleIPAllowed, &service.IPAllowedRule{
379+
Log: log,
380+
Config: cfg,
381+
})
382+
policyEngine.RegisterRule(service.RuleIPBypassed, &service.IPBypassedRule{
383+
Log: log,
384+
})
388385

389386
for _, test := range tests {
390387
t.Run(test.description, func(t *testing.T) {
@@ -399,7 +396,7 @@ func TestProxyController(t *testing.T) {
399396

400397
recorder := httptest.NewRecorder()
401398

402-
controller.NewProxyController(log, runtime, group, aclsService, authService)
399+
controller.NewProxyController(log, runtime, group, aclsService, authService, policyEngine)
403400

404401
test.run(t, router, recorder)
405402
})

internal/model/config.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ func NewDefaultConfiguration() *Config {
2525
SessionMaxLifetime: 0, // disabled
2626
LoginTimeout: 300, // 5 minutes
2727
LoginMaxRetries: 3,
28+
ACLs: ACLsConfig{
29+
Policy: "allow",
30+
},
2831
},
2932
UI: UIConfig{
3033
Title: "Tinyauth",
@@ -79,7 +82,7 @@ type Config struct {
7982
UI UIConfig `description:"UI customization." yaml:"ui"`
8083
LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap"`
8184
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
82-
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, or kubernetes). auto detects the environment." yaml:"labelProvider"`
85+
LabelProvider string `description:"Label provider to use for ACLs (auto, docker, kubernetes or none to disable). auto detects the environment." yaml:"labelProvider"`
8386
Log LogConfig `description:"Logging configuration." yaml:"log"`
8487
}
8588

@@ -116,6 +119,7 @@ type AuthConfig struct {
116119
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
117120
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
118121
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
122+
ACLs ACLsConfig `description:"ACLs configuration." yaml:"acls"`
119123
}
120124

121125
type UserAttributes struct {
@@ -225,6 +229,10 @@ type OIDCClientConfig struct {
225229
Name string `description:"Client name in UI." yaml:"name"`
226230
}
227231

232+
type ACLsConfig struct {
233+
Policy string `description:"ACL policy for allow-by-default or deny-by-default, available options are allow and deny, default is allow." yaml:"policy"`
234+
}
235+
228236
// ACLs
229237

230238
type Apps struct {

0 commit comments

Comments
 (0)