diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/UserSessionProcessLauncher.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/UserSessionProcessLauncher.cs
index b57af965..93fd059f 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/UserSessionProcessLauncher.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/UserSessionProcessLauncher.cs
@@ -111,10 +111,26 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
return false;
}
- // Close the process and thread handles — we don't need them (fire-and-forget)
- NativeMethods.CloseHandle(pi.hProcess);
+ // Close the thread handle — we don't need it
NativeMethods.CloseHandle(pi.hThread);
+ logger.Info($"UserSessionProcessLauncher: Created process PID {pi.dwProcessId} in user session — verifying startup...");
+
+ // Verify the process actually started (CLR initialized, no immediate crash).
+ // If the process dies within 3 seconds, the exit code tells us exactly why
+ // (e.g. 0xC0000135 = DLL not found, 0xC000007B = invalid image format,
+ // 0xE0434352 = .NET unhandled exception, 0x80131522 = fusion/assembly load error).
+ var waitResult = NativeMethods.WaitForSingleObject(pi.hProcess, 3000);
+ if (waitResult != NativeMethods.WAIT_TIMEOUT)
+ {
+ NativeMethods.GetExitCodeProcess(pi.hProcess, out var exitCode);
+ NativeMethods.CloseHandle(pi.hProcess);
+ logger.Warning($"UserSessionProcessLauncher: Process PID {pi.dwProcessId} exited within 3s — " +
+ $"exit code 0x{exitCode:X8} (startup crash or missing dependency)");
+ return false;
+ }
+
+ NativeMethods.CloseHandle(pi.hProcess);
logger.Info($"UserSessionProcessLauncher: Successfully launched process PID {pi.dwProcessId} in user session");
return true;
}
@@ -325,6 +341,14 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr hObject);
+
+ [DllImport("kernel32.dll")]
+ public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
+
+ public const uint WAIT_TIMEOUT = 258;
}
}
}
diff --git a/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs b/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs
index 5cbe9491..773b2536 100644
--- a/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs
+++ b/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs
@@ -27,9 +27,11 @@ namespace AutopilotMonitor.SummaryDialog
System.Reflection.Assembly.GetExecutingAssembly().Location) ?? ".";
_logFile = Path.Combine(exeDir, "SummaryDialog.log");
- // Error log in user's temp folder — survives self-cleanup for troubleshooting
- try { _tempLogFile = Path.Combine(Path.GetTempPath(), "SummaryDialog-error.log"); }
- catch { /* temp path unavailable */ }
+ // Error log in ProgramData — fixed, known path that survives self-cleanup.
+ // Always accessible regardless of user profile state or session assignment.
+ var programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
+ try { _tempLogFile = Path.Combine(programData, "SummaryDialog-error.log"); }
+ catch { /* path unavailable */ }
Log("App constructor — process started");
LogDiagnostics();
diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/UnitTest1.cs b/src/Backend/AutopilotMonitor.Functions.Tests/BuiltInRulesTests.cs
similarity index 96%
rename from src/Backend/AutopilotMonitor.Functions.Tests/UnitTest1.cs
rename to src/Backend/AutopilotMonitor.Functions.Tests/BuiltInRulesTests.cs
index 4ecc0e78..a289f06c 100644
--- a/src/Backend/AutopilotMonitor.Functions.Tests/UnitTest1.cs
+++ b/src/Backend/AutopilotMonitor.Functions.Tests/BuiltInRulesTests.cs
@@ -75,7 +75,8 @@ public class BuiltInRulesTests
"policiesDiscovered", "ignoreCompletedApp", "imeAgentVersion",
"espTrackStatus", "updateName", "updateWin32AppState",
"cancelStuckAndSetCurrent", "imeSessionChange", "imeImpersonation",
- "enrollmentCompleted"
+ "enrollmentCompleted", "updateDoTelemetry", "scriptStarted",
+ "scriptContext", "scriptExitCode", "scriptOutput", "scriptCompleted"
};
var patterns = BuiltInImeLogPatterns.GetAll();
diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/EndpointPolicyCatalogCompletenessTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/EndpointPolicyCatalogCompletenessTests.cs
new file mode 100644
index 00000000..bc7f3288
--- /dev/null
+++ b/src/Backend/AutopilotMonitor.Functions.Tests/EndpointPolicyCatalogCompletenessTests.cs
@@ -0,0 +1,185 @@
+using System.Reflection;
+using AutopilotMonitor.Functions.Middleware;
+using AutopilotMonitor.Functions.Security;
+using Microsoft.Azure.Functions.Worker;
+
+namespace AutopilotMonitor.Functions.Tests;
+
+///
+/// Ensures the EndpointAccessPolicyCatalog is a complete and accurate mapping
+/// of every HTTP route in the Functions project. Fail-closed: any unregistered
+/// route causes a test failure.
+///
+public class EndpointPolicyCatalogCompletenessTests
+{
+ ///
+ /// Every [HttpTrigger] route + method combination must have a matching catalog entry.
+ /// This prevents new endpoints from being deployed without an explicit policy decision.
+ ///
+ [Fact]
+ public void AllHttpTriggers_HaveMatchingCatalogEntry()
+ {
+ var assembly = typeof(AuthenticationMiddleware).Assembly;
+ var missing = new List();
+
+ foreach (var type in assembly.GetTypes())
+ {
+ foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance))
+ {
+ var triggerParam = method.GetParameters()
+ .FirstOrDefault(p => p.GetCustomAttribute() != null);
+
+ if (triggerParam == null)
+ continue;
+
+ var trigger = triggerParam.GetCustomAttribute()!;
+ var route = trigger.Route;
+
+ if (string.IsNullOrEmpty(route))
+ continue;
+
+ var httpMethods = trigger.Methods ?? Array.Empty();
+ if (httpMethods.Length == 0)
+ httpMethods = new[] { "GET" }; // default if no method specified
+
+ foreach (var httpMethod in httpMethods)
+ {
+ var entry = EndpointAccessPolicyCatalog.FindPolicy(httpMethod, $"/api/{route}");
+ if (entry == null)
+ {
+ missing.Add($"{httpMethod.ToUpper()} /api/{route}");
+ }
+ }
+ }
+ }
+
+ Assert.True(missing.Count == 0,
+ $"The following HTTP routes are NOT registered in EndpointAccessPolicyCatalog (fail-closed):\n" +
+ string.Join("\n", missing.Select(m => $" - {m}")));
+ }
+
+ ///
+ /// Every catalog entry must match at least one actual [HttpTrigger] route.
+ /// Prevents stale/orphaned entries that could mask security misconfigurations.
+ ///
+ [Fact]
+ public void AllCatalogEntries_MatchExistingRoutes()
+ {
+ var assembly = typeof(AuthenticationMiddleware).Assembly;
+
+ // Collect all actual (method, route) pairs from HttpTrigger attributes
+ var actualRoutes = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var type in assembly.GetTypes())
+ {
+ foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance))
+ {
+ var triggerParam = method.GetParameters()
+ .FirstOrDefault(p => p.GetCustomAttribute() != null);
+
+ if (triggerParam == null)
+ continue;
+
+ var trigger = triggerParam.GetCustomAttribute()!;
+ var route = trigger.Route;
+
+ if (string.IsNullOrEmpty(route))
+ continue;
+
+ var httpMethods = trigger.Methods ?? Array.Empty();
+ if (httpMethods.Length == 0)
+ httpMethods = new[] { "GET" };
+
+ foreach (var httpMethod in httpMethods)
+ {
+ actualRoutes.Add($"{httpMethod.ToUpper()}:{route}");
+ }
+ }
+ }
+
+ var orphaned = new List();
+
+ foreach (var entry in EndpointAccessPolicyCatalog.Entries)
+ {
+ // Check if any actual route matches this catalog entry via FindPolicy
+ var hasMatch = actualRoutes.Any(ar =>
+ {
+ var parts = ar.Split(':', 2);
+ var method = parts[0];
+ var route = parts[1];
+ var found = EndpointAccessPolicyCatalog.FindPolicy(method, $"/api/{route}");
+ return found != null && found.RouteTemplate == entry.RouteTemplate && found.HttpMethod == entry.HttpMethod;
+ });
+
+ if (!hasMatch)
+ {
+ orphaned.Add($"{entry.HttpMethod} {entry.RouteTemplate} [{entry.Policy}]");
+ }
+ }
+
+ Assert.True(orphaned.Count == 0,
+ $"The following catalog entries have no matching [HttpTrigger] route (stale entries):\n" +
+ string.Join("\n", orphaned.Select(o => $" - {o}")));
+ }
+
+ ///
+ /// Verifies that parameterized route templates correctly match actual request paths.
+ ///
+ [Theory]
+ [InlineData("GET", "/api/sessions/abc-123", "sessions/{sessionId}")]
+ [InlineData("GET", "/api/sessions/abc-123/events", "sessions/{sessionId}/events")]
+ [InlineData("GET", "/api/sessions/abc-123/analysis", "sessions/{sessionId}/analysis")]
+ [InlineData("DELETE", "/api/sessions/abc-123", "sessions/{sessionId}")]
+ [InlineData("GET", "/api/bootstrap/validate/ABCD12", "bootstrap/validate/{code}")]
+ [InlineData("DELETE", "/api/bootstrap/sessions/MYCODE", "bootstrap/sessions/{code}")]
+ [InlineData("GET", "/api/config/00000000-0000-0000-0000-000000000001", "config/{tenantId}")]
+ [InlineData("PUT", "/api/rules/gather/rule-1", "rules/gather/{ruleId}")]
+ [InlineData("DELETE", "/api/tenants/tid-1/admins/user@contoso.com", "tenants/{tenantId}/admins/{adminUpn}")]
+ [InlineData("PATCH", "/api/tenants/tid-1/admins/user@contoso.com/permissions", "tenants/{tenantId}/admins/{adminUpn}/permissions")]
+ [InlineData("DELETE", "/api/devices/block/SN123456", "devices/block/{encodedSerialNumber}")]
+ [InlineData("DELETE", "/api/versions/block/v1.0.*", "versions/block/{encodedPattern}")]
+ [InlineData("PATCH", "/api/galactic/session-reports/report-1/note", "galactic/session-reports/{reportId}/note")]
+ public void ParameterizedRoutes_MatchCorrectly(string httpMethod, string requestPath, string expectedTemplate)
+ {
+ var entry = EndpointAccessPolicyCatalog.FindPolicy(httpMethod, requestPath);
+
+ Assert.NotNull(entry);
+ Assert.Equal(expectedTemplate, entry.RouteTemplate);
+ }
+
+ ///
+ /// Routes that differ only by HTTP method should resolve to different policies.
+ ///
+ [Fact]
+ public void SameRoute_DifferentMethods_ResolveToDifferentPolicies()
+ {
+ var getConfig = EndpointAccessPolicyCatalog.FindPolicy("GET", "/api/config/tenant-1");
+ var putConfig = EndpointAccessPolicyCatalog.FindPolicy("PUT", "/api/config/tenant-1");
+
+ Assert.NotNull(getConfig);
+ Assert.NotNull(putConfig);
+ Assert.Equal(EndpointPolicy.MemberRead, getConfig.Policy);
+ Assert.Equal(EndpointPolicy.TenantAdminOrGA, putConfig.Policy);
+
+ var getRules = EndpointAccessPolicyCatalog.FindPolicy("GET", "/api/rules/gather");
+ var postRules = EndpointAccessPolicyCatalog.FindPolicy("POST", "/api/rules/gather");
+
+ Assert.NotNull(getRules);
+ Assert.NotNull(postRules);
+ Assert.Equal(EndpointPolicy.MemberRead, getRules.Policy);
+ Assert.Equal(EndpointPolicy.TenantAdminOrGA, postRules.Policy);
+ }
+
+ ///
+ /// Unregistered routes must return null (fail-closed).
+ ///
+ [Theory]
+ [InlineData("GET", "/api/nonexistent")]
+ [InlineData("POST", "/api/sessions")] // GET exists, POST doesn't
+ [InlineData("DELETE", "/api/health")] // GET exists, DELETE doesn't
+ public void UnregisteredRoutes_ReturnNull(string httpMethod, string requestPath)
+ {
+ var entry = EndpointAccessPolicyCatalog.FindPolicy(httpMethod, requestPath);
+ Assert.Null(entry);
+ }
+}
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/AutopilotDeviceValidationConsentFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/AutopilotDeviceValidationConsentFunction.cs
index 5802c571..b12c8098 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/AutopilotDeviceValidationConsentFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/AutopilotDeviceValidationConsentFunction.cs
@@ -97,13 +97,7 @@ public class AutopilotDeviceValidationConsentFunction
private async Task EnsureAuthorizedTenantAdminAsync(HttpRequestData req, string tenantId)
{
- if (!TenantHelper.IsAuthenticated(req))
- {
- var unauthorized = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorized.WriteAsJsonAsync(new { error = "Authentication required." });
- return unauthorized;
- }
-
+ // Authentication enforced by PolicyEnforcementMiddleware
var authenticatedTenantId = TenantHelper.GetTenantId(req);
var userIdentifier = TenantHelper.GetUserIdentifier(req);
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/DeviceBlockFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/DeviceBlockFunction.cs
index 619ce527..e74e9d85 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/DeviceBlockFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/DeviceBlockFunction.cs
@@ -15,16 +15,13 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
private readonly ILogger _logger;
private readonly BlockedDeviceService _blockedDeviceService;
- private readonly GalacticAdminService _galacticAdminService;
public DeviceBlockFunction(
ILogger logger,
- BlockedDeviceService blockedDeviceService,
- GalacticAdminService galacticAdminService)
+ BlockedDeviceService blockedDeviceService)
{
_logger = logger;
_blockedDeviceService = blockedDeviceService;
- _galacticAdminService = galacticAdminService;
}
/// GET /api/devices/blocked?tenantId={tenantId} — list all active blocks for a tenant
@@ -34,12 +31,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
try
{
- if (!TenantHelper.IsAuthenticated(req))
- return await UnauthorizedAsync(req);
-
- var userIdentifier = TenantHelper.GetUserIdentifier(req);
- if (!await _galacticAdminService.IsGalacticAdminAsync(userIdentifier))
- return await ForbiddenAsync(req);
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var tenantId = req.Query["tenantId"];
if (string.IsNullOrEmpty(tenantId))
@@ -67,12 +59,8 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
try
{
- if (!TenantHelper.IsAuthenticated(req))
- return await UnauthorizedAsync(req);
-
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userIdentifier = TenantHelper.GetUserIdentifier(req);
- if (!await _galacticAdminService.IsGalacticAdminAsync(userIdentifier))
- return await ForbiddenAsync(req);
string body;
using (var reader = new System.IO.StreamReader(req.Body))
@@ -137,12 +125,8 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
try
{
- if (!TenantHelper.IsAuthenticated(req))
- return await UnauthorizedAsync(req);
-
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userIdentifier = TenantHelper.GetUserIdentifier(req);
- if (!await _galacticAdminService.IsGalacticAdminAsync(userIdentifier))
- return await ForbiddenAsync(req);
var tenantId = req.Query["tenantId"];
if (string.IsNullOrEmpty(tenantId))
@@ -175,20 +159,6 @@ namespace AutopilotMonitor.Functions.Functions.Admin
// Helpers
// -----------------------------------------------------------------------
- private static async Task UnauthorizedAsync(HttpRequestData req)
- {
- var r = req.CreateResponse(HttpStatusCode.Unauthorized);
- await r.WriteAsJsonAsync(new { success = false, message = "Authentication required." });
- return r;
- }
-
- private static async Task ForbiddenAsync(HttpRequestData req)
- {
- var r = req.CreateResponse(HttpStatusCode.Forbidden);
- await r.WriteAsJsonAsync(new { success = false, message = "Access denied. Only Galactic Admins can manage device blocks." });
- return r;
- }
-
private static async Task BadRequestAsync(HttpRequestData req, string message)
{
var r = req.CreateResponse(HttpStatusCode.BadRequest);
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/GetAllBlockedDevicesFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/GetAllBlockedDevicesFunction.cs
index ead851e2..0dcb1f02 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/GetAllBlockedDevicesFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/GetAllBlockedDevicesFunction.cs
@@ -1,7 +1,6 @@
using System;
using System.Net;
using System.Threading.Tasks;
-using AutopilotMonitor.Functions.Helpers;
using AutopilotMonitor.Functions.Services;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
@@ -13,16 +12,13 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
private readonly ILogger _logger;
private readonly BlockedDeviceService _blockedDeviceService;
- private readonly GalacticAdminService _galacticAdminService;
public GetAllBlockedDevicesFunction(
ILogger logger,
- BlockedDeviceService blockedDeviceService,
- GalacticAdminService galacticAdminService)
+ BlockedDeviceService blockedDeviceService)
{
_logger = logger;
_blockedDeviceService = blockedDeviceService;
- _galacticAdminService = galacticAdminService;
}
/// GET /api/galactic/devices/blocked — list all active blocks across all tenants
@@ -34,21 +30,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
try
{
- if (!TenantHelper.IsAuthenticated(req))
- {
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new { success = false, message = "Authentication required." });
- return unauthorizedResponse;
- }
-
- var userEmail = TenantHelper.GetUserIdentifier(req);
- if (!await _galacticAdminService.IsGalacticAdminAsync(userEmail))
- {
- _logger.LogWarning("Non-Galactic Admin user {User} attempted to access GetAllBlockedDevices", userEmail);
- var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbiddenResponse.WriteAsJsonAsync(new { success = false, message = "Access denied. Galactic Admin role required." });
- return forbiddenResponse;
- }
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var blocked = await _blockedDeviceService.GetAllBlockedDevicesAsync();
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/GetAuditLogsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/GetAuditLogsFunction.cs
index 277d5a65..4f5e6823 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/GetAuditLogsFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/GetAuditLogsFunction.cs
@@ -26,19 +26,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
try
{
- // Validate authentication
- if (!TenantHelper.IsAuthenticated(req))
- {
- _logger.LogWarning("Unauthenticated GetAuditLogs attempt");
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Authentication required. Please provide a valid JWT token."
- });
- return unauthorizedResponse;
- }
-
+ // Authentication + MemberRead authorization enforced by PolicyEnforcementMiddleware
string tenantId = TenantHelper.GetTenantId(req);
string userIdentifier = TenantHelper.GetUserIdentifier(req);
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/GetGalacticAuditLogsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/GetGalacticAuditLogsFunction.cs
index 088a3fc8..47b02b8d 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/GetGalacticAuditLogsFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/GetGalacticAuditLogsFunction.cs
@@ -11,16 +11,13 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
private readonly ILogger _logger;
private readonly TableStorageService _storageService;
- private readonly GalacticAdminService _galacticAdminService;
public GetGalacticAuditLogsFunction(
ILogger logger,
- TableStorageService storageService,
- GalacticAdminService galacticAdminService)
+ TableStorageService storageService)
{
_logger = logger;
_storageService = storageService;
- _galacticAdminService = galacticAdminService;
}
[Function("GetGalacticAuditLogs")]
@@ -31,34 +28,8 @@ namespace AutopilotMonitor.Functions.Functions.Admin
try
{
- // Validate authentication
- if (!TenantHelper.IsAuthenticated(req))
- {
- _logger.LogWarning("Unauthenticated GetGalacticAuditLogs attempt");
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Authentication required. Please provide a valid JWT token."
- });
- return unauthorizedResponse;
- }
-
- // Check if user is Galactic Admin
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userEmail = TenantHelper.GetUserIdentifier(req);
- var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userEmail);
-
- if (!isGalacticAdmin)
- {
- _logger.LogWarning($"Non-Galactic Admin user {userEmail} attempted to access GetGalacticAuditLogs");
- var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbiddenResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Access denied. Galactic Admin role required."
- });
- return forbiddenResponse;
- }
_logger.LogInformation($"Fetching all audit logs across all tenants (User: {userEmail})");
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/ReseedFromGitHubFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/ReseedFromGitHubFunction.cs
index 4f1a70db..e1b73114 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/ReseedFromGitHubFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/ReseedFromGitHubFunction.cs
@@ -19,7 +19,6 @@ namespace AutopilotMonitor.Functions.Functions.Admin
private readonly AnalyzeRuleService _analyzeRuleService;
private readonly ImeLogPatternService _imeLogPatternService;
private readonly TableStorageService _storageService;
- private readonly GalacticAdminService _galacticAdminService;
public ReseedFromGitHubFunction(
ILogger logger,
@@ -27,8 +26,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
GatherRuleService gatherRuleService,
AnalyzeRuleService analyzeRuleService,
ImeLogPatternService imeLogPatternService,
- TableStorageService storageService,
- GalacticAdminService galacticAdminService)
+ TableStorageService storageService)
{
_logger = logger;
_gitHubRepo = gitHubRepo;
@@ -36,7 +34,6 @@ namespace AutopilotMonitor.Functions.Functions.Admin
_analyzeRuleService = analyzeRuleService;
_imeLogPatternService = imeLogPatternService;
_storageService = storageService;
- _galacticAdminService = galacticAdminService;
}
[Function("ReseedFromGitHub")]
@@ -45,20 +42,8 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
try
{
- if (!TenantHelper.IsAuthenticated(req))
- {
- var unauthorized = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorized.WriteAsJsonAsync(new { success = false, message = "Authentication required" });
- return unauthorized;
- }
-
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var upn = TenantHelper.GetUserIdentifier(req);
- if (!await _galacticAdminService.IsGalacticAdminAsync(upn))
- {
- var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbidden.WriteAsJsonAsync(new { success = false, message = "Galactic Admin privileges required" });
- return forbidden;
- }
// Parse ?type= parameter (default: all)
var typeParam = "all";
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/TenantAdminManagementFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/TenantAdminManagementFunction.cs
index 24dfa9ae..553c7b71 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/TenantAdminManagementFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/TenantAdminManagementFunction.cs
@@ -41,11 +41,8 @@ public class TenantAdminManagementFunction
string tenantId,
FunctionContext context)
{
- var principal = context.GetUser();
- if (principal == null)
- {
- return req.CreateResponse(HttpStatusCode.Unauthorized);
- }
+ // Authentication enforced by PolicyEnforcementMiddleware
+ var principal = context.GetUser()!;
var userTenantId = principal.GetTenantId();
var upn = principal.GetUserPrincipalName();
@@ -84,11 +81,8 @@ public class TenantAdminManagementFunction
string tenantId,
FunctionContext context)
{
- var principal = context.GetUser();
- if (principal == null)
- {
- return req.CreateResponse(HttpStatusCode.Unauthorized);
- }
+ // Authentication enforced by PolicyEnforcementMiddleware
+ var principal = context.GetUser()!;
var userTenantId = principal.GetTenantId();
var upn = principal.GetUserPrincipalName();
@@ -140,11 +134,8 @@ public class TenantAdminManagementFunction
string adminUpn,
FunctionContext context)
{
- var principal = context.GetUser();
- if (principal == null)
- {
- return req.CreateResponse(HttpStatusCode.Unauthorized);
- }
+ // Authentication enforced by PolicyEnforcementMiddleware
+ var principal = context.GetUser()!;
var userTenantId = principal.GetTenantId();
var upn = principal.GetUserPrincipalName();
@@ -202,11 +193,8 @@ public class TenantAdminManagementFunction
string adminUpn,
FunctionContext context)
{
- var principal = context.GetUser();
- if (principal == null)
- {
- return req.CreateResponse(HttpStatusCode.Unauthorized);
- }
+ // Authentication enforced by PolicyEnforcementMiddleware
+ var principal = context.GetUser()!;
var userTenantId = principal.GetTenantId();
var upn = principal.GetUserPrincipalName();
@@ -247,11 +235,8 @@ public class TenantAdminManagementFunction
string adminUpn,
FunctionContext context)
{
- var principal = context.GetUser();
- if (principal == null)
- {
- return req.CreateResponse(HttpStatusCode.Unauthorized);
- }
+ // Authentication enforced by PolicyEnforcementMiddleware
+ var principal = context.GetUser()!;
var userTenantId = principal.GetTenantId();
var upn = principal.GetUserPrincipalName();
@@ -291,11 +276,8 @@ public class TenantAdminManagementFunction
string adminUpn,
FunctionContext context)
{
- var principal = context.GetUser();
- if (principal == null)
- {
- return req.CreateResponse(HttpStatusCode.Unauthorized);
- }
+ // Authentication enforced by PolicyEnforcementMiddleware
+ var principal = context.GetUser()!;
var userTenantId = principal.GetTenantId();
var upn = principal.GetUserPrincipalName();
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/TenantOffboardFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/TenantOffboardFunction.cs
index 2a54aad5..da7b2b91 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/TenantOffboardFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/TenantOffboardFunction.cs
@@ -51,11 +51,8 @@ public class TenantOffboardFunction
string tenantId,
FunctionContext context)
{
- var principal = context.GetUser();
- if (principal == null)
- {
- return req.CreateResponse(HttpStatusCode.Unauthorized);
- }
+ // Authentication enforced by PolicyEnforcementMiddleware
+ var principal = context.GetUser()!;
var userTenantId = principal.GetTenantId();
var upn = principal.GetUserPrincipalName();
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/TriggerMaintenanceFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/TriggerMaintenanceFunction.cs
index 2ffad423..06c3152e 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/TriggerMaintenanceFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/TriggerMaintenanceFunction.cs
@@ -1,7 +1,6 @@
using System;
using System.Net;
using System.Threading.Tasks;
-using AutopilotMonitor.Functions.Extensions;
using AutopilotMonitor.Functions.Helpers;
using AutopilotMonitor.Functions.Services;
using Microsoft.Azure.Functions.Worker;
@@ -17,16 +16,13 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
private readonly ILogger _logger;
private readonly MaintenanceService _maintenanceService;
- private readonly GalacticAdminService _galacticAdminService;
public TriggerMaintenanceFunction(
ILogger logger,
- MaintenanceService maintenanceService,
- GalacticAdminService galacticAdminService)
+ MaintenanceService maintenanceService)
{
_logger = logger;
_maintenanceService = maintenanceService;
- _galacticAdminService = galacticAdminService;
}
///
@@ -43,34 +39,8 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
_logger.LogInformation("Manual maintenance trigger requested");
- // Validate authentication
- if (!TenantHelper.IsAuthenticated(req))
- {
- _logger.LogWarning("Unauthenticated maintenance trigger attempt");
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Authentication required. Please provide a valid JWT token."
- });
- return unauthorizedResponse;
- }
-
- // Check if user is Galactic Admin via GalacticAdminService (Azure Table Storage)
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userEmail = TenantHelper.GetUserIdentifier(req);
- var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userEmail);
-
- if (!isGalacticAdmin)
- {
- _logger.LogWarning($"Non-Galactic Admin user {userEmail} attempted to trigger maintenance");
- var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbiddenResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Access denied. Galactic Admin role required."
- });
- return forbiddenResponse;
- }
_logger.LogInformation($"Maintenance trigger initiated by Galactic Admin: {userEmail}");
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/VersionBlockFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/VersionBlockFunction.cs
index efe1e185..56342aef 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/VersionBlockFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/VersionBlockFunction.cs
@@ -15,16 +15,13 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
private readonly ILogger _logger;
private readonly BlockedVersionService _blockedVersionService;
- private readonly GalacticAdminService _galacticAdminService;
public VersionBlockFunction(
ILogger logger,
- BlockedVersionService blockedVersionService,
- GalacticAdminService galacticAdminService)
+ BlockedVersionService blockedVersionService)
{
_logger = logger;
_blockedVersionService = blockedVersionService;
- _galacticAdminService = galacticAdminService;
}
/// GET /api/versions/blocked — list all active version block rules
@@ -34,12 +31,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
try
{
- if (!TenantHelper.IsAuthenticated(req))
- return await UnauthorizedAsync(req);
-
- var userIdentifier = TenantHelper.GetUserIdentifier(req);
- if (!await _galacticAdminService.IsGalacticAdminAsync(userIdentifier))
- return await ForbiddenAsync(req);
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var rules = await _blockedVersionService.GetBlockedVersionsAsync();
@@ -63,12 +55,8 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
try
{
- if (!TenantHelper.IsAuthenticated(req))
- return await UnauthorizedAsync(req);
-
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userIdentifier = TenantHelper.GetUserIdentifier(req);
- if (!await _galacticAdminService.IsGalacticAdminAsync(userIdentifier))
- return await ForbiddenAsync(req);
string body;
using (var reader = new System.IO.StreamReader(req.Body))
@@ -130,12 +118,8 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
try
{
- if (!TenantHelper.IsAuthenticated(req))
- return await UnauthorizedAsync(req);
-
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userIdentifier = TenantHelper.GetUserIdentifier(req);
- if (!await _galacticAdminService.IsGalacticAdminAsync(userIdentifier))
- return await ForbiddenAsync(req);
var versionPattern = Uri.UnescapeDataString(encodedPattern ?? string.Empty);
if (string.IsNullOrEmpty(versionPattern))
@@ -164,20 +148,6 @@ namespace AutopilotMonitor.Functions.Functions.Admin
// Helpers
// -----------------------------------------------------------------------
- private static async Task UnauthorizedAsync(HttpRequestData req)
- {
- var r = req.CreateResponse(HttpStatusCode.Unauthorized);
- await r.WriteAsJsonAsync(new { success = false, message = "Authentication required." });
- return r;
- }
-
- private static async Task ForbiddenAsync(HttpRequestData req)
- {
- var r = req.CreateResponse(HttpStatusCode.Forbidden);
- await r.WriteAsJsonAsync(new { success = false, message = "Access denied. Only Galactic Admins can manage version blocks." });
- return r;
- }
-
private static async Task BadRequestAsync(HttpRequestData req, string message)
{
var r = req.CreateResponse(HttpStatusCode.BadRequest);
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/CreateBootstrapSessionFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/CreateBootstrapSessionFunction.cs
index f32ba024..6f1004bd 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/CreateBootstrapSessionFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/CreateBootstrapSessionFunction.cs
@@ -21,20 +21,17 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
private readonly ILogger _logger;
private readonly BootstrapSessionService _bootstrapService;
private readonly GalacticAdminService _galacticAdminService;
- private readonly TenantAdminsService _tenantAdminsService;
private readonly TenantConfigurationService _configService;
public CreateBootstrapSessionFunction(
ILogger logger,
BootstrapSessionService bootstrapService,
GalacticAdminService galacticAdminService,
- TenantAdminsService tenantAdminsService,
TenantConfigurationService configService)
{
_logger = logger;
_bootstrapService = bootstrapService;
_galacticAdminService = galacticAdminService;
- _tenantAdminsService = tenantAdminsService;
_configService = configService;
}
@@ -44,13 +41,7 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
{
try
{
- if (!TenantHelper.IsAuthenticated(req))
- {
- var unauth = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauth.WriteAsJsonAsync(new { error = "Authentication required" });
- return unauth;
- }
-
+ // Authentication + BootstrapManagerOrGA authorization enforced by PolicyEnforcementMiddleware
var authenticatedTenantId = TenantHelper.GetTenantId(req);
var userIdentifier = TenantHelper.GetUserIdentifier(req);
@@ -70,7 +61,7 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
// Use tenant from JWT if not specified in body
var tenantId = !string.IsNullOrEmpty(request.TenantId) ? request.TenantId : authenticatedTenantId;
- // Tenant boundary check
+ // Cross-tenant boundary check — only Galactic Admins may operate on other tenants
if (!string.Equals(authenticatedTenantId, tenantId, StringComparison.OrdinalIgnoreCase))
{
var isGalactic = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
@@ -82,16 +73,6 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
}
}
- // Admin or Operator with bootstrap permission check
- var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
- var canManageBootstrap = await _tenantAdminsService.CanManageBootstrapAsync(tenantId, userIdentifier);
- if (!isGalacticAdmin && !canManageBootstrap)
- {
- var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbidden.WriteAsJsonAsync(new { error = "Bootstrap token management access required" });
- return forbidden;
- }
-
// Check if bootstrap token feature is enabled for this tenant
var tenantConfig = await _configService.GetConfigurationAsync(tenantId);
if (!tenantConfig.BootstrapTokenEnabled)
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/ListBootstrapSessionsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/ListBootstrapSessionsFunction.cs
index 4172f684..66fa5291 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/ListBootstrapSessionsFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/ListBootstrapSessionsFunction.cs
@@ -20,20 +20,17 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
private readonly ILogger _logger;
private readonly BootstrapSessionService _bootstrapService;
private readonly GalacticAdminService _galacticAdminService;
- private readonly TenantAdminsService _tenantAdminsService;
private readonly TenantConfigurationService _configService;
public ListBootstrapSessionsFunction(
ILogger logger,
BootstrapSessionService bootstrapService,
GalacticAdminService galacticAdminService,
- TenantAdminsService tenantAdminsService,
TenantConfigurationService configService)
{
_logger = logger;
_bootstrapService = bootstrapService;
_galacticAdminService = galacticAdminService;
- _tenantAdminsService = tenantAdminsService;
_configService = configService;
}
@@ -43,19 +40,13 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
{
try
{
- if (!TenantHelper.IsAuthenticated(req))
- {
- var unauth = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauth.WriteAsJsonAsync(new { error = "Authentication required" });
- return unauth;
- }
-
+ // Authentication + BootstrapManagerOrGA authorization enforced by PolicyEnforcementMiddleware
var authenticatedTenantId = TenantHelper.GetTenantId(req);
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var tenantId = req.Query["tenantId"] ?? authenticatedTenantId;
- // Tenant boundary check
+ // Cross-tenant boundary check — only Galactic Admins may operate on other tenants
if (!string.Equals(authenticatedTenantId, tenantId, StringComparison.OrdinalIgnoreCase))
{
var isGalactic = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
@@ -67,16 +58,6 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
}
}
- // Admin or Operator with bootstrap permission check
- var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
- var canManageBootstrap = await _tenantAdminsService.CanManageBootstrapAsync(tenantId, userIdentifier);
- if (!isGalacticAdmin && !canManageBootstrap)
- {
- var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbidden.WriteAsJsonAsync(new { error = "Bootstrap token management access required" });
- return forbidden;
- }
-
// Check if bootstrap token feature is enabled for this tenant
var tenantConfig = await _configService.GetConfigurationAsync(tenantId);
if (!tenantConfig.BootstrapTokenEnabled)
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/RevokeBootstrapSessionFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/RevokeBootstrapSessionFunction.cs
index 5e0e8af1..94e0ec2d 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/RevokeBootstrapSessionFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/RevokeBootstrapSessionFunction.cs
@@ -18,20 +18,17 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
private readonly ILogger _logger;
private readonly BootstrapSessionService _bootstrapService;
private readonly GalacticAdminService _galacticAdminService;
- private readonly TenantAdminsService _tenantAdminsService;
private readonly TenantConfigurationService _configService;
public RevokeBootstrapSessionFunction(
ILogger logger,
BootstrapSessionService bootstrapService,
GalacticAdminService galacticAdminService,
- TenantAdminsService tenantAdminsService,
TenantConfigurationService configService)
{
_logger = logger;
_bootstrapService = bootstrapService;
_galacticAdminService = galacticAdminService;
- _tenantAdminsService = tenantAdminsService;
_configService = configService;
}
@@ -42,19 +39,13 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
{
try
{
- if (!TenantHelper.IsAuthenticated(req))
- {
- var unauth = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauth.WriteAsJsonAsync(new { error = "Authentication required" });
- return unauth;
- }
-
+ // Authentication + BootstrapManagerOrGA authorization enforced by PolicyEnforcementMiddleware
var authenticatedTenantId = TenantHelper.GetTenantId(req);
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var tenantId = req.Query["tenantId"] ?? authenticatedTenantId;
- // Tenant boundary check
+ // Cross-tenant boundary check — only Galactic Admins may operate on other tenants
if (!string.Equals(authenticatedTenantId, tenantId, StringComparison.OrdinalIgnoreCase))
{
var isGalactic = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
@@ -66,16 +57,6 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
}
}
- // Admin or Operator with bootstrap permission check
- var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
- var canManageBootstrap = await _tenantAdminsService.CanManageBootstrapAsync(tenantId, userIdentifier);
- if (!isGalacticAdmin && !canManageBootstrap)
- {
- var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbidden.WriteAsJsonAsync(new { error = "Bootstrap token management access required" });
- return forbidden;
- }
-
// Check if bootstrap token feature is enabled for this tenant
var tenantConfig = await _configService.GetConfigurationAsync(tenantId);
if (!tenantConfig.BootstrapTokenEnabled)
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAdminConfigurationFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAdminConfigurationFunction.cs
index 78c8667f..dfe4cbff 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAdminConfigurationFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAdminConfigurationFunction.cs
@@ -13,16 +13,13 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
private readonly ILogger _logger;
private readonly AdminConfigurationService _adminConfigService;
- private readonly GalacticAdminService _galacticAdminService;
public GetAdminConfigurationFunction(
ILogger logger,
- AdminConfigurationService adminConfigService,
- GalacticAdminService galacticAdminService)
+ AdminConfigurationService adminConfigService)
{
_logger = logger;
_adminConfigService = adminConfigService;
- _galacticAdminService = galacticAdminService;
}
[Function("GetAdminConfiguration")]
@@ -31,36 +28,9 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
try
{
- // Validate authentication
- if (!TenantHelper.IsAuthenticated(req))
- {
- _logger.LogWarning("Unauthenticated GetAdminConfiguration attempt");
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Authentication required. Please provide a valid JWT token."
- });
- return unauthorizedResponse;
- }
-
- // Get tenant ID and user identifier from JWT token
- string tenantId = TenantHelper.GetTenantId(req);
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
string userIdentifier = TenantHelper.GetUserIdentifier(req);
- // Validate Galactic Admin role
- if (!await _galacticAdminService.IsGalacticAdminAsync(userIdentifier))
- {
- _logger.LogWarning($"Non-Galactic Admin user {userIdentifier} from tenant {tenantId} attempted to access admin configuration");
- var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbiddenResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Access denied. Only Galactic Admins can access global configuration."
- });
- return forbiddenResponse;
- }
-
_logger.LogInformation($"GetAdminConfiguration by Galactic Admin user {userIdentifier}");
var config = await _adminConfigService.GetConfigurationAsync();
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAllTenantConfigurationsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAllTenantConfigurationsFunction.cs
index daf0a32b..0d355d35 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAllTenantConfigurationsFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAllTenantConfigurationsFunction.cs
@@ -13,16 +13,13 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
private readonly ILogger _logger;
private readonly TenantConfigurationService _configService;
- private readonly GalacticAdminService _galacticAdminService;
public GetAllTenantConfigurationsFunction(
ILogger logger,
- TenantConfigurationService configService,
- GalacticAdminService galacticAdminService)
+ TenantConfigurationService configService)
{
_logger = logger;
_configService = configService;
- _galacticAdminService = galacticAdminService;
}
[Function("GetAllTenantConfigurations")]
@@ -31,37 +28,9 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
try
{
- // Validate authentication
- if (!TenantHelper.IsAuthenticated(req))
- {
- _logger.LogWarning("Unauthenticated GetAllTenantConfigurations attempt");
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Authentication required. Please provide a valid JWT token."
- });
- return unauthorizedResponse;
- }
-
- // Get user identifier
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
string userIdentifier = TenantHelper.GetUserIdentifier(req);
- // Check if user is Galactic Admin
- bool isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
-
- if (!isGalacticAdmin)
- {
- _logger.LogWarning($"User {userIdentifier} attempted to access all tenant configurations without Galactic Admin privileges");
- var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbiddenResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Access denied. This endpoint requires Galactic Admin privileges."
- });
- return forbiddenResponse;
- }
-
_logger.LogInformation($"GetAllTenantConfigurations by Galactic Admin {userIdentifier}");
var configurations = await _configService.GetAllConfigurationsAsync();
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetTenantConfigurationFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetTenantConfigurationFunction.cs
index 3dca215f..604ce5c1 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetTenantConfigurationFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetTenantConfigurationFunction.cs
@@ -13,13 +13,16 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
private readonly ILogger _logger;
private readonly TenantConfigurationService _configService;
+ private readonly GalacticAdminService _galacticAdminService;
public GetTenantConfigurationFunction(
ILogger logger,
- TenantConfigurationService configService)
+ TenantConfigurationService configService,
+ GalacticAdminService galacticAdminService)
{
_logger = logger;
_configService = configService;
+ _galacticAdminService = galacticAdminService;
}
[Function("GetTenantConfiguration")]
@@ -29,34 +32,26 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
try
{
- // Validate authentication
- if (!TenantHelper.IsAuthenticated(req))
- {
- _logger.LogWarning($"Unauthenticated GetTenantConfiguration attempt for tenant {tenantId}");
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Authentication required. Please provide a valid JWT token."
- });
- return unauthorizedResponse;
- }
-
- // Get tenant ID from JWT token and validate access
+ // Authentication + MemberRead authorization enforced by PolicyEnforcementMiddleware
string authenticatedTenantId = TenantHelper.GetTenantId(req);
string userIdentifier = TenantHelper.GetUserIdentifier(req);
- // Validate tenant access: User can only access their own tenant's configuration
- if (authenticatedTenantId != tenantId)
+ // Validate tenant access: cross-tenant only for Galactic Admins
+ if (!string.Equals(authenticatedTenantId, tenantId, StringComparison.OrdinalIgnoreCase))
{
- _logger.LogWarning($"User {userIdentifier} from tenant {authenticatedTenantId} attempted to access configuration for tenant {tenantId}");
- var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbiddenResponse.WriteAsJsonAsync(new
+ var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
+ if (!isGalacticAdmin)
{
- success = false,
- message = "Access denied. You can only access your own tenant's configuration."
- });
- return forbiddenResponse;
+ _logger.LogWarning("User {User} from tenant {AuthTenant} attempted to access configuration for tenant {TargetTenant}",
+ userIdentifier, authenticatedTenantId, tenantId);
+ var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
+ await forbiddenResponse.WriteAsJsonAsync(new
+ {
+ success = false,
+ message = "Access denied. You can only access your own tenant's configuration."
+ });
+ return forbiddenResponse;
+ }
}
_logger.LogInformation($"GetTenantConfiguration: {tenantId} by user {userIdentifier}");
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Config/UpdateAdminConfigurationFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Config/UpdateAdminConfigurationFunction.cs
index 27ed68dd..48a46b4e 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Config/UpdateAdminConfigurationFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Config/UpdateAdminConfigurationFunction.cs
@@ -16,16 +16,13 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
private readonly ILogger _logger;
private readonly AdminConfigurationService _adminConfigService;
- private readonly GalacticAdminService _galacticAdminService;
public UpdateAdminConfigurationFunction(
ILogger logger,
- AdminConfigurationService adminConfigService,
- GalacticAdminService galacticAdminService)
+ AdminConfigurationService adminConfigService)
{
_logger = logger;
_adminConfigService = adminConfigService;
- _galacticAdminService = galacticAdminService;
}
[Function("UpdateAdminConfiguration")]
@@ -34,36 +31,9 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
try
{
- // Validate authentication
- if (!TenantHelper.IsAuthenticated(req))
- {
- _logger.LogWarning("Unauthenticated UpdateAdminConfiguration attempt");
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Authentication required. Please provide a valid JWT token."
- });
- return unauthorizedResponse;
- }
-
- // Get tenant ID and user identifier from JWT token
- string tenantId = TenantHelper.GetTenantId(req);
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
string userIdentifier = TenantHelper.GetUserIdentifier(req);
- // Validate Galactic Admin role
- if (!await _galacticAdminService.IsGalacticAdminAsync(userIdentifier))
- {
- _logger.LogWarning($"Non-Galactic Admin user {userIdentifier} from tenant {tenantId} attempted to update admin configuration");
- var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbiddenResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Access denied. Only Galactic Admins can modify global configuration."
- });
- return forbiddenResponse;
- }
-
_logger.LogInformation($"UpdateAdminConfiguration by Galactic Admin user {userIdentifier}");
// Parse request body
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Config/UpdateTenantConfigurationFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Config/UpdateTenantConfigurationFunction.cs
index c2073921..d9175207 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Config/UpdateTenantConfigurationFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Config/UpdateTenantConfigurationFunction.cs
@@ -16,18 +16,15 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
private readonly ILogger _logger;
private readonly TenantConfigurationService _configService;
- private readonly TenantAdminsService _tenantAdminsService;
private readonly GalacticAdminService _galacticAdminService;
public UpdateTenantConfigurationFunction(
ILogger logger,
TenantConfigurationService configService,
- TenantAdminsService tenantAdminsService,
GalacticAdminService galacticAdminService)
{
_logger = logger;
_configService = configService;
- _tenantAdminsService = tenantAdminsService;
_galacticAdminService = galacticAdminService;
}
@@ -38,27 +35,18 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
try
{
- // Validate authentication
- if (!TenantHelper.IsAuthenticated(req))
- {
- _logger.LogWarning($"Unauthenticated UpdateTenantConfiguration attempt for tenant {tenantId}");
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Authentication required. Please provide a valid JWT token."
- });
- return unauthorizedResponse;
- }
-
- // Get tenant ID from JWT token and validate access
+ // Authentication + TenantAdminOrGA authorization enforced by PolicyEnforcementMiddleware
string authenticatedTenantId = TenantHelper.GetTenantId(req);
string userIdentifier = TenantHelper.GetUserIdentifier(req);
- // Validate tenant access: User can only update their own tenant's configuration
- if (authenticatedTenantId != tenantId)
+ // GA check needed for: cross-tenant bypass + protected field filtering
+ var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
+
+ // Validate tenant access: cross-tenant only for Galactic Admins
+ if (!isGalacticAdmin && !string.Equals(authenticatedTenantId, tenantId, StringComparison.OrdinalIgnoreCase))
{
- _logger.LogWarning($"User {userIdentifier} from tenant {authenticatedTenantId} attempted to update configuration for tenant {tenantId}");
+ _logger.LogWarning("User {User} from tenant {AuthTenant} attempted to update configuration for tenant {TargetTenant}",
+ userIdentifier, authenticatedTenantId, tenantId);
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new
{
@@ -68,22 +56,7 @@ namespace AutopilotMonitor.Functions.Functions.Config
return forbiddenResponse;
}
- // Require Tenant Admin or Galactic Admin for configuration changes
- var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
- var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
- if (!isGalacticAdmin && !isTenantAdmin)
- {
- _logger.LogWarning($"Non-admin user {userIdentifier} attempted to update configuration for tenant {tenantId}");
- var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbiddenResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Access denied. Tenant Admin or Galactic Admin role required."
- });
- return forbiddenResponse;
- }
-
- _logger.LogInformation($"UpdateTenantConfiguration: {tenantId} by user {userIdentifier}");
+ _logger.LogInformation("UpdateTenantConfiguration: {TenantId} by user {User}", tenantId, userIdentifier);
// Parse request body
if (req.Headers.TryGetValues("Content-Length", out var clValues)
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Diagnostics/DiagnosticsDownloadFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Diagnostics/DiagnosticsDownloadFunction.cs
index 335ef51f..1d39d30f 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Diagnostics/DiagnosticsDownloadFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Diagnostics/DiagnosticsDownloadFunction.cs
@@ -34,14 +34,7 @@ namespace AutopilotMonitor.Functions.Functions.Diagnostics
{
try
{
- // Validate authentication
- if (!TenantHelper.IsAuthenticated(req))
- {
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new { success = false, message = "Authentication required." });
- return unauthorizedResponse;
- }
-
+ // Authentication + MemberRead authorization enforced by PolicyEnforcementMiddleware
var userTenantId = TenantHelper.GetTenantId(req);
var userIdentifier = TenantHelper.GetUserIdentifier(req);
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/AuthFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/AuthFunction.cs
index 718bdbe6..9fa9c0c9 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/AuthFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/AuthFunction.cs
@@ -217,21 +217,7 @@ public class AuthFunction
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "auth/galactic-admins")] HttpRequestData req,
FunctionContext context)
{
- var principal = context.GetUser();
- if (principal == null)
- {
- return req.CreateResponse(HttpStatusCode.Unauthorized);
- }
-
- var upn = principal.GetUserPrincipalName();
- var isAdmin = await _galacticAdminService.IsGalacticAdminAsync(upn);
-
- if (!isAdmin)
- {
- var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbiddenResponse.WriteAsJsonAsync(new { error = "Only Galactic Admins can access this endpoint" });
- return forbiddenResponse;
- }
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var admins = await _galacticAdminService.GetAllGalacticAdminsAsync();
@@ -250,21 +236,9 @@ public class AuthFunction
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "auth/galactic-admins")] HttpRequestData req,
FunctionContext context)
{
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var principal = context.GetUser();
- if (principal == null)
- {
- return req.CreateResponse(HttpStatusCode.Unauthorized);
- }
-
- var currentUpn = principal.GetUserPrincipalName();
- var isAdmin = await _galacticAdminService.IsGalacticAdminAsync(currentUpn);
-
- if (!isAdmin)
- {
- var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbiddenResponse.WriteAsJsonAsync(new { error = "Only Galactic Admins can add other admins" });
- return forbiddenResponse;
- }
+ var currentUpn = principal?.GetUserPrincipalName();
// Parse request body
var body = await req.ReadFromJsonAsync();
@@ -295,21 +269,9 @@ public class AuthFunction
string upn,
FunctionContext context)
{
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var principal = context.GetUser();
- if (principal == null)
- {
- return req.CreateResponse(HttpStatusCode.Unauthorized);
- }
-
- var currentUpn = principal.GetUserPrincipalName();
- var isAdmin = await _galacticAdminService.IsGalacticAdminAsync(currentUpn);
-
- if (!isAdmin)
- {
- var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbiddenResponse.WriteAsJsonAsync(new { error = "Only Galactic Admins can remove other admins" });
- return forbiddenResponse;
- }
+ var currentUpn = principal?.GetUserPrincipalName();
// Prevent self-removal
if (upn.Equals(currentUpn, StringComparison.OrdinalIgnoreCase))
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/HealthCheckFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/HealthCheckFunction.cs
index 9e22b7af..896df03f 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/HealthCheckFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/HealthCheckFunction.cs
@@ -1,6 +1,4 @@
using System.Net;
-using AutopilotMonitor.Functions.Extensions;
-using AutopilotMonitor.Functions.Helpers;
using AutopilotMonitor.Functions.Services;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
@@ -12,16 +10,13 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
{
private readonly ILogger _logger;
private readonly HealthCheckService _healthCheckService;
- private readonly GalacticAdminService _galacticAdminService;
public HealthCheckFunction(
ILogger logger,
- HealthCheckService healthCheckService,
- GalacticAdminService galacticAdminService)
+ HealthCheckService healthCheckService)
{
_logger = logger;
_healthCheckService = healthCheckService;
- _galacticAdminService = galacticAdminService;
}
///
@@ -57,36 +52,7 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
{
_logger.LogInformation("Detailed health check requested");
- // Validate authentication
- if (!TenantHelper.IsAuthenticated(req))
- {
- _logger.LogWarning("Unauthenticated detailed health check attempt");
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Authentication required. Please provide a valid JWT token."
- });
- return unauthorizedResponse;
- }
-
- // Check if user is Galactic Admin via GalacticAdminService (Azure Table Storage)
- var userEmail = TenantHelper.GetUserIdentifier(req);
- var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userEmail);
-
- if (!isGalacticAdmin)
- {
- _logger.LogWarning($"Non-Galactic Admin user {userEmail} attempted to access detailed health check");
- var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbiddenResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Access denied. Galactic Admin role required."
- });
- return forbiddenResponse;
- }
-
- _logger.LogInformation($"Detailed health check accessed by Galactic Admin: {userEmail}");
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Perform comprehensive health checks
var healthCheckResult = await _healthCheckService.PerformAllChecksAsync();
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/SignalRAddToGroupFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/SignalRAddToGroupFunction.cs
index 9190224d..f7e8bd04 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/SignalRAddToGroupFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/SignalRAddToGroupFunction.cs
@@ -28,14 +28,7 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
{
try
{
- // Validate authentication
- if (!TenantHelper.IsAuthenticated(req))
- {
- _logger.LogWarning("Unauthenticated AddToGroup attempt");
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new { success = false, message = "Authentication required" });
- return new AddToGroupOutput { HttpResponse = unauthorizedResponse };
- }
+ // Authentication + AuthenticatedUser authorization enforced by PolicyEnforcementMiddleware
// Parse request
if (req.Headers.TryGetValues("Content-Length", out var clValues)
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/SignalRRemoveFromGroupFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/SignalRRemoveFromGroupFunction.cs
index 553fb225..77244fd3 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/SignalRRemoveFromGroupFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/SignalRRemoveFromGroupFunction.cs
@@ -28,14 +28,7 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
{
try
{
- // Validate authentication
- if (!TenantHelper.IsAuthenticated(req))
- {
- _logger.LogWarning("Unauthenticated RemoveFromGroup attempt");
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new { success = false, message = "Authentication required" });
- return new RemoveFromGroupOutput { HttpResponse = unauthorizedResponse };
- }
+ // Authentication + AuthenticatedUser authorization enforced by PolicyEnforcementMiddleware
// Parse request
if (req.Headers.TryGetValues("Content-Length", out var clValues)
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetAppMetricsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetAppMetricsFunction.cs
index ff7bc790..0f27bc37 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetAppMetricsFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetAppMetricsFunction.cs
@@ -24,14 +24,7 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
{
try
{
- // Validate authentication
- if (!TenantHelper.IsAuthenticated(req))
- {
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new { success = false, message = "Authentication required." });
- return unauthorizedResponse;
- }
-
+ // Authentication + MemberRead authorization enforced by PolicyEnforcementMiddleware
var tenantId = TenantHelper.GetTenantId(req);
_logger.LogInformation($"Fetching app metrics for tenant {tenantId}");
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGalacticAppMetricsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGalacticAppMetricsFunction.cs
index a112005f..14e2708e 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGalacticAppMetricsFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGalacticAppMetricsFunction.cs
@@ -11,16 +11,13 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
{
private readonly ILogger _logger;
private readonly TableStorageService _storageService;
- private readonly GalacticAdminService _galacticAdminService;
public GetGalacticAppMetricsFunction(
ILogger logger,
- TableStorageService storageService,
- GalacticAdminService galacticAdminService)
+ TableStorageService storageService)
{
_logger = logger;
_storageService = storageService;
- _galacticAdminService = galacticAdminService;
}
[Function("GetGalacticAppMetrics")]
@@ -29,22 +26,8 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
{
try
{
- if (!TenantHelper.IsAuthenticated(req))
- {
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new { success = false, message = "Authentication required." });
- return unauthorizedResponse;
- }
-
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userEmail = TenantHelper.GetUserIdentifier(req);
- var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userEmail);
-
- if (!isGalacticAdmin)
- {
- var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbiddenResponse.WriteAsJsonAsync(new { success = false, message = "Access denied. Galactic Admin role required." });
- return forbiddenResponse;
- }
_logger.LogInformation($"Fetching galactic app metrics (User: {userEmail})");
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGalacticGeographicMetricsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGalacticGeographicMetricsFunction.cs
index 0325bffe..b308f105 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGalacticGeographicMetricsFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGalacticGeographicMetricsFunction.cs
@@ -11,16 +11,13 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
{
private readonly ILogger _logger;
private readonly TableStorageService _storageService;
- private readonly GalacticAdminService _galacticAdminService;
public GetGalacticGeographicMetricsFunction(
ILogger logger,
- TableStorageService storageService,
- GalacticAdminService galacticAdminService)
+ TableStorageService storageService)
{
_logger = logger;
_storageService = storageService;
- _galacticAdminService = galacticAdminService;
}
[Function("GetGalacticGeographicMetrics")]
@@ -29,22 +26,8 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
{
try
{
- if (!TenantHelper.IsAuthenticated(req))
- {
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new { success = false, message = "Authentication required." });
- return unauthorizedResponse;
- }
-
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userEmail = TenantHelper.GetUserIdentifier(req);
- var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userEmail);
-
- if (!isGalacticAdmin)
- {
- var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbiddenResponse.WriteAsJsonAsync(new { success = false, message = "Access denied. Galactic Admin role required." });
- return forbiddenResponse;
- }
_logger.LogInformation("Fetching galactic geographic metrics (User: {UserEmail})", userEmail);
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGalacticPlatformMetricsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGalacticPlatformMetricsFunction.cs
index b4f1980a..81baa5c5 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGalacticPlatformMetricsFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGalacticPlatformMetricsFunction.cs
@@ -1,5 +1,4 @@
using System.Net;
-using AutopilotMonitor.Functions.Helpers;
using AutopilotMonitor.Functions.Services;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
@@ -15,16 +14,13 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
{
private readonly ILogger _logger;
private readonly PlatformMetricsService _platformMetricsService;
- private readonly GalacticAdminService _galacticAdminService;
public GetGalacticPlatformMetricsFunction(
ILogger logger,
- PlatformMetricsService platformMetricsService,
- GalacticAdminService galacticAdminService)
+ PlatformMetricsService platformMetricsService)
{
_logger = logger;
_platformMetricsService = platformMetricsService;
- _galacticAdminService = galacticAdminService;
}
[Function("GetGalacticPlatformMetrics")]
@@ -35,36 +31,7 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
try
{
- // Validate authentication
- if (!TenantHelper.IsAuthenticated(req))
- {
- _logger.LogWarning("Unauthenticated platform metrics attempt");
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Authentication required. Please provide a valid JWT token."
- });
- return unauthorizedResponse;
- }
-
- // Check if user is Galactic Admin
- var userEmail = TenantHelper.GetUserIdentifier(req);
- var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userEmail);
-
- if (!isGalacticAdmin)
- {
- _logger.LogWarning($"Non-Galactic Admin user {userEmail} attempted to access platform metrics");
- var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbiddenResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Access denied. Galactic Admin role required."
- });
- return forbiddenResponse;
- }
-
- _logger.LogInformation($"Platform agent metrics accessed by Galactic Admin: {userEmail}");
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var metrics = await _platformMetricsService.ComputePlatformMetricsAsync();
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGeographicMetricsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGeographicMetricsFunction.cs
index 64bff5e4..0dc58279 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGeographicMetricsFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGeographicMetricsFunction.cs
@@ -27,13 +27,7 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
{
try
{
- if (!TenantHelper.IsAuthenticated(req))
- {
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new { success = false, message = "Authentication required." });
- return unauthorizedResponse;
- }
-
+ // Authentication + MemberRead authorization enforced by PolicyEnforcementMiddleware
var tenantId = TenantHelper.GetTenantId(req);
_logger.LogInformation("Fetching geographic metrics for tenant {TenantId}", tenantId);
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/PlatformUsageMetricsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/PlatformUsageMetricsFunction.cs
index 963088ee..95d1c9ca 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/PlatformUsageMetricsFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/PlatformUsageMetricsFunction.cs
@@ -1,5 +1,4 @@
using System.Net;
-using AutopilotMonitor.Functions.Helpers;
using AutopilotMonitor.Functions.Services;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
@@ -14,16 +13,13 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
{
private readonly ILogger _logger;
private readonly UsageMetricsService _usageMetricsService;
- private readonly GalacticAdminService _galacticAdminService;
public PlatformUsageMetricsFunction(
ILogger logger,
- UsageMetricsService usageMetricsService,
- GalacticAdminService galacticAdminService)
+ UsageMetricsService usageMetricsService)
{
_logger = logger;
_usageMetricsService = usageMetricsService;
- _galacticAdminService = galacticAdminService;
}
///
@@ -39,36 +35,7 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
try
{
- // Validate authentication
- if (!TenantHelper.IsAuthenticated(req))
- {
- _logger.LogWarning("Unauthenticated platform usage metrics attempt");
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Authentication required. Please provide a valid JWT token."
- });
- return unauthorizedResponse;
- }
-
- // Check if user is Galactic Admin via GalacticAdminService (Azure Table Storage)
- var userEmail = TenantHelper.GetUserIdentifier(req);
- var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userEmail);
-
- if (!isGalacticAdmin)
- {
- _logger.LogWarning($"Non-Galactic Admin user {userEmail} attempted to access platform usage metrics");
- var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
- await forbiddenResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Access denied. Galactic Admin role required."
- });
- return forbiddenResponse;
- }
-
- _logger.LogInformation($"Platform usage metrics accessed by Galactic Admin: {userEmail}");
+ // Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var metrics = await _usageMetricsService.ComputeUsageMetricsAsync();
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/UsageMetricsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/UsageMetricsFunction.cs
index c8f9a1fa..ca3698cd 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/UsageMetricsFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/UsageMetricsFunction.cs
@@ -36,19 +36,7 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
try
{
- // Validate authentication
- if (!TenantHelper.IsAuthenticated(req))
- {
- _logger.LogWarning("Unauthenticated UsageMetrics attempt");
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Authentication required. Please provide a valid JWT token."
- });
- return unauthorizedResponse;
- }
-
+ // Authentication + MemberRead authorization enforced by PolicyEnforcementMiddleware
string tenantId = TenantHelper.GetTenantId(req);
string userIdentifier = TenantHelper.GetUserIdentifier(req);
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Progress/ProgressPortalFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Progress/ProgressPortalFunction.cs
index 2c6aedd1..1337bbf0 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Progress/ProgressPortalFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Progress/ProgressPortalFunction.cs
@@ -41,19 +41,7 @@ public class ProgressPortalFunction
try
{
- if (!TenantHelper.IsAuthenticated(req))
- {
- var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
- await unauthorizedResponse.WriteAsJsonAsync(new
- {
- success = false,
- message = "Authentication required. Please provide a valid JWT token.",
- count = 0,
- sessions = Array.Empty
public static class TenantHelper
{
- ///
- /// Checks if the request has an authenticated user
- /// Azure Functions Isolated Worker compatible
- ///
- /// The HTTP request
- /// True if user is authenticated, false otherwise
- public static bool IsAuthenticated(HttpRequestData req)
- {
- // Azure Functions Isolated Worker: Try FunctionContext.Items first
- // This is set by AuthenticationMiddleware and is more reliable than httpContext.User
- if (req.FunctionContext.Items.TryGetValue("ClaimsPrincipal", out var principalObj)
- && principalObj is ClaimsPrincipal principal)
- {
- return principal.Identity?.IsAuthenticated == true;
- }
-
- // Fallback to HTTP context (may not work reliably in isolated worker)
- var httpContext = req.FunctionContext.GetHttpContext();
- return httpContext?.User?.Identity?.IsAuthenticated == true;
- }
-
///
/// Extracts the tenant ID from the authenticated user's JWT token claims.
/// Uses the Azure AD tenant ID claim which identifies which customer/organization owns the data.
diff --git a/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs b/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs
index 32a1b595..6b6e7a08 100644
--- a/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs
@@ -34,9 +34,9 @@ public class AuthenticationMiddleware : IFunctionsWorkerMiddleware
private readonly object _initLock = new object();
///
- /// Routes that do not require JWT authentication.
- /// Device-to-cloud routes (sessions/register, events/ingest, agent/config) use their own
- /// certificate/hardware validation via SecurityValidator.
+ /// Routes that do not require JWT authentication (exact match).
+ /// Device-to-cloud routes use their own certificate/hardware validation via SecurityValidator.
+ /// Bootstrap device routes use bootstrap token validation via SecurityValidator.
///
private static readonly string[] _anonymousRoutes =
{
@@ -45,7 +45,21 @@ public class AuthenticationMiddleware : IFunctionsWorkerMiddleware
"/api/agent/register-session",
"/api/agent/ingest",
"/api/agent/config",
- "/api/agent/upload-url"
+ "/api/agent/upload-url",
+ "/api/agent/error",
+ "/api/bootstrap/register-session",
+ "/api/bootstrap/ingest",
+ "/api/bootstrap/config",
+ "/api/bootstrap/error",
+ };
+
+ ///
+ /// Route prefixes that do not require JWT authentication (prefix match).
+ /// Used for routes with path parameters where exact match is impossible.
+ ///
+ private static readonly string[] _anonymousPrefixes =
+ {
+ "/api/bootstrap/validate/", // /api/bootstrap/validate/{code}
};
public AuthenticationMiddleware(
@@ -244,6 +258,11 @@ public class AuthenticationMiddleware : IFunctionsWorkerMiddleware
if (path.Equals(route, StringComparison.OrdinalIgnoreCase))
return true;
}
+ foreach (var prefix in _anonymousPrefixes)
+ {
+ if (path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ return true;
+ }
return false;
}
}
diff --git a/src/Backend/AutopilotMonitor.Functions/Middleware/MemberAuthorizationMiddleware.cs b/src/Backend/AutopilotMonitor.Functions/Middleware/MemberAuthorizationMiddleware.cs
deleted file mode 100644
index e7b08771..00000000
--- a/src/Backend/AutopilotMonitor.Functions/Middleware/MemberAuthorizationMiddleware.cs
+++ /dev/null
@@ -1,128 +0,0 @@
-using System.Net;
-using System.Security.Claims;
-using AutopilotMonitor.Functions.Extensions;
-using AutopilotMonitor.Functions.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Azure.Functions.Worker;
-using Microsoft.Azure.Functions.Worker.Middleware;
-using Microsoft.Extensions.Logging;
-
-namespace AutopilotMonitor.Functions.Middleware;
-
-///
-/// Middleware that enforces tenant membership (Admin or Operator role) on protected routes.
-/// Runs after AuthenticationMiddleware. Non-members receive 403 and can only use the Progress Portal.
-///
-public class MemberAuthorizationMiddleware : IFunctionsWorkerMiddleware
-{
- private readonly ILogger _logger;
- private readonly TenantAdminsService _tenantAdminsService;
- private readonly GalacticAdminService _galacticAdminService;
-
- ///
- /// Route prefixes that are exempt from the member check.
- /// These routes either have their own authorization, use device auth, or are open to any tenant user.
- ///
- private static readonly string[] _exemptPrefixes =
- {
- "/api/health", // Anonymous health check
- "/api/stats/", // Anonymous platform stats
- "/api/agent/", // Device cert auth, no JWT
- "/api/bootstrap/device", // Bootstrap device routes use cert auth
- "/api/auth/", // Must work for any authenticated user (role info)
- "/api/progress/", // Progress Portal — open to any tenant user
- "/api/realtime/", // SignalR — tenant isolation via group validation
- "/api/galactic/", // Galactic Admin routes — have own GA checks
- "/api/global/", // Galactic Admin config — has own GA check
- "/api/versions/", // Galactic Admin — has own GA check
- "/api/preview/", // Preview whitelist — has own GA check
- };
-
- public MemberAuthorizationMiddleware(
- ILogger logger,
- TenantAdminsService tenantAdminsService,
- GalacticAdminService galacticAdminService)
- {
- _logger = logger;
- _tenantAdminsService = tenantAdminsService;
- _galacticAdminService = galacticAdminService;
- }
-
- public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
- {
- var httpContext = context.GetHttpContext();
- if (httpContext == null)
- {
- // Non-HTTP trigger (e.g. timer, queue) — no auth needed
- await next(context);
- return;
- }
-
- var requestPath = httpContext.Request.Path.Value ?? string.Empty;
-
- // Skip exempt routes
- if (IsExemptRoute(requestPath))
- {
- await next(context);
- return;
- }
-
- // Get the ClaimsPrincipal set by AuthenticationMiddleware
- if (!context.Items.TryGetValue("ClaimsPrincipal", out var principalObj)
- || principalObj is not ClaimsPrincipal principal
- || principal.Identity?.IsAuthenticated != true)
- {
- // Not authenticated — AuthenticationMiddleware will have already returned 401
- // or this is an anonymous route handled above. Just pass through.
- await next(context);
- return;
- }
-
- var tenantId = principal.GetTenantId();
- var upn = principal.GetUserPrincipalName();
-
- if (string.IsNullOrEmpty(tenantId) || string.IsNullOrEmpty(upn))
- {
- // Missing claims — let the function handle it
- await next(context);
- return;
- }
-
- // Galactic Admins bypass member check
- var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(upn);
- if (isGalacticAdmin)
- {
- await next(context);
- return;
- }
-
- // Check if user is a tenant member (Admin or Operator)
- var isMember = await _tenantAdminsService.IsTenantMemberAsync(tenantId, upn);
- if (isMember)
- {
- await next(context);
- return;
- }
-
- // Non-member — block access
- _logger.LogWarning("[MemberAuth] Non-member {Upn} (tenant {TenantId}) blocked from {Path}", upn, tenantId, requestPath);
-
- httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
- httpContext.Response.ContentType = "application/json";
- await httpContext.Response.WriteAsJsonAsync(new
- {
- error = "MembershipRequired",
- message = "Access denied. You must be an Admin or Operator for this tenant. Use the Progress Portal for session monitoring."
- });
- }
-
- private static bool IsExemptRoute(string path)
- {
- foreach (var prefix in _exemptPrefixes)
- {
- if (path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
- return true;
- }
- return false;
- }
-}
diff --git a/src/Backend/AutopilotMonitor.Functions/Middleware/PolicyEnforcementMiddleware.cs b/src/Backend/AutopilotMonitor.Functions/Middleware/PolicyEnforcementMiddleware.cs
new file mode 100644
index 00000000..ca212ecd
--- /dev/null
+++ b/src/Backend/AutopilotMonitor.Functions/Middleware/PolicyEnforcementMiddleware.cs
@@ -0,0 +1,236 @@
+using System.Net;
+using System.Security.Claims;
+using AutopilotMonitor.Functions.Extensions;
+using AutopilotMonitor.Functions.Security;
+using AutopilotMonitor.Functions.Services;
+using AutopilotMonitor.Shared;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Middleware;
+using Microsoft.Extensions.Logging;
+
+namespace AutopilotMonitor.Functions.Middleware;
+
+///
+/// Policy enforcement middleware that evaluates the EndpointAccessPolicyCatalog against each request
+/// and blocks requests that don't meet the required policy. Fail-closed: unregistered routes are denied.
+///
+/// Phase 3 of the auth refactor: replaces both PolicyAuditMiddleware (logging-only) and
+/// MemberAuthorizationMiddleware (coarse member check) with catalog-driven enforcement.
+///
+/// Middleware order: AuthenticationMiddleware → PolicyEnforcementMiddleware → Function
+///
+public class PolicyEnforcementMiddleware : IFunctionsWorkerMiddleware
+{
+ private readonly ILogger _logger;
+ private readonly GalacticAdminService _galacticAdminService;
+ private readonly TenantAdminsService _tenantAdminsService;
+
+ public PolicyEnforcementMiddleware(
+ ILogger logger,
+ GalacticAdminService galacticAdminService,
+ TenantAdminsService tenantAdminsService)
+ {
+ _logger = logger;
+ _galacticAdminService = galacticAdminService;
+ _tenantAdminsService = tenantAdminsService;
+ }
+
+ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
+ {
+ var httpContext = context.GetHttpContext();
+ if (httpContext == null)
+ {
+ // Non-HTTP trigger (e.g. timer, queue) — no auth needed
+ await next(context);
+ return;
+ }
+
+ var requestPath = httpContext.Request.Path.Value ?? string.Empty;
+ var httpMethod = httpContext.Request.Method;
+
+ // Look up the catalog policy for this route
+ var catalogEntry = EndpointAccessPolicyCatalog.FindPolicy(httpMethod, requestPath);
+
+ if (catalogEntry == null)
+ {
+ // Fail-closed: unregistered route → 403
+ _logger.LogError("[PolicyEnforcement] BLOCKED unregistered route: {Method} {Path} — not in catalog (fail-closed)",
+ httpMethod, requestPath);
+ httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ httpContext.Response.ContentType = "application/json";
+ await httpContext.Response.WriteAsJsonAsync(new
+ {
+ error = "Forbidden",
+ message = "Access denied."
+ });
+ return;
+ }
+
+ // Evaluate the catalog policy for the current user
+ var decision = await EvaluateCatalogPolicyAsync(context, catalogEntry);
+
+ if (decision.IsAllowed)
+ {
+ _logger.LogDebug("[PolicyEnforcement] ALLOW {Method} {Path} policy={Policy} user={User} role={Role}",
+ httpMethod, requestPath, catalogEntry.Policy, decision.UserIdentifier, decision.UserRole);
+ await next(context);
+ return;
+ }
+
+ // Denied — determine 401 vs 403
+ var statusCode = decision.Reason is "NoJWT" or "MissingClaims"
+ ? HttpStatusCode.Unauthorized
+ : HttpStatusCode.Forbidden;
+
+ _logger.LogWarning("[PolicyEnforcement] DENIED {Method} {Path} policy={Policy} status={Status} user={User} role={Role} reason={Reason}",
+ httpMethod, requestPath, catalogEntry.Policy, (int)statusCode,
+ decision.UserIdentifier, decision.UserRole, decision.Reason);
+
+ httpContext.Response.StatusCode = (int)statusCode;
+ httpContext.Response.ContentType = "application/json";
+
+ if (statusCode == HttpStatusCode.Unauthorized)
+ {
+ await httpContext.Response.WriteAsJsonAsync(new
+ {
+ error = "AuthenticationRequired",
+ message = "Authentication required. Please provide a valid JWT token."
+ });
+ }
+ else
+ {
+ await httpContext.Response.WriteAsJsonAsync(new
+ {
+ error = "InsufficientPermissions",
+ message = "Access denied. You do not have permission to access this resource."
+ });
+ }
+ }
+
+ private async Task EvaluateCatalogPolicyAsync(
+ FunctionContext context, EndpointPolicyEntry entry)
+ {
+ // Get the ClaimsPrincipal set by AuthenticationMiddleware (may be null for anonymous routes)
+ ClaimsPrincipal? principal = null;
+ if (context.Items.TryGetValue("ClaimsPrincipal", out var principalObj)
+ && principalObj is ClaimsPrincipal cp
+ && cp.Identity?.IsAuthenticated == true)
+ {
+ principal = cp;
+ }
+
+ var tenantId = principal?.GetTenantId();
+ var upn = principal?.GetUserPrincipalName();
+ var userIdentifier = upn ?? "anonymous";
+
+ switch (entry.Policy)
+ {
+ case EndpointPolicy.PublicAnonymous:
+ case EndpointPolicy.DeviceOrBootstrapAuth:
+ // These are always allowed at the middleware level
+ // (device auth is enforced in functions via ValidateSecurityAsync)
+ return CatalogDecisionResult.Allow(userIdentifier, "N/A", "PolicyDoesNotRequireJWT");
+
+ case EndpointPolicy.AuthenticatedUser:
+ if (principal != null)
+ return CatalogDecisionResult.Allow(userIdentifier, "Authenticated", "ValidJWT");
+ return CatalogDecisionResult.Deny(userIdentifier, "N/A", "NoJWT");
+
+ case EndpointPolicy.MemberRead:
+ return await EvaluateMemberReadAsync(tenantId, upn, userIdentifier);
+
+ case EndpointPolicy.TenantAdminOrGA:
+ return await EvaluateTenantAdminOrGAAsync(tenantId, upn, userIdentifier);
+
+ case EndpointPolicy.BootstrapManagerOrGA:
+ return await EvaluateBootstrapManagerOrGAAsync(tenantId, upn, userIdentifier);
+
+ case EndpointPolicy.GalacticAdminOnly:
+ return await EvaluateGalacticAdminOnlyAsync(upn, userIdentifier);
+
+ default:
+ return CatalogDecisionResult.Deny(userIdentifier, "N/A", $"UnknownPolicy:{entry.Policy}");
+ }
+ }
+
+ private async Task EvaluateMemberReadAsync(
+ string? tenantId, string? upn, string userIdentifier)
+ {
+ if (string.IsNullOrEmpty(tenantId) || string.IsNullOrEmpty(upn))
+ return CatalogDecisionResult.Deny(userIdentifier, "N/A", "MissingClaims");
+
+ if (await _galacticAdminService.IsGalacticAdminAsync(upn))
+ return CatalogDecisionResult.Allow(userIdentifier, "GalacticAdmin", "GABypass");
+
+ var role = await _tenantAdminsService.GetMemberRoleAsync(tenantId, upn);
+ if (role == null)
+ return CatalogDecisionResult.Deny(userIdentifier, "NonMember", "NotInTenant");
+
+ // MemberRead allows Admin, Operator, AND Viewer
+ return CatalogDecisionResult.Allow(userIdentifier, role.Role ?? "Admin", "TenantMember");
+ }
+
+ private async Task EvaluateTenantAdminOrGAAsync(
+ string? tenantId, string? upn, string userIdentifier)
+ {
+ if (string.IsNullOrEmpty(tenantId) || string.IsNullOrEmpty(upn))
+ return CatalogDecisionResult.Deny(userIdentifier, "N/A", "MissingClaims");
+
+ if (await _galacticAdminService.IsGalacticAdminAsync(upn))
+ return CatalogDecisionResult.Allow(userIdentifier, "GalacticAdmin", "GABypass");
+
+ if (await _tenantAdminsService.IsTenantAdminAsync(tenantId, upn))
+ return CatalogDecisionResult.Allow(userIdentifier, Constants.TenantRoles.Admin, "TenantAdmin");
+
+ var role = await _tenantAdminsService.GetMemberRoleAsync(tenantId, upn);
+ var roleName = role?.Role ?? "NonMember";
+ return CatalogDecisionResult.Deny(userIdentifier, roleName, "NotAdminOrGA");
+ }
+
+ private async Task EvaluateBootstrapManagerOrGAAsync(
+ string? tenantId, string? upn, string userIdentifier)
+ {
+ if (string.IsNullOrEmpty(tenantId) || string.IsNullOrEmpty(upn))
+ return CatalogDecisionResult.Deny(userIdentifier, "N/A", "MissingClaims");
+
+ if (await _galacticAdminService.IsGalacticAdminAsync(upn))
+ return CatalogDecisionResult.Allow(userIdentifier, "GalacticAdmin", "GABypass");
+
+ if (await _tenantAdminsService.CanManageBootstrapAsync(tenantId, upn))
+ return CatalogDecisionResult.Allow(userIdentifier, "BootstrapManager", "CanManageBootstrap");
+
+ var role = await _tenantAdminsService.GetMemberRoleAsync(tenantId, upn);
+ var roleName = role?.Role ?? "NonMember";
+ return CatalogDecisionResult.Deny(userIdentifier, roleName, "NoBootstrapPermission");
+ }
+
+ private async Task EvaluateGalacticAdminOnlyAsync(
+ string? upn, string userIdentifier)
+ {
+ if (string.IsNullOrEmpty(upn))
+ return CatalogDecisionResult.Deny(userIdentifier, "N/A", "MissingClaims");
+
+ if (await _galacticAdminService.IsGalacticAdminAsync(upn))
+ return CatalogDecisionResult.Allow(userIdentifier, "GalacticAdmin", "IsGA");
+
+ return CatalogDecisionResult.Deny(userIdentifier, "NonGA", "NotGalacticAdmin");
+ }
+
+ ///
+ /// Result of evaluating a catalog policy against the current request context.
+ ///
+ private sealed class CatalogDecisionResult
+ {
+ public bool IsAllowed { get; private init; }
+ public string UserIdentifier { get; private init; } = "anonymous";
+ public string UserRole { get; private init; } = "N/A";
+ public string Reason { get; private init; } = "";
+
+ public static CatalogDecisionResult Allow(string user, string role, string reason)
+ => new() { IsAllowed = true, UserIdentifier = user, UserRole = role, Reason = reason };
+
+ public static CatalogDecisionResult Deny(string user, string role, string reason)
+ => new() { IsAllowed = false, UserIdentifier = user, UserRole = role, Reason = reason };
+ }
+}
diff --git a/src/Backend/AutopilotMonitor.Functions/Program.cs b/src/Backend/AutopilotMonitor.Functions/Program.cs
index 1c0c86b2..633966ce 100644
--- a/src/Backend/AutopilotMonitor.Functions/Program.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Program.cs
@@ -19,9 +19,9 @@ var builder = FunctionsApplication.CreateBuilder(args);
builder.ConfigureFunctionsWebApplication();
// Register middleware pipeline (Azure Functions .NET 8 isolated worker pattern)
-// Order matters: authentication first, then member authorization
+// Order matters: authentication (401) → policy enforcement (403)
builder.UseMiddleware();
-builder.UseMiddleware();
+builder.UseMiddleware();
// Configure JSON serialization to use camelCase
builder.Services.Configure(options =>
diff --git a/src/Backend/AutopilotMonitor.Functions/Security/EndpointAccessPolicyCatalog.cs b/src/Backend/AutopilotMonitor.Functions/Security/EndpointAccessPolicyCatalog.cs
new file mode 100644
index 00000000..6345431d
--- /dev/null
+++ b/src/Backend/AutopilotMonitor.Functions/Security/EndpointAccessPolicyCatalog.cs
@@ -0,0 +1,230 @@
+using System.Text.RegularExpressions;
+
+namespace AutopilotMonitor.Functions.Security;
+
+///
+/// Authorization policy tiers for endpoint access control.
+/// Ordered from least restrictive to most restrictive.
+///
+public enum EndpointPolicy
+{
+ /// No authentication required. Fully public.
+ PublicAnonymous,
+
+ /// Device certificate or bootstrap token auth. No JWT.
+ DeviceOrBootstrapAuth,
+
+ /// Valid JWT token required. Any authenticated user.
+ AuthenticatedUser,
+
+ /// Tenant member with Admin, Operator, or Viewer role. Tenant-scoped read access.
+ MemberRead,
+
+ /// Tenant Admin or Galactic Admin. Tenant-scoped write access.
+ TenantAdminOrGA,
+
+ /// Admin (always) or Operator with CanManageBootstrapTokens permission, or Galactic Admin.
+ BootstrapManagerOrGA,
+
+ /// Galactic Admin only. Platform-wide access.
+ GalacticAdminOnly,
+}
+
+///
+/// A single entry in the endpoint access policy catalog.
+///
+public sealed class EndpointPolicyEntry
+{
+ public string HttpMethod { get; }
+ public string RouteTemplate { get; }
+ public EndpointPolicy Policy { get; }
+
+ // Pre-compiled regex for matching actual request paths against the route template
+ internal Regex RouteRegex { get; }
+
+ public EndpointPolicyEntry(string httpMethod, string routeTemplate, EndpointPolicy policy)
+ {
+ HttpMethod = httpMethod.ToUpperInvariant();
+ RouteTemplate = routeTemplate;
+ Policy = policy;
+ RouteRegex = BuildRouteRegex(routeTemplate);
+ }
+
+ ///
+ /// Converts a route template like "sessions/{sessionId}/events" into a regex
+ /// that matches actual paths like "sessions/abc-123/events".
+ ///
+ private static Regex BuildRouteRegex(string routeTemplate)
+ {
+ // Escape regex special chars, then replace {param} placeholders with [^/]+
+ // Note: Regex.Escape escapes { to \{ but does NOT escape } — so pattern matches \{...}
+ var escaped = Regex.Escape(routeTemplate);
+ var pattern = Regex.Replace(escaped, @"\\\{[^}]+}", "[^/]+");
+ return new Regex($"^{pattern}$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+ }
+}
+
+///
+/// Single source of truth for endpoint authorization policies.
+/// Every HTTP route must be registered here. Unregistered routes fail closed.
+///
+public static class EndpointAccessPolicyCatalog
+{
+ private static readonly EndpointPolicyEntry[] _entries =
+ {
+ // ── PublicAnonymous ─────────────────────────────────────────────
+ new("GET", "health", EndpointPolicy.PublicAnonymous),
+ new("GET", "stats/platform", EndpointPolicy.PublicAnonymous),
+ new("GET", "bootstrap/validate/{code}", EndpointPolicy.PublicAnonymous),
+
+ // ── DeviceOrBootstrapAuth ───────────────────────────────────────
+ new("POST", "agent/register-session", EndpointPolicy.DeviceOrBootstrapAuth),
+ new("POST", "agent/ingest", EndpointPolicy.DeviceOrBootstrapAuth),
+ new("GET", "agent/config", EndpointPolicy.DeviceOrBootstrapAuth),
+ new("POST", "agent/upload-url", EndpointPolicy.DeviceOrBootstrapAuth),
+ new("POST", "agent/error", EndpointPolicy.DeviceOrBootstrapAuth),
+ new("POST", "bootstrap/register-session", EndpointPolicy.DeviceOrBootstrapAuth),
+ new("POST", "bootstrap/ingest", EndpointPolicy.DeviceOrBootstrapAuth),
+ new("GET", "bootstrap/config", EndpointPolicy.DeviceOrBootstrapAuth),
+ new("POST", "bootstrap/error", EndpointPolicy.DeviceOrBootstrapAuth),
+
+ // ── AuthenticatedUser ───────────────────────────────────────────
+ new("GET", "auth/me", EndpointPolicy.AuthenticatedUser),
+ new("GET", "auth/is-galactic-admin", EndpointPolicy.AuthenticatedUser),
+ new("POST", "realtime/negotiate", EndpointPolicy.AuthenticatedUser),
+ new("POST", "realtime/groups/join", EndpointPolicy.AuthenticatedUser),
+ new("POST", "realtime/groups/leave", EndpointPolicy.AuthenticatedUser),
+ new("GET", "progress/sessions", EndpointPolicy.AuthenticatedUser),
+ new("GET", "progress/sessions/{sessionId}/events", EndpointPolicy.AuthenticatedUser),
+
+ // ── MemberRead (Admin + Operator, later + Viewer) ───────────────
+ new("GET", "sessions", EndpointPolicy.MemberRead),
+ new("GET", "sessions/{sessionId}", EndpointPolicy.MemberRead),
+ new("GET", "sessions/{sessionId}/events", EndpointPolicy.MemberRead),
+ new("GET", "sessions/{sessionId}/analysis", EndpointPolicy.MemberRead),
+ new("GET", "metrics/app", EndpointPolicy.MemberRead),
+ new("GET", "metrics/usage", EndpointPolicy.MemberRead),
+ new("GET", "metrics/geographic", EndpointPolicy.MemberRead),
+ new("GET", "audit/logs", EndpointPolicy.MemberRead),
+ new("GET", "diagnostics/download-url", EndpointPolicy.MemberRead),
+ new("GET", "rules/gather", EndpointPolicy.MemberRead),
+ new("GET", "rules/analyze", EndpointPolicy.MemberRead),
+ new("GET", "rules/ime-log-patterns", EndpointPolicy.MemberRead),
+ new("GET", "config/{tenantId}", EndpointPolicy.MemberRead),
+
+ // ── TenantAdminOrGA ─────────────────────────────────────────────
+ new("PUT", "config/{tenantId}", EndpointPolicy.TenantAdminOrGA),
+ new("POST", "config/{tenantId}", EndpointPolicy.TenantAdminOrGA),
+ new("POST", "rules/gather", EndpointPolicy.TenantAdminOrGA),
+ new("PUT", "rules/gather/{ruleId}", EndpointPolicy.TenantAdminOrGA),
+ new("DELETE", "rules/gather/{ruleId}", EndpointPolicy.TenantAdminOrGA),
+ new("POST", "rules/analyze", EndpointPolicy.TenantAdminOrGA),
+ new("PUT", "rules/analyze/{ruleId}", EndpointPolicy.TenantAdminOrGA),
+ new("DELETE", "rules/analyze/{ruleId}", EndpointPolicy.TenantAdminOrGA),
+ new("PUT", "rules/ime-log-patterns/{patternId}", EndpointPolicy.TenantAdminOrGA),
+ new("POST", "sessions/{sessionId}/mark-failed", EndpointPolicy.TenantAdminOrGA),
+ new("POST", "sessions/{sessionId}/report", EndpointPolicy.TenantAdminOrGA),
+ new("DELETE", "sessions/{sessionId}", EndpointPolicy.TenantAdminOrGA),
+ new("GET", "tenants/{tenantId}/admins", EndpointPolicy.TenantAdminOrGA),
+ new("POST", "tenants/{tenantId}/admins", EndpointPolicy.TenantAdminOrGA),
+ new("DELETE", "tenants/{tenantId}/admins/{adminUpn}", EndpointPolicy.TenantAdminOrGA),
+ new("PATCH", "tenants/{tenantId}/admins/{adminUpn}/disable", EndpointPolicy.TenantAdminOrGA),
+ new("PATCH", "tenants/{tenantId}/admins/{adminUpn}/enable", EndpointPolicy.TenantAdminOrGA),
+ new("PATCH", "tenants/{tenantId}/admins/{adminUpn}/permissions", EndpointPolicy.TenantAdminOrGA),
+ new("DELETE", "tenants/{tenantId}/offboard", EndpointPolicy.TenantAdminOrGA),
+ new("GET", "config/{tenantId}/autopilot-device-validation/consent-url", EndpointPolicy.TenantAdminOrGA),
+ new("GET", "config/{tenantId}/autopilot-device-validation/consent-status", EndpointPolicy.TenantAdminOrGA),
+
+ // ── BootstrapManagerOrGA ────────────────────────────────────────
+ new("GET", "bootstrap/sessions", EndpointPolicy.BootstrapManagerOrGA),
+ new("POST", "bootstrap/sessions", EndpointPolicy.BootstrapManagerOrGA),
+ new("DELETE", "bootstrap/sessions/{code}", EndpointPolicy.BootstrapManagerOrGA),
+
+ // ── GalacticAdminOnly ───────────────────────────────────────────
+ new("GET", "global/config", EndpointPolicy.GalacticAdminOnly),
+ new("PUT", "global/config", EndpointPolicy.GalacticAdminOnly),
+ new("POST", "global/config", EndpointPolicy.GalacticAdminOnly),
+ new("GET", "config/all", EndpointPolicy.GalacticAdminOnly),
+ new("GET", "auth/galactic-admins", EndpointPolicy.GalacticAdminOnly),
+ new("POST", "auth/galactic-admins", EndpointPolicy.GalacticAdminOnly),
+ new("DELETE", "auth/galactic-admins/{upn}", EndpointPolicy.GalacticAdminOnly),
+ new("GET", "preview/whitelist", EndpointPolicy.GalacticAdminOnly),
+ new("POST", "preview/whitelist/{tenantId}", EndpointPolicy.GalacticAdminOnly),
+ new("DELETE", "preview/whitelist/{tenantId}", EndpointPolicy.GalacticAdminOnly),
+ new("GET", "galactic/sessions", EndpointPolicy.GalacticAdminOnly),
+ new("GET", "galactic/audit/logs", EndpointPolicy.GalacticAdminOnly),
+ new("GET", "galactic/metrics/platform", EndpointPolicy.GalacticAdminOnly),
+ new("GET", "galactic/metrics/app", EndpointPolicy.GalacticAdminOnly),
+ new("GET", "galactic/metrics/geographic", EndpointPolicy.GalacticAdminOnly),
+ new("GET", "galactic/metrics/usage", EndpointPolicy.GalacticAdminOnly),
+ new("GET", "galactic/session-reports", EndpointPolicy.GalacticAdminOnly),
+ new("GET", "galactic/session-reports/download-url", EndpointPolicy.GalacticAdminOnly),
+ new("PATCH", "galactic/session-reports/{reportId}/note", EndpointPolicy.GalacticAdminOnly),
+ new("GET", "galactic/devices/blocked", EndpointPolicy.GalacticAdminOnly),
+ new("GET", "devices/blocked", EndpointPolicy.GalacticAdminOnly),
+ new("POST", "devices/block", EndpointPolicy.GalacticAdminOnly),
+ new("DELETE", "devices/block/{encodedSerialNumber}", EndpointPolicy.GalacticAdminOnly),
+ new("GET", "versions/blocked", EndpointPolicy.GalacticAdminOnly),
+ new("POST", "versions/block", EndpointPolicy.GalacticAdminOnly),
+ new("DELETE", "versions/block/{encodedPattern}", EndpointPolicy.GalacticAdminOnly),
+ new("POST", "maintenance/trigger", EndpointPolicy.GalacticAdminOnly),
+ new("GET", "health/detailed", EndpointPolicy.GalacticAdminOnly),
+ new("POST", "rules/reseed-from-github", EndpointPolicy.GalacticAdminOnly),
+ new("POST", "rules/ime-log-patterns/reseed", EndpointPolicy.GalacticAdminOnly),
+ };
+
+ ///
+ /// All registered policy entries. Used by completeness tests.
+ ///
+ public static IReadOnlyList Entries => _entries;
+
+ ///
+ /// Finds the policy for a given HTTP method and request path.
+ /// Path should include /api/ prefix (e.g., "/api/sessions/abc-123/events").
+ /// Returns null if no matching entry is found (fail-closed: caller should deny).
+ ///
+ public static EndpointPolicyEntry? FindPolicy(string httpMethod, string path)
+ {
+ // Strip /api/ prefix for matching against route templates
+ var normalizedPath = path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase)
+ ? path.Substring(5)
+ : path;
+
+ var method = httpMethod.ToUpperInvariant();
+
+ // Find the best match. Priority:
+ // 1. Literal (no {param}) matches over parameterized ones
+ // 2. Among same type, longer template wins (more specific)
+ EndpointPolicyEntry? bestMatch = null;
+ var bestIsLiteral = false;
+
+ foreach (var entry in _entries)
+ {
+ if (entry.HttpMethod != method)
+ continue;
+
+ if (!entry.RouteRegex.IsMatch(normalizedPath))
+ continue;
+
+ var isLiteral = !entry.RouteTemplate.Contains('{');
+
+ // Literal match always beats parameterized match
+ if (isLiteral && !bestIsLiteral)
+ {
+ bestMatch = entry;
+ bestIsLiteral = true;
+ }
+ else if (isLiteral == bestIsLiteral)
+ {
+ // Same category: prefer longer (more specific) template
+ if (bestMatch == null || entry.RouteTemplate.Length > bestMatch.RouteTemplate.Length)
+ {
+ bestMatch = entry;
+ bestIsLiteral = isLiteral;
+ }
+ }
+ }
+
+ return bestMatch;
+ }
+}