Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
This PR will trigger a minor release when merged. |
There was a problem hiding this comment.
Hey @ravverma
Solid foundation for consumer lifecycle management. The factory pattern is consistent with the codebase, immutable field protection is clean, revocation as a separate lifecycle action is the right design, and Slack notification resilience (fire-and-forget) is correct. Test coverage is broad at 827 lines covering happy paths, validation, forbidden access, DB failures, and edge cases.
However, there are issues that need to be addressed before merging.
CRITICAL: Missing access control on getByConsumerId and getByClientId
getAll correctly gates on hasS2SAdminAccess(), but getByConsumerId (~line 128) and getByClientId (~line 155) have no access control whatsoever. Any authenticated user can look up any consumer's details - including clientId, technicalAccountId, imsOrgId, and capabilities - by guessing or enumerating IDs.
Both functions need the same guard:
if (!accessControlUtil.hasS2SAdminAccess()) {
return forbidden('Only S2S admins can view consumers');
}The tests confirm this gap: getByConsumerId and getByClientId describe blocks have no "returns forbidden for non-admin users" test, unlike all four other endpoints (getAll, register, update, revoke).
If the intent is to allow consumers to look up their own record, a narrower self-access check (e.g., caller's clientId matches the consumer's clientId) would be appropriate instead. Either way, the current state of zero access control on these endpoints is not correct.
IMPORTANT: Access token passed in request body
The register endpoint accepts accessToken in the JSON request body. This token will appear in any middleware-level request/body logging, APM traces, and error stack traces. Consider accepting it via the Authorization header instead (e.g., Authorization: Bearer <ta-token>), which logging frameworks typically redact by default. The code itself is careful (the log.info on ~line 199 logs consumerName but not the token), but middleware-level body logging is the concern.
IMPORTANT: Slack mrkdwn injection via unsanitized user input
User-controlled consumerName and capabilities values are interpolated directly into Slack mrkdwn messages. While backtick-wrapping provides some protection, a consumer name containing backticks breaks out of the code block. For example, `foo` *bold injection* would render as active Slack markup. This affects register (~line 252), update (~line 337), and revoke notifications.
This could be used to craft misleading admin notifications (e.g., making an action appear to be performed by a different person). Sanitize or escape mrkdwn special characters (*, _, `, ~, <, >) before interpolation.
IMPORTANT: No capability validation
capabilities accepts any array with Array.isArray(capabilities) && capabilities.length > 0. There is no check that elements are strings, are non-empty, match a known set, or are free of duplicates. capabilities: [null, 123, {}, ""] or capabilities: ["<script>alert(1)</script>"] would be stored verbatim. Same issue in the update path.
At minimum, validate that all elements are non-empty strings. Ideally validate against an enum of known capabilities, or document that capabilities are freeform and downstream must not trust them for authorization.
MINOR: TOCTOU race on register duplicate check
const existing = await Consumer.findByClientId(clientId);
if (existing) { throw ... }
const consumer = await Consumer.create({...});A concurrent request could register the same clientId between the check and the create. If the data layer supports a unique constraint on clientId, that would be the proper guard. Otherwise, document this as a known limitation.
MINOR: Consumer enumeration via error messages
The notFound responses echo the lookup key: Consumer with consumerId ${consumerId} not found. Less critical given admin-only access (once the access control fix is applied), but for defense-in-depth, consider a generic "Consumer not found" message.
MINOR: getUpdatedBy fallback to 'system'
When the auth profile lacks an email, updatedBy is set to 'system'. This is ambiguous in audit trails - was this an actual system operation or a user whose profile lacked an email? Consider a more distinctive marker or logging a warning when the fallback is used.
MINOR: notifySlack pattern and channel validation
The notifySlack function uses Promise.resolve().then().catch() when try/catch would be simpler (the function is already async). Also, if ctx.env exists but S2S_SLACK_CHANNEL_ID is unset, postMessage gets called with channel: undefined. Validate the channel ID exists and log a specific warning if missing.
Process: Missing integration tests and OpenAPI specs
The repo requires integration tests for new/modified endpoints (test/it/) and OpenAPI-first API contract definitions (docs/openapi/). Neither is present in this PR. These can be follow-up PRs, but should be tracked.
…ck escaping, IT tests Made-with: Cursor
|
@solaris007 review comment addressed ChangesAPI
Security
Tests
Other
|
@ -0,0 +1,56 @@
PR Summary – Consumer Management API
Summary
Adds a new Consumer Management API for registering and managing API consumers (Technical Accounts). This enables service-to-service integrations to be tracked, permissioned, and lifecycle-managed via a set of RESTful endpoints.
New Endpoints
GET/consumersGET/consumers/:consumerIdGET/consumers/by-client-id/:clientIdPOST/consumers/registerPATCH/consumers/:consumerIdPOST/consumers/:consumerId/revokeKey Design Decisions
:consumerId(primary key) for get/update/revoke. A separate routeGET /consumers/by-client-id/:clientIdsupports lookup by IMSclient_idand usesConsumer.findByClientId(clientId).registerendpoint validates the TA access token viaimsClient.validateAccessToken(). The validation response includes atokenobject withclient_id,user_id, andorg; identity is taken from that payload (no separate profile API). If the response lackstoken, the request fails with 400.clientId,technicalAccountId, andimsOrgIdare immutable after registration. Attempts to modify them in update return 400 with a clear error.updatedoes not acceptrevokedAtorREVOKEDstatus. Revocation is a dedicatedPOST /consumers/:consumerId/revokeoperation. Once revoked, no further updates are permitted.hasS2SAdminAccess()onAccessControlUtil(delegates toauthInfo.isS2SAdmin()).register,update,revoke) log an info message and send a Slack notification to channelC0AFRHVRMPTwith operation details (including a delta of changes for updates).updatedBy: Every mutation records who performed it, from the caller's auth profile email (falls back to'system').messagefield (in addition to thex-errorheader) so clients can read the error message from the body.Consumer.findById(consumerId); lookup by IMS client ID usesConsumer.findByClientId(clientId), consistent with patterns used in other controllers.Files Changed
src/controllers/consumers.js— New controller (factory function pattern): getAll, getByConsumerId, getByClientId, register, update, revoke.src/dto/consumer.js— New DTO for API response serialization (includes consumerId, clientId, technicalAccountId, imsOrgId, consumerName, capabilities, status, revokedAt, createdAt, updatedAt, updatedBy).src/routes/index.js— Six consumer route definitions (includingGET /consumers/by-client-id/:clientId).src/index.js— Controller instantiation and wiring.src/support/access-control-util.js— AddedhasS2SAdminAccess().test/controllers/consumers.test.js— Unit tests for all endpoints, access control, validation, error paths, and edge cases (including getByClientId, token payload validation, and error body message).test/dto/consumer.test.js— DTO serialization tests.test/support/access-control-util.test.js— Test forhasS2SAdminAccess().test/routes/index.test.js— Consumer routes included in route segregation test.test/controllers/fixes.test.js— FixedEntityRegistryconstructor call for@adobe/spacecat-shared-data-access@2.109.0.docs/access-control-matrix.md— Capabilities matrix (Admin vs LLMO Administrator); S2S/Consumer not in matrix per request.package.json— Bumpedspacecat-shared-data-accessandspacecat-shared-http-utilsas needed.Test Plan
validateAccessToken; missing or invalid token payload returns 400 with message in body.messagein JSON body for 4xx/5xx.