Skip to content

feat: precomputed scopes#2719

Open
adityathebe wants to merge 4 commits intomainfrom
feat/precomputed-scopes
Open

feat: precomputed scopes#2719
adityathebe wants to merge 4 commits intomainfrom
feat/precomputed-scopes

Conversation

@adityathebe
Copy link
Copy Markdown
Member

@adityathebe adityathebe commented Jan 14, 2026

resolves: #2395
dependsOn: flanksource/duty#1728

Summary by CodeRabbit

  • New Features

    • Background jobs added to materialize scopes and purge deleted scopes on a 24h schedule
    • New materialization workflow and event handlers for scope and permission materialization
  • Bug Fixes / Improvements

    • Improved role selection for RLS-related tokens and more deterministic RLS payload ordering
  • Tests

    • Tests updated to materialize scopes/permissions during setup for more reliable assertions

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 14, 2026

Walkthrough

Adds scope/permission materialization: new event constants, background jobs to materialize/cleanup scopes, an RLS payload refactor to collect scope UUIDs, permission-driven materialization engine, test updates, and a minor dependency/go.mod and server import change.

Changes

Cohort / File(s) Summary
API Event Constants
api/event.go
Added EventScopeMaterialize and EventPermissionMaterialize constants.
RLS Payload Refactor
auth/rls.go
Rewrote payload construction to collect scope UUIDs via a map[uuid.UUID]struct{}; introduced processScopeRefs(ctx, scopeRefs, scopeIDs) and setToSortedUUIDSlice; changed RLS bypass to SET LOCAL ROLE when configured.
Background Jobs
jobs/jobs.go, jobs/rls_scope_jobs.go
Scheduled two new jobs (materializeAllScopesJob, cleanupDeletedScopesJob) and added job implementations and 24h schedule constants.
Permission Events & Processor
permission/events.go, permission/rls_scopes.go
New event handlers for materialize events and a full ScopeProcessor materialization engine (GetProcessScopeJob, ScopeMaterializationStats, apply/remove/rebuild flows, per-table batch updates).
Server Import Registration
cmd/server.go
Added side-effect import of the permission package to register handlers.
Tests & Fixtures
tests/permissions/*, tests/permissions/testdata/scopes/...
Added test helper materializeScopesAndPermissions, updated test assertions to check payload.Scopes, and updated scope test UID.
Permission / DB model tweak
api/v1/permission_types.go, db/permissions.go
PermissionObject.GlobalObject() now returns ("", false) unconditionally; db code simplified to always set ObjectSelector.
Auth token / RLS usage
auth/tokens.go
Retrieve RLS payload earlier and compute DB role (supports DBRoleBypass when RLS disabled).
Dependency
go.mod
Updated github.com/flanksource/duty to a pseudo-version (temporary).

Sequence Diagram

sequenceDiagram
    participant Scheduler as Background Scheduler
    participant Jobs as Job System
    participant Events as Event Handler
    participant Permission as Permission Processor
    participant DB as Database

    Scheduler->>Jobs: schedule materializeAllScopesJob
    Jobs->>DB: query non-deleted scopes
    DB-->>Jobs: scopes list

    loop per scope
        Jobs->>Events: enqueue EventScopeMaterialize(scope_id, rebuild)
        Events->>Permission: GetProcessScopeJob(scope_id, "rebuild")
        Permission->>DB: fetch scope/permission details
        DB-->>Permission: scope targets & selectors
        Permission->>DB: apply __scope updates (batch per table)
        DB-->>Permission: update results
        Permission-->>Events: job result (success/errors)
    end

    Scheduler->>Jobs: schedule cleanupDeletedScopesJob
    Jobs->>DB: query deleted scopes
    DB-->>Jobs: deleted scope list

    loop per deleted scope
        Jobs->>Events: enqueue EventScopeMaterialize(scope_id, remove)
        Events->>Permission: GetProcessScopeJob(scope_id, "remove")
        Permission->>DB: remove scope from tables
        DB-->>Permission: removal confirmation
    end
Loading

Possibly related PRs

Suggested labels

ready

Suggested reviewers

  • moshloop
  • yashmehrotra
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.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 'feat: precomputed scopes' is concise and directly related to the main objective of introducing precomputed scope materialization for row-level security.
Linked Issues check ✅ Passed The PR implements core requirements from issue #2395: introduces Scope resource processing, materializes scope assignments to resources via RLS scope tables, and enables row-level security based on scope membership.
Out of Scope Changes check ✅ Passed All changes directly support scope materialization: new event constants, RLS payload refactoring for scope-centric approach, background jobs for materialization, permission event handling, and test updates.

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

✨ 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 feat/precomputed-scopes

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.

@adityathebe adityathebe marked this pull request as ready for review January 14, 2026 15:18
@adityathebe adityathebe changed the title feat: process materialization events feat: precomputed scopes Jan 14, 2026
@adityathebe adityathebe force-pushed the feat/precomputed-scopes branch from f8347d0 to 0f32be7 Compare January 16, 2026 10:17
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
auth/rls.go (1)

97-104: Build failure: SetPostgresSessionRLSWithRole is undefined on rls.Payload.

The pipeline failures show that rlsPayload.SetPostgresSessionRLSWithRole does not exist on *rls.Payload. This method must be added to the duty package.

Both this and the DBRoleBypass issue indicate the duty dependency PR needs to be merged first, and this PR's go.mod updated to reference the merged version.

🤖 Fix all issues with AI agents
In `@auth/rls.go`:
- Around line 79-94: Add the missing DBRoleBypass field to the PostgrestConfig
in the duty package so the reference
dutyAPI.DefaultConfig.Postgrest.DBRoleBypass compiles: add a DBRoleBypass string
field to the PostgrestConfig struct, ensure DefaultConfig initializes it (and
any config parsing/loading populates it, e.g., from env/YAML), and update any
relevant tests or config docs; this will allow the rls.go code that reads
dutyAPI.DefaultConfig.Postgrest.DBRoleBypass and falls back to DBRole to build
successfully.

In `@auth/tokens.go`:
- Around line 74-82: The code references the non-existent field
config.Postgrest.DBRoleBypass causing build failures; update the role selection
in the block around GetRLSPayload and the local variable role so it does not
reference DBRoleBypass — keep role := config.Postgrest.DBRole and, for now,
remove the rlsPayload.Disable && config.Postgrest.DBRoleBypass != "" branch (or
replace it with a no-op/fallback that leaves role as DBRole), and add a TODO
noting to reintroduce DBRoleBypass once the duty dependency adds that field; use
the symbols GetRLSPayload, rlsPayload, and the local role to locate the change.

In `@permission/rls_scopes.go`:
- Around line 275-276: permission.Action tokens aren't trimmed before matching,
so MatchItems(policy.ActionRead, strings.Split(permission.Action, ",")...) can
miss "read" when permission.Action includes spaces; update the check in the
materialization path to normalize tokens by splitting and trimming each token
(e.g., use strings.Split and strings.TrimSpace or strings.FieldsFunc) before
calling collections.MatchItems with the cleaned slice, keeping the same
semantics for policy.ActionRead and permission.Action.
- Around line 429-433: The update can append duplicate scopeIDs when concurrent
workers race; modify the UpdateColumn call that uses
ctx.DB().Table(table).Where("id IN ?", ids).UpdateColumn("__scope",
gorm.Expr("array_append(COALESCE(__scope, '{}'::uuid[]), ?)", scopeID)) so the
WHERE clause also excludes rows that already contain scopeID (e.g. add a NOT
condition like NOT (? = ANY(__scope)) or equivalent SQL to the Where) so the
update is idempotent; keep the same symbols (ctx.DB(), table, ids, scopeID,
UpdateColumn, gorm.Expr) and ensure the combined WHERE prevents appending if
__scope already includes scopeID.
🧹 Nitpick comments (2)
jobs/rls_scope_jobs.go (1)

18-26: Consider gating RunNow after the migration.
With RunNow: true, every startup will trigger a full rebuild. If this was only for the initial migration, a config/feature flag gate in Start would avoid unnecessary load later.

permission/rls_scopes.go (1)

445-452: Wrap DB errors with ctx.Oops for consistent error codes.

Both functions return raw GORM errors without context. Prefer ctx.Oops() wrapping for consistent error codes and diagnostics. As per coding guidelines, use ctx.Oops().

♻️ Proposed refactor
 	result := ctx.DB().Table(table).
 		Where("id IN ?", ids).
 		Where("NOT (COALESCE(__scope, '{}'::uuid[]) @> ARRAY[?]::uuid[])", scopeID).
 		UpdateColumn("__scope", gorm.Expr("array_append(COALESCE(__scope, '{}'::uuid[]), ?)", scopeID))
 	if result.Error != nil {
-		return 0, result.Error
+		return 0, ctx.Oops().Wrapf(result.Error, "failed to update __scope for %s", table)
 	}
 	return result.RowsAffected, nil
 }
@@
 	result := ctx.DB().Table(table).
 		Where("__scope @> ARRAY[?]::uuid[]", scopeID).
 		UpdateColumn("__scope", gorm.Expr("array_remove(__scope, ?)", scopeID))
 	if result.Error != nil {
-		return 0, result.Error
+		return 0, ctx.Oops().Wrapf(result.Error, "failed to remove __scope for %s", table)
 	}
 	return result.RowsAffected, nil
 }

Also applies to: 469-474

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8347d0 and 0f32be7.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (13)
  • api/event.go
  • api/v1/permission_types.go
  • auth/rls.go
  • auth/tokens.go
  • cmd/server.go
  • db/permissions.go
  • go.mod
  • jobs/jobs.go
  • jobs/rls_scope_jobs.go
  • permission/events.go
  • permission/rls_scopes.go
  • tests/permissions/permissions_test.go
  • tests/permissions/testdata/scopes/multi-target-scope.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
  • cmd/server.go
  • tests/permissions/testdata/scopes/multi-target-scope.yaml
🧰 Additional context used
📓 Path-based instructions (3)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Use context from github.com/flanksource/duty/context.Context to access db (ctx.DB()), properties (ctx.Properties()), and logger (ctx.Logger)
If a file needs to use both context and duty's context, alias context to 'gocontext'
Prefer any over interface{} in Go type declarations
Use ctx.Oops() to craft new errors with error codes from github.com/flanksource/duty/api as tags
Use dutyAPI.WriteError(c, err) in HTTP handlers to return errors with proper HTTP status code mapping based on error codes
For validation errors with no underlying error in HTTP handlers, use dutyAPI.Errorf(dutyAPI.EINVALID, "message")
For wrapping database/internal errors in HTTP handlers, use ctx.Oops().Wrap(err) or ctx.Oops().Wrapf(err, "context")
For permission errors in HTTP handlers, use ctx.Oops().Code(dutyAPI.EFORBIDDEN).Errorf("message")
Use duty.Now() instead of time.Now() for database timestamps and soft deletes
Only add comments if really necessary. Do not add comments that simply explain the code. Exception: comments about functions are considered good practice in Go even if they are self-explanatory

Files:

  • db/permissions.go
  • api/v1/permission_types.go
  • api/event.go
  • auth/tokens.go
  • jobs/rls_scope_jobs.go
  • permission/events.go
  • tests/permissions/permissions_test.go
  • jobs/jobs.go
  • auth/rls.go
  • permission/rls_scopes.go
api/v1/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

CRD definitions must be located in the api/v1 directory

Files:

  • api/v1/permission_types.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*_test.go: Always use ginkgo to run tests. Never run go test directly
Always use github.com/onsi/gomega package for assertions in test files
When using gomega with native go tests, use g := gomega.NewWithT(t) and g.Expect() for assertions

Files:

  • tests/permissions/permissions_test.go
🧠 Learnings (8)
📚 Learning: 2025-11-24T18:35:17.129Z
Learnt from: CR
Repo: flanksource/mission-control PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T18:35:17.129Z
Learning: Applies to **/*.go : Use context from github.com/flanksource/duty/context.Context to access db (ctx.DB()), properties (ctx.Properties()), and logger (ctx.Logger)

