Skip to content

Pr/config access#1945

Closed
moshloop wants to merge 3 commits into
mainfrom
pr/config-access
Closed

Pr/config access#1945
moshloop wants to merge 3 commits into
mainfrom
pr/config-access

Conversation

@moshloop
Copy link
Copy Markdown
Member

@moshloop moshloop commented May 1, 2026

Summary by CodeRabbit

  • Chores
    • Reorganized internal build configuration targets for improved maintainability. No user-facing changes.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 1, 2026

Review Change Stack

Warning

Rate limit exceeded

@adityathebe has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 46 minutes and 55 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 55b5fc7a-4da9-429c-9799-0d2d3c08babd

📥 Commits

Reviewing files that changed from the base of the PR and between ac34658 and ded47ac.

📒 Files selected for processing (22)
  • Makefile
  • PROPERTIES.md
  • PROPERTIES.schema.json
  • models/common.go
  • models/config.go
  • models/config_access.go
  • query/config_tree.go
  • query/config_tree_test.go
  • query/resource_selector.go
  • rbac/objects.go
  • schema/config.hcl
  • schema/config_access.hcl
  • tests/config_access_test.go
  • tests/config_relationship_test.go
  • tests/e2e-blobs/containers.go
  • tests/fixtures/dummy/all.go
  • tests/setup/common.go
  • types/client_options.go
  • views/006_config_views.sql
  • views/038_config_access.sql
  • views/045_merge_external_entities.sql
  • views/9998_rls_enable.sql

Walkthrough

The captain, PROPERTIES.md, and properties-doc Makefile targets are relocated from lines 86–102 to lines 121–137. The targets remain functionally unchanged, preserving the install-on-demand logic and claude | captain regeneration pipeline for documentation artifacts.

Changes

Documentation Targets Reorganization

Layer / File(s) Summary
Makefile Target Relocation
Makefile
The captain, PROPERTIES.md, and properties-doc targets are moved from their original position to a later location in the file, with no functional changes to the target logic or dependencies.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'Pr/config access' does not clearly relate to the changeset, which only reorganizes Makefile targets for documentation regeneration. Update the title to reflect the actual change, such as 'Reorganize Makefile documentation targets' or 'Move captain and PROPERTIES.md targets in Makefile'.
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr/config-access
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch pr/config-access

Tip

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

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

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

Built for teams:

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

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

❤️ Share

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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

Benchstat (Other)

Base: a2b12868ff621af8aa6e665d52f220c118147030
Head: ac346581043d6fb9c312ff84af891ab73f7a1839

📊 1 minor regression(s) (all within 5% threshold)

Benchmark Base Head Change p-value
UpdateOfConfigsWithProperties-4 7.060m 7.087m +0.39% 0.009
✅ 1 improvement(s)
Benchmark Base Head Change p-value
ResourceSelectorQueryBuild/name_and_type-4 59.57µ 58.50µ -1.81% 0.041
Full benchstat output
goos: linux
goarch: amd64
pkg: github.com/flanksource/duty/bench
cpu: AMD EPYC 9V74 80-Core Processor                
                                                       │ bench-base.txt │           bench-head.txt           │
                                                       │     sec/op     │    sec/op     vs base              │
InsertionForRowsWithAliases/external_users.aliases-4       516.2µ ±  9%   528.5µ ±  2%       ~ (p=0.093 n=6)
InsertionForRowsWithAliases/config_items.external_id-4     1.034m ±  9%   1.036m ± 11%       ~ (p=0.818 n=6)
InsertionOfConfigsWithProperties-4                         3.251m ±  1%   3.275m ±  2%       ~ (p=0.065 n=6)
UpdateOfConfigsWithProperties-4                            7.060m ±  1%   7.087m ±  0%  +0.39% (p=0.009 n=6)
ResourceSelectorConfigs/name-4                             196.8µ ±  5%   195.6µ ±  3%       ~ (p=1.000 n=6)
ResourceSelectorConfigs/name_and_type-4                    207.7µ ±  5%   214.2µ ±  5%       ~ (p=0.180 n=6)
ResourceSelectorConfigs/tags-4                             58.96m ± 48%   58.53m ± 49%       ~ (p=0.818 n=6)
ResourceSelectorQueryBuild/name-4                          39.95µ ±  1%   39.72µ ±  1%       ~ (p=0.240 n=6)
ResourceSelectorQueryBuild/name_and_type-4                 59.57µ ± 10%   58.50µ ±  4%  -1.81% (p=0.041 n=6)
ResourceSelectorQueryBuild/tags-4                          16.13µ ±  2%   15.92µ ± 13%       ~ (p=0.093 n=6)
geomean                                                    507.5µ         508.3µ        +0.17%

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

Benchstat (RLS)

Base: a2b12868ff621af8aa6e665d52f220c118147030
Head: ac346581043d6fb9c312ff84af891ab73f7a1839

📊 9 minor regression(s) (all within 5% threshold)

