Skip to content

Refactor/acl decoder#435

Closed
nicotsx wants to merge 3 commits intomainfrom
refactor/acl-decoder
Closed

Refactor/acl decoder#435
nicotsx wants to merge 3 commits intomainfrom
refactor/acl-decoder

Conversation

@nicotsx
Copy link
Copy Markdown
Collaborator

@nicotsx nicotsx commented Oct 29, 2025

Implement a new recursive algorithm to parse ACL flags and environment variables and expand it.
Recursively navigates nested struct/map types using reflection to normalize flat key paths into hierarchical structures.

Allows stuff like:

--apps-whoami-super-config-domain=...
TINYAUTH_APPS_WHOAMI_SUPER_CONFIG_DOMAIN=whoami_super.127.0.0.1.sslip.io

Summary by CodeRabbit

  • New Features
    • ACL configuration can be provided via command-line flags (e.g., --apps- / --tinyauth-apps-) as an alternative to environment variables.
    • Command-line ACL flags override corresponding environment variable settings.
    • The CLI now tolerates unknown flags to allow passing ACL-related flags without interfering with other commands.
  • Refactor
    • ACL parsing and merging logic centralized to handle env + flag sources consistently.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Oct 29, 2025

Walkthrough

This PR adds end-to-end ACL flag support: CLI ACL flags are extracted at startup, passed into the bootstrap/app, and injected into the AccessControlsService. ACL config is now assembled from environment and CLI flags (flags override env) using new utils and decoder helpers.

Changes

Cohort / File(s) Summary
CLI ACL flag extraction
cmd/root.go
Adds os import, introduces aclFlags field on rootCmd, enables Cobra to ignore unknown flags, extracts ACL flags from args via utils.ExtractACLFlags(os.Args[1:]), and passes them to bootstrap.NewBootstrapApp.
Bootstrap ACL flag wiring
internal/bootstrap/app_bootstrap.go
Extends BootstrapApp to store aclFlags and accepts aclFlags in NewBootstrapApp; passes flags to AccessControlsService during setup.
AccessControlsService ACL configuration
internal/service/access_controls_service.go
Adds aclFlags field and SetACLFlags() method; Init() now delegates ACL aggregation to utils.GetACLsConfig(env, aclFlags) instead of internal env parsing.
ACL utilities and aggregation
internal/utils/app_utils.go
Adds GetACLsConfig(env, flagsMap) to decode and merge ACLs from env and flags (flags override env), and ExtractACLFlags(args) to collect --apps- / --tinyauth-apps- flags.
ACL decoder utilities
internal/utils/decoders/decoders.go
Adds ParsePath() and normalizeACLKeys[T] to map ACL-style keys into dot-delimited tinyauth paths using reflection.
Environment and flags ACL decoders
internal/utils/decoders/env_decoder.go, internal/utils/decoders/flags_decoder.go
Adds DecodeACLEnv[T] and DecodeACLFlags[T] to decode ACL-specific environment variables and flag maps using the ACL normalization helpers.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI Startup
    participant Root as cmd/root.go
    participant Bootstrap as bootstrap.NewBootstrapApp
    participant ACLService as AccessControlsService
    participant Utils as utils.GetACLsConfig
    participant Decoders as Decoders

    CLI->>Root: start with args
    Root->>Root: ExtractACLFlags(os.Args[1:]) -> aclFlags
    Root->>Bootstrap: NewBootstrapApp(conf, aclFlags)
    Bootstrap->>ACLService: New + SetACLFlags(aclFlags)
    ACLService->>Utils: GetACLsConfig(env, aclFlags)
    Utils->>Decoders: DecodeACLEnv(env)
    Utils->>Decoders: DecodeACLFlags(flags)
    Utils->>ACLService: Merged ACL config (flags override env)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–25 minutes

  • Key areas requiring attention:
    • normalizeACLKeys / ParsePath reflection logic (edge cases for nested structs/maps and key normalization).
    • Correctness of ExtractACLFlags parsing (prefix matching, splitting on '=', handling repeated keys).
    • Merge precedence and error propagation in GetACLsConfig and decoder functions.
    • Ensure FParseErrWhitelist change in cmd/root.go doesn't mask important flag errors.

Possibly related PRs

Suggested reviewers

  • steveiliop56
  • Rycochet

Poem

🐰 I hopped through args at break of day,
Collected flags that came my way,
Carried them to bootstrap's nest,
Decoders sorted, merged the rest,
Now ACLs prance — all set to play! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "Refactor/acl decoder" accurately references a core component of the changeset. The PR implements substantial changes across multiple files to introduce new ACL flag parsing and decoding functionality, including new recursive algorithms and decoder functions (DecodeACLEnv, DecodeACLFlags, GetACLsConfig, ExtractACLFlags). While the title uses a "Refactor/" prefix that might suggest code reorganization rather than feature addition, it clearly identifies the ACL decoder as the focal point of the changes. A teammate scanning history would understand this PR involves significant ACL decoder modifications, making it a suitable albeit somewhat partial descriptor of the full scope.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/acl-decoder

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.

@nicotsx nicotsx force-pushed the refactor/acl-decoder branch from a0fa45f to 9014be9 Compare October 29, 2025 17:50
@nicotsx nicotsx marked this pull request as ready for review October 29, 2025 17:51
@codecov
Copy link
Copy Markdown

codecov Bot commented Oct 29, 2025

Codecov Report

❌ Patch coverage is 0% with 128 lines in your changes missing coverage. Please review.
✅ Project coverage is 22.18%. Comparing base (0227af6) to head (59e997d).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
internal/utils/decoders/decoders.go 0.00% 69 Missing ⚠️
internal/utils/app_utils.go 0.00% 26 Missing ⚠️
internal/utils/decoders/flags_decoder.go 0.00% 8 Missing ⚠️
cmd/root.go 0.00% 7 Missing ⚠️
internal/service/access_controls_service.go 0.00% 7 Missing ⚠️
internal/utils/decoders/env_decoder.go 0.00% 7 Missing ⚠️
internal/bootstrap/app_bootstrap.go 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #435      +/-   ##
==========================================
- Coverage   23.15%   22.18%   -0.98%     
==========================================
  Files          36       36              
  Lines        2237     2335      +98     
==========================================
  Hits          518      518              
- Misses       1684     1782      +98     
  Partials       35       35              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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)

29-41: Clean refactoring to centralized ACL loading.

The delegation to utils.GetACLsConfig simplifies the initialization logic and enables proper merging of environment and flag-based configurations. The error handling is appropriately preserved.

Consider renaming envACLs to aclConfig or acls since it now contains merged environment and flag-based configurations, not just environment variables:

-envACLs  config.Apps
+aclConfig config.Apps

And update references accordingly (lines 38, 44, 48-49).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0227af6 and 9014be9.

📒 Files selected for processing (7)
  • cmd/root.go (5 hunks)
  • internal/bootstrap/app_bootstrap.go (2 hunks)
  • internal/service/access_controls_service.go (1 hunks)
  • internal/utils/app_utils.go (1 hunks)
  • internal/utils/decoders/decoders.go (2 hunks)
  • internal/utils/decoders/env_decoder.go (1 hunks)
  • internal/utils/decoders/flags_decoder.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
internal/utils/decoders/env_decoder.go (2)
internal/utils/decoders/env_decoder_test.go (1)
  • TestDecodeEnv (11-37)
internal/utils/decoders/flags_decoder_test.go (1)
  • TestDecodeFlags (11-37)
internal/utils/decoders/flags_decoder.go (1)
internal/utils/decoders/flags_decoder_test.go (1)
  • TestDecodeFlags (11-37)
internal/bootstrap/app_bootstrap.go (1)
internal/config/config.go (1)
  • Config (17-44)
internal/utils/app_utils.go (3)
internal/config/config.go (2)
  • Apps (125-127)
  • App (129-136)
internal/utils/decoders/env_decoder.go (1)
  • DecodeACLEnv (21-33)
internal/utils/decoders/flags_decoder.go (1)
  • DecodeACLFlags (24-37)
internal/service/access_controls_service.go (3)
internal/service/docker_service.go (1)
  • DockerService (14-18)
internal/config/config.go (1)
  • Apps (125-127)
internal/utils/app_utils.go (1)
  • GetACLsConfig (212-245)