Applied to files:

  • api/v1/permission_types.go
  • tests/permissions/permissions_test.go
  • go.mod
  • auth/rls.go
📚 Learning: 2025-11-24T18:35:17.129Z
Learnt from: CR
Repo: flanksource/mission-control PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T18:35:17.129Z
Learning: Applies to **/*.go : Use `ctx.Oops()` to craft new errors with error codes from `github.com/flanksource/duty/api` as tags

Applied to files:

  • api/v1/permission_types.go
  • tests/permissions/permissions_test.go
  • go.mod
  • auth/rls.go
📚 Learning: 2025-11-24T18:35:17.129Z
Learnt from: CR
Repo: flanksource/mission-control PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T18:35:17.129Z
Learning: Migrations are handled by the external package `github.com/flanksource/duty` using Atlas-go

Applied to files:

  • api/v1/permission_types.go
  • tests/permissions/permissions_test.go
  • go.mod
  • auth/rls.go
📚 Learning: 2025-11-24T18:35:17.129Z
Learnt from: CR
Repo: flanksource/mission-control PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T18:35:17.129Z
Learning: Applies to **/*.go : If a file needs to use both context and duty's context, alias context to 'gocontext'

Applied to files:

  • api/v1/permission_types.go
📚 Learning: 2025-11-24T18:35:17.129Z
Learnt from: CR
Repo: flanksource/mission-control PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T18:35:17.129Z
Learning: Applies to **/*.go : Use `duty.Now()` instead of `time.Now()` for database timestamps and soft deletes

Applied to files:

  • api/v1/permission_types.go
  • go.mod
  • auth/rls.go
📚 Learning: 2025-11-24T18:35:17.129Z
Learnt from: CR
Repo: flanksource/mission-control PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T18:35:17.129Z
Learning: Applies to **/*.go : For permission errors in HTTP handlers, use `ctx.Oops().Code(dutyAPI.EFORBIDDEN).Errorf("message")`