Benchmark Base Head Change p-value
RLS/Sample-15000/config_changes/Without_RLS-4 5.302m 5.532m +4.33% 0.002
RLS/Sample-15000/catalog_changes/With_RLS-4 142.3m 147.2m +3.48% 0.002
RLS/Sample-15000/config_summary/With_RLS-4 715.6m 734.4m +2.63% 0.002
RLS/Sample-15000/config_types/With_RLS-4 137.3m 140.4m +2.30% 0.002
RLS/Sample-15000/config_summary/Without_RLS-4 98.72m 100.59m +1.89% 0.002
RLS/Sample-15000/config_detail/With_RLS-4 137.2m 139.5m +1.69% 0.002
RLS/Sample-15000/config_types/Without_RLS-4 4.780m 4.853m +1.52% 0.026
RLS/Sample-15000/analyzer_types/Without_RLS-4 3.738m 3.774m +0.96% 0.041
RLS/Sample-15000/config_names/Without_RLS-4 13.54m 13.67m +0.93% 0.002
✅ 4 improvement(s)
Benchmark Base Head Change p-value
RLS/Sample-15000/change_types/With_RLS-4 5.495m 5.306m -3.45% 0.002
RLS/Sample-15000/configs/With_RLS-4 139.9m 137.6m -1.63% 0.002
RLS/Sample-15000/config_names/With_RLS-4 143.5m 141.7m -1.22% 0.002
RLS/Sample-15000/config_classes/Without_RLS-4 4.197m 4.151m -1.09% 0.026
Full benchstat output
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              │
RLS/Sample-15000/catalog_changes/Without_RLS-4      5.335m ± 1%    5.371m ± 2%       ~ (p=0.065 n=6)
RLS/Sample-15000/catalog_changes/With_RLS-4         142.3m ± 2%    147.2m ± 1%  +3.48% (p=0.002 n=6)
RLS/Sample-15000/config_changes/Without_RLS-4       5.302m ± 1%    5.532m ± 6%  +4.33% (p=0.002 n=6)
RLS/Sample-15000/config_changes/With_RLS-4          142.3m ± 1%    143.0m ± 0%       ~ (p=0.310 n=6)
RLS/Sample-15000/config_detail/Without_RLS-4        4.811m ± 1%    4.814m ± 2%       ~ (p=0.937 n=6)
RLS/Sample-15000/config_detail/With_RLS-4           137.2m ± 0%    139.5m ± 2%  +1.69% (p=0.002 n=6)
RLS/Sample-15000/config_names/Without_RLS-4         13.54m ± 1%    13.67m ± 1%  +0.93% (p=0.002 n=6)
RLS/Sample-15000/config_names/With_RLS-4            143.5m ± 0%    141.7m ± 1%  -1.22% (p=0.002 n=6)
RLS/Sample-15000/config_summary/Without_RLS-4       98.72m ± 1%   100.59m ± 1%  +1.89% (p=0.002 n=6)
RLS/Sample-15000/config_summary/With_RLS-4          715.6m ± 0%    734.4m ± 2%  +2.63% (p=0.002 n=6)
RLS/Sample-15000/configs/Without_RLS-4              8.017m ± 3%    8.149m ± 3%       ~ (p=0.065 n=6)
RLS/Sample-15000/configs/With_RLS-4                 139.9m ± 1%    137.6m ± 1%  -1.63% (p=0.002 n=6)
RLS/Sample-15000/analysis_types/Without_RLS-4       3.927m ± 1%    3.915m ± 1%       ~ (p=0.485 n=6)
RLS/Sample-15000/analysis_types/With_RLS-4          3.957m ± 4%    3.922m ± 2%       ~ (p=0.180 n=6)
RLS/Sample-15000/analyzer_types/Without_RLS-4       3.738m ± 1%    3.774m ± 1%  +0.96% (p=0.041 n=6)
RLS/Sample-15000/analyzer_types/With_RLS-4          3.798m ± 1%    3.778m ± 2%       ~ (p=0.394 n=6)
RLS/Sample-15000/change_types/Without_RLS-4         5.331m ± 1%    5.335m ± 1%       ~ (p=0.937 n=6)
RLS/Sample-15000/change_types/With_RLS-4            5.495m ± 3%    5.306m ± 1%  -3.45% (p=0.002 n=6)
RLS/Sample-15000/config_classes/Without_RLS-4       4.197m ± 1%    4.151m ± 1%  -1.09% (p=0.026 n=6)
RLS/Sample-15000/config_classes/With_RLS-4          140.8m ± 2%    140.2m ± 2%       ~ (p=0.699 n=6)
RLS/Sample-15000/config_types/Without_RLS-4         4.780m ± 1%    4.853m ± 2%  +1.52% (p=0.026 n=6)
RLS/Sample-15000/config_types/With_RLS-4            137.3m ± 1%    140.4m ± 1%  +2.30% (p=0.002 n=6)
geomean                                             21.16m         21.29m       +0.59%

@moshloop moshloop requested a review from adityathebe May 1, 2026 11:53
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: 5

Caution

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

⚠️ Outside diff range comments (1)
views/006_config_views.sql (1)

1171-1184: ⚠️ Potential issue | 🟠 Major

Add explicit scraper_id to the INSERT statement or use a specific ON CONFLICT target.

The concern is valid. The config_relationships table has a 4-column unique constraint (related_id, config_id, relation, scraper_id) with scraper_id defaulting to 00000000-0000-0000-0000-000000000000. Without explicitly including scraper_id in the INSERT, ON CONFLICT DO NOTHING will only detect conflicts when all 4 columns match. This means a duplicate Alias relationship can exist with the same (config_id, related_id, relation) but a different scraper_id, violating intended uniqueness.

The same pattern exists in two other INSERT statements at lines 1081–1083 and 1093–1095, which also omit scraper_id. Additionally, the DELETE statements at lines 1088–1089 only filter on (config_id, related_id, relation) and miss scraper_id, potentially leaving orphaned rows.

Fix: Either (1) add scraper_id to all three INSERT column lists, or (2) specify an explicit ON CONFLICT (config_id, related_id, relation) DO NOTHING target that excludes scraper_id if relationships are intentionally global across scrapers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@views/006_config_views.sql` around lines 1171 - 1184, The INSERT into
config_relationships omits scraper_id so ON CONFLICT DO NOTHING doesn't match
the table's unique key (related_id, config_id, relation, scraper_id); update the
three INSERT statements that write Alias/other relations to include scraper_id
in the INSERT column list and in the SELECT (e.g., use v_config_item.scraper_id
or the appropriate source/default UUID) so the inserted rows include the correct
scraper_id, and also update the corresponding DELETE statements to include
scraper_id in their WHERE predicates (or alternatively, if relationships are
intended to be global across scrapers, change the ON CONFLICT clause to an
explicit ON CONFLICT (config_id, related_id, relation) DO NOTHING). Ensure
changes apply to the INSERTs/DELETEs that reference config_relationships and use
v_config_item and ci as shown.
🧹 Nitpick comments (1)
Makefile (1)

14-35: 💤 Low value

Consider updating AGENTS.md to reflect the ginkgo → gavel transition.

The switch from ginkgo to gavel for unit tests appears intentional (E2E tests still use ginkgo at line 39). However, the repository learning from AGENTS.md states:

"Always use ginkgo to run tests, never run go test directly"

This documentation should be updated to clarify that:

  • Unit tests now use gavel test
  • E2E tests continue to use ginkgo

Based on learnings, the documented guidance may need to be synchronized with this change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 14 - 35, Update AGENTS.md to reflect the test runner
change: note that unit tests now use "gavel test" (include the equivalent flags
used in the Makefile such as --timeout and --test-timeout and the ignore
patterns or link to the Makefile target), and explicitly state that E2E tests
still use "ginkgo" (keep the existing E2E guidance). Mention the policy change
that contributors should run unit tests with the Makefile "make test" / "make
test-concurrent" which invoke gavel, and keep the existing admonition about not
running "go test" directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@PROPERTIES.md`:
- Around line 9-10: Update the hardcoded absolute file URL in the PROPERTIES.md
link to a repository-relative path so other developers can access it; replace
the link target that currently points to /Users/moshe/go/src/... with a relative
reference like PROPERTIES.schema.json or ./PROPERTIES.schema.json in the line
containing [PROPERTIES.schema.json] to ensure the schema is resolved from the
repo rather than a local filesystem path.

