feat: implement validation schema and handler for org key rotations - #131
Open
Bogunrot wants to merge 1 commit into
Open
feat: implement validation schema and handler for org key rotations#131Bogunrot wants to merge 1 commit into
Bogunrot wants to merge 1 commit into
Conversation
Add POST /api/v1/organizations/keys/rotate so organization owners can request a fresh active admin key. The endpoint is guarded by @roles(OWNER), validates the request body with a strict Zod schema, revokes every active admin key, mints a replacement (only its SHA-256 hash is stored), and emits an organization.key_rotated domain event that the audit listener records as an AuditLog row. Closes ASTROIDX556#5
|
@Bogunrot Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
1. Linked Issue
Closes #5
2. Problem Statement (The Bug)
Organizations had no way to rotate their admin API key. The
ApiKeymodel stores only a SHA-256 hash (hashedKey), so a leaked or compromised key can never be recovered or invalidated through the API — the only existing operations were create/list/revoke of arbitrary developer keys. This is a security gap that cannot be fixed with a local patch: rotating a credential is a cross-cutting operation that must atomically (a) supersede every currently valid admin key, (b) mint a replacement whose raw secret is shown exactly once, and (c) leave an immutable audit trail. Doing any of that by hand in the database would bypass the application's event ledger and audit log, which are the platform's record of every financial decision.3. Solution Comparison and Decision (Why rotation, and not the alternatives)
revoke+createand let clients compose a rotationapi-keysendpoint with an extrarotatequery flagDecision — a dedicated
POST /api/v1/organizations/keys/rotateendpoint onOrganizationController. It is the only option that keeps the whole rotation in one request: authorize (owner-only) → validate (Zod) → revoke all active admin keys → mint one new admin key → emitorganization.key_rotated. The operation is additive and cannot be composed correctly by clients, so it must be a first-class handler.4. The Change (Code modifications)
organization.dto.ts— new strict Zod schemarotateKeysSchema(name/reason, unknown keys rejected) plus the matching Swagger DTO.organization.controller.ts—rotateKeysroute with@Roles(UserRole.OWNER)and@Body(new ZodValidationPipe(rotateKeysSchema)).organization.repository.ts—findActiveApiKeys,createApiKey,revokeApiKeypersistence methods.organization.service.ts—rotateKeysorchestrates revoke → mint → emit.event-names.ts—OrganizationKeyRotated: 'organization.key_rotated'; the globalAuditListener(wildcard@OnEvent('**')) turns the same event into anAuditLogrow, satisfying the "AuditLog / DomainEvent" criterion.Core handler:
Behavioral comparison across entry points:
@Roles(UserRole.OWNER)enforced by the globalRolesGuard(403 for any other role)VALIDATION_ERRORpermissions: ['admin']; raw secret returned exactly once, only the SHA-256 hash persistedorganization.key_rotatedemitted → immutabledomain_eventsrow +audit_logsrow viaAuditListener5. Compatibility Note (On INTERFACE_VERSION)
This project has no versioned interface constant — the API version is the stable
api/v1prefix configured inapp.config(apiPrefix), which is not modified. No bump is needed because the change is purely additive: a new route under the existing prefix, a new event name, and new repository methods. No existing endpoint, payload shape, or event contract changes.6. Incidental Fixes (Two things the issue called out that also got fixed)
prismacalls in the service.rotateKeysreuses the existinggenerateApiKeycrypto util (raw + prefix + hash), keeping the "hash-only storage, show-once" invariant consistent with the developerapi-keysflow.7. Testing (Proving it works)
New tests (all green):
organization.service.spec.tsrevokes every active admin key and mints a fresh one— asserts every admin key is revoked, the new key carriespermissions: ['admin'], and the stored hash equalssha256(raw)of the returned secret (proving the show-once invariant through the realcrypto.util).emits an OrganizationKeyRotated domain event with audit context— asserts the real event name and the org/actor/aggregate envelope.mints a key when the organization has no admin key yet— non-admin keys are never touched.throws when the organization does not exist— 404 path.organization.controller.spec.tsis restricted to organization owners via @Roles(OWNER)— reads the actualROLES_KEYmetadata off the real handler, proving the guard blocks non-owners.delegates to the service with the caller context and validated body— real controller → real service boundary.Results:
organization.*specs 7/7 passed; full suiteTest Files 12 passed, Tests 117 passed;npm run typecheckandnpm run lintclean. No pre-existing failures observed.8. Additional Notes (Scope)
Single focused commit on
feat/org-key-rotation. Scope is limited to the organizations module + one event name; wallets, policies, budgets, transactions, and the developerapi-keysmodule are untouched.