$attributes */ public function createExactProviderIdentity(User $actor, ManagedEnvironment|int $environment, ResourceIdentity $identity, array $attributes): ProviderResourceBinding { return $this->createDecision($actor, $environment, ProviderResourceResolutionMode::ExactProviderIdentity, $identity, $attributes); } /** * @param array $attributes */ public function createCanonicalBuiltin(User $actor, ManagedEnvironment|int $environment, ResourceIdentity $identity, array $attributes): ProviderResourceBinding { return $this->createDecision($actor, $environment, ProviderResourceResolutionMode::CanonicalBuiltin, $identity, $attributes); } /** * @param array $attributes */ public function createCanonicalVirtualTarget(User $actor, ManagedEnvironment|int $environment, ResourceIdentity $identity, array $attributes): ProviderResourceBinding { return $this->createDecision($actor, $environment, ProviderResourceResolutionMode::CanonicalVirtualTarget, $identity, $attributes); } /** * @param array $attributes */ public function createManualBinding(User $actor, ManagedEnvironment|int $environment, ResourceIdentity $identity, array $attributes): ProviderResourceBinding { return $this->createDecision($actor, $environment, ProviderResourceResolutionMode::ManualBinding, $identity, $attributes); } /** * @param array $attributes */ public function createExclusion(User $actor, ManagedEnvironment|int $environment, ResourceIdentity $identity, array $attributes): ProviderResourceBinding { return $this->createDecision($actor, $environment, ProviderResourceResolutionMode::ExcludedNonGoverned, $identity, $attributes); } /** * @param array $attributes */ public function createAcceptedLimitation(User $actor, ManagedEnvironment|int $environment, ResourceIdentity $identity, array $attributes): ProviderResourceBinding { return $this->createDecision($actor, $environment, ProviderResourceResolutionMode::AcceptedLimitation, $identity, $attributes); } /** * @param array $attributes */ public function markUnsupported(User $actor, ManagedEnvironment|int $environment, ResourceIdentity $identity, array $attributes): ProviderResourceBinding { return $this->createDecision($actor, $environment, ProviderResourceResolutionMode::UnsupportedCoverage, $identity, $attributes); } /** * @param array $attributes */ public function markMissingExpected(User $actor, ManagedEnvironment|int $environment, ResourceIdentity $identity, array $attributes): ProviderResourceBinding { return $this->createDecision($actor, $environment, ProviderResourceResolutionMode::MissingExpected, $identity, $attributes); } public function revoke(User $actor, ProviderResourceBinding|int $binding, string $operatorNote, ?string $resolutionReason = null): ProviderResourceBinding { $binding = $binding instanceof ProviderResourceBinding ? $binding : ProviderResourceBinding::query()->findOrFail($binding); Gate::forUser($actor)->authorize('revoke', $binding); $note = $this->requiredNote($operatorNote); return DB::transaction(function () use ($binding, $actor, $note, $resolutionReason): ProviderResourceBinding { /** @var ProviderResourceBinding $locked */ $locked = ProviderResourceBinding::query()->lockForUpdate()->findOrFail((int) $binding->getKey()); if (! $locked->isActive()) { return $locked->refresh(); } $locked->forceFill([ 'binding_status' => ProviderResourceBindingStatus::Revoked->value, 'operator_note' => $note, 'resolution_reason' => $this->nullableString($resolutionReason), 'ended_at' => now(), ])->save(); $this->audit( action: AuditActionId::ProviderResourceBindingRevoked, actor: $actor, binding: $locked->refresh(), oldBinding: $locked, operatorNote: $note, ); return $locked; }); } /** * @param array $attributes */ private function createDecision( User $actor, ManagedEnvironment|int $environment, ProviderResourceResolutionMode $resolutionMode, ResourceIdentity $identity, array $attributes, ): ProviderResourceBinding { $tenant = $this->resolveEnvironment($environment); Gate::forUser($actor)->authorize('createForEnvironment', [ProviderResourceBinding::class, $tenant]); $operatorNote = $this->requiredNote($attributes['operator_note'] ?? null); $subjectDomain = $this->requiredString($attributes, 'subject_domain'); $subjectClass = $this->requiredSubjectClass($attributes['subject_class'] ?? null); $subjectTypeKey = $this->requiredString($attributes, 'subject_type_key'); $canonicalSubjectKey = $this->nullableString($attributes['canonical_subject_key'] ?? null) ?? BaselineSubjectKey::forProviderResourceIdentity($subjectDomain, $subjectClass, $subjectTypeKey, $identity); if ($canonicalSubjectKey === null) { throw new InvalidArgumentException('Provider resource binding requires a canonical subject key.'); } $scope = [ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), ]; $providerConnectionId = $this->validatedProviderConnectionId($attributes['provider_connection_id'] ?? null, $tenant, $identity->providerKey); $sourceAttributes = $this->validatedSourceAttributes($attributes, $tenant); return DB::transaction(function () use ( $actor, $tenant, $identity, $resolutionMode, $attributes, $operatorNote, $subjectDomain, $subjectClass, $subjectTypeKey, $canonicalSubjectKey, $scope, $providerConnectionId, $sourceAttributes, ): ProviderResourceBinding { $oldBinding = ProviderResourceBinding::query() ->where($scope) ->where('provider_key', $identity->providerKey) ->where('canonical_subject_key', $canonicalSubjectKey) ->active() ->lockForUpdate() ->first(); if ($oldBinding instanceof ProviderResourceBinding) { $oldBinding->forceFill([ 'binding_status' => ProviderResourceBindingStatus::Superseded->value, 'ended_at' => now(), ])->save(); } $binding = ProviderResourceBinding::query()->create([ ...$scope, 'provider_key' => $identity->providerKey, 'provider_connection_id' => $providerConnectionId, 'subject_domain' => $subjectDomain, 'subject_class' => $subjectClass instanceof SubjectClass ? $subjectClass->value : $subjectClass, 'subject_type_key' => $subjectTypeKey, 'legacy_subject_key' => $this->nullableString($attributes['legacy_subject_key'] ?? null), 'canonical_subject_key' => $canonicalSubjectKey, 'provider_resource_type' => $identity->providerResourceType, 'provider_resource_id' => $identity->providerResourceId, 'provider_resource_discriminator' => $identity->canonicalDiscriminator, 'provider_resource_identity_kind' => $identity->identityKind, 'provider_resource_fingerprint' => $identity->fingerprint(), 'display_label' => $this->nullableString($attributes['display_label'] ?? null), 'binding_status' => ProviderResourceBindingStatus::Active->value, 'resolution_mode' => $resolutionMode->value, 'resolution_reason' => $this->nullableString($attributes['resolution_reason'] ?? null), 'operator_note' => $operatorNote, ...$sourceAttributes, 'decided_by_user_id' => (int) $actor->getKey(), 'decided_at' => now(), 'ended_at' => null, ]); $this->audit( action: $oldBinding instanceof ProviderResourceBinding ? AuditActionId::ProviderResourceBindingSuperseded : AuditActionId::ProviderResourceBindingCreated, actor: $actor, binding: $binding, oldBinding: $oldBinding, operatorNote: $operatorNote, ); return $binding->refresh(); }); } private function resolveEnvironment(ManagedEnvironment|int $environment): ManagedEnvironment { if ($environment instanceof ManagedEnvironment) { $environment->refresh(); return $environment; } return ManagedEnvironment::query()->whereKey($environment)->firstOrFail(); } private function requiredNote(mixed $value): string { if (! is_string($value) || trim($value) === '') { throw new InvalidArgumentException('Provider resource binding decisions require an operator note.'); } return trim($value); } /** * @param array $attributes */ private function requiredString(array $attributes, string $key): string { $value = $attributes[$key] ?? null; if (! is_string($value) || trim($value) === '') { throw new InvalidArgumentException(sprintf('Provider resource binding requires [%s].', $key)); } return trim($value); } private function requiredSubjectClass(mixed $value): SubjectClass|string { if ($value instanceof SubjectClass) { return $value; } if (! is_string($value) || trim($value) === '') { throw new InvalidArgumentException('Provider resource binding requires [subject_class].'); } return trim($value); } private function validatedProviderConnectionId(mixed $providerConnectionId, ManagedEnvironment $tenant, string $providerKey): ?int { if ($providerConnectionId === null || $providerConnectionId === '') { return null; } if (! is_numeric($providerConnectionId)) { throw new InvalidArgumentException('Provider connection reference must be numeric.'); } $connection = ProviderConnection::query() ->whereKey((int) $providerConnectionId) ->where('workspace_id', (int) $tenant->workspace_id) ->where('managed_environment_id', (int) $tenant->getKey()) ->where('provider', $providerKey) ->first(); if (! $connection instanceof ProviderConnection) { $this->throwNotFound(ProviderConnection::class, (int) $providerConnectionId); } return (int) $connection->getKey(); } /** * @param array $attributes * @return array */ private function validatedSourceAttributes(array $attributes, ManagedEnvironment $tenant): array { return [ 'source_operation_run_id' => $this->validatedScopedId(OperationRun::class, $attributes['source_operation_run_id'] ?? null, $tenant, true), 'source_baseline_snapshot_id' => $this->validatedBaselineSnapshotId($attributes['source_baseline_snapshot_id'] ?? null, $tenant), 'source_inventory_item_id' => $this->validatedScopedId(InventoryItem::class, $attributes['source_inventory_item_id'] ?? null, $tenant, true), 'source_policy_version_id' => $this->validatedScopedId(PolicyVersion::class, $attributes['source_policy_version_id'] ?? null, $tenant, true), ]; } private function validatedBaselineSnapshotId(mixed $id, ManagedEnvironment $tenant): ?int { if ($id === null || $id === '') { return null; } if (! is_numeric($id)) { throw new InvalidArgumentException('BaselineSnapshot reference must be numeric.'); } $snapshot = BaselineSnapshot::query() ->whereKey((int) $id) ->where('workspace_id', (int) $tenant->workspace_id) ->first(); if (! $snapshot instanceof BaselineSnapshot) { $this->throwNotFound(BaselineSnapshot::class, (int) $id); } $assignedToTenant = BaselineTenantAssignment::query() ->where('workspace_id', (int) $tenant->workspace_id) ->where('managed_environment_id', (int) $tenant->getKey()) ->where('baseline_profile_id', (int) $snapshot->baseline_profile_id) ->exists(); if (! $assignedToTenant) { $this->throwNotFound(BaselineSnapshot::class, (int) $id); } return (int) $snapshot->getKey(); } /** * @param class-string $modelClass */ private function validatedScopedId(string $modelClass, mixed $id, ManagedEnvironment $tenant, bool $requiresEnvironment): ?int { if ($id === null || $id === '') { return null; } if (! is_numeric($id)) { throw new InvalidArgumentException(sprintf('%s reference must be numeric.', class_basename($modelClass))); } $query = $modelClass::query() ->whereKey((int) $id) ->where('workspace_id', (int) $tenant->workspace_id); if ($requiresEnvironment) { $query->where('managed_environment_id', (int) $tenant->getKey()); } if (! $query->exists()) { $this->throwNotFound($modelClass, (int) $id); } return (int) $id; } /** * @param class-string $modelClass */ private function throwNotFound(string $modelClass, int $id): never { throw (new ModelNotFoundException)->setModel($modelClass, [$id]); } private function nullableString(mixed $value): ?string { return is_string($value) && trim($value) !== '' ? trim($value) : null; } private function audit( AuditActionId $action, User $actor, ProviderResourceBinding $binding, ?ProviderResourceBinding $oldBinding, string $operatorNote, ): void { $metadata = [ 'provider_resource_binding_id' => (int) $binding->getKey(), 'old_binding_id' => $oldBinding instanceof ProviderResourceBinding ? (int) $oldBinding->getKey() : null, 'new_binding_id' => (int) $binding->getKey(), 'provider_key' => (string) $binding->provider_key, 'canonical_subject_key' => (string) $binding->canonical_subject_key, 'subject_class' => (string) $binding->subject_class, 'subject_type_key' => (string) $binding->subject_type_key, 'resolution_mode' => $binding->resolution_mode instanceof ProviderResourceResolutionMode ? $binding->resolution_mode->value : (string) $binding->resolution_mode, 'binding_status' => $binding->binding_status instanceof ProviderResourceBindingStatus ? $binding->binding_status->value : (string) $binding->binding_status, 'provider_resource_type' => $binding->provider_resource_type, 'provider_resource_identity_kind' => $binding->provider_resource_identity_kind, 'provider_resource_fingerprint' => $binding->provider_resource_fingerprint, 'source_operation_run_id' => $binding->source_operation_run_id, 'source_baseline_snapshot_id' => $binding->source_baseline_snapshot_id, 'source_inventory_item_id' => $binding->source_inventory_item_id, 'source_policy_version_id' => $binding->source_policy_version_id, 'operator_note_sha256' => hash('sha256', $operatorNote), 'operator_note_length' => mb_strlen($operatorNote), 'resolution_reason_present' => filled($binding->resolution_reason), ]; $this->auditRecorder->record( action: $action, context: ['metadata' => array_filter($metadata, static fn (mixed $value): bool => $value !== null)], workspace: $binding->workspace, tenant: $binding->tenant, actor: AuditActorSnapshot::human($actor), target: new AuditTargetSnapshot( type: 'provider_resource_binding', id: (string) $binding->getKey(), label: 'Provider resource binding #'.$binding->getKey(), ), outcome: AuditOutcome::Success, ); } }