161 lines
13 KiB
Markdown
161 lines
13 KiB
Markdown
# Implementation Report: Exchange Credential and Permission Evidence Support
|
|
|
|
## Result
|
|
|
|
- **Gate result**: PASS WITH CONDITIONS.
|
|
- **Active spec**: `specs/433-exchange-credential-permission-evidence-support/`.
|
|
- **Branch**: `433-exchange-credential-permission-evidence-support`.
|
|
- **HEAD at implementation start**: `f4e34212 feat: add Exchange PowerShell production runner gate (#499)`.
|
|
- **Initial dirty state**: only untracked active Spec 433 package.
|
|
- **Final dirty state**: application/config/test changes plus the active Spec 433 package and this report.
|
|
- **Condition**: browser Product Surface sanity found an existing broader provider-readiness guidance card can still show a generic provider credential blocker next to the Exchange-specific capability result. The changed Exchange capability slots themselves were canonical and redacted. No UI code was changed because this spec forbids new UI/surface work.
|
|
|
|
## Activated Skills and Gates
|
|
|
|
- `spec-kit-implementation-loop`: active implementation workflow.
|
|
- `pest-testing`: Pest 4 focused unit/feature coverage.
|
|
- Browser skills: `browser:control-in-app-browser` and repo `browsertest` for focused read-only browser proof.
|
|
- TenantPilot repo gates: spec readiness, workspace-scope safety, RBAC/action safety, provider freshness semantics, OperationRun truth, evidence-anchor contract, Product Surface gate, Filament/Livewire v5 change loop.
|
|
|
|
## Implementation Summary
|
|
|
|
- Added `ExchangePowerShellInvocationReadinessEvaluator` as the shared readiness truth for `exchange_powershell_invoke` capability evaluation and the production runner gate.
|
|
- Kept the no-migration path. Existing `ProviderCredential` fields and `ManagedEnvironmentPermission.details` are sufficient when permission details explicitly match workspace, managed environment, provider, and provider connection.
|
|
- Tightened certificate readiness so certificate references require both rotation and expiry metadata.
|
|
- Extended `ExchangePowerShellPermissionEvidenceEvaluator` with supported-provider checks, status normalization, configurable currentness, safe source metadata, scoped provider-connection validation, and sanitized blocker states.
|
|
- Post-review hardening requires trusted permission evidence provenance: `source=provider_verification`, canonical, calendar-valid ISO/RFC3339 non-future `observed_at`/`verified_at`, `verified_at >= observed_at`, evaluator identity/version metadata, and current `last_checked_at` plus current observed/verified timestamps. Missing, malformed, relative, untrusted, or time-inconsistent metadata now fails closed as unvalidated evidence; stale row or provenance timestamps fail closed as stale evidence.
|
|
- Post-review scope hardening requires `workspace_id`, `managed_environment_id`, and `provider_connection_id` evidence metadata to be canonical positive integer values before any scope comparison. Numeric-prefix, decimal, scientific-notation, whitespace-padded, missing, or otherwise malformed scope IDs now fail closed as unvalidated evidence instead of passing through PHP integer coercion.
|
|
- Updated `ProviderCapabilityEvaluator` so `exchange_powershell_invoke` is Supported only when combined credential and permission readiness passes.
|
|
- Post-review hardening makes the shared Exchange readiness evaluator the single capability truth for `exchange_powershell_invoke`; the capability no longer applies a separate generic provider-readiness freshness precheck that can diverge from the Exchange evidence currentness config.
|
|
- Updated `ExchangePowerShellProductionRunner` to consume combined readiness before runtime checks or process execution.
|
|
- Added safe `readiness_state` and `readiness_blocker` propagation into OperationRun runner-result context.
|
|
- Added env/config defaults for `TENANTPILOT_EXCHANGE_POWERSHELL_PERMISSION_EVIDENCE_CURRENTNESS_DAYS=30`.
|
|
|
|
## Data Model Decision
|
|
|
|
| Area | Decision | Proof |
|
|
| --- | --- | --- |
|
|
| Provider credential | No migration | `provider_connection_id`, `credential_kind`, `source`, `last_rotated_at`, and `expires_at` cover the observable readiness states for this slice. |
|
|
| Permission evidence | No migration | `ManagedEnvironmentPermission` remains one row per environment + permission key; `details` must match workspace, environment, provider, and provider connection or fail closed. |
|
|
| Provider connection scope | Existing model | Readiness requires matching `workspace_id` and `managed_environment_id`; wrong connection details block. |
|
|
| Secret material | No new storage | Existing encrypted payload remains hidden and is never returned in readiness/OperationRun context. |
|
|
| Migration lane | N/A | No schema file added; no PostgreSQL schema lane required. |
|
|
|
|
## Credential Matrix
|
|
|
|
| Credential state | Result |
|
|
| --- | --- |
|
|
| Missing reference | Blocks as `credential_blocked_missing_reference`. |
|
|
| Client secret | Blocks as `credential_blocked_secret_based_app`. |
|
|
| Certificate missing rotation or expiry metadata | Blocks as `credential_blocked_certificate_missing`. |
|
|
| Expired certificate | Blocks as `credential_blocked_certificate_expired`. |
|
|
| Certificate unsupported by config | Blocks as `credential_blocked_certificate_unsupported`. |
|
|
| Certificate explicitly supported for readiness | Passes as `certificate_supported`. |
|
|
| Federated | Blocks unless future config/support is added. |
|
|
| Managed identity string | Blocks as unsupported/unknown. |
|
|
| Unknown kind | Blocks fail-closed. |
|
|
|
|
## Permission Matrix
|
|
|
|
| Permission evidence state | Result |
|
|
| --- | --- |
|
|
| Missing row | Blocks as `permission_evidence_blocked_missing`. |
|
|
| Missing/absent/revoked status | Blocks as `permission_evidence_blocked_absent`. |
|
|
| Unknown/error status | Blocks as `permission_evidence_blocked_unknown`. |
|
|
| Granted without trusted source, observed/verified timestamps, evaluator identity, or evaluator version | Blocks as unvalidated. |
|
|
| Granted with malformed, relative, future, or time-inconsistent provenance metadata | Blocks as unvalidated. |
|
|
| Granted with malformed scope identifiers | Blocks as unvalidated before scope matching. |
|
|
| Stale row, observed timestamp, or verified timestamp beyond configured currentness | Blocks as `permission_evidence_blocked_stale`. |
|
|
| Wrong workspace/environment/provider/provider connection | Blocks with scoped failure code. |
|
|
| Current scoped `Exchange.ManageAsApp` | Passes as `verified`. |
|
|
|
|
## Capability and Runner Proof
|
|
|
|
- `exchange_powershell_invoke` uses the combined readiness evaluator and no longer relies on admin consent, generic provider freshness, or generic stored permission rows alone.
|
|
- Missing credential maps to Missing; stale/unknown/unvalidated permission evidence maps to Unknown; wrong-scope or unsupported readiness maps to Blocked.
|
|
- The production runner evaluates combined readiness before runtime readiness, command building, locks, or process execution.
|
|
- Successful runner context persists only safe state: runner mode, execution enabled, readiness state, runtime state, credential state, permission evidence state, command builder/output/concurrency state, counts, and duration.
|
|
|
|
## No-Promotion Proof
|
|
|
|
- No `TenantConfigurationResource` or `TenantConfigurationResourceEvidence` rows are created.
|
|
- No raw or normalized Exchange payload, compare/render/certification/restore/customer-ready state, report output, Review Pack output, PDF output, route, Filament page/resource/widget/action, Livewire component, navigation, asset, global search change, job, schedule, listener, console trigger, Exchange-specific table, legacy shim, fallback reader, dual write, or `tenant_id` ownership path was introduced.
|
|
- Completed Specs 429-432 were read-only context and were not rewritten.
|
|
|
|
## Product Surface and Filament Close-Out
|
|
|
|
- **No-legacy posture**: canonical addition only; no aliases, fallback readers, hidden routes, duplicate UI, or historical compatibility path.
|
|
- **Product Surface Impact**: existing provider capability/readiness surfaces can render changed evaluator output; no new surface budget introduced.
|
|
- **UI Surface Impact**: no UI files changed; existing provider connection and required-permissions surfaces render the changed status data.
|
|
- **Browser proof**: PASS WITH CONDITION. See browser section below.
|
|
- **Human Product Sanity**: PASS WITH CONDITION. Exchange capability slots are canonical/redacted; existing broader readiness card can still show a generic credential blocker beside Exchange-specific Supported.
|
|
- **Visible complexity outcome**: neutral; no new visible components, only more truthful existing status values.
|
|
- **Livewire v4 compliance**: unchanged repo baseline; no Livewire runtime code added.
|
|
- **Provider registration location**: no Filament panel provider registration changed; Laravel panel providers remain in `apps/platform/bootstrap/providers.php`.
|
|
- **Global search**: no Filament resources or global search behavior changed.
|
|
- **Destructive/high-impact actions**: no UI action added; runner remains behind existing Spec 432 gates and provider-run authorization.
|
|
- **Asset strategy**: no assets; `filament:assets` not required for this slice.
|
|
- **Deployment impact**: no migration, queue, schedule, storage, asset, or build impact. New env example key: `TENANTPILOT_EXCHANGE_POWERSHELL_PERMISSION_EVIDENCE_CURRENTNESS_DAYS=30`.
|
|
|
|
## Browser Proof
|
|
|
|
- Sail was running and migrations were current. A local smoke fixture was seeded with:
|
|
- ready environment + certificate credential + scoped current `Exchange.ManageAsApp` evidence + all normal required permission rows;
|
|
- blocked environment + client-secret credential + stale permission evidence.
|
|
- A temporary ignored local `.env` override `TENANTPILOT_EXCHANGE_POWERSHELL_SUPPORTED_REFERENCE_KINDS=certificate` was used only during browser proof and removed afterward; config cache was cleared after removal.
|
|
- Provider connection view, ready fixture:
|
|
- `Exchange PowerShell invocation: Supported` rendered in existing capability detail.
|
|
- No raw grant, access token, private key, certificate password, or client secret sentinel rendered.
|
|
- No browser console errors from the tab.
|
|
- Provider connection view, blocked fixture:
|
|
- `Exchange PowerShell invocation: Unknown` rendered for the blocked client-secret/stale-evidence fixture.
|
|
- No raw token/client-secret sentinel rendered.
|
|
- No browser console errors from the tab.
|
|
- Required-permissions view, ready fixture:
|
|
- `Provider capabilities` section rendered `Exchange PowerShell invocation` as Supported with `0 missing, 0 need review`.
|
|
- No raw grant, access token, private key, certificate password, or client secret sentinel rendered.
|
|
- No browser console errors from the tab.
|
|
- Required-permissions view, blocked fixture:
|
|
- Required-permissions/provider capability surface stayed non-supported and showed missing/stale guidance rather than ready.
|
|
- No raw token/client-secret sentinel rendered.
|
|
- No browser console errors from the tab.
|
|
- Laravel Boost browser log contained older unrelated June operation-page errors, not Spec 433 route errors.
|
|
|
|
## Validation
|
|
|
|
- Post-review remediation:
|
|
- `cd apps/platform && php artisan test --filter=Spec433 --compact`
|
|
- Result: PASS, 48 tests, 211 assertions.
|
|
- `cd apps/platform && php artisan test --filter='Spec433|Spec432|Spec431' --compact`
|
|
- Result: PASS, 150 tests, 1057 assertions.
|
|
- `cd apps/platform && ./vendor/bin/pint --dirty`
|
|
- Result: PASS, 10 files.
|
|
- `git diff --check`
|
|
- Result: PASS.
|
|
- `cd apps/platform && php artisan test --filter=Spec433 --compact`
|
|
- Result: PASS, 30 tests, 157 assertions.
|
|
- `cd apps/platform && php artisan test --filter=Spec432 --compact`
|
|
- Result: PASS, 72 tests, 430 assertions.
|
|
- `cd apps/platform && php artisan test --filter=Spec431 --compact`
|
|
- Result: PASS, 30 tests, 416 assertions.
|
|
- `cd apps/platform && php artisan test --filter='Spec433|Spec432|Spec431' --compact`
|
|
- Result: PASS, 132 tests, 1003 assertions.
|
|
- `cd apps/platform && php artisan test --filter='Spec433|Spec432|Spec431|Spec430|Spec427ExchangeTeamsNoEvidencePromotion|Spec427ExchangeTeamsNoCompareRenderCertification|Spec426ExchangeTeamsNoTenantId|Spec426ExchangeTeamsNoMiniPlatform|Spec417IdentityNoLegacyNoUiActivation|Spec419M365RegistryExpansion|Spec420M365NoOverclaim|ProviderCapabilityRegistryTest|ProviderCapabilityEvaluationTest' --compact`
|
|
- Result: PASS, 222 tests, 2028 assertions, 1 PostgreSQL-only skip.
|
|
- `cd apps/platform && ./vendor/bin/pint --dirty`
|
|
- Result: PASS, 10 files.
|
|
- `cd apps/platform && ./vendor/bin/pint app/Services/TenantConfiguration/ExchangePowerShellInvocationReadinessEvaluator.php tests/Feature/TenantConfiguration/Spec433ExchangePowerShellEvidenceReadinessTest.php tests/Feature/TenantConfiguration/Spec431ExchangePowerShellInvocationGateTest.php tests/Feature/TenantConfiguration/Spec432ExchangePowerShellProductionRunnerGateTest.php --format=txt`
|
|
- Result: PASS.
|
|
- `git diff --check`
|
|
- Result: PASS.
|
|
- `cd apps/platform && ./vendor/bin/sail artisan migrate --force`
|
|
- Result: PASS, nothing to migrate.
|
|
- `cd apps/platform && ./vendor/bin/sail artisan config:clear`
|
|
- Result: PASS after browser override removal.
|
|
|
|
## Deferred Work
|
|
|
|
- Content-backed Exchange evidence capture/promotion remains Spec 434 or later.
|
|
- Live Exchange execution success, customer claims, compare/render/certification/restore readiness, Teams, Exchange Admin API, broader Exchange targets, and UI productization remain out of scope.
|
|
- A future UI/product-surface cleanup should decide how to distinguish generic provider readiness from Exchange PowerShell-specific capability readiness on provider connection pages.
|