TenantAtlas/apps/platform/app/Services/Intune/ManagedEnvironmentRequiredPermissionsViewModelBuilder.php
Ahmed Darrazi 1245af12af
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m25s
feat: improve provider readiness semantics and freshness guidance
2026-06-21 19:18:00 +02:00

651 lines
24 KiB
PHP

<?php
namespace App\Services\Intune;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Support\Providers\Capabilities\ProviderCapabilityDefinition;
use App\Support\Providers\Capabilities\ProviderCapabilityRegistry;
use App\Support\Providers\Capabilities\ProviderCapabilityStatus;
use App\Support\Providers\Readiness\ProviderReadinessResolver;
use App\Support\Verification\ManagedEnvironmentPermissionCheckClusters;
use App\Support\Verification\VerificationReportOverall;
use Carbon\CarbonInterface;
use Illuminate\Support\Carbon;
class ManagedEnvironmentRequiredPermissionsViewModelBuilder
{
/**
* @phpstan-type ManagedEnvironmentPermissionRow array{key:string,type:'application'|'delegated',description:?string,features:array<int,string>,status:'granted'|'missing'|'blocked'|'expired'|'unknown'|'not_applicable',details:array<string,mixed>|null}
* @phpstan-type FeatureImpact array{feature:string,missing:int,required_application:int,required_delegated:int,blocked:bool}
* @phpstan-type CapabilityGroup array{provider_capability_key:string,label:string,status:string,provider_requirement_keys:array<int,string>,missing_requirement_keys:array<int,string>,evidence_counts:array{requirements:int,missing:int,errors:int},message:string}
* @phpstan-type FilterState array{status:'missing'|'granted'|'all',type:'application'|'delegated'|'all',features:array<int,string>,search:string}
* @phpstan-type ViewModel array{
* tenant: array{id:int,external_id:string,name:string},
* overview: array{
* overall: string,
* counts: array<string,int>,
* feature_impacts: array<int, FeatureImpact>,
* capability_groups: array<int, CapabilityGroup>,
* primary_capability_group: CapabilityGroup|null,
* freshness: array{last_refreshed_at:?string,is_stale:bool}
* },
* permissions: array<int, ManagedEnvironmentPermissionRow>,
* filters: FilterState,
* copy: array{application:string,delegated:string}
* }
*/
public function __construct(private readonly ManagedEnvironmentPermissionService $permissionService) {}
/**
* @param array<string, mixed> $filters
* @return ViewModel
*/
public function build(ManagedEnvironment $tenant, array $filters = []): array
{
$comparison = $this->readinessComparison($tenant);
/** @var array<int, ManagedEnvironmentPermissionRow> $allPermissions */
$allPermissions = collect($comparison['permissions'] ?? [])
->filter(fn (mixed $row): bool => is_array($row))
->map(fn (array $row): array => self::normalizePermissionRow($row))
->values()
->all();
$state = self::normalizeFilterState($filters);
$filteredPermissions = self::applyFilterState($allPermissions, $state);
$freshness = is_array($comparison['freshness'] ?? null)
? array_replace([
'last_refreshed_at' => null,
'is_stale' => true,
], $comparison['freshness'])
: self::deriveFreshness(self::parseLastRefreshedAt($comparison['last_refreshed_at'] ?? null));
$summaryPermissions = $allPermissions;
$capabilityGroups = self::deriveCapabilityGroups($allPermissions, $freshness);
return [
'tenant' => [
'id' => (int) $tenant->getKey(),
'external_id' => (string) $tenant->external_id,
'name' => (string) $tenant->name,
],
'overview' => [
'overall' => self::deriveOverallStatus($summaryPermissions, (bool) ($freshness['is_stale'] ?? true)),
'counts' => self::deriveCounts($summaryPermissions),
'feature_impacts' => self::deriveFeatureImpacts($summaryPermissions),
'capability_groups' => $capabilityGroups,
'primary_capability_group' => self::primaryCapabilityGroup($capabilityGroups),
'freshness' => $freshness,
],
'permissions' => $filteredPermissions,
'filters' => $state,
'copy' => [
'application' => self::deriveCopyPayload($allPermissions, 'application', $state['features']),
'delegated' => self::deriveCopyPayload($allPermissions, 'delegated', $state['features']),
],
];
}
/**
* @param array<int, ManagedEnvironmentPermissionRow> $permissions
* @param array{last_refreshed_at:?string,is_stale:bool} $freshness
* @return array<int, CapabilityGroup>
*/
public static function deriveCapabilityGroups(array $permissions, array $freshness): array
{
/** @var ProviderCapabilityRegistry $registry */
$registry = app(ProviderCapabilityRegistry::class);
return array_map(
static fn (ProviderCapabilityDefinition $definition): array => self::deriveCapabilityGroup(
definition: $definition,
permissions: $permissions,
isStale: (bool) ($freshness['is_stale'] ?? true),
),
array_values($registry->all()),
);
}
/**
* @param array<int, CapabilityGroup> $groups
* @return CapabilityGroup|null
*/
public static function primaryCapabilityGroup(array $groups): ?array
{
if ($groups === []) {
return null;
}
usort($groups, static function (array $a, array $b): int {
$aStatus = ProviderCapabilityStatus::tryFrom((string) ($a['status'] ?? 'unknown')) ?? ProviderCapabilityStatus::Unknown;
$bStatus = ProviderCapabilityStatus::tryFrom((string) ($b['status'] ?? 'unknown')) ?? ProviderCapabilityStatus::Unknown;
return $aStatus->priority() <=> $bStatus->priority();
});
return $groups[0];
}
/**
* @param array<int, ManagedEnvironmentPermissionRow> $permissions
* @return CapabilityGroup
*/
private static function deriveCapabilityGroup(
ProviderCapabilityDefinition $definition,
array $permissions,
bool $isStale,
): array {
$rowsByRequirement = [];
foreach ($definition->providerRequirementKeys as $requirementKey) {
$rowsByRequirement[$requirementKey] = ManagedEnvironmentPermissionCheckClusters::rowsForRequirementKey($permissions, $requirementKey);
}
$rows = array_values(array_merge(...array_values($rowsByRequirement ?: [[]])));
$missingRows = array_values(array_filter(
$rows,
static fn (array $row): bool => ($row['status'] ?? null) === 'missing',
));
$blockedRows = array_values(array_filter(
$rows,
static fn (array $row): bool => ($row['status'] ?? null) === 'blocked',
));
$expiredRows = array_values(array_filter(
$rows,
static fn (array $row): bool => ($row['status'] ?? null) === 'expired',
));
$unknownRows = array_values(array_filter(
$rows,
static fn (array $row): bool => ($row['status'] ?? null) === 'unknown',
));
$missingRequirementKeys = [];
foreach ($rowsByRequirement as $requirementKey => $requirementRows) {
foreach ($requirementRows as $row) {
if (in_array(($row['status'] ?? null), ['missing', 'blocked', 'expired', 'unknown'], true)) {
$missingRequirementKeys[] = (string) $requirementKey;
break;
}
}
}
$status = match (true) {
$rows === [] => ProviderCapabilityStatus::NotApplicable,
$blockedRows !== [] => ProviderCapabilityStatus::Blocked,
$missingRows !== [] => ProviderCapabilityStatus::Missing,
$expiredRows !== [] || $unknownRows !== [] => ProviderCapabilityStatus::Unknown,
$isStale => ProviderCapabilityStatus::Unknown,
default => ProviderCapabilityStatus::Supported,
};
$message = match ($status) {
ProviderCapabilityStatus::Supported => "{$definition->label} capability is supported by stored permission evidence.",
ProviderCapabilityStatus::Missing => "{$definition->label} capability is missing required provider permissions.",
ProviderCapabilityStatus::Unknown => "{$definition->label} capability needs refreshed permission evidence.",
ProviderCapabilityStatus::Blocked => "{$definition->label} capability is blocked.",
ProviderCapabilityStatus::NotApplicable => "{$definition->label} capability has no mapped permission rows for this tenant.",
};
return [
'provider_capability_key' => $definition->key,
'label' => $definition->label,
'status' => $status->value,
'provider_requirement_keys' => $definition->providerRequirementKeys,
'missing_requirement_keys' => array_values(array_unique($missingRequirementKeys)),
'evidence_counts' => [
'requirements' => count($rows),
'missing' => count($missingRows),
'errors' => 0,
'blocked' => count($blockedRows),
'expired' => count($expiredRows),
'unknown' => count($unknownRows),
],
'message' => $message,
];
}
/**
* @param array<int, ManagedEnvironmentPermissionRow> $permissions
*/
public static function deriveOverallStatus(array $permissions, bool $hasStaleFreshness = false): string
{
$hasBlockedApplication = collect($permissions)->contains(
fn (array $row): bool => in_array($row['status'], ['missing', 'blocked'], true) && $row['type'] === 'application',
);
if ($hasBlockedApplication) {
return VerificationReportOverall::Blocked->value;
}
$hasNeedsReview = collect($permissions)->contains(
fn (array $row): bool => in_array($row['status'], ['expired', 'unknown'], true),
);
$hasMissingDelegated = collect($permissions)->contains(
fn (array $row): bool => $row['status'] === 'missing' && $row['type'] === 'delegated',
);
if ($hasNeedsReview || $hasMissingDelegated || $hasStaleFreshness) {
return VerificationReportOverall::NeedsAttention->value;
}
return VerificationReportOverall::Ready->value;
}
/**
* @return array{last_refreshed_at:?string,is_stale:bool}
*/
public static function deriveFreshness(?CarbonInterface $lastRefreshedAt, ?CarbonInterface $referenceTime = null): array
{
$reference = $referenceTime instanceof Carbon
? $referenceTime->copy()
: ($referenceTime !== null ? Carbon::instance($referenceTime) : now());
$lastRefreshed = $lastRefreshedAt instanceof Carbon
? $lastRefreshedAt
: ($lastRefreshedAt !== null ? Carbon::instance($lastRefreshedAt) : null);
$isStale = $lastRefreshed === null
|| $lastRefreshed->lt($reference->copy()->subDays(30));
return [
'last_refreshed_at' => $lastRefreshed?->toIso8601String(),
'is_stale' => $isStale,
];
}
/**
* @param array<int, ManagedEnvironmentPermissionRow> $permissions
* @return array<string, int>
*/
public static function deriveCounts(array $permissions): array
{
$counts = [
'missing_application' => 0,
'missing_delegated' => 0,
'missing' => 0,
'granted' => 0,
'blocked' => 0,
'expired' => 0,
'unknown' => 0,
'not_applicable' => 0,
'required' => 0,
];
foreach ($permissions as $row) {
if (($row['status'] ?? null) === 'missing') {
if (($row['type'] ?? null) === 'delegated') {
$counts['missing_delegated'] += 1;
} else {
$counts['missing_application'] += 1;
}
$counts['missing'] += 1;
continue;
}
if (($row['status'] ?? null) === 'granted') {
$counts['granted'] += 1;
continue;
}
if (in_array(($row['status'] ?? null), ['blocked', 'expired', 'unknown', 'not_applicable'], true)) {
$counts[(string) $row['status']] += 1;
continue;
}
if (($row['status'] ?? null) === 'error') {
$counts['unknown'] += 1;
}
}
$counts['required'] = $counts['missing']
+ $counts['granted']
+ $counts['blocked']
+ $counts['expired']
+ $counts['unknown'];
return $counts;
}
/**
* @param array<int, ManagedEnvironmentPermissionRow> $permissions
* @return array<int, FeatureImpact>
*/
public static function deriveFeatureImpacts(array $permissions): array
{
/** @var array<string, FeatureImpact> $impacts */
$impacts = [];
foreach ($permissions as $row) {
$features = array_values(array_unique($row['features'] ?? []));
foreach ($features as $feature) {
if (! isset($impacts[$feature])) {
$impacts[$feature] = [
'feature' => $feature,
'missing' => 0,
'required_application' => 0,
'required_delegated' => 0,
'blocked' => false,
];
}
if (($row['type'] ?? null) === 'delegated') {
$impacts[$feature]['required_delegated'] += 1;
} else {
$impacts[$feature]['required_application'] += 1;
}
if (in_array(($row['status'] ?? null), ['missing', 'blocked', 'expired', 'unknown'], true)) {
$impacts[$feature]['missing'] += 1;
if (($row['type'] ?? null) === 'application' && in_array(($row['status'] ?? null), ['missing', 'blocked'], true)) {
$impacts[$feature]['blocked'] = true;
}
}
}
}
$values = array_values($impacts);
usort($values, static function (array $a, array $b): int {
$blocked = (int) ($b['blocked'] <=> $a['blocked']);
if ($blocked !== 0) {
return $blocked;
}
$missing = (int) (($b['missing'] ?? 0) <=> ($a['missing'] ?? 0));
if ($missing !== 0) {
return $missing;
}
return strcmp((string) ($a['feature'] ?? ''), (string) ($b['feature'] ?? ''));
});
return $values;
}
/**
* Copy payload semantics:
* - Always Missing-only
* - Always Type fixed by button (application vs delegated)
* - Respects Feature filter only
* - Ignores Search
*
* @param array<int, ManagedEnvironmentPermissionRow> $permissions
* @param 'application'|'delegated' $type
* @param array<int, string> $featureFilter
*/
public static function deriveCopyPayload(array $permissions, string $type, array $featureFilter = []): string
{
$featureFilter = array_values(array_unique(array_filter(array_map('strval', $featureFilter))));
$payload = collect($permissions)
->filter(function (array $row) use ($type, $featureFilter): bool {
if (($row['status'] ?? null) !== 'missing') {
return false;
}
if (($row['type'] ?? null) !== $type) {
return false;
}
if ($featureFilter === []) {
return true;
}
$rowFeatures = $row['features'] ?? [];
return count(array_intersect($featureFilter, $rowFeatures)) > 0;
})
->pluck('key')
->map(fn (mixed $key): string => (string) $key)
->filter()
->unique()
->sort()
->values()
->all();
return implode("\n", $payload);
}
/**
* @param array<int, ManagedEnvironmentPermissionRow> $permissions
* @return array<int, ManagedEnvironmentPermissionRow>
*/
public static function applyFilterState(array $permissions, array $state): array
{
$status = $state['status'] ?? 'missing';
$type = $state['type'] ?? 'all';
$features = $state['features'] ?? [];
$search = $state['search'] ?? '';
$search = is_string($search) ? trim($search) : '';
$searchLower = strtolower($search);
$features = array_values(array_unique(array_filter(array_map('strval', $features))));
$filtered = collect($permissions)
->filter(function (array $row) use ($status, $type, $features): bool {
$rowStatus = $row['status'] ?? null;
$rowType = $row['type'] ?? null;
if ($status === 'missing' && ! in_array($rowStatus, ['missing', 'blocked', 'expired', 'unknown'], true)) {
return false;
}
if ($status === 'granted' && $rowStatus !== 'granted') {
return false;
}
if ($type !== 'all' && $rowType !== $type) {
return false;
}
if ($features === []) {
return true;
}
$rowFeatures = $row['features'] ?? [];
return count(array_intersect($features, $rowFeatures)) > 0;
})
->when($searchLower !== '', function ($collection) use ($searchLower) {
return $collection->filter(function (array $row) use ($searchLower): bool {
$key = strtolower((string) ($row['key'] ?? ''));
$description = strtolower((string) ($row['description'] ?? ''));
return str_contains($key, $searchLower) || ($description !== '' && str_contains($description, $searchLower));
});
})
->values()
->all();
usort($filtered, static function (array $a, array $b): int {
$weight = static function (array $row): int {
return match ($row['status'] ?? null) {
'missing' => 0,
'blocked' => 1,
'expired' => 2,
'unknown' => 3,
'granted' => 4,
default => 5,
};
};
$cmp = $weight($a) <=> $weight($b);
if ($cmp !== 0) {
return $cmp;
}
return strcmp((string) ($a['key'] ?? ''), (string) ($b['key'] ?? ''));
});
return $filtered;
}
/**
* @param array<string, mixed> $filters
* @return FilterState
*/
public static function normalizeFilterState(array $filters): array
{
$status = (string) ($filters['status'] ?? 'missing');
$type = (string) ($filters['type'] ?? 'all');
$features = $filters['features'] ?? [];
$search = (string) ($filters['search'] ?? '');
if (! in_array($status, ['missing', 'granted', 'all'], true)) {
$status = 'missing';
}
if (! in_array($type, ['application', 'delegated', 'all'], true)) {
$type = 'all';
}
if (! is_array($features)) {
$features = [];
}
$features = array_values(array_unique(array_filter(array_map('strval', $features))));
return [
'status' => $status,
'type' => $type,
'features' => $features,
'search' => $search,
];
}
/**
* @param array<string, mixed> $row
* @return ManagedEnvironmentPermissionRow
*/
private static function normalizePermissionRow(array $row): array
{
$key = (string) ($row['key'] ?? '');
$type = (string) ($row['type'] ?? 'application');
$description = $row['description'] ?? null;
$features = $row['features'] ?? [];
$status = (string) ($row['status'] ?? 'missing');
$details = $row['details'] ?? null;
if (! in_array($type, ['application', 'delegated'], true)) {
$type = 'application';
}
if (! is_string($description) || $description === '') {
$description = null;
}
if (! is_array($features)) {
$features = [];
}
$features = array_values(array_unique(array_filter(array_map('strval', $features))));
if ($status === 'error') {
$status = 'unknown';
}
if (! in_array($status, ['granted', 'missing', 'blocked', 'expired', 'unknown', 'not_applicable'], true)) {
$status = 'missing';
}
if (! is_array($details)) {
$details = null;
}
return [
'key' => $key,
'type' => $type,
'description' => $description,
'features' => $features,
'status' => $status,
'details' => $details,
];
}
private static function parseLastRefreshedAt(mixed $value): ?Carbon
{
if ($value instanceof Carbon) {
return $value;
}
if ($value instanceof CarbonInterface) {
return Carbon::instance($value);
}
if (is_string($value) && $value !== '') {
try {
return Carbon::parse($value);
} catch (\Throwable) {
return null;
}
}
return null;
}
/**
* @return array<string, mixed>
*/
private function readinessComparison(ManagedEnvironment $tenant): array
{
try {
$actor = auth()->user();
$actor = $actor instanceof User ? $actor : null;
$readiness = app(ProviderReadinessResolver::class)
->forEnvironment($tenant, $actor)
->toArray();
return [
'permissions' => is_array($readiness['permission_rows'] ?? null) ? $readiness['permission_rows'] : [],
'last_refreshed_at' => data_get($readiness, 'freshness.last_refreshed_at'),
'freshness' => is_array($readiness['freshness'] ?? null) ? $readiness['freshness'] : [],
'counts' => is_array($readiness['counts'] ?? null) ? $readiness['counts'] : [],
'readiness' => $readiness,
];
} catch (\Throwable) {
return [
'permissions' => $this->unknownRequiredPermissionRows(),
'last_refreshed_at' => null,
'freshness' => [
'last_refreshed_at' => null,
'is_stale' => true,
],
'counts' => [],
'readiness' => [],
];
}
}
/**
* @return array<int, array{key:string,type:string,description:?string,features:array<int,string>,status:string,details:null}>
*/
private function unknownRequiredPermissionRows(): array
{
return collect($this->permissionService->getRequiredPermissions())
->filter(static fn (mixed $permission): bool => is_array($permission) && filled($permission['key'] ?? null))
->map(static fn (array $permission): array => [
'key' => (string) $permission['key'],
'type' => in_array(($permission['type'] ?? null), ['application', 'delegated'], true)
? (string) $permission['type']
: 'application',
'description' => is_string($permission['description'] ?? null) ? (string) $permission['description'] : null,
'features' => is_array($permission['features'] ?? null) ? array_values($permission['features']) : [],
'status' => 'unknown',
'details' => null,
])
->values()
->all();
}
}