# 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/_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.