TenantAtlas/apps/platform/app/Services/Providers/ProviderOperationRegistry.php
ahmido bd26e209de
Some checks failed
Main Confidence / confidence (push) Failing after 57s
feat: harden provider boundaries (#273)
## Summary
- add the provider boundary catalog, boundary support types, and guardrails for platform-core versus provider-owned seams
- harden provider gateway, identity resolution, operation registry, and start-gate behavior to require explicit provider bindings
- add unit and feature coverage for boundary classification, runtime preservation, unsupported paths, and platform-core leakage guards
- add the full Spec Kit artifact set for spec 237 and update roadmap/spec-candidate tracking

## Validation
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Providers/ProviderBoundaryClassificationTest.php tests/Unit/Providers/ProviderBoundaryGuardrailTest.php tests/Feature/Providers/ProviderBoundaryHardeningTest.php tests/Feature/Providers/UnsupportedProviderBoundaryPathTest.php tests/Feature/Guards/ProviderBoundaryPlatformCoreGuardTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Providers/ProviderGatewayTest.php tests/Unit/Providers/ProviderIdentityResolverTest.php tests/Unit/Providers/ProviderOperationStartGateTest.php`
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- browser smoke: `http://localhost/admin/provider-connections?tenant_id=18000000-0000-4000-8000-000000000180` loaded with the local smoke user, the empty-state CTA reached the canonical create route, and cancel returned to the scoped list

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #273
2026-04-24 21:05:37 +00:00

220 lines
8.8 KiB
PHP

<?php
namespace App\Services\Providers;
use App\Support\Auth\Capabilities;
use InvalidArgumentException;
final class ProviderOperationRegistry
{
public const string BINDING_ACTIVE = 'active';
public const string BINDING_UNSUPPORTED = 'unsupported';
/**
* @return array<string, array{operation_type: string, module: string, label: string, required_capability: string}>
*/
public function definitions(): array
{
return [
'provider.connection.check' => [
'operation_type' => 'provider.connection.check',
'module' => 'health_check',
'label' => 'Provider connection check',
'required_capability' => Capabilities::PROVIDER_RUN,
],
'inventory_sync' => [
'operation_type' => 'inventory_sync',
'module' => 'inventory',
'label' => 'Inventory sync',
'required_capability' => Capabilities::PROVIDER_RUN,
],
'compliance.snapshot' => [
'operation_type' => 'compliance.snapshot',
'module' => 'compliance',
'label' => 'Compliance snapshot',
'required_capability' => Capabilities::PROVIDER_RUN,
],
'restore.execute' => [
'operation_type' => 'restore.execute',
'module' => 'restore',
'label' => 'Restore execution',
'required_capability' => Capabilities::TENANT_MANAGE,
],
'entra_group_sync' => [
'operation_type' => 'entra_group_sync',
'module' => 'directory_groups',
'label' => 'Directory groups sync',
'required_capability' => Capabilities::TENANT_SYNC,
],
'directory_role_definitions.sync' => [
'operation_type' => 'directory_role_definitions.sync',
'module' => 'directory_role_definitions',
'label' => 'Role definitions sync',
'required_capability' => Capabilities::TENANT_MANAGE,
],
];
}
/**
* @return array<string, array{operation_type: string, module: string, label: string, required_capability: string}>
*/
public function all(): array
{
return $this->definitions();
}
/**
* @return array<string, array<string, array{operation_type: string, provider: string, binding_status: string, handler_notes: string, exception_notes: string}>>
*/
public function providerBindings(): array
{
return [
'provider.connection.check' => [
'microsoft' => $this->activeMicrosoftBinding(
operationType: 'provider.connection.check',
handlerNotes: 'Uses the current Microsoft Graph provider connection health-check workflow.',
exceptionNotes: 'Current-release provider binding remains Microsoft-only until a real second provider case exists.',
),
],
'inventory_sync' => [
'microsoft' => $this->activeMicrosoftBinding(
operationType: 'inventory_sync',
handlerNotes: 'Uses the current Microsoft Intune inventory sync workflow.',
exceptionNotes: 'Inventory collection is currently Microsoft Intune-specific provider behavior.',
),
],
'compliance.snapshot' => [
'microsoft' => $this->activeMicrosoftBinding(
operationType: 'compliance.snapshot',
handlerNotes: 'Uses the current Microsoft compliance snapshot workflow.',
exceptionNotes: 'Compliance snapshot runtime remains bounded to the Microsoft provider.',
),
],
'restore.execute' => [
'microsoft' => $this->activeMicrosoftBinding(
operationType: 'restore.execute',
handlerNotes: 'Uses the current Microsoft restore execution workflow.',
exceptionNotes: 'Restore execution remains Microsoft-only and must preserve dry-run and audit safeguards.',
),
],
'entra_group_sync' => [
'microsoft' => $this->activeMicrosoftBinding(
operationType: 'entra_group_sync',
handlerNotes: 'Uses the current Microsoft Entra group synchronization workflow.',
exceptionNotes: 'The operation type keeps current Entra vocabulary until the identity-neutrality follow-up.',
),
],
'directory_role_definitions.sync' => [
'microsoft' => $this->activeMicrosoftBinding(
operationType: 'directory_role_definitions.sync',
handlerNotes: 'Uses the current Microsoft directory role definition synchronization workflow.',
exceptionNotes: 'Directory role definitions are Microsoft-owned provider semantics.',
),
],
];
}
public function isAllowed(string $operationType): bool
{
return array_key_exists(trim($operationType), $this->definitions());
}
/**
* @return array{operation_type: string, module: string, label: string, required_capability: string}
*/
public function get(string $operationType): array
{
$operationType = trim($operationType);
$definition = $this->definitions()[$operationType] ?? null;
if (! is_array($definition)) {
throw new InvalidArgumentException("Unknown provider operation type: {$operationType}");
}
return $definition;
}
/**
* @return array{operation_type: string, provider: string, binding_status: string, handler_notes: string, exception_notes: string}|null
*/
public function bindingFor(string $operationType, string $provider): ?array
{
$operationType = trim($operationType);
$provider = trim($provider);
if ($operationType === '' || $provider === '') {
return null;
}
$bindings = $this->providerBindings()[$operationType] ?? [];
return $bindings[$provider] ?? null;
}
/**
* @return array{operation_type: string, provider: string, binding_status: string, handler_notes: string, exception_notes: string}|null
*/
public function activeBindingFor(string $operationType): ?array
{
$operationType = trim($operationType);
$bindings = $this->providerBindings()[$operationType] ?? [];
foreach ($bindings as $binding) {
if (($binding['binding_status'] ?? null) === self::BINDING_ACTIVE) {
return $binding;
}
}
return null;
}
/**
* @return array{
* definition: array{operation_type: string, module: string, label: string, required_capability: string},
* binding: array{operation_type: string, provider: string, binding_status: string, handler_notes: string, exception_notes: string}
* }
*/
public function boundaryOperation(string $operationType, ?string $provider = null): array
{
$definition = $this->get($operationType);
$binding = is_string($provider) && trim($provider) !== ''
? $this->bindingFor($operationType, $provider)
: $this->activeBindingFor($operationType);
return [
'definition' => $definition,
'binding' => $binding ?? $this->unsupportedBinding($operationType, $provider ?? 'unknown'),
];
}
/**
* @return array{operation_type: string, provider: string, binding_status: string, handler_notes: string, exception_notes: string}
*/
public function unsupportedBinding(string $operationType, string $provider): array
{
return [
'operation_type' => trim($operationType),
'provider' => trim($provider) !== '' ? trim($provider) : 'unknown',
'binding_status' => self::BINDING_UNSUPPORTED,
'handler_notes' => 'No explicit provider binding exists for this operation/provider combination.',
'exception_notes' => 'Unsupported combinations must block explicitly instead of inheriting Microsoft behavior.',
];
}
/**
* @return array{operation_type: string, provider: string, binding_status: string, handler_notes: string, exception_notes: string}
*/
private function activeMicrosoftBinding(string $operationType, string $handlerNotes, string $exceptionNotes): array
{
return [
'operation_type' => $operationType,
'provider' => 'microsoft',
'binding_status' => self::BINDING_ACTIVE,
'handler_notes' => $handlerNotes,
'exception_notes' => $exceptionNotes,
];
}
}