In `@query/config_tree.go`:
- Around line 225-237: getExistingConfigsByIDs currently calls
ConfigItemFromCache for each id which causes a DB fallback per cache miss and
makes the operation O(n) round-trips; instead implement a batch fetch: try
resolving items from cache in bulk (or collect IDs that miss) and then run a
single DB query like WHERE id IN ? AND deleted_at IS NULL to load all missing
ConfigItem rows, merge results preserving the "skip missing rows" behavior, and
return the combined slice; update references in getExistingConfigsByIDs and the
cache-resolve logic (ConfigItemFromCache usage) to perform bulk resolution
rather than per-id queries.

In `@tests/config_access_test.go`:
- Around line 107-131: The test reads rows from the external_group_summary view
into summaries and builds byID; however missing rows make zero-value entries
pass the count asserts for deletedGroup.ID. Before asserting
MembersCount/PermissionsCount, assert the view actually returned the seeded
group by adding an existence check such as Expect(summaries).To(HaveLen(4)) or
Expect(byID).To(HaveKey(deletedGroup.ID)); locate the query that fills summaries
and the byID map in the test and insert the HaveLen/HaveKey assertion
immediately after building byID and before the existing count Expectations.

In `@tests/setup/common.go`:
- Around line 187-190: The current code constructs PgUrl by string replacement
which can corrupt credentials or query params; instead parse the Postgres DSN
with net/url (e.g., url.Parse), set the URL's Path to "/"+dbName and then
reserialize (u.String()) so only the path component changes, and use that value
for PgUrl before calling execPostgres (referencing dbName, PgUrl, execPostgres,
postgresDBUrl); apply the same change to the other occurrence around lines
278-282 so both places update only the URL path.

In `@views/045_merge_external_entities.sql`:
- Around line 291-295: The INSERT into external_user_groups can create duplicate
(external_user_id, external_group_id, scraper_id) rows across scrapers and thus
overcount in external_group_summary; modify the SELECT that feeds the INSERT
(the JOIN between external_user_groups eug and _eu_merges mp using mp.loser_id
-> mp.winner_id) to collapse duplicates before inserting by selecting distinct
tuples (mp.winner_id, eug.external_group_id, eug.scraper_id, eug.created_at) or
by grouping, so only unique scraper-scoped memberships are inserted; apply the
same dedup change to the other mirrored insert at lines 494-498 and/or
alternatively update external_group_summary to COUNT(DISTINCT external_user_id)
per external_group_id if you prefer to de-duplicate at summary time.

---

Outside diff comments:
In `@views/006_config_views.sql`:
- Around line 1171-1184: The INSERT into config_relationships omits scraper_id
so ON CONFLICT DO NOTHING doesn't match the table's unique key (related_id,
config_id, relation, scraper_id); update the three INSERT statements that write
Alias/other relations to include scraper_id in the INSERT column list and in the
SELECT (e.g., use v_config_item.scraper_id or the appropriate source/default
UUID) so the inserted rows include the correct scraper_id, and also update the
corresponding DELETE statements to include scraper_id in their WHERE predicates
(or alternatively, if relationships are intended to be global across scrapers,
change the ON CONFLICT clause to an explicit ON CONFLICT (config_id, related_id,
relation) DO NOTHING). Ensure changes apply to the INSERTs/DELETEs that
reference config_relationships and use v_config_item and ci as shown.

---

Nitpick comments:
In `@Makefile`:
- Around line 14-35: Update AGENTS.md to reflect the test runner change: note
that unit tests now use "gavel test" (include the equivalent flags used in the
Makefile such as --timeout and --test-timeout and the ignore patterns or link to
the Makefile target), and explicitly state that E2E tests still use "ginkgo"
(keep the existing E2E guidance). Mention the policy change that contributors
should run unit tests with the Makefile "make test" / "make test-concurrent"
which invoke gavel, and keep the existing admonition about not running "go test"
directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49a5a897-06cd-4e5d-8969-6fdfdf05dff1

📥 Commits

Reviewing files that changed from the base of the PR and between 7ce008e and b6a1892.

📒 Files selected for processing (23)
  • Makefile
  • PROPERTIES.md
  • PROPERTIES.schema.json
  • Taskfile.yaml
  • models/common.go
  • models/config.go
  • models/config_access.go
  • query/config_tree.go
  • query/config_tree_test.go
  • query/resource_selector.go
  • rbac/objects.go
  • schema/config.hcl
  • schema/config_access.hcl
  • tests/config_access_test.go
  • tests/config_relationship_test.go
  • tests/e2e-blobs/containers.go
  • tests/fixtures/dummy/all.go
  • tests/setup/common.go
  • types/client_options.go
  • views/006_config_views.sql
  • views/038_config_access.sql
  • views/045_merge_external_entities.sql
  • views/9998_rls_enable.sql

Comment thread PROPERTIES.md
Comment thread query/config_tree.go
Comment on lines +225 to +237
func getExistingConfigsByIDs(ctx context.Context, ids []uuid.UUID) ([]models.ConfigItem, error) {
configs := make([]models.ConfigItem, 0, len(ids))
for _, id := range ids {
config, err := ConfigItemFromCache(ctx, id.String())
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return nil, err
}
configs = append(configs, config)
}
return configs, 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 | ⚡ Quick win

This reintroduces one cache/DB lookup per config ID.

ConfigItemFromCache falls back to a SQL query on every cache miss, so this helper turns one parent/child expansion into O(n) round-trips. On larger trees that will dominate the request. Keep the “skip missing rows” behavior, but fetch the ID set in one WHERE id IN ? AND deleted_at IS NULL query or bulk-resolve cache misses instead.

Batch-fetch variant
 func getExistingConfigsByIDs(ctx context.Context, ids []uuid.UUID) ([]models.ConfigItem, error) {
-	configs := make([]models.ConfigItem, 0, len(ids))
-	for _, id := range ids {
-		config, err := ConfigItemFromCache(ctx, id.String())
-		if err != nil {
-			if errors.Is(err, gorm.ErrRecordNotFound) {
-				continue
-			}
-			return nil, err
-		}
-		configs = append(configs, config)
-	}
-	return configs, nil
+	if len(ids) == 0 {
+		return nil, nil
+	}
+	var configs []models.ConfigItem
+	if err := ctx.DB().
+		Where("id IN ?", lo.Uniq(ids)).
+		Where("deleted_at IS NULL").
+		Find(&configs).Error; err != nil {
+		return nil, err
+	}
+	return configs, nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@query/config_tree.go` around lines 225 - 237, getExistingConfigsByIDs
currently calls ConfigItemFromCache for each id which causes a DB fallback per
cache miss and makes the operation O(n) round-trips; instead implement a batch
fetch: try resolving items from cache in bulk (or collect IDs that miss) and
then run a single DB query like WHERE id IN ? AND deleted_at IS NULL to load all
missing ConfigItem rows, merge results preserving the "skip missing rows"
behavior, and return the combined slice; update references in
getExistingConfigsByIDs and the cache-resolve logic (ConfigItemFromCache usage)
to perform bulk resolution rather than per-id queries.

Comment thread tests/config_access_test.go
Comment on lines +291 to +295
INSERT INTO external_user_groups (external_user_id, external_group_id, scraper_id, created_at)
SELECT mp.winner_id, eug.external_group_id, eug.scraper_id, eug.created_at
FROM external_user_groups eug JOIN _eu_merges mp ON eug.external_user_id = mp.loser_id
WHERE eug.deleted_at IS NULL
ON CONFLICT (external_user_id, external_group_id) DO NOTHING;
ON CONFLICT (external_user_id, external_group_id, scraper_id) DO NOTHING;
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 | ⚡ Quick win

Deduplicate member counts across scrapers before preserving scraper-scoped memberships.

With scraper_id now part of the membership key, the same logical user↔group link can exist once per scraper. external_group_summary still does COUNT(*) grouped only by external_group_id in views/038_config_access.sql:225-231, so these inserts will overstate members_count as soon as two scrapers report the same membership. Either collapse duplicates here or make the summary count distinct members per group instead.

Also applies to: 494-498

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@views/045_merge_external_entities.sql` around lines 291 - 295, The INSERT
into external_user_groups can create duplicate (external_user_id,
external_group_id, scraper_id) rows across scrapers and thus overcount in
external_group_summary; modify the SELECT that feeds the INSERT (the JOIN
between external_user_groups eug and _eu_merges mp using mp.loser_id ->
mp.winner_id) to collapse duplicates before inserting by selecting distinct
tuples (mp.winner_id, eug.external_group_id, eug.scraper_id, eug.created_at) or
by grouping, so only unique scraper-scoped memberships are inserted; apply the
same dedup change to the other mirrored insert at lines 494-498 and/or
alternatively update external_group_summary to COUNT(DISTINCT external_user_id)
per external_group_id if you prefer to de-duplicate at summary time.

Comment thread tests/setup/common.go Outdated
return err
}

func randomDatabaseName(prefix string) string {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@moshloop unintentionally removed?

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: 3

♻️ Duplicate comments (1)
tests/setup/common.go (1)

187-188: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace DSN string rewrites with URL path mutation.

Line 188 and Line 282 still rewrite the full URL via string replacement, which can accidentally modify credentials/query fragments or the wrong path segment. Parse with net/url and only set u.Path.

Suggested fix
 import (
 	"database/sql"
 	"fmt"
+	neturl "net/url"
 	"net/http"
 	"os"
 	"path"
 	"strconv"
 	"strings"
@@
 )
+
+func withDatabaseName(rawURL, dbName string) (string, error) {
+	u, err := neturl.Parse(rawURL)
+	if err != nil {
+		return "", err
+	}
+	u.Path = "/" + dbName
+	return u.String(), nil
+}
@@
-		PgUrl = strings.Replace(url, "/postgres", "/"+dbName, 1)
+		newURL, err := withDatabaseName(url, dbName)
+		if err != nil {
+			return context.Context{}, err
+		}
+		PgUrl = newURL
@@
-	config := api.NewConfig(strings.ReplaceAll(pgUrl, pgDbName, newName))
+	newURL, err := withDatabaseName(pgUrl, newName)
+	if err != nil {
+		return nil, nil, err
+	}
+	config := api.NewConfig(newURL)

Also applies to: 282-282

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

In `@tests/setup/common.go` around lines 187 - 188, The code currently builds
dbName and then rewrites PgUrl by string replacement of url (using dbName and
PgUrl variables), which can corrupt credentials or query strings; instead parse
the DSN with net/url (parse the existing url variable into u via url.Parse), set
u.Path = "/"+dbName (or use path.Join with the existing Path if needed), then
assign PgUrl = u.String(); apply the same change to the other occurrence that
rewrites the URL so only the path is mutated and all other URL components
(userinfo, host, query) are preserved.
🧹 Nitpick comments (1)
Makefile (1)

18-35: ⚡ Quick win

Factor shared test ignore flags into one variable.

test and test-concurrent duplicate the same ignore set; centralizing it reduces drift and addresses body-length churn.

♻️ Proposed refactor
+TEST_IGNORES := \
+	--ignore ./bench \
+	--ignore ./hack \
+	--ignore ./specs \
+	--ignore ./tests/e2e \
+	--ignore ./tests/e2e-blobs
+
 test: gavel
 	gavel test --timeout 30m --test-timeout 15m \
-		--ignore ./bench \
-		--ignore ./hack \
-		--ignore ./specs \
-		--ignore ./tests/e2e \
-		--ignore ./tests/e2e-blobs \
+		$(TEST_IGNORES) \
 		./...

 test-concurrent: gavel
 	gavel test --timeout 30m --test-timeout 15m \
 		--nodes 4 \
-		--ignore ./bench \
-		--ignore ./hack \
-		--ignore ./specs \
-		--ignore ./tests/e2e \
-		--ignore ./tests/e2e-blobs \
+		$(TEST_IGNORES) \
 		./...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Makefile` around lines 18 - 35, The duplicated ignore list in the Makefile
targets test and test-concurrent should be moved into a single Makefile variable
(e.g., TEST_IGNORES) and referenced in both targets to avoid drift; update the
test target and the test-concurrent target (which also adds --nodes 4) to
replace the repeated --ignore ... lines with the TEST_IGNORES variable, keeping
the existing flags like --timeout and --test-timeout intact and preserving the
trailing ./... argument.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Makefile`:
- Around line 95-99: The Make recipe for the target labeled "PROPERTIES.md:
captain" runs the pipeline "claude -p ... | captain" without preserving upstream
failures; change the shell invocation to enable pipefail so the recipe fails
when claude fails—either set the makefile shell flags (e.g., ensure SHELL is
bash and add .SHELLFLAGS = -eu -o pipefail -c) or wrap the single recipe line
with bash -o pipefail -c 'claude -p ... | captain' so the exit status reflects
failures from both claude and captain.
- Around line 14-17: Replace the floating `@latest` for gavel with a pinned
version variable: add a GAVEL_VERSION variable (e.g., GAVEL_VERSION := vX.Y.Z)
at the top of the Makefile and update the gavel target to use go install
github.com/flanksource/gavel/cmd/gavel@$(GAVEL_VERSION); ensure the variable is
documented alongside existing tool version variables (like
CONTROLLER_TOOLS_VERSION) so CI is reproducible and easy to update.

In `@tests/setup/common.go`:
- Line 190: The CREATE/DROP DATABASE SQL is building identifiers by direct
string interpolation (see execPostgres usage with postgresDBUrl and dbName),
which is brittle and risky; instead validate the dbName against a safe pattern
(e.g., only letters, digits, underscore) and quote/escape it as a SQL identifier
before formatting the statement. Add a small helper (e.g., quoteIdentifier or
validateAndQuoteDBName) used where you call execPostgres for CREATE DATABASE and
DROP DATABASE to reject invalid names and return the quoted identifier (escape
any internal double-quotes by doubling and wrap in double quotes) and use that
quoted value when building the SQL string. Ensure all places that interpolate
dbName (the CREATE DATABASE and DROP DATABASE calls) use this helper.

---

Duplicate comments:
In `@tests/setup/common.go`:
- Around line 187-188: The code currently builds dbName and then rewrites PgUrl
by string replacement of url (using dbName and PgUrl variables), which can
corrupt credentials or query strings; instead parse the DSN with net/url (parse
the existing url variable into u via url.Parse), set u.Path = "/"+dbName (or use
path.Join with the existing Path if needed), then assign PgUrl = u.String();
apply the same change to the other occurrence that rewrites the URL so only the
path is mutated and all other URL components (userinfo, host, query) are
preserved.

---

Nitpick comments:
In `@Makefile`:
- Around line 18-35: The duplicated ignore list in the Makefile targets test and
test-concurrent should be moved into a single Makefile variable (e.g.,
TEST_IGNORES) and referenced in both targets to avoid drift; update the test
target and the test-concurrent target (which also adds --nodes 4) to replace the
repeated --ignore ... lines with the TEST_IGNORES variable, keeping the existing
flags like --timeout and --test-timeout intact and preserving the trailing ./...
argument.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4595ddc7-d351-4165-9a33-ab4af4713077

📥 Commits

Reviewing files that changed from the base of the PR and between b6a1892 and edfdbbb.

📒 Files selected for processing (23)
  • Makefile
  • PROPERTIES.md
  • PROPERTIES.schema.json
  • Taskfile.yaml
  • models/common.go
  • models/config.go
  • models/config_access.go
  • query/config_tree.go
  • query/config_tree_test.go
  • query/resource_selector.go
  • rbac/objects.go
  • schema/config.hcl
  • schema/config_access.hcl
  • tests/config_access_test.go
  • tests/config_relationship_test.go
  • tests/e2e-blobs/containers.go
  • tests/fixtures/dummy/all.go
  • tests/setup/common.go
  • types/client_options.go
  • views/006_config_views.sql
  • views/038_config_access.sql
  • views/045_merge_external_entities.sql
  • views/9998_rls_enable.sql
✅ Files skipped from review due to trivial changes (5)
  • views/9998_rls_enable.sql
  • tests/e2e-blobs/containers.go
  • views/045_merge_external_entities.sql
  • PROPERTIES.schema.json
  • query/config_tree_test.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • types/client_options.go
  • schema/config_access.hcl
  • schema/config.hcl
  • models/config.go
  • Taskfile.yaml
  • query/config_tree.go
  • tests/config_access_test.go
  • views/038_config_access.sql

Comment thread Makefile Outdated
Comment thread Makefile Outdated
Comment thread tests/setup/common.go
dbName = fmt.Sprintf("duty_gingko%d", port)
PgUrl = strings.Replace(url, "/postgres", "/"+dbName, 1)
_ = execPostgres(postgresDBUrl, "DROP DATABASE "+dbName)
if err := execPostgres(postgresDBUrl, "CREATE DATABASE "+dbName); err != 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 | ⚡ Quick win

Avoid raw identifier interpolation in CREATE/DROP DATABASE SQL.

Line 190, Line 195, Line 278, and Line 299 interpolate DB names directly into SQL. Even in test code, this is brittle (identifier syntax) and opens injection risk if names ever become unsanitized. Validate and quote identifiers before formatting SQL.

Suggested fix
 import (
 	"database/sql"
 	"fmt"
 	"net/http"
 	"os"
 	"path"
+	"regexp"
 	"strconv"
 	"strings"
@@
 )
+
+var validDBIdentifier = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
+
+func quoteDBIdentifier(name string) (string, error) {
+	if !validDBIdentifier.MatchString(name) {
+		return "", fmt.Errorf("invalid database identifier: %q", name)
+	}
+	return `"` + name + `"`, nil
+}
@@
-		_ = execPostgres(postgresDBUrl, "DROP DATABASE "+dbName)
-		if err := execPostgres(postgresDBUrl, "CREATE DATABASE "+dbName); err != nil {
+		qdb, err := quoteDBIdentifier(dbName)
+		if err != nil {
+			return context.Context{}, err
+		}
+		_ = execPostgres(postgresDBUrl, "DROP DATABASE "+qdb)
+		if err := execPostgres(postgresDBUrl, "CREATE DATABASE "+qdb); err != nil {
 			return context.Context{}, fmt.Errorf("cannot create %s: %v", dbName, err)
 		}
@@
-			if err := execPostgres(postgresDBUrl, fmt.Sprintf("DROP DATABASE %s (FORCE)", dbName)); err != nil {
+			if err := execPostgres(postgresDBUrl, fmt.Sprintf("DROP DATABASE %s (FORCE)", qdb)); err != nil {
 				logger.Errorf("execPostgres: %v", err)
 			}
@@
-	if err := ctx.DB().Exec(fmt.Sprintf("CREATE DATABASE %s", newName)).Error; err != nil {
+	qNewName, err := quoteDBIdentifier(newName)
+	if err != nil {
+		return nil, nil, err
+	}
+	if err := ctx.DB().Exec(fmt.Sprintf("CREATE DATABASE %s", qNewName)).Error; err != nil {
 		return nil, nil, err
 	}
@@
-		if err := ctx.DB().Exec(fmt.Sprintf("DROP DATABASE  %s (FORCE)", newName)).Error; err != nil {
+		if err := ctx.DB().Exec(fmt.Sprintf("DROP DATABASE %s (FORCE)", qNewName)).Error; err != nil {
 			logger.Errorf("error cleaning up db: %v", err)
 		}

Also applies to: 195-195, 278-278, 299-299

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

In `@tests/setup/common.go` at line 190, The CREATE/DROP DATABASE SQL is building
identifiers by direct string interpolation (see execPostgres usage with
postgresDBUrl and dbName), which is brittle and risky; instead validate the
dbName against a safe pattern (e.g., only letters, digits, underscore) and
quote/escape it as a SQL identifier before formatting the statement. Add a small
helper (e.g., quoteIdentifier or validateAndQuoteDBName) used where you call
execPostgres for CREATE DATABASE and DROP DATABASE to reject invalid names and
return the quoted identifier (escape any internal double-quotes by doubling and
wrap in double quotes) and use that quoted value when building the SQL string.
Ensure all places that interpolate dbName (the CREATE DATABASE and DROP DATABASE
calls) use this helper.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

Gavel results

Gavel exited with code .

View full results

moshloop added 2 commits May 5, 2026 23:07
…r groups

Introduce scraper_id as part of composite primary keys for ConfigRelationship and ExternalUserGroup to support multi-scraper ownership. This allows multiple scrapers to independently maintain relationship tuples without colliding, while uuid.Nil represents legacy scraper-agnostic relationships.

Add Health.Badge() method to render health status as colored pills for dense table cells. Implement MergeConfigTrees() to collapse independently-built config trees into a unified forest by merging shared ancestors recursively.

Enhance SearchResources to populate Health and Status fields for configs and components. Add RoleExternalIDs to ConfigAccessSummary. Create external_group_summary view with member and permission counts.

Update SQL views to filter deleted relationships and add GROUP BY clauses for accurate counts. Simplify database setup helpers by removing unnecessary quoting and URL parsing utilities.

BREAKING CHANGE: ConfigRelationship and ExternalUserGroup primary keys now include scraper_id; existing code must populate this field from scraper context.
Replace ginkgo-based test targets with gavel, a unified test runner that provides better output formatting and cross-platform compatibility. Update test and test-concurrent targets to use gavel with consistent timeout and ignore patterns. Add gavel installation target and captain target for properties documentation generation. Add PROPERTIES.md and PROPERTIES.schema.json documenting all runtime properties across duty and related services. Update Taskfile.yaml test task to use gavel with passthrough argument support.
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: 2

♻️ Duplicate comments (4)
PROPERTIES.md (1)

10-10: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace local absolute schema link with a repo-relative link

Line 10 points to a machine-local absolute path, which breaks for everyone else. Use a repository-relative path (./PROPERTIES.schema.json).

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

In `@PROPERTIES.md` at line 10, The markdown link in PROPERTIES.md currently
points to a machine-local absolute path
"[PROPERTIES.schema.json](/Users/moshe/go/src/github.com/flanksource/duty/PROPERTIES.schema.json)";
change that link to a repository-relative path by replacing the absolute URL
with "./PROPERTIES.schema.json" so the link works for all contributors.
tests/setup/common.go (1)

189-190: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use validated/quoted SQL identifiers for database names

Line 189, Line 190, Line 195, Line 278, and Line 299 still build CREATE/DROP DATABASE SQL with direct name interpolation. Even in test utilities, this is brittle for identifier syntax and keeps an injection footgun. Please route all DB-name SQL through one helper that validates and quotes identifiers.

Proposed minimal fix
+var validDBIdentifier = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
+
+func quoteDBIdentifier(name string) (string, error) {
+	if !validDBIdentifier.MatchString(name) {
+		return "", fmt.Errorf("invalid database identifier: %q", name)
+	}
+	return `"` + name + `"`, nil
+}
@@
-		_ = execPostgres(postgresDBUrl, "DROP DATABASE "+dbName)
-		if err := execPostgres(postgresDBUrl, "CREATE DATABASE "+dbName); err != nil {
+		qdb, err := quoteDBIdentifier(dbName)
+		if err != nil {
+			return context.Context{}, err
+		}
+		_ = execPostgres(postgresDBUrl, "DROP DATABASE "+qdb)
+		if err := execPostgres(postgresDBUrl, "CREATE DATABASE "+qdb); err != nil {
@@
-			if err := execPostgres(postgresDBUrl, fmt.Sprintf("DROP DATABASE %s (FORCE)", dbName)); err != nil {
+			if err := execPostgres(postgresDBUrl, fmt.Sprintf("DROP DATABASE %s (FORCE)", qdb)); err != nil {
@@
-	if err := ctx.DB().Exec(fmt.Sprintf("CREATE DATABASE %s", newName)).Error; err != nil {
+	qNewName, err := quoteDBIdentifier(newName)
+	if err != nil {
+		return nil, nil, err
+	}
+	if err := ctx.DB().Exec(fmt.Sprintf("CREATE DATABASE %s", qNewName)).Error; err != nil {
@@
-		if err := ctx.DB().Exec(fmt.Sprintf("DROP DATABASE  %s (FORCE)", newName)).Error; err != nil {
+		if err := ctx.DB().Exec(fmt.Sprintf("DROP DATABASE %s (FORCE)", qNewName)).Error; err != nil {

Also applies to: 195-195, 278-278, 299-299

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

In `@tests/setup/common.go` around lines 189 - 190, Replace all direct string
concatenation that builds "CREATE DATABASE "+dbName and "DROP DATABASE "+dbName
with a single helper (e.g., validateAndQuoteIdentifier or quoteSQLIdentifier)
that validates the identifier characters and returns a properly quoted
identifier; update every call site that calls execPostgres(postgresDBUrl,
"CREATE DATABASE "+dbName) or execPostgres(postgresDBUrl, "DROP DATABASE
"+dbName) to use execPostgres(postgresDBUrl, "CREATE DATABASE
"+quoteSQLIdentifier(dbName)) and similarly for DROP, and ensure the helper is
used for all occurrences in tests/setup/common.go (references: execPostgres,
dbName, CREATE DATABASE, DROP DATABASE).
Makefile (1)

96-98: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve failures from the claude | captain pipeline

Line 96-98 currently allows a successful captain exit to hide an upstream claude failure. Run this recipe with pipefail so either side failing fails the target.

Proposed fix
-	claude -p --permission-mode acceptEdits --verbose --output-format stream-json --model sonnet \
-		"/properties-doc Refresh PROPERTIES.md and PROPERTIES.schema.json from the current source tree. Cross-reference ../incident-commander, ../config-db, ../canary-checker, ../flanksource-ui, and ../commons when present. Run in update mode and preserve hand-written prose sections." \
-		| captain
+	bash -o pipefail -c 'claude -p --permission-mode acceptEdits --verbose --output-format stream-json --model sonnet \
+		"/properties-doc Refresh PROPERTIES.md and PROPERTIES.schema.json from the current source tree. Cross-reference ../incident-commander, ../config-db, ../canary-checker, ../flanksource-ui, and ../commons when present. Run in update mode and preserve hand-written prose sections." \
+		| captain'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Makefile` around lines 96 - 98, The Makefile recipe that runs the pipeline
"claude -p ... | captain" hides upstream failures; update that recipe so the
shell runs with pipefail enabled (e.g., prefix the recipe with set -o pipefail
or invoke bash with -o pipefail) so any nonzero exit from the claude stage
causes the target to fail; modify the Makefile entry containing the "claude -p
--permission-mode acceptEdits ... | captain" command accordingly.
tests/config_access_test.go (1)

119-131: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add an existence assertion before zero-value count checks.

Line 130 and Line 131 can pass even if deletedGroup.ID is absent, because map lookup returns a zero-value struct. Assert presence first (HaveKey) or exact row count.

Proposed test hardening
 		byID := map[uuid.UUID]externalGroupSummary{}
 		for _, summary := range summaries {
 			byID[summary.ID] = summary
 		}
+		Expect(byID).To(HaveKey(dummy.MissionControlAdminsGroup.ID))
+		Expect(byID).To(HaveKey(dummy.MissionControlReadersGroup.ID))
+		Expect(byID).To(HaveKey(dummy.MissionControlEmptyGroup.ID))
+		Expect(byID).To(HaveKey(deletedGroup.ID))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/config_access_test.go` around lines 119 - 131, The test currently
indexes into byID and asserts counts for deletedGroup.ID which will succeed with
zero-values if the key is missing; before checking MembersCount/PermissionsCount
for deletedGroup.ID, add an explicit existence assertion such as
Expect(byID).To(HaveKey(deletedGroup.ID)) (or
Expect(summaries).To(ContainElement(...)) / assert exact row count) so the test
fails when the summary is absent rather than silently passing on zero-values;
update the checks around byID, deletedGroup.ID to include this HaveKey
assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/setup/common.go`:
- Line 282: Replace the blind strings.ReplaceAll used when constructing config
(the call site creating config := api.NewConfig(strings.ReplaceAll(pgUrl,
pgDbName, newName))) with proper URL parsing: parse pgUrl (using net/url),
update only the path component that holds the database name (e.g., swap the path
segment equal to pgDbName to newName or set u.Path = "/" + newName), then
reconstruct the URL (u.String()) and pass that into api.NewConfig so only the DB
path is changed and usernames/query values remain untouched.

In `@views/038_config_access.sql`:
- Around line 16-20: The CTE active_external_user_groups and all
joins/aggregations that currently match only on external_group_id must include
scraper_id to prevent cross-scraper mixing: update the CTE SELECT DISTINCT to
include external_user_id, external_group_id AND scraper_id (from
external_user_groups), add scraper_id to every JOIN predicate that currently
matches on external_group_id alone, and add scraper_id to all GROUP BY clauses
and aggregation keys (the queries that aggregate by group id must aggregate by
(external_group_id, scraper_id)). Ensure every reference to
external_user_groups/external_group_id in this view (including the blocks
derived from active_external_user_groups and the aggregations around group
membership/grants) is adjusted to carry and join on scraper_id.

---

Duplicate comments:
In `@Makefile`:
- Around line 96-98: The Makefile recipe that runs the pipeline "claude -p ... |
captain" hides upstream failures; update that recipe so the shell runs with
pipefail enabled (e.g., prefix the recipe with set -o pipefail or invoke bash
with -o pipefail) so any nonzero exit from the claude stage causes the target to
fail; modify the Makefile entry containing the "claude -p --permission-mode
acceptEdits ... | captain" command accordingly.

In `@PROPERTIES.md`:
- Line 10: The markdown link in PROPERTIES.md currently points to a
machine-local absolute path
"[PROPERTIES.schema.json](/Users/moshe/go/src/github.com/flanksource/duty/PROPERTIES.schema.json)";
change that link to a repository-relative path by replacing the absolute URL
with "./PROPERTIES.schema.json" so the link works for all contributors.

In `@tests/config_access_test.go`:
- Around line 119-131: The test currently indexes into byID and asserts counts
for deletedGroup.ID which will succeed with zero-values if the key is missing;
before checking MembersCount/PermissionsCount for deletedGroup.ID, add an
explicit existence assertion such as Expect(byID).To(HaveKey(deletedGroup.ID))
(or Expect(summaries).To(ContainElement(...)) / assert exact row count) so the
test fails when the summary is absent rather than silently passing on
zero-values; update the checks around byID, deletedGroup.ID to include this
HaveKey assertion.

In `@tests/setup/common.go`:
- Around line 189-190: Replace all direct string concatenation that builds
"CREATE DATABASE "+dbName and "DROP DATABASE "+dbName with a single helper
(e.g., validateAndQuoteIdentifier or quoteSQLIdentifier) that validates the
identifier characters and returns a properly quoted identifier; update every
call site that calls execPostgres(postgresDBUrl, "CREATE DATABASE "+dbName) or
execPostgres(postgresDBUrl, "DROP DATABASE "+dbName) to use
execPostgres(postgresDBUrl, "CREATE DATABASE "+quoteSQLIdentifier(dbName)) and
similarly for DROP, and ensure the helper is used for all occurrences in
tests/setup/common.go (references: execPostgres, dbName, CREATE DATABASE, DROP
DATABASE).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 76e9c536-d66a-43eb-b726-b3370de1568c

📥 Commits

Reviewing files that changed from the base of the PR and between edfdbbb and 10a4417.

📒 Files selected for processing (23)
  • Makefile
  • PROPERTIES.md
  • PROPERTIES.schema.json
  • Taskfile.yaml
  • models/common.go
  • models/config.go
  • models/config_access.go
  • query/config_tree.go
  • query/config_tree_test.go
  • query/resource_selector.go
  • rbac/objects.go
  • schema/config.hcl
  • schema/config_access.hcl
  • tests/config_access_test.go
  • tests/config_relationship_test.go
  • tests/e2e-blobs/containers.go
  • tests/fixtures/dummy/all.go
  • tests/setup/common.go
  • types/client_options.go
  • views/006_config_views.sql
  • views/038_config_access.sql
  • views/045_merge_external_entities.sql
  • views/9998_rls_enable.sql
✅ Files skipped from review due to trivial changes (3)
  • views/9998_rls_enable.sql
  • models/common.go
  • tests/e2e-blobs/containers.go
🚧 Files skipped from review as they are similar to previous changes (11)
  • views/045_merge_external_entities.sql
  • models/config_access.go
  • query/resource_selector.go
  • schema/config.hcl
  • rbac/objects.go
  • schema/config_access.hcl
  • query/config_tree.go
  • views/006_config_views.sql
  • query/config_tree_test.go
  • models/config.go
  • PROPERTIES.schema.json

Comment thread tests/setup/common.go
}

config := api.NewConfig(newPgURL)
config := api.NewConfig(strings.ReplaceAll(pgUrl, pgDbName, newName))
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 | ⚡ Quick win

Avoid global string replacement when switching DB name in DSN

Line 282 uses strings.ReplaceAll(pgUrl, pgDbName, newName), which can modify unintended URL parts (e.g., username/query values containing the old DB name). Parse the URL and replace only the path/database segment.

Proposed fix
-	config := api.NewConfig(strings.ReplaceAll(pgUrl, pgDbName, newName))
+	u, err := neturl.Parse(pgUrl)
+	if err != nil {
+		return nil, nil, err
+	}
+	u.Path = "/" + newName
+	config := api.NewConfig(u.String())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/setup/common.go` at line 282, Replace the blind strings.ReplaceAll used
when constructing config (the call site creating config :=
api.NewConfig(strings.ReplaceAll(pgUrl, pgDbName, newName))) with proper URL
parsing: parse pgUrl (using net/url), update only the path component that holds
the database name (e.g., swap the path segment equal to pgDbName to newName or
set u.Path = "/" + newName), then reconstruct the URL (u.String()) and pass that
into api.NewConfig so only the DB path is changed and usernames/query values
remain untouched.

Comment on lines +16 to +20
WITH active_external_user_groups AS (
SELECT DISTINCT external_user_id, external_group_id
FROM external_user_groups
WHERE deleted_at IS NULL
)
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 | 🏗️ Heavy lift

Scope joins and aggregations by scraper_id to avoid cross-scraper access mixing.

Line 36 and Line 58 match only on external_group_id, and Line 230 / Line 238 aggregate only by group ID. With scraper-scoped membership/grant data, this can blend rows across scrapers and inflate/misattribute members/permissions.

Suggested direction
 WITH active_external_user_groups AS (
-  SELECT DISTINCT external_user_id, external_group_id
+  SELECT DISTINCT external_user_id, external_group_id, scraper_id
   FROM external_user_groups
   WHERE deleted_at IS NULL
 )
 ...
-  INNER JOIN active_external_user_groups ON config_access.external_group_id = active_external_user_groups.external_group_id
+  INNER JOIN active_external_user_groups
+    ON config_access.external_group_id = active_external_user_groups.external_group_id
+   AND config_access.scraper_id = active_external_user_groups.scraper_id
 ...
   AND NOT EXISTS (
     SELECT 1 FROM active_external_user_groups
-    WHERE active_external_user_groups.external_group_id = config_access.external_group_id
+    WHERE active_external_user_groups.external_group_id = config_access.external_group_id
+      AND active_external_user_groups.scraper_id = config_access.scraper_id
   )
 LEFT JOIN (
   SELECT
     external_user_groups.external_group_id,
+    external_user_groups.scraper_id,
     COUNT(*) AS members_count
   FROM external_user_groups
   WHERE external_user_groups.deleted_at IS NULL
-  GROUP BY external_user_groups.external_group_id
-) group_members ON group_members.external_group_id = external_groups.id
+  GROUP BY external_user_groups.external_group_id, external_user_groups.scraper_id
+) group_members
+  ON group_members.external_group_id = external_groups.id
+ AND group_members.scraper_id = external_groups.scraper_id
 LEFT JOIN (
   SELECT
     config_access_summary.external_group_id,
+    -- include scraper scope in source view or switch source to one that already has scraper_id
+    config_access_summary.scraper_id,
     COUNT(*) AS permissions_count
   FROM config_access_summary
   WHERE config_access_summary.external_group_id IS NOT NULL
-  GROUP BY config_access_summary.external_group_id
-) group_permissions ON group_permissions.external_group_id = external_groups.id;
+  GROUP BY config_access_summary.external_group_id, config_access_summary.scraper_id
+) group_permissions
+  ON group_permissions.external_group_id = external_groups.id
+ AND group_permissions.scraper_id = external_groups.scraper_id;

Also applies to: 36-37, 57-59, 229-235, 237-243

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

In `@views/038_config_access.sql` around lines 16 - 20, The CTE
active_external_user_groups and all joins/aggregations that currently match only
on external_group_id must include scraper_id to prevent cross-scraper mixing:
update the CTE SELECT DISTINCT to include external_user_id, external_group_id
AND scraper_id (from external_user_groups), add scraper_id to every JOIN
predicate that currently matches on external_group_id alone, and add scraper_id
to all GROUP BY clauses and aggregation keys (the queries that aggregate by
group id must aggregate by (external_group_id, scraper_id)). Ensure every
reference to external_user_groups/external_group_id in this view (including the
blocks derived from active_external_user_groups and the aggregations around
group membership/grants) is adjusted to carry and join on scraper_id.

Error: ../../../go/pkg/mod/github.com/flanksource/gavel@v0.0.35/testrunner/ui/assets.go:5:12: pattern dist/testui.js: no matching files found
Error: ../../../go/pkg/mod/github.com/flanksource/gavel@v0.0.35/pr/ui/assets.go:5:12: pattern dist/prui.js: no matching files found
make: *** [Makefile:16: gavel] Error 1
Error: Process completed with exit code 2.
@adityathebe
Copy link
Copy Markdown
Member

https://github.com/flanksource/duty/pull/1946/changes

was merged and it has all the changes from this PR

@adityathebe adityathebe closed this May 8, 2026
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