cmd/root.go (2)
internal/bootstrap/app_bootstrap.go (1)
  • NewBootstrapApp (45-50)
internal/utils/app_utils.go (1)
  • ExtractACLFlags (247-260)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (14)
internal/utils/decoders/flags_decoder.go (1)

24-37: LGTM!

The DecodeACLFlags function correctly mirrors the existing DecodeFlags pattern while using ACL-specific normalization. The implementation properly filters flags, normalizes keys with the hyphen delimiter, and delegates to the parser.

internal/utils/decoders/decoders.go (1)

132-171: LGTM!

The normalizeACLKeys function correctly filters ACL-prefixed keys and uses ParsePath to resolve nested structures. The lowerCamelCase conversion ensures consistent key formatting for the parser.

cmd/root.go (4)

17-29: LGTM!

The addition of the aclFlags field and its initialization in the constructor is clean and follows Go conventions. The explicit viper initialization also improves clarity.


37-39: Enabling unknown flags for dynamic ACL configuration.

Setting UnknownFlags: true allows dynamically-named ACL flags (e.g., --apps-myapp-config-domain) to pass through Cobra's validation. This is appropriate for the use case, though it means typos in standard flags won't be caught by Cobra.


124-124: LGTM!

The ACL flags are properly wired into the bootstrap layer, enabling the configuration to flow through to the service layer.


134-135: LGTM!

ACL flags are extracted at the appropriate time (before command registration) using os.Args, ensuring they're available when the bootstrap process begins.

internal/utils/decoders/env_decoder.go (1)

21-33: LGTM!

The DecodeACLEnv function correctly implements ACL-specific environment variable decoding, using the underscore delimiter appropriate for environment variables.

internal/bootstrap/app_bootstrap.go (3)

39-43: LGTM!

The addition of the aclFlags field to BootstrapApp is clean and follows the existing struct pattern.


45-50: LGTM!

The constructor correctly accepts and initializes the aclFlags parameter, maintaining consistency with the existing initialization pattern.


145-145: LGTM!

The ACL flags are properly wired to the service before initialization, ensuring they're available when Init() processes the configuration.

internal/service/access_controls_service.go (2)

12-23: LGTM!

The addition of the aclFlags field and its initialization in the constructor is clean and consistent with the service design pattern.


25-27: LGTM!

The SetACLFlags setter is straightforward and enables external configuration of ACL flags before initialization.

internal/utils/app_utils.go (2)

212-245: LGTM!

The GetACLsConfig function correctly merges ACL configurations from environment variables and command-line flags, with flags taking precedence. The implementation follows the established pattern from GetOAuthProvidersConfig and includes proper nil checks.


247-260: LGTM!

The ExtractACLFlags function correctly filters ACL-related command-line flags by prefix and extracts key-value pairs. The use of SplitN with limit 2 properly handles values containing "=" characters.

Comment thread internal/utils/decoders/decoders.go
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

♻️ Duplicate comments (1)
internal/utils/decoders/decoders.go (1)

174-188: Filter empty string segments from ParsePath output (past review concern not addressed).

As flagged in the previous review, ParsePath can return empty strings when processing map elements or when input contains empty segments. These empty strings pass through unfiltered and create malformed keys like "tinyauth.acl..field" at line 180-188, which will likely cause parser errors or silent config misapplication.

The check at line 176 only verifies that parsedParts is not completely empty—it doesn't remove empty string elements within the slice.

Apply this diff to filter out empty segments after calling ParsePath:

 		parsedParts := ParsePath(parts[startIdx:], 0, rootType)

 		if len(parsedParts) == 0 {
 			continue
 		}
+
+		// Filter out empty strings to prevent malformed keys
+		filtered := make([]string, 0, len(parsedParts))
+		for _, part := range parsedParts {
+			if part != "" {
+				filtered = append(filtered, part)
+			}
+		}
+
+		if len(filtered) == 0 {
+			continue
+		}

 		final := "tinyauth." + root

