Skip to content

Conversation

@adityathebe
Copy link
Member

@adityathebe adityathebe commented Jan 14, 2026

related: flanksource/mission-control#2395

Summary by CodeRabbit

Release Notes

  • New Features

    • Added scope-based row-level security with precomputed scope support.
    • Introduced configurable database role settings for authentication and admin bypass.
  • Database Schema

    • Added scope tracking columns and indexes across canaries, checks, components, configs, playbooks, and views tables.
  • Improvements

    • Enhanced access control with wildcard scope support in RLS policies.
    • Added feature flag for enabling precomputed RLS mode.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 14, 2026

Walkthrough

This PR introduces a scope-based RLS system with UUID-scoped tracking, precomputed mode support, and event-driven scope materialization. It adds __scope columns to multiple tables, implements trigger-based materialization events, enhances role management with bypass capabilities, and enables feature-flagged precomputed RLS policies via SQL migrations.

Changes

Cohort / File(s) Summary
Documentation
AGENTS.md
Updated RLS documentation to reflect new scope-based default policies, expanded examples with views, precomputed mode behavior, and payload composition using scope UUIDs with wildcard support.
Build & CI
Makefile, .github/workflows/benchmark.yml
Added Makefile pre-test cleanup for git directories and test checkout; removed bench_test.go copy step from benchmark workflow.
RLS Core Logic
rls/payload.go
Changed Payload.Scopes from []string to []uuid.UUID; added new public methods SetPostgresSessionRLSWithRole() and SetGlobalPostgresSessionRLSWithRole() with dynamic role parameter support; introduced role validation.
Database Schema
schema/checks.hcl, schema/components.hcl, schema/config.hcl, schema/playbooks.hcl, schema/views.hcl
Added nullable __scope column (type uuid[]) and GIN index (\*__scope_gin_idx) to each table for scope-based access control.
RLS Utilities & Events
views/035_rls_utils.sql, views/043_scope_permission_events.sql
Added rls_scope_access() function to extract scopes from JWT claims; introduced trigger functions and triggers (insert_scope_materialization_event(), insert_permission_materialization_event()) for per-row scope and permission materialization events with upsert semantics.
Benchmark Tests
bench/bench_test.go, bench/utils_test.go
Updated payload field from Config to Scopes with uuid.UUID type; added scope materialization logic in setupConfigsForSize() to populate __scope columns.
Type System
types/resource_selector.go
Removed early wildcard short-circuit in ResourceSelector.Matches(), requiring full PEG evaluation for all matching cases.
Configuration & Database Setup
api/config.go, db.go, migrate/migrate.go
Added DBRole and DBRoleBypass fields to PostgrestConfig with defaults; implemented safe identifier quoting via pq.QuoteIdentifier for role names in ALTER and CREATE statements; added bypass role creation and privilege grants with BYPASSRLS.
RLS Migrations & Feature Flag
views/9998_rls_enable_precomputed.sql, views/views.go
Created comprehensive RLS enable migration with policies for 12+ tables and security_invoker settings on views; added feature flag check in views.go to conditionally activate precomputed RLS mode.
Tests
tests/rls_test.go
Minor formatting adjustment to variable declaration alignment.

Possibly related PRs

  • #1646: Overlapping documentation updates to AGENTS.md regarding PostgREST JWT claims injection and RLS payload/flow semantics.
  • #1642: Related RLS scope handling changes in Payload.Scopes field type and rls utilities modifications.
  • #1729: Connected benchmark CI changes; this PR removes the bench_test.go copy step introduced in that PR.

Suggested reviewers

  • moshloop
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 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' accurately summarizes the main change, which involves implementing precomputed scopes for row-level security across multiple database tables and Go RLS payload handling.

✏️ 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 draft January 14, 2026 08:37
@adityathebe adityathebe changed the title feat/precomputed scopes feat: precomputed scopes Jan 14, 2026
@adityathebe adityathebe force-pushed the feat/precomputed-scopes branch from a4519c6 to 43b8b6d Compare January 14, 2026 08:54
@adityathebe adityathebe marked this pull request as ready for review January 14, 2026 09:44
Copy link
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

🤖 Fix all issues with AI agents
In `@tests/rls_test.go`:
- Around line 69-112: Add parallel RLS tests for components, canaries,
playbooks, and views mirroring the existing config_items/config_changes cases:
use rls.Payload with Scopes, multiple Scopes (OR), WildcardScopes, and an
unknown scope and call payload.SetPostgresSessionRLS(tx) before counting rows
via tx.Model(&models.Component{}), tx.Model(&models.Canary{}),
tx.Model(&models.Playbook{}), tx.Model(&models.View{}) (and
tx.Table("component_changes") style where inheritance applies) to assert counts
(e.g., awsComponents/totalComponents/awsOrGcpComponents/etc.); ensure you create
appropriate expected count variables (aws*, total*, awsOrGcp*, unknown->0) and
mirror the exact test structure and assertions used in the existing tests for
config_items and config_changes.
🧹 Nitpick comments (3)
tests/rls_test.go (2)

35-61: Consider using Succeed() matcher for error assertions.

The test setup correctly initializes scopes, runs RLS migrations, and prepares the transaction context. However, using To(BeNil()) for error assertions is less idiomatic than To(Succeed()).

♻️ Suggested improvement for error assertions
-		Expect(DefaultContext.DB().Model(&models.ConfigItem{}).Count(&totalConfigs).Error).To(BeNil())
+		Expect(DefaultContext.DB().Model(&models.ConfigItem{}).Count(&totalConfigs).Error).To(Succeed())

Apply similar changes throughout the file for consistency.


63-67: Minor: Consider Rollback() instead of Commit() to preserve test isolation.

Using Commit() in AfterAll persists the __scope updates made during test setup. If the test database is reused, this could affect other test suites. Rolling back would ensure clean state.

♻️ Suggested change
 	AfterAll(func() {
 		if tx != nil {
-			Expect(tx.Commit().Error).To(BeNil())
+			Expect(tx.Rollback().Error).To(BeNil())
 		}
 	})

Note: This only matters if the test database persists across test runs. If the database is reset between runs, Commit() is fine.

rls/payload.go (1)

60-92: Redundant final sort of parts slice.

The slices.Sort(parts) on line 90 is unnecessary since parts is appended in a deterministic order (Scopes first, then WildcardScopes). The individual components are already sorted, so the final sort doesn't change the outcome.

♻️ Suggested simplification
 	if len(parts) == 0 {
 		t.fingerprint = "empty"
 		return
 	}

-	slices.Sort(parts)
 	t.fingerprint = hash.Sha256Hex(strings.Join(parts, " | "))
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4519c6 and 43b8b6d.

📒 Files selected for processing (16)
  • AGENTS.md
  • Makefile
  • bench/bench_test.go
  • bench/utils_test.go
  • rls/payload.go
  • rls/payload_test.go
  • schema/checks.hcl
  • schema/components.hcl
  • schema/config.hcl
  • schema/playbooks.hcl
  • schema/views.hcl
  • tests/rls_test.go
  • views/035_rls_utils.sql
  • views/035_view_rls.sql
  • views/043_scope_permission_events.sql
  • views/9998_rls_enable.sql
🚧 Files skipped from review as they are similar to previous changes (6)
  • schema/config.hcl
  • Makefile
  • AGENTS.md
  • schema/components.hcl
  • schema/playbooks.hcl
  • views/035_rls_utils.sql
🧰 Additional context used
📓 Path-based instructions (2)
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*_test.go: Always use github.com/onsi/gomega package for assertions in test files
When using gomega with native go tests, use the gomega.NewWithT(t) approach for assertions

Files:

  • bench/utils_test.go
  • rls/payload_test.go
  • bench/bench_test.go
  • tests/rls_test.go
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Only add comments if really necessary; do not add comments that simply explain the code
Comments about functions are considered good practice in Go even if they are self-explanatory

Files:

  • bench/utils_test.go
  • rls/payload_test.go
  • rls/payload.go
  • bench/bench_test.go
  • tests/rls_test.go
🧠 Learnings (13)
📓 Common learnings
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/**/*.sql : Tables with direct RLS policies must use the `match_scope()` function to evaluate JWT claims against row attributes (tags, agents, names, id)
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to rls/payload.go : Build RLS Payload with scopes in `rls/payload.go` and use `SetPostgresSessionRLS()` to serialize the Payload to JSON
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/**/*.sql : RLS policies must read JWT claims from PostgreSQL session variables via `request.jwt.claims` using `current_setting('request.jwt.claims')::jsonb`
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Use PostgreSQL Row-Level Security (RLS) to enforce multi-tenant access control with JWT claims passed via PostgREST
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/9998_rls_enable.sql : Add RLS enable logic to `views/9998_rls_enable.sql` including enable RLS on the table and create the policy

Applied to files:

  • views/9998_rls_enable.sql
  • views/043_scope_permission_events.sql
  • views/035_view_rls.sql
  • schema/views.hcl
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/**/*.sql : Tables with direct RLS policies must use the `match_scope()` function to evaluate JWT claims against row attributes (tags, agents, names, id)

