feat(store): audit project creation and deletion - #1955
rohanchkrabrty wants to merge 4 commits into
Conversation
Record project.created and project.deleted in the same transaction as the project write, so a project cannot be created or deleted without its audit record. The record sits on the project's org, with the org title read back in the same statement, and targets the project with its slug in the target metadata so a deleted project stays identifiable. Delete now returns the removed row for the record. Deleting a project that does not exist still succeeds and writes nothing, as before.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 📝 SummarySummary by CodeRabbit
WalkthroughProject creation and deletion now return the project and organization title, then write the corresponding audit record in the same transaction. The records identify the organization as the resource and the project as the target. Deleting a nonexistent project succeeds without writing an audit record. ChangesProject audit records
Priority: ⬇️ Low Estimated code review effort: 2 (Simple) | ~15 minutes Change: Feature Merge Risk: 🟡 Moderate · up to Project creation and deletion can remain stuck on audit database work when callers have no deadline, potentially holding transactions open. Add a bounded timeout before merging or explicitly accept that risk. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to Transactional auditing improves the integrity of project history. The remaining risks concern failure containment: audit work can outlive the normal database query timeout, and an audit failure during deletion can leave a project after its policies and resources have already been removed. No new authorization bypass was established. Retained concerns
Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Hardening Proposals
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
internal/store/postgres/project_repository_test.go (1)
121-135: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winAssert the organization resource ID and target metadata.
auditCountchecks only the event, project ID, and resource type. Both tests can pass when the persisted audit record has the wrongresource_idor lacks the project'snameintarget_metadata. Extend the helper to include both fields.Suggested fix
-func (s *ProjectRepositoryTestSuite) auditCount(event pkgAuditRecord.Event, projectID string) int { +func (s *ProjectRepositoryTestSuite) auditCount(event pkgAuditRecord.Event, projectID, organizationID, projectName string) int { var n int err := s.client.QueryRowxContext(s.ctx, fmt.Sprintf( - "SELECT count(*) FROM %s WHERE event = $1 AND target_id = $2 AND resource_type = $3", postgres.TABLE_AUDITRECORDS), - event.String(), projectID, pkgAuditRecord.OrganizationType.String()).Scan(&n) + "SELECT count(*) FROM %s WHERE event = $1 AND target_id = $2 AND resource_type = $3 AND resource_id = $4 AND target_metadata->>'name' = $5", postgres.TABLE_AUDITRECORDS), + event.String(), projectID, pkgAuditRecord.OrganizationType.String(), organizationID, projectName).Scan(&n) s.Require().NoError(err) return n }
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: raystack/frontier/.coderabbit.yaml
Review profile: CHILL
Plan: Advanced
Run ID: ffd39fbb-4a84-47f2-bbf3-f3f40f232342
📒 Files selected for processing (3)
internal/store/postgres/project_repository.gointernal/store/postgres/project_repository_test.gopkg/auditrecord/consts.go
Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.
| }); err != nil { | ||
| return err | ||
| } | ||
| return InsertAuditRecordInTx(ctx, tx, buildProjectAuditRecord(ctx, auditrecord.ProjectCreatedEvent, result, result.CreatedAt)) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Bound the audit insert as well as the project write.
If ctx has no deadline and the audit insert waits on a database lock, InsertAuditRecordInTx can block after WithTimeout has finished. Create then holds its project transaction open. The same issue affects Delete at Line 404. Apply a timeout to the transaction or to both audit inserts, and keep audit-insert errors inside the transaction so they cause a rollback. (pkg.go.dev)
Coverage Report for CI Build 36833774058Coverage increased (+0.06%) to 53.829%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
# Conflicts: # pkg/auditrecord/consts.go
There was a problem hiding this comment.
🧹 Nitpick comments (1)
internal/store/postgres/project_repository_test.go (1)
304-321: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winAssert the complete project audit target.
The current tests only verify that an audit row exists.
auditCountfilters by event, project ID, and organization resource type, but it does not verify the resource organization ID or target metadata. A regression that writes the project audit under the wrong organization or with the wrongnamemetadata can pass bothTestCreateandTestDelete.Read the persisted audit row and assert
resource.id == project.Organization.ID,target.id == project.ID,target.type == pkgAuditRecord.ProjectType, andtarget.metadata["name"] == project.Namefor both mutation paths.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: raystack/frontier/.coderabbit.yaml
Review profile: CHILL
Plan: Advanced
Run ID: 0170e037-383a-4302-b2fb-628542b45033
📒 Files selected for processing (1)
pkg/auditrecord/consts.go
Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.
Summary
project.createdorproject.deletedaudit record in the same transaction as the project change, so one can't be saved without the other.Deletenow returns the deleted row to build the record. Deleting a project that doesn't exist still succeeds and writes no record, as before.project.Service.Createdeletes the new project again, so the history showsproject.createdfollowed byproject.deleted.E2E Result
I ran this against a local server built from this branch, acting as an ordinary org owner, not a superadmin:
audit-demo-owner@example.orgcreates the orgAudit Demo Org.audit-demo-project("Audit Demo Project") withFrontierService/CreateProject.FrontierService/DeleteProject.AdminService/ListAuditRecords, filtered bytarget_id, returns exactly oneproject.createdand oneproject.deletedrecord. Both are filed under the org:resourceis the org, named by its title, andorg_idis the org's id. Thetargetis the project, named by its title, with its slug inmetadata.name. Deleting the project a second time fails the permission check before the store runs, so no extra record is written. The records below are copied from that response. The only edit is the email domain, replaced withexample.org.project.created{ "id": "01a0ec0a-323d-7ab7-a484-e7b909f6913b", "actor": { "id": "aee938cd-96ff-476f-9a95-ac299c94ed35", "type": "app/user", "name": "auditdemoowner_example_org", "title": "Audit Demo", "metadata": { "context": { "Browser": "curl", "IpAddress": "", "Location": { "City": "", "Country": "", "Latitude": "", "Longitude": "" }, "OperatingSystem": "Other" } } }, "event": "project.created", "resource": { "id": "f2af66e3-b1ad-4fc7-8816-c9851ba45749", "type": "organization", "name": "Audit Demo Org", "metadata": {} }, "target": { "id": "3c51a8ff-87cb-49f1-9142-bdf97cd1c340", "type": "project", "name": "Audit Demo Project", "metadata": { "name": "audit-demo-project" } }, "occurred_at": "2026-09-29T07:21:26.325449Z", "org_id": "f2af66e3-b1ad-4fc7-8816-c9851ba45749", "org_name": "Audit Demo Org", "metadata": {}, "created_at": "2026-09-29T07:21:26.325449Z" }project.deleted{ "id": "01a0ec0a-4f6b-7fbc-81be-671153c9ae2f", "actor": { "id": "aee938cd-96ff-476f-9a95-ac299c94ed35", "type": "app/user", "name": "auditdemoowner_example_org", "title": "Audit Demo", "metadata": { "context": { "Browser": "curl", "IpAddress": "", "Location": { "City": "", "Country": "", "Latitude": "", "Longitude": "" }, "OperatingSystem": "Other" } } }, "event": "project.deleted", "resource": { "id": "f2af66e3-b1ad-4fc7-8816-c9851ba45749", "type": "organization", "name": "Audit Demo Org", "metadata": {} }, "target": { "id": "3c51a8ff-87cb-49f1-9142-bdf97cd1c340", "type": "project", "name": "Audit Demo Project", "metadata": { "name": "audit-demo-project" } }, "occurred_at": "2026-09-29T07:21:33.804064Z", "org_id": "f2af66e3-b1ad-4fc7-8816-c9851ba45749", "org_name": "Audit Demo Org", "metadata": {}, "created_at": "2026-09-29T07:21:33.802118Z" }