up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
This commit is contained in:
259
docs/notifications/pack-approvals-contract.md
Normal file
259
docs/notifications/pack-approvals-contract.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# Pack Approvals Notification Contract
|
||||
|
||||
> **Status:** Implemented (NOTIFY-SVC-37-001)
|
||||
> **Last Updated:** 2025-11-27
|
||||
> **OpenAPI Spec:** `src/Notifier/StellaOps.Notifier/StellaOps.Notifier.WebService/openapi/pack-approvals.yaml`
|
||||
|
||||
## Overview
|
||||
|
||||
This document defines the canonical contract for pack approval notifications between Task Runner and the Notifier service. It covers event payloads, resume token mechanics, error handling, and security requirements.
|
||||
|
||||
## Event Kinds
|
||||
|
||||
| Kind | Description | Trigger |
|
||||
|------|-------------|---------|
|
||||
| `pack.approval.requested` | Approval required for pack deployment | Task Runner initiates deployment requiring approval |
|
||||
| `pack.approval.updated` | Approval state changed | Decision recorded or timeout |
|
||||
| `pack.policy.hold` | Policy gate blocked deployment | Policy Engine rejects deployment |
|
||||
| `pack.policy.released` | Policy hold lifted | Policy conditions satisfied |
|
||||
|
||||
## Canonical Event Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"eventId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"issuedAt": "2025-11-27T10:30:00Z",
|
||||
"kind": "pack.approval.requested",
|
||||
"packId": "pkg:oci/stellaops/scanner@v2.1.0",
|
||||
"policy": {
|
||||
"id": "policy-prod-deploy",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
"decision": "pending",
|
||||
"actor": "ci-pipeline@stellaops.example.com",
|
||||
"resumeToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"summary": "Deployment approval required for production scanner update",
|
||||
"labels": {
|
||||
"environment": "production",
|
||||
"team": "security"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Required Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `eventId` | UUID | Unique event identifier; used for deduplication |
|
||||
| `issuedAt` | ISO 8601 | Event timestamp in UTC |
|
||||
| `kind` | string | Event type (see Event Kinds table) |
|
||||
| `packId` | string | Package identifier in PURL format |
|
||||
| `decision` | string | Current state: `pending`, `approved`, `rejected`, `hold`, `expired` |
|
||||
| `actor` | string | Identity that triggered the event |
|
||||
|
||||
### Optional Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `policy` | object | Policy metadata (`id`, `version`) |
|
||||
| `resumeToken` | string | Opaque token for Task Runner resume flow |
|
||||
| `summary` | string | Human-readable summary for notifications |
|
||||
| `labels` | object | Custom key-value metadata |
|
||||
|
||||
## Resume Token Mechanics
|
||||
|
||||
### Token Flow
|
||||
|
||||
```
|
||||
┌─────────────┐ POST /pack-approvals ┌──────────────┐
|
||||
│ Task Runner │ ──────────────────────────────►│ Notifier │
|
||||
│ │ { resumeToken: "abc123" } │ │
|
||||
│ │◄──────────────────────────────│ │
|
||||
│ │ X-Resume-After: "abc123" │ │
|
||||
└─────────────┘ └──────────────┘
|
||||
│ │
|
||||
│ │
|
||||
│ User acknowledges approval │
|
||||
│ ▼
|
||||
│ ┌──────────────────────────────┐
|
||||
│ │ POST /pack-approvals/{id}/ack
|
||||
│ │ { ackToken: "..." } │
|
||||
│ └──────────────────────────────┘
|
||||
│ │
|
||||
│◄─────────────────────────────────────────────┤
|
||||
│ Resume callback (webhook or message bus) │
|
||||
```
|
||||
|
||||
### Token Properties
|
||||
|
||||
- **Format:** Opaque string; clients must not parse or modify
|
||||
- **TTL:** 24 hours from `issuedAt`
|
||||
- **Uniqueness:** Scoped to tenant + packId + eventId
|
||||
- **Expiry Handling:** Expired tokens return `410 Gone`
|
||||
|
||||
### X-Resume-After Header
|
||||
|
||||
When `resumeToken` is present in the request, the server echoes it in the `X-Resume-After` response header. This enables cursor-based processing for Task Runner polling.
|
||||
|
||||
## Error Handling
|
||||
|
||||
### HTTP Status Codes
|
||||
|
||||
| Code | Meaning | Client Action |
|
||||
|------|---------|---------------|
|
||||
| `200` | Duplicate request (idempotent) | Treat as success |
|
||||
| `202` | Accepted for processing | Continue normal flow |
|
||||
| `204` | Acknowledgement recorded | Continue normal flow |
|
||||
| `400` | Validation error | Fix request and retry |
|
||||
| `401` | Authentication required | Refresh token and retry |
|
||||
| `403` | Insufficient permissions | Check scope; contact admin |
|
||||
| `404` | Resource not found | Verify packId; may have expired |
|
||||
| `410` | Token expired | Re-initiate approval flow |
|
||||
| `429` | Rate limited | Retry after `Retry-After` seconds |
|
||||
| `5xx` | Server error | Retry with exponential backoff |
|
||||
|
||||
### Error Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "invalid_request",
|
||||
"message": "eventId, packId, kind, decision, actor are required.",
|
||||
"traceId": "00-abc123-def456-00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Retry Strategy
|
||||
|
||||
- **Transient errors (5xx, 429):** Exponential backoff starting at 1s, max 60s, max 5 retries
|
||||
- **Validation errors (4xx except 429):** Do not retry; fix request
|
||||
- **Idempotency:** Safe to retry any request with the same `Idempotency-Key`
|
||||
|
||||
## Security Requirements
|
||||
|
||||
### Authentication
|
||||
|
||||
All endpoints require a valid OAuth2 bearer token with one of these scopes:
|
||||
- `packs.approve` — Full approval flow access
|
||||
- `Notifier.Events:Write` — Event ingestion only
|
||||
|
||||
### Tenant Isolation
|
||||
|
||||
- `X-StellaOps-Tenant` header is **required** on all requests
|
||||
- Server validates token tenant claim matches header
|
||||
- Cross-tenant access returns `403 Forbidden`
|
||||
|
||||
### Idempotency
|
||||
|
||||
- `Idempotency-Key` header is **required** for POST endpoints
|
||||
- Keys are scoped to tenant and expire after 15 minutes
|
||||
- Duplicate requests within the window return `200 OK`
|
||||
|
||||
### HMAC Signature (Webhooks)
|
||||
|
||||
For webhook callbacks from Notifier to Task Runner:
|
||||
|
||||
```
|
||||
X-StellaOps-Signature: sha256=<hex-encoded-signature>
|
||||
X-StellaOps-Timestamp: <unix-timestamp>
|
||||
```
|
||||
|
||||
Signature computed as:
|
||||
```
|
||||
HMAC-SHA256(secret, timestamp + "." + body)
|
||||
```
|
||||
|
||||
Verification requirements:
|
||||
- Reject if timestamp is >5 minutes old
|
||||
- Reject if signature does not match
|
||||
- Reject if body has been modified
|
||||
|
||||
### IP Allowlist
|
||||
|
||||
Configurable per environment in `notifier:security:ipAllowlist`:
|
||||
```yaml
|
||||
notifier:
|
||||
security:
|
||||
ipAllowlist:
|
||||
- "10.0.0.0/8"
|
||||
- "192.168.1.100"
|
||||
```
|
||||
|
||||
### Sensitive Data Handling
|
||||
|
||||
- **Resume tokens:** Encrypted at rest; never logged in full
|
||||
- **Ack tokens:** Signed with KMS; validated on acknowledgement
|
||||
- **Labels:** Redacted if keys match `secret`, `password`, `token`, `key` patterns
|
||||
|
||||
## Audit Trail
|
||||
|
||||
All operations emit structured audit events:
|
||||
|
||||
| Event | Fields | Retention |
|
||||
|-------|--------|-----------|
|
||||
| `pack.approval.ingested` | packId, kind, decision, actor, eventId | 90 days |
|
||||
| `pack.approval.acknowledged` | packId, ackToken, decision, actor | 90 days |
|
||||
| `pack.policy.hold` | packId, policyId, reason | 90 days |
|
||||
|
||||
## Observability
|
||||
|
||||
### Metrics
|
||||
|
||||
| Metric | Type | Labels |
|
||||
|--------|------|--------|
|
||||
| `notifier_pack_approvals_total` | Counter | `kind`, `decision`, `tenant` |
|
||||
| `notifier_pack_approvals_outstanding` | Gauge | `tenant` |
|
||||
| `notifier_pack_approval_ack_latency_seconds` | Histogram | `decision` |
|
||||
| `notifier_pack_approval_errors_total` | Counter | `code`, `tenant` |
|
||||
|
||||
### Structured Logs
|
||||
|
||||
All operations include:
|
||||
- `traceId` — Distributed trace correlation
|
||||
- `tenantId` — Tenant identifier
|
||||
- `packId` — Package identifier
|
||||
- `eventId` — Event identifier
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Task Runner → Notifier (Ingestion)
|
||||
|
||||
```bash
|
||||
curl -X POST https://notifier.stellaops.example.com/api/v1/notify/pack-approvals \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "X-StellaOps-Tenant: tenant-acme-corp" \
|
||||
-H "Idempotency-Key: $(uuidgen)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"eventId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"issuedAt": "2025-11-27T10:30:00Z",
|
||||
"kind": "pack.approval.requested",
|
||||
"packId": "pkg:oci/stellaops/scanner@v2.1.0",
|
||||
"decision": "pending",
|
||||
"actor": "ci-pipeline@stellaops.example.com",
|
||||
"resumeToken": "abc123",
|
||||
"summary": "Approval required for production deployment"
|
||||
}'
|
||||
```
|
||||
|
||||
### Console → Notifier (Acknowledgement)
|
||||
|
||||
```bash
|
||||
curl -X POST https://notifier.stellaops.example.com/api/v1/notify/pack-approvals/pkg%3Aoci%2Fstellaops%2Fscanner%40v2.1.0/ack \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "X-StellaOps-Tenant: tenant-acme-corp" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"ackToken": "ack-token-xyz789",
|
||||
"decision": "approved",
|
||||
"comment": "Reviewed and approved"
|
||||
}'
|
||||
```
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [Pack Approvals Integration Requirements](pack-approvals-integration.md)
|
||||
- [Notifications Architecture](architecture.md)
|
||||
- [Notifications API Reference](api.md)
|
||||
- [Notification Templates](templates.md)
|
||||
Reference in New Issue
Block a user