Applied to files:

  • views/9998_rls_enable.sql
  • views/043_scope_permission_events.sql
  • views/035_view_rls.sql
  • tests/rls_test.go
  • schema/views.hcl
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/9999_rls_disable.sql : Add RLS disable logic to `views/9999_rls_disable.sql` including disable RLS on the table and drop the policy

Applied to files:

  • views/9998_rls_enable.sql
  • views/043_scope_permission_events.sql
  • views/035_view_rls.sql
  • schema/views.hcl
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/**/*.sql : Child tables must inherit access control from their parent using `EXISTS` clauses in RLS policies

Applied to files:

  • views/9998_rls_enable.sql
  • views/035_view_rls.sql
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/**/*.sql : RLS policies must read JWT claims from PostgreSQL session variables via `request.jwt.claims` using `current_setting('request.jwt.claims')::jsonb`

Applied to files:

  • views/9998_rls_enable.sql
  • views/035_view_rls.sql
  • tests/rls_test.go
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to rls/payload.go : Build RLS Payload with scopes in `rls/payload.go` and use `SetPostgresSessionRLS()` to serialize the Payload to JSON

Applied to files:

  • views/9998_rls_enable.sql
  • rls/payload_test.go
  • rls/payload.go
  • bench/bench_test.go
  • tests/rls_test.go
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to tests/rls_test.go : Add comprehensive RLS test cases to `tests/rls_test.go` covering access granted scenarios, access denied scenarios, and edge cases

Applied to files:

  • views/9998_rls_enable.sql
  • rls/payload.go
  • bench/bench_test.go
  • tests/rls_test.go
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Use PostgreSQL Row-Level Security (RLS) to enforce multi-tenant access control with JWT claims passed via PostgREST

Applied to files:

  • views/9998_rls_enable.sql
  • views/035_view_rls.sql
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to **/*_test.go : Always use `github.com/onsi/gomega` package for assertions in test files

Applied to files:

  • rls/payload_test.go
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to **/*_test.go : When using gomega with native go tests, use the `gomega.NewWithT(t)` approach for assertions

Applied to files:

  • rls/payload_test.go
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to tests/**/*_test.go : Use resources from the dummy dataset in tests/fixtures/dummy/all.go in tests before creating new test resources

Applied to files:

  • rls/payload_test.go
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Always use ginkgo to run tests, never run `go test` directly

Applied to files:

  • rls/payload_test.go
🧬 Code graph analysis (2)
rls/payload_test.go (1)
rls/payload.go (6)
  • WildcardResourceScope (15-15)
  • WildcardResourceScopePlaybook (21-21)
  • WildcardResourceScopeConfig (18-18)
  • WildcardResourceScopeView (22-22)
  • Payload (26-39)
  • WildcardResourceScopeCanary (20-20)
bench/bench_test.go (1)
rls/payload.go (1)
  • Payload (26-39)
🔇 Additional comments (16)
views/035_view_rls.sql (1)

11-27: LGTM! Clean refactor to centralized scope utilities.

The function correctly:

  1. Short-circuits with wildcard access check for efficiency
  2. Denies access for NULL/empty grants
  3. Uses array overlap with rls_scope_access() for scope matching

The change from STABLE to VOLATILE is appropriate since rls_scope_access() reads session variables that can change within a transaction.

schema/views.hcl (1)

29-32: LGTM! Consistent with the PR's scope architecture.

The __scope column and GIN index follow the same pattern applied to other resource tables (config_items, components, canaries, playbooks). The GIN index is the correct choice for efficient array overlap (&&) queries used in RLS policies.

Also applies to: 68-71

views/9998_rls_enable.sql (2)

57-65: Consistent and secure RLS policy pattern across resources.

The unified pattern rls_has_wildcard('<resource>') OR (COALESCE(table.__scope, '{}'::uuid[]) && rls_scope_access()) is correctly applied. The COALESCE with empty array ensures rows with NULL __scope are hidden by default (secure-by-default behavior).

Also applies to: 133-141, 146-154, 159-167, 221-229


70-81: Child table RLS inheritance correctly maintained.

The EXISTS pattern for child tables (config_changes, config_analysis, checks, view_panels, etc.) properly delegates access control to their parent tables, ensuring the new scope-based RLS automatically propagates.

Also applies to: 205-216, 234-244

schema/checks.hcl (1)

25-28: LGTM! Consistent scope column and index for canaries.

The __scope column and GIN index follow the established pattern. This enables the canaries_auth RLS policy to efficiently evaluate scope overlap queries.

Also applies to: 91-94

rls/payload.go (3)

15-23: LGTM! Well-defined wildcard scope constants.

The WildcardResourceScope type and constants provide clear, type-safe identifiers for resource types that support wildcard access. This aligns with the RLS payload design documented in the learnings.


26-39: Clean payload structure with UUID-based scopes.

The consolidated Payload struct with Scopes []uuid.UUID and WildcardScopes []WildcardResourceScope simplifies the previous per-resource scope model. The comments clearly explain the wildcard scope behavior and why they're not materialized into table rows.


102-135: LGTM! Secure RLS session setup.

The use of pq.QuoteLiteral for the RLS JSON payload properly prevents SQL injection. The local vs global scope distinction is handled correctly.

rls/payload_test.go (3)

10-47: LGTM! Well-structured test cases following guidelines.

Tests correctly use gomega.NewWithT(t) for assertions as per coding guidelines. Coverage includes disabled state, deterministic fingerprint computation, and empty scope handling.


49-72: Good test for deterministic sorting.

This test verifies that different ordering of Scopes and WildcardScopes produces the same fingerprint, which is essential for caching correctness.


74-87: LGTM! Cache behavior test is correct.

The test properly verifies that the fingerprint is cached after EvalFingerprint() and doesn't change when the underlying Scopes slice is mutated. This confirms the expected caching behavior.

bench/utils_test.go (2)

32-33: Consider test isolation for benchScopeIDs.

Using a package-level variable for benchScopeIDs is acceptable for benchmark setup but couples setupConfigsForSize to the global state. This is fine for benchmarks but worth noting.


124-141: LGTM! Scope materialization logic is correct.

The SQL update correctly:

  1. Handles NULL __scope columns with COALESCE
  2. Uses JSONB containment (@>) for tag matching
  3. Prevents duplicate scope UUIDs with the NOT (... @> ARRAY[?]) check
bench/bench_test.go (1)

138-161: LGTM! Benchmark updated to use UUID-based scopes.

The benchmark correctly uses the new Payload.Scopes field with benchScopeIDs populated during setup. The modulo cycling through scope IDs provides varied test coverage.

views/043_scope_permission_events.sql (2)

35-64: No changes needed for DELETE operations.

Hard deletes are not performed on the scopes or permissions tables in this codebase. The application uses soft-delete exclusively via the deleted_at column, which is already handled by the trigger's UPDATE logic. When a row is soft-deleted (deleted_at transitions from NULL to NOT NULL), the trigger emits a 'remove' action as intended.

Likely an incorrect or invalid review comment.


1-34: LGTM! Scope materialization trigger is well-designed.

The trigger correctly emits events for INSERT and UPDATE operations, with appropriate action determination for soft deletes. The upsert pattern with conflict resolution on (name, properties) ensures idempotent event creation. The unique index on event_queue(name, properties) is properly configured in the schema.

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

@github-actions
Copy link

github-actions bot commented Jan 14, 2026

Benchstat

Base: 5e75b26b3adf33643e72436fca3a33c0a3c07d91
Head: 56ec56d6da7eafe7c78c0be57d719dbb51051e1b

goos: linux
goarch: amd64
pkg: github.com/flanksource/duty/bench
cpu: AMD EPYC 7763 64-Core Processor                
                                                │ bench-base.txt │            bench-head.txt             │
                                                │     sec/op     │    sec/op     vs base                 │