Applied to files:

  • api/v1/permission_types.go
  • auth/rls.go
📚 Learning: 2025-11-24T18:35:17.129Z
Learnt from: CR
Repo: flanksource/mission-control PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T18:35:17.129Z
Learning: Applies to api/**/*.go : Domain models and REST API models are located in the api/ directory

Applied to files:

  • tests/permissions/permissions_test.go
  • auth/rls.go
📚 Learning: 2025-11-24T18:35:17.129Z
Learnt from: CR
Repo: flanksource/mission-control PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-24T18:35:17.129Z
Learning: Applies to **/*.go : Use `dutyAPI.WriteError(c, err)` in HTTP handlers to return errors with proper HTTP status code mapping based on error codes

Applied to files:

  • auth/rls.go
🧬 Code graph analysis (4)
auth/tokens.go (1)
auth/rls.go (1)
  • GetRLSPayload (37-63)
permission/events.go (3)
events/event_queue.go (2)
  • Register (36-38)
  • RegisterAsyncHandler (40-48)
api/event.go (3)
  • EventScopeMaterialize (71-71)
  • EventPermissionMaterialize (72-72)
  • Events (164-164)
permission/rls_scopes.go (6)
  • ScopeQueueSourceScope (21-21)
  • ScopeQueueSourcePermission (22-22)
  • ScopeQueueActionRebuild (26-26)
  • ScopeQueueActionApply (24-24)
  • ScopeQueueActionRemove (25-25)
  • GetProcessScopeJob (55-92)
tests/permissions/permissions_test.go (3)
api/v1/permission_types.go (1)
  • Permission (19-25)
api/v1/scope_types.go (1)
  • Scope (77-83)
permission/rls_scopes.go (4)
  • GetProcessScopeJob (55-92)
  • ScopeQueueSourceScope (21-21)
  • ScopeQueueActionRebuild (26-26)
  • ScopeQueueSourcePermission (22-22)
jobs/jobs.go (1)
notification/context.go (1)
  • Context (24-29)
🪛 GitHub Actions: Build
auth/tokens.go

[error] 80-81: Build failed: config.Postgrest.DBRoleBypass is undefined in tokens.go (lines 80-81). Go code references a field that no longer exists in the duty API.

auth/rls.go

[error] 80-98: Build failed: dutyAPI.DefaultConfig.Postgrest.DBRoleBypass is undefined and SetPostgresSessionRLSWithRole is undefined (rls.go:80, rls.go:98). This indicates API type changes in the duty package breaking Go compilation during 'make build'.

🪛 GitHub Actions: CodeQL
auth/rls.go

[error] 80-80: Go compile error: undefined field DBRoleBypass in PostgrestConfig (duty API). The field DBRoleBypass does not exist on type 'github.com/flanksource/duty/api'.PostgrestConfig.

🪛 GitHub Actions: Lint
auth/rls.go

[error] 80-80: auth/rls.go:80:44: dutyAPI.DefaultConfig.Postgrest.DBRoleBypass undefined (type "github.com/flanksource/duty/api'.PostgrestConfig has no field or method DBRoleBypass). GolangCI-Lint: run command failed: golangci-lint run --verbose --max-same-issues=0 --max-issues-per-linter=0

🪛 GitHub Actions: Test
auth/rls.go

[error] 80-80: dutyAPI.DefaultConfig.Postgrest.DBRoleBypass undefined (type "github.com/flanksource/duty/api".PostgrestConfig has no field or method DBRoleBypass)

🪛 GitHub Check: lint
auth/tokens.go

[failure] 81-81:
config.Postgrest.DBRoleBypass undefined (type "github.com/flanksource/duty/api".PostgrestConfig has no field or method DBRoleBypass) (typecheck)


[failure] 80-80:
config.Postgrest.DBRoleBypass undefined (type "github.com/flanksource/duty/api".PostgrestConfig has no field or method DBRoleBypass)

auth/rls.go

[failure] 98-98:
rlsPayload.SetPostgresSessionRLSWithRole undefined (type *"github.com/flanksource/duty/rls".Payload has no field or method SetPostgresSessionRLSWithRole)


[failure] 80-80:
dutyAPI.DefaultConfig.Postgrest.DBRoleBypass undefined (type "github.com/flanksource/duty/api".PostgrestConfig has no field or method DBRoleBypass)

🔇 Additional comments (13)
db/permissions.go (1)

47-51: LGTM - Simplified object selector handling.

The refactored code now unconditionally marshals obj.Spec.Object to JSON, removing the previous global object branch. This aligns with the broader shift to scope-based permissions where global object semantics are handled elsewhere via the Scope CRD.

api/v1/permission_types.go (1)

147-150: LGTM - Appropriate deprecation of GlobalObject.

The method now returns a no-op result, which is consistent with the shift to scope-based permissions. The deprecation comment clearly communicates this change. The corresponding change in db/permissions.go (removing the globalObject branch) aligns with this deprecation.

auth/rls.go (2)

128-188: Scope-ID-centric refactor looks correct.

The refactored buildRLSPayloadFromScopes function:

  1. Collects scope IDs into a map[uuid.UUID]struct{} for deduplication
  2. Uses the permission's own ID as the scope ID when direct resource selectors are present
  3. Builds the final payload from sorted scope IDs for deterministic ordering

One minor observation: Line 131-133 filters by ActionRead after the query already filters by action = policy.ActionRead. This is redundant but harmless since permissions can have comma-separated actions.


213-228: LGTM - Deterministic UUID sorting helper.

The setToSortedUUIDSlice helper correctly:

  • Returns nil for empty sets (avoiding empty slice allocation)
  • Pre-allocates the output slice with correct capacity
  • Sorts by string representation for deterministic ordering

This ensures consistent RLS payloads across requests, which is important for caching and testing.

auth/tokens.go (1)

86-93: LGTM - JWT claims construction.

The JWT claims construction correctly:

  1. Uses the computed role variable based on RLS state
  2. Merges RLS payload claims when available via JWTClaims()
  3. Maintains backward compatibility with existing claim structure

Error handling follows coding guidelines with ctx.Oops().Wrap().

go.mod (1)

15-15: Temporary pseudo-version dependency should be replaced before merge.

This dependency uses a pseudo-version from an unmerged PR branch. Please verify that the referenced PR is merged and update this to a stable release version before this PR can be merged.

jobs/jobs.go (1)

116-124: Scope materialization jobs are wired consistently.
Scheduling with shared FuncScheduler and setting the job context mirrors existing patterns.

api/event.go (1)

70-72: New materialization event names look consistent.
These align with the new materialization handlers.

permission/events.go (1)

17-70: Materialization handler validation is solid.
Defaulting the action, validating IDs/actions, and collecting failed events keeps the handler robust.

jobs/rls_scope_jobs.go (1)

69-117: Cleanup job flow mirrors rebuild logic cleanly.
The deleted-scope sweep and per-scope removal handling are consistent with the rebuild job.

tests/permissions/permissions_test.go (3)

68-69: Materialization before assertions improves determinism.
Calling materializeScopesAndPermissions() in setup makes the tests resilient to asynchronous materialization timing.

Also applies to: 633-635


108-233: Scope-based payload assertions are aligned with the new model.
The shift to validating payload.Scopes and direct-permission IDs matches the precomputed scope approach.


1114-1138: Helper covers scope + permission materialization cleanly.
The helper’s per-scope/per-permission processing matches the new pipeline and keeps test setup centralized.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread auth/rls.go
Comment on lines +79 to +94
return ctx.Transaction(func(txCtx context.Context, _ trace.Span) error {
role := dutyAPI.DefaultConfig.Postgrest.DBRoleBypass
if role == "" {
role = dutyAPI.DefaultConfig.Postgrest.DBRole
if role != "" {
txCtx.Logger.Warnf("RLS bypass role not configured, using role=%s", role)
}
}
if role == "" {
return fmt.Errorf("role is required")
}
if err := txCtx.DB().Exec(fmt.Sprintf("SET LOCAL ROLE %s", pq.QuoteIdentifier(role))).Error; err != nil {
return err
}
return fn(txCtx)
})
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.

⚠️ Potential issue | 🔴 Critical

Build failure: DBRoleBypass is undefined on PostgrestConfig.

The static analysis and pipeline failures confirm that dutyAPI.DefaultConfig.Postgrest.DBRoleBypass does not exist in the current duty version. This field must be added to the duty package before this code will compile.

The logic itself is sound: when RLS is disabled, set a local role for the transaction to ensure proper database access. However, the implementation depends on API that doesn't exist yet in the pinned duty version.

🧰 Tools
🪛 GitHub Actions: CodeQL

[error] 80-80: Go compile error: undefined field DBRoleBypass in PostgrestConfig (duty API). The field DBRoleBypass does not exist on type 'github.com/flanksource/duty/api'.PostgrestConfig.

🪛 GitHub Actions: Lint

[error] 80-80: auth/rls.go:80:44: dutyAPI.DefaultConfig.Postgrest.DBRoleBypass undefined (type "github.com/flanksource/duty/api'.PostgrestConfig has no field or method DBRoleBypass). GolangCI-Lint: run command failed: golangci-lint run --verbose --max-same-issues=0 --max-issues-per-linter=0

🪛 GitHub Actions: Test

[error] 80-80: dutyAPI.DefaultConfig.Postgrest.DBRoleBypass undefined (type "github.com/flanksource/duty/api".PostgrestConfig has no field or method DBRoleBypass)

🪛 GitHub Check: lint

[failure] 80-80:
dutyAPI.DefaultConfig.Postgrest.DBRoleBypass undefined (type "github.com/flanksource/duty/api".PostgrestConfig has no field or method DBRoleBypass)

🤖 Prompt for AI Agents
In `@auth/rls.go` around lines 79 - 94, Add the missing DBRoleBypass field to the
PostgrestConfig in the duty package so the reference
dutyAPI.DefaultConfig.Postgrest.DBRoleBypass compiles: add a DBRoleBypass string
field to the PostgrestConfig struct, ensure DefaultConfig initializes it (and
any config parsing/loading populates it, e.g., from env/YAML), and update any
relevant tests or config docs; this will allow the rls.go code that reads
dutyAPI.DefaultConfig.Postgrest.DBRoleBypass and falls back to DBRole to build
successfully.

Comment thread auth/tokens.go
Comment on lines +74 to +82
rlsPayload, err := GetRLSPayload(ctx.WithUser(user))
if err != nil {
return "", ctx.Oops().Wrap(err)
}

role := config.Postgrest.DBRole
if rlsPayload.Disable && config.Postgrest.DBRoleBypass != "" {
role = config.Postgrest.DBRoleBypass
}
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.

⚠️ Potential issue | 🔴 Critical

Build failure: DBRoleBypass is undefined on PostgrestConfig.

The pipeline failures confirm that config.Postgrest.DBRoleBypass does not exist in the current duty API.

The refactored logic is correct:

  1. Fetch RLS payload upfront to determine the appropriate role
  2. Use the bypass role when RLS is disabled and a bypass role is configured
  3. Fall back to the standard DBRole otherwise

This aligns with the changes in auth/rls.go. Once the duty dependency is updated with the DBRoleBypass field, this will work correctly.

🧰 Tools
🪛 GitHub Actions: Build

[error] 80-81: Build failed: config.Postgrest.DBRoleBypass is undefined in tokens.go (lines 80-81). Go code references a field that no longer exists in the duty API.

🪛 GitHub Check: lint

[failure] 81-81:
config.Postgrest.DBRoleBypass undefined (type "github.com/flanksource/duty/api".PostgrestConfig has no field or method DBRoleBypass) (typecheck)


[failure] 80-80:
config.Postgrest.DBRoleBypass undefined (type "github.com/flanksource/duty/api".PostgrestConfig has no field or method DBRoleBypass)

🤖 Prompt for AI Agents
In `@auth/tokens.go` around lines 74 - 82, The code references the non-existent
field config.Postgrest.DBRoleBypass causing build failures; update the role
selection in the block around GetRLSPayload and the local variable role so it
does not reference DBRoleBypass — keep role := config.Postgrest.DBRole and, for
now, remove the rlsPayload.Disable && config.Postgrest.DBRoleBypass != "" branch
(or replace it with a no-op/fallback that leaves role as DBRole), and add a TODO
noting to reintroduce DBRoleBypass once the duty dependency adds that field; use
the symbols GetRLSPayload, rlsPayload, and the local role to locate the change.

Comment thread permission/rls_scopes.go
Comment on lines +275 to +276
if !collections.MatchItems(policy.ActionRead, strings.Split(permission.Action, ",")...) {
return nil
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.

⚠️ Potential issue | 🟠 Major

Normalize permission actions before the read check.

If permission.Action contains spaces (e.g., "read, write"), MatchItems can miss read, skipping materialization. Trim tokens first to avoid false negatives.

🛠️ Proposed fix
-	if !collections.MatchItems(policy.ActionRead, strings.Split(permission.Action, ",")...) {
+	actions := strings.Split(permission.Action, ",")
+	for i, a := range actions {
+		actions[i] = strings.TrimSpace(a)
+	}
+	if !collections.MatchItems(policy.ActionRead, actions...) {
 		return nil
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if !collections.MatchItems(policy.ActionRead, strings.Split(permission.Action, ",")...) {
return nil
actions := strings.Split(permission.Action, ",")
for i, a := range actions {
actions[i] = strings.TrimSpace(a)
}
if !collections.MatchItems(policy.ActionRead, actions...) {
return nil
}
🤖 Prompt for AI Agents
In `@permission/rls_scopes.go` around lines 275 - 276, permission.Action tokens
aren't trimmed before matching, so MatchItems(policy.ActionRead,
strings.Split(permission.Action, ",")...) can miss "read" when permission.Action
includes spaces; update the check in the materialization path to normalize
tokens by splitting and trimming each token (e.g., use strings.Split and
strings.TrimSpace or strings.FieldsFunc) before calling collections.MatchItems
with the cleaned slice, keeping the same semantics for policy.ActionRead and
permission.Action.

Comment thread permission/rls_scopes.go
Comment on lines +429 to +433
if err := ctx.DB().Table(table).
Where("id IN ?", ids).
UpdateColumn("__scope", gorm.Expr("array_append(COALESCE(__scope, '{}'::uuid[]), ?)", scopeID)).Error; err != nil {
return 0, ctx.Oops().Wrapf(err, "failed to update __scope for %s", table)
}
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.

⚠️ Potential issue | 🟠 Major

Guard against duplicate __scope entries on concurrent runs.

The select/update split can append duplicates if another worker updates between the two. Add the same NOT condition to the update query for idempotency.

🛠️ Proposed fix
-		if err := ctx.DB().Table(table).
-			Where("id IN ?", ids).
-			UpdateColumn("__scope", gorm.Expr("array_append(COALESCE(__scope, '{}'::uuid[]), ?)", scopeID)).Error; err != nil {
+		if err := ctx.DB().Table(table).
+			Where("id IN ?", ids).
+			Where("NOT (COALESCE(__scope, '{}'::uuid[]) @> ARRAY[?]::uuid[])", scopeID).
+			UpdateColumn("__scope", gorm.Expr("array_append(COALESCE(__scope, '{}'::uuid[]), ?)", scopeID)).Error; err != nil {
 			return 0, ctx.Oops().Wrapf(err, "failed to update __scope for %s", table)
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := ctx.DB().Table(table).
Where("id IN ?", ids).
UpdateColumn("__scope", gorm.Expr("array_append(COALESCE(__scope, '{}'::uuid[]), ?)", scopeID)).Error; err != nil {
return 0, ctx.Oops().Wrapf(err, "failed to update __scope for %s", table)
}
if err := ctx.DB().Table(table).
Where("id IN ?", ids).
Where("NOT (COALESCE(__scope, '{}'::uuid[]) @> ARRAY[?]::uuid[])", scopeID).
UpdateColumn("__scope", gorm.Expr("array_append(COALESCE(__scope, '{}'::uuid[]), ?)", scopeID)).Error; err != nil {
return 0, ctx.Oops().Wrapf(err, "failed to update __scope for %s", table)
}
🤖 Prompt for AI Agents
In `@permission/rls_scopes.go` around lines 429 - 433, The update can append
duplicate scopeIDs when concurrent workers race; modify the UpdateColumn call
that uses ctx.DB().Table(table).Where("id IN ?", ids).UpdateColumn("__scope",
gorm.Expr("array_append(COALESCE(__scope, '{}'::uuid[]), ?)", scopeID)) so the
WHERE clause also excludes rows that already contain scopeID (e.g. add a NOT
condition like NOT (? = ANY(__scope)) or equivalent SQL to the Where) so the
update is idempotent; keep the same symbols (ctx.DB(), table, ids, scopeID,
UpdateColumn, gorm.Expr) and ensure the combined WHERE prevents appending if
__scope already includes scopeID.

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.

Scope on Resources

1 participant