TenantAtlas/specs/381-provider-resource-identity-binding/plan.md
Ahmed Darrazi fb2642e941
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m9s
feat(resources): implement provider resource identity binding
Added ProviderResourceBinding model, migrations, policies, and supporting framework for canonical resource identity mapping as defined in Spec 381.
2026-06-15 17:37:06 +02:00

348 lines
26 KiB
Markdown

# Implementation Plan: Spec 381 - Provider Resource Identity and Binding Foundation v1
**Branch**: `381-provider-resource-identity-binding` | **Date**: 2026-06-15 | **Spec**: `specs/381-provider-resource-identity-binding/spec.md`
**Input**: Feature specification from `specs/381-provider-resource-identity-binding/spec.md`
## Summary
Add the managed-environment-scoped provider resource identity and binding foundation needed before future baseline matching, resolution UI, and evidence/review integration. The plan introduces provider-neutral identity primitives, extends the existing `BaselineSubjectKey` path for canonical keys, persists auditable provider resource bindings, and proves isolation, uniqueness, RBAC, auditability, and no-op compare/evidence behavior. It does not add UI, Graph calls, OperationRun behavior, matching changes, or evidence/review readiness changes.
## Technical Context
**Language/Version**: PHP 8.4.15, Laravel 12.52.0
**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Pest 4.3.1, PostgreSQL, existing Laravel policies/Gates, existing `AuditLog`, existing baseline compare/resolution support
**Storage**: PostgreSQL table `provider_resource_bindings` with managed-environment scope and partial unique active-binding index
**Testing**: Pest 4 Unit, Feature, and PostgreSQL lane
**Validation Lanes**: fast-feedback, confidence, PostgreSQL; no browser lane because no UI surface changes
**Target Platform**: Laravel Sail locally; Dokploy-first Staging/Production containers
**Project Type**: Laravel monolith with Filament admin/system panels
**Performance Goals**: Active binding lookup must be indexed by workspace, managed environment, provider key, canonical subject key, and active status. No render-time provider calls are introduced.
**Constraints**: no UI, no Graph calls, no OperationRun creation, no matching pipeline rewrite, no evidence/review behavior change, no workspace/baseline-profile binding scopes in v1
**Scale/Scope**: One new scoped binding table, identity/descriptor support classes, one binding service, one policy/capability path, focused tests
## UI / Surface Guardrail Plan
- **Guardrail scope**: no operator-facing surface change.
- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**: none in v1; existing baseline compare/evidence/review surfaces are regression targets only.
- **No-impact class, if applicable**: backend-only identity and persistence foundation.
- **Native vs custom classification summary**: N/A.
- **Shared-family relevance**: provider boundary vocabulary, baseline subject identity, RBAC, audit, and future evidence/review truth.
- **State layers in scope**: backend persistence and service state only; no shell/page/detail/URL-query state.
- **Audience modes in scope**: future operator/MSP and support/platform users are represented through audit and service semantics only. No customer-readable surface changes.
- **Decision/diagnostic/raw hierarchy plan**: N/A for UI. Audit metadata must remain safe and redacted.
- **Raw/support gating plan**: no raw provider payloads, secrets, tokens, signed URLs, or raw Graph data in binding records or audit metadata.
- **One-primary-action / duplicate-truth control**: `binding_status` is the single active/superseded/revoked truth. Do not add an `is_active` mirror.
- **Handling modes by drift class or surface**: hard-stop if implementation adds UI, matching behavior, workspace-level scope, Graph calls, or a new OperationRun type without updating the spec/plan first.
- **Repository-signal treatment**: review-mandatory for any separate capability family; follow-up-spec for workspace/baseline-profile scopes or automatic canonicalization.
- **Special surface test profiles**: N/A.
- **Required tests or manual smoke**: Unit, Feature, PostgreSQL, and targeted no-op regression tests.
- **Exception path and spread control**: none.
- **Active feature PR close-out entry**: Identity Binding Foundation / Provider Boundary.
- **UI/Productization coverage decision**: No UI surface impact.
- **Coverage artifacts to update**: none.
- **No-impact rationale**: The spec adds backend identity/persistence only and no route/page/action/navigation/presentation changes.
- **Navigation / Filament provider-panel handling**: N/A.
- **Screenshot or page-report need**: no.
## Shared Pattern & System Fit
- **Cross-cutting feature marker**: yes.
- **Systems touched**: baseline subject key support, provider resource descriptors, binding persistence, capability/policy enforcement, audit logging, PostgreSQL migration/index patterns.
- **Shared abstractions reused**: `App\Support\Baselines\BaselineSubjectKey`, `App\Support\Baselines\SubjectClass`, `App\Support\Baselines\ResolutionOutcome`, `App\Models\ProviderConnection`, `App\Models\ManagedEnvironment`, `App\Models\AuditLog`, `App\Support\Auth\Capabilities`.
- **New abstraction introduced? why?**: `ResourceIdentity` and `ProviderResourceDescriptor` are introduced because stable provider identity and descriptor serialization are not currently represented independently of compare runtime records. `ProviderResourceBindingService` is introduced because binding mutation requires scoped authorization, uniqueness, supersession, note validation, and audit in one transaction.
- **Why the existing abstraction was sufficient or insufficient**: Existing `BaselineSubjectKey` is sufficient to host canonical key generation. Existing compare/resolution classes are insufficient as durable binding truth because they are run/result semantics, not reusable operator decisions.
- **Bounded deviation / spread control**: New binding foundation must remain passive until Spec 382 or later explicitly consumes it.
## OperationRun UX Impact
- **Touches OperationRun start/completion/link UX?**: no.
- **Central contract reused**: N/A.
- **Delegated UX behaviors**: N/A.
- **Surface-owned behavior kept local**: N/A.
- **Queued DB-notification policy**: N/A.
- **Terminal notification path**: N/A.
- **Exception path**: none.
Binding records may reference a source `OperationRun`, but creating or revoking a binding is a short DB-only security-relevant action. It writes `AuditLog` and does not create a new run.
## Provider Boundary & Portability Fit
- **Shared provider/platform boundary touched?**: yes.
- **Provider-owned seams**: concrete provider resource IDs, external IDs, resource type names, discriminators, fingerprints, and future built-in canonicalization rules.
- **Platform-core seams**: `ResourceIdentity`, descriptor shape, canonical subject key generation, binding persistence, active uniqueness, audit/action semantics.
- **Neutral platform terms / contracts preserved**: provider, provider connection, managed environment, governed subject, canonical subject key, resource identity, descriptor, binding, resolution mode.
- **Retained provider-specific semantics and why**: provider fields are retained as opaque descriptors because exact provider identity must be represented. The core must not interpret Microsoft display labels.
- **Bounded extraction or follow-up path**: automatic Microsoft built-in mapping and matching behavior are follow-up specs.
## Constitution Check
- **Inventory-first, Snapshots-second**: PASS. Inventory remains last-observed provider state. Bindings are operator/provider identity decision truth, not a replacement for inventory or snapshots.
- **Read/Write Separation by Default**: PASS. No provider writes. Binding mutations affect TenantPilot only and require authorization plus audit.
- **Single Contract Path to Graph**: PASS. No Graph calls are introduced.
- **Deterministic Capabilities**: PASS with constraint. V1 reuses existing baseline capabilities unless spec/plan update authorizes dedicated capabilities.
- **Workspace isolation**: PASS. All rows include `workspace_id`, and services/policies must enforce workspace entitlement.
- **Tenant/Managed-environment isolation**: PASS. V1 rows include non-null `managed_environment_id`; no workspace-level binding scope is implemented.
- **RBAC-UX**: PASS. Non-member access is deny-as-not-found; member without capability is forbidden; policies/Gates enforce server-side.
- **Run observability**: PASS. No new long-running/remote/queued work. Security-relevant DB-only actions audit-log instead of creating an OperationRun.
- **Proportionality / No premature abstraction**: PASS as narrowed. New persistence is justified because bindings outlive runs and affect future governance truth. No provider adapter framework or UI is included.
- **Persisted truth**: PASS. Binding records have independent lifecycle and auditability.
- **Behavioral state**: PASS. `binding_status` affects active lookup and supersession/revocation; `resolution_mode` affects audit and future operator/matching behavior.
- **Provider boundary**: PASS with guardrails. Core stores provider identity but must not branch on Microsoft display names or Graph endpoint assumptions.
- **UI/Productization coverage**: PASS. `No UI surface impact` is recorded.
- **Test governance**: PASS. Unit/Feature/PostgreSQL lanes are named and narrow.
- **Release safety**: PASS. Migration is additive and reversible where practical. PostgreSQL partial unique behavior is test-covered before production promotion.
## Existing Repository Surfaces
Read-only context and likely implementation targets:
- `apps/platform/app/Support/Baselines/BaselineSubjectKey.php`
- `apps/platform/app/Support/Baselines/SubjectClass.php`
- `apps/platform/app/Support/Baselines/ResolutionOutcome.php`
- `apps/platform/app/Support/Baselines/Compare/CompareSubjectIdentity.php`
- `apps/platform/app/Support/Baselines/Compare/IntuneCompareStrategy.php`
- `apps/platform/app/Services/Baselines/BaselineCompareService.php`
- `apps/platform/app/Jobs/CompareBaselineToTenantJob.php`
- `apps/platform/app/Services/Baselines/BaselineCaptureService.php`
- `apps/platform/app/Jobs/CaptureBaselineSnapshotJob.php`
- `apps/platform/app/Models/ProviderConnection.php`
- `apps/platform/app/Models/ManagedEnvironment.php`
- `apps/platform/app/Models/InventoryItem.php`
- `apps/platform/app/Models/PolicyVersion.php`
- `apps/platform/app/Models/BaselineSnapshot.php`
- `apps/platform/app/Models/OperationRun.php`
- `apps/platform/app/Models/AuditLog.php`
- `apps/platform/app/Support/Auth/Capabilities.php`
- `apps/platform/app/Services/Auth/WorkspaceRoleCapabilityMap.php`
- `apps/platform/app/Services/Auth/RoleCapabilityMap.php`
- `apps/platform/database/migrations/2026_03_20_000000_create_environment_reviews_table.php`
- `apps/platform/database/migrations/2026_02_19_100003_create_baseline_snapshot_items_table.php`
- `apps/platform/database/migrations/2026_01_07_142720_create_inventory_items_table.php`
Expected write surfaces during later implementation:
- `apps/platform/database/migrations/<timestamp>_create_provider_resource_bindings_table.php`
- `apps/platform/app/Models/ProviderResourceBinding.php`
- `apps/platform/database/factories/ProviderResourceBindingFactory.php`
- `apps/platform/app/Policies/ProviderResourceBindingPolicy.php`
- `apps/platform/app/Support/Resources/ResourceIdentity.php`
- `apps/platform/app/Support/Resources/ProviderResourceDescriptor.php`
- `apps/platform/app/Support/Resources/ProviderResourceBindingStatus.php`
- `apps/platform/app/Support/Resources/ProviderResourceResolutionMode.php`
- `apps/platform/app/Services/Resources/ProviderResourceBindingService.php`
- `apps/platform/app/Support/Baselines/BaselineSubjectKey.php`
- `apps/platform/app/Support/Audit/AuditActionId.php`
- `apps/platform/tests/Unit/Support/Resources/ResourceIdentityTest.php`
- `apps/platform/tests/Unit/Support/Resources/ProviderResourceDescriptorTest.php`
- `apps/platform/tests/Unit/Support/Baselines/BaselineSubjectKeyCanonicalIdentityTest.php`
- `apps/platform/tests/Feature/ProviderResources/ProviderResourceBindingServiceTest.php`
- `apps/platform/tests/Feature/ProviderResources/ProviderResourceBindingAuthorizationTest.php`
- `apps/platform/tests/Feature/ProviderResources/ProviderResourceBindingPostgresTest.php`
- Targeted existing baseline/evidence/review tests listed in the validation section
No Filament resource/page/action, route, Livewire component, Blade view, job, Graph client, provider adapter, or customer output surface is planned.
## Technical Approach
1. Add deterministic provider-neutral identity and descriptor primitives.
2. Extend `BaselineSubjectKey` with canonical key methods that accept provider key, subject class, subject type key, resource type, and stable identity or canonical discriminator.
3. Add a managed-environment-scoped `provider_resource_bindings` table:
- `workspace_id` non-null foreign key
- `managed_environment_id` non-null foreign key
- composite foreign key from `(managed_environment_id, workspace_id)` to `managed_environments(id, workspace_id)`
- `provider_key`
- nullable `provider_connection_id`
- `subject_domain`, `subject_class`, `subject_type_key`
- nullable `legacy_subject_key`
- `canonical_subject_key`
- nullable provider resource descriptor columns
- `binding_status`
- `resolution_mode`
- nullable `resolution_reason`
- required `operator_note` for manual decisions
- nullable source references to `operation_runs`, `baseline_snapshots`, `inventory_items`, and `policy_versions`
- `decided_by_user_id`, `decided_at`, nullable `ended_at`
- timestamps
4. Use PostgreSQL partial unique index for active rows:
- `(workspace_id, managed_environment_id, provider_key, canonical_subject_key) WHERE binding_status = 'active'`
5. Add a model, factory, policy, and service:
- model uses `DerivesWorkspaceIdFromTenant` or an equivalent invariant to derive and protect `workspace_id`
- service methods create manual binding, create exclusion, create accepted limitation, mark unsupported, mark missing expected, supersede active binding, and revoke binding
- all mutations run in transactions
- all mutations enforce authorization, workspace/managed-environment scope, provider connection scope, source-reference scope, note requirements, active uniqueness, and audit
6. Add tests before or alongside implementation.
7. Add targeted regression proof that current compare/evidence/review behavior does not consume bindings yet.
## Domain / Model Implications
- `ProviderResourceBinding` is tenant-owned operational truth because it is bound to a managed environment and affects future tenant governance semantics.
- Binding scope is deliberately not a persisted axis in v1. All rows are managed-environment scoped.
- Provider defaults use `canonical_builtin` resolution mode in v1; do not introduce a separate default-specific resolution mode unless spec/plan are updated first.
- The model must reject or prevent workspace drift from the managed environment, matching the repo's tenant-owned model invariant.
- `binding_status` is the lifecycle truth:
- `active`
- `superseded`
- `revoked`
- `resolution_mode` is the decision meaning:
- `exact_provider_identity`
- `canonical_builtin`
- `canonical_virtual_target`
- `manual_binding`
- `excluded_non_governed`
- `accepted_limitation`
- `unsupported_coverage`
- `missing_expected`
- Do not add `is_active`, validity windows, workspace-level rows, baseline-profile rows, or subject-only rows in v1.
## UI / Filament Implications
- No Filament Resource, Page, RelationManager, action, table, form, navigation item, modal, drawer, wizard, or view is added or changed.
- Livewire v4.0+ compliance remains unchanged; installed Livewire is 4.1.4.
- Laravel 12 panel providers remain in `apps/platform/bootstrap/providers.php`.
- No globally searchable resource is added. `ProviderResourceBinding` must not be globally searchable because no Filament Resource/View/Edit page is introduced.
- No destructive Filament action is added. Service-level revocation and supersession are high-impact backend mutations and require server-side authorization plus audit; future UI confirmation belongs to Spec 384.
- No assets are registered. Normal deployment still runs `php artisan filament:assets` when the app deploys existing Filament assets, but Spec 381 adds none.
## OperationRun / Monitoring Implications
- No new OperationRun type or lifecycle transition.
- Binding mutations are DB-only and expected to complete under 2 seconds.
- Binding audit events must include safe source references where provided.
- Future matching specs may link compare OperationRuns to binding decisions, but v1 only stores optional source references.
## RBAC / Policy Implications
- Add a `ProviderResourceBindingPolicy` or an equivalent policy path consistent with existing model policy conventions.
- Reuse existing `workspace_baselines.view` for read authorization and `workspace_baselines.manage` for create/supersede/revoke decisions in v1.
- Do not introduce raw capability strings in feature code.
- Non-members or actors outside the workspace/managed environment receive 404 deny-as-not-found.
- Entitled members lacking baseline management capability receive 403 for mutations.
- Register the policy explicitly through the existing Laravel provider convention used by this app (`apps/platform/app/Providers/AuthServiceProvider.php` unless implementation finds a stronger adjacent precedent and updates this plan).
- Include positive and negative authorization tests.
## Audit / Evidence Implications
- Manual binding, exclusion, accepted limitation, unsupported coverage, missing expected, supersession, and revocation emit audit events.
- Audit metadata includes only safe identifiers:
- provider key
- canonical subject key
- subject class/type
- old/new binding IDs
- resolution mode
- source operation/snapshot/inventory/policy version IDs
- redacted, summarized, length-limited, or reference-only operator note/reason metadata
- Audit metadata must not include tokens, secrets, raw credential payloads, raw provider payloads, raw Graph response bodies, signed URLs, or customer-sensitive raw JSON.
- Raw operator notes must not be copied unchecked into audit metadata. The binding row may retain the required note; audit assertions must prove the audit payload remains safe.
## Data / Migration Implications
- Add one reversible migration for `provider_resource_bindings`.
- Add the repo-standard tenant-owned composite foreign key `(managed_environment_id, workspace_id)` -> `managed_environments(id, workspace_id)`.
- Add scoped foreign keys or service-enforced validation for `provider_connection_id` and source reference IDs so referenced records cannot point outside the binding workspace/managed environment. `provider_connection_id`, when present, must match `provider_key`.
- Use `jsonb` only if implementation needs a small descriptor payload; prefer explicit indexed columns listed above for lookup truth.
- Add indexes:
- `workspace_id`
- `managed_environment_id`
- `provider_key`
- `provider_connection_id`
- `canonical_subject_key`
- `binding_status`
- `resolution_mode`
- `source_operation_run_id`
- `source_baseline_snapshot_id`
- `source_inventory_item_id`
- `source_policy_version_id`
- Add partial unique active index in a PostgreSQL migration statement and drop it in `down()` before dropping the table if required by migration ordering.
- PostgreSQL lane is required because SQLite cannot prove partial unique index, composite foreign key, and tenant/workspace constraint behavior.
## Test Strategy
Required validation during implementation:
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/Resources/ResourceIdentityTest.php tests/Unit/Support/Resources/ProviderResourceDescriptorTest.php tests/Unit/Support/Baselines/BaselineSubjectKeyCanonicalIdentityTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ProviderResources/ProviderResourceBindingServiceTest.php tests/Feature/ProviderResources/ProviderResourceBindingAuthorizationTest.php`
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest -c phpunit.pgsql.xml tests/Feature/ProviderResources/ProviderResourceBindingPostgresTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Baselines/BaselineCompareGapClassificationTest.php tests/Feature/Evidence/BaselineDriftPostureSourceTest.php tests/Feature/ReviewPack/Spec349ReviewPackResolutionGuidanceTest.php`
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `git diff --check`
If Sail is unavailable, use `./scripts/platform-sail ...` only if configured and record the blocker in the implementation close-out.
## Rollout Considerations
- Additive schema migration only.
- Staging must run the migration and PostgreSQL lane before Production.
- No env var, queue, scheduler, storage, or reverse-proxy changes are expected.
- No queue worker restart is specifically required beyond the normal deploy/reload process.
- Rollback is dropping the new table before any follow-up spec consumes it; after follow-up consumption exists, rollback must be redesigned.
- Since the product is pre-production, no historical backfill is required.
## Risk Controls
- Keep all binding records managed-environment scoped.
- Enforce workspace/environment alignment in the database and the model, not only in the service.
- Reject cross-scope provider connections and source references before persistence.
- Use partial unique index and service transaction to prevent duplicate active bindings.
- Require operator note for all governance-impacting decisions.
- Audit every mutation with redacted metadata and safe note handling.
- Add fake-provider tests to block Microsoft-only leakage through both identity primitives and service-level binding persistence.
- Add no-op regression tests so compare/evidence/review behavior remains unchanged.
- Stop and update spec/plan before adding UI, matching pipeline consumption, workspace-level scope, provider-specific built-in mappings, or evidence/review changes.
## Implementation Phases
### Phase 1 - Baseline And Guardrail Reconfirmation
Confirm repo state, completed-spec guardrails, current baseline/provider classes, capability patterns, audit patterns, and PostgreSQL migration conventions.
### Phase 2 - Identity And Canonical Key Foundation
Add identity/descriptor primitives and extend `BaselineSubjectKey` for canonical provider resource keys with unit coverage.
### Phase 3 - Binding Persistence And Integrity
Add migration, model, factory, enums, policy, service, PostgreSQL partial unique index, and tests.
### Phase 4 - Audit, RBAC, And Fake Provider Proof
Add service-level authorization, audit events, fake-provider fixtures/tests, and no-secret audit metadata assertions.
### Phase 5 - No-Op Runtime Regression
Run targeted compare, evidence, and review tests; ensure no current pipeline consumes bindings.
### Phase 6 - Final Validation And Hygiene
Run formatting, `git diff --check`, PostgreSQL lane, and final artifact close-out.
## Structure Decision
Use the existing Laravel monolith structure under `apps/platform`. Add small support classes under `app/Support/Resources`, extend the existing baseline support namespace only where current semantics already live, and keep all mutation behavior in a service under `app/Services/Resources`.
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|---|---|---|
| New table/model `provider_resource_bindings` | Binding decisions must survive runs, be unique, be auditable, and affect future governance truth | Run-local JSON context or display-name normalization would not be durable, constrained, or independently auditable |
| New identity/descriptor classes | Provider resource identity must represent built-ins, virtuals, unsupported resources, and fake providers without display names | Extending compare result objects would couple persistent identity to one runtime pipeline |
| New status/mode enums | Active lookup, supersession, revocation, audit meaning, and future operator action differ by state/mode | String literals or presentation labels would spread state semantics through services/tests |
## Proportionality Review
- **Current operator problem**: Future operators need stable, auditable provider resource identity decisions instead of repeated ambiguity around mutable labels.
- **Existing structure is insufficient because**: Current subject/resolution support classifies run outcomes but does not persist binding decisions or provider resource identity truth.
- **Narrowest correct implementation**: Managed-environment-only binding table, reuse existing baseline capability and subject-key seams, no UI, no matching rewrite, no provider canonicalizer.
- **Ownership cost created**: Migration/index, model/policy/service, two support classes, two enum families, PostgreSQL tests, and provider-boundary review burden.
- **Alternative intentionally rejected**: Display-name normalization, run-context notes, and workspace-level/baseline-profile scope in v1.
- **Release truth**: Current-release foundation needed to unblock the immediately proposed follow-up specs without increasing current runtime behavior risk.
## Filament v5 Output Contract For Implementation Close-Out
- **Livewire v4.0+ compliance**: unchanged; Livewire 4.1.4 is installed and no Livewire component changes are planned.
- **Provider registration location**: unchanged; Laravel 12 panel providers remain in `apps/platform/bootstrap/providers.php`.
- **Global search**: no Filament Resource is added; `ProviderResourceBinding` is not globally searchable.
- **Destructive/high-impact actions**: no Filament action is added. Backend supersede/revoke decisions are high-impact and require policy authorization plus audit; future UI confirmation belongs to Spec 384.
- **Asset strategy**: no new assets; no Spec 381-specific `filament:assets` concern beyond normal deployment.
- **Testing plan**: Unit, Feature, PostgreSQL, and targeted no-op regression tests listed above.
- **Deployment impact**: additive migration only; no env vars, queues, scheduler, storage, or assets expected.