219 lines
13 KiB
Markdown
219 lines
13 KiB
Markdown
# Feature Specification: SaaS Product Integration & Inbound Request Trust
|
|||
|
|
|
||
|
|
**Feature Branch**: `002-saas-integration`
|
||
|
|
|
||
|
|
**Created**: 2026-08-21
|
||
|
|
|
||
|
|
**Status**: Draft
|
||
|
|
|
||
|
|
**Input**: User description: "Phase 2 of docs/10-implementation-roadmap.md: SaaS integration —
|
||
|
|
Product/ProductIntegration models, credential validation, service-to-service auth (signed
|
||
|
|
tokens/OAuth2/mTLS), inbound request contract, rate limiting. Per docs/02-integration-and-security.md."
|
||
|
|
|
||
|
|
## User Scenarios & Testing *(mandatory)*
|
||
|
|
|
||
|
|
### User Story 1 - Every inbound request is authenticated and trusted before anything happens (Priority: P1)
|
||
|
|
|
||
|
|
A registered SaaS product's backend calls SupportHub on a customer's behalf (e.g., a customer
|
||
|
|
clicked "Help" inside the product). SupportHub validates the calling product's identity, the
|
||
|
|
credential presented, the product's current status, and the accompanying user/tenant context
|
||
|
|
against that product's registered integration and its allowed scope — before any downstream
|
||
|
|
processing occurs. A request from an unregistered product, an invalid/expired/revoked
|
||
|
|
credential, a suspended product, or context outside the credential's scope is rejected outright.
|
||
|
|
|
||
|
|
**Why this priority**: This is the trust boundary everything else in the system depends on.
|
||
|
|
Without it, SupportHub cannot safely accept "this is customer X of tenant Y using product Z" as
|
||
|
|
true, which every later phase (ticketing, AI, orchestration) relies on completely.
|
||
|
|
|
||
|
|
**Independent Test**: Send a request with a valid, correctly-scoped credential and confirm it is
|
||
|
|
accepted and its product/tenant/user context is trusted; send the same request with an invalid,
|
||
|
|
expired, or wrong-product credential and confirm it is rejected with no side effects.
|
||
|
|
|
||
|
|
**Acceptance Scenarios**:
|
||
|
|
|
||
|
|
1. **Given** a product has a registered, active integration with a valid credential, **When** it
|
||
|
|
sends a request with that credential and in-scope context, **Then** the request is accepted
|
||
|
|
and the product/tenant/user identity it carries is treated as trusted.
|
||
|
|
2. **Given** a request presents a credential that doesn't match any registered integration,
|
||
|
|
**When** SupportHub validates it, **Then** the request is rejected and no ticket, session, or
|
||
|
|
other record is created.
|
||
|
|
3. **Given** a product's integration status is "suspended," **When** a request arrives for that
|
||
|
|
product, **Then** it is rejected with a reason distinguishable from "invalid credential" (so
|
||
|
|
the calling product can tell the difference between "you're not registered" and "you're
|
||
|
|
registered but temporarily disabled").
|
||
|
|
4. **Given** a request includes fields not defined in the inbound contract, **When** it is
|
||
|
|
validated, **Then** the entire request is rejected rather than the unknown fields being
|
||
|
|
silently ignored.
|
||
|
|
5. **Given** a request's tenant/user context doesn't fall within the presented credential's
|
||
|
|
allowed scope, **When** it is validated, **Then** the request is rejected even though the
|
||
|
|
credential itself is valid.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### User Story 2 - An admin can onboard, rotate, and revoke a product's integration credential (Priority: P2)
|
||
|
|
|
||
|
|
An operator/admin registers a new SaaS product as a SupportHub integration client, issuing it a
|
||
|
|
credential scoped to that product alone. Later, the admin can rotate that credential (issue a new
|
||
|
|
one while the old one keeps working for a defined transition window) or revoke it immediately
|
||
|
|
(e.g., on suspected compromise), without any SupportHub downtime or a deploy.
|
||
|
|
|
||
|
|
**Why this priority**: Without this, User Story 1 has nothing to validate against, and there's no
|
||
|
|
way to safely respond to a leaked credential — but it's second because a single seeded
|
||
|
|
integration is enough to prove Story 1 works end to end before onboarding/rotation tooling exists.
|
||
|
|
|
||
|
|
**Independent Test**: Register a new product integration and confirm a request using its
|
||
|
|
credential is accepted (Story 1); rotate the credential and confirm both old and new credentials
|
||
|
|
work during the transition, then only the new one after; revoke a credential and confirm the very
|
||
|
|
next request using it is rejected.
|
||
|
|
|
||
|
|
**Acceptance Scenarios**:
|
||
|
|
|
||
|
|
1. **Given** an admin registers a new product integration, **When** they issue its credential,
|
||
|
|
**Then** that credential is scoped to that product alone — it is never valid for any other
|
||
|
|
product's requests.
|
||
|
|
2. **Given** an active integration, **When** an admin rotates its credential, **Then** requests
|
||
|
|
using either the old or new credential succeed until the transition window ends, after which
|
||
|
|
only the new one works.
|
||
|
|
3. **Given** an active integration, **When** an admin revokes its credential, **Then** the next
|
||
|
|
request using that credential is rejected, and the revocation is recorded in the audit trail.
|
||
|
|
4. **Given** any authentication attempt (success or failure) against any integration, **When** it
|
||
|
|
occurs, **Then** it is recorded in an audit trail an admin can review — including which
|
||
|
|
integration was involved and the outcome.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### User Story 3 - No single product integration or end user can overwhelm the system (Priority: P3)
|
||
|
|
|
||
|
|
Inbound requests are rate-limited both per product integration and per end user within that
|
||
|
|
integration, using limits an admin can change without a deploy. A product (or a single customer
|
||
|
|
within it) sending requests far beyond its configured limit is throttled; other integrations and
|
||
|
|
users are unaffected.
|
||
|
|
|
||
|
|
**Why this priority**: Important for production resilience and fairness across multiple
|
||
|
|
integrated products, but the system is meaningfully useful (and Stories 1-2 fully testable)
|
||
|
|
without it — this hardens an already-working trust boundary rather than enabling new behavior.
|
||
|
|
|
||
|
|
**Independent Test**: Send requests from one integration far beyond its configured rate limit and
|
||
|
|
confirm later requests in the burst are throttled while a concurrent, well-behaved second
|
||
|
|
integration's requests continue to succeed normally.
|
||
|
|
|
||
|
|
**Acceptance Scenarios**:
|
||
|
|
|
||
|
|
1. **Given** an integration has a configured rate limit, **When** it is exceeded within the
|
||
|
|
configured window, **Then** further requests from that integration are throttled until the
|
||
|
|
window resets.
|
||
|
|
2. **Given** two different end users under the same integration, **When** one exceeds their
|
||
|
|
per-user limit, **Then** the other user's requests continue to succeed normally.
|
||
|
|
3. **Given** an admin changes a rate limit value, **When** the change is saved, **Then** it takes
|
||
|
|
effect without requiring a deploy or restart.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Edge Cases
|
||
|
|
|
||
|
|
- What happens when a credential is presented after its rotation transition window has fully
|
||
|
|
elapsed? It MUST be treated identically to an already-revoked credential (rejected).
|
||
|
|
- What happens when the inbound request's signature/token appears valid but is a replay of a
|
||
|
|
previously-used one (e.g., a captured and resent signed token)? It MUST be rejected — accepted
|
||
|
|
service-to-service auth mechanisms must be replay-resistant (short-lived tokens with a
|
||
|
|
nonce/timestamp check, or equivalent).
|
||
|
|
- What happens when a product has no integration configured at all (never registered)? Requests
|
||
|
|
MUST be rejected the same way as an invalid credential, without leaking whether the product ID
|
||
|
|
itself is known to SupportHub.
|
||
|
|
- What happens when clock skew between the calling product and SupportHub affects a
|
||
|
|
time-bound signed token's validity window? A small, explicitly bounded tolerance is allowed;
|
||
|
|
anything beyond it is rejected.
|
||
|
|
- What happens when an integration is rotated or revoked while a request is mid-flight? The
|
||
|
|
in-flight request's outcome is decided by validation at the moment it's checked — no partial
|
||
|
|
application, no race that lets a revoked credential's request complete after revocation is
|
||
|
|
recorded.
|
||
|
|
- What happens when the same underlying customer problem is submitted twice in quick succession
|
||
|
|
(e.g., the calling product's own client retried after a timeout)? Out of scope for this
|
||
|
|
feature — de-duplicating retried problem reports into a single ticket depends on the `Ticket`
|
||
|
|
entity, which doesn't exist until the ticketing feature. This feature's contract MUST still
|
||
|
|
reserve a field for an idempotency key so that later feature can use it without a contract
|
||
|
|
change (see Assumptions).
|
||
|
|
|
||
|
|
## Requirements *(mandatory)*
|
||
|
|
|
||
|
|
### Functional Requirements
|
||
|
|
|
||
|
|
- **FR-001**: The system MUST require every SaaS product that wants support to be registered as
|
||
|
|
a distinct integration, each with its own credential — never shared across products.
|
||
|
|
- **FR-002**: The system MUST validate, on every inbound request before any downstream
|
||
|
|
processing: the calling product's identity, the presented credential, the product's current
|
||
|
|
status (active/suspended/deprecated), the accompanying user/tenant context, and the
|
||
|
|
credential's allowed scope.
|
||
|
|
- **FR-003**: The system MUST reject a request whose product, tenant, or user values are not
|
||
|
|
corroborated by the validated integration and its scope — a caller-supplied ID is never trusted
|
||
|
|
by itself.
|
||
|
|
- **FR-004**: The system MUST support at least one production-appropriate, replay-resistant
|
||
|
|
service-to-service authentication mechanism (signed short-lived scoped tokens, OAuth2
|
||
|
|
client-credentials, or mTLS), selectable per integration.
|
||
|
|
- **FR-005**: The system MUST support rotating an integration's credential with a defined
|
||
|
|
transition window in which both the old and new credential are valid, with zero downtime.
|
||
|
|
- **FR-006**: The system MUST support revoking an integration's credential with immediate effect
|
||
|
|
on the next request.
|
||
|
|
- **FR-007**: The system MUST audit every authentication attempt (success and failure), recording
|
||
|
|
which integration/credential was involved, without ever recording the raw credential value
|
||
|
|
itself.
|
||
|
|
- **FR-008**: The system MUST reject any inbound request containing fields outside the defined
|
||
|
|
contract, rather than silently accepting or ignoring them.
|
||
|
|
- **FR-009**: The system MUST rate-limit inbound requests per integration and independently per
|
||
|
|
end user within an integration, with limit values configurable without a deploy.
|
||
|
|
- **FR-010**: The system MUST distinguish, in its rejection response, between "unregistered/
|
||
|
|
invalid credential," "suspended product," and "out-of-scope request" where doing so does not
|
||
|
|
leak whether an unregistered product ID exists in the system.
|
||
|
|
- **FR-011**: The system MUST let an admin change an integration's status (active/suspended/
|
||
|
|
deprecated) and have that change take effect on the very next request, without a deploy.
|
||
|
|
- **FR-012**: The inbound request contract MUST include an optional idempotency-key field,
|
||
|
|
reserved for the ticketing feature's future use, even though this feature does not implement
|
||
|
|
deduplication against it.
|
||
|
|
|
||
|
|
### Key Entities
|
||
|
|
|
||
|
|
- **Product Integration**: One SaaS product's registration with SupportHub — its identity,
|
||
|
|
current status, credential reference, chosen authentication mechanism, allowed scope, and
|
||
|
|
rotation/revocation timestamps. Exactly one per product; never shared.
|
||
|
|
- **Customer Reference**: The external user/tenant identifiers a validated request carries,
|
||
|
|
scoped to the Product Integration that vouched for them — a reference into the SaaS's own
|
||
|
|
identity system, never a second copy of it (per Constitution Principle I).
|
||
|
|
- **Authentication Audit Event**: A record of one authentication attempt (success or failure)
|
||
|
|
against a Product Integration, including outcome and timestamp, but never the raw credential.
|
||
|
|
|
||
|
|
## Success Criteria *(mandatory)*
|
||
|
|
|
||
|
|
### Measurable Outcomes
|
||
|
|
|
||
|
|
- **SC-001**: 100% of inbound requests presenting an invalid, unregistered, expired, or
|
||
|
|
out-of-scope credential are rejected before any downstream record is created.
|
||
|
|
- **SC-002**: Revoking a credential stops it from being accepted on the very next request after
|
||
|
|
revocation — no propagation delay beyond normal request processing.
|
||
|
|
- **SC-003**: Rotating a credential causes zero failed requests for a well-behaved caller using
|
||
|
|
either the old or new credential during the transition window.
|
||
|
|
- **SC-004**: An admin can retrieve a complete authentication audit trail (success and failure)
|
||
|
|
for any given integration on demand.
|
||
|
|
- **SC-005**: An integration or user sending requests at 10x its configured rate limit is
|
||
|
|
measurably throttled while unrelated integrations/users see no change in their own success
|
||
|
|
rate.
|
||
|
|
- **SC-006**: Onboarding a new SaaS product as an integration requires no code change or
|
||
|
|
deploy — it is a configuration/data action only.
|
||
|
|
|
||
|
|
## Assumptions
|
||
|
|
|
||
|
|
- This feature covers the trust boundary and its own admin/audit surface only. It does not
|
||
|
|
implement ticket creation, the AI agent, or any business logic beyond validating and scoping an
|
||
|
|
inbound request — those are later features per the roadmap (Phases 3-5+).
|
||
|
|
- The idempotency key reserved in FR-012 is deliberately not enforced here (no `Ticket` entity
|
||
|
|
exists yet to deduplicate against) — this is a forward-compatibility placeholder so the
|
||
|
|
ticketing feature doesn't need a breaking contract change later, per the gap noted in
|
||
|
|
`docs/11-architect-additions-gaps-and-recommendations.md` §A1.
|
||
|
|
- "Rate limiting... configurable without a deploy" follows Constitution Principle II
|
||
|
|
(configuration over hardcoding) — exact default limit values are a
|
||
|
|
`REQUIRES BUSINESS CONFIRMATION` item per `docs/10-implementation-roadmap.md`, not invented
|
||
|
|
here.
|
||
|
|
- Choice of authentication mechanism (signed tokens vs. OAuth2 vs. mTLS) per integration, and the
|
||
|
|
credential rotation transition-window length, are technical decisions deferred to
|
||
|
|
`/speckit-plan` — this spec only requires that *a* production-appropriate, replay-resistant
|
||
|
|
mechanism exists and that rotation/revocation behave as described.
|