Skip to content

feat(store): audit user creation and deletion - #1954

Merged
rohanchkrabrty merged 4 commits into
mainfrom
fix-auditlog-user
Oct 1, 2026
Merged

rohanchkrabrty merged 4 commits into
mainfrom
fix-auditlog-user

Conversation

@rohanchkrabrty

@rohanchkrabrty rohanchkrabrty commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Creating or deleting a user now writes an audit record (user.created, user.deleted) in the same transaction as the user write. The user and its audit record are saved together, or neither is. Service users and orgs already work this way.

Changes

  • Add the UserCreatedEvent and UserDeletedEvent audit events.
  • createWithTx writes the user.created record. Create now goes through createWithTx, the path CreateWithConsent already used, so both create paths write the record from one place.
  • Delete now runs in a transaction, so the row delete and the user.deleted record are saved together.

Technical Details

  • Record shape: users belong to no org, so the record uses the platform as the resource and PlatformOrgID as the org, the same as the platform admin/member records. The user is the target, with their email in the target metadata so a deleted user can still be identified.
  • Signup actor: the signup endpoints are unauthenticated, so the request has no caller and the record would say "system". In that case the new user is recorded as the actor, the same way user.consent_granted handles it.
  • Old Create bug: on a duplicate email, the old Create returned ErrConflict without closing its transaction. That code is gone.
  • Errors: WithTxn wraps errors, so Create unwraps ErrConflict and ErrInvalidDetails and returns the plain errors, as before. Other errors now start with rollback:. Callers match with errors.Is, so they're unaffected.
  • Metric label: create latency is now reported as users / createWithTx instead of users / Create.

E2E Result

I ran this against a local server built from this branch. Consent is turned on there, so the signup goes through CreateWithConsent.

  1. audit-demo-signup@example.org signs up through mailotp, so the request has no caller.
  2. A superadmin creates audit-demo-created@example.org with FrontierService/CreateUser.
  3. The superadmin deletes both users with FrontierService/DeleteUser.

AdminService/ListAuditRecords, filtered by target_id, then returns exactly one user.created and one user.deleted record for each user. The records below are copied from that response. The only edit is the email domain, replaced with example.org.

Event How Actor
user.created self signup the new user. They have no title, so actor.name is their email, the same as the user.consent_granted record from that signup.
user.created superadmin CreateUser the superadmin
user.deleted superadmin DeleteUser the superadmin

user.created, self signup

{
  "id": "01a0ebf5-fd7d-76e0-b006-a54176073f78",
  "actor": {
    "id": "4841f2be-b16e-4aac-9f0d-a5c1a34f94f4",
    "type": "app/user",
    "name": "audit-demo-signup@example.org",
    "metadata": {}
  },
  "event": "user.created",
  "resource": {
    "id": "platform",
    "type": "platform",
    "name": "platform",
    "metadata": {}
  },
  "target": {
    "id": "4841f2be-b16e-4aac-9f0d-a5c1a34f94f4",
    "type": "user",
    "name": "auditdemosignup_example_org",
    "metadata": {
      "email": "audit-demo-signup@example.org"
    }
  },
  "occurred_at": "2026-09-29T06:59:22.100910Z",
  "org_id": "00000000-0000-0000-0000-000000000000",
  "metadata": {},
  "created_at": "2026-09-29T06:59:22.100910Z"
}

user.created, a superadmin creates the user

{
  "id": "01a0ebf5-5996-7847-aba7-0191533fc65a",
  "actor": {
    "id": "5b640f15-c248-4765-9759-1524034fddf0",
    "type": "app/user",
    "name": "rohancsa_example_org",
    "title": "Rohan",
    "metadata": {
      "context": {
        "Browser": "curl",
        "IpAddress": "",
        "Location": {
          "City": "",
          "Country": "",
          "Latitude": "",
          "Longitude": ""
        },
        "OperatingSystem": "Other"
      },
      "is_super_user": true
    }
  },
  "event": "user.created",
  "resource": {
    "id": "platform",
    "type": "platform",
    "name": "platform",
    "metadata": {}
  },
  "target": {
    "id": "5c274d2f-35a7-4db4-abaf-e19077cb5c3a",
    "type": "user",
    "name": "auditdemocreated_example_org",
    "metadata": {
      "email": "audit-demo-created@example.org"
    }
  },
  "occurred_at": "2026-09-29T06:58:40.126758Z",
  "org_id": "00000000-0000-0000-0000-000000000000",
  "metadata": {},
  "created_at": "2026-09-29T06:58:40.126758Z"
}

user.deleted, a superadmin deletes the user. The record for deleting the signup user has the same shape.

{
  "id": "01a0ebf6-1a94-7dba-a655-5ec48ea9d807",
  "actor": {
    "id": "5b640f15-c248-4765-9759-1524034fddf0",
    "type": "app/user",
    "name": "rohancsa_example_org",
    "title": "Rohan",
    "metadata": {
      "context": {
        "Browser": "curl",
        "IpAddress": "",
        "Location": {
          "City": "",
          "Country": "",
          "Latitude": "",
          "Longitude": ""
        },
        "OperatingSystem": "Other"
      },
      "is_super_user": true
    }
  },
  "event": "user.deleted",
  "resource": {
    "id": "platform",
    "type": "platform",
    "name": "platform",
    "metadata": {}
  },
  "target": {
    "id": "5c274d2f-35a7-4db4-abaf-e19077cb5c3a",
    "type": "user",
    "name": "auditdemocreated_example_org",
    "metadata": {
      "email": "audit-demo-created@example.org"
    }
  },
  "occurred_at": "2026-09-29T06:59:29.558951Z",
  "org_id": "00000000-0000-0000-0000-000000000000",
  "metadata": {},
  "created_at": "2026-09-29T06:59:29.555934Z"
}

Test Plan

  • TestCreate and TestDelete check that exactly one audit record is written, with the right actor: the new user on create, system on delete.
  • The internal/store/postgres tests pass, along with the core/user, core/deleter and core/authenticate tests.
  • golangci-lint reports 0 issues on the changed packages.
  • Manual testing completed (see E2E Result)
  • Build and type checking passes

SQL Safety (if your PR touches *_repository.go or goqu.*)

  • Values flow through ? placeholders, goqu.Ex{}, or goqu.Record{} — never fmt.Sprintf or + building a query that gets executed.
  • ToSQL() callers capture and forward params (query, params, err := stmt.ToSQL(); db.…Context(ctx, …, query, params...)). Never query, _, err := ….
  • No ? placeholders inside single-quoted SQL literals in goqu.L (use make_interval(hours => ?)-style functions instead).
  • Any //nolint:forbidigo or // #nosec G20x annotation has a one-line justification on the same line that a reviewer can verify.

Record user.created and user.deleted in the same transaction as the user
write, so a user cannot be created or deleted without its audit record.

Users belong to no org, so the records sit on the platform org with the
user as the target and their email in the target metadata. Signup runs
unauthenticated; with no caller in the context the new user is recorded
as their own actor instead of the system.

Create now goes through createWithTx, the path CreateWithConsent already
used, so both create paths write the record from one place. This also
closes the transaction the old Create left open on a duplicate email.
@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 Sep 29, 2026 7:15am 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.

Warning

Review limit reached

Next included review available in 9 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: raystack/frontier/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 179251b0-0457-4f71-978c-ccbb09b99eb4

📥 Commits

Reviewing files that changed from the base of the PR and between bab9fe2 and fa4ccca.

📒 Files selected for processing (3)
  • internal/store/postgres/user_repository.go
  • internal/store/postgres/user_repository_test.go
  • pkg/auditrecord/consts.go

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: raystack/frontier/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 6ad95f10-251f-4890-af3f-4f0c09638015

📥 Commits

Reviewing files that changed from the base of the PR and between 358c6be and bab9fe2.

📒 Files selected for processing (1)
  • internal/store/postgres/user_repository_test.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.


📝 Summary

Summary by CodeRabbit

  • New Features
    • User creation and deletion are recorded in the audit history.

Walkthrough

User creation and deletion now write audit records in the same transaction as their database operations. New event constants identify each audit record. Repository tests verify the event, target user, and actor.

Changes

User lifecycle audit records

Layer / File(s) Summary
User creation audit transaction
pkg/auditrecord/consts.go, internal/store/postgres/user_repository.go, internal/store/postgres/user_repository_test.go
User creation writes a UserCreatedEvent audit record in the creation transaction. The repository maps conflict and invalid-detail errors, and tests check the audit record for successful creation.
User deletion audit transaction
internal/store/postgres/user_repository.go, internal/store/postgres/user_repository_test.go
User deletion writes a UserDeletedEvent audit record in the deletion transaction. Tests check the event, target user, and actor.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Feature

Merge Risk: ⚪ Minimal · up to bab9f

User creation and deletion now write audit records in the same transaction as the user change. No merge-blocking risk was identified in the supplied changes.

Security Architecture Review

Security architecture risk: 🔵 Low · up to bab9f

The change improves audit consistency by saving each user change with its audit record. No new access bypass was identified, but failure behavior and access to the retained audit data still need verification.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The new records retain a user email in platform-scoped audit data even for deletion events. Audit-reader access and retention controls were not established by the reviewed evidence.

Trust Boundaries and Controls

  • inferred — The changed repository tests invoke repository methods directly rather than changing a production endpoint. The production create and delete paths pass through services, but endpoint authorization enforcement was not verified.

Hardening Proposals

  • proposed — Verify row and audit state after an audit-insert failure and after repeated create or delete requests; confirm the authorization and retention policy for platform-scoped audit records containing email addresses.
🚥 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.

@coveralls

coveralls commented Sep 28, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 36535535428

Coverage increased (+0.03%) to 52.732%

Details

  • Coverage increased (+0.03%) from the base build.
  • Patch coverage: 6 uncovered changes across 1 file (32 of 38 lines covered, 84.21%).
  • 1 coverage regression across 1 file.

Uncovered Changes

File Changed Covered %
internal/store/postgres/user_repository.go 38 32 84.21%

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
internal/store/postgres/user_repository.go 1 70.68%

Coverage Stats

Coverage Status
Relevant Lines: 41256
Covered Lines: 21755
Line Coverage: 52.73%
Coverage Strength: 17.26 hits per line

💛 - Coveralls

When signup records the new user as their own actor, use their title
for the actor name and fall back to their email when it is empty.

@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/user_repository_test.go (1)

234-237: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add an audit-insert failure case to TestCreate.

createWithTx inserts the user and then calls InsertAuditRecordInTx. TestCreate currently covers only successful audit insertion and duplicate-user failure. It does not force the audit insert to fail or assert that the user row is rolled back. A regression that commits the user before returning an audit error can therefore pass the test suite.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: raystack/frontier/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 51b8e0c8-8b7b-44d2-a31a-fc8ea224caa8

📥 Commits

Reviewing files that changed from the base of the PR and between 1c78634 and 358c6be.

📒 Files selected for processing (1)
  • internal/store/postgres/user_repository.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.

@rohanchkrabrty
rohanchkrabrty merged commit b51bd0a into main Oct 1, 2026
8 checks passed
@rohanchkrabrty
rohanchkrabrty deleted the fix-auditlog-user branch October 1, 2026 07:57

This branch was successfully deployed

1 active deployment
Preview — fa4ccca9 Deployed Sep 29, 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