fix(resource): reconcile #project/#owner relations on update#1686
fix(resource): reconcile #project/#owner relations on update#1686whoAbhishekSah wants to merge 2 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughSummary by CodeRabbitRelease Notes
WalkthroughService.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. ChangesResource Update with SpiceDB Reconciliation
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ 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. Comment |
76e7d0c to
8ea5f1d
Compare
8ea5f1d to
95452af
Compare
95452af to
3dd0010
Compare
3dd0010 to
2b63dda
Compare
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>
2b63dda to
3e08168
Compare
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.
Core reconcile (the PR's purpose)
Ownership
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 |
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>
Coverage Report for CI Build 27334871464Coverage increased (+0.01%) to 43.295%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
core/resource/service_test.go (1)
470-518: ⚡ Quick winConsider 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/PrincipalTypeand asserts the old#ownerrelation 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
📒 Files selected for processing (5)
core/resource/service.gocore/resource/service_test.gointernal/store/postgres/resource_repository.gointernal/store/postgres/resource_repository_test.gotest/e2e/regression/api_test.go
| "principal_id": res.PrincipalID, | ||
| "principal_type": res.PrincipalType, |
There was a problem hiding this comment.
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.
| "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() |
What
resource.Updatewas DB-only and persisted justtitle/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/#ownerSpiceDB tuples stayed in place while the new ones were never written.Fix
Updatenow:project_id,principal_id,principal_type, and a recomputedURN(the URN embeds the project name).nameandnamespacestay immutable on update;app/<ns>:<id>#projecttuple and writes the new one;#ownertuple and writes the new one;Create.Reconciliation is scoped to the
#project/#ownerrelations — resource grant tuples (#grantedrolebindings) are left untouched.Test
New e2e regression test
TestProjectResourceUpdateReconcile: create a resource in project A, move it to project B, then assert viaListRelationsthat the#projectrelation 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.