Skip to content

fix(auth): attribute API-key mutations to the key's creator, not the org owner - #3472

Merged
Marfuen merged 18 commits into
mainfrom
mariano/api-key-creator-attribution
Jul 22, 2026
Merged

Marfuen merged 18 commits into
mainfrom
mariano/api-key-creator-attribution

Conversation

@Marfuen

@Marfuen Marfuen commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Problem

API keys are org-scoped and record no creator, so ActingUserResolver attributes every API-key / MCP / automation mutation to the org's oldest owner. In the audit trail this makes all automation look like the owner performed it, masking the real actor — a meaningful gap for a compliance product (an auditor sees "one person did everything").

Fix

Record who created each key (as a Member, since keys are org-scoped) and attribute mutations to that member.

  • Schema: ApiKey.createdByMemberId — nullable, FK → Member, onDelete: SetNull (matches the FindingCreatedBy convention). Nullable so legacy keys, and keys whose creator was removed, fall back cleanly. Reverse relation Member.createdApiKeys. Migration: 20260721193000_add_api_key_creator.
  • api-key.service.ts: create() stores the creating member; validateApiKey() selects + returns it (both the primary and legacy-key paths); ApiKeyValidationResult gains createdByMemberId.
  • organization.controller.ts: the create endpoint passes the session member's id (authContext.memberId); null when a key is created via API key/service token.
  • hybrid-auth.guard.ts + types.ts: carry apiKeyCreatedByMemberId on the request.
  • acting-user.service.ts: new rule — attribute to the key's creator if they're still an active member of the org; otherwise fall back to the org owner exactly as before.

Behavior

Caller Attributed to
Session / service-token-with-x-user-id that user (unchanged)
API key with a recorded, still-active creator the creator
API key with no creator (legacy) or creator deactivated/removed org owner (unchanged)

No backfill

Existing keys have no recorded creator, so they keep attributing to the org owner. Recreating a key after this ships records the new creator.

Tests

  • acting-user.service.spec.ts: +2 cases (creator attribution; deactivated-creator fallback). 13/13 pass.
  • Typecheck clean for the changed files (the unrelated .spec.ts errors turbo reports also fail on main).

Deploy note

Additive migration (nullable column + FK + index) — no data change, no downtime.


Summary by cubic

Attributes API‑key mutations to the key’s creator and ensures they’re fully audit‑logged with clear provenance. Adds a PR security‑review GitHub Action that re‑reviews every commit; removes the misfiring PreToolUse hook and the Husky pre‑push block.

  • Bug Fixes

    • Schema: added ApiKey.createdByMemberId (nullable FK → Member); createApiKey stores the session member (else null) and validateApiKey returns it (primary + legacy).
    • HybridAuthGuard: forwards apiKeyCreatedByMemberId; restricts service‑token x-user-id to active members and sets request.memberId for acting; preserves API‑key org scoping.
    • ActingUserResolver: attributes to the key’s creator when active, else falls back to the oldest active owner; returns userId, memberId, and a callerLabel.
    • AuditLogInterceptor: resolves the actor for non‑session requests so API‑key/MCP mutations are logged; appends provenance in the description and via in audit JSON; skips only when no user can be resolved; fixes pluralization in control‑mapping messages.
    • Controllers: vendors create/triggerAssessment (400 if no actor; attribute to resolved user), policies publish‑all (per‑policy attribution for API‑key calls), ISMS createRow and bulkCreateMeasurements (session‑first; else resolved memberId for enteredById), and cloud‑security scan completion (best‑effort attribution with provenance label).
    • Security tooling: added a PR Security Review GitHub Action with repo‑specific guidance that re‑reviews the diff on every commit; removed the misfiring .claude PreToolUse hook and the .husky/pre-push block.
    • Spec: regenerated packages/docs/openapi.json to match current code (ISMS audit schema updates and recent DTO renames).
  • Migration

    • Apply 20260721193000_add_api_key_creator (additive). Existing keys keep owner fallback until recreated.

Written for commit 3096f90. Summary will update on new commits.

Review in cubic

…org owner

API keys were org-scoped with no recorded creator, so ActingUserResolver
attributed every API-key/MCP mutation to the org's oldest owner. In the audit
trail this made all automation look like the owner performed it, masking the
real actor — a problem for a compliance product.

- Add ApiKey.createdByMemberId (nullable, FK to Member, onDelete: SetNull).
  Populated on creation with the acting member. Nullable so legacy keys and
  keys whose creator was removed fall back cleanly.
