Skip to content

feat(store): audit project creation and deletion - #1955

Open
rohanchkrabrty wants to merge 4 commits into
mainfrom
feat-audit-project
Open

rohanchkrabrty wants to merge 4 commits into
mainfrom
feat-audit-project

Conversation

@rohanchkrabrty

@rohanchkrabrty rohanchkrabrty commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Creating or deleting a project now writes a project.created or project.deleted audit record in the same transaction as the project change, so one can't be saved without the other.
  • The record is filed under the project's org and points at the project. The org's title is read back in the same statement, and the project's slug goes in the target metadata so a deleted project can still be identified.
  • Delete now returns the deleted row to build the record. Deleting a project that doesn't exist still succeeds and writes no record, as before.
  • If creating the project's memberships fails, project.Service.Create deletes the new project again, so the history shows project.created followed by project.deleted.

E2E Result

I ran this against a local server built from this branch, acting as an ordinary org owner, not a superadmin:

  1. audit-demo-owner@example.org creates the org Audit Demo Org.
  2. They create the project audit-demo-project ("Audit Demo Project") with FrontierService/CreateProject.
  3. They delete it with FrontierService/DeleteProject.

AdminService/ListAuditRecords, filtered by target_id, returns exactly one project.created and one project.deleted record. Both are filed under the org: resource is the org, named by its title, and org_id is the org's id. The target is the project, named by its title, with its slug in metadata.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 with example.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"
}

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.
@vercel

vercel Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
frontier Ready Ready Preview Oct 1, 2026 8:02am UTC

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Summary

Summary by CodeRabbit

  • New Features
    • Project creation and deletion are now recorded in the organization’s audit history, with the project name and event time included.
    • Audit entries identify the organization and the affected project, making it easier to see which project each event concerns.
  • Bug Fixes
    • Deleting a project that doesn’t exist still succeeds without adding an audit record, keeping the audit history free of entries for projects that were not deleted.

Walkthrough

Project 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.

Changes

Project audit records

Layer / File(s) Summary
Project audit record contract
pkg/auditrecord/consts.go, internal/store/postgres/project_repository.go
Adds project creation and deletion event constants, plus shared data and record-building logic for project audit records.
Audit project creation
internal/store/postgres/project_repository.go, internal/store/postgres/project_repository_test.go
Project creation returns the project with its organization title and writes a creation audit record in its transaction. Tests check for one creation event after successful creation.
Audit project deletion
internal/store/postgres/project_repository.go, internal/store/postgres/project_repository_test.go
Project deletion returns the deleted project with its organization title and writes a deletion audit record when a row is deleted. Tests check that deleting a nonexistent project succeeds without an audit record.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~15 minutes

Change: Feature

Merge Risk: 🟡 Moderate · up to 8ee29

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 Review

Security architecture risk: 🔵 Low · up to 8ee29

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

  • Medium · reliability · observed: The project mutation is query-timeout bounded, but the following audit lookup and insert use the original context. If that context has no deadline, blocked audit work can retain the transaction’s connection and locks beyond the normal query limit, weakening shared-database failure containment.
  • Medium · reliability · inferred: Project deletion removes policies and resources before deleting the project row. The new audit insert can fail and roll back that final row deletion without restoring earlier cleanup, leaving a surviving project with removed access policies and resources. This adds a failure trigger to an existing non-atomic teardown; it does not introduce that topology.
Security review details

Security Blast Radius

  • inferred — The immediate state exposure is the mutated project, its organization-attributed audit record, and the policies/resources removed by project teardown. A blocked audit transaction can additionally affect other work sharing database capacity. No cross-tenant authorization or external webhook attack path was established.

Trust Boundaries and Controls

  • observed — Audit insertion errors are returned through the transaction callback, whose error path attempts rollback. This prevents an ordinary successful project commit without its corresponding audit insert; it does not make prior policy/resource cleanup part of the same transaction.

Hardening Proposals

  • proposed — Bound mutation and audit work with a common operation deadline. For broader deletion recovery, consider a resumable teardown state that remains recoverable after policy removal or audit failure, rather than relying on final-row rollback to represent the whole operation.
🚥 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.

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.

@rohanchkrabrty
rohanchkrabrty requested review from AmanGIT07 and removed request for Shreyag02 September 28, 2026 09:05

@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)
internal/store/postgres/project_repository_test.go (1)

121-135: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert the organization resource ID and target metadata.

auditCount checks only the event, project ID, and resource type. Both tests can pass when the persisted audit record has the wrong resource_id or lacks the project's name in target_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

📥 Commits

Reviewing files that changed from the base of the PR and between 52db5ba and b64e45f.

📒 Files selected for processing (3)
  • internal/store/postgres/project_repository.go
  • internal/store/postgres/project_repository_test.go
  • pkg/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))

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.

🩺 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)

@coveralls

coveralls commented Sep 28, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 36833774058

Coverage increased (+0.06%) to 53.829%

Details

  • Coverage increased (+0.06%) from the base build.
  • Patch coverage: 35 of 35 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 41379
Covered Lines: 22274
Line Coverage: 53.83%
Coverage Strength: 17.73 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.

🧹 Nitpick comments (1)
internal/store/postgres/project_repository_test.go (1)

304-321: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert the complete project audit target.

The current tests only verify that an audit row exists. auditCount filters 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 wrong name metadata can pass both TestCreate and TestDelete.

Read the persisted audit row and assert resource.id == project.Organization.ID, target.id == project.ID, target.type == pkgAuditRecord.ProjectType, and target.metadata["name"] == project.Name for 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

📥 Commits

Reviewing files that changed from the base of the PR and between ccc532e and 8ee297e.

📒 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.

This branch was successfully deployed

1 active deployment
Preview — 8ee297e0 Deployed Oct 1, 2026 by vercel[bot]
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.

3 participants