-		for _, part := range parsedParts {
+		for _, part := range filtered {
 			if strings.Contains(part, "_") {
 				final += "." + part
 			} else {
 				final += "." + strcase.LowerCamelCase(part)
 			}
 		}

 		normalized[final] = v
🧹 Nitpick comments (2)
internal/utils/decoders/decoders.go (2)

11-19: Remove duplicate bounds check.

The bounds check if idx >= len(parts) is performed twice: once at lines 11-13 (entry point) and again at lines 17-19 (inside the Map handling block). The second check at lines 17-19 is redundant since the first check already guarantees idx < len(parts) when execution reaches line 15.

Apply this diff to remove the duplicate check:

 	if t.Kind() == reflect.Map {
-
-		if idx >= len(parts) {
-			return []string{}
-		}
-
 		elemType := t.Elem()

52-69: Document the two-pass struct field search logic.

The function searches struct fields in two passes: first for Map types (lines 52-60), then for matching field names (lines 62-69). This prioritization means that if a struct has both a Map field and a regular field matching parts[idx], the Map will be preferred. This behavior is non-obvious and could lead to confusion during maintenance.

Add a brief comment before line 52 explaining the search order:

 	if t.Kind() == reflect.Struct {
+		// First pass: check for Map fields that can consume remaining parts
 		for i := 0; i < t.NumField(); i++ {

And before line 62:

+		// Second pass: find direct field name matches
 		for i := 0; i < t.NumField(); i++ {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9014be9 and 59e997d.

📒 Files selected for processing (1)
  • internal/utils/decoders/decoders.go (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (1)
internal/utils/decoders/decoders.go (1)

173-173: Review comment is incorrect—the boundary condition cannot occur.

The original concern assumes startIdx == len(parts) is possible, but the preceding logic makes this impossible:

  • Initial constraint (line 150): len(parts) >= 2 always holds
  • Case 1 (tinyauth, line 161): len(parts) >= 3 is mandatory, and startIdx = 2, so 2 < len(parts) is always true
  • Case 2 (root, line 167): len(parts) >= 2 from initial check, and startIdx = 1, so 1 < len(parts) is always true

The check at line 173 is therefore always true and is defensive programming, not a bug. ParsePath also safely handles empty slices by returning []string{} if idx >= len(parts).

Likely an incorrect or invalid review comment.

Comment on lines +43 to +44
keyParts := parts[idx:keyEndIdx]
keyName := strings.ToLower(strings.Join(keyParts, "_"))
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Oct 29, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Validate input parts to prevent empty string segments.

If parts contains empty strings (e.g., from splitting "apps--field" by "-"), then keyName at line 44 can become an empty string when keyParts contains only empty elements. This empty string propagates into the result array and later causes malformed keys like "tinyauth.acl..field" in normalizeACLKeys.

While the past review suggested filtering in normalizeACLKeys, it's more robust to prevent empty strings at the source.

Consider one of these approaches:

Option 1: Filter empty strings from keyParts before joining:

 	keyParts := parts[idx:keyEndIdx]
+	filtered := make([]string, 0, len(keyParts))
+	for _, part := range keyParts {
+		if part != "" {
+			filtered = append(filtered, part)
+		}
+	}
+	if len(filtered) == 0 {
+		return []string{}
+	}
-	keyName := strings.ToLower(strings.Join(keyParts, "_"))
+	keyName := strings.ToLower(strings.Join(filtered, "_"))

Option 2: Validate at entry that parts contains no empty strings (add after line 11):

 	if idx >= len(parts) {
 		return []string{}
 	}
+	// Skip empty parts
+	for idx < len(parts) && parts[idx] == "" {
+		idx++
+	}
+	if idx >= len(parts) {
+		return []string{}
+	}

Run this script to check if there are existing test cases or usage patterns that might produce empty string segments:


🏁 Script executed:

#!/bin/bash
# Search for Split calls that might produce empty strings
rg -nP --type=go 'strings\.Split.*["\047]-["\047]' 

# Search for how parts are constructed before calling ParsePath
rg -nP --type=go -B5 'ParsePath\(' | grep -E 'Split|parts.*='

Length of output: 79


🏁 Script executed:

#!/bin/bash
# Examine the decoders.go file around lines 43-44
echo "=== Content of decoders.go lines 35-55 ==="
sed -n '35,55p' internal/utils/decoders/decoders.go

# Show the full ParsePath function signature and initial logic
echo -e "\n=== ParsePath function signature and initial lines ==="
sed -n '1,100p' internal/utils/decoders/decoders.go | tail -50

# Find how ParsePath is called
echo -e "\n=== All ParsePath call sites ==="
rg -nP --type=go 'ParsePath\(' -B3 -A1

Length of output: 3425


🏁 Script executed:

#!/bin/bash
# Get the full normalizeKeys function to see how parts array is built
echo "=== normalizeKeys function (lines 88-175) ==="
sed -n '88,175p' internal/utils/decoders/decoders.go

# Also check for any test cases that might show expected behavior
echo -e "\n=== Test files for decoders ==="
find . -name '*decoder*test*.go' -o -name '*test*decoder*.go' | head -10

# Look for test cases that call ParsePath or normalizeKeys
echo -e "\n=== Test content ==="
fd -e 'go' -path '*/test*' | xargs grep -l 'ParsePath\|normalizeKeys' 2>/dev/null | head -5

Length of output: 2080


🏁 Script executed:

#!/bin/bash
# Look at test files to see if edge cases with empty strings or double delimiters are tested
echo "=== Test file list ==="
ls -la internal/utils/decoders/*test.go

echo -e "\n=== Content of env_decoder_test.go ==="
cat internal/utils/decoders/env_decoder_test.go | head -100

echo -e "\n=== Search for test cases with double delimiters or consecutive separators ==="
grep -rn '\-\-\|__' internal/utils/decoders/*test.go

echo -e "\n=== Search for any validation of parts array in ParsePath ==="
sed -n '10,50p' internal/utils/decoders/decoders.go

Length of output: 2657


Filter empty string segments from keyParts before joining at line 44.

The concern is valid. Go's strings.Split() produces empty strings when consecutive delimiters exist (e.g., splitting "tinyauth__apps" by "_" yields ["tinyauth", "", "apps"]). This empty string can propagate into keyParts at line 43, causing strings.Join(keyParts, "_") to produce malformed keys like "_field" or "". The current code has no validation preventing this.

Implement Option 1 from the original suggestion—filter empty strings before joining:

 	keyParts := parts[idx:keyEndIdx]
+	filtered := make([]string, 0, len(keyParts))
+	for _, part := range keyParts {
+		if part != "" {
+			filtered = append(filtered, part)
+		}
+	}
+	if len(filtered) == 0 {
+		return []string{}
+	}
-	keyName := strings.ToLower(strings.Join(keyParts, "_"))
+	keyName := strings.ToLower(strings.Join(filtered, "_"))

This handles the issue at the source where keys are constructed, preventing malformed output downstream.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In internal/utils/decoders/decoders.go around lines 43 to 44, keyParts may
contain empty strings from strings.Split which leads to malformed keys when
joined; filter out any empty string elements from keyParts before calling
strings.Join so the resulting keyName contains only non-empty segments (e.g.,
build a new slice, append only parts with len>0, then join with "_"), ensuring
empty segments are removed at the source before creating keyName.

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.

That's not a bad suggestion @nicotsx.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment thread cmd/root.go
}

app := bootstrap.NewBootstrapApp(conf)
app := bootstrap.NewBootstrapApp(conf, c.aclFlags)
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.

The flags can be parsed directly here avoiding the need to store them in the rootCmd struct. Since we are fetching directly from os.Args, cobra will not touch them so nothing to worry about.

// Create services
dockerService := service.NewDockerService()
aclsService := service.NewAccessControlsService(dockerService)
aclsService.SetACLFlags(app.aclFlags)
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.

Why use a function and not pass them in the initialization function?

Comment on lines +25 to +28
for i := idx + 1; i < len(parts); i++ {
found := false

for j := 0; j < elemType.NumField(); j++ {
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.

These loops can probably be replaced with:

for j := range elemType.NumField() {
  ...
}

Also applies to lines 52 and 62.

Comment on lines +43 to +44
keyParts := parts[idx:keyEndIdx]
keyName := strings.ToLower(strings.Join(keyParts, "_"))
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.

That's not a bad suggestion @nicotsx.

Comment thread internal/utils/decoders/decoders.go
@steveiliop56
Copy link
Copy Markdown
Member

Closing - This logic will be completely removed and Traefik's parser will be used in v5.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants