Skip to content

feat: add support for deny by default access controls#852

Open
steveiliop56 wants to merge 4 commits into
mainfrom
feat/deny-by-default-acls
Open

feat: add support for deny by default access controls#852
steveiliop56 wants to merge 4 commits into
mainfrom
feat/deny-by-default-acls

Conversation

@steveiliop56
Copy link
Copy Markdown
Member

@steveiliop56 steveiliop56 commented May 12, 2026

Solves #850


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

Summary by CodeRabbit

  • New Features

    • Configurable ACL policy (allow/deny) with default "allow".
    • OAuth/LDAP group checks, URI pattern rules, and IP allow/block/bypass enforcement.
    • Option to disable external label provider ("none").
  • Refactor

    • Authorization responsibilities moved into a dedicated access-controls service.
  • Tests

    • Updated test configs to cover ACL policy, path rules, user rules, and IP bypass cases.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e83183d8-5dcf-4ffc-8b80-a7668241abe5

📥 Commits

Reviewing files that changed from the base of the PR and between b9abab2 and f8b0188.

📒 Files selected for processing (4)
  • internal/bootstrap/service_bootstrap.go
  • internal/controller/proxy_controller.go
  • internal/model/config.go
  • internal/service/auth_service.go
💤 Files with no reviewable changes (1)
  • internal/service/auth_service.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/model/config.go
  • internal/controller/proxy_controller.go

📝 Walkthrough

Walkthrough

Authorization predicates were moved out of AuthService into a config-driven AccessControlsService (policy: allow/deny), AccessControlsService gained new predicate methods, bootstrap now wires label provider and config into the service, proxy controller calls were updated, and tests/default configs were adjusted.

Changes

ACL Logic Refactoring

Layer / File(s) Summary
Config and service contract
internal/model/config.go, internal/service/access_controls_service.go
ACLSConfig added under AuthConfig; AccessControlPolicy introduced; AccessControlsService now accepts model.Config, parses/stores policy, and had import updates.
Static and dynamic ACL lookup
internal/service/access_controls_service.go
Static ACL lookup iterates config.Apps for domain/name matches; GetAccessControls prefers static config ACLs then falls back to LabelProvider.
Authorization predicates
internal/service/access_controls_service.go
New methods: IsUserAllowed, IsInOAuthGroup, IsInLDAPGroup, IsAuthEnabled (URI regex allow/block), IsIPAllowed (global+app allow/block with policy fallback), IsIPBypassed, and policyResult.
AuthService cleanup
internal/service/auth_service.go
Removed Gin/regexp-dependent ACL helper methods so AuthService focuses on OAuth session management.
ProxyController integration
internal/controller/proxy_controller.go
ProxyController.proxyHandler now uses AccessControlsService methods for IP bypass, auth-enabled, IP allow, user allow, and OAuth/LDAP group checks (no gin.Context param).
Bootstrap label-provider wiring
internal/bootstrap/service_bootstrap.go
Added getLabelProvider() helper; bootstrap initializes LabelProvider (supports none) and passes app.config + labelProvider into NewAccessControlsService.
Tests and test configs
internal/controller/proxy_controller_test.go, internal/test/test.go
Tests removed hardcoded ACL map, construct AccessControlsService from test config; test helper sets Auth.ACLs.Policy = "allow" and adds sample apps for path/user/IP scenarios.

Sequence Diagram

sequenceDiagram
  participant Client
  participant ProxyController
  participant AccessControlsService
  participant LabelProvider
  participant AuthService

  Client->>ProxyController: HTTP request
  ProxyController->>AccessControlsService: GetAccessControls(domain)
  AccessControlsService->>LabelProvider: Query labels (if no static ACLs)
  LabelProvider-->>AccessControlsService: app ACLs
  ProxyController->>AccessControlsService: IsIPBypassed / IsAuthEnabled / IsIPAllowed / IsUserAllowed
  AccessControlsService-->>ProxyController: boolean decisions
  ProxyController->>AuthService: OAuth flow (if auth required)
  AuthService-->>ProxyController: auth result / session
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • tinyauthapp/tinyauth#627: Main PR’s refactor of label-provider wiring (service_bootstrap.go) and the corresponding AccessControlsService constructor/label fallback logic directly builds on the Kubernetes label-provider feature introduced in PR #627.
  • tinyauthapp/tinyauth#549: Modifies AccessControlsService constructor to support config-based ACLs and updates wiring/tests.
  • tinyauthapp/tinyauth#422: Integrates AccessControlsService into the proxy flow and adjusts auth/ACL responsibilities.

Suggested labels

lgtm

Suggested reviewers

  • Rycochet
  • scottmckendry

"A rabbit hops through code so spry,
Moving guards where rules apply,
Config whispers 'allow' or 'deny',
ACLs stand watch as requests pass by,
Tests and bootstrap nod—mission spry."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: adding support for deny-by-default access controls, which is reflected across multiple files including the new policy configuration, access controls service refactoring, and authorization logic changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/deny-by-default-acls

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label May 12, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
internal/controller/proxy_controller_test.go (1)

25-25: ⚡ Quick win

Add tests for the new deny/block policy.

The shared cfg from test.CreateTestConfigs(t) doesn't set Auth.ACLS.Policy, so the entire deny-by-default code path introduced in this PR is never executed by TestProxyController. The existing happy-path/path-allow/ip-bypass/user-allow cases all bypass policyResult either via explicit filters or via the bypass/auth-disabled short circuits.

Consider adding at least:

  • A test where cfg.Auth.ACLS.Policy = "block" and a request hits a domain with no matching Apps entry — assert it's denied at the IP allow step.
  • A test where cfg.Auth.ACLS.Policy = "block" and a request hits a domain with an explicit IP.Allow (or Users.Allow) — assert it succeeds.
  • A test with an invalid policy string (e.g., "deny", "") — assert it logs a warning and behaves as the documented default ("allow"). This will also catch the fallback regression flagged in NewAccessControlsService.

Also applies to: 367-367

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/proxy_controller_test.go` at line 25, The tests for
TestProxyController never exercise the new deny-by-default policy because
test.CreateTestConfigs(t) doesn't set cfg.Auth.ACLS.Policy; add new unit tests
that set cfg.Auth.ACLS.Policy = "block" and exercise the code paths around
policyResult: (1) a request to a domain with no matching Apps entry should be
denied at the IP allow step, (2) a request to a domain with an explicit IP.Allow
or Users.Allow should succeed, and (3) a test with an invalid policy string
(e.g., "deny" or "") should assert a warning is logged and behavior falls back
to documented default ("allow") to catch the NewAccessControlsService fallback
regression. Ensure tests reference and assert on the same controller flow
exercised by TestProxyController so policyResult is evaluated rather than
bypassed.
internal/controller/proxy_controller.go (1)

53-72: ⚡ Quick win

Remove unused auth field and constructor parameter from ProxyController.

After the refactor, all authorization checks in proxyHandler delegate to controller.acls (IP bypass, auth-enabled status, user allowed, groups). The auth field on the struct and corresponding auth parameter in NewProxyController are never referenced. Remove them to keep the dependency surface minimal and make intent clearer. Update the wiring in internal/bootstrap/router_bootstrap.go:47 and test setup accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/proxy_controller.go` around lines 53 - 72, The
ProxyController struct and constructor still include an unused auth dependency:
remove the auth field from the ProxyController struct and remove the auth
parameter (type *service.AuthService) from NewProxyController signature and its
struct literal, then update all call sites that invoke NewProxyController to
stop passing the auth argument (including bootstrap/router wiring and tests) so
the code compiles with the reduced dependency surface; ensure any imports or
test fixtures related only to auth are cleaned up.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/model/config.go`:
- Around line 230-232: The ACLSConfig description string is inconsistent and has
a typo: change "deny-by-defaut" to "deny-by-default" and make the allowed values
consistent with accessControlPolicyFromString by either (preferred) switching
the internal parser/constants in access_controls_service.go (and any related
constants) to accept the YAML value "deny" instead of "block", or conversely
update this description to list "allow and block"; specifically modify
ACLSConfig (type ACLSConfig) description text and update
accessControlPolicyFromString (and its associated policy constants) so the
accepted policy string matches the user-facing value ("deny") throughout the
codebase.

In `@internal/service/access_controls_service.go`:
- Around line 213-215: The current append usage can mutate the shared backing
arrays for service.config.Auth.IP.Block/Allow; to fix, create new slices before
concatenation so you never write into the shared slice: allocate a fresh slice
(e.g., with make or by starting from a nil-typed slice) and copy or append the
global entries then the per-app entries to produce blockedIps and allowedIPs;
update the code that constructs blockedIps and allowedIPs (the lines using
append with service.config.Auth.IP.Block and acls.IP.Block, and similarly for
.Allow) to use the safe allocation/copy approach (or slices.Concat if you target
Go ≥1.22).
- Around line 41-68: NewAccessControlsService sets service.policy incorrectly
when accessControlPolicyFromString returns ok==false: it first assigns
service.policy = PolicyAllow then later logs and overwrites service.policy with
the empty/local variable policy, leaving an invalid value. Fix by using the
validated policy variable (or set policy = PolicyAllow when !ok) before any
logging and before assigning into service.policy; specifically adjust
NewAccessControlsService to set policy = PolicyAllow when
accessControlPolicyFromString returns ok==false (and keep the warning log), then
use that policy for the subsequent if/else logging and final service.policy
assignment so service.policy and the logged policy remain consistent
(references: NewAccessControlsService, accessControlPolicyFromString,
service.policy, policy, PolicyAllow).
- Around line 106-125: IsUserAllowed currently treats an existing ACL with empty
users filters as allowing access because utils.CheckFilter("", username) returns
true; change IsUserAllowed (and similarly handle the OAuth whitelist if desired)
to detect when both acls.Users.Block and acls.Users.Allow are empty and route
that case through service.policyResult(true) instead of calling
utils.CheckFilter, so the global policy (policyResult) is respected when an ACL
exists but contains no user filters; use the symbols IsUserAllowed,
acls.Users.Block, acls.Users.Allow, utils.CheckFilter, and service.policyResult
to locate and modify the logic.

---

Nitpick comments:
In `@internal/controller/proxy_controller_test.go`:
- Line 25: The tests for TestProxyController never exercise the new
deny-by-default policy because test.CreateTestConfigs(t) doesn't set
cfg.Auth.ACLS.Policy; add new unit tests that set cfg.Auth.ACLS.Policy = "block"
and exercise the code paths around policyResult: (1) a request to a domain with
no matching Apps entry should be denied at the IP allow step, (2) a request to a
domain with an explicit IP.Allow or Users.Allow should succeed, and (3) a test
with an invalid policy string (e.g., "deny" or "") should assert a warning is
logged and behavior falls back to documented default ("allow") to catch the
NewAccessControlsService fallback regression. Ensure tests reference and assert
on the same controller flow exercised by TestProxyController so policyResult is
evaluated rather than bypassed.

In `@internal/controller/proxy_controller.go`:
- Around line 53-72: The ProxyController struct and constructor still include an
unused auth dependency: remove the auth field from the ProxyController struct
and remove the auth parameter (type *service.AuthService) from
NewProxyController signature and its struct literal, then update all call sites
that invoke NewProxyController to stop passing the auth argument (including
bootstrap/router wiring and tests) so the code compiles with the reduced
dependency surface; ensure any imports or test fixtures related only to auth are
cleaned up.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d27ab4c6-72fd-4514-9274-045ba1cf85e8

📥 Commits

Reviewing files that changed from the base of the PR and between a9eac7e and 3fd5627.

📒 Files selected for processing (7)
  • internal/bootstrap/service_bootstrap.go
  • internal/controller/proxy_controller.go
  • internal/controller/proxy_controller_test.go
  • internal/model/config.go
  • internal/service/access_controls_service.go
  • internal/service/auth_service.go
  • internal/test/test.go
💤 Files with no reviewable changes (1)
  • internal/service/auth_service.go

Comment thread internal/model/config.go Outdated
Comment thread internal/service/access_controls_service.go
Comment thread internal/service/access_controls_service.go
Comment thread internal/service/access_controls_service.go Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/service/access_controls_service.go (1)

71-82: 💤 Low value

Prefer the explicit local-copy pattern when storing a pointer to the loop value.

appAcls = &config takes the address of the range loop variable. Although the immediate break makes this functionally safe, the project convention is to materialize an explicit local copy to make intent obvious and avoid loop-variable capture confusion.

♻️ Proposed refactor
 	var appAcls *model.App
 	for app, config := range service.config.Apps {
+		appConfig := config
 		if config.Config.Domain == domain {
 			service.log.App.Debug().Str("name", app).Msg("Found matching container by domain")
-			appAcls = &config
+			appAcls = &appConfig
 			break // If we find a match by domain, we can stop searching
 		}
 
 		if strings.SplitN(domain, ".", 2)[0] == app {
 			service.log.App.Debug().Str("name", app).Msg("Found matching container by app name")
-			appAcls = &config
+			appAcls = &appConfig
 			break // If we find a match by app name, we can stop searching
 		}
 	}

Based on learnings: "prefer the explicit local copy pattern (e.g., result := config; return &result) rather than returning &<range-variable> directly … the project prefers the explicit copy for clarity and to avoid reviewer confusion about loop-variable capture semantics."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/access_controls_service.go` around lines 71 - 82, The code
takes the address of the range loop variable (config) when assigning appAcls
(appAcls = &config); change this to the explicit local-copy pattern to avoid
loop-variable capture confusion: create a new local variable (e.g., cfg :=
config) and assign appAcls = &cfg instead for both matches (the domain check and
the app-name check) while keeping the break logic and using the existing
service.config.Apps, appAcls, and config identifiers to locate the spots to
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/service/access_controls_service.go`:
- Around line 173-205: The Path.Block branch in
AccessControlsService.IsAuthEnabled currently uses a negation (if
!regex.MatchString(uri) { return false }) which inverts the intended behavior;
remove the negation so that matching the block regex disables auth (i.e., if
regex.MatchString(uri) { return false }). Also avoid compiling regexes on every
call to IsAuthEnabled: precompile and cache the regex objects for
acls.Path.Block and acls.Path.Allow (either during service initialization or by
adding compiled fields on the model.App/ACL struct) and use those cached
*regexp.Regexp instances in IsAuthEnabled; preserve existing error logging via
service.log.App.Error() when compilation fails during initialization or when
setting the ACL.

---

Nitpick comments:
In `@internal/service/access_controls_service.go`:
- Around line 71-82: The code takes the address of the range loop variable
(config) when assigning appAcls (appAcls = &config); change this to the explicit
local-copy pattern to avoid loop-variable capture confusion: create a new local
variable (e.g., cfg := config) and assign appAcls = &cfg instead for both
matches (the domain check and the app-name check) while keeping the break logic
and using the existing service.config.Apps, appAcls, and config identifiers to
locate the spots to change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a7e25ccc-eb0b-4e59-b3b4-8d0fc39ae11f

📥 Commits

Reviewing files that changed from the base of the PR and between 3fd5627 and b9abab2.

📒 Files selected for processing (3)
  • internal/model/config.go
  • internal/service/access_controls_service.go
  • internal/test/test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/test/test.go
  • internal/model/config.go

Comment thread internal/service/access_controls_service.go
@arcoast
Copy link
Copy Markdown

arcoast commented May 14, 2026

Whilst I can't help with code reviews, I 'm more than happy to build locally and help test this if that's of any use to you @steveiliop56

@steveiliop56
Copy link
Copy Markdown
Member Author

@arcoast it would be better to wait for an alpha/beta release. But if you like, you can test locally using the development mode and let me know if everything works correctly. Any feedback is appreciated!

@arcoast
Copy link
Copy Markdown

arcoast commented May 15, 2026

I don't mind, whatever is most useful for you.

If you do want me to test then let me know what the labels/envvars required.

@steveiliop56
Copy link
Copy Markdown
Member Author

This change is quite simple, all you need to do is set TINYAUTH_AUTH_ACLS_POLICY=deny and you are done. Tinyauth will reject everything and everyone unless specifically authorized.

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label May 16, 2026
Comment on lines +66 to +75
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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically there might not be a docker provider available either - more defensive to check for it and report if it's set to auto?


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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OAuth bypasses policy?

Suggested change
return utils.CheckFilter(acls.OAuth.Whitelist, context.OAuth.Email)
return service.policyResult(utils.CheckFilter(acls.OAuth.Whitelist, context.OAuth.Email))

Otherwise "deny-by-default" provides false sense of security for Oauth users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants