TenantAtlas/specs/381-provider-resource-identity-binding/plan.md
ahmido 04d0d6184f feat(resources): implement provider resource identity binding (#452)
Added `ProviderResourceBinding` model, migrations, policies, and supporting framework for canonical resource identity mapping as defined in Spec 381. This provides the structural capability to resolve baseline and posture discrepancies by binding logical entities across source providers to canonical identities.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #452
2026-06-15 18:45:38 +00:00

26 KiB

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.