Files
AutopilotMonitor/src/Backend/AutopilotMonitor.Functions/DataAccess/DataAccessServiceExtensions.cs
T
Oliver Kieselbach e4c4dd57db feat(backend,web): optional Graph add-on permissions for Intune script display names
Adds opt-in resolution of Intune Platform Script + Remediation display names in
session timelines via a tenant-side appRoleAssignment grant. No app manifest
change, no impact on customers who don't enable it. Architecture is reusable for
future optional Graph permissions: catalog -> detector -> resolver -> endpoint.

Backend
- GraphFeatureDetector parses the `roles` claim from the SP access token and
  caches the (token, roles) bundle per tenant. GetSnapshotAsync surfaces a
  transient flag so admin-UI callers can render "unknown" instead of a
  misleading "0 of 0 granted" when token acquire times out.
- GraphTokenService now accepts a CancellationToken so optional-feature callers
  can apply tight budgets instead of inheriting the 5+15+30 s retry chain.
- ScriptDisplayNameResolver: cache-first, list-pull-on-cold-tenant, per-ID
  fallback. Throttle-aware abort: skip per-ID for kinds whose full-pull hit a
  transient failure; break the per-ID loop on the first HttpRequestException.
  Total budget 5 s, with a tight 1.5 s sub-budget on the token acquire.
- Azure Table `ScriptNameCache` + direct-TableClient fail-loud repository
  (mirrors TableOffboardingAuditRepository pattern). Daily
  ScriptNameCacheCleanupFunction: 7 d positive / 1 h NotFound TTL.
- POST /api/tenants/{id}/scripts/display-names (body-carried refs to dodge URL
  length limits; backend cap 200 refs/request).
- GET /api/tenants/{id}/graph-permissions/status (4 s budget; includes app
  ClientId so the UI can render an exact grant command).
- POST /api/tenants/{id}/graph-permissions/refresh (invalidates token cache).
- All three routes registered in EndpointAccessPolicyCatalog (MemberRead for the
  resolve, TenantAdminOrGA for the admin-UI surfaces).

Web
- `useScriptDisplayNames` hook with stable refset fingerprint (no spurious
  refetches on parent re-renders); clears stale map on tenant/refset change so
  a session switch never leaks names across tenants.
- Chunked POST loop: 200-ref batches with partial-success semantics. Preserves
  earlier chunks when later ones fail (non-2xx OR transport throw); only
  TokenExpiredError propagates so auth flow can react.
- ScriptExecutions renders " . {DisplayName}" alongside existing label/short-id,
  keyed by Kind + policyId so Platform/Remediation under the same id never
  collide.
- New Settings section "Optional Graph capabilities": status table with granted
  / not granted / unknown, copy-paste PS one-liner with ClientId baked in,
  refresh button gated on response.ok.

Customer-facing
- scripts/CustomerSetup/Grant-AutopilotMonitorAddOn.ps1: idempotent
  grant / verify / revoke script. -Features ScriptDisplayNames (high level) or
  -Permissions ... (admin-UI generated). Pure ASCII for PS 5.1 compat.
- docs/customer-graph-add-on-permissions.md: end-to-end customer guide
  covering prerequisites, the appRoleAssignment pattern, and revocation.

Tests
- 1718 backend xUnit / 364 web vitest. Highlights: JWT roles parsing edge
  cases; full-pull-429 suppresses per-ID; per-ID-429 breaks loop; transient
  detection via GetSnapshotAsync; chunked partial success (HTTP failure + thrown
  transport); TokenExpiredError propagation; EndpointPolicyCatalogCompleteness
  covers the three new routes by reflection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:14:21 +02:00

78 lines
4.4 KiB
C#

using AutopilotMonitor.Shared.DataAccess;
using AutopilotMonitor.Functions.DataAccess.TableStorage;
using Microsoft.Extensions.DependencyInjection;
namespace AutopilotMonitor.Functions.DataAccess
{
/// <summary>
/// Extension methods for registering Data Access Layer services.
/// Currently registers Azure Table Storage implementations.
/// To switch to Cosmos DB, replace the implementation registrations here.
/// </summary>
public static class DataAccessServiceExtensions
{
/// <summary>
/// Registers all Data Access Layer services with Table Storage implementations.
/// </summary>
public static IServiceCollection AddTableStorageDataAccess(this IServiceCollection services)
{
// Event publisher (no-op by default; replace with Event Hub/Service Bus implementation)
services.AddSingleton<IDataEventPublisher, NullDataEventPublisher>();
// Storage initializer
services.AddSingleton<IStorageInitializer, TableStorageInitializer>();
// Repository implementations (delegate to existing TableStorageService)
services.AddSingleton<ISessionRepository, TableSessionRepository>();
services.AddSingleton<IRuleRepository, TableRuleRepository>();
services.AddSingleton<IMetricsRepository, TableMetricsRepository>();
services.AddSingleton<IMaintenanceRepository, TableMaintenanceRepository>();
services.AddSingleton<IVulnerabilityRepository, TableVulnerabilityRepository>();
services.AddSingleton<IAdminRepository, TableAdminRepository>();
services.AddSingleton<IConfigRepository, TableConfigRepository>();
services.AddSingleton<IBootstrapRepository, TableBootstrapRepository>();
services.AddSingleton<INotificationRepository, TableNotificationRepository>();
services.AddSingleton<ITenantNotificationRepository, TableTenantNotificationRepository>();
services.AddSingleton<ISlaTenantStatusRepository, TableSlaTenantStatusRepository>();
services.AddSingleton<IHardwareRejectionNotificationTracker, TableHardwareRejectionNotificationTracker>();
services.AddSingleton<IDeviceSecurityRepository, TableDeviceSecurityRepository>();
services.AddSingleton<IUserUsageRepository, TableUserUsageRepository>();
services.AddSingleton<IDistressReportRepository, TableDistressReportRepository>();
services.AddSingleton<IOpsEventRepository, TableOpsEventRepository>();
// V2 Decision Engine repositories (Plan §M5).
services.AddSingleton<ISignalRepository, TableSignalRepository>();
services.AddSingleton<IDecisionTransitionRepository, TableDecisionTransitionRepository>();
services.AddSingleton<IIndexTableRepository, TableIndexRepository>();
// Tenant-offboarding audit (Marker / History / ByTenant pointer).
services.AddSingleton<IOffboardingAuditRepository, TableOffboardingAuditRepository>();
// Tenant-offboarding customs archive (PR3.B): snapshot of GatherRules /
// AnalyzeRules / ImeLogPatterns rows that were wiped during Phase 2.D-archive,
// surfaced via the Global Admin /admin/customs-archive page.
services.AddSingleton<ITenantCustomsArchiveRepository, TableTenantCustomsArchiveRepository>();
// Graph add-on permission feature: per-tenant cache of Intune script display names.
services.AddSingleton<IScriptNameCacheRepository, TableScriptNameCacheRepository>();
return services;
}
/// <summary>
/// Replaces the default NullDataEventPublisher with a custom implementation.
/// Call this AFTER AddTableStorageDataAccess() to override.
/// Example: services.AddEventStreaming&lt;EventHubPublisher&gt;();
/// </summary>
public static IServiceCollection AddEventStreaming<TPublisher>(this IServiceCollection services)
where TPublisher : class, IDataEventPublisher
{
// Remove existing registration and replace
var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IDataEventPublisher));
if (descriptor != null)
services.Remove(descriptor);
services.AddSingleton<IDataEventPublisher, TPublisher>();
return services;
}
}
}