Skip to content

fix(resource): reconcile #project/#owner relations on update#1686

Open
whoAbhishekSah wants to merge 2 commits into
mainfrom
fix/resource-update-reconcile
Open

fix(resource): reconcile #project/#owner relations on update#1686
whoAbhishekSah wants to merge 2 commits into
mainfrom
fix/resource-update-reconcile

Conversation

@whoAbhishekSah

Copy link
Copy Markdown
Member

What

resource.Update was DB-only and persisted just title/metadata. Moving a resource to a new project or reassigning its owner was therefore a silent no-op: the change was dropped from the row, and the old #project / #owner SpiceDB tuples stayed in place while the new ones were never written.

Fix

Update now:

  • persists project_id, principal_id, principal_type, and a recomputed URN (the URN embeds the project name). name and namespace stay immutable on update;
  • when the project changes, deletes the old app/<ns>:<id>#project tuple and writes the new one;
  • when the owner changes, deletes the old #owner tuple and writes the new one;
  • resolves PAT owners to the underlying user, mirroring Create.

Reconciliation is scoped to the #project / #owner relations — resource grant tuples (#granted rolebindings) are left untouched.

Test

New e2e regression test TestProjectResourceUpdateReconcile: create a resource in project A, move it to project B, then assert via ListRelations that the #project relation now points at B (old A tuple gone). Verified it fails without the reconcile (relation stays on A) and passes with it.

Addresses gap (7) of #1661.

@vercel

vercel Bot commented Jun 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Jun 11, 2026 8:42am

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Fixed resource update operations to properly maintain project and owner associations.
    • Ensured resource permissions and relationships are correctly synchronized when updating resource metadata.
    • Resources can now be moved between projects with automatic relationship reconciliation.
    • Improved owner preservation during resource updates when no new owner is specified.

Walkthrough

Service.Update method is reworked from a simple repository passthrough into a full resource reconciliation operation that loads the existing resource, resolves PAT principals, preserves current owner when not supplied, fetches the target project to set URN and immutable fields, and reconciles SpiceDB project and owner relations when those values change. Repository persistence, unit tests, and end-to-end regression test validate the behavior.

Changes

Resource Update with SpiceDB Reconciliation

Layer / File(s) Summary
Repository Update Field Persistence
internal/store/postgres/resource_repository.go, internal/store/postgres/resource_repository_test.go
Repository.Update now persists URN, ProjectID, PrincipalID, and PrincipalType columns in addition to title and metadata. Test payload expanded to provide complete resource fields for update input.
Service Update Reconciliation Logic
core/resource/service.go, core/resource/service_test.go
Service.Update loads existing resource, resolves PAT principals to underlying users, preserves current owner when not supplied, fetches project to set URN and immutable fields, updates via repository, and reconciles SpiceDB #project and owner relations when those values change. Unit tests verify metadata-only updates skip relation changes and project changes trigger relation reconciliation.
End-to-End Integration Test
test/e2e/regression/api_test.go
TestProjectResourceUpdateReconcile creates resource under project A, confirms #project relation points to A, updates resource to project B, and asserts relation now points to B only.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes


Suggested reviewers

  • rohilsurana
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
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.


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.

resource.Update was DB-only and persisted just title/metadata, so moving a
resource to a new project or reassigning its owner was a silent no-op: the
change was dropped from the row and the old #project/#owner SpiceDB tuples
stayed put while the new ones were never written.

Update now:
- persists project_id, principal_id, principal_type, and a recomputed URN
  (name and namespace remain immutable on update);
- when the project changes, deletes the old app/<ns>:<id>#project tuple and
  writes the new one;
- when the owner changes, deletes the old #owner tuple and writes the new one.

PAT owners are resolved to the underlying user, mirroring Create.

Adds an e2e regression test: create a resource in project A, move it to
project B, assert the #project relation now points at B (old tuple gone).
Verified it fails without the reconcile (relation stays on A) and passes
with it.

Refs #1661

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@whoAbhishekSah

Copy link
Copy Markdown
Member Author

Manual verification ✅

Verified end-to-end against a local build (live ConnectRPC + SpiceDB) as super admin. Setup: one org with two projects (A, B), a third project in a second org, plus user / service-user / PAT owners and a custom resource namespace.

One gotcha worth recording: the resource-mutation RPCs run an authz precheck for the update verb, so the namespace must actually define update (custom namespaces from CreatePermission only have the verbs you create). With a namespace missing update, every call 500s at the interceptor before Update runs.

Core reconcile (the PR's purpose)

Case Result
Move project A→B #project tuple repointed to B (old A tuple gone), DB project_id = B, URN recomputed to embed B's name ✅
Reassign owner U1→U2 (same project) #owner repointed to U2 (U1 gone), #project untouched ✅
Move project and change owner in one call both reconciled ✅
No-op update (same project, no principal) no relation churn; existing owner kept ✅
Partial payload (metadata only) owner + project preserved ✅
name / namespace on update immutable; URN keeps the original name ✅
#granted rolebinding across a move survives untouched (matches the PR's stated scoping) ✅

Ownership

Owner form Result
app/user:<id> #owner set/reassigned correctly ✅
app/serviceuser:<id> #owner set to the service user ✅
app/pat:<id> resolved to the underlying user (mirrors Create) ✅
empty principal keeps the existing owner ✅
bogus principal (garbageNoColon) clean InvalidArgument (non-UUID) ✅

All core paths behave as intended — project moves and owner reassignments now persist in both Postgres and SpiceDB, the URN is recomputed, and grant tuples are left alone.

Pre-existing issues found (NOT introduced by this PR)

A few rough edges in UpdateProjectResource / the resource authz path surfaced during testing — tracked separately in #1697. Most predate this PR; the most relevant one is that the handler doesn't forward body.title, so an update silently clears the title — which is in the same RPC this PR reworks, so it may be worth folding in here.

The resource repository Update now persists urn, project_id, principal_id,
and principal_type (not just title/metadata), so the test must pass a
complete resource. Passing an empty principal_id wrote an invalid value
into the uuid column and failed. Refs #1661.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 27334871464

Coverage increased (+0.01%) to 43.295%

Details

  • Coverage increased (+0.01%) from the base build.
  • Patch coverage: 28 uncovered changes across 1 file (36 of 64 lines covered, 56.25%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
core/resource/service.go 58 30 51.72%
Total (2 files) 64 36 56.25%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 37205
Covered Lines: 16108
Line Coverage: 43.3%
Coverage Strength: 12.29 hits per line

💛 - Coveralls

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
core/resource/service_test.go (1)

470-518: ⚡ Quick win

Consider adding test coverage for owner reconciliation.

The unit tests verify project reconciliation but don't cover owner change reconciliation (lines 236-246 in service.go). Consider adding a test case that changes PrincipalID/PrincipalType and asserts the old #owner relation is deleted and new one created.

Similarly, testing PAT principal resolution on Update (lines 188-195 in service.go) would increase confidence that the "mirrors Create" behavior works correctly.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9b3965eb-72e9-4f79-9aa8-086e3ca162c0

📥 Commits

Reviewing files that changed from the base of the PR and between 2834886 and d6a4f38.

📒 Files selected for processing (5)
  • core/resource/service.go
  • core/resource/service_test.go
  • internal/store/postgres/resource_repository.go
  • internal/store/postgres/resource_repository_test.go
  • test/e2e/regression/api_test.go

Comment on lines +193 to +194
"principal_id": res.PrincipalID,
"principal_type": res.PrincipalType,

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

Inconsistent NULL handling for principal_id/principal_type.

Create wraps principal_id and principal_type in sql.NullString (lines 51-52) to store NULL when empty, but Update writes them as raw strings. If the service passes empty strings, the DB will store "" instead of NULL, breaking consistency with Create behavior and potentially causing filter/query issues.

🛡️ Proposed fix to match Create's NULL handling
+	principalID := sql.NullString{String: res.PrincipalID, Valid: res.PrincipalID != ""}
+	principalType := sql.NullString{String: res.PrincipalType, Valid: res.PrincipalType != ""}
+
 	query, params, err := dialect.Update(TABLE_RESOURCES).Set(
 		goqu.Record{
 			"title":          res.Title,
 			"metadata":       marshaledMetadata,
 			"urn":            res.URN,
 			"project_id":     res.ProjectID,
-			"principal_id":   res.PrincipalID,
-			"principal_type": res.PrincipalType,
+			"principal_id":   principalID,
+			"principal_type": principalType,
 		},
 	).Where(goqu.Ex{"id": res.ID}).Returning(&ResourceCols{}).ToSQL()
📝 Committable suggestion

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

Suggested change
"principal_id": res.PrincipalID,
"principal_type": res.PrincipalType,
principalID := sql.NullString{String: res.PrincipalID, Valid: res.PrincipalID != ""}
principalType := sql.NullString{String: res.PrincipalType, Valid: res.PrincipalType != ""}
query, params, err := dialect.Update(TABLE_RESOURCES).Set(
goqu.Record{
"title": res.Title,
"metadata": marshaledMetadata,
"urn": res.URN,
"project_id": res.ProjectID,
"principal_id": principalID,
"principal_type": principalType,
},
).Where(goqu.Ex{"id": res.ID}).Returning(&ResourceCols{}).ToSQL()

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