Skip to content

Document quotation-comment impersonation API + integrator must-knows #103

@eelkekramer

Description

@eelkekramer

Context

🤖 Filed by Argus Agent (automated) on behalf of @eelkekramer.

The next release ships a new impersonation capability on the quotation comments endpoints (tracked in EL-328 / EL-329 / EL-330, customer-facing for McHale's Intercom-history migration). Today this behaviour is undocumented in Elfsquad/docsdocs/apis/quotation-api.md is a stub that just links to the OpenAPI spec, and there is no prose about comments at all.

Customer integrators will need a guide explaining the impersonation flow, the required permission, and a few non-obvious side effects that consumers of comment events / watcher lists need to know about up-front. The points below were surfaced during Argus's pre-release security review.

What needs to be written

A new page (probably docs/apis/quotation-comments.md or under a docs/guides/ folder) covering at least these topics:

1. Endpoint shapes

  • POST /api/2/quotations/{id}/comments (JSON body)
  • POST /quotation/1/quotations/{id}/comments (form-encoded body, multipart for file uploads)
  • DTO fields: message, isInternal, creatorUserId (Guid?, optional), creatorEmail (string?, optional)

2. Required permission for impersonation

Any caller (machine token or user token) supplying creatorUserId and/or creatorEmail must hold the CRUD QuotationComment.Create permission on the entity. Action-level permissions (CanCreatePublicComments / CanCreateInternalComments) are not sufficient and will return 403. Without those fields, normal action-level permissions still apply and the comment is attributed to the authenticated user.

3. Identity resolution rules

Inputs Behaviour
Neither field, user token Comment attributed to the authenticated user (current behaviour, unchanged).
Neither field, machine token 400 Bad Request — machine tokens have no user identity, so one must be supplied.
creatorUserId only User must exist in the tenant; both CreatorUserId and CreatorEmail are populated from the user record. 400 if not in tenant.
creatorEmail only, user resolves Both fields populated.
creatorEmail only, user does not resolve, machine token Comment created with the supplied email and Guid.Empty as CreatorUserId. Used for migrating historical chats from users who no longer exist.
creatorEmail only, user does not resolve, user token 400 — only machine tokens get the email-only fallback.
Both fields, mismatched 400 — the email must belong to the supplied userId (case-insensitive match).

4. Important side effects integrators must know

These are not bugs — they are intentional consequences of the design — but they will surprise integrators who haven't been told:

4a. Watcher list auto-add uses the impersonated identity

The comment-create endpoint automatically subscribes the comment author to the quotation as a watcher. When the request is impersonated, the impersonated user (not the API caller) is added as a watcher. Implication: if you migrate 5,000 historical Intercom chats attributed to ten different users via impersonation, those ten users will be automatically subscribed to every quotation involved and receive notification email for any future comment activity on those quotations.

Recommendation for integrators: if you don't want the impersonation targets to start receiving live notifications, unwatch them after the migration:

DELETE /quotation/1/quotations/{id}/watchers

(authenticated as the impersonated user, or via the bulk OData surface with the appropriate permission).

4b. Webhook events fire with the impersonated identity, not the caller

The quotation.comment.created event payload includes creatorEmail from the persisted comment, which is the impersonated email. Consumers that key on creatorEmail will see the migrated identity, not the API caller's.

The actual API caller's userId and applicationId are still in the event payload (separate fields). Integrators who care about provenance should inspect both — creatorEmail answers "who does the comment belong to?" while userId answers "who actually called the API?".

4c. "Email-only" historical comments cannot be deleted via the standard delete endpoint

When creatorEmail doesn't resolve to a tenant user (the historical-migration case), the comment's CreatorUserId is Guid.Empty. The own-delete endpoint checks CreatorUserId == authenticatedUserId, which never matches Guid.Empty, so no end-user can delete these comments through the UI. They are removable only via the admin bulk-delete endpoint (DELETE /api/2/quotations/{id}/comments/{commentId}, requires QuotationComment.Delete CRUD).

5. Worked example: McHale Intercom migration

A short worked example would help — something like:

POST /api/2/quotations/{quotationId}/comments
Authorization: Bearer <machine token with QuotationComment.Create CRUD>
Content-Type: application/json

{
  \"message\": \"Customer asked about lead time on cylinder model F-560\",
  \"isInternal\": false,
  \"creatorEmail\": \"sales-rep@mchale.example\",
  \"creatorUserId\": \"<resolved user GUID, optional but preferred when known>\"
}

Plus a note about how to disable the auto-watcher side effect (4a) at the end of a bulk import.

Source references for the writer

  • API spec doc: cpq repo, docs/specs/security/quotation-comment-authorization.md (INV-5, INV-6, FORBID-4, FORBID-5, ERR-4 all map to behaviours described above).
  • Implementation: projects/cpq/src/Core/Elfskot.Core/Services/QuotationComments/{ImpersonationInputNormalizer.cs,QuotationCommentService.cs}.
  • Argus integration tests (concrete repro for every rule): Elfsquad/elfsquad-argus repo, tests/api/EL-328-comment-impersonation.api.spec.ts.

Out of scope for this issue

There is a separate security finding (EL-378) about cross-organisation impersonation that is being fixed in the platform — do not document the current cross-org behaviour as expected. Wait for that fix to land, then describe whatever org-scoping rule is in effect post-fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions