This commit is contained in:
@@ -86,6 +86,7 @@ AUTH-TEN-49-001 | DOING (2025-11-02) | Implement service accounts & delegation t
|
|||||||
> 2025-11-02: Service account store + configuration wired, delegation quotas enforced, token persistence extended with `serviceAccountId`/`tokenKind`/`actorChain`, docs & samples refreshed, and new tests cover delegated issuance/persistence.
|
> 2025-11-02: Service account store + configuration wired, delegation quotas enforced, token persistence extended with `serviceAccountId`/`tokenKind`/`actorChain`, docs & samples refreshed, and new tests cover delegated issuance/persistence.
|
||||||
> 2025-11-02: Updated bootstrap test fixtures to use AuthorityDelegation seed types and verified `/internal/service-accounts` endpoints respond as expected via targeted Authority tests.
|
> 2025-11-02: Updated bootstrap test fixtures to use AuthorityDelegation seed types and verified `/internal/service-accounts` endpoints respond as expected via targeted Authority tests.
|
||||||
> 2025-11-02: Documented bootstrap admin API usage (`/internal/service-accounts/**`) and clarified that repeated seeding preserves Mongo `_id`/`createdAt` values to avoid immutable field errors.
|
> 2025-11-02: Documented bootstrap admin API usage (`/internal/service-accounts/**`) and clarified that repeated seeding preserves Mongo `_id`/`createdAt` values to avoid immutable field errors.
|
||||||
|
> 2025-11-03: Patched Authority test harness to seed enabled service-account records deterministically and restored `StellaOps.Authority.Tests` to green (covers `/internal/service-accounts` listing + revocation paths).
|
||||||
AUTH-VULN-29-001 | DONE (2025-11-03) | Define Vuln Explorer scopes/roles (`vuln:view`, `vuln:investigate`, `vuln:operate`, `vuln:audit`) with ABAC attributes (env, owner, business_tier) and update discovery metadata/offline kit defaults. Dependencies: AUTH-POLICY-27-001. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md)
|
AUTH-VULN-29-001 | DONE (2025-11-03) | Define Vuln Explorer scopes/roles (`vuln:view`, `vuln:investigate`, `vuln:operate`, `vuln:audit`) with ABAC attributes (env, owner, business_tier) and update discovery metadata/offline kit defaults. Dependencies: AUTH-POLICY-27-001. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md)
|
||||||
AUTH-VULN-29-002 | DONE (2025-11-03) | Enforce CSRF/anti-forgery tokens for workflow actions, sign attachment tokens, and record audit logs with ledger event hashes. Dependencies: AUTH-VULN-29-001, LEDGER-29-002. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md)
|
AUTH-VULN-29-002 | DONE (2025-11-03) | Enforce CSRF/anti-forgery tokens for workflow actions, sign attachment tokens, and record audit logs with ledger event hashes. Dependencies: AUTH-VULN-29-001, LEDGER-29-002. | Authority Core & Security Guild (src/Authority/StellaOps.Authority/TASKS.md)
|
||||||
AUTH-VULN-29-003 | DOING (2025-11-03) | Update security docs/config samples for Vuln Explorer roles, ABAC policies, attachment signing, and ledger verification guidance. Dependencies: AUTH-VULN-29-001..002. | Authority Core & Docs Guild (src/Authority/StellaOps.Authority/TASKS.md)
|
AUTH-VULN-29-003 | DOING (2025-11-03) | Update security docs/config samples for Vuln Explorer roles, ABAC policies, attachment signing, and ledger verification guidance. Dependencies: AUTH-VULN-29-001..002. | Authority Core & Docs Guild (src/Authority/StellaOps.Authority/TASKS.md)
|
||||||
|
|||||||
423
docs/replay/DETERMINISTIC_REPLAY.md
Normal file
423
docs/replay/DETERMINISTIC_REPLAY.md
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
# Stella Ops — Deterministic Replay Specification
|
||||||
|
|
||||||
|
Version: 1.0
|
||||||
|
Status: Draft / Internal Technical Reference
|
||||||
|
Audience: Core developers, module maintainers, audit engineers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Purpose
|
||||||
|
|
||||||
|
Deterministic Replay allows any completed Stella Ops scan to be **reproduced byte-for-byte** with full cryptographic validation.
|
||||||
|
It guarantees that SBOMs, Findings, and VEX evaluations can be re-executed later to:
|
||||||
|
|
||||||
|
- prove historical compliance decisions,
|
||||||
|
- attribute changes precisely to feeds, rules, or tools,
|
||||||
|
- support dual-signing (FIPS + regional crypto),
|
||||||
|
- and anchor cryptographic evidence in offline or public ledgers.
|
||||||
|
|
||||||
|
Replay requires that all inputs and environmental conditions are **captured, hashed, and sealed** at scan time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Architecture Overview
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[Scanner.WebService] --> B[Replay Manifest]
|
||||||
|
A --> C[InputBundle]
|
||||||
|
A --> D[OutputBundle]
|
||||||
|
B --> E[DSSE Envelope]
|
||||||
|
C --> F[Feedser Snapshot Export]
|
||||||
|
C --> G[Policy/Lattice Bundle]
|
||||||
|
D --> H[DSSE Outputs (SBOM, Findings, VEX)]
|
||||||
|
E --> I[MongoDB: replay_runs]
|
||||||
|
C --> J[Blob Store: Input/Output Bundles]
|
||||||
|
````
|
||||||
|
|
||||||
|
### Core Artifacts
|
||||||
|
|
||||||
|
| Artifact | Description | Format |
|
||||||
|
| ------------------- | ------------------------------------------------------ | -------------------------- |
|
||||||
|
| **Replay Manifest** | Immutable JSON describing all scan inputs and outputs. | JSON (canonicalized) |
|
||||||
|
| **InputBundle** | Feeds, rules, policies, tool binaries (hashed). | `.tar.zst` |
|
||||||
|
| **OutputBundle** | SBOM, Findings, VEX, logs. | `.tar.zst` |
|
||||||
|
| **DSSE Envelope** | Signed metadata for each artifact. | JSON / JWS |
|
||||||
|
| **Merkle Map** | Layer and feed chunk trees. | JSON (embedded or sidecar) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Replay Manifest Schema (v1)
|
||||||
|
|
||||||
|
### 3.1 Top-level Layout
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"schemaVersion": "1.0",
|
||||||
|
"scan": {
|
||||||
|
"id": "uuid",
|
||||||
|
"time": "2025-10-29T13:05:33Z",
|
||||||
|
"mode": "record",
|
||||||
|
"scannerVersion": "10.1.3",
|
||||||
|
"cryptoProfile": "FIPS-140-3+GOST-R-34.10-2012"
|
||||||
|
},
|
||||||
|
"subject": {
|
||||||
|
"ociDigest": "sha256:abcd...",
|
||||||
|
"layers": [
|
||||||
|
{ "layerDigest": "...", "merkleRoot": "...", "leafCount": 144 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"feeds": [
|
||||||
|
{
|
||||||
|
"name": "nvd",
|
||||||
|
"snapshotHash": "sha256:...",
|
||||||
|
"snapshotTime": "2025-10-29T12:00:00Z",
|
||||||
|
"merkleRoot": "..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rulesBundleHash": "sha256:...",
|
||||||
|
"tools": [
|
||||||
|
{ "name": "sbomer", "version": "10.1.3", "sha256": "..." },
|
||||||
|
{ "name": "scanner", "version": "10.1.3", "sha256": "..." },
|
||||||
|
{ "name": "vexer", "version": "10.1.3", "sha256": "..." }
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"os": "linux",
|
||||||
|
"arch": "x64",
|
||||||
|
"locale": "en_US.UTF-8",
|
||||||
|
"tz": "UTC",
|
||||||
|
"seed": "H(scan.id||merkleRootAllLayers)",
|
||||||
|
"flags": ["offline"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"latticeHash": "sha256:...",
|
||||||
|
"mutes": [
|
||||||
|
{ "id": "MUTE-1234", "reason": "vendor ack", "approvedBy": "authority@example.com", "approvedAt": "2025-10-29T12:55Z" }
|
||||||
|
],
|
||||||
|
"trustProfile": "sha256:..."
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"sbomHash": "sha256:...",
|
||||||
|
"findingsHash": "sha256:...",
|
||||||
|
"vexHash": "sha256:...",
|
||||||
|
"logHash": "sha256:..."
|
||||||
|
},
|
||||||
|
"provenance": {
|
||||||
|
"signer": "scanner.authority",
|
||||||
|
"dsseEnvelopeHash": "sha256:...",
|
||||||
|
"rekorEntry": "optional"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Deterministic Execution Rules
|
||||||
|
|
||||||
|
### 4.1 Environment Normalization
|
||||||
|
|
||||||
|
* **Clock:** frozen to `scan.time` unless a rule explicitly requires “now”.
|
||||||
|
* **Random seed:** derived as `H(scan.id || MerkleRootAllLayers)`.
|
||||||
|
* **Locale/TZ:** enforced per manifest; deviations cause validation error.
|
||||||
|
* **Filesystem normalization:**
|
||||||
|
|
||||||
|
* Normalize perms to 0644/0755.
|
||||||
|
* Path separators = `/`.
|
||||||
|
* Newlines = LF.
|
||||||
|
* JSON key order = lexical.
|
||||||
|
|
||||||
|
### 4.2 Concurrency & I/O
|
||||||
|
|
||||||
|
* File traversal: stable lexicographic order.
|
||||||
|
* Parallel jobs: ordered reduction by subject path.
|
||||||
|
* Temporary directories: ephemeral but deterministic hash seeds.
|
||||||
|
|
||||||
|
### 4.3 Feeds & Policies
|
||||||
|
|
||||||
|
* All network I/O disabled; feeds must be read from snapshot bundles.
|
||||||
|
* Policies and suppressions must resolve by hash, not name.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. DSSE and Signing
|
||||||
|
|
||||||
|
### 5.1 Envelope Structure
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"payloadType": "application/vnd.stella.replay.manifest+json",
|
||||||
|
"payload": "<base64-encoded canonical JSON>",
|
||||||
|
"signatures": [
|
||||||
|
{ "keyid": "authority-root-fips", "sig": "..." },
|
||||||
|
{ "keyid": "authority-root-gost", "sig": "..." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Verification Steps
|
||||||
|
|
||||||
|
1. Decode payload → verify canonical form.
|
||||||
|
2. Verify each signature chain against RootPack (offline trust anchors).
|
||||||
|
3. Recompute hash and compare to `dsseEnvelopeHash` in manifest.
|
||||||
|
4. Optionally verify Rekor inclusion proof.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. CLI Interface
|
||||||
|
|
||||||
|
### 6.1 Recording a Scan
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stella scan image:tag --record ./out/
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces:
|
||||||
|
|
||||||
|
```
|
||||||
|
out/
|
||||||
|
├─ manifest.json
|
||||||
|
├─ manifest.dsse.json
|
||||||
|
├─ inputbundle.tar.zst
|
||||||
|
├─ outputbundle.tar.zst
|
||||||
|
└─ signatures/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Verifying
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stella verify manifest.json
|
||||||
|
```
|
||||||
|
|
||||||
|
* Checks all hashes and DSSE envelopes.
|
||||||
|
* Prints summary:
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Verified: SBOM, Findings, VEX, Tools, Feeds, Policy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Replaying
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stella replay manifest.json --strict
|
||||||
|
stella replay manifest.json --what-if --vary=feeds
|
||||||
|
```
|
||||||
|
|
||||||
|
* `--strict`: all inputs locked; identical result expected.
|
||||||
|
* `--what-if`: varies only specified dimension(s).
|
||||||
|
|
||||||
|
### 6.4 Diffing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stella diff manifestA.json manifestB.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Shows field-level differences (feed snapshot, tool, or policy hash).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. MongoDB Schema
|
||||||
|
|
||||||
|
### 7.1 `replay_runs`
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"_id": "uuid",
|
||||||
|
"manifestHash": "sha256:...",
|
||||||
|
"status": "verified|failed|replayed",
|
||||||
|
"createdAt": "...",
|
||||||
|
"updatedAt": "...",
|
||||||
|
"signatures": [{ "profile": "FIPS", "verified": true }],
|
||||||
|
"outputs": {
|
||||||
|
"sbom": "sha256:...",
|
||||||
|
"findings": "sha256:..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 `bundles`
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"_id": "sha256:...",
|
||||||
|
"type": "input|output|rootpack",
|
||||||
|
"size": 4123123,
|
||||||
|
"location": "/var/lib/stella/bundles/<sha>.tar.zst"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 `subjects`
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"ociDigest": "sha256:abcd...",
|
||||||
|
"layers": [
|
||||||
|
{ "layerDigest": "...", "merkleRoot": "...", "leafCount": 120 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Layer Merkle Implementation
|
||||||
|
|
||||||
|
### 8.1 Algorithm
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
static string ComputeMerkleRoot(string layerTarPath)
|
||||||
|
{
|
||||||
|
const int ChunkSize = 4 * 1024 * 1024;
|
||||||
|
var hashes = new List<byte[]>();
|
||||||
|
using var fs = File.OpenRead(layerTarPath);
|
||||||
|
var buffer = new byte[ChunkSize];
|
||||||
|
int read;
|
||||||
|
using var sha = SHA256.Create();
|
||||||
|
while ((read = fs.Read(buffer, 0, buffer.Length)) > 0)
|
||||||
|
hashes.Add(sha.ComputeHash(buffer, 0, read));
|
||||||
|
while (hashes.Count > 1)
|
||||||
|
hashes = hashes
|
||||||
|
.Select((h, i) => (h, i))
|
||||||
|
.GroupBy(x => x.i / 2)
|
||||||
|
.Select(g => sha.ComputeHash(g.SelectMany(x => x.h).ToArray()))
|
||||||
|
.ToList();
|
||||||
|
return Convert.ToHexString(hashes.Single());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 Stored Values
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layerDigest": "sha256:...",
|
||||||
|
"merkleRoot": "b81f...",
|
||||||
|
"leafCount": 240,
|
||||||
|
"leavesHash": "sha256:..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Replay Engine Implementation Notes (.NET 10)
|
||||||
|
|
||||||
|
### 9.1 Manifest Parsing
|
||||||
|
|
||||||
|
Use `System.Text.Json` with deterministic ordering:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var options = new JsonSerializerOptions {
|
||||||
|
WriteIndented = false,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
TypeInfoResolverChain = { new OrderedResolver() }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 Stable Output
|
||||||
|
|
||||||
|
Normalize SBOM/Findings/VEX JSON:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
string Canonicalize(string json) =>
|
||||||
|
JsonSerializer.Serialize(
|
||||||
|
JsonSerializer.Deserialize<JsonDocument>(json),
|
||||||
|
options);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.3 Verification Flow
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var manifest = Manifest.Load("manifest.json");
|
||||||
|
VerifySignatures(manifest);
|
||||||
|
VerifyHashes(manifest);
|
||||||
|
if (mode == Strict) RunPipeline(manifest);
|
||||||
|
else RunPipelineWithVariation(manifest, vary);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.4 Failure Modes
|
||||||
|
|
||||||
|
| Condition | Action |
|
||||||
|
| -------------------------------- | ----------------------------- |
|
||||||
|
| Missing snapshot or bundle | Error: `InputBundleMissing` |
|
||||||
|
| Feed hash mismatch | Error: `FeedSnapshotDrift` |
|
||||||
|
| Tool binary hash mismatch | Reject replay |
|
||||||
|
| Output hash drift in strict mode | Mark as failed, emit diff log |
|
||||||
|
| Invalid signature | Reject manifest |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Crypto Profiles and RootPack
|
||||||
|
|
||||||
|
### 10.1 Example Profiles
|
||||||
|
|
||||||
|
| Profile | Algorithms | Notes |
|
||||||
|
| -------------- | ------------------------------------- | ----------------------- |
|
||||||
|
| **FIPS-140-3** | ECDSA-P256 / SHA-256 / AES-GCM | Default for US/EU |
|
||||||
|
| **GOST** | GOST R 34.10-2012 / GOST R 34.11-2012 | Russia |
|
||||||
|
| **SM** | SM2 / SM3 / SM4 | China |
|
||||||
|
| **eIDAS** | RSA-PSS / SHA-256 | EU qualified signatures |
|
||||||
|
|
||||||
|
### 10.2 Dual-Signing Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stella sign manifest.json --profiles=FIPS,GOST
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces:
|
||||||
|
|
||||||
|
```
|
||||||
|
signatures/
|
||||||
|
├─ manifest.dsse.fips.json
|
||||||
|
└─ manifest.dsse.gost.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Test Strategy
|
||||||
|
|
||||||
|
| Test | Description | Expected Result |
|
||||||
|
| ---------------------- | ------------------------------------ | --------------------------- |
|
||||||
|
| **Golden Replay** | Repeat identical scan → same outputs | ✅ identical hashes |
|
||||||
|
| **Feed Drift Test** | Replay with updated feeds | Only `inputs.feeds` changes |
|
||||||
|
| **Tool Upgrade Test** | Replay with new scanner version | Reject or diff by `tools` |
|
||||||
|
| **Policy Change Test** | Different lattice/mutes | Diff by `policy` section |
|
||||||
|
| **Cross-Arch Test** | x64 vs arm64 | Identical outputs |
|
||||||
|
| **Corrupted Bundle** | Tamper bundle | Verification fails |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Example Verification Output
|
||||||
|
|
||||||
|
```
|
||||||
|
$ stella verify manifest.json
|
||||||
|
|
||||||
|
[✓] Manifest integrity: OK
|
||||||
|
[✓] DSSE signatures (FIPS,GOST): OK
|
||||||
|
[✓] Feeds snapshot hash: OK
|
||||||
|
[✓] Policy + mutes hash: OK
|
||||||
|
[✓] Toolchain hash: OK
|
||||||
|
[✓] SBOM/VEX outputs: OK
|
||||||
|
|
||||||
|
Result: VERIFIED
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Future Extensions
|
||||||
|
|
||||||
|
* Support **SPDX 3.0.1** alongside CycloneDX 1.6.
|
||||||
|
* Add **per-file Merkle proofs** for local scans.
|
||||||
|
* Ledger anchoring (Rekor, distributed Proof-Market).
|
||||||
|
* Post-quantum signatures (Dilithium/Falcon).
|
||||||
|
* Replay orchestration API (`/api/replay/:id`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Summary
|
||||||
|
|
||||||
|
Deterministic Replay freezes every element of a scan:
|
||||||
|
|
||||||
|
> *image → feeds → policy → toolchain → environment → outputs → signatures.*
|
||||||
|
|
||||||
|
By enforcing canonical input/output states and verifiable cryptographic bindings, Stella Ops achieves **regulatory-grade replayability**, **regional crypto compliance**, and **immutable provenance** across all scans.
|
||||||
|
|
||||||
|
---
|
||||||
113
docs/replay/DEVS_GUIDE_REPLAY.md
Normal file
113
docs/replay/DEVS_GUIDE_REPLAY.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# Stella Ops — Developer Guide: Deterministic Replay
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Deterministic Replay ensures any past scan can be re-executed byte-for-byte, producing identical SBOM, Findings, and VEX results, cryptographically verifiable for audits or compliance.
|
||||||
|
|
||||||
|
Replay is the foundation for:
|
||||||
|
- **Audit proofs** (exact past state reproduction)
|
||||||
|
- **Diff analysis** (feeds, policies, tool versions)
|
||||||
|
- **Cross-region verification** (same outputs on different hosts)
|
||||||
|
- **Long-term cryptographic trust** (re-sign with new crypto profiles)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
| Term | Description |
|
||||||
|
|------|--------------|
|
||||||
|
| **Replay Manifest** | Immutable JSON describing all inputs, tools, env, and outputs of a scan. |
|
||||||
|
| **InputBundle** | Snapshot of feeds, rules, policies, and toolchain binaries used. |
|
||||||
|
| **OutputBundle** | SBOM, Findings, VEX, and logs from a completed scan. |
|
||||||
|
| **Layer Merkle** | Per-layer hash tree for precise deduplication and drift detection. |
|
||||||
|
| **DSSE Envelope** | Digital signature wrapper for each attestation (SBOM, Findings, Manifest, etc.). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What to Freeze
|
||||||
|
|
||||||
|
| Category | Example Contents | Required in Manifest |
|
||||||
|
|-----------|------------------|----------------------|
|
||||||
|
| **Subject** | OCI image digest, per-layer Merkle roots | ✅ |
|
||||||
|
| **Outputs** | SBOM, Findings, VEX, logs (content hashes) | ✅ |
|
||||||
|
| **Toolchain** | Sbomer, Scanner, Vexer binaries + versions + SHA256 | ✅ |
|
||||||
|
| **Feeds/VEX sources** | Full or pruned snapshot with Merkle proofs | ✅ |
|
||||||
|
| **Policy Bundle** | Lattice rules, mutes, trust profiles, thresholds | ✅ |
|
||||||
|
| **Environment** | OS, arch, locale, TZ, deterministic seed, runtime flags | ✅ |
|
||||||
|
| **Crypto Profile** | Algorithm suites (FIPS, GOST, SM, eIDAS) | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Replay Modes
|
||||||
|
|
||||||
|
| Mode | Purpose | Input Variation | Expected Output |
|
||||||
|
|------|----------|-----------------|-----------------|
|
||||||
|
| **Strict Replay** | Audit proof | None | Bit-for-bit identical |
|
||||||
|
| **What-If Replay** | Change impact analysis | One dimension (feeds/tools/policy) | Deterministic diff |
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
|
||||||
|
stella replay manifest.json --strict
|
||||||
|
stella replay manifest.json --what-if --vary=feeds
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Developer Responsibilities
|
||||||
|
|
||||||
|
| Module | Role |
|
||||||
|
|---------|------|
|
||||||
|
| **Scanner.WebService** | Capture full input set and produce Replay Manifest + DSSE sigs. |
|
||||||
|
| **Sbomer** | Generate deterministic SBOM; normalize ordering and JSON formatting. |
|
||||||
|
| **Vexer/Excititor** | Apply lattice and mutes from policy bundle; record gating logic. |
|
||||||
|
| **Feedser/Concelier** | Freeze and export feed snapshots or Merkle proofs. |
|
||||||
|
| **Authority** | Manage signer keys and crypto profiles; issue DSSE envelopes. |
|
||||||
|
| **CLI** | Provide `scan --record`, `replay`, `verify`, `diff` commands. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. `stella scan image:tag --record out/`
|
||||||
|
- Generates Replay Manifest, InputBundle, OutputBundle, DSSE sigs.
|
||||||
|
2. `stella verify manifest.json`
|
||||||
|
- Validates hashes, signatures, and completeness.
|
||||||
|
3. `stella replay manifest.json --strict`
|
||||||
|
- Re-executes in sealed mode; expect byte-identical results.
|
||||||
|
4. `stella replay manifest.json --what-if --vary=feeds`
|
||||||
|
- Runs with new feeds; diff is attributed to feeds only.
|
||||||
|
5. `stella diff manifestA manifestB`
|
||||||
|
- Attribute differences by hash comparison.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
- **Mongo collections**
|
||||||
|
- `replay_runs`: manifest + DSSE envelopes + status
|
||||||
|
- `bundles`: content-addressed (input/output/rootpack)
|
||||||
|
- `subjects`: OCI digests, Merkle roots per layer
|
||||||
|
- **File store**
|
||||||
|
- Bundles stored as `<sha256>.tar.zst`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Developer Checklist
|
||||||
|
|
||||||
|
- [ ] All inputs (feeds, policies, tools, env) hashed and recorded.
|
||||||
|
- [ ] JSON normalization: key order, number format, newline mode.
|
||||||
|
- [ ] Random seed = `H(scan.id || MerkleRootAllLayers)`.
|
||||||
|
- [ ] Clock fixed to `scan.time` unless policy requires “now”.
|
||||||
|
- [ ] DSSE multi-sig supported (FIPS + regional).
|
||||||
|
- [ ] Manifest signed + optionally anchored to Rekor ledger.
|
||||||
|
- [ ] Replay comparison mode tested across x64/arm64.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
See also:
|
||||||
|
- `DETERMINISTIC_REPLAY.md` — detailed manifest schema & CLI examples.
|
||||||
|
- `../docs/CRYPTO_SOVEREIGN_READY.md` — RootPack and dual-signature handling.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -110,6 +110,24 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
|
|||||||
{
|
{
|
||||||
var options = scope.ServiceProvider.GetRequiredService<IOptions<StellaOpsAuthorityOptions>>();
|
var options = scope.ServiceProvider.GetRequiredService<IOptions<StellaOpsAuthorityOptions>>();
|
||||||
Assert.True(options.Value.Bootstrap.Enabled);
|
Assert.True(options.Value.Bootstrap.Enabled);
|
||||||
|
var seededAccount = Assert.Single(options.Value.Delegation.ServiceAccounts);
|
||||||
|
Assert.True(seededAccount.Enabled);
|
||||||
|
var accountStore = scope.ServiceProvider.GetRequiredService<IAuthorityServiceAccountStore>();
|
||||||
|
var existingDocument = await accountStore.FindByAccountIdAsync(ServiceAccountId, CancellationToken.None);
|
||||||
|
var document = existingDocument ?? new AuthorityServiceAccountDocument { AccountId = ServiceAccountId };
|
||||||
|
document.Tenant = TenantId;
|
||||||
|
document.DisplayName = "Observability Exporter";
|
||||||
|
document.Description = "Automates evidence exports.";
|
||||||
|
document.Enabled = true;
|
||||||
|
document.AllowedScopes = new List<string> { "jobs:read", "findings:read" };
|
||||||
|
document.AuthorizedClients = new List<string> { "export-center-worker" };
|
||||||
|
document.Attributes = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["env"] = new List<string> { "prod" },
|
||||||
|
["owner"] = new List<string> { "vuln-team" },
|
||||||
|
["business_tier"] = new List<string> { "tier-1" }
|
||||||
|
};
|
||||||
|
await accountStore.UpsertAsync(document, CancellationToken.None);
|
||||||
var endpoints = scope.ServiceProvider.GetRequiredService<EndpointDataSource>().Endpoints;
|
var endpoints = scope.ServiceProvider.GetRequiredService<EndpointDataSource>().Endpoints;
|
||||||
var serviceAccountsEndpoint = endpoints
|
var serviceAccountsEndpoint = endpoints
|
||||||
.OfType<RouteEndpoint>()
|
.OfType<RouteEndpoint>()
|
||||||
@@ -142,6 +160,14 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
|
|||||||
Assert.Equal(new[] { "vuln-team" }, ownerValues);
|
Assert.Equal(new[] { "vuln-team" }, ownerValues);
|
||||||
Assert.True(serviceAccount.Attributes.TryGetValue("business_tier", out var tierValues));
|
Assert.True(serviceAccount.Attributes.TryGetValue("business_tier", out var tierValues));
|
||||||
Assert.Equal(new[] { "tier-1" }, tierValues);
|
Assert.Equal(new[] { "tier-1" }, tierValues);
|
||||||
|
|
||||||
|
await using (var verificationScope = app.Services.CreateAsyncScope())
|
||||||
|
{
|
||||||
|
var accountStore = verificationScope.ServiceProvider.GetRequiredService<IAuthorityServiceAccountStore>();
|
||||||
|
var document = await accountStore.FindByAccountIdAsync(ServiceAccountId, CancellationToken.None);
|
||||||
|
Assert.NotNull(document);
|
||||||
|
Assert.True(document!.Enabled);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -165,7 +191,7 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
|
|||||||
TokenKind = "service_account"
|
TokenKind = "service_account"
|
||||||
};
|
};
|
||||||
|
|
||||||
await tokenStore.InsertAsync(document, CancellationToken.None).ConfigureAwait(false);
|
await tokenStore.InsertAsync(document, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
using var client = app.CreateClient();
|
using var client = app.CreateClient();
|
||||||
@@ -251,7 +277,7 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
|
|||||||
Tenant = TenantId,
|
Tenant = TenantId,
|
||||||
ServiceAccountId = ServiceAccountId,
|
ServiceAccountId = ServiceAccountId,
|
||||||
TokenKind = "service_account"
|
TokenKind = "service_account"
|
||||||
}, CancellationToken.None).ConfigureAwait(false);
|
}, CancellationToken.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,14 +304,14 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
|
|||||||
foreach (var tokenId in tokenIds)
|
foreach (var tokenId in tokenIds)
|
||||||
{
|
{
|
||||||
var sessionAccessor = scope.ServiceProvider.GetRequiredService<IAuthorityMongoSessionAccessor>();
|
var sessionAccessor = scope.ServiceProvider.GetRequiredService<IAuthorityMongoSessionAccessor>();
|
||||||
var session = await sessionAccessor.GetSessionAsync().ConfigureAwait(false);
|
var session = await sessionAccessor.GetSessionAsync();
|
||||||
var token = await tokenStore.FindByTokenIdAsync(tokenId, CancellationToken.None, session).ConfigureAwait(false);
|
var token = await tokenStore.FindByTokenIdAsync(tokenId, CancellationToken.None, session);
|
||||||
Assert.NotNull(token);
|
Assert.NotNull(token);
|
||||||
Assert.Equal("revoked", token!.Status);
|
Assert.Equal("revoked", token!.Status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var audit = Assert.Single(sink.Events.Where(evt => evt.EventType == "authority.delegation.revoked"));
|
var audit = Assert.Single(sink.Events, evt => evt.EventType == "authority.delegation.revoked");
|
||||||
Assert.Equal(AuthEventOutcome.Success, audit.Outcome);
|
Assert.Equal(AuthEventOutcome.Success, audit.Outcome);
|
||||||
Assert.Equal("operator_request", audit.Reason);
|
Assert.Equal("operator_request", audit.Reason);
|
||||||
Assert.Contains(audit.Properties, property =>
|
Assert.Contains(audit.Properties, property =>
|
||||||
@@ -389,7 +415,7 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
|
|||||||
Tenant = TenantId,
|
Tenant = TenantId,
|
||||||
ServiceAccountId = ServiceAccountId,
|
ServiceAccountId = ServiceAccountId,
|
||||||
TokenKind = "service_account"
|
TokenKind = "service_account"
|
||||||
}, CancellationToken.None).ConfigureAwait(false);
|
}, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
using var client = app.CreateClient();
|
using var client = app.CreateClient();
|
||||||
@@ -447,7 +473,7 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
|
|||||||
Tenant = TenantId,
|
Tenant = TenantId,
|
||||||
ServiceAccountId = ServiceAccountId,
|
ServiceAccountId = ServiceAccountId,
|
||||||
TokenKind = "service_account"
|
TokenKind = "service_account"
|
||||||
}, CancellationToken.None).ConfigureAwait(false);
|
}, CancellationToken.None);
|
||||||
|
|
||||||
await tokenStore.InsertAsync(new AuthorityTokenDocument
|
await tokenStore.InsertAsync(new AuthorityTokenDocument
|
||||||
{
|
{
|
||||||
@@ -459,7 +485,7 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
|
|||||||
Tenant = TenantId,
|
Tenant = TenantId,
|
||||||
ServiceAccountId = ServiceAccountId,
|
ServiceAccountId = ServiceAccountId,
|
||||||
TokenKind = "service_account"
|
TokenKind = "service_account"
|
||||||
}, CancellationToken.None).ConfigureAwait(false);
|
}, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
using var client = app.CreateClient();
|
using var client = app.CreateClient();
|
||||||
@@ -491,7 +517,7 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
|
|||||||
{
|
{
|
||||||
await using var scope = firstApp.Services.CreateAsyncScope();
|
await using var scope = firstApp.Services.CreateAsyncScope();
|
||||||
var store = scope.ServiceProvider.GetRequiredService<IAuthorityServiceAccountStore>();
|
var store = scope.ServiceProvider.GetRequiredService<IAuthorityServiceAccountStore>();
|
||||||
var document = await store.FindByAccountIdAsync(ServiceAccountId, CancellationToken.None).ConfigureAwait(false);
|
var document = await store.FindByAccountIdAsync(ServiceAccountId, CancellationToken.None);
|
||||||
|
|
||||||
Assert.NotNull(document);
|
Assert.NotNull(document);
|
||||||
initialId = document!.Id;
|
initialId = document!.Id;
|
||||||
@@ -502,7 +528,7 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
|
|||||||
{
|
{
|
||||||
await using var scope = secondApp.Services.CreateAsyncScope();
|
await using var scope = secondApp.Services.CreateAsyncScope();
|
||||||
var store = scope.ServiceProvider.GetRequiredService<IAuthorityServiceAccountStore>();
|
var store = scope.ServiceProvider.GetRequiredService<IAuthorityServiceAccountStore>();
|
||||||
var document = await store.FindByAccountIdAsync(ServiceAccountId, CancellationToken.None).ConfigureAwait(false);
|
var document = await store.FindByAccountIdAsync(ServiceAccountId, CancellationToken.None);
|
||||||
|
|
||||||
Assert.NotNull(document);
|
Assert.NotNull(document);
|
||||||
Assert.Equal(initialId, document!.Id);
|
Assert.Equal(initialId, document!.Id);
|
||||||
@@ -521,6 +547,7 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
|
|||||||
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__QUOTAS__MAXACTIVETOKENS", "50");
|
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__QUOTAS__MAXACTIVETOKENS", "50");
|
||||||
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ACCOUNTID", ServiceAccountId);
|
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ACCOUNTID", ServiceAccountId);
|
||||||
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__TENANT", TenantId);
|
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__TENANT", TenantId);
|
||||||
|
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ENABLED", "true");
|
||||||
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__DISPLAYNAME", "Observability Exporter");
|
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__DISPLAYNAME", "Observability Exporter");
|
||||||
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__DESCRIPTION", "Automates evidence exports.");
|
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__DESCRIPTION", "Automates evidence exports.");
|
||||||
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ALLOWEDSCOPES__0", "jobs:read");
|
Environment.SetEnvironmentVariable("STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ALLOWEDSCOPES__0", "jobs:read");
|
||||||
@@ -568,25 +595,31 @@ public sealed class ServiceAccountAdminEndpointsTests : IClassFixture<AuthorityW
|
|||||||
|
|
||||||
options.Delegation.Quotas.MaxActiveTokens = 50;
|
options.Delegation.Quotas.MaxActiveTokens = 50;
|
||||||
|
|
||||||
if (options.Delegation.ServiceAccounts.Count == 0)
|
var serviceAccount = options.Delegation.ServiceAccounts
|
||||||
|
.FirstOrDefault(account => string.Equals(account.AccountId, ServiceAccountId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (serviceAccount is null)
|
||||||
{
|
{
|
||||||
var serviceAccount = new AuthorityServiceAccountSeedOptions
|
serviceAccount = new AuthorityServiceAccountSeedOptions();
|
||||||
{
|
|
||||||
AccountId = ServiceAccountId,
|
|
||||||
Tenant = TenantId,
|
|
||||||
DisplayName = "Observability Exporter",
|
|
||||||
Description = "Automates evidence exports."
|
|
||||||
};
|
|
||||||
|
|
||||||
serviceAccount.AllowedScopes.Add("jobs:read");
|
|
||||||
serviceAccount.AllowedScopes.Add("findings:read");
|
|
||||||
serviceAccount.AuthorizedClients.Add("export-center-worker");
|
|
||||||
serviceAccount.Attributes["env"] = new List<string> { "prod" };
|
|
||||||
serviceAccount.Attributes["owner"] = new List<string> { "vuln-team" };
|
|
||||||
serviceAccount.Attributes["business_tier"] = new List<string> { "tier-1" };
|
|
||||||
|
|
||||||
options.Delegation.ServiceAccounts.Add(serviceAccount);
|
options.Delegation.ServiceAccounts.Add(serviceAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serviceAccount.AccountId = ServiceAccountId;
|
||||||
|
serviceAccount.Tenant = TenantId;
|
||||||
|
serviceAccount.DisplayName = "Observability Exporter";
|
||||||
|
serviceAccount.Description = "Automates evidence exports.";
|
||||||
|
serviceAccount.Enabled = true;
|
||||||
|
|
||||||
|
serviceAccount.AllowedScopes.Clear();
|
||||||
|
serviceAccount.AllowedScopes.Add("jobs:read");
|
||||||
|
serviceAccount.AllowedScopes.Add("findings:read");
|
||||||
|
|
||||||
|
serviceAccount.AuthorizedClients.Clear();
|
||||||
|
serviceAccount.AuthorizedClients.Add("export-center-worker");
|
||||||
|
|
||||||
|
serviceAccount.Attributes["env"] = new List<string> { "prod" };
|
||||||
|
serviceAccount.Attributes["owner"] = new List<string> { "vuln-team" };
|
||||||
|
serviceAccount.Attributes["business_tier"] = new List<string> { "tier-1" };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ public sealed class ConsoleEndpointsTests
|
|||||||
Assert.Equal(1, tenants.GetArrayLength());
|
Assert.Equal(1, tenants.GetArrayLength());
|
||||||
Assert.Equal("tenant-default", tenants[0].GetProperty("id").GetString());
|
Assert.Equal("tenant-default", tenants[0].GetProperty("id").GetString());
|
||||||
|
|
||||||
var events = sink.Events;
|
var events = sink.Events;
|
||||||
var authorizeEvent = Assert.Single(events.Where(evt => evt.EventType == "authority.resource.authorize"));
|
var authorizeEvent = Assert.Single(events, evt => evt.EventType == "authority.resource.authorize");
|
||||||
Assert.Equal(AuthEventOutcome.Success, authorizeEvent.Outcome);
|
Assert.Equal(AuthEventOutcome.Success, authorizeEvent.Outcome);
|
||||||
|
|
||||||
var consoleEvent = Assert.Single(events.Where(evt => evt.EventType == "authority.console.tenants.read"));
|
var consoleEvent = Assert.Single(events, evt => evt.EventType == "authority.console.tenants.read");
|
||||||
Assert.Equal(AuthEventOutcome.Success, consoleEvent.Outcome);
|
Assert.Equal(AuthEventOutcome.Success, consoleEvent.Outcome);
|
||||||
Assert.Contains("tenant.resolved", consoleEvent.Properties.Select(property => property.Name));
|
Assert.Contains("tenant.resolved", consoleEvent.Properties.Select(property => property.Name));
|
||||||
Assert.Equal(2, events.Count);
|
Assert.Equal(2, events.Count);
|
||||||
@@ -146,10 +146,10 @@ public sealed class ConsoleEndpointsTests
|
|||||||
Assert.Equal("tenant-default", json.RootElement.GetProperty("tenant").GetString());
|
Assert.Equal("tenant-default", json.RootElement.GetProperty("tenant").GetString());
|
||||||
|
|
||||||
var events = sink.Events;
|
var events = sink.Events;
|
||||||
var authorizeEvent = Assert.Single(events.Where(evt => evt.EventType == "authority.resource.authorize"));
|
var authorizeEvent = Assert.Single(events, evt => evt.EventType == "authority.resource.authorize");
|
||||||
Assert.Equal(AuthEventOutcome.Success, authorizeEvent.Outcome);
|
Assert.Equal(AuthEventOutcome.Success, authorizeEvent.Outcome);
|
||||||
|
|
||||||
var consoleEvent = Assert.Single(events.Where(evt => evt.EventType == "authority.console.profile.read"));
|
var consoleEvent = Assert.Single(events, evt => evt.EventType == "authority.console.profile.read");
|
||||||
Assert.Equal(AuthEventOutcome.Success, consoleEvent.Outcome);
|
Assert.Equal(AuthEventOutcome.Success, consoleEvent.Outcome);
|
||||||
Assert.Equal(2, events.Count);
|
Assert.Equal(2, events.Count);
|
||||||
}
|
}
|
||||||
@@ -184,10 +184,10 @@ public sealed class ConsoleEndpointsTests
|
|||||||
Assert.Equal("token-abc", json.RootElement.GetProperty("tokenId").GetString());
|
Assert.Equal("token-abc", json.RootElement.GetProperty("tokenId").GetString());
|
||||||
|
|
||||||
var events = sink.Events;
|
var events = sink.Events;
|
||||||
var authorizeEvent = Assert.Single(events.Where(evt => evt.EventType == "authority.resource.authorize"));
|
var authorizeEvent = Assert.Single(events, evt => evt.EventType == "authority.resource.authorize");
|
||||||
Assert.Equal(AuthEventOutcome.Success, authorizeEvent.Outcome);
|
Assert.Equal(AuthEventOutcome.Success, authorizeEvent.Outcome);
|
||||||
|
|
||||||
var consoleEvent = Assert.Single(events.Where(evt => evt.EventType == "authority.console.token.introspect"));
|
var consoleEvent = Assert.Single(events, evt => evt.EventType == "authority.console.token.introspect");
|
||||||
Assert.Equal(AuthEventOutcome.Success, consoleEvent.Outcome);
|
Assert.Equal(AuthEventOutcome.Success, consoleEvent.Outcome);
|
||||||
Assert.Equal(2, events.Count);
|
Assert.Equal(2, events.Count);
|
||||||
}
|
}
|
||||||
@@ -287,7 +287,7 @@ public sealed class ConsoleEndpointsTests
|
|||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.MapConsoleEndpoints();
|
app.MapConsoleEndpoints();
|
||||||
|
|
||||||
await app.StartAsync().ConfigureAwait(false);
|
await app.StartAsync();
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,12 +321,11 @@ public sealed class ConsoleEndpointsTests
|
|||||||
|
|
||||||
private sealed class TestAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
private sealed class TestAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||||
{
|
{
|
||||||
public TestAuthenticationHandler(
|
public TestAuthenticationHandler(
|
||||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||||
ILoggerFactory logger,
|
ILoggerFactory logger,
|
||||||
UrlEncoder encoder,
|
UrlEncoder encoder)
|
||||||
ISystemClock clock)
|
: base(options, logger, encoder)
|
||||||
: base(options, logger, encoder, clock)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,4 +355,4 @@ internal static class HostTestClientExtensions
|
|||||||
internal static class TestAuthenticationDefaults
|
internal static class TestAuthenticationDefaults
|
||||||
{
|
{
|
||||||
public const string AuthenticationScheme = "AuthorityConsoleTests";
|
public const string AuthenticationScheme = "AuthorityConsoleTests";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public sealed class AuthorityWebApplicationFactory : WebApplicationFactory<Progr
|
|||||||
private const string DelegationQuotaKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__QUOTAS__MAXACTIVETOKENS";
|
private const string DelegationQuotaKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__QUOTAS__MAXACTIVETOKENS";
|
||||||
private const string ServiceAccountIdKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ACCOUNTID";
|
private const string ServiceAccountIdKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ACCOUNTID";
|
||||||
private const string ServiceAccountTenantKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__TENANT";
|
private const string ServiceAccountTenantKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__TENANT";
|
||||||
|
private const string ServiceAccountEnabledKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ENABLED";
|
||||||
private const string ServiceAccountDisplayNameKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__DISPLAYNAME";
|
private const string ServiceAccountDisplayNameKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__DISPLAYNAME";
|
||||||
private const string ServiceAccountDescriptionKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__DESCRIPTION";
|
private const string ServiceAccountDescriptionKey = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__DESCRIPTION";
|
||||||
private const string ServiceAccountScope0Key = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ALLOWEDSCOPES__0";
|
private const string ServiceAccountScope0Key = "STELLAOPS_AUTHORITY_AUTHORITY__DELEGATION__SERVICEACCOUNTS__0__ALLOWEDSCOPES__0";
|
||||||
@@ -64,6 +65,7 @@ public sealed class AuthorityWebApplicationFactory : WebApplicationFactory<Progr
|
|||||||
Environment.SetEnvironmentVariable(DelegationQuotaKey, "50");
|
Environment.SetEnvironmentVariable(DelegationQuotaKey, "50");
|
||||||
Environment.SetEnvironmentVariable(ServiceAccountIdKey, "svc-observer");
|
Environment.SetEnvironmentVariable(ServiceAccountIdKey, "svc-observer");
|
||||||
Environment.SetEnvironmentVariable(ServiceAccountTenantKey, "tenant-default");
|
Environment.SetEnvironmentVariable(ServiceAccountTenantKey, "tenant-default");
|
||||||
|
Environment.SetEnvironmentVariable(ServiceAccountEnabledKey, "true");
|
||||||
Environment.SetEnvironmentVariable(ServiceAccountDisplayNameKey, "Observability Exporter");
|
Environment.SetEnvironmentVariable(ServiceAccountDisplayNameKey, "Observability Exporter");
|
||||||
Environment.SetEnvironmentVariable(ServiceAccountDescriptionKey, "Automates evidence exports.");
|
Environment.SetEnvironmentVariable(ServiceAccountDescriptionKey, "Automates evidence exports.");
|
||||||
Environment.SetEnvironmentVariable(ServiceAccountScope0Key, "jobs:read");
|
Environment.SetEnvironmentVariable(ServiceAccountScope0Key, "jobs:read");
|
||||||
@@ -85,11 +87,12 @@ public sealed class AuthorityWebApplicationFactory : WebApplicationFactory<Progr
|
|||||||
["Authority:SchemaVersion"] = "1",
|
["Authority:SchemaVersion"] = "1",
|
||||||
["Authority:Storage:ConnectionString"] = mongoRunner.ConnectionString,
|
["Authority:Storage:ConnectionString"] = mongoRunner.ConnectionString,
|
||||||
["Authority:Storage:DatabaseName"] = "authority-tests",
|
["Authority:Storage:DatabaseName"] = "authority-tests",
|
||||||
["Authority:Signing:Enabled"] = "false",
|
["Authority:Signing:Enabled"] = "false",
|
||||||
["Authority:Notifications:AckTokens:Enabled"] = "false",
|
["Authority:Notifications:AckTokens:Enabled"] = "false",
|
||||||
["Authority:Notifications:Webhooks:Enabled"] = "false"
|
["Authority:Notifications:Webhooks:Enabled"] = "false",
|
||||||
});
|
["Authority:Delegation:ServiceAccounts:0:Enabled"] = "true"
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,11 +106,12 @@ public sealed class AuthorityWebApplicationFactory : WebApplicationFactory<Progr
|
|||||||
["Authority:SchemaVersion"] = "1",
|
["Authority:SchemaVersion"] = "1",
|
||||||
["Authority:Storage:ConnectionString"] = mongoRunner.ConnectionString,
|
["Authority:Storage:ConnectionString"] = mongoRunner.ConnectionString,
|
||||||
["Authority:Storage:DatabaseName"] = "authority-tests",
|
["Authority:Storage:DatabaseName"] = "authority-tests",
|
||||||
["Authority:Signing:Enabled"] = "false",
|
["Authority:Signing:Enabled"] = "false",
|
||||||
["Authority:Notifications:AckTokens:Enabled"] = "false",
|
["Authority:Notifications:AckTokens:Enabled"] = "false",
|
||||||
["Authority:Notifications:Webhooks:Enabled"] = "false"
|
["Authority:Notifications:Webhooks:Enabled"] = "false",
|
||||||
});
|
["Authority:Delegation:ServiceAccounts:0:Enabled"] = "true"
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return base.CreateHost(builder);
|
return base.CreateHost(builder);
|
||||||
}
|
}
|
||||||
@@ -150,6 +154,7 @@ public sealed class AuthorityWebApplicationFactory : WebApplicationFactory<Progr
|
|||||||
Environment.SetEnvironmentVariable(DelegationQuotaKey, null);
|
Environment.SetEnvironmentVariable(DelegationQuotaKey, null);
|
||||||
Environment.SetEnvironmentVariable(ServiceAccountIdKey, null);
|
Environment.SetEnvironmentVariable(ServiceAccountIdKey, null);
|
||||||
Environment.SetEnvironmentVariable(ServiceAccountTenantKey, null);
|
Environment.SetEnvironmentVariable(ServiceAccountTenantKey, null);
|
||||||
|
Environment.SetEnvironmentVariable(ServiceAccountEnabledKey, null);
|
||||||
Environment.SetEnvironmentVariable(ServiceAccountDisplayNameKey, null);
|
Environment.SetEnvironmentVariable(ServiceAccountDisplayNameKey, null);
|
||||||
Environment.SetEnvironmentVariable(ServiceAccountDescriptionKey, null);
|
Environment.SetEnvironmentVariable(ServiceAccountDescriptionKey, null);
|
||||||
Environment.SetEnvironmentVariable(ServiceAccountScope0Key, null);
|
Environment.SetEnvironmentVariable(ServiceAccountScope0Key, null);
|
||||||
@@ -168,7 +173,7 @@ public sealed class AuthorityWebApplicationFactory : WebApplicationFactory<Progr
|
|||||||
// ignore cleanup failures
|
// ignore cleanup failures
|
||||||
}
|
}
|
||||||
|
|
||||||
await base.DisposeAsync().ConfigureAwait(false);
|
await base.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
Task IAsyncLifetime.DisposeAsync() => DisposeAsync().AsTask();
|
Task IAsyncLifetime.DisposeAsync() => DisposeAsync().AsTask();
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace StellaOps.Authority.Tests.Infrastructure;
|
||||||
|
|
||||||
|
internal sealed class EnvironmentVariableScope : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, string?> originals = new(StringComparer.Ordinal);
|
||||||
|
private bool disposed;
|
||||||
|
|
||||||
|
public EnvironmentVariableScope(IEnumerable<KeyValuePair<string, string?>> overrides)
|
||||||
|
{
|
||||||
|
if (overrides is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(overrides));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var kvp in overrides)
|
||||||
|
{
|
||||||
|
if (originals.ContainsKey(kvp.Key))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
originals.Add(kvp.Key, Environment.GetEnvironmentVariable(kvp.Key));
|
||||||
|
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var kvp in originals)
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,9 +17,8 @@ internal sealed class TestAuthHandler : AuthenticationHandler<AuthenticationSche
|
|||||||
public TestAuthHandler(
|
public TestAuthHandler(
|
||||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||||
ILoggerFactory logger,
|
ILoggerFactory logger,
|
||||||
UrlEncoder encoder,
|
UrlEncoder encoder)
|
||||||
ISystemClock clock)
|
: base(options, logger, encoder)
|
||||||
: base(options, logger, encoder, clock)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,14 @@ public sealed class NotifyAckTokenRotationEndpointTests : IClassFixture<Authorit
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Rotate_ReturnsOk_AndEmitsAuditEvent()
|
public async Task Rotate_ReturnsOk_AndEmitsAuditEvent()
|
||||||
{
|
{
|
||||||
|
const string AckEnabledKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__ENABLED";
|
||||||
|
const string AckActiveKeyIdKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__ACTIVEKEYID";
|
||||||
|
const string AckKeyPathKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__KEYPATH";
|
||||||
|
const string AckKeySourceKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__KEYSOURCE";
|
||||||
|
const string AckAlgorithmKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__ACKTOKENS__ALGORITHM";
|
||||||
|
const string WebhooksEnabledKey = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__WEBHOOKS__ENABLED";
|
||||||
|
const string WebhooksAllowedHost0Key = "STELLAOPS_AUTHORITY_AUTHORITY__NOTIFICATIONS__WEBHOOKS__ALLOWEDHOSTS__0";
|
||||||
|
|
||||||
var tempDir = Directory.CreateTempSubdirectory("ack-rotation-success");
|
var tempDir = Directory.CreateTempSubdirectory("ack-rotation-success");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -43,10 +51,21 @@ public sealed class NotifyAckTokenRotationEndpointTests : IClassFixture<Authorit
|
|||||||
CreateEcPrivateKey(key1Path);
|
CreateEcPrivateKey(key1Path);
|
||||||
CreateEcPrivateKey(key2Path);
|
CreateEcPrivateKey(key2Path);
|
||||||
|
|
||||||
|
using var env = new EnvironmentVariableScope(new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string?>(AckEnabledKey, "true"),
|
||||||
|
new KeyValuePair<string, string?>(AckActiveKeyIdKey, "ack-key-1"),
|
||||||
|
new KeyValuePair<string, string?>(AckKeyPathKey, key1Path),
|
||||||
|
new KeyValuePair<string, string?>(AckKeySourceKey, "file"),
|
||||||
|
new KeyValuePair<string, string?>(AckAlgorithmKey, SignatureAlgorithms.Es256),
|
||||||
|
new KeyValuePair<string, string?>(WebhooksEnabledKey, "true"),
|
||||||
|
new KeyValuePair<string, string?>(WebhooksAllowedHost0Key, "hooks.slack.com")
|
||||||
|
});
|
||||||
|
|
||||||
var sink = new RecordingAuthEventSink();
|
var sink = new RecordingAuthEventSink();
|
||||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T12:00:00Z"));
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T12:00:00Z"));
|
||||||
|
|
||||||
using var app = factory.WithWebHostBuilder(host =>
|
using var scopedFactory = factory.WithWebHostBuilder(host =>
|
||||||
{
|
{
|
||||||
host.ConfigureAppConfiguration((_, configuration) =>
|
host.ConfigureAppConfiguration((_, configuration) =>
|
||||||
{
|
{
|
||||||
@@ -87,7 +106,8 @@ public sealed class NotifyAckTokenRotationEndpointTests : IClassFixture<Authorit
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
using var client = app.CreateClient();
|
using var client = scopedFactory.CreateClient();
|
||||||
|
|
||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.SchemeName);
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.NotifyAdmin);
|
client.DefaultRequestHeaders.Add("X-Test-Scopes", StellaOpsScopes.NotifyAdmin);
|
||||||
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
client.DefaultRequestHeaders.Add("X-Test-Tenant", "tenant-default");
|
||||||
@@ -106,7 +126,7 @@ public sealed class NotifyAckTokenRotationEndpointTests : IClassFixture<Authorit
|
|||||||
Assert.Equal("ack-key-2", payload!.ActiveKeyId);
|
Assert.Equal("ack-key-2", payload!.ActiveKeyId);
|
||||||
Assert.Equal("ack-key-1", payload.PreviousKeyId);
|
Assert.Equal("ack-key-1", payload.PreviousKeyId);
|
||||||
|
|
||||||
var rotationEvent = Assert.Single(sink.Events.Where(evt => evt.EventType == "notify.ack.key_rotated"));
|
var rotationEvent = Assert.Single(sink.Events, evt => evt.EventType == "notify.ack.key_rotated");
|
||||||
Assert.Equal(AuthEventOutcome.Success, rotationEvent.Outcome);
|
Assert.Equal(AuthEventOutcome.Success, rotationEvent.Outcome);
|
||||||
Assert.Contains(rotationEvent.Properties, property =>
|
Assert.Contains(rotationEvent.Properties, property =>
|
||||||
string.Equals(property.Name, "notify.ack.key_id", StringComparison.Ordinal) &&
|
string.Equals(property.Name, "notify.ack.key_id", StringComparison.Ordinal) &&
|
||||||
@@ -184,7 +204,7 @@ public sealed class NotifyAckTokenRotationEndpointTests : IClassFixture<Authorit
|
|||||||
|
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
|
||||||
var failureEvent = Assert.Single(sink.Events.Where(evt => evt.EventType == "notify.ack.key_rotation_failed"));
|
var failureEvent = Assert.Single(sink.Events, evt => evt.EventType == "notify.ack.key_rotation_failed");
|
||||||
Assert.Equal(AuthEventOutcome.Failure, failureEvent.Outcome);
|
Assert.Equal(AuthEventOutcome.Failure, failureEvent.Outcome);
|
||||||
Assert.Contains("keyId", failureEvent.Reason, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("keyId", failureEvent.Reason, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public sealed class OpenApiDiscoveryEndpointTests : IClassFixture<AuthorityWebAp
|
|||||||
{
|
{
|
||||||
using var client = factory.CreateClient();
|
using var client = factory.CreateClient();
|
||||||
|
|
||||||
using var response = await client.GetAsync("/.well-known/openapi").ConfigureAwait(false);
|
using var response = await client.GetAsync("/.well-known/openapi");
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
Assert.NotNull(response.Headers.ETag);
|
Assert.NotNull(response.Headers.ETag);
|
||||||
@@ -36,7 +36,7 @@ public sealed class OpenApiDiscoveryEndpointTests : IClassFixture<AuthorityWebAp
|
|||||||
var contentType = response.Content.Headers.ContentType?.ToString();
|
var contentType = response.Content.Headers.ContentType?.ToString();
|
||||||
Assert.Equal("application/openapi+json; charset=utf-8", contentType);
|
Assert.Equal("application/openapi+json; charset=utf-8", contentType);
|
||||||
|
|
||||||
var payload = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
var payload = await response.Content.ReadAsStringAsync();
|
||||||
using var document = JsonDocument.Parse(payload);
|
using var document = JsonDocument.Parse(payload);
|
||||||
Assert.Equal("3.1.0", document.RootElement.GetProperty("openapi").GetString());
|
Assert.Equal("3.1.0", document.RootElement.GetProperty("openapi").GetString());
|
||||||
|
|
||||||
@@ -61,12 +61,12 @@ public sealed class OpenApiDiscoveryEndpointTests : IClassFixture<AuthorityWebAp
|
|||||||
using var request = new HttpRequestMessage(HttpMethod.Get, "/.well-known/openapi");
|
using var request = new HttpRequestMessage(HttpMethod.Get, "/.well-known/openapi");
|
||||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/openapi+yaml"));
|
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/openapi+yaml"));
|
||||||
|
|
||||||
using var response = await client.SendAsync(request).ConfigureAwait(false);
|
using var response = await client.SendAsync(request);
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
Assert.Equal("application/openapi+yaml; charset=utf-8", response.Content.Headers.ContentType?.ToString());
|
Assert.Equal("application/openapi+yaml; charset=utf-8", response.Content.Headers.ContentType?.ToString());
|
||||||
|
|
||||||
var payload = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
var payload = await response.Content.ReadAsStringAsync();
|
||||||
Assert.StartsWith("openapi: 3.1.0", payload.TrimStart(), StringComparison.Ordinal);
|
Assert.StartsWith("openapi: 3.1.0", payload.TrimStart(), StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,14 +75,14 @@ public sealed class OpenApiDiscoveryEndpointTests : IClassFixture<AuthorityWebAp
|
|||||||
{
|
{
|
||||||
using var client = factory.CreateClient();
|
using var client = factory.CreateClient();
|
||||||
|
|
||||||
using var initialResponse = await client.GetAsync("/.well-known/openapi").ConfigureAwait(false);
|
using var initialResponse = await client.GetAsync("/.well-known/openapi");
|
||||||
var etag = initialResponse.Headers.ETag;
|
var etag = initialResponse.Headers.ETag;
|
||||||
Assert.NotNull(etag);
|
Assert.NotNull(etag);
|
||||||
|
|
||||||
using var conditionalRequest = new HttpRequestMessage(HttpMethod.Get, "/.well-known/openapi");
|
using var conditionalRequest = new HttpRequestMessage(HttpMethod.Get, "/.well-known/openapi");
|
||||||
conditionalRequest.Headers.IfNoneMatch.Add(etag!);
|
conditionalRequest.Headers.IfNoneMatch.Add(etag!);
|
||||||
|
|
||||||
using var conditionalResponse = await client.SendAsync(conditionalRequest).ConfigureAwait(false);
|
using var conditionalResponse = await client.SendAsync(conditionalRequest);
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.NotModified, conditionalResponse.StatusCode);
|
Assert.Equal(HttpStatusCode.NotModified, conditionalResponse.StatusCode);
|
||||||
Assert.Equal(etag!.Tag, conditionalResponse.Headers.ETag?.Tag);
|
Assert.Equal(etag!.Tag, conditionalResponse.Headers.ETag?.Tag);
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ using Microsoft.Extensions.Options;
|
|||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using StellaOps.Configuration;
|
using StellaOps.Configuration;
|
||||||
|
using OpenIddict.Abstractions;
|
||||||
using StellaOps.Authority.Security;
|
using StellaOps.Authority.Security;
|
||||||
using StellaOps.Auth.Security.Dpop;
|
using StellaOps.Auth.Security.Dpop;
|
||||||
using OpenIddict.Abstractions;
|
|
||||||
using OpenIddict.Extensions;
|
using OpenIddict.Extensions;
|
||||||
using OpenIddict.Server;
|
using OpenIddict.Server;
|
||||||
using OpenIddict.Server.AspNetCore;
|
using OpenIddict.Server.AspNetCore;
|
||||||
@@ -225,7 +225,7 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
|
||||||
transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-observer");
|
SetParameter(transaction, AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-observer");
|
||||||
|
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -296,7 +296,7 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
|
||||||
transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-observer");
|
SetParameter(transaction, AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-observer");
|
||||||
|
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -346,7 +346,7 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "vuln:view");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "vuln:view");
|
||||||
transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln");
|
SetParameter(transaction, AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln");
|
||||||
|
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -398,10 +398,10 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "vuln:view");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "vuln:view");
|
||||||
transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln");
|
SetParameter(transaction, AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln");
|
||||||
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnEnvironmentParameterName, "prod");
|
SetParameter(transaction, AuthorityOpenIddictConstants.VulnEnvironmentParameterName, "prod");
|
||||||
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnOwnerParameterName, "security");
|
SetParameter(transaction, AuthorityOpenIddictConstants.VulnOwnerParameterName, "security");
|
||||||
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnBusinessTierParameterName, "tier-1");
|
SetParameter(transaction, AuthorityOpenIddictConstants.VulnBusinessTierParameterName, "tier-1");
|
||||||
|
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -464,10 +464,10 @@ public class ClientCredentialsHandlersTests
|
|||||||
{
|
{
|
||||||
AccessTokenLifetime = TimeSpan.FromMinutes(5)
|
AccessTokenLifetime = TimeSpan.FromMinutes(5)
|
||||||
};
|
};
|
||||||
transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln");
|
SetParameter(transaction, AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln");
|
||||||
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnEnvironmentParameterName, "prod");
|
SetParameter(transaction, AuthorityOpenIddictConstants.VulnEnvironmentParameterName, "prod");
|
||||||
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnOwnerParameterName, "security");
|
SetParameter(transaction, AuthorityOpenIddictConstants.VulnOwnerParameterName, "security");
|
||||||
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnBusinessTierParameterName, "tier-1");
|
SetParameter(transaction, AuthorityOpenIddictConstants.VulnBusinessTierParameterName, "tier-1");
|
||||||
|
|
||||||
var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
await validateHandler.HandleAsync(validateContext);
|
await validateHandler.HandleAsync(validateContext);
|
||||||
@@ -1018,8 +1018,8 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:operate");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:operate");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.OperatorReasonParameterName, "resume source after maintenance");
|
SetParameter(transaction, AuthorityOpenIddictConstants.OperatorReasonParameterName, "resume source after maintenance");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.OperatorTicketParameterName, "INC-2045");
|
SetParameter(transaction, AuthorityOpenIddictConstants.OperatorTicketParameterName, "INC-2045");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1056,7 +1056,7 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:operate");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:operate");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.OperatorTicketParameterName, "INC-2045");
|
SetParameter(transaction, AuthorityOpenIddictConstants.OperatorTicketParameterName, "INC-2045");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1093,7 +1093,7 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:operate");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:operate");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.OperatorReasonParameterName, "resume source after maintenance");
|
SetParameter(transaction, AuthorityOpenIddictConstants.OperatorReasonParameterName, "resume source after maintenance");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1129,8 +1129,8 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillReasonParameterName, "Backfill drift repair");
|
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillReasonParameterName, "Backfill drift repair");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillTicketParameterName, "INC-9981");
|
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillTicketParameterName, "INC-9981");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1167,7 +1167,7 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillTicketParameterName, "INC-9981");
|
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillTicketParameterName, "INC-9981");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1204,7 +1204,7 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillReasonParameterName, "Backfill drift repair");
|
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillReasonParameterName, "Backfill drift repair");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1242,8 +1242,8 @@ public class ClientCredentialsHandlersTests
|
|||||||
|
|
||||||
var longReason = new string('a', 257);
|
var longReason = new string('a', 257);
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillReasonParameterName, longReason);
|
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillReasonParameterName, longReason);
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillTicketParameterName, "INC-9981");
|
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillTicketParameterName, "INC-9981");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1281,8 +1281,8 @@ public class ClientCredentialsHandlersTests
|
|||||||
|
|
||||||
var longTicket = new string('b', 129);
|
var longTicket = new string('b', 129);
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillReasonParameterName, "Backfill drift repair");
|
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillReasonParameterName, "Backfill drift repair");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillTicketParameterName, longTicket);
|
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillTicketParameterName, longTicket);
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1319,8 +1319,8 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:backfill");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillReasonParameterName, "Backfill drift repair");
|
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillReasonParameterName, "Backfill drift repair");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.BackfillTicketParameterName, "INC-9981");
|
SetParameter(transaction, AuthorityOpenIddictConstants.BackfillTicketParameterName, "INC-9981");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1361,8 +1361,8 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:operate");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:operate");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.OperatorReasonParameterName, "resume source after maintenance");
|
SetParameter(transaction, AuthorityOpenIddictConstants.OperatorReasonParameterName, "resume source after maintenance");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.OperatorTicketParameterName, "INC-2045");
|
SetParameter(transaction, AuthorityOpenIddictConstants.OperatorTicketParameterName, "INC-2045");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1406,8 +1406,8 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:quota");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:quota");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.QuotaReasonParameterName, "raise export center quota for tenant");
|
SetParameter(transaction, AuthorityOpenIddictConstants.QuotaReasonParameterName, "raise export center quota for tenant");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.QuotaTicketParameterName, "CHG-7721");
|
SetParameter(transaction, AuthorityOpenIddictConstants.QuotaTicketParameterName, "CHG-7721");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1481,7 +1481,7 @@ public class ClientCredentialsHandlersTests
|
|||||||
|
|
||||||
var longReason = new string('a', 257);
|
var longReason = new string('a', 257);
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:quota");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:quota");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.QuotaReasonParameterName, longReason);
|
SetParameter(transaction, AuthorityOpenIddictConstants.QuotaReasonParameterName, longReason);
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1518,8 +1518,8 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:quota");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:quota");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.QuotaReasonParameterName, "increase concurrency to unblock digests");
|
SetParameter(transaction, AuthorityOpenIddictConstants.QuotaReasonParameterName, "increase concurrency to unblock digests");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.QuotaTicketParameterName, new string('b', 129));
|
SetParameter(transaction, AuthorityOpenIddictConstants.QuotaTicketParameterName, new string('b', 129));
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1556,7 +1556,7 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:quota");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:quota");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.QuotaReasonParameterName, "grant five extra concurrent backfills");
|
SetParameter(transaction, AuthorityOpenIddictConstants.QuotaReasonParameterName, "grant five extra concurrent backfills");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1598,8 +1598,8 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:quota");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "orch:quota");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.QuotaReasonParameterName, "temporary burst for export audit");
|
SetParameter(transaction, AuthorityOpenIddictConstants.QuotaReasonParameterName, "temporary burst for export audit");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.QuotaTicketParameterName, "RFC-5541");
|
SetParameter(transaction, AuthorityOpenIddictConstants.QuotaTicketParameterName, "RFC-5541");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1796,7 +1796,7 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "export.admin");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "export.admin");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.ExportAdminTicketParameterName, "INC-9001");
|
SetParameter(transaction, AuthorityOpenIddictConstants.ExportAdminTicketParameterName, "INC-9001");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1832,7 +1832,7 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "export.admin");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "export.admin");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.ExportAdminReasonParameterName, "Rotate encryption keys after incident postmortem");
|
SetParameter(transaction, AuthorityOpenIddictConstants.ExportAdminReasonParameterName, "Rotate encryption keys after incident postmortem");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -1868,8 +1868,8 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "export.admin");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "export.admin");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.ExportAdminReasonParameterName, "Rotate encryption keys after incident postmortem");
|
SetParameter(transaction, AuthorityOpenIddictConstants.ExportAdminReasonParameterName, "Rotate encryption keys after incident postmortem");
|
||||||
transaction.Request?.SetParameter(AuthorityOpenIddictConstants.ExportAdminTicketParameterName, "INC-9001");
|
SetParameter(transaction, AuthorityOpenIddictConstants.ExportAdminTicketParameterName, "INC-9001");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await handler.HandleAsync(context);
|
await handler.HandleAsync(context);
|
||||||
@@ -2310,7 +2310,7 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
|
||||||
transaction.Request?.SetParameter("unexpected_param", "value");
|
SetParameter(transaction,"unexpected_param", "value");
|
||||||
|
|
||||||
await handler.HandleAsync(new OpenIddictServerEvents.ValidateTokenRequestContext(transaction));
|
await handler.HandleAsync(new OpenIddictServerEvents.ValidateTokenRequestContext(transaction));
|
||||||
|
|
||||||
@@ -2522,8 +2522,9 @@ public class ClientCredentialsHandlersTests
|
|||||||
|
|
||||||
Assert.True(validateContext.IsRejected);
|
Assert.True(validateContext.IsRejected);
|
||||||
Assert.Equal(OpenIddictConstants.Errors.InvalidClient, validateContext.Error);
|
Assert.Equal(OpenIddictConstants.Errors.InvalidClient, validateContext.Error);
|
||||||
var authenticateHeader = Assert.Single(httpContext.Response.Headers.Select(header => header)
|
var authenticateHeader = Assert.Single(
|
||||||
.Where(header => string.Equals(header.Key, "WWW-Authenticate", StringComparison.OrdinalIgnoreCase))).Value;
|
httpContext.Response.Headers,
|
||||||
|
header => string.Equals(header.Key, "WWW-Authenticate", StringComparison.OrdinalIgnoreCase)).Value;
|
||||||
Assert.Contains("use_dpop_nonce", authenticateHeader.ToString());
|
Assert.Contains("use_dpop_nonce", authenticateHeader.ToString());
|
||||||
Assert.True(httpContext.Response.Headers.TryGetValue("DPoP-Nonce", out var nonceValues));
|
Assert.True(httpContext.Response.Headers.TryGetValue("DPoP-Nonce", out var nonceValues));
|
||||||
Assert.False(StringValues.IsNullOrEmpty(nonceValues));
|
Assert.False(StringValues.IsNullOrEmpty(nonceValues));
|
||||||
@@ -2862,7 +2863,7 @@ public class ClientCredentialsHandlersTests
|
|||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
|
||||||
transaction.Options.AccessTokenLifetime = TimeSpan.FromMinutes(10);
|
transaction.Options.AccessTokenLifetime = TimeSpan.FromMinutes(10);
|
||||||
transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-ops");
|
SetParameter(transaction, AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-ops");
|
||||||
|
|
||||||
var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
await validateHandler.HandleAsync(validateContext);
|
await validateHandler.HandleAsync(validateContext);
|
||||||
@@ -2951,10 +2952,10 @@ public class ClientCredentialsHandlersTests
|
|||||||
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
NullLogger<ValidateClientCredentialsHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "vuln:view vuln:investigate");
|
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "vuln:view vuln:investigate");
|
||||||
transaction.Request.SetParameter(AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln");
|
SetParameter(transaction, AuthorityOpenIddictConstants.ServiceAccountParameterName, "svc-vuln");
|
||||||
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnEnvironmentParameterName, "prod");
|
SetParameter(transaction, AuthorityOpenIddictConstants.VulnEnvironmentParameterName, "prod");
|
||||||
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnOwnerParameterName, "secops");
|
SetParameter(transaction, AuthorityOpenIddictConstants.VulnOwnerParameterName, "secops");
|
||||||
transaction.Request.SetParameter(AuthorityOpenIddictConstants.VulnBusinessTierParameterName, "tier-1");
|
SetParameter(transaction, AuthorityOpenIddictConstants.VulnBusinessTierParameterName, "tier-1");
|
||||||
|
|
||||||
var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
await validateHandler.HandleAsync(validateContext);
|
await validateHandler.HandleAsync(validateContext);
|
||||||
@@ -3071,7 +3072,7 @@ public class TokenValidationHandlersTests
|
|||||||
Request = new OpenIddictRequest()
|
Request = new OpenIddictRequest()
|
||||||
};
|
};
|
||||||
|
|
||||||
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", clientDocument.Plugin);
|
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", ResolveProvider(clientDocument));
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
|
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
|
||||||
{
|
{
|
||||||
Principal = principal,
|
Principal = principal,
|
||||||
@@ -3125,7 +3126,7 @@ public class TokenValidationHandlersTests
|
|||||||
Request = new OpenIddictRequest()
|
Request = new OpenIddictRequest()
|
||||||
};
|
};
|
||||||
|
|
||||||
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", clientDocument.Plugin);
|
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", ResolveProvider(clientDocument));
|
||||||
principal.Identities.First().AddClaim(new Claim(StellaOpsClaimTypes.Tenant, "tenant-beta"));
|
principal.Identities.First().AddClaim(new Claim(StellaOpsClaimTypes.Tenant, "tenant-beta"));
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
|
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
|
||||||
{
|
{
|
||||||
@@ -3175,7 +3176,7 @@ public class TokenValidationHandlersTests
|
|||||||
Request = new OpenIddictRequest()
|
Request = new OpenIddictRequest()
|
||||||
};
|
};
|
||||||
|
|
||||||
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", clientDocument.Plugin);
|
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", ResolveProvider(clientDocument));
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
|
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
|
||||||
{
|
{
|
||||||
Principal = principal,
|
Principal = principal,
|
||||||
@@ -3224,7 +3225,7 @@ public class TokenValidationHandlersTests
|
|||||||
Request = new OpenIddictRequest()
|
Request = new OpenIddictRequest()
|
||||||
};
|
};
|
||||||
|
|
||||||
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", clientDocument.Plugin);
|
var principal = CreatePrincipal(clientDocument.ClientId, "token-tenant", ResolveProvider(clientDocument));
|
||||||
principal.Identities.First().AddClaim(new Claim(StellaOpsClaimTypes.Tenant, "tenant-alpha"));
|
principal.Identities.First().AddClaim(new Claim(StellaOpsClaimTypes.Tenant, "tenant-alpha"));
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
|
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
|
||||||
{
|
{
|
||||||
@@ -3326,7 +3327,7 @@ public class TokenValidationHandlersTests
|
|||||||
Request = new OpenIddictRequest()
|
Request = new OpenIddictRequest()
|
||||||
};
|
};
|
||||||
|
|
||||||
var principal = CreatePrincipal(clientDocument.ClientId, tokenDocument.TokenId, clientDocument.Plugin);
|
var principal = CreatePrincipal(clientDocument.ClientId, tokenDocument.TokenId, ResolveProvider(clientDocument));
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
|
var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
|
||||||
{
|
{
|
||||||
Principal = principal,
|
Principal = principal,
|
||||||
@@ -3963,7 +3964,7 @@ public class ObservabilityIncidentTokenHandlerTests
|
|||||||
Request = new OpenIddictRequest()
|
Request = new OpenIddictRequest()
|
||||||
};
|
};
|
||||||
|
|
||||||
var principal = CreatePrincipal(clientDocument.ClientId, "token-incident", clientDocument.Plugin);
|
var principal = CreatePrincipal(clientDocument.ClientId, "token-incident", ResolveProvider(clientDocument));
|
||||||
principal.SetScopes(StellaOpsScopes.ObservabilityIncident);
|
principal.SetScopes(StellaOpsScopes.ObservabilityIncident);
|
||||||
var staleAuthTime = DateTimeOffset.UtcNow.AddMinutes(-10);
|
var staleAuthTime = DateTimeOffset.UtcNow.AddMinutes(-10);
|
||||||
principal.SetClaim(OpenIddictConstants.Claims.AuthenticationTime, staleAuthTime.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture));
|
principal.SetClaim(OpenIddictConstants.Claims.AuthenticationTime, staleAuthTime.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture));
|
||||||
@@ -4018,7 +4019,7 @@ public class ObservabilityIncidentTokenHandlerTests
|
|||||||
Request = new OpenIddictRequest()
|
Request = new OpenIddictRequest()
|
||||||
};
|
};
|
||||||
|
|
||||||
var principal = CreatePrincipal(clientDocument.ClientId, "token-policy", clientDocument.Plugin);
|
var principal = CreatePrincipal(clientDocument.ClientId, "token-policy", ResolveProvider(clientDocument));
|
||||||
principal.SetScopes(StellaOpsScopes.PolicyPublish);
|
principal.SetScopes(StellaOpsScopes.PolicyPublish);
|
||||||
principal.SetClaim(StellaOpsClaimTypes.PolicyOperation, AuthorityOpenIddictConstants.PolicyOperationPublishValue);
|
principal.SetClaim(StellaOpsClaimTypes.PolicyOperation, AuthorityOpenIddictConstants.PolicyOperationPublishValue);
|
||||||
principal.SetClaim(StellaOpsClaimTypes.PolicyReason, "Publish approved policy");
|
principal.SetClaim(StellaOpsClaimTypes.PolicyReason, "Publish approved policy");
|
||||||
@@ -4075,7 +4076,7 @@ public class ObservabilityIncidentTokenHandlerTests
|
|||||||
Request = new OpenIddictRequest()
|
Request = new OpenIddictRequest()
|
||||||
};
|
};
|
||||||
|
|
||||||
var principal = CreatePrincipal(clientDocument.ClientId, "token-policy-stale", clientDocument.Plugin);
|
var principal = CreatePrincipal(clientDocument.ClientId, "token-policy-stale", ResolveProvider(clientDocument));
|
||||||
principal.SetScopes(StellaOpsScopes.PolicyPublish);
|
principal.SetScopes(StellaOpsScopes.PolicyPublish);
|
||||||
principal.SetClaim(StellaOpsClaimTypes.PolicyOperation, AuthorityOpenIddictConstants.PolicyOperationPublishValue);
|
principal.SetClaim(StellaOpsClaimTypes.PolicyOperation, AuthorityOpenIddictConstants.PolicyOperationPublishValue);
|
||||||
principal.SetClaim(StellaOpsClaimTypes.PolicyDigest, new string('a', 64));
|
principal.SetClaim(StellaOpsClaimTypes.PolicyDigest, new string('a', 64));
|
||||||
@@ -4136,7 +4137,7 @@ public class ObservabilityIncidentTokenHandlerTests
|
|||||||
Request = new OpenIddictRequest()
|
Request = new OpenIddictRequest()
|
||||||
};
|
};
|
||||||
|
|
||||||
var principal = CreatePrincipal(clientDocument.ClientId, $"token-{expectedOperation}", clientDocument.Plugin);
|
var principal = CreatePrincipal(clientDocument.ClientId, $"token-{expectedOperation}", ResolveProvider(clientDocument));
|
||||||
principal.SetScopes(scope);
|
principal.SetScopes(scope);
|
||||||
principal.SetClaim(StellaOpsClaimTypes.PolicyOperation, expectedOperation);
|
principal.SetClaim(StellaOpsClaimTypes.PolicyOperation, expectedOperation);
|
||||||
principal.SetClaim(StellaOpsClaimTypes.PolicyDigest, new string('b', 64));
|
principal.SetClaim(StellaOpsClaimTypes.PolicyDigest, new string('b', 64));
|
||||||
@@ -4245,6 +4246,27 @@ internal static class TestHelpers
|
|||||||
private static string? NormalizeTenant(string? value)
|
private static string? NormalizeTenant(string? value)
|
||||||
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToLowerInvariant();
|
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
public static string ResolveProvider(AuthorityClientDocument document)
|
||||||
|
=> string.IsNullOrWhiteSpace(document.Plugin) ? "standard" : document.Plugin;
|
||||||
|
|
||||||
|
private static OpenIddictRequest GetRequest(OpenIddictServerTransaction transaction)
|
||||||
|
=> transaction.Request ?? throw new InvalidOperationException("OpenIddict request is required for this test.");
|
||||||
|
|
||||||
|
public static void SetParameter(OpenIddictServerTransaction transaction, string name, object? value)
|
||||||
|
{
|
||||||
|
var parameter = value switch
|
||||||
|
{
|
||||||
|
null => default,
|
||||||
|
OpenIddictParameter existing => existing,
|
||||||
|
string s => new OpenIddictParameter(s),
|
||||||
|
bool b => new OpenIddictParameter(b),
|
||||||
|
int i => new OpenIddictParameter(i),
|
||||||
|
long l => new OpenIddictParameter(l),
|
||||||
|
_ => new OpenIddictParameter(value?.ToString())
|
||||||
|
};
|
||||||
|
GetRequest(transaction).SetParameter(name, parameter);
|
||||||
|
}
|
||||||
|
|
||||||
public static AuthorityClientDescriptor CreateDescriptor(AuthorityClientDocument document)
|
public static AuthorityClientDescriptor CreateDescriptor(AuthorityClientDocument document)
|
||||||
{
|
{
|
||||||
var allowedGrantTypes = document.Properties.TryGetValue(AuthorityClientMetadataKeys.AllowedGrantTypes, out var grants) ? grants?.Split(' ', StringSplitOptions.RemoveEmptyEntries) : Array.Empty<string>();
|
var allowedGrantTypes = document.Properties.TryGetValue(AuthorityClientMetadataKeys.AllowedGrantTypes, out var grants) ? grants?.Split(' ', StringSplitOptions.RemoveEmptyEntries) : Array.Empty<string>();
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ public sealed class DiscoveryMetadataTests : IClassFixture<AuthorityWebApplicati
|
|||||||
{
|
{
|
||||||
using var client = factory.CreateClient();
|
using var client = factory.CreateClient();
|
||||||
|
|
||||||
using var response = await client.GetAsync("/.well-known/openid-configuration").ConfigureAwait(false);
|
using var response = await client.GetAsync("/.well-known/openid-configuration");
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
|
||||||
var payload = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
var payload = await response.Content.ReadAsStringAsync();
|
||||||
using var document = JsonDocument.Parse(payload);
|
using var document = JsonDocument.Parse(payload);
|
||||||
|
|
||||||
var root = document.RootElement;
|
var root = document.RootElement;
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public sealed class LegacyAuthDeprecationTests : IClassFixture<AuthorityWebAppli
|
|||||||
new FormUrlEncodedContent(new Dictionary<string, string>
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["grant_type"] = "client_credentials"
|
["grant_type"] = "client_credentials"
|
||||||
})).ConfigureAwait(false);
|
}));
|
||||||
|
|
||||||
Assert.NotNull(response);
|
Assert.NotNull(response);
|
||||||
Assert.True(response.Headers.TryGetValues("Deprecation", out var deprecationValues));
|
Assert.True(response.Headers.TryGetValues("Deprecation", out var deprecationValues));
|
||||||
@@ -77,7 +77,7 @@ public sealed class LegacyAuthDeprecationTests : IClassFixture<AuthorityWebAppli
|
|||||||
new FormUrlEncodedContent(new Dictionary<string, string>
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["grant_type"] = "client_credentials"
|
["grant_type"] = "client_credentials"
|
||||||
})).ConfigureAwait(false);
|
}));
|
||||||
|
|
||||||
Assert.NotNull(response);
|
Assert.NotNull(response);
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ public class PasswordGrantHandlersTests
|
|||||||
var handle = new HandlePasswordGrantHandler(registry, clientStore, TestActivitySource, sink, metadataAccessor, TimeProvider.System, NullLogger<HandlePasswordGrantHandler>.Instance);
|
var handle = new HandlePasswordGrantHandler(registry, clientStore, TestActivitySource, sink, metadataAccessor, TimeProvider.System, NullLogger<HandlePasswordGrantHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreatePasswordTransaction("alice", "Password1!", "obs:incident");
|
var transaction = CreatePasswordTransaction("alice", "Password1!", "obs:incident");
|
||||||
transaction.Request.SetParameter("incident_reason", "Sev1 drill activation");
|
SetParameter(transaction, "incident_reason", "Sev1 drill activation");
|
||||||
|
|
||||||
var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
await validate.HandleAsync(validateContext);
|
await validate.HandleAsync(validateContext);
|
||||||
@@ -213,8 +213,8 @@ public class PasswordGrantHandlersTests
|
|||||||
var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, clientStore, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
|
var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, clientStore, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreatePasswordTransaction("alice", "Password1!", "policy:publish");
|
var transaction = CreatePasswordTransaction("alice", "Password1!", "policy:publish");
|
||||||
transaction.Request.SetParameter("policy_ticket", "CR-1001");
|
SetParameter(transaction, "policy_ticket", "CR-1001");
|
||||||
transaction.Request.SetParameter("policy_digest", new string('a', 64));
|
SetParameter(transaction, "policy_digest", new string('a', 64));
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await validate.HandleAsync(context);
|
await validate.HandleAsync(context);
|
||||||
@@ -239,8 +239,8 @@ public class PasswordGrantHandlersTests
|
|||||||
var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, clientStore, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
|
var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, clientStore, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreatePasswordTransaction("alice", "Password1!", "policy:publish");
|
var transaction = CreatePasswordTransaction("alice", "Password1!", "policy:publish");
|
||||||
transaction.Request.SetParameter("policy_reason", "Publish approved policy");
|
SetParameter(transaction, "policy_reason", "Publish approved policy");
|
||||||
transaction.Request.SetParameter("policy_digest", new string('b', 64));
|
SetParameter(transaction, "policy_digest", new string('b', 64));
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await validate.HandleAsync(context);
|
await validate.HandleAsync(context);
|
||||||
@@ -261,8 +261,8 @@ public class PasswordGrantHandlersTests
|
|||||||
var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, clientStore, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
|
var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, clientStore, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreatePasswordTransaction("alice", "Password1!", "policy:publish");
|
var transaction = CreatePasswordTransaction("alice", "Password1!", "policy:publish");
|
||||||
transaction.Request.SetParameter("policy_reason", "Publish approved policy");
|
SetParameter(transaction, "policy_reason", "Publish approved policy");
|
||||||
transaction.Request.SetParameter("policy_ticket", "CR-1002");
|
SetParameter(transaction, "policy_ticket", "CR-1002");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await validate.HandleAsync(context);
|
await validate.HandleAsync(context);
|
||||||
@@ -283,9 +283,9 @@ public class PasswordGrantHandlersTests
|
|||||||
var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, clientStore, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
|
var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, clientStore, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreatePasswordTransaction("alice", "Password1!", "policy:publish");
|
var transaction = CreatePasswordTransaction("alice", "Password1!", "policy:publish");
|
||||||
transaction.Request.SetParameter("policy_reason", "Publish approved policy");
|
SetParameter(transaction, "policy_reason", "Publish approved policy");
|
||||||
transaction.Request.SetParameter("policy_ticket", "CR-1003");
|
SetParameter(transaction, "policy_ticket", "CR-1003");
|
||||||
transaction.Request.SetParameter("policy_digest", "not-hex");
|
SetParameter(transaction, "policy_digest", "not-hex");
|
||||||
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
|
|
||||||
await validate.HandleAsync(context);
|
await validate.HandleAsync(context);
|
||||||
@@ -311,9 +311,9 @@ public class PasswordGrantHandlersTests
|
|||||||
var handle = new HandlePasswordGrantHandler(registry, clientStore, TestActivitySource, sink, metadataAccessor, TimeProvider.System, NullLogger<HandlePasswordGrantHandler>.Instance);
|
var handle = new HandlePasswordGrantHandler(registry, clientStore, TestActivitySource, sink, metadataAccessor, TimeProvider.System, NullLogger<HandlePasswordGrantHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreatePasswordTransaction("alice", "Password1!", scope);
|
var transaction = CreatePasswordTransaction("alice", "Password1!", scope);
|
||||||
transaction.Request.SetParameter("policy_reason", "Promote approved policy");
|
SetParameter(transaction, "policy_reason", "Promote approved policy");
|
||||||
transaction.Request.SetParameter("policy_ticket", "CR-1004");
|
SetParameter(transaction, "policy_ticket", "CR-1004");
|
||||||
transaction.Request.SetParameter("policy_digest", new string('c', 64));
|
SetParameter(transaction, "policy_digest", new string('c', 64));
|
||||||
|
|
||||||
var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
|
||||||
await validate.HandleAsync(validateContext);
|
await validate.HandleAsync(validateContext);
|
||||||
@@ -403,7 +403,7 @@ public class PasswordGrantHandlersTests
|
|||||||
var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, clientStore, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
|
var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, clientStore, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
|
||||||
|
|
||||||
var transaction = CreatePasswordTransaction("alice", "Password1!");
|
var transaction = CreatePasswordTransaction("alice", "Password1!");
|
||||||
transaction.Request?.SetParameter("unexpected_param", "value");
|
SetParameter(transaction, "unexpected_param", "value");
|
||||||
|
|
||||||
await validate.HandleAsync(new OpenIddictServerEvents.ValidateTokenRequestContext(transaction));
|
await validate.HandleAsync(new OpenIddictServerEvents.ValidateTokenRequestContext(transaction));
|
||||||
|
|
||||||
@@ -506,6 +506,22 @@ public class PasswordGrantHandlersTests
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void SetParameter(OpenIddictServerTransaction transaction, string name, object? value)
|
||||||
|
{
|
||||||
|
var request = transaction.Request ?? throw new InvalidOperationException("OpenIddict request is required for this test.");
|
||||||
|
var parameter = value switch
|
||||||
|
{
|
||||||
|
null => default,
|
||||||
|
OpenIddictParameter existing => existing,
|
||||||
|
string s => new OpenIddictParameter(s),
|
||||||
|
bool b => new OpenIddictParameter(b),
|
||||||
|
int i => new OpenIddictParameter(i),
|
||||||
|
long l => new OpenIddictParameter(l),
|
||||||
|
_ => new OpenIddictParameter(value?.ToString())
|
||||||
|
};
|
||||||
|
request.SetParameter(name, parameter);
|
||||||
|
}
|
||||||
|
|
||||||
private static StellaOpsAuthorityOptions CreateAuthorityOptions(Action<StellaOpsAuthorityOptions>? configure = null)
|
private static StellaOpsAuthorityOptions CreateAuthorityOptions(Action<StellaOpsAuthorityOptions>? configure = null)
|
||||||
{
|
{
|
||||||
var options = new StellaOpsAuthorityOptions
|
var options = new StellaOpsAuthorityOptions
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ using System.Collections.Generic;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.TestHost;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Hosting;
|
||||||
using StellaOps.Authority.RateLimiting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using StellaOps.Configuration;
|
using StellaOps.Authority.RateLimiting;
|
||||||
using Xunit;
|
using StellaOps.Configuration;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
namespace StellaOps.Authority.Tests.RateLimiting;
|
namespace StellaOps.Authority.Tests.RateLimiting;
|
||||||
|
|
||||||
@@ -91,46 +92,52 @@ public class AuthorityRateLimiterIntegrationTests
|
|||||||
|
|
||||||
configure?.Invoke(options);
|
configure?.Invoke(options);
|
||||||
|
|
||||||
var builder = new WebHostBuilder()
|
var hostBuilder = new HostBuilder()
|
||||||
.ConfigureServices(services =>
|
.ConfigureWebHost(web =>
|
||||||
{
|
{
|
||||||
services.AddSingleton(options);
|
web.UseTestServer();
|
||||||
services.AddSingleton<IOptions<StellaOpsAuthorityOptions>>(Options.Create(options));
|
web.ConfigureServices(services =>
|
||||||
services.TryAddSingleton<TimeProvider>(_ => TimeProvider.System);
|
{
|
||||||
services.AddHttpContextAccessor();
|
services.AddSingleton(options);
|
||||||
services.TryAddSingleton<IAuthorityRateLimiterMetadataAccessor, AuthorityRateLimiterMetadataAccessor>();
|
services.AddSingleton<IOptions<StellaOpsAuthorityOptions>>(Options.Create(options));
|
||||||
services.TryAddSingleton<IAuthorityRateLimiterPartitionKeyResolver, DefaultAuthorityRateLimiterPartitionKeyResolver>();
|
services.TryAddSingleton<TimeProvider>(_ => TimeProvider.System);
|
||||||
services.AddRateLimiter(rateLimiterOptions =>
|
services.AddHttpContextAccessor();
|
||||||
{
|
services.TryAddSingleton<IAuthorityRateLimiterMetadataAccessor, AuthorityRateLimiterMetadataAccessor>();
|
||||||
AuthorityRateLimiter.Configure(rateLimiterOptions, options);
|
services.TryAddSingleton<IAuthorityRateLimiterPartitionKeyResolver, DefaultAuthorityRateLimiterPartitionKeyResolver>();
|
||||||
});
|
services.AddRateLimiter(rateLimiterOptions =>
|
||||||
})
|
{
|
||||||
.Configure(app =>
|
AuthorityRateLimiter.Configure(rateLimiterOptions, options);
|
||||||
{
|
});
|
||||||
app.UseAuthorityRateLimiterContext();
|
});
|
||||||
app.UseRateLimiter();
|
|
||||||
|
web.Configure(app =>
|
||||||
app.Map("/token", builder =>
|
{
|
||||||
{
|
app.UseAuthorityRateLimiterContext();
|
||||||
builder.Run(async context =>
|
app.UseRateLimiter();
|
||||||
{
|
|
||||||
context.Response.StatusCode = StatusCodes.Status200OK;
|
app.Map("/token", tokenBuilder =>
|
||||||
await context.Response.WriteAsync("token-ok");
|
{
|
||||||
});
|
tokenBuilder.Run(async context =>
|
||||||
});
|
{
|
||||||
|
context.Response.StatusCode = StatusCodes.Status200OK;
|
||||||
app.Map("/internal/ping", builder =>
|
await context.Response.WriteAsync("token-ok");
|
||||||
{
|
});
|
||||||
builder.Run(async context =>
|
});
|
||||||
{
|
|
||||||
context.Response.StatusCode = StatusCodes.Status200OK;
|
app.Map("/internal/ping", internalBuilder =>
|
||||||
await context.Response.WriteAsync("internal-ok");
|
{
|
||||||
});
|
internalBuilder.Run(async context =>
|
||||||
});
|
{
|
||||||
});
|
context.Response.StatusCode = StatusCodes.Status200OK;
|
||||||
|
await context.Response.WriteAsync("internal-ok");
|
||||||
return new TestServer(builder);
|
});
|
||||||
}
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var host = hostBuilder.Start();
|
||||||
|
return host.GetTestServer() ?? throw new InvalidOperationException("Failed to create TestServer.");
|
||||||
|
}
|
||||||
|
|
||||||
private static FormUrlEncodedContent CreateTokenForm(string clientId)
|
private static FormUrlEncodedContent CreateTokenForm(string clientId)
|
||||||
=> new(new Dictionary<string, string>
|
=> new(new Dictionary<string, string>
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ namespace StellaOps.Authority.Tests.Vulnerability;
|
|||||||
public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebApplicationFactory>
|
public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebApplicationFactory>
|
||||||
{
|
{
|
||||||
private readonly AuthorityWebApplicationFactory factory;
|
private readonly AuthorityWebApplicationFactory factory;
|
||||||
|
private const string SigningEnabledKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__ENABLED";
|
||||||
|
private const string SigningActiveKeyIdKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__ACTIVEKEYID";
|
||||||
|
private const string SigningKeyPathKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__KEYPATH";
|
||||||
|
private const string SigningKeySourceKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__KEYSOURCE";
|
||||||
|
private const string SigningAlgorithmKey = "STELLAOPS_AUTHORITY_AUTHORITY__SIGNING__ALGORITHM";
|
||||||
|
|
||||||
public VulnWorkflowTokenEndpointTests(AuthorityWebApplicationFactory factory)
|
public VulnWorkflowTokenEndpointTests(AuthorityWebApplicationFactory factory)
|
||||||
{
|
{
|
||||||
@@ -44,6 +49,15 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
|
|||||||
{
|
{
|
||||||
CreateEcPrivateKey(keyPath);
|
CreateEcPrivateKey(keyPath);
|
||||||
|
|
||||||
|
using var env = new EnvironmentVariableScope(new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
|
||||||
|
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "workflow-key"),
|
||||||
|
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
|
||||||
|
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
|
||||||
|
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
|
||||||
|
});
|
||||||
|
|
||||||
var sink = new RecordingAuthEventSink();
|
var sink = new RecordingAuthEventSink();
|
||||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:00:00Z"));
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:00:00Z"));
|
||||||
|
|
||||||
@@ -94,10 +108,10 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
|
|||||||
Assert.Equal("tenant-default", verified!.Tenant);
|
Assert.Equal("tenant-default", verified!.Tenant);
|
||||||
Assert.Equal("workflow-nonce-123456", verified.Nonce);
|
Assert.Equal("workflow-nonce-123456", verified.Nonce);
|
||||||
|
|
||||||
var issuedEvent = Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.workflow.csrf.issued"));
|
var issuedEvent = Assert.Single(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.issued");
|
||||||
Assert.Contains(issuedEvent.Properties, property => property.Name == "vuln.workflow.actor");
|
Assert.Contains(issuedEvent.Properties, property => property.Name == "vuln.workflow.actor");
|
||||||
|
|
||||||
var verifiedEvent = Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.workflow.csrf.verified"));
|
var verifiedEvent = Assert.Single(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.verified");
|
||||||
Assert.Contains(verifiedEvent.Properties, property => property.Name == "vuln.workflow.nonce" && property.Value.Value == "workflow-nonce-123456");
|
Assert.Contains(verifiedEvent.Properties, property => property.Name == "vuln.workflow.nonce" && property.Value.Value == "workflow-nonce-123456");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -116,6 +130,15 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
|
|||||||
{
|
{
|
||||||
CreateEcPrivateKey(keyPath);
|
CreateEcPrivateKey(keyPath);
|
||||||
|
|
||||||
|
using var env = new EnvironmentVariableScope(new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
|
||||||
|
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "workflow-key"),
|
||||||
|
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
|
||||||
|
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
|
||||||
|
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
|
||||||
|
});
|
||||||
|
|
||||||
var sink = new RecordingAuthEventSink();
|
var sink = new RecordingAuthEventSink();
|
||||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:10:00Z"));
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:10:00Z"));
|
||||||
|
|
||||||
@@ -158,6 +181,15 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
|
|||||||
{
|
{
|
||||||
CreateEcPrivateKey(keyPath);
|
CreateEcPrivateKey(keyPath);
|
||||||
|
|
||||||
|
using var env = new EnvironmentVariableScope(new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
|
||||||
|
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "workflow-key"),
|
||||||
|
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
|
||||||
|
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
|
||||||
|
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
|
||||||
|
});
|
||||||
|
|
||||||
var sink = new RecordingAuthEventSink();
|
var sink = new RecordingAuthEventSink();
|
||||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:20:00Z"));
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T09:20:00Z"));
|
||||||
|
|
||||||
@@ -196,7 +228,7 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
|
|||||||
Assert.Equal("invalid_token", error!["error"]);
|
Assert.Equal("invalid_token", error!["error"]);
|
||||||
Assert.Contains("Token does not permit action", error["message"], StringComparison.Ordinal);
|
Assert.Contains("Token does not permit action", error["message"], StringComparison.Ordinal);
|
||||||
|
|
||||||
Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.workflow.csrf.issued"));
|
Assert.Single(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.issued");
|
||||||
Assert.DoesNotContain(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.verified");
|
Assert.DoesNotContain(sink.Events, evt => evt.EventType == "vuln.workflow.csrf.verified");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -215,6 +247,15 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
|
|||||||
{
|
{
|
||||||
CreateEcPrivateKey(keyPath);
|
CreateEcPrivateKey(keyPath);
|
||||||
|
|
||||||
|
using var env = new EnvironmentVariableScope(new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
|
||||||
|
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "attachment-key"),
|
||||||
|
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
|
||||||
|
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
|
||||||
|
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
|
||||||
|
});
|
||||||
|
|
||||||
var sink = new RecordingAuthEventSink();
|
var sink = new RecordingAuthEventSink();
|
||||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T11:00:00Z"));
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T11:00:00Z"));
|
||||||
|
|
||||||
@@ -233,7 +274,7 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
|
|||||||
FindingId = "find-456",
|
FindingId = "find-456",
|
||||||
ContentHash = "sha256:abc123",
|
ContentHash = "sha256:abc123",
|
||||||
ContentType = "application/pdf",
|
ContentType = "application/pdf",
|
||||||
Metadata = new Dictionary<string, string> { ["origin"] = "vuln-workflow" }
|
Metadata = new Dictionary<string, string?> { ["origin"] = "vuln-workflow" }
|
||||||
};
|
};
|
||||||
|
|
||||||
var issueResponse = await client.PostAsJsonAsync("/vuln/attachments/tokens/issue", issuePayload);
|
var issueResponse = await client.PostAsJsonAsync("/vuln/attachments/tokens/issue", issuePayload);
|
||||||
@@ -256,10 +297,10 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
|
|||||||
Assert.NotNull(verified);
|
Assert.NotNull(verified);
|
||||||
Assert.Equal("ledger-hash-001", verified!.LedgerEventHash);
|
Assert.Equal("ledger-hash-001", verified!.LedgerEventHash);
|
||||||
|
|
||||||
var issuedEvent = Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.attachment.token.issued"));
|
var issuedEvent = Assert.Single(sink.Events, evt => evt.EventType == "vuln.attachment.token.issued");
|
||||||
Assert.Contains(issuedEvent.Properties, property => property.Name == "vuln.attachment.ledger_hash" && property.Value.Value == "ledger-hash-001");
|
Assert.Contains(issuedEvent.Properties, property => property.Name == "vuln.attachment.ledger_hash" && property.Value.Value == "ledger-hash-001");
|
||||||
|
|
||||||
var verifiedEvent = Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.attachment.token.verified"));
|
var verifiedEvent = Assert.Single(sink.Events, evt => evt.EventType == "vuln.attachment.token.verified");
|
||||||
Assert.Contains(verifiedEvent.Properties, property => property.Name == "vuln.attachment.ledger_hash" && property.Value.Value == "ledger-hash-001");
|
Assert.Contains(verifiedEvent.Properties, property => property.Name == "vuln.attachment.ledger_hash" && property.Value.Value == "ledger-hash-001");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -278,6 +319,15 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
|
|||||||
{
|
{
|
||||||
CreateEcPrivateKey(keyPath);
|
CreateEcPrivateKey(keyPath);
|
||||||
|
|
||||||
|
using var env = new EnvironmentVariableScope(new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string?>(SigningEnabledKey, "true"),
|
||||||
|
new KeyValuePair<string, string?>(SigningActiveKeyIdKey, "attachment-key"),
|
||||||
|
new KeyValuePair<string, string?>(SigningKeyPathKey, keyPath),
|
||||||
|
new KeyValuePair<string, string?>(SigningKeySourceKey, "file"),
|
||||||
|
new KeyValuePair<string, string?>(SigningAlgorithmKey, SignatureAlgorithms.Es256)
|
||||||
|
});
|
||||||
|
|
||||||
var sink = new RecordingAuthEventSink();
|
var sink = new RecordingAuthEventSink();
|
||||||
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T11:10:00Z"));
|
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2025-11-02T11:10:00Z"));
|
||||||
|
|
||||||
@@ -316,7 +366,7 @@ public sealed class VulnWorkflowTokenEndpointTests : IClassFixture<AuthorityWebA
|
|||||||
Assert.Equal("invalid_token", error!["error"]);
|
Assert.Equal("invalid_token", error!["error"]);
|
||||||
Assert.Contains("ledger reference", error["message"], StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("ledger reference", error["message"], StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
Assert.Single(sink.Events.Where(evt => evt.EventType == "vuln.attachment.token.issued"));
|
Assert.Single(sink.Events, evt => evt.EventType == "vuln.attachment.token.issued");
|
||||||
Assert.DoesNotContain(sink.Events, evt => evt.EventType == "vuln.attachment.token.verified");
|
Assert.DoesNotContain(sink.Events, evt => evt.EventType == "vuln.attachment.token.verified");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
@@ -134,6 +134,7 @@
|
|||||||
> 2025-11-02: Authority bootstrap test harness now seeds service accounts via AuthorityDelegation options; `/internal/service-accounts` endpoints validated with targeted vstest run.
|
> 2025-11-02: Authority bootstrap test harness now seeds service accounts via AuthorityDelegation options; `/internal/service-accounts` endpoints validated with targeted vstest run.
|
||||||
> 2025-11-02: Added Mongo service-account store, seeded options/collection initializers, token persistence metadata (`tokenKind`, `serviceAccountId`, `actorChain`), and docs/config samples. Introduced quota checks + tests covering service account issuance and persistence.
|
> 2025-11-02: Added Mongo service-account store, seeded options/collection initializers, token persistence metadata (`tokenKind`, `serviceAccountId`, `actorChain`), and docs/config samples. Introduced quota checks + tests covering service account issuance and persistence.
|
||||||
> 2025-11-02: Documented bootstrap service-account admin APIs in `docs/11_AUTHORITY.md`, noting API key requirements and stable upsert behaviour.
|
> 2025-11-02: Documented bootstrap service-account admin APIs in `docs/11_AUTHORITY.md`, noting API key requirements and stable upsert behaviour.
|
||||||
|
> 2025-11-03: Seeded explicit enabled service-account fixtures for integration tests and reran `StellaOps.Authority.Tests` to greenlight `/internal/service-accounts` listing + revocation scenarios.
|
||||||
|
|
||||||
## Observability & Forensics (Epic 15)
|
## Observability & Forensics (Epic 15)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user