Skip to content

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

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

feat: add support for deny by default access controls#852
steveiliop56 wants to merge 2 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 set to allow.
    • OAuth/LDAP group-based access control, URI pattern rules, and IP allow/block/bypass support.
  • Refactor

    • Authorization logic reorganized into a dedicated access-controls service for clearer responsibility.
  • Tests

    • Test configurations updated to exercise ACL policy, path rules, user rules, and IP bypass scenarios.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

📝 Walkthrough

Walkthrough

Authorization logic is moved from AuthService to AccessControlsService, which now reads ACLS from model.Config (policy: allow/deny), implements user/group/IP/path predicates, and is wired in bootstrap, proxy controller, and tests.

Changes

ACL Logic Refactoring

Layer / File(s) Summary
Config and service contract
internal/model/config.go, internal/service/access_controls_service.go
ACLSConfig added to AuthConfig; AccessControlPolicy introduced; AccessControlsService constructor changed to accept model.Config and store a parsed policy.
Static and dynamic ACL lookup
internal/service/access_controls_service.go
Static ACL lookup iterates config.Apps; GetAccessControls prefers static 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; file now focuses on OAuth session management.
ProxyController integration
internal/controller/proxy_controller.go
All authorization checks switched from AuthService to AccessControlsService; group checks no longer accept gin.Context.
Bootstrap and tests
internal/bootstrap/service_bootstrap.go, internal/controller/proxy_controller_test.go, internal/test/test.go
Bootstrap passes config to NewAccessControlsService; tests remove hardcoded ACL map and use test config; test helper sets Auth.ACLs and sample apps for path/user/IP scenarios.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

size:XXL, 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 clearly summarizes the main change: adding support for deny-by-default access controls, which aligns with the refactoring of AccessControlsService to use a configurable policy (allow/deny) and the movement of authorization logic from AuthService.
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant