Summary add appProtectionPolicy coverage for assignments, normalize settings for UI, and skip targetedManagedAppConfiguration noise during inventory wire up derived Graph endpoints/contracts so restores use the correct /assign paths per platform and assignments no longer rely on unsupported $expand add normalization logic/tests plus Pact/Plan updates so capture+restore behave more like Intune’s app protection workflows and no longer expose unsupported fields Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local> Reviewed-on: #11
328 lines
9.9 KiB
PHP
328 lines
9.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Graph;
|
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class AssignmentFetcher
|
|
{
|
|
public function __construct(
|
|
private readonly MicrosoftGraphClient $graphClient,
|
|
private readonly GraphContractRegistry $contracts,
|
|
) {}
|
|
|
|
/**
|
|
* Fetch policy assignments with fallback strategy.
|
|
*
|
|
* Primary: GET {assignments_list_path}
|
|
* Fallback: GET {resource}?$expand=assignments&$filter=id eq '{id}'
|
|
*
|
|
* @return array Returns assignment array or empty array on failure
|
|
*/
|
|
public function fetch(
|
|
string $policyType,
|
|
string $tenantId,
|
|
string $policyId,
|
|
array $options = [],
|
|
bool $throwOnFailure = false,
|
|
?string $policyOdataType = null,
|
|
): array {
|
|
$contract = $this->contracts->get($policyType);
|
|
$listPathTemplate = $contract['assignments_list_path'] ?? null;
|
|
$resource = $contract['resource'] ?? null;
|
|
$requestOptions = array_merge($options, ['tenant' => $tenantId]);
|
|
$context = [
|
|
'tenant_id' => $tenantId,
|
|
'policy_type' => $policyType,
|
|
'policy_id' => $policyId,
|
|
];
|
|
|
|
$primaryException = null;
|
|
$assignments = [];
|
|
$primarySucceeded = false;
|
|
|
|
// Try primary endpoint(s)
|
|
$listPathTemplates = [];
|
|
|
|
if ($policyType === 'appProtectionPolicy') {
|
|
$derivedTemplate = $this->resolveAppProtectionAssignmentsListTemplate($policyOdataType);
|
|
|
|
if ($derivedTemplate !== null) {
|
|
$listPathTemplates[] = $derivedTemplate;
|
|
}
|
|
}
|
|
|
|
if (is_string($listPathTemplate) && $listPathTemplate !== '' && ! in_array($listPathTemplate, $listPathTemplates, true)) {
|
|
$listPathTemplates[] = $listPathTemplate;
|
|
}
|
|
|
|
foreach ($listPathTemplates as $template) {
|
|
try {
|
|
$assignments = $this->fetchPrimary(
|
|
$template,
|
|
$policyId,
|
|
$requestOptions,
|
|
$context,
|
|
$throwOnFailure
|
|
);
|
|
$primarySucceeded = true;
|
|
|
|
if (! empty($assignments)) {
|
|
Log::debug('Fetched assignments via primary endpoint', [
|
|
'tenant_id' => $tenantId,
|
|
'policy_type' => $policyType,
|
|
'policy_id' => $policyId,
|
|
'count' => count($assignments),
|
|
]);
|
|
|
|
return $assignments;
|
|
}
|
|
} catch (GraphException $e) {
|
|
$primaryException = $primaryException ?? $e;
|
|
}
|
|
}
|
|
|
|
if ($primarySucceeded && $policyType === 'appProtectionPolicy') {
|
|
Log::debug('Assignments fetched via primary endpoint(s)', [
|
|
'tenant_id' => $tenantId,
|
|
'policy_type' => $policyType,
|
|
'policy_id' => $policyId,
|
|
'count' => count($assignments),
|
|
]);
|
|
|
|
return $assignments;
|
|
}
|
|
|
|
// Try fallback with $expand
|
|
Log::debug('Primary endpoint returned empty, trying fallback', [
|
|
'tenant_id' => $tenantId,
|
|
'policy_type' => $policyType,
|
|
'policy_id' => $policyId,
|
|
]);
|
|
|
|
if (! is_string($resource) || $resource === '') {
|
|
Log::debug('Assignments resource not configured for policy type', [
|
|
'tenant_id' => $tenantId,
|
|
'policy_type' => $policyType,
|
|
'policy_id' => $policyId,
|
|
]);
|
|
|
|
if ($throwOnFailure && $primaryException) {
|
|
Log::warning('Failed to fetch assignments', [
|
|
'tenant_id' => $tenantId,
|
|
'policy_type' => $policyType,
|
|
'policy_id' => $policyId,
|
|
'error' => $primaryException->getMessage(),
|
|
'context' => $primaryException->context,
|
|
]);
|
|
|
|
throw $primaryException;
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
if ($policyType === 'appProtectionPolicy') {
|
|
if ($throwOnFailure && $primaryException) {
|
|
throw $primaryException;
|
|
}
|
|
|
|
return $assignments;
|
|
}
|
|
|
|
$fallbackException = null;
|
|
|
|
try {
|
|
$assignments = $this->fetchWithExpand(
|
|
$resource,
|
|
$policyId,
|
|
$requestOptions,
|
|
$context,
|
|
$throwOnFailure
|
|
);
|
|
} catch (GraphException $e) {
|
|
$fallbackException = $e;
|
|
}
|
|
|
|
if (! empty($assignments)) {
|
|
Log::debug('Fetched assignments via fallback endpoint', [
|
|
'tenant_id' => $tenantId,
|
|
'policy_type' => $policyType,
|
|
'policy_id' => $policyId,
|
|
'count' => count($assignments),
|
|
]);
|
|
|
|
return $assignments;
|
|
}
|
|
|
|
// Both methods returned empty
|
|
Log::debug('No assignments found for policy', [
|
|
'tenant_id' => $tenantId,
|
|
'policy_type' => $policyType,
|
|
'policy_id' => $policyId,
|
|
]);
|
|
|
|
if ($throwOnFailure && ($fallbackException || $primaryException)) {
|
|
$exception = $fallbackException ?? $primaryException;
|
|
|
|
Log::warning('Failed to fetch assignments', [
|
|
'tenant_id' => $tenantId,
|
|
'policy_type' => $policyType,
|
|
'policy_id' => $policyId,
|
|
'error' => $exception->getMessage(),
|
|
'context' => $exception->context,
|
|
]);
|
|
|
|
throw $exception;
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
private function resolveAppProtectionAssignmentsListTemplate(?string $odataType): ?string
|
|
{
|
|
$entitySet = $this->resolveAppProtectionEntitySet($odataType);
|
|
|
|
if ($entitySet === null) {
|
|
return null;
|
|
}
|
|
|
|
return "/deviceAppManagement/{$entitySet}/{id}/assignments";
|
|
}
|
|
|
|
private function resolveAppProtectionEntitySet(?string $odataType): ?string
|
|
{
|
|
if (! is_string($odataType) || $odataType === '') {
|
|
return null;
|
|
}
|
|
|
|
return match (strtolower($odataType)) {
|
|
'#microsoft.graph.androidmanagedappprotection' => 'androidManagedAppProtections',
|
|
'#microsoft.graph.iosmanagedappprotection' => 'iosManagedAppProtections',
|
|
'#microsoft.graph.windowsinformationprotectionpolicy' => 'windowsInformationProtectionPolicies',
|
|
'#microsoft.graph.mdmwindowsinformationprotectionpolicy' => 'mdmWindowsInformationProtectionPolicies',
|
|
'#microsoft.graph.targetedmanagedappprotection' => 'targetedManagedAppProtections',
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Fetch assignments using primary endpoint.
|
|
*/
|
|
private function fetchPrimary(
|
|
?string $listPathTemplate,
|
|
string $policyId,
|
|
array $options,
|
|
array $context,
|
|
bool $throwOnFailure
|
|
): array {
|
|
if (! is_string($listPathTemplate) || $listPathTemplate === '') {
|
|
return [];
|
|
}
|
|
|
|
$path = $this->resolvePath($listPathTemplate, $policyId);
|
|
|
|
if ($path === null) {
|
|
return [];
|
|
}
|
|
|
|
$response = $this->graphClient->request('GET', $path, $options);
|
|
|
|
if ($response->failed()) {
|
|
$this->logAssignmentFailure('primary', $response, $context + ['path' => $path]);
|
|
|
|
if ($throwOnFailure) {
|
|
throw new GraphException(
|
|
$this->resolveErrorMessage($response),
|
|
$response->status,
|
|
$context + ['path' => $path]
|
|
);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
return $response->data['value'] ?? [];
|
|
}
|
|
|
|
/**
|
|
* Fetch assignments using $expand fallback.
|
|
*/
|
|
private function fetchWithExpand(
|
|
string $resource,
|
|
string $policyId,
|
|
array $options,
|
|
array $context,
|
|
bool $throwOnFailure
|
|
): array {
|
|
$path = $resource;
|
|
$params = [
|
|
'$expand' => 'assignments',
|
|
'$filter' => "id eq '{$policyId}'",
|
|
];
|
|
|
|
$response = $this->graphClient->request('GET', $path, array_merge($options, [
|
|
'query' => $params,
|
|
]));
|
|
|
|
if ($response->failed()) {
|
|
$this->logAssignmentFailure('fallback', $response, $context + ['path' => $path]);
|
|
|
|
if ($throwOnFailure) {
|
|
throw new GraphException(
|
|
$this->resolveErrorMessage($response),
|
|
$response->status,
|
|
$context + ['path' => $path]
|
|
);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
$policies = $response->data['value'] ?? [];
|
|
|
|
if (empty($policies)) {
|
|
return [];
|
|
}
|
|
|
|
return $policies[0]['assignments'] ?? [];
|
|
}
|
|
|
|
private function resolvePath(string $template, string $policyId): ?string
|
|
{
|
|
if ($template === '') {
|
|
return null;
|
|
}
|
|
|
|
return str_replace('{id}', urlencode($policyId), $template);
|
|
}
|
|
|
|
private function resolveErrorMessage(GraphResponse $response): string
|
|
{
|
|
$error = $response->errors[0] ?? null;
|
|
|
|
if (is_array($error)) {
|
|
if (isset($error['message']) && is_string($error['message'])) {
|
|
return $error['message'];
|
|
}
|
|
|
|
return json_encode($error, JSON_UNESCAPED_SLASHES) ?: 'Graph request failed';
|
|
}
|
|
|
|
if (is_string($error) && $error !== '') {
|
|
return $error;
|
|
}
|
|
|
|
return 'Graph request failed';
|
|
}
|
|
|
|
private function logAssignmentFailure(string $stage, GraphResponse $response, array $context): void
|
|
{
|
|
Log::warning('Assignment fetch failed', $context + [
|
|
'stage' => $stage,
|
|
'status' => $response->status,
|
|
'errors' => $response->errors,
|
|
]);
|
|
}
|
|
}
|