Main/Sample-15000/catalog_changes/Without_RLS-4      5.108m ± 0%    5.547m ± 4%   +8.61% (p=0.002 n=6)
Main/Sample-15000/catalog_changes/With_RLS-4         128.3m ± 1%
Main/Sample-15000/config_changes/Without_RLS-4       5.085m ± 1%    5.215m ± 6%   +2.57% (p=0.002 n=6)
Main/Sample-15000/config_changes/With_RLS-4          127.5m ± 0%
Main/Sample-15000/config_detail/Without_RLS-4        3.824m ± 1%    4.108m ± 1%   +7.43% (p=0.002 n=6)
Main/Sample-15000/config_detail/With_RLS-4           124.0m ± 1%
Main/Sample-15000/config_names/Without_RLS-4         12.58m ± 2%    13.42m ± 1%   +6.71% (p=0.002 n=6)
Main/Sample-15000/config_names/With_RLS-4            125.0m ± 1%
Main/Sample-15000/config_summary/Without_RLS-4       60.47m ± 1%   116.71m ± 2%  +93.01% (p=0.002 n=6)
Main/Sample-15000/config_summary/With_RLS-4          733.8m ± 1%
Main/Sample-15000/configs/Without_RLS-4              7.047m ± 1%    7.459m ± 1%   +5.84% (p=0.002 n=6)
Main/Sample-15000/configs/With_RLS-4                 124.5m ± 1%
Main/Sample-15000/analysis_types/Without_RLS-4       3.778m ± 1%    3.859m ± 0%   +2.12% (p=0.002 n=6)
Main/Sample-15000/analysis_types/With_RLS-4          3.823m ± 3%    3.851m ± 0%        ~ (p=0.065 n=6)
Main/Sample-15000/analyzer_types/Without_RLS-4       3.670m ± 1%    3.698m ± 1%   +0.74% (p=0.002 n=6)
Main/Sample-15000/analyzer_types/With_RLS-4          3.769m ± 4%    3.723m ± 0%   -1.23% (p=0.015 n=6)
Main/Sample-15000/change_types/Without_RLS-4         5.398m ± 4%    5.244m ± 3%        ~ (p=0.180 n=6)
Main/Sample-15000/change_types/With_RLS-4            5.225m ± 1%    5.121m ± 6%        ~ (p=0.240 n=6)
Main/Sample-15000/config_classes/Without_RLS-4       3.285m ± 1%    3.413m ± 1%   +3.88% (p=0.002 n=6)
Main/Sample-15000/config_classes/With_RLS-4          124.7m ± 1%
Main/Sample-15000/config_types/Without_RLS-4         3.909m ± 0%    3.982m ± 1%   +1.86% (p=0.002 n=6)
Main/Sample-15000/config_types/With_RLS-4            125.0m ± 2%
geomean                                              19.02m         6.121m        +7.32%               ¹
¹ benchmark set differs from baseline; geomeans may not be comparable

@adityathebe adityathebe force-pushed the feat/precomputed-scopes branch from e677f4f to f5312b3 Compare January 14, 2026 15:56
@adityathebe
Copy link
Member Author


› EXPLAIN (ANALYZE, BUFFERS)SELECT DISTINCT type FROM config_items
  +---------------------------------------------------------------------------------------------------------------------+
  | QUERY PLAN                                                                                                          |
  |---------------------------------------------------------------------------------------------------------------------|
  | HashAggregate  (cost=3410.78..3411.97 rows=119 width=23) (actual time=44.434..44.434 rows=0 loops=1)                |
  |   Group Key: type                                                                                                   |
  |   Batches: 1  Memory Usage: 40kB                                                                                    |
  |   Buffers: shared hit=1003                                                                                          |
  |   ->  Seq Scan on config_items  (cost=0.00..3406.40 rows=1751 width=23) (actual time=44.430..44.430 rows=0 loops=1) |
  |         Filter: (rls_has_wildcard('config'::text) OR (__scope && rls_scope_access()) OR is_rls_disabled())          |
  |         Rows Removed by Filter: 3152                                                                                |
  |         Buffers: shared hit=1003                                                                                    |
  | Planning Time: 0.300 ms                                                                                             |
  | Execution Time: 44.457 ms                                                                                           |
  +---------------------------------------------------------------------------------------------------------------------+

@adityathebe adityathebe force-pushed the feat/precomputed-scopes branch from 1a3b5d2 to c7bc2c5 Compare January 15, 2026 20:00
Copy link
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: 3

Caution

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

⚠️ Outside diff range comments (2)
bench/utils_test.go (1)

160-164: Misleading error message in the non-RLS branch.

The error message says "failed to enable rls" but this branch runs migrations without RLS enabled.

Suggested fix
 	} else {
 		if err := duty.Migrate(duty.RunMigrations(api.NewConfig(connUrl))); err != nil {
-			b.Fatalf("failed to enable rls: %v", err)
+			b.Fatalf("failed to run migrations: %v", err)
 		}
 	}
rls/payload.go (1)

86-102: Potential SQL injection risk in role parameter.

The role parameter is inserted directly into the SQL query using fmt.Sprintf without sanitization. While the current callers use hardcoded "postgrest_api", the public methods SetPostgresSessionRLSWithRole and SetGlobalPostgresSessionRLSWithRole accept arbitrary role strings.

If a caller passes an untrusted role value, this could lead to SQL injection.

🔒 Recommended fix: Sanitize the role parameter
+import "github.com/lib/pq"
+
 func (t Payload) setPostgresSessionRLS(db *gorm.DB, local bool, role string) error {
 	if role == "" {
 		return fmt.Errorf("role is required")
 	}
+
+	// Validate role is a valid PostgreSQL identifier
+	// PostgreSQL role names must be valid identifiers (alphanumeric + underscore)
+	for _, r := range role {
+		if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_') {
+			return fmt.Errorf("invalid role name: %s", role)
+		}
+	}
+
 	rlsJSON, err := json.Marshal(t)
 	if err != nil {
 		return fmt.Errorf("failed to marshall to json: %w", err)
 	}
 	// ... rest of function
-	if err := db.Exec(fmt.Sprintf("SET %s ROLE %s", scope, role)).Error; err != nil {
+	if err := db.Exec(fmt.Sprintf("SET %s ROLE %s", scope, pq.QuoteIdentifier(role))).Error; err != nil {
 		return fmt.Errorf("failed to set role: %w", err)
 	}

Alternatively, use pq.QuoteIdentifier(role) for the role name to ensure proper escaping.

🤖 Fix all issues with AI agents
In `@api/config.go`:
- Line 15: The default DB role name DBRoleBypass currently uses a hyphen
("rls-bypasser") which breaks PostgreSQL identifier syntax; change the default
to use an underscore (e.g., "rls_bypasser") in api/config.go and update any
places that rely on DBRoleBypass accordingly (search for DBRoleBypass, db.go,
and migrate/migrate.go) so SQL identifiers no longer require quoting; if you
prefer to keep the hyphen, instead ensure all SQL generation code in db.go and
migrate/migrate.go always double-quote the identifier where it's interpolated.

In `@db.go`:
- Around line 262-266: The ALTER ROLE SQL interpolates role names without
quoting (e.g. config.Postgrest.DBRoleBypass) which breaks on hyphens; update the
Raw(...) calls that build ALTER ROLE for DBRole, DBRoleBypass and AnonDBRole to
quote the identifier with double quotes and escape any embedded double quotes in
the role value before formatting (or use a proper identifier-quoting helper), so
the SQL becomes ALTER ROLE "role-name" ... instead of unquoted text; locate the
ctx.DB().Raw(fmt.Sprintf(...)) calls that construct ALTER ROLE and apply the
quoting/escaping to each role variable.

In `@migrate/migrate.go`:
- Around line 274-284: The SQL uses unquoted role identifiers which break on
names with hyphens (config.Postgrest.DBRoleBypass); update createRole (the
function that issues CREATE ROLE and GRANT statements) to quote/escape the role
identifier in every SQL string it builds, and also change the pool.Exec call
that runs fmt.Sprintf("ALTER ROLE %s BYPASSRLS", config.Postgrest.DBRoleBypass)
to quote/escape the role name before insertion so the ALTER ROLE ... BYPASSRLS
statement uses a properly quoted identifier; ensure all SQL templates in
createRole and the ALTER ROLE invocation consistently wrap the role name as an
SQL identifier.
🧹 Nitpick comments (1)
tests/rls_test.go (1)

24-26: The setRLS helper swallows the error type check.

Using BeNil() is correct, but the function name setRLS doesn't indicate it will fail the test on error. Consider renaming or documenting this side effect.

♻️ Optional: Make the assertion clearer
-func setRLS(tx *gorm.DB, payload rls.Payload) {
-	Expect(payload.SetPostgresSessionRLS(tx)).To(BeNil())
-}
+func setRLS(tx *gorm.DB, payload rls.Payload) {
+	Expect(payload.SetPostgresSessionRLS(tx)).To(Succeed())
+}

Using Succeed() is more idiomatic for error checks in Gomega.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a3b5d2 and c7bc2c5.

📒 Files selected for processing (21)
  • .github/workflows/benchmark.yml
  • AGENTS.md
  • Makefile
  • api/config.go
  • bench/bench_test.go
  • bench/utils_test.go
  • db.go
  • migrate/migrate.go
  • rls/payload.go
  • rls/payload_test.go
  • schema/checks.hcl
  • schema/components.hcl
  • schema/config.hcl
  • schema/playbooks.hcl
  • schema/views.hcl
  • tests/rls_test.go
  • types/resource_selector.go
  • views/035_rls_utils.sql
  • views/035_view_rls.sql
  • views/043_scope_permission_events.sql
  • views/9998_rls_enable.sql
💤 Files with no reviewable changes (2)
  • types/resource_selector.go
  • .github/workflows/benchmark.yml
🚧 Files skipped from review as they are similar to previous changes (5)
  • schema/checks.hcl
  • AGENTS.md
  • schema/components.hcl
  • schema/playbooks.hcl
  • views/043_scope_permission_events.sql
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Only add comments if really necessary; do not add comments that simply explain the code
Comments about functions are considered good practice in Go even if they are self-explanatory

Files:

  • db.go
  • migrate/migrate.go
  • rls/payload_test.go
  • bench/utils_test.go
  • tests/rls_test.go
  • bench/bench_test.go
  • rls/payload.go
  • api/config.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*_test.go: Always use github.com/onsi/gomega package for assertions in test files
When using gomega with native go tests, use the gomega.NewWithT(t) approach for assertions

Files:

  • rls/payload_test.go
  • bench/utils_test.go
  • tests/rls_test.go
  • bench/bench_test.go
🧠 Learnings (15)
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to rls/payload.go : Build RLS Payload with scopes in `rls/payload.go` and use `SetPostgresSessionRLS()` to serialize the Payload to JSON

Applied to files:

  • migrate/migrate.go
  • rls/payload_test.go
  • views/9998_rls_enable.sql
  • tests/rls_test.go
  • bench/bench_test.go
  • rls/payload.go
  • views/035_rls_utils.sql
  • views/035_view_rls.sql
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to tests/rls_test.go : Add comprehensive RLS test cases to `tests/rls_test.go` covering access granted scenarios, access denied scenarios, and edge cases

Applied to files:

  • rls/payload_test.go
  • tests/rls_test.go
  • bench/bench_test.go
  • rls/payload.go
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to **/*_test.go : Always use `github.com/onsi/gomega` package for assertions in test files

Applied to files:

  • rls/payload_test.go
  • Makefile
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to **/*_test.go : When using gomega with native go tests, use the `gomega.NewWithT(t)` approach for assertions

Applied to files:

  • rls/payload_test.go
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Always use ginkgo to run tests, never run `go test` directly

Applied to files:

  • rls/payload_test.go
  • Makefile
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to tests/**/*_test.go : Use resources from the dummy dataset in tests/fixtures/dummy/all.go in tests before creating new test resources

Applied to files:

  • rls/payload_test.go
  • Makefile
  • tests/rls_test.go
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/**/*.sql : Tables with direct RLS policies must use the `match_scope()` function to evaluate JWT claims against row attributes (tags, agents, names, id)

Applied to files:

  • schema/views.hcl
  • views/9998_rls_enable.sql
  • tests/rls_test.go
  • rls/payload.go
  • views/035_rls_utils.sql
  • views/035_view_rls.sql
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/9998_rls_enable.sql : Add RLS enable logic to `views/9998_rls_enable.sql` including enable RLS on the table and create the policy

Applied to files:

  • schema/views.hcl
  • views/9998_rls_enable.sql
  • tests/rls_test.go
  • views/035_rls_utils.sql
  • views/035_view_rls.sql
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to **/zz_generated.deepcopy.go : Never manually edit `zz_generated.deepcopy.go` files; use `make generate` to generate them

Applied to files:

  • Makefile
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Use ginkgo with `--label-filter='!ignore_local'` flag to run tests in a package

Applied to files:

  • Makefile
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Use `ginkgo -focus "TestName" -r` to run a specific test

Applied to files:

  • Makefile
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/9999_rls_disable.sql : Add RLS disable logic to `views/9999_rls_disable.sql` including disable RLS on the table and drop the policy

Applied to files:

  • views/9998_rls_enable.sql
  • tests/rls_test.go
  • views/035_rls_utils.sql
  • views/035_view_rls.sql
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/**/*.sql : Child tables must inherit access control from their parent using `EXISTS` clauses in RLS policies

Applied to files:

  • views/9998_rls_enable.sql
  • tests/rls_test.go
  • views/035_view_rls.sql
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/**/*.sql : RLS policies must read JWT claims from PostgreSQL session variables via `request.jwt.claims` using `current_setting('request.jwt.claims')::jsonb`

Applied to files:

  • views/9998_rls_enable.sql
  • tests/rls_test.go
  • views/035_rls_utils.sql
  • views/035_view_rls.sql
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Use PostgreSQL Row-Level Security (RLS) to enforce multi-tenant access control with JWT claims passed via PostgREST

Applied to files:

  • views/9998_rls_enable.sql
  • rls/payload.go
  • views/035_rls_utils.sql
  • views/035_view_rls.sql
🧬 Code graph analysis (3)
rls/payload_test.go (1)
rls/payload.go (1)
  • Payload (16-24)
tests/rls_test.go (6)
rls/payload.go (1)
  • Payload (16-24)
tests/setup/common.go (1)
  • DefaultContext (53-53)
tests/fixtures/dummy/all.go (2)
  • Logistics (468-477)
  • LogisticsAPICanary (635-641)
models/playbooks.go (1)
  • Playbook (93-107)
tests/fixtures/dummy/playbooks.go (2)
  • EchoConfig (9-34)
  • RestartPod (36-61)
tests/fixtures/dummy/views.go (2)
  • PodView (60-128)
  • ViewDev (130-161)
bench/bench_test.go (1)
rls/payload.go (1)
  • Payload (16-24)
🪛 GitHub Actions: Test
migrate/migrate.go

[error] 79-79: failed to grant postgrest roles: ERROR: syntax error at or near '-' (SQLSTATE 42601)

🔇 Additional comments (20)
Makefile (1)

15-18: LGTM!

The pre-test cleanup step is a sensible addition to ensure a clean state before running tests. Removing previously downloaded git directories prevents their unit tests from being inadvertently executed. Based on learnings, this correctly integrates with the ginkgo-based test workflow.

schema/views.hcl (1)

29-32: LGTM!

The __scope column and corresponding GIN index are well-structured for the RLS scope-based access pattern. The GIN index will efficiently support the array overlap (&&) queries used in rls_scope_access() checks.

Also applies to: 68-71

schema/config.hcl (1)

311-314: LGTM!

The __scope column and GIN index additions to config_items are consistent with the broader RLS scope pattern introduced in this PR. This will enable efficient scope-based filtering for config items.

Also applies to: 415-418

api/config.go (1)

126-146: LGTM on the struct definition and documentation.

The field comments clearly document the purpose of each role. The struct organization is clean.

views/035_view_rls.sql (1)

18-23: LGTM!

The updated check_view_grants function correctly uses rls_scope_access() to check scope overlap. The EXISTS pattern with jsonb_array_elements_text is an efficient approach for checking array membership. The VOLATILE marking is appropriate since it depends on session state.

bench/bench_test.go (1)

164-186: LGTM!

The benchmark correctly migrates to the UUID-based Scopes payload structure. The cycling through benchScopeIDs with modulo ensures varied scope testing, and error messages are appropriately updated.

bench/utils_test.go (1)

124-141: LGTM with a minor observation.

The scope materialization logic correctly associates UUIDs with tags and updates the __scope column. The idempotency check (NOT (__scope @> ARRAY[?])) is technically unnecessary since uuid.New() generates unique UUIDs each time, but it's a harmless safeguard that doesn't impact benchmark correctness.

rls/payload_test.go (1)

10-84: LGTM!

Tests correctly migrate to UUID-based Scopes and maintain comprehensive coverage of fingerprint functionality. The use of gomega.NewWithT(t) follows the coding guidelines. Test cases appropriately cover edge cases including empty scopes, deterministic ordering, and caching behavior.

views/035_rls_utils.sql (1)

1-17: Verify upstream payload construction for scope validation.

The function correctly extracts scope UUIDs from JWT claims using current_setting('request.jwt.claims')::jsonb. The STABLE marking is appropriate since session settings don't change within a transaction, and SECURITY INVOKER ensures proper permission context.

However, the ::uuid cast on line 13 will raise a runtime error if scopes contains non-UUID values. Verify that the upstream payload construction in rls/payload.go using SetPostgresSessionRLS() validates and guarantees only valid UUIDs are serialized to the JWT claims scopes field.

views/9998_rls_enable.sql (4)

54-61: Clean and consistent scope-based policy pattern.

The simplified RLS policy using __scope && rls_scope_access() is a good approach. This relies on the GIN index on __scope for performance.


143-170: The playbook_runs policy logic is comprehensive.

The policy correctly enforces:

  1. User must have access to the parent playbook
  2. If run has a config_id, user must have access to that config
  3. If run has a check_id, user must have access to that check

The IS NULL OR EXISTS pattern correctly handles optional foreign keys.


207-234: Security invoker settings ensure views inherit RLS from underlying tables.

The security_invoker = true setting on all these views ensures RLS policies are evaluated for the current user rather than the view owner. This is the correct approach for multi-tenant access control.


102-114: Verify if config_component_relationships should check access to both sides of the relationship.

Currently, the policy only checks if the user has access to the config_id. This means a user can see a relationship if they have access to the config, even if they lack access to the component. If both sides should be protected equally, add a check for component_id:

EXISTS (SELECT 1 FROM config_items WHERE config_items.id = config_component_relationships.config_id)
AND EXISTS (SELECT 1 FROM components WHERE components.id = config_component_relationships.component_id)

Check if other similar relationship tables in the codebase verify access to both sides to determine the intended access control pattern.

tests/rls_test.go (4)

46-55: SQL injection risk in test helpers mitigated by context.

The resetScopes and assignScope functions use fmt.Sprintf for table names. While this is generally risky, it's acceptable in test code where table names are hardcoded strings. The where clause arguments are properly parameterized.


64-112: Good test coverage for config_items scope-based RLS.

The test correctly:

  • Sets up scope IDs before tests
  • Resets and assigns scopes to specific rows
  • Tests both roles (postgrest_anon, postgrest_api)
  • Covers no scopes (0 results), single scope, and combined scopes (OR)

Based on learnings, this aligns with the expected comprehensive RLS test cases.


215-271: Tests verify RLS inheritance for checks via canaries.

This correctly tests that checks inherit access control from their parent canary via the EXISTS clause in the RLS policy.


273-341: Tests verify RLS inheritance for view_panels via views.

This correctly tests that view_panels inherit access control from their parent view via the EXISTS clause in the RLS policy.

rls/payload.go (3)

15-24: Clean simplified Payload structure.

The transition from complex per-resource-type scopes to a single []uuid.UUID aligns with the precomputed scopes approach mentioned in the PR objectives. This should improve query performance by eliminating the complex match_scope() function evaluations.


47-57: Fingerprint computation is deterministic and correct.

Sorting the scope UUIDs before hashing ensures consistent fingerprints regardless of input order. Converting UUIDs to strings before sorting and joining is the correct approach.


68-84: Good API design with default and custom role variants.

The separation of SetPostgresSessionRLS (uses default postgrest_api role) and SetPostgresSessionRLSWithRole (custom role) provides both convenience and flexibility. The local vs global variants handle transaction-scoped vs session-scoped settings appropriately.

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

@adityathebe adityathebe force-pushed the feat/precomputed-scopes branch from c7bc2c5 to 442560f Compare January 15, 2026 20:28
@adityathebe adityathebe force-pushed the feat/precomputed-scopes branch from 442560f to 5dc9494 Compare January 15, 2026 20:33
Copy link
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

🤖 Fix all issues with AI agents
In `@views/9998_rls_enable.sql`:
- Around line 119-123: The policies components_auth, canaries_auth,
playbooks_auth, and views_auth currently block rows where __scope is NULL;
update each policy's USING clause to treat NULL __scope as non-restrictive by
either coalescing __scope to an empty array before the overlap check (e.g., use
coalesce(<table>.__scope, ARRAY[]::text[]) && rls_scope_access()) or by adding
an OR <table>.__scope IS NULL condition so rows with NULL __scope are allowed;
apply this change to the components_auth, canaries_auth, playbooks_auth and
views_auth policy definitions that call rls_scope_access().
🧹 Nitpick comments (5)
views/035_rls_utils.sql (1)

8-15: Consider defensive handling for malformed JWT claims.

The function will throw a runtime error if:

  1. The scopes field exists but is not a JSON array
  2. Any element in the scopes array is not a valid UUID

While JWT claims are typically controlled server-side, a malformed payload could cause query failures. Consider wrapping the extraction in an exception handler:

♻️ Proposed defensive implementation
 CREATE
 OR REPLACE FUNCTION rls_scope_access() RETURNS UUID[] AS $$
 DECLARE
   jwt_claims TEXT;
 BEGIN
   jwt_claims := current_setting('request.jwt.claims', TRUE);
   IF jwt_claims IS NULL OR jwt_claims = '' THEN
     RETURN '{}'::uuid[];
   END IF;

-  RETURN COALESCE(
-    ARRAY(SELECT jsonb_array_elements_text(jwt_claims::jsonb -> 'scopes')::uuid),
-    '{}'::uuid[]
-  );
+  BEGIN
+    RETURN COALESCE(
+      ARRAY(SELECT jsonb_array_elements_text(jwt_claims::jsonb -> 'scopes')::uuid),
+      '{}'::uuid[]
+    );
+  EXCEPTION WHEN OTHERS THEN
+    -- Return empty array on malformed claims (invalid UUID, non-array scopes, etc.)
+    RETURN '{}'::uuid[];
+  END;
 END;
 $$ LANGUAGE plpgsql STABLE SECURITY INVOKER;
migrate/migrate.go (1)

236-261: LGTM - Proper identifier quoting fixes SQL injection risk and syntax errors.

Role identifiers are now properly quoted using pq.QuoteIdentifier, addressing the previous pipeline failure with hyphenated role names.

For consistency, consider using pq.QuoteIdentifier for the user parameter as well on line 249:

♻️ Optional consistency improvement
-		if _, err := db.Exec(fmt.Sprintf(`GRANT %s TO "%s"`, pq.QuoteIdentifier(roleName), user)); err != nil {
+		if _, err := db.Exec(fmt.Sprintf(`GRANT %s TO %s`, pq.QuoteIdentifier(roleName), pq.QuoteIdentifier(user))); err != nil {
rls/payload.go (1)

68-84: Consider parameterizing the default role.

The convenience methods hardcode "postgrest_api" as the role. While this works, it creates a coupling that could diverge from api.DefaultConfig.Postgrest.DBRole.

If the default role name ever changes in configuration, these methods would need updating. Consider whether a package-level constant or accessing the config would be more maintainable:

♻️ Optional: Extract default role as constant
+const DefaultDBRole = "postgrest_api"
+
 // Injects the payload as local parameter
 func (t Payload) SetPostgresSessionRLS(db *gorm.DB) error {
-	return t.setPostgresSessionRLS(db, true, "postgrest_api")
+	return t.setPostgresSessionRLS(db, true, DefaultDBRole)
 }
 
 // Injects the payload as sessions parameter
 func (t Payload) SetGlobalPostgresSessionRLS(db *gorm.DB) error {
-	return t.setPostgresSessionRLS(db, false, "postgrest_api")
+	return t.setPostgresSessionRLS(db, false, DefaultDBRole)
 }
tests/rls_test.go (2)

46-55: Consider using parameterized queries for table names.

While this is test code with hardcoded table names, using fmt.Sprintf to interpolate table names into SQL queries is a pattern that could be problematic if replicated elsewhere. Since table names cannot be parameterized in prepared statements, this is acceptable here, but worth noting for consistency.


67-68: Unused variables: totalConfigs and totalComponents.

These variables are declared and populated but never used in the test assertions. If they're intentionally kept for potential future wildcard scope tests, consider adding a comment. Otherwise, they can be removed to reduce clutter.

Also applies to: 117-118

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7bc2c5 and 5dc9494.

📒 Files selected for processing (22)
  • .github/workflows/benchmark.yml
  • AGENTS.md
  • Makefile
  • api/config.go
  • bench/bench_test.go
  • bench/utils_test.go
  • db.go
  • migrate/migrate.go
  • rls/payload.go
  • rls/payload_test.go
  • schema/checks.hcl
  • schema/components.hcl
  • schema/config.hcl
  • schema/playbooks.hcl
  • schema/views.hcl
  • tests/rls_test.go
  • types/resource_selector.go
  • view/db.go
  • views/035_rls_utils.sql
  • views/035_view_rls.sql
  • views/043_scope_permission_events.sql
  • views/9998_rls_enable.sql
💤 Files with no reviewable changes (2)
  • types/resource_selector.go
  • .github/workflows/benchmark.yml
🚧 Files skipped from review as they are similar to previous changes (7)
  • schema/checks.hcl
  • Makefile
  • db.go
  • views/035_view_rls.sql
  • schema/config.hcl
  • views/043_scope_permission_events.sql
  • AGENTS.md
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Only add comments if really necessary; do not add comments that simply explain the code
Comments about functions are considered good practice in Go even if they are self-explanatory

Files:

  • view/db.go
  • bench/bench_test.go
  • migrate/migrate.go
  • bench/utils_test.go
  • rls/payload.go
  • tests/rls_test.go
  • api/config.go
  • rls/payload_test.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*_test.go: Always use github.com/onsi/gomega package for assertions in test files
When using gomega with native go tests, use the gomega.NewWithT(t) approach for assertions

Files:

  • bench/bench_test.go
  • bench/utils_test.go
  • tests/rls_test.go
  • rls/payload_test.go
🧠 Learnings (13)
📓 Common learnings
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/**/*.sql : Tables with direct RLS policies must use the `match_scope()` function to evaluate JWT claims against row attributes (tags, agents, names, id)
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to rls/payload.go : Build RLS Payload with scopes in `rls/payload.go` and use `SetPostgresSessionRLS()` to serialize the Payload to JSON
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/**/*.sql : RLS policies must read JWT claims from PostgreSQL session variables via `request.jwt.claims` using `current_setting('request.jwt.claims')::jsonb`
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/9998_rls_enable.sql : Add RLS enable logic to `views/9998_rls_enable.sql` including enable RLS on the table and create the policy
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/**/*.sql : Child tables must inherit access control from their parent using `EXISTS` clauses in RLS policies
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Use PostgreSQL Row-Level Security (RLS) to enforce multi-tenant access control with JWT claims passed via PostgREST
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/9998_rls_enable.sql : Add RLS enable logic to `views/9998_rls_enable.sql` including enable RLS on the table and create the policy

Applied to files:

  • views/9998_rls_enable.sql
  • view/db.go
  • tests/rls_test.go
  • views/035_rls_utils.sql
  • schema/views.hcl
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/9999_rls_disable.sql : Add RLS disable logic to `views/9999_rls_disable.sql` including disable RLS on the table and drop the policy

Applied to files:

  • views/9998_rls_enable.sql
  • view/db.go
  • tests/rls_test.go
  • views/035_rls_utils.sql
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/**/*.sql : Tables with direct RLS policies must use the `match_scope()` function to evaluate JWT claims against row attributes (tags, agents, names, id)

Applied to files:

  • views/9998_rls_enable.sql
  • view/db.go
  • migrate/migrate.go
  • tests/rls_test.go
  • views/035_rls_utils.sql
  • schema/views.hcl
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/**/*.sql : Child tables must inherit access control from their parent using `EXISTS` clauses in RLS policies

Applied to files:

  • views/9998_rls_enable.sql
  • view/db.go
  • tests/rls_test.go
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to views/**/*.sql : RLS policies must read JWT claims from PostgreSQL session variables via `request.jwt.claims` using `current_setting('request.jwt.claims')::jsonb`

Applied to files:

  • views/9998_rls_enable.sql
  • view/db.go
  • migrate/migrate.go
  • tests/rls_test.go
  • views/035_rls_utils.sql
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to rls/payload.go : Build RLS Payload with scopes in `rls/payload.go` and use `SetPostgresSessionRLS()` to serialize the Payload to JSON

Applied to files:

  • views/9998_rls_enable.sql
  • bench/bench_test.go
  • migrate/migrate.go
  • rls/payload.go
  • tests/rls_test.go
  • rls/payload_test.go
  • views/035_rls_utils.sql
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Use PostgreSQL Row-Level Security (RLS) to enforce multi-tenant access control with JWT claims passed via PostgREST

Applied to files:

  • views/9998_rls_enable.sql
  • view/db.go
  • rls/payload.go
  • views/035_rls_utils.sql
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to tests/rls_test.go : Add comprehensive RLS test cases to `tests/rls_test.go` covering access granted scenarios, access denied scenarios, and edge cases

Applied to files:

  • view/db.go
  • bench/bench_test.go
  • rls/payload.go
  • tests/rls_test.go
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to tests/**/*_test.go : Use resources from the dummy dataset in tests/fixtures/dummy/all.go in tests before creating new test resources

Applied to files:

  • tests/rls_test.go
  • rls/payload_test.go
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to **/*_test.go : Always use `github.com/onsi/gomega` package for assertions in test files

Applied to files:

  • rls/payload_test.go
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Applies to **/*_test.go : When using gomega with native go tests, use the `gomega.NewWithT(t)` approach for assertions

Applied to files:

  • rls/payload_test.go
📚 Learning: 2025-12-11T13:04:55.228Z
Learnt from: CR
Repo: flanksource/duty PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-11T13:04:55.228Z
Learning: Always use ginkgo to run tests, never run `go test` directly

Applied to files:

  • rls/payload_test.go
🧬 Code graph analysis (2)
bench/bench_test.go (1)
rls/payload.go (1)
  • Payload (16-24)
rls/payload_test.go (1)
rls/payload.go (1)
  • Payload (16-24)
🔇 Additional comments (25)
schema/components.hcl (1)

200-203: LGTM - Schema changes for scope-based RLS.

The __scope column with uuid[] type and GIN index are correctly configured to support the && (overlap) operator used in RLS policy checks. The nullable column allows backward-compatible migration.

Also applies to: 371-374

schema/playbooks.hcl (1)

34-37: LGTM - Consistent with scope-based RLS pattern.

The __scope column and GIN index follow the same pattern as other tables, enabling efficient scope overlap checks in RLS policies.

Also applies to: 73-76

schema/views.hcl (1)

29-32: LGTM - Consistent scope column and index.

The __scope column and GIN index are properly configured for the views table, matching the pattern used across other resource tables.

Also applies to: 68-71

views/9998_rls_enable.sql (5)

146-170: LGTM - Playbook runs policy correctly inherits access from related resources.

The policy properly:

  • Requires access to the parent playbook
  • Conditionally requires access to related config/check if present
  • Documents the intentional omission of component_id check

The NULL OR EXISTS pattern correctly handles optional foreign keys.


66-75: LGTM - Child table policies correctly inherit from parents.

The EXISTS clause pattern properly delegates access control to the parent tables (config_items, canaries, views), following the established RLS inheritance pattern.

Also applies to: 80-89, 105-114, 175-184, 198-205


94-100: LGTM - Relationship visibility requires access to both endpoints.

Correctly enforces that users can only see config relationships when they have access to both the source and target config items, preventing information leakage.


54-61: The referenced bypass functions do not exist in the current implementation.

The review references rls_has_wildcard() and is_rls_disabled() functions in an EXPLAIN output, but these functions are not present in the codebase. The current RLS implementation uses a strictly scope-based approach via rls_scope_access(), which returns an empty array when JWT claims are missing—effectively blocking all access without explicit scope assignments.

If admin bypass is required, you should either:

  1. Verify this stricter security model is intentional
  2. Implement an admin bypass mechanism (e.g., checking for wildcard scopes or special roles in the JWT claims)
  3. Handle admin access at the application or PostgREST configuration level

57-61: No action needed. The RLS policy correctly denies access to rows with NULL __scope values; this is the intended behavior as confirmed by the test suite and documentation. Rows without assigned scopes should be inaccessible to all users.

api/config.go (2)

13-19: LGTM - Role name fix and proper defaults.

The DBRoleBypass default correctly uses underscore (rls_bypasser) instead of hyphen, addressing the previous SQL syntax error issue. The default configuration is well-structured.


126-147: Good documentation of role fields.

The added comments clarify the purpose of each PostgREST role, improving maintainability.

migrate/migrate.go (1)

275-286: LGTM - Bypass role creation with appropriate error handling.

The bypass role is created with equivalent privileges and BYPASSRLS is granted. The error logging (rather than returning) on line 284 is reasonable since BYPASSRLS may require superuser privileges that aren't always available, and the role can still function without it in some scenarios.

rls/payload.go (3)

16-24: Good transition to UUID-based scopes.

Using uuid.UUID instead of strings provides stronger type safety and aligns with the database's UUID representation for scope identifiers.


41-58: LGTM - Deterministic fingerprint calculation.

The implementation correctly converts UUIDs to strings, sorts them, and computes a hash. This ensures fingerprints are consistent regardless of input order.


86-112: LGTM - Proper role validation and secure SQL generation.

Good defensive check for empty role. The SQL generation correctly uses pq.QuoteIdentifier for the role and pq.QuoteLiteral for the JSON payload, preventing SQL injection.

rls/payload_test.go (1)

10-83: LGTM - Comprehensive test coverage for UUID-based scopes.

The tests properly cover key behaviors:

  • Disabled state fingerprint
  • Deterministic fingerprint computation
  • Empty scopes handling
  • Order-independent fingerprint (sorting)
  • Fingerprint caching after initial computation

Tests correctly use gomega.NewWithT(t) per coding guidelines.

view/db.go (1)

152-158: LGTM - RLS policy correctly delegates grant checks to scope-based framework.

The policy properly uses check_view_grants(__grants) which internally validates that the row's grant UUIDs match the user's accessible scope via rls_scope_access(). The __grants column is part of the view table structure and the function correctly handles null/empty grant cases.

bench/bench_test.go (2)

11-11: LGTM!

Import added correctly to support the new UUID-based scope model.


169-172: LGTM!

The payload construction correctly uses the new Scopes field with UUID values, and the error message is appropriately updated to reference "scope" instead of "tag".

tests/rls_test.go (5)

19-26: LGTM!

The scopeCase struct and setRLS helper provide a clean abstraction for the table-driven tests. Using a function for payload allows lazy evaluation, which is appropriate since scope IDs are generated in BeforeAll.


28-44: LGTM!

Well-designed helper functions. payloadWithScopes correctly filters out nil pointers before constructing the payload, preventing potential issues with optional scope parameters.


64-112: LGTM!

The config_items test suite is well-structured:

  • Properly sets up scope assignments based on tag filtering
  • Tests no scopes (expects 0), single scope, and combined scopes (OR behavior)
  • Runs for both postgrest_anon and postgrest_api roles
  • Uses resetScopes in BeforeAll to ensure clean state

215-271: Good coverage of RLS inheritance for checks.

The test correctly validates that checks inherit RLS access from their parent canaries via the scope assigned to canaries table, not directly to checks. This aligns with the learning that "Child tables must inherit access control from their parent using EXISTS clauses in RLS policies."


273-341: Good coverage of RLS inheritance for view_panels.

Similar to checks, the test validates that view_panels inherit access from their parent views. The combined scopes test ensures OR behavior works correctly across multiple views.

bench/utils_test.go (2)

32-33: LGTM!

Package-level variable declaration for benchScopeIDs is appropriate here since it's populated during setupConfigsForSize and consumed by BenchmarkMain in bench_test.go.


124-141: LGTM!

The scope materialization logic is well-implemented:

  • Creates a unique scope UUID for each sample tag
  • Uses tags @> ?::jsonb for proper JSON containment matching
  • Includes idempotency check to avoid duplicate scope assignments
  • Properly handles null __scope columns with COALESCE
  • Error handling includes descriptive wrapped messages

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

Copy link
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

🤖 Fix all issues with AI agents
In `@views/9998_rls_enable_precomputed.sql`:
- Around line 54-234: The policies use precomputed __scope (uuid[]), but __scope
is never populated and no handler exists for
scope.materialize/permission.materialize events, so either implement a
materialization handler that listens for scope.materialize and
permission.materialize events and populates table.__scope using the same logic
as match_scope()/match_scope function (ensuring compatibility with
rls_scope_access() and the rls.precomputed_scope flag), or revert the policies
to call match_scope() directly (remove direct "__scope && rls_scope_access()"
uses in policies like components_auth, canaries_auth, playbooks_auth,
views_auth, config_items_auth and others) and disable the precomputed mode;
update the config flag handling code where rls.precomputed_scope is read to
reflect non-functional status if you choose the revert path.

Comment on lines +54 to +234
-- Policy config items
DROP POLICY IF EXISTS config_items_auth ON config_items;

CREATE POLICY config_items_auth ON config_items
FOR ALL TO postgrest_api, postgrest_anon
USING (
CASE WHEN (SELECT is_rls_disabled()) THEN TRUE
ELSE (config_items.__scope && rls_scope_access())
END
);

-- Policy config_changes
DROP POLICY IF EXISTS config_changes_auth ON config_changes;

CREATE POLICY config_changes_auth ON config_changes
FOR ALL TO postgrest_api, postgrest_anon
USING (
CASE WHEN (SELECT is_rls_disabled()) THEN TRUE
ELSE EXISTS (
-- just leverage the RLS on config_items
SELECT 1
FROM config_items
WHERE config_items.id = config_changes.config_id
)
END
);

-- Policy config_analysis
DROP POLICY IF EXISTS config_analysis_auth ON config_analysis;

CREATE POLICY config_analysis_auth ON config_analysis
FOR ALL TO postgrest_api, postgrest_anon
USING (
CASE WHEN (SELECT is_rls_disabled()) THEN TRUE
ELSE EXISTS (
-- just leverage the RLS on config_items
SELECT 1
FROM config_items
WHERE config_items.id = config_analysis.config_id
)
END
);

-- Policy config_relationships
DROP POLICY IF EXISTS config_relationships_auth ON config_relationships;

CREATE POLICY config_relationships_auth ON config_relationships
FOR ALL TO postgrest_api, postgrest_anon
USING (
CASE WHEN (SELECT is_rls_disabled()) THEN TRUE
ELSE (
-- just leverage the RLS on config_items - user must have access to both items
EXISTS (SELECT 1 FROM config_items WHERE config_items.id = config_relationships.config_id)
AND EXISTS (SELECT 1 FROM config_items WHERE config_items.id = config_relationships.related_id)
)
END
);

-- Policy config_component_relationships
DROP POLICY IF EXISTS config_component_relationships_auth ON config_component_relationships;

CREATE POLICY config_component_relationships_auth ON config_component_relationships
FOR ALL TO postgrest_api, postgrest_anon
USING (
CASE WHEN (SELECT is_rls_disabled()) THEN TRUE
ELSE EXISTS (
-- just leverage the RLS on config_items
SELECT 1
FROM config_items
WHERE config_items.id = config_component_relationships.config_id
)
END
);

-- Policy components
DROP POLICY IF EXISTS components_auth ON components;

CREATE POLICY components_auth ON components
FOR ALL TO postgrest_api, postgrest_anon
USING (
CASE WHEN (SELECT is_rls_disabled()) THEN TRUE
ELSE (components.__scope && rls_scope_access())
END
);

-- Policy canaries
DROP POLICY IF EXISTS canaries_auth ON canaries;

CREATE POLICY canaries_auth ON canaries
FOR ALL TO postgrest_api, postgrest_anon
USING (
CASE WHEN (SELECT is_rls_disabled()) THEN TRUE
ELSE (canaries.__scope && rls_scope_access())
END
);

-- Policy playbooks
DROP POLICY IF EXISTS playbooks_auth ON playbooks;

CREATE POLICY playbooks_auth ON playbooks
FOR ALL TO postgrest_api, postgrest_anon
USING (
CASE WHEN (SELECT is_rls_disabled()) THEN TRUE
ELSE (playbooks.__scope && rls_scope_access())
END
);

-- Policy playbook_runs
DROP POLICY IF EXISTS playbook_runs_auth ON playbook_runs;

CREATE POLICY playbook_runs_auth ON playbook_runs
FOR ALL TO postgrest_api, postgrest_anon
USING (
CASE WHEN (SELECT is_rls_disabled()) THEN TRUE
ELSE (
-- User must have access to the playbook
EXISTS (
SELECT 1
FROM playbooks
WHERE playbooks.id = playbook_runs.playbook_id
)
AND
-- AND if run has a config_id, user must have access to that config
(playbook_runs.config_id IS NULL OR EXISTS (
SELECT 1
FROM config_items
WHERE config_items.id = playbook_runs.config_id
))
AND
-- AND if run has a check_id, user must have access to that check (via its canary)
(playbook_runs.check_id IS NULL OR EXISTS (
SELECT 1
FROM checks
WHERE checks.id = playbook_runs.check_id
))
-- Note: component_id check omitted (phasing out topology soon)
)
END
);

-- Policy checks
DROP POLICY IF EXISTS checks_auth ON checks;

CREATE POLICY checks_auth ON checks
FOR ALL TO postgrest_api, postgrest_anon
USING (
CASE WHEN (SELECT is_rls_disabled()) THEN TRUE
ELSE EXISTS (
-- just leverage the RLS on canaries
SELECT 1
FROM canaries
WHERE canaries.id = checks.canary_id
)
END
);

-- Policy views
DROP POLICY IF EXISTS views_auth ON views;

CREATE POLICY views_auth ON views
FOR ALL TO postgrest_api, postgrest_anon
USING (
CASE WHEN (SELECT is_rls_disabled()) THEN TRUE
ELSE (views.__scope && rls_scope_access())
END
);

-- Policy view_panels (inherits from parent views table)
DROP POLICY IF EXISTS view_panels_auth ON view_panels;

CREATE POLICY view_panels_auth ON view_panels
FOR ALL TO postgrest_api, postgrest_anon
USING (
CASE WHEN (SELECT is_rls_disabled()) THEN TRUE
ELSE EXISTS (
SELECT 1 FROM views
WHERE views.id = view_panels.view_id
)
END
);

Copy link
Contributor

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

🏁 Script executed:

#!/bin/bash
# Locate RLS helper functions and JWT claim usage
echo "=== Searching for rls_scope_access function ==="
rg -n "rls_scope_access" -g '*.sql' -B2 -A10 | head -100

echo -e "\n=== Searching for match_scope function ==="
rg -n "match_scope" -g '*.sql' -B2 -A10 | head -100

echo -e "\n=== Searching for JWT claims usage ==="
rg -n "request\.jwt\.claims" -g '*.sql' -B1 -A1

echo -e "\n=== Searching for __scope references ==="
rg -n "__scope" -g '*.sql' | head -50

Repository: flanksource/duty

Length of output: 12325


🏁 Script executed:

#!/bin/bash
# Find where __scope column is populated/materialized
echo "=== Checking scope_permission_events.sql ==="
cat -n views/043_scope_permission_events.sql

echo -e "\n=== Searching for __scope column creation ==="
rg -n "ALTER TABLE.*ADD.*__scope" -g '*.sql'
rg -n "CREATE TABLE.*__scope" -g '*.sql'

echo -e "\n=== Searching for UPDATE/INSERT with __scope assignment ==="
rg -n "UPDATE.*__scope\|INSERT.*__scope" -g '*.sql' -B2 -A2 | head -80

echo -e "\n=== Searching for triggers related to __scope ==="
rg -n "CREATE.*TRIGGER.*__scope\|__scope.*TRIGGER" -g '*.sql' -B2 -A10 | head -100

Repository: flanksource/duty

Length of output: 2570


🏁 Script executed:

#!/bin/bash
# Search for __scope computation in Go code
echo "=== Searching for __scope in Go files ==="
rg -n "__scope" -g '*.go' -B3 -A3 | head -150

echo -e "\n=== Searching for scope.materialize event handler ==="
rg -n "scope\.materialize\|materialize.*scope" -g '*.go' -i -B2 -A5 | head -100

echo -e "\n=== Checking rls/payload.go per learnings ==="
cat -n rls/payload.go 2>/dev/null | head -150

Repository: flanksource/duty

Length of output: 5486


🏁 Script executed:

#!/bin/bash
# Search for event processor that handles materialization
echo "=== Searching for event processor/handler ==="
rg -n "scope.materialize\|permission.materialize" -g '*.go' -i -B2 -A10

echo -e "\n=== Searching for materialization logic in Go ==="
rg -n "materialize|materialization" -g '*.go' -i | head -40

echo -e "\n=== Searching for scope computation logic ==="
rg -n "func.*Scope.*UUID|func.*Compute.*Scope|scope.*permission" -g '*.go' -i -B1 -A5 | head -100

Repository: flanksource/duty

Length of output: 2619


🏁 Script executed:

#!/bin/bash
# Search more broadly for __scope updates
echo "=== Searching for actual __scope updates (not tests) ==="
rg -n "UPDATE.*__scope\|SET __scope" -g '*.go' --type-list | grep -v test | head -50

echo -e "\n=== Searching for scope permission logic ==="
rg -n "scope.*permission\|permission.*scope" -g '*.go' -i | grep -v test | head -50

echo -e "\n=== Check for scope models/database operations ==="
find . -type f -name "*.go" | xargs grep -l "type.*Scope" | grep -v test | head -20

echo -e "\n=== Look for migration files that might define __scope ==="
find . -type f \( -name "*.sql" -o -name "*.go" \) -path "*/migrations/*" | head -20
</parameter>

Repository: flanksource/duty

Length of output: 843


🏁 Script executed:

#!/bin/bash
# Inspect scope model
echo "=== Checking models/scope.go ==="
cat -n models/scope.go

echo -e "\n=== Checking for scope permission relationship ==="
rg -n "Scope\|Permission" models/scope.go

Repository: flanksource/duty

Length of output: 1514


🏁 Script executed:

#!/bin/bash
# Search for table definitions with __scope column
echo "=== Searching for __scope column in table definitions ==="
rg -n "ALTER TABLE.*ADD.*__scope|__scope.*uuid" -g '*.sql'

echo -e "\n=== Searching for scope assignment/computation ==="
rg -n "SELECT.*scope\|scope.*permission" -g '*.sql' -i | head -50

echo -e "\n=== Check what file defines config_items and check for __scope ==="
rg -n "CREATE TABLE config_items" -g '*.sql' -A 30 | head -80

echo -e "\n=== Search for any precomputed or materialization comments ==="
rg -n "precomputed|materialization" -g '*.sql' -B2 -A2

Repository: flanksource/duty

Length of output: 1572


🏁 Script executed:

#!/bin/bash
# Check if __scope column actually exists in table definitions
echo "=== Searching for any schema definition mentioning __scope ==="
find . -type f -name "*.sql" -exec grep -l "__scope" {} \;

echo -e "\n=== Check all views files for table schema definitions ==="
ls -la views/*.sql | head -20

echo -e "\n=== Look for migration or schema files ==="
find . -type f \( -name "schema.sql" -o -name "*migration*" -o -name "*.sql" \) -path "*/migrations/*" -o -path "*/schema/*" 2>/dev/null | head -20

echo -e "\n=== Search views directory for table create statements ==="
grep -r "CREATE TABLE" views/ --include="*.sql" | grep -E "config_items|components|canaries" | head -10

Repository: flanksource/duty

Length of output: 2545


🏁 Script executed:

#!/bin/bash
# Check HCL schema files for __scope column
echo "=== Searching for __scope in HCL schema files ==="
rg -n "__scope" schema/ 2>/dev/null | head -50

echo -e "\n=== Check if schema files define config_items, components, etc ==="
rg -n "config_items|components|canaries" schema/ --include="*.hcl" -B2 -A10 | head -100

echo -e "\n=== Search entire repo for where __scope is added/defined ==="
rg -n "ADD COLUMN.*__scope\|__scope.*uuid\[\]" -g '*.{sql,hcl,go}' --no-heading | head -50

Repository: flanksource/duty

Length of output: 1482


🏁 Script executed:

#!/bin/bash
# Check the schema definition of __scope
echo "=== Check __scope column type in schema/config.hcl ==="
sed -n '311,320p' schema/config.hcl

echo -e "\n=== Check __scope in schema/views.hcl ==="
sed -n '29,40p' schema/views.hcl

echo -e "\n=== Check if there's any documentation about precomputed vs non-precomputed ==="
rg -n "precomputed" -g '*.md' -B2 -A2

echo -e "\n=== Search for scope event handler in other directories ==="
find . -type f -name "*.go" -exec grep -l "scope.materialize\|permission.materialize" {} \; 2>/dev/null

echo -e "\n=== Look for any processor or worker that handles events ==="
find . -type f -name "*.go" | xargs grep -l "event_queue\|EventQueue" 2>/dev/null | head -20

Repository: flanksource/duty

Length of output: 1848


🏁 Script executed:

#!/bin/bash
# Check the event handler to confirm __scope is being materialized
echo "=== Inspect postq/event.go for scope materialization handling ==="
cat -n postq/event.go | head -100

echo -e "\n=== Check async_consumer.go for event processing ==="
grep -n "scope.materialize\|permission.materialize" postq/async_consumer.go -B5 -A10 2>/dev/null | head -80

echo -e "\n=== Check if there's scope materialization in models ==="
find . -type f -name "*.go" -path "*/models/*" -exec grep -l "Scope\|Permission" {} \; 2>/dev/null

Repository: flanksource/duty

Length of output: 3220


🏁 Script executed:

#!/bin/bash
# Check async_consumer for all event handlers
echo "=== Full async_consumer.go ==="
wc -l postq/async_consumer.go
cat -n postq/async_consumer.go | head -150

echo -e "\n=== Check models/permission.go for scope materialization logic ==="
cat -n models/permission.go | head -100

Repository: flanksource/duty

Length of output: 7325


🏁 Script executed:

#!/bin/bash
# Search for where scope.materialize events are actually handled
echo "=== Search for event handler registration/dispatch ==="
find . -type f -name "*.go" -exec grep -l "scope.materialize\|permission.materialize" {} \; 2>/dev/null

echo -e "\n=== Check if there's an events module ==="
ls -la events/ postq/ 2>/dev/null | head -20

echo -e "\n=== Search for any updates to config_items/components with __scope ==="
find . -type f -name "*.go" -exec grep -n "config_items\|components\|canaries" {} \; 2>/dev/null | grep -i "scope\|update" | head -30

echo -e "\n=== Check if precomputed scope is actually implemented ==="
rg -n "rls\.precomputed\|precomputed.*scope" -g '*.go' -B2 -A5

echo -e "\n=== Look in main app init or startup for event handlers ==="
rg -n "scope.materialize\|permission.materialize\|WatchEvents" -g '*.go' -B2 -A2 | head -50

Repository: flanksource/duty

Length of output: 1477


🏁 Script executed:

#!/bin/bash
# Check AGENTS.md more thoroughly
echo "=== Full AGENTS.md content ==="
cat -n AGENTS.md

echo -e "\n=== Check if precomputed scope is documented as incomplete/WIP ==="
rg -n "TODO\|FIXME\|WIP\|incomplete\|work.*progress" AGENTS.md -i

echo -e "\n=== Search for any configuration that enables/disables precomputed ==="
rg -n "precomputed.*scope\|rls.*precomputed" -g '*.go' -i -B2 -A2

Repository: flanksource/duty

Length of output: 4375


🏁 Script executed:

#!/bin/bash
# Check if precomputed mode is actually being used
echo "=== Check if views/9998_rls_enable_precomputed.sql is even loaded ==="
rg -n "9998_rls_enable_precomputed" -g '*.sql' -g '*.go' -g '*.md'

echo -e "\n=== Check migrations to see if precomputed is enabled ==="
find . -name "*.sql" | xargs grep -l "rls_enable_precomputed" 2>/dev/null

echo -e "\n=== Check if there's any comment about unimplemented precomputed ==="
rg -n "TODO\|FIXME\|precomputed" views/9998_rls_enable_precomputed.sql -i -B1 -A1

echo -e "\n=== Verify the default RLS file is being used ==="
ls -la views/9998_rls_enable*.sql
</parameter>

Repository: flanksource/duty

Length of output: 867


🏁 Script executed:

#!/bin/bash
# Check views/views.go to understand when precomputed is used
echo "=== Check views/views.go logic ==="
cat -n views/views.go

Repository: flanksource/duty

Length of output: 1049


Precomputed RLS policies rely on incomplete __scope materialization—direct __scope && rls_scope_access() cannot substitute match_scope() without population logic.

The flagged policies (lines 57–63, 131–137, 142–148, 153–159, 213–219) correctly use rls_scope_access() to read JWT claims via current_setting('request.jwt.claims', TRUE). However, the precomputed approach depends entirely on __scope being populated with scope UUIDs computed by match_scope()-equivalent logic.

Verification found:

  • __scope column (uuid[] type) is defined in schema but never populated in SQL or Go
  • Event system emits scope.materialize and permission.materialize events (views/043_scope_permission_events.sql) but no handler exists to process them
  • If rls.precomputed_scope property is enabled (currently OFF by default), all direct-table RLS policies will fail silently—NULL arrays fail the && intersection, blocking all access

Either implement the missing __scope population handler in the application, or revert to match_scope() approach documented in AGENTS.md. If precomputed mode remains WIP, mark it explicitly as non-functional until materialization is complete.

🤖 Prompt for AI Agents
In `@views/9998_rls_enable_precomputed.sql` around lines 54 - 234, The policies
use precomputed __scope (uuid[]), but __scope is never populated and no handler
exists for scope.materialize/permission.materialize events, so either implement
a materialization handler that listens for scope.materialize and
permission.materialize events and populates table.__scope using the same logic
as match_scope()/match_scope function (ensuring compatibility with
rls_scope_access() and the rls.precomputed_scope flag), or revert the policies
to call match_scope() directly (remove direct "__scope && rls_scope_access()"
uses in policies like components_auth, canaries_auth, playbooks_auth,
views_auth, config_items_auth and others) and disable the precomputed mode;
update the config flag handling code where rls.precomputed_scope is read to
reflect non-functional status if you choose the revert path.

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