feat/004-assignments-scope-tags #4
@ -54,8 +54,9 @@ public function enrichWithAssignments(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch assignments from Graph API
|
// Fetch assignments from Graph API
|
||||||
$tenantId = $tenant->external_id ?? $tenant->tenant_id;
|
$graphOptions = $tenant->graphOptions();
|
||||||
$assignments = $this->assignmentFetcher->fetch($tenantId, $policyId);
|
$tenantId = $graphOptions['tenant'] ?? $tenant->external_id ?? $tenant->tenant_id;
|
||||||
|
$assignments = $this->assignmentFetcher->fetch($tenantId, $policyId, $graphOptions);
|
||||||
|
|
||||||
if (empty($assignments)) {
|
if (empty($assignments)) {
|
||||||
// No assignments or fetch failed
|
// No assignments or fetch failed
|
||||||
@ -82,7 +83,7 @@ public function enrichWithAssignments(
|
|||||||
$hasOrphanedGroups = false;
|
$hasOrphanedGroups = false;
|
||||||
|
|
||||||
if (! empty($groupIds)) {
|
if (! empty($groupIds)) {
|
||||||
$resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantId);
|
$resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantId, $graphOptions);
|
||||||
$hasOrphanedGroups = collect($resolvedGroups)->contains('orphaned', true);
|
$hasOrphanedGroups = collect($resolvedGroups)->contains('orphaned', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,11 +18,13 @@ public function __construct(
|
|||||||
*
|
*
|
||||||
* @return array Returns assignment array or empty array on failure
|
* @return array Returns assignment array or empty array on failure
|
||||||
*/
|
*/
|
||||||
public function fetch(string $tenantId, string $policyId): array
|
public function fetch(string $tenantId, string $policyId, array $options = []): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
$requestOptions = array_merge($options, ['tenant' => $tenantId]);
|
||||||
|
|
||||||
// Try primary endpoint
|
// Try primary endpoint
|
||||||
$assignments = $this->fetchPrimary($tenantId, $policyId);
|
$assignments = $this->fetchPrimary($policyId, $requestOptions);
|
||||||
|
|
||||||
if (! empty($assignments)) {
|
if (! empty($assignments)) {
|
||||||
Log::debug('Fetched assignments via primary endpoint', [
|
Log::debug('Fetched assignments via primary endpoint', [
|
||||||
@ -40,7 +42,7 @@ public function fetch(string $tenantId, string $policyId): array
|
|||||||
'policy_id' => $policyId,
|
'policy_id' => $policyId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$assignments = $this->fetchWithExpand($tenantId, $policyId);
|
$assignments = $this->fetchWithExpand($policyId, $requestOptions);
|
||||||
|
|
||||||
if (! empty($assignments)) {
|
if (! empty($assignments)) {
|
||||||
Log::debug('Fetched assignments via fallback endpoint', [
|
Log::debug('Fetched assignments via fallback endpoint', [
|
||||||
@ -74,13 +76,11 @@ public function fetch(string $tenantId, string $policyId): array
|
|||||||
/**
|
/**
|
||||||
* Fetch assignments using primary endpoint.
|
* Fetch assignments using primary endpoint.
|
||||||
*/
|
*/
|
||||||
private function fetchPrimary(string $tenantId, string $policyId): array
|
private function fetchPrimary(string $policyId, array $options): array
|
||||||
{
|
{
|
||||||
$path = "/deviceManagement/configurationPolicies/{$policyId}/assignments";
|
$path = "/deviceManagement/configurationPolicies/{$policyId}/assignments";
|
||||||
|
|
||||||
$response = $this->graphClient->request('GET', $path, [
|
$response = $this->graphClient->request('GET', $path, $options);
|
||||||
'tenant' => $tenantId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $response->data['value'] ?? [];
|
return $response->data['value'] ?? [];
|
||||||
}
|
}
|
||||||
@ -88,7 +88,7 @@ private function fetchPrimary(string $tenantId, string $policyId): array
|
|||||||
/**
|
/**
|
||||||
* Fetch assignments using $expand fallback.
|
* Fetch assignments using $expand fallback.
|
||||||
*/
|
*/
|
||||||
private function fetchWithExpand(string $tenantId, string $policyId): array
|
private function fetchWithExpand(string $policyId, array $options): array
|
||||||
{
|
{
|
||||||
$path = '/deviceManagement/configurationPolicies';
|
$path = '/deviceManagement/configurationPolicies';
|
||||||
$params = [
|
$params = [
|
||||||
@ -96,10 +96,9 @@ private function fetchWithExpand(string $tenantId, string $policyId): array
|
|||||||
'$filter' => "id eq '{$policyId}'",
|
'$filter' => "id eq '{$policyId}'",
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = $this->graphClient->request('GET', $path, [
|
$response = $this->graphClient->request('GET', $path, array_merge($options, [
|
||||||
'tenant' => $tenantId,
|
|
||||||
'query' => $params,
|
'query' => $params,
|
||||||
]);
|
]));
|
||||||
|
|
||||||
$policies = $response->data['value'] ?? [];
|
$policies = $response->data['value'] ?? [];
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@ public function __construct(
|
|||||||
* @param string $tenantId Target tenant ID
|
* @param string $tenantId Target tenant ID
|
||||||
* @return array Keyed array: ['group-id' => ['id' => ..., 'displayName' => ..., 'orphaned' => bool]]
|
* @return array Keyed array: ['group-id' => ['id' => ..., 'displayName' => ..., 'orphaned' => bool]]
|
||||||
*/
|
*/
|
||||||
public function resolveGroupIds(array $groupIds, string $tenantId): array
|
public function resolveGroupIds(array $groupIds, string $tenantId, array $options = []): array
|
||||||
{
|
{
|
||||||
if (empty($groupIds)) {
|
if (empty($groupIds)) {
|
||||||
return [];
|
return [];
|
||||||
@ -30,27 +30,27 @@ public function resolveGroupIds(array $groupIds, string $tenantId): array
|
|||||||
// Create cache key
|
// Create cache key
|
||||||
$cacheKey = $this->getCacheKey($groupIds, $tenantId);
|
$cacheKey = $this->getCacheKey($groupIds, $tenantId);
|
||||||
|
|
||||||
return Cache::remember($cacheKey, 300, function () use ($groupIds, $tenantId) {
|
return Cache::remember($cacheKey, 300, function () use ($groupIds, $tenantId, $options) {
|
||||||
return $this->fetchAndResolveGroups($groupIds, $tenantId);
|
return $this->fetchAndResolveGroups($groupIds, $tenantId, $options);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch groups from Graph API and resolve orphaned IDs.
|
* Fetch groups from Graph API and resolve orphaned IDs.
|
||||||
*/
|
*/
|
||||||
private function fetchAndResolveGroups(array $groupIds, string $tenantId): array
|
private function fetchAndResolveGroups(array $groupIds, string $tenantId, array $options = []): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$response = $this->graphClient->request(
|
$response = $this->graphClient->request(
|
||||||
'POST',
|
'POST',
|
||||||
'/directoryObjects/getByIds',
|
'/directoryObjects/getByIds',
|
||||||
[
|
array_merge($options, [
|
||||||
'tenant' => $tenantId,
|
'tenant' => $tenantId,
|
||||||
'json' => [
|
'json' => [
|
||||||
'ids' => array_values($groupIds),
|
'ids' => array_values($groupIds),
|
||||||
'types' => ['group'],
|
'types' => ['group'],
|
||||||
],
|
],
|
||||||
]
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
$resolvedGroups = $response->data['value'] ?? [];
|
$resolvedGroups = $response->data['value'] ?? [];
|
||||||
|
|||||||
@ -7,11 +7,12 @@
|
|||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Services\Graph\AssignmentFetcher;
|
use App\Services\Graph\AssignmentFetcher;
|
||||||
use App\Services\Graph\GroupResolver;
|
use App\Services\Graph\GroupResolver;
|
||||||
|
use App\Services\Graph\ScopeTagResolver;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Orchestrates policy capture with assignments and scope tags.
|
* Orchestrates policy capture with assignments and scope tags.
|
||||||
*
|
*
|
||||||
* Ensures PolicyVersion is the source of truth, with BackupItem as restore copy.
|
* Ensures PolicyVersion is the source of truth, with BackupItem as restore copy.
|
||||||
*/
|
*/
|
||||||
class PolicyCaptureOrchestrator
|
class PolicyCaptureOrchestrator
|
||||||
@ -21,17 +22,12 @@ public function __construct(
|
|||||||
private readonly PolicySnapshotService $snapshotService,
|
private readonly PolicySnapshotService $snapshotService,
|
||||||
private readonly AssignmentFetcher $assignmentFetcher,
|
private readonly AssignmentFetcher $assignmentFetcher,
|
||||||
private readonly GroupResolver $groupResolver,
|
private readonly GroupResolver $groupResolver,
|
||||||
|
private readonly ScopeTagResolver $scopeTagResolver,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capture policy snapshot with optional assignments/scope tags.
|
* Capture policy snapshot with optional assignments/scope tags.
|
||||||
*
|
*
|
||||||
* @param Policy $policy
|
|
||||||
* @param Tenant $tenant
|
|
||||||
* @param bool $includeAssignments
|
|
||||||
* @param bool $includeScopeTags
|
|
||||||
* @param string|null $createdBy
|
|
||||||
* @param array $metadata
|
|
||||||
* @return array ['version' => PolicyVersion, 'captured' => array]
|
* @return array ['version' => PolicyVersion, 'captured' => array]
|
||||||
*/
|
*/
|
||||||
public function capture(
|
public function capture(
|
||||||
@ -42,6 +38,9 @@ public function capture(
|
|||||||
?string $createdBy = null,
|
?string $createdBy = null,
|
||||||
array $metadata = []
|
array $metadata = []
|
||||||
): array {
|
): array {
|
||||||
|
$graphOptions = $tenant->graphOptions();
|
||||||
|
$tenantIdentifier = $graphOptions['tenant'] ?? $tenant->graphTenantId() ?? (string) $tenant->getKey();
|
||||||
|
|
||||||
// 1. Fetch policy snapshot
|
// 1. Fetch policy snapshot
|
||||||
$snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy);
|
$snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy);
|
||||||
|
|
||||||
@ -57,9 +56,9 @@ public function capture(
|
|||||||
// 2. Fetch assignments if requested
|
// 2. Fetch assignments if requested
|
||||||
if ($includeAssignments) {
|
if ($includeAssignments) {
|
||||||
try {
|
try {
|
||||||
$rawAssignments = $this->assignmentFetcher->fetch($tenant->id, $policy->external_id);
|
$rawAssignments = $this->assignmentFetcher->fetch($tenantIdentifier, $policy->external_id, $graphOptions);
|
||||||
|
|
||||||
if (!empty($rawAssignments)) {
|
if (! empty($rawAssignments)) {
|
||||||
$assignments = $rawAssignments;
|
$assignments = $rawAssignments;
|
||||||
|
|
||||||
// Resolve groups for orphaned detection
|
// Resolve groups for orphaned detection
|
||||||
@ -70,9 +69,10 @@ public function capture(
|
|||||||
->values()
|
->values()
|
||||||
->toArray();
|
->toArray();
|
||||||
|
|
||||||
if (!empty($groupIds)) {
|
if (! empty($groupIds)) {
|
||||||
$resolvedGroups = $this->groupResolver->resolve($tenant->id, $groupIds);
|
$resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantIdentifier, $graphOptions);
|
||||||
$captureMetadata['has_orphaned_assignments'] = !empty($resolvedGroups['orphaned']);
|
$captureMetadata['has_orphaned_assignments'] = collect($resolvedGroups)
|
||||||
|
->contains(fn (array $group) => $group['orphaned'] ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
$captureMetadata['assignments_count'] = count($rawAssignments);
|
$captureMetadata['assignments_count'] = count($rawAssignments);
|
||||||
@ -80,7 +80,7 @@ public function capture(
|
|||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$captureMetadata['assignments_fetch_failed'] = true;
|
$captureMetadata['assignments_fetch_failed'] = true;
|
||||||
$captureMetadata['assignments_fetch_error'] = $e->getMessage();
|
$captureMetadata['assignments_fetch_error'] = $e->getMessage();
|
||||||
|
|
||||||
Log::warning('Failed to fetch assignments during capture', [
|
Log::warning('Failed to fetch assignments during capture', [
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
'policy_id' => $policy->id,
|
'policy_id' => $policy->id,
|
||||||
@ -91,15 +91,13 @@ public function capture(
|
|||||||
|
|
||||||
// 3. Fetch scope tags if requested
|
// 3. Fetch scope tags if requested
|
||||||
if ($includeScopeTags) {
|
if ($includeScopeTags) {
|
||||||
$scopeTags = [
|
$scopeTagIds = $payload['roleScopeTagIds'] ?? ['0'];
|
||||||
'ids' => $payload['roleScopeTagIds'] ?? ['0'],
|
$scopeTags = $this->resolveScopeTags($tenant, $scopeTagIds);
|
||||||
'names' => ['Default'], // Could fetch from Graph if needed
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Check if PolicyVersion with same snapshot already exists
|
// 4. Check if PolicyVersion with same snapshot already exists
|
||||||
$snapshotHash = hash('sha256', json_encode($payload));
|
$snapshotHash = hash('sha256', json_encode($payload));
|
||||||
|
|
||||||
// Find existing version by comparing snapshot content (database-agnostic)
|
// Find existing version by comparing snapshot content (database-agnostic)
|
||||||
$existingVersion = PolicyVersion::where('policy_id', $policy->id)
|
$existingVersion = PolicyVersion::where('policy_id', $policy->id)
|
||||||
->get()
|
->get()
|
||||||
@ -107,35 +105,42 @@ public function capture(
|
|||||||
return hash('sha256', json_encode($version->snapshot)) === $snapshotHash;
|
return hash('sha256', json_encode($version->snapshot)) === $snapshotHash;
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($existingVersion && $includeAssignments && is_null($existingVersion->assignments)) {
|
|
||||||
// Backfill existing version with assignments (idempotent)
|
|
||||||
$existingVersion->update([
|
|
||||||
'assignments' => $assignments,
|
|
||||||
'scope_tags' => $scopeTags,
|
|
||||||
'assignments_hash' => $assignments ? hash('sha256', json_encode($assignments)) : null,
|
|
||||||
'scope_tags_hash' => $scopeTags ? hash('sha256', json_encode($scopeTags)) : null,
|
|
||||||
]);
|
|
||||||
|
|
||||||
Log::info('Backfilled existing PolicyVersion with assignments', [
|
|
||||||
'tenant_id' => $tenant->id,
|
|
||||||
'policy_id' => $policy->id,
|
|
||||||
'version_id' => $existingVersion->id,
|
|
||||||
'version_number' => $existingVersion->version_number,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'version' => $existingVersion->fresh(),
|
|
||||||
'captured' => [
|
|
||||||
'payload' => $payload,
|
|
||||||
'assignments' => $assignments,
|
|
||||||
'scope_tags' => $scopeTags,
|
|
||||||
'metadata' => $captureMetadata,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($existingVersion) {
|
if ($existingVersion) {
|
||||||
// Reuse existing version without modification
|
$updates = [];
|
||||||
|
|
||||||
|
if ($includeAssignments && $existingVersion->assignments === null) {
|
||||||
|
$updates['assignments'] = $assignments;
|
||||||
|
$updates['assignments_hash'] = $assignments ? hash('sha256', json_encode($assignments)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($includeScopeTags && $existingVersion->scope_tags === null) {
|
||||||
|
$updates['scope_tags'] = $scopeTags;
|
||||||
|
$updates['scope_tags_hash'] = $scopeTags ? hash('sha256', json_encode($scopeTags)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($updates)) {
|
||||||
|
$existingVersion->update($updates);
|
||||||
|
|
||||||
|
Log::info('Backfilled existing PolicyVersion with capture data', [
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'policy_id' => $policy->id,
|
||||||
|
'version_id' => $existingVersion->id,
|
||||||
|
'version_number' => $existingVersion->version_number,
|
||||||
|
'assignments_backfilled' => array_key_exists('assignments', $updates),
|
||||||
|
'scope_tags_backfilled' => array_key_exists('scope_tags', $updates),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'version' => $existingVersion->fresh(),
|
||||||
|
'captured' => [
|
||||||
|
'payload' => $payload,
|
||||||
|
'assignments' => $assignments,
|
||||||
|
'scope_tags' => $scopeTags,
|
||||||
|
'metadata' => $captureMetadata,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
Log::info('Reusing existing PolicyVersion', [
|
Log::info('Reusing existing PolicyVersion', [
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
'policy_id' => $policy->id,
|
'policy_id' => $policy->id,
|
||||||
@ -175,8 +180,8 @@ public function capture(
|
|||||||
'policy_id' => $policy->id,
|
'policy_id' => $policy->id,
|
||||||
'version_id' => $version->id,
|
'version_id' => $version->id,
|
||||||
'version_number' => $version->version_number,
|
'version_number' => $version->version_number,
|
||||||
'has_assignments' => !is_null($assignments),
|
'has_assignments' => ! is_null($assignments),
|
||||||
'has_scope_tags' => !is_null($scopeTags),
|
'has_scope_tags' => ! is_null($scopeTags),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -192,13 +197,6 @@ public function capture(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure existing PolicyVersion has assignments if missing.
|
* Ensure existing PolicyVersion has assignments if missing.
|
||||||
*
|
|
||||||
* @param PolicyVersion $version
|
|
||||||
* @param Tenant $tenant
|
|
||||||
* @param Policy $policy
|
|
||||||
* @param bool $includeAssignments
|
|
||||||
* @param bool $includeScopeTags
|
|
||||||
* @return PolicyVersion
|
|
||||||
*/
|
*/
|
||||||
public function ensureVersionHasAssignments(
|
public function ensureVersionHasAssignments(
|
||||||
PolicyVersion $version,
|
PolicyVersion $version,
|
||||||
@ -207,17 +205,19 @@ public function ensureVersionHasAssignments(
|
|||||||
bool $includeAssignments = false,
|
bool $includeAssignments = false,
|
||||||
bool $includeScopeTags = false
|
bool $includeScopeTags = false
|
||||||
): PolicyVersion {
|
): PolicyVersion {
|
||||||
// If version already has assignments, don't overwrite (idempotent)
|
$graphOptions = $tenant->graphOptions();
|
||||||
if ($version->assignments !== null) {
|
$tenantIdentifier = $graphOptions['tenant'] ?? $tenant->graphTenantId() ?? (string) $tenant->getKey();
|
||||||
|
|
||||||
|
if ($version->assignments !== null && $version->scope_tags !== null) {
|
||||||
Log::debug('Version already has assignments, skipping', [
|
Log::debug('Version already has assignments, skipping', [
|
||||||
'version_id' => $version->id,
|
'version_id' => $version->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $version;
|
return $version;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only fetch if requested
|
// Only fetch if requested
|
||||||
if (!$includeAssignments && !$includeScopeTags) {
|
if (! $includeAssignments && ! $includeScopeTags) {
|
||||||
return $version;
|
return $version;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,12 +225,11 @@ public function ensureVersionHasAssignments(
|
|||||||
$scopeTags = null;
|
$scopeTags = null;
|
||||||
$metadata = $version->metadata ?? [];
|
$metadata = $version->metadata ?? [];
|
||||||
|
|
||||||
// Fetch assignments
|
if ($includeAssignments && $version->assignments === null) {
|
||||||
if ($includeAssignments) {
|
|
||||||
try {
|
try {
|
||||||
$rawAssignments = $this->assignmentFetcher->fetch($tenant->id, $policy->external_id);
|
$rawAssignments = $this->assignmentFetcher->fetch($tenantIdentifier, $policy->external_id, $graphOptions);
|
||||||
|
|
||||||
if (!empty($rawAssignments)) {
|
if (! empty($rawAssignments)) {
|
||||||
$assignments = $rawAssignments;
|
$assignments = $rawAssignments;
|
||||||
|
|
||||||
// Resolve groups
|
// Resolve groups
|
||||||
@ -241,9 +240,10 @@ public function ensureVersionHasAssignments(
|
|||||||
->values()
|
->values()
|
||||||
->toArray();
|
->toArray();
|
||||||
|
|
||||||
if (!empty($groupIds)) {
|
if (! empty($groupIds)) {
|
||||||
$resolvedGroups = $this->groupResolver->resolve($tenant->id, $groupIds);
|
$resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantIdentifier, $graphOptions);
|
||||||
$metadata['has_orphaned_assignments'] = !empty($resolvedGroups['orphaned']);
|
$metadata['has_orphaned_assignments'] = collect($resolvedGroups)
|
||||||
|
->contains(fn (array $group) => $group['orphaned'] ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
$metadata['assignments_count'] = count($rawAssignments);
|
$metadata['assignments_count'] = count($rawAssignments);
|
||||||
@ -251,7 +251,7 @@ public function ensureVersionHasAssignments(
|
|||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$metadata['assignments_fetch_failed'] = true;
|
$metadata['assignments_fetch_failed'] = true;
|
||||||
$metadata['assignments_fetch_error'] = $e->getMessage();
|
$metadata['assignments_fetch_error'] = $e->getMessage();
|
||||||
|
|
||||||
Log::warning('Failed to backfill assignments for version', [
|
Log::warning('Failed to backfill assignments for version', [
|
||||||
'version_id' => $version->id,
|
'version_id' => $version->id,
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
@ -261,28 +261,53 @@ public function ensureVersionHasAssignments(
|
|||||||
|
|
||||||
// Fetch scope tags
|
// Fetch scope tags
|
||||||
if ($includeScopeTags && $version->scope_tags === null) {
|
if ($includeScopeTags && $version->scope_tags === null) {
|
||||||
// Try to get from snapshot
|
$scopeTagIds = $version->snapshot['roleScopeTagIds'] ?? ['0'];
|
||||||
$scopeTags = [
|
$scopeTags = $this->resolveScopeTags($tenant, $scopeTagIds);
|
||||||
'ids' => $version->snapshot['roleScopeTagIds'] ?? ['0'],
|
|
||||||
'names' => ['Default'],
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update version
|
$updates = [];
|
||||||
$version->update([
|
|
||||||
'assignments' => $assignments,
|
|
||||||
'scope_tags' => $scopeTags,
|
|
||||||
'assignments_hash' => $assignments ? hash('sha256', json_encode($assignments)) : null,
|
|
||||||
'scope_tags_hash' => $scopeTags ? hash('sha256', json_encode($scopeTags)) : null,
|
|
||||||
'metadata' => $metadata,
|
|
||||||
]);
|
|
||||||
|
|
||||||
Log::info('Version backfilled with assignments', [
|
if ($includeAssignments && $version->assignments === null) {
|
||||||
|
$updates['assignments'] = $assignments;
|
||||||
|
$updates['assignments_hash'] = $assignments ? hash('sha256', json_encode($assignments)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($includeScopeTags && $version->scope_tags === null) {
|
||||||
|
$updates['scope_tags'] = $scopeTags;
|
||||||
|
$updates['scope_tags_hash'] = $scopeTags ? hash('sha256', json_encode($scopeTags)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($updates)) {
|
||||||
|
$updates['metadata'] = $metadata;
|
||||||
|
$version->update($updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info('Version backfilled with capture data', [
|
||||||
'version_id' => $version->id,
|
'version_id' => $version->id,
|
||||||
'has_assignments' => !is_null($assignments),
|
'has_assignments' => ! is_null($assignments),
|
||||||
'has_scope_tags' => !is_null($scopeTags),
|
'has_scope_tags' => ! is_null($scopeTags),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $version->refresh();
|
return $version->refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $scopeTagIds
|
||||||
|
* @return array{ids:array<int, string>,names:array<int, string>}
|
||||||
|
*/
|
||||||
|
private function resolveScopeTags(Tenant $tenant, array $scopeTagIds): array
|
||||||
|
{
|
||||||
|
$scopeTags = $this->scopeTagResolver->resolve($scopeTagIds, $tenant);
|
||||||
|
|
||||||
|
$names = [];
|
||||||
|
foreach ($scopeTagIds as $id) {
|
||||||
|
$scopeTag = collect($scopeTags)->firstWhere('id', $id);
|
||||||
|
$names[] = $scopeTag['displayName'] ?? ($id === '0' ? 'Default' : "Unknown (ID: {$id})");
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ids' => $scopeTagIds,
|
||||||
|
'names' => $names,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Services\Graph\AssignmentFetcher;
|
use App\Services\Graph\AssignmentFetcher;
|
||||||
use App\Services\Graph\GroupResolver;
|
use App\Services\Graph\GroupResolver;
|
||||||
|
use App\Services\Graph\ScopeTagResolver;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
|
|
||||||
class VersionService
|
class VersionService
|
||||||
@ -16,6 +17,7 @@ public function __construct(
|
|||||||
private readonly PolicySnapshotService $snapshotService,
|
private readonly PolicySnapshotService $snapshotService,
|
||||||
private readonly AssignmentFetcher $assignmentFetcher,
|
private readonly AssignmentFetcher $assignmentFetcher,
|
||||||
private readonly GroupResolver $groupResolver,
|
private readonly GroupResolver $groupResolver,
|
||||||
|
private readonly ScopeTagResolver $scopeTagResolver,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function captureVersion(
|
public function captureVersion(
|
||||||
@ -69,6 +71,9 @@ public function captureFromGraph(
|
|||||||
bool $includeAssignments = true,
|
bool $includeAssignments = true,
|
||||||
bool $includeScopeTags = true,
|
bool $includeScopeTags = true,
|
||||||
): PolicyVersion {
|
): PolicyVersion {
|
||||||
|
$graphOptions = $tenant->graphOptions();
|
||||||
|
$tenantIdentifier = $graphOptions['tenant'] ?? $tenant->graphTenantId() ?? (string) $tenant->getKey();
|
||||||
|
|
||||||
$snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy);
|
$snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy);
|
||||||
|
|
||||||
if (isset($snapshot['failure'])) {
|
if (isset($snapshot['failure'])) {
|
||||||
@ -84,7 +89,7 @@ public function captureFromGraph(
|
|||||||
|
|
||||||
if ($includeAssignments) {
|
if ($includeAssignments) {
|
||||||
try {
|
try {
|
||||||
$rawAssignments = $this->assignmentFetcher->fetch($tenant->id, $policy->external_id);
|
$rawAssignments = $this->assignmentFetcher->fetch($tenantIdentifier, $policy->external_id, $graphOptions);
|
||||||
|
|
||||||
if (! empty($rawAssignments)) {
|
if (! empty($rawAssignments)) {
|
||||||
$assignments = $rawAssignments;
|
$assignments = $rawAssignments;
|
||||||
@ -97,9 +102,10 @@ public function captureFromGraph(
|
|||||||
->values()
|
->values()
|
||||||
->toArray();
|
->toArray();
|
||||||
|
|
||||||
$resolvedGroups = $this->groupResolver->resolve($tenant->id, $groupIds);
|
$resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantIdentifier, $graphOptions);
|
||||||
|
|
||||||
$assignmentMetadata['has_orphaned_assignments'] = ! empty($resolvedGroups['orphaned']);
|
$assignmentMetadata['has_orphaned_assignments'] = collect($resolvedGroups)
|
||||||
|
->contains(fn (array $group) => $group['orphaned'] ?? false);
|
||||||
$assignmentMetadata['assignments_count'] = count($rawAssignments);
|
$assignmentMetadata['assignments_count'] = count($rawAssignments);
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
@ -109,10 +115,8 @@ public function captureFromGraph(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($includeScopeTags) {
|
if ($includeScopeTags) {
|
||||||
$scopeTags = [
|
$scopeTagIds = $payload['roleScopeTagIds'] ?? ['0'];
|
||||||
'ids' => $payload['roleScopeTagIds'] ?? ['0'],
|
$scopeTags = $this->resolveScopeTags($tenant, $scopeTagIds);
|
||||||
'names' => ['Default'], // Could be fetched from Graph if needed
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$metadata = array_merge(
|
$metadata = array_merge(
|
||||||
@ -131,6 +135,26 @@ public function captureFromGraph(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $scopeTagIds
|
||||||
|
* @return array{ids:array<int, string>,names:array<int, string>}
|
||||||
|
*/
|
||||||
|
private function resolveScopeTags(Tenant $tenant, array $scopeTagIds): array
|
||||||
|
{
|
||||||
|
$scopeTags = $this->scopeTagResolver->resolve($scopeTagIds, $tenant);
|
||||||
|
|
||||||
|
$names = [];
|
||||||
|
foreach ($scopeTagIds as $id) {
|
||||||
|
$scopeTag = collect($scopeTags)->firstWhere('id', $id);
|
||||||
|
$names[] = $scopeTag['displayName'] ?? ($id === '0' ? 'Default' : "Unknown (ID: {$id})");
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ids' => $scopeTagIds,
|
||||||
|
'names' => $names,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function nextVersionNumber(Policy $policy): int
|
private function nextVersionNumber(Policy $policy): int
|
||||||
{
|
{
|
||||||
$current = PolicyVersion::query()
|
$current = PolicyVersion::query()
|
||||||
|
|||||||
@ -5,9 +5,8 @@
|
|||||||
use App\Models\PolicyVersion;
|
use App\Models\PolicyVersion;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Services\Graph\AssignmentFetcher;
|
use App\Services\Graph\AssignmentFetcher;
|
||||||
use App\Services\Graph\MicrosoftGraphClient;
|
use App\Services\Graph\GroupResolver;
|
||||||
use App\Services\Intune\BackupService;
|
use App\Services\Intune\BackupService;
|
||||||
use App\Services\Intune\GroupResolver;
|
|
||||||
use App\Services\Intune\PolicySnapshotService;
|
use App\Services\Intune\PolicySnapshotService;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Mockery\MockInterface;
|
use Mockery\MockInterface;
|
||||||
@ -16,7 +15,7 @@
|
|||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
$this->tenant = Tenant::factory()->create(['status' => 'active']);
|
$this->tenant = Tenant::factory()->create(['status' => 'active']);
|
||||||
|
|
||||||
$this->policy = Policy::factory()->create([
|
$this->policy = Policy::factory()->create([
|
||||||
'tenant_id' => $this->tenant->id,
|
'tenant_id' => $this->tenant->id,
|
||||||
'external_id' => 'test-policy-123',
|
'external_id' => 'test-policy-123',
|
||||||
@ -86,10 +85,13 @@
|
|||||||
|
|
||||||
// Mock GroupResolver
|
// Mock GroupResolver
|
||||||
$this->mock(GroupResolver::class, function (MockInterface $mock) {
|
$this->mock(GroupResolver::class, function (MockInterface $mock) {
|
||||||
$mock->shouldReceive('resolve')
|
$mock->shouldReceive('resolveGroupIds')
|
||||||
->andReturn([
|
->andReturn([
|
||||||
'resolved' => ['group-123' => 'Test Group'],
|
'group-123' => [
|
||||||
'orphaned' => [],
|
'id' => 'group-123',
|
||||||
|
'displayName' => 'Test Group',
|
||||||
|
'orphaned' => false,
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -189,7 +191,7 @@
|
|||||||
expect($backupSet->items)->toHaveCount(1);
|
expect($backupSet->items)->toHaveCount(1);
|
||||||
|
|
||||||
$backupItem = $backupSet->items->first();
|
$backupItem = $backupSet->items->first();
|
||||||
|
|
||||||
// BackupItem should have assignments
|
// BackupItem should have assignments
|
||||||
expect($backupItem->assignments)->not->toBeNull();
|
expect($backupItem->assignments)->not->toBeNull();
|
||||||
expect($backupItem->assignments)->toHaveCount(2);
|
expect($backupItem->assignments)->toHaveCount(2);
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Graph\AssignmentFetcher;
|
use App\Services\Graph\AssignmentFetcher;
|
||||||
|
use App\Services\Graph\ScopeTagResolver;
|
||||||
use App\Services\Intune\PolicySnapshotService;
|
use App\Services\Intune\PolicySnapshotService;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
@ -38,6 +39,14 @@
|
|||||||
$mock->shouldReceive('fetch')->never();
|
$mock->shouldReceive('fetch')->never();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$this->mock(ScopeTagResolver::class, function (MockInterface $mock) {
|
||||||
|
$mock->shouldReceive('resolve')
|
||||||
|
->once()
|
||||||
|
->andReturn([
|
||||||
|
['id' => '0', 'displayName' => 'Default'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
Livewire::test(ViewPolicy::class, ['record' => $policy->getRouteKey()])
|
Livewire::test(ViewPolicy::class, ['record' => $policy->getRouteKey()])
|
||||||
->callAction('capture_snapshot', data: [
|
->callAction('capture_snapshot', data: [
|
||||||
'include_assignments' => false,
|
'include_assignments' => false,
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Services\Graph\AssignmentFetcher;
|
use App\Services\Graph\AssignmentFetcher;
|
||||||
use App\Services\Graph\GroupResolver;
|
use App\Services\Graph\GroupResolver;
|
||||||
|
use App\Services\Graph\ScopeTagResolver;
|
||||||
use App\Services\Intune\PolicySnapshotService;
|
use App\Services\Intune\PolicySnapshotService;
|
||||||
use App\Services\Intune\VersionService;
|
use App\Services\Intune\VersionService;
|
||||||
|
|
||||||
@ -13,6 +14,13 @@
|
|||||||
'tenant_id' => $this->tenant->id,
|
'tenant_id' => $this->tenant->id,
|
||||||
'external_id' => 'test-policy-id',
|
'external_id' => 'test-policy-id',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->mock(ScopeTagResolver::class, function ($mock) {
|
||||||
|
$mock->shouldReceive('resolve')
|
||||||
|
->andReturn([
|
||||||
|
['id' => '0', 'displayName' => 'Default'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('captures policy version with assignments from graph', function () {
|
it('captures policy version with assignments from graph', function () {
|
||||||
@ -45,13 +53,14 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$this->mock(GroupResolver::class, function ($mock) {
|
$this->mock(GroupResolver::class, function ($mock) {
|
||||||
$mock->shouldReceive('resolve')
|
$mock->shouldReceive('resolveGroupIds')
|
||||||
->once()
|
->once()
|
||||||
->andReturn([
|
->andReturn([
|
||||||
'resolved' => [
|
'group-123' => [
|
||||||
'group-123' => ['id' => 'group-123', 'displayName' => 'Test Group'],
|
'id' => 'group-123',
|
||||||
|
'displayName' => 'Test Group',
|
||||||
|
'orphaned' => false,
|
||||||
],
|
],
|
||||||
'orphaned' => [],
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -68,7 +77,11 @@
|
|||||||
->and($version->assignments[0]['target']['groupId'])->toBe('group-123')
|
->and($version->assignments[0]['target']['groupId'])->toBe('group-123')
|
||||||
->and($version->assignments_hash)->not->toBeNull()
|
->and($version->assignments_hash)->not->toBeNull()
|
||||||
->and($version->metadata['assignments_count'])->toBe(1)
|
->and($version->metadata['assignments_count'])->toBe(1)
|
||||||
->and($version->metadata['has_orphaned_assignments'])->toBeFalse();
|
->and($version->metadata['has_orphaned_assignments'])->toBeFalse()
|
||||||
|
->and($version->scope_tags)->toBe([
|
||||||
|
'ids' => ['0'],
|
||||||
|
'names' => ['Default'],
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('captures policy version without assignments when none exist', function () {
|
it('captures policy version without assignments when none exist', function () {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user