- create-api-key now records the creating member; validateApiKey surfaces it.
- HybridAuthGuard puts it on the request; ActingUserResolver attributes the
  mutation to the creator (when still an active member of the org), else falls
  back to the org owner as before.
- Tests for both the creator-attribution path and the deactivated-creator
  fallback.

No backfill: existing keys have no recorded creator and keep falling back to
the org owner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
comp-framework-editor Ready Ready Preview, Comment Jul 22, 2026 3:03am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
app Skipped Skipped Jul 22, 2026 3:03am
portal Skipped Skipped Jul 22, 2026 3:03am

Request Review

The legacy-key branch's select was missing createdByMemberId (its deeper
indentation meant the earlier bulk edit didn't cover it), so the return that
references legacyMatch.createdByMemberId failed to typecheck. Add the field to
the legacy select to match the primary path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai 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.

Review completed against the latest diff

Confidence score: 5/5

  • Safe to merge after the addressed issues were fixed.

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/organization/organization.controller.ts
The global AuditLogInterceptor early-returned when request.userId was
absent, so API-key (and MCP) mutations were never audit-logged at all —
the createdByMemberId attribution added earlier had no effect on the
trail. Inject ActingUserResolver and resolve the responsible user (key
creator, else org owner) when there's no session userId; skip logging
only when no user can be attributed (no null-FK rows).

Also fixes a pre-existing bug surfaced once the spec could load: the
control mapping/unmapping descriptions read "policie" because the
resolver naive-stripped the trailing "s" of "policies". Use the known
Prisma model name (policies→policy) with an "ies"→"y" fallback.

The interceptor spec never ran before (it pulled better-auth's ESM
subpaths via permission.guard); mock @trycompai/auth like the other
specs so all 41 tests execute.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel
vercel Bot temporarily deployed to Preview – app July 22, 2026 01:12 Inactive
@vercel
vercel Bot temporarily deployed to Preview – portal July 22, 2026 01:12 Inactive
@mintlify

mintlify Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
CompAI 🟢 Ready View Preview Jul 22, 2026, 1:13 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

…nt sinks

The interceptor fix covered automatic @RequirePermission audit logging. This
covers the remaining sinks where an API-key mutation succeeded but attribution
was silently lost or credited to the org owner instead of the responsible user:

- vendors create + triggerAssessment: the controller now resolves the acting
  user and threads it as createdByUserId, so the auto-generated risk-assessment
  task ("created this task") credits the key creator, not the admin fallback.
- cloud-security scan: attribute scan_completed via ActingUserResolver instead
  of raw req.userId (was skipped entirely for API keys).
- policies publish-all: per-policy audit rows were dropped for API-key auth
  (authContext.userId undefined) — resolve the actor first.
- isms createRow: enteredById (a Member FK) was null for API keys — resolve the
  acting member.

ActingUserResolver now populates memberId on every path (session member, key
creator, or fallback owner's member), so Member-FK sinks like isms enteredById
attribute correctly. Owner lookup selects the member id alongside the user id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 9 files (changes from recent commits).

Tip: instead of fixing issues one by one fix them all with cubic
Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread apps/api/src/cloud-security/cloud-security.controller.ts Outdated
Comment thread apps/api/src/isms/isms-registers.controller.ts
Comment thread apps/api/src/vendors/vendors.controller.ts Outdated
Comment thread apps/api/src/vendors/vendors.controller.ts Outdated
Comment thread apps/api/src/auth/acting-user.service.ts
@linear

linear Bot commented Jul 22, 2026

Copy link
Copy Markdown

ENG-251

- vendors create/triggerAssessment: 400 when no actor resolves (org has no
  owner) instead of creating a vendor whose assessment task has no attributed
  user — matches ActingUserResolver's contract.
- cloud-security scan: attribution is best-effort (try/catch) so a transient
  resolver/audit failure can't fail an already-completed scan and invite a
  re-run.
- isms bulkCreateMeasurements: resolve the acting member (session-first, then
  api-key creator/owner) so bulk saves via API key don't persist null enteredById.
- hybrid-auth guard: service-token x-user-id now sets request.memberId so
  Member-FK sinks can attribute service-token-acting mutations.
- tests: createApiKey creator attribution (session forwards memberId, api-key
  forwards null), vendors 400-on-null, isms bulk api-key attribution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 7 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/auth/hybrid-auth.guard.ts
Cubic follow-up: the x-user-id member lookup didn't filter deactivated /
inactive memberships, so an offboarded user supplied via x-user-id could
receive new audit / enteredById attribution. Add deactivated:false + isActive:true
to the lookup (matching ActingUserResolver's filters); an inactive member now
resolves to no acting user and falls back to owner resolution downstream.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/auth/hybrid-auth.guard.ts

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".husky/pre-push">

<violation number="1">
P2: The new hook tells developers to bypass all client-side checks with `--no-verify`, contradicting repository policy and undermining the stated security gate. Remove the emergency-bypass guidance; retain the explicit review attestation path instead.</violation>

<violation number="2">
P1: Security-sensitive committed changes pass unchecked when `origin/main` is unavailable, because fallback `git diff HEAD` only inspects uncommitted worktree changes. Fail closed when no merge base can be resolved (or derive a real committed baseline) so this gate cannot silently become a no-op.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

The agent hook fired on non-git-push commands and blocked them (its
allow/block semantics were inverted and the `if` filter didn't scope). The
PR GitHub Action is the reliable auto-run gate; drop the local hook.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Set run-every-commit so the security Action re-reviews the latest diff on each
push to a PR. Without it the action runs once per PR and skips later commits,
leaving code added after the first review unchecked while the required check
stays green. Findings remain advisory PR comments (no merge block on noise).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…or-attribution

# Conflicts:
#	packages/docs/openapi.json
@vercel
vercel Bot temporarily deployed to Preview – portal July 22, 2026 02:57 Inactive
@vercel
vercel Bot temporarily deployed to Preview – app July 22, 2026 02:57 Inactive
Regenerated from the merged code so the spec carries #3469's
CreatePolicyVersionDto rename plus the current ISMS-audit schema (main's
committed spec had drifted). Written as-generated by the dev boot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel
vercel Bot temporarily deployed to Preview – app July 22, 2026 03:01 Inactive
@vercel
vercel Bot temporarily deployed to Preview – portal July 22, 2026 03:01 Inactive
@Marfuen
Marfuen merged commit 206ed96 into main Jul 22, 2026
16 checks passed
@Marfuen
Marfuen deleted the mariano/api-key-creator-attribution branch July 22, 2026 03:04
claudfuen pushed a commit that referenced this pull request Jul 22, 2026
# [3.106.0](v3.105.0...v3.106.0) (2026-07-22)

### Bug Fixes

* **auth:** attribute API-key mutations to the key's creator, not the org owner ([#3472](#3472)) ([206ed96](206ed96)), closes [hi#risk](https://github.com/hi/issues/risk)
* **deps:** bump adm-zip 0.5.18 -> 0.6.0 in apps/api (Dependabot [#88](https://github.com/trycompai/comp/issues/88)/[#89](https://github.com/trycompai/comp/issues/89)) ([#3462](#3462)) ([300f2a1](300f2a1)), closes [#3451](#3451)
* **deps:** override tar to ^7.5.19 to clear node-tar Dependabot alerts ([#94](https://github.com/trycompai/comp/issues/94)-[#104](https://github.com/trycompai/comp/issues/104)) ([#3466](#3466)) ([8ab5709](8ab5709))
* **deps:** patch engine.io ([#93](#93)) and body-parser ([#92](#92)) Dependabot alerts ([#3464](#3464)) ([94c33b1](94c33b1))
* **isms:** harden internal-audit validation and edge cases from deploy review ([#3473](#3473)) ([c6c7379](c6c7379))
* **policies:** create draft version on policy regenerate instead of overwriting published ([#3471](#3471)) ([ff31dbd](ff31dbd))
* **policies:** delete detached PDF objects when regenerating a draft ([#3474](#3474)) ([ecd1bd0](ecd1bd0))
* **policies:** rename CreateVersionDto to avoid swagger collision with automations ([#3469](#3469)) ([2d5290a](2d5290a))

### Features

* **isms:** internal audit programme, plan and report — clause 9.2 (CS-724) ([#3468](#3468)) ([42e5ebd](42e5ebd)), closes [hi#impact](https://github.com/hi/issues/impact)
@claudfuen

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.106.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

This branch was successfully deployed

2 active (1 outdated) and 2 inactive deployments
Preview – comp-framework-editor — 3096f90c Deployed Jul 22, 2026 by vercel[bot]
Preview – portal — 3096f90c Deployed Jul 22, 2026 by vercel[bot]
Preview – app — 3096f90c Deployed Jul 22, 2026 by vercel[bot]
staging - packages/docs — 4c9c3c16 Deployed Jul 22, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants