Merge branch 'RefactoringSecurityArchitecture'

This commit is contained in:
Oliver Kieselbach
2026-03-08 17:59:47 +01:00
56 changed files with 851 additions and 1328 deletions
@@ -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;
}
}
}
@@ -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();
@@ -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();
@@ -0,0 +1,185 @@
using System.Reflection;
using AutopilotMonitor.Functions.Middleware;
using AutopilotMonitor.Functions.Security;
using Microsoft.Azure.Functions.Worker;
namespace AutopilotMonitor.Functions.Tests;
/// <summary>
/// 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.
/// </summary>
public class EndpointPolicyCatalogCompletenessTests
{
/// <summary>
/// Every [HttpTrigger] route + method combination must have a matching catalog entry.
/// This prevents new endpoints from being deployed without an explicit policy decision.
/// </summary>
[Fact]
public void AllHttpTriggers_HaveMatchingCatalogEntry()
{
var assembly = typeof(AuthenticationMiddleware).Assembly;
var missing = new List<string>();
foreach (var type in assembly.GetTypes())
{
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance))
{
var triggerParam = method.GetParameters()
.FirstOrDefault(p => p.GetCustomAttribute<HttpTriggerAttribute>() != null);
if (triggerParam == null)
continue;
var trigger = triggerParam.GetCustomAttribute<HttpTriggerAttribute>()!;
var route = trigger.Route;
if (string.IsNullOrEmpty(route))
continue;
var httpMethods = trigger.Methods ?? Array.Empty<string>();
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}")));
}
/// <summary>
/// Every catalog entry must match at least one actual [HttpTrigger] route.
/// Prevents stale/orphaned entries that could mask security misconfigurations.
/// </summary>
[Fact]
public void AllCatalogEntries_MatchExistingRoutes()
{
var assembly = typeof(AuthenticationMiddleware).Assembly;
// Collect all actual (method, route) pairs from HttpTrigger attributes
var actualRoutes = new HashSet<string>(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<HttpTriggerAttribute>() != null);
if (triggerParam == null)
continue;
var trigger = triggerParam.GetCustomAttribute<HttpTriggerAttribute>()!;
var route = trigger.Route;
if (string.IsNullOrEmpty(route))
continue;
var httpMethods = trigger.Methods ?? Array.Empty<string>();
if (httpMethods.Length == 0)
httpMethods = new[] { "GET" };
foreach (var httpMethod in httpMethods)
{
actualRoutes.Add($"{httpMethod.ToUpper()}:{route}");
}
}
}
var orphaned = new List<string>();
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}")));
}
/// <summary>
/// Verifies that parameterized route templates correctly match actual request paths.
/// </summary>
[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);
}
/// <summary>
/// Routes that differ only by HTTP method should resolve to different policies.
/// </summary>
[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);
}
/// <summary>
/// Unregistered routes must return null (fail-closed).
/// </summary>
[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);
}
}
@@ -97,13 +97,7 @@ public class AutopilotDeviceValidationConsentFunction
private async Task<HttpResponseData?> 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);
@@ -15,16 +15,13 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
private readonly ILogger<DeviceBlockFunction> _logger;
private readonly BlockedDeviceService _blockedDeviceService;
private readonly GalacticAdminService _galacticAdminService;
public DeviceBlockFunction(
ILogger<DeviceBlockFunction> logger,
BlockedDeviceService blockedDeviceService,
GalacticAdminService galacticAdminService)
BlockedDeviceService blockedDeviceService)
{
_logger = logger;
_blockedDeviceService = blockedDeviceService;
_galacticAdminService = galacticAdminService;
}
/// <summary>GET /api/devices/blocked?tenantId={tenantId} — list all active blocks for a tenant</summary>
@@ -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<HttpResponseData> UnauthorizedAsync(HttpRequestData req)
{
var r = req.CreateResponse(HttpStatusCode.Unauthorized);
await r.WriteAsJsonAsync(new { success = false, message = "Authentication required." });
return r;
}
private static async Task<HttpResponseData> 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<HttpResponseData> BadRequestAsync(HttpRequestData req, string message)
{
var r = req.CreateResponse(HttpStatusCode.BadRequest);
@@ -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<GetAllBlockedDevicesFunction> _logger;
private readonly BlockedDeviceService _blockedDeviceService;
private readonly GalacticAdminService _galacticAdminService;
public GetAllBlockedDevicesFunction(
ILogger<GetAllBlockedDevicesFunction> logger,
BlockedDeviceService blockedDeviceService,
GalacticAdminService galacticAdminService)
BlockedDeviceService blockedDeviceService)
{
_logger = logger;
_blockedDeviceService = blockedDeviceService;
_galacticAdminService = galacticAdminService;
}
/// <summary>GET /api/galactic/devices/blocked — list all active blocks across all tenants</summary>
@@ -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();
@@ -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);
@@ -11,16 +11,13 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
private readonly ILogger<GetGalacticAuditLogsFunction> _logger;
private readonly TableStorageService _storageService;
private readonly GalacticAdminService _galacticAdminService;
public GetGalacticAuditLogsFunction(
ILogger<GetGalacticAuditLogsFunction> 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})");
@@ -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<ReseedFromGitHubFunction> 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";
@@ -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();
@@ -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();
@@ -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<TriggerMaintenanceFunction> _logger;
private readonly MaintenanceService _maintenanceService;
private readonly GalacticAdminService _galacticAdminService;
public TriggerMaintenanceFunction(
ILogger<TriggerMaintenanceFunction> logger,
MaintenanceService maintenanceService,
GalacticAdminService galacticAdminService)
MaintenanceService maintenanceService)
{
_logger = logger;
_maintenanceService = maintenanceService;
_galacticAdminService = galacticAdminService;
}
/// <summary>
@@ -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}");
@@ -15,16 +15,13 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
private readonly ILogger<VersionBlockFunction> _logger;
private readonly BlockedVersionService _blockedVersionService;
private readonly GalacticAdminService _galacticAdminService;
public VersionBlockFunction(
ILogger<VersionBlockFunction> logger,
BlockedVersionService blockedVersionService,
GalacticAdminService galacticAdminService)
BlockedVersionService blockedVersionService)
{
_logger = logger;
_blockedVersionService = blockedVersionService;
_galacticAdminService = galacticAdminService;
}
/// <summary>GET /api/versions/blocked — list all active version block rules</summary>
@@ -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<HttpResponseData> UnauthorizedAsync(HttpRequestData req)
{
var r = req.CreateResponse(HttpStatusCode.Unauthorized);
await r.WriteAsJsonAsync(new { success = false, message = "Authentication required." });
return r;
}
private static async Task<HttpResponseData> 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<HttpResponseData> BadRequestAsync(HttpRequestData req, string message)
{
var r = req.CreateResponse(HttpStatusCode.BadRequest);
@@ -21,20 +21,17 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
private readonly ILogger<CreateBootstrapSessionFunction> _logger;
private readonly BootstrapSessionService _bootstrapService;
private readonly GalacticAdminService _galacticAdminService;
private readonly TenantAdminsService _tenantAdminsService;
private readonly TenantConfigurationService _configService;
public CreateBootstrapSessionFunction(
ILogger<CreateBootstrapSessionFunction> 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)
@@ -20,20 +20,17 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
private readonly ILogger<ListBootstrapSessionsFunction> _logger;
private readonly BootstrapSessionService _bootstrapService;
private readonly GalacticAdminService _galacticAdminService;
private readonly TenantAdminsService _tenantAdminsService;
private readonly TenantConfigurationService _configService;
public ListBootstrapSessionsFunction(
ILogger<ListBootstrapSessionsFunction> 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)
@@ -18,20 +18,17 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
private readonly ILogger<RevokeBootstrapSessionFunction> _logger;
private readonly BootstrapSessionService _bootstrapService;
private readonly GalacticAdminService _galacticAdminService;
private readonly TenantAdminsService _tenantAdminsService;
private readonly TenantConfigurationService _configService;
public RevokeBootstrapSessionFunction(
ILogger<RevokeBootstrapSessionFunction> 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)
@@ -13,16 +13,13 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
private readonly ILogger<GetAdminConfigurationFunction> _logger;
private readonly AdminConfigurationService _adminConfigService;
private readonly GalacticAdminService _galacticAdminService;
public GetAdminConfigurationFunction(
ILogger<GetAdminConfigurationFunction> 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();
@@ -13,16 +13,13 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
private readonly ILogger<GetAllTenantConfigurationsFunction> _logger;
private readonly TenantConfigurationService _configService;
private readonly GalacticAdminService _galacticAdminService;
public GetAllTenantConfigurationsFunction(
ILogger<GetAllTenantConfigurationsFunction> 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();
@@ -13,13 +13,16 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
private readonly ILogger<GetTenantConfigurationFunction> _logger;
private readonly TenantConfigurationService _configService;
private readonly GalacticAdminService _galacticAdminService;
public GetTenantConfigurationFunction(
ILogger<GetTenantConfigurationFunction> 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}");
@@ -16,16 +16,13 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
private readonly ILogger<UpdateAdminConfigurationFunction> _logger;
private readonly AdminConfigurationService _adminConfigService;
private readonly GalacticAdminService _galacticAdminService;
public UpdateAdminConfigurationFunction(
ILogger<UpdateAdminConfigurationFunction> 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
@@ -16,18 +16,15 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
private readonly ILogger<UpdateTenantConfigurationFunction> _logger;
private readonly TenantConfigurationService _configService;
private readonly TenantAdminsService _tenantAdminsService;
private readonly GalacticAdminService _galacticAdminService;
public UpdateTenantConfigurationFunction(
ILogger<UpdateTenantConfigurationFunction> 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)
@@ -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);
@@ -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<AddGalacticAdminRequest>();
@@ -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))
@@ -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<HealthCheckFunction> _logger;
private readonly HealthCheckService _healthCheckService;
private readonly GalacticAdminService _galacticAdminService;
public HealthCheckFunction(
ILogger<HealthCheckFunction> logger,
HealthCheckService healthCheckService,
GalacticAdminService galacticAdminService)
HealthCheckService healthCheckService)
{
_logger = logger;
_healthCheckService = healthCheckService;
_galacticAdminService = galacticAdminService;
}
/// <summary>
@@ -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();
@@ -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)
@@ -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)
@@ -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}");
@@ -11,16 +11,13 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
{
private readonly ILogger<GetGalacticAppMetricsFunction> _logger;
private readonly TableStorageService _storageService;
private readonly GalacticAdminService _galacticAdminService;
public GetGalacticAppMetricsFunction(
ILogger<GetGalacticAppMetricsFunction> 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})");
@@ -11,16 +11,13 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
{
private readonly ILogger<GetGalacticGeographicMetricsFunction> _logger;
private readonly TableStorageService _storageService;
private readonly GalacticAdminService _galacticAdminService;
public GetGalacticGeographicMetricsFunction(
ILogger<GetGalacticGeographicMetricsFunction> 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);
@@ -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<GetGalacticPlatformMetricsFunction> _logger;
private readonly PlatformMetricsService _platformMetricsService;
private readonly GalacticAdminService _galacticAdminService;
public GetGalacticPlatformMetricsFunction(
ILogger<GetGalacticPlatformMetricsFunction> 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();
@@ -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);
@@ -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<PlatformUsageMetricsFunction> _logger;
private readonly UsageMetricsService _usageMetricsService;
private readonly GalacticAdminService _galacticAdminService;
public PlatformUsageMetricsFunction(
ILogger<PlatformUsageMetricsFunction> logger,
UsageMetricsService usageMetricsService,
GalacticAdminService galacticAdminService)
UsageMetricsService usageMetricsService)
{
_logger = logger;
_usageMetricsService = usageMetricsService;
_galacticAdminService = galacticAdminService;
}
/// <summary>
@@ -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();
@@ -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);
@@ -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<object>()
});
return unauthorizedResponse;
}
// Authentication + AuthenticatedUser authorization enforced by PolicyEnforcementMiddleware
var tenantId = TenantHelper.GetTenantId(req);
_logger.LogInformation("ProgressGetSessions: Fetching sessions for tenant {TenantId}", tenantId);
@@ -106,20 +94,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.",
sessionId = sessionId,
count = 0,
events = Array.Empty<object>()
});
return unauthorizedResponse;
}
// Authentication + AuthenticatedUser authorization enforced by PolicyEnforcementMiddleware
var userTenantId = TenantHelper.GetTenantId(req);
var userIdentifier = TenantHelper.GetUserIdentifier(req);
@@ -1,7 +1,6 @@
using System.Net;
using System.Web;
using Azure.Storage.Blobs;
using AutopilotMonitor.Functions.Helpers;
using AutopilotMonitor.Functions.Services;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
@@ -17,17 +16,14 @@ namespace AutopilotMonitor.Functions.Functions.Reports
public class GetSessionReportDownloadUrlFunction
{
private readonly ILogger<GetSessionReportDownloadUrlFunction> _logger;
private readonly GalacticAdminService _galacticAdminService;
private readonly string _blobConnectionString;
private const string ContainerName = "session-reports";
public GetSessionReportDownloadUrlFunction(
ILogger<GetSessionReportDownloadUrlFunction> logger,
GalacticAdminService galacticAdminService,
IConfiguration configuration)
{
_logger = logger;
_galacticAdminService = galacticAdminService;
_blobConnectionString = configuration["AzureBlobStorageConnectionString"]
?? throw new InvalidOperationException("AzureBlobStorageConnectionString is not configured");
}
@@ -38,21 +34,7 @@ namespace AutopilotMonitor.Functions.Functions.Reports
{
try
{
if (!TenantHelper.IsAuthenticated(req))
{
var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorizedResponse.WriteAsJsonAsync(new { success = false, message = "Authentication required." });
return unauthorizedResponse;
}
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
if (!isGalacticAdmin)
{
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 query = HttpUtility.ParseQueryString(req.Url.Query);
var blobName = query["blobName"];
@@ -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;
@@ -11,16 +10,13 @@ namespace AutopilotMonitor.Functions.Functions.Reports
{
private readonly ILogger<GetSessionReportsFunction> _logger;
private readonly SessionReportService _sessionReportService;
private readonly GalacticAdminService _galacticAdminService;
public GetSessionReportsFunction(
ILogger<GetSessionReportsFunction> logger,
SessionReportService sessionReportService,
GalacticAdminService galacticAdminService)
SessionReportService sessionReportService)
{
_logger = logger;
_sessionReportService = sessionReportService;
_galacticAdminService = galacticAdminService;
}
[Function("GetSessionReports")]
@@ -31,32 +27,7 @@ namespace AutopilotMonitor.Functions.Functions.Reports
try
{
// Validate authentication
if (!TenantHelper.IsAuthenticated(req))
{
var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorizedResponse.WriteAsJsonAsync(new
{
success = false,
message = "Authentication required."
});
return unauthorizedResponse;
}
string userIdentifier = TenantHelper.GetUserIdentifier(req);
// Require Galactic Admin only (this is admin-config data)
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
if (!isGalacticAdmin)
{
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 reports = await _sessionReportService.GetAllReportsAsync();
@@ -13,7 +13,6 @@ namespace AutopilotMonitor.Functions.Functions.Reports
private readonly ILogger<SubmitSessionReportFunction> _logger;
private readonly SessionReportService _sessionReportService;
private readonly TableStorageService _storageService;
private readonly TenantAdminsService _tenantAdminsService;
private readonly GalacticAdminService _galacticAdminService;
private readonly TelegramNotificationService _telegramNotificationService;
@@ -21,14 +20,12 @@ namespace AutopilotMonitor.Functions.Functions.Reports
ILogger<SubmitSessionReportFunction> logger,
SessionReportService sessionReportService,
TableStorageService storageService,
TenantAdminsService tenantAdminsService,
GalacticAdminService galacticAdminService,
TelegramNotificationService telegramNotificationService)
{
_logger = logger;
_sessionReportService = sessionReportService;
_storageService = storageService;
_tenantAdminsService = tenantAdminsService;
_galacticAdminService = galacticAdminService;
_telegramNotificationService = telegramNotificationService;
}
@@ -42,36 +39,12 @@ namespace AutopilotMonitor.Functions.Functions.Reports
try
{
// Validate authentication
if (!TenantHelper.IsAuthenticated(req))
{
_logger.LogWarning("Unauthenticated session report attempt for session {SessionId}", sessionId);
var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorizedResponse.WriteAsJsonAsync(new
{
success = false,
message = "Authentication required. Please provide a valid JWT token."
});
return unauthorizedResponse;
}
// Authentication + TenantAdminOrGA authorization enforced by PolicyEnforcementMiddleware
string tenantId = TenantHelper.GetTenantId(req);
string userIdentifier = TenantHelper.GetUserIdentifier(req);
// Require Tenant Admin or Galactic Admin
// Still needed: GA check for audit log skip logic
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
_logger.LogWarning("Non-admin user {UserIdentifier} attempted to submit session report for {SessionId}", userIdentifier, sessionId);
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new
{
success = false,
message = "Access denied. Tenant Admin or Galactic Admin role required."
});
return forbiddenResponse;
}
// Parse request body
var request = await req.ReadFromJsonAsync<SubmitSessionReportRequest>();
@@ -16,16 +16,13 @@ namespace AutopilotMonitor.Functions.Functions.Reports
{
private readonly ILogger<UpdateSessionReportNoteFunction> _logger;
private readonly SessionReportService _sessionReportService;
private readonly GalacticAdminService _galacticAdminService;
public UpdateSessionReportNoteFunction(
ILogger<UpdateSessionReportNoteFunction> logger,
SessionReportService sessionReportService,
GalacticAdminService galacticAdminService)
SessionReportService sessionReportService)
{
_logger = logger;
_sessionReportService = sessionReportService;
_galacticAdminService = galacticAdminService;
}
[Function("UpdateSessionReportNote")]
@@ -35,21 +32,8 @@ namespace AutopilotMonitor.Functions.Functions.Reports
{
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 userIdentifier = TenantHelper.GetUserIdentifier(req);
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
if (!isGalacticAdmin)
{
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new { success = false, message = "Access denied. Galactic Admin role required." });
return forbiddenResponse;
}
if (string.IsNullOrEmpty(reportId))
{
@@ -16,32 +16,21 @@ namespace AutopilotMonitor.Functions.Functions.Rules
{
private readonly ILogger<AnalyzeRulesFunction> _logger;
private readonly AnalyzeRuleService _ruleService;
private readonly TenantAdminsService _tenantAdminsService;
private readonly GalacticAdminService _galacticAdminService;
public AnalyzeRulesFunction(
ILogger<AnalyzeRulesFunction> logger,
AnalyzeRuleService ruleService,
TenantAdminsService tenantAdminsService,
GalacticAdminService galacticAdminService)
AnalyzeRuleService ruleService)
{
_logger = logger;
_ruleService = ruleService;
_tenantAdminsService = tenantAdminsService;
_galacticAdminService = galacticAdminService;
}
[Function("GetAnalyzeRules")]
public async Task<HttpResponseData> GetRules(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "rules/analyze")] HttpRequestData req)
{
var tenantId = TenantHelper.IsAuthenticated(req) ? TenantHelper.GetTenantId(req) : null;
if (string.IsNullOrEmpty(tenantId))
{
var unauthorized = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorized.WriteAsJsonAsync(new { success = false, message = "Authentication required" });
return unauthorized;
}
// Authentication + MemberRead authorization enforced by PolicyEnforcementMiddleware
var tenantId = TenantHelper.GetTenantId(req);
var rules = await _ruleService.GetAllRulesForTenantAsync(tenantId);
@@ -54,24 +43,8 @@ namespace AutopilotMonitor.Functions.Functions.Rules
public async Task<HttpResponseData> CreateRule(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "rules/analyze")] HttpRequestData req)
{
var tenantId = TenantHelper.IsAuthenticated(req) ? TenantHelper.GetTenantId(req) : null;
if (string.IsNullOrEmpty(tenantId))
{
var unauthorized = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorized.WriteAsJsonAsync(new { success = false, message = "Authentication required" });
return unauthorized;
}
// Require Tenant Admin or Galactic Admin for rule modifications
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
return forbidden;
}
// Authentication + TenantAdminOrGA authorization enforced by PolicyEnforcementMiddleware
var tenantId = TenantHelper.GetTenantId(req);
if (req.Headers.TryGetValues("Content-Length", out var clValues)
&& long.TryParse(clValues.FirstOrDefault(), out var contentLength)
@@ -103,24 +76,8 @@ namespace AutopilotMonitor.Functions.Functions.Rules
[HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "rules/analyze/{ruleId}")] HttpRequestData req,
string ruleId)
{
var tenantId = TenantHelper.IsAuthenticated(req) ? TenantHelper.GetTenantId(req) : null;
if (string.IsNullOrEmpty(tenantId))
{
var unauthorized = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorized.WriteAsJsonAsync(new { success = false, message = "Authentication required" });
return unauthorized;
}
// Require Tenant Admin or Galactic Admin for rule modifications
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
return forbidden;
}
// Authentication + TenantAdminOrGA authorization enforced by PolicyEnforcementMiddleware
var tenantId = TenantHelper.GetTenantId(req);
if (req.Headers.TryGetValues("Content-Length", out var clValues2)
&& long.TryParse(clValues2.FirstOrDefault(), out var contentLength2)
@@ -153,24 +110,8 @@ namespace AutopilotMonitor.Functions.Functions.Rules
[HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "rules/analyze/{ruleId}")] HttpRequestData req,
string ruleId)
{
var tenantId = TenantHelper.IsAuthenticated(req) ? TenantHelper.GetTenantId(req) : null;
if (string.IsNullOrEmpty(tenantId))
{
var unauthorized = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorized.WriteAsJsonAsync(new { success = false, message = "Authentication required" });
return unauthorized;
}
// Require Tenant Admin or Galactic Admin for rule modifications
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
return forbidden;
}
// Authentication + TenantAdminOrGA authorization enforced by PolicyEnforcementMiddleware
var tenantId = TenantHelper.GetTenantId(req);
// Load the rule to determine its type (built-in/community vs. custom)
var rules = await _ruleService.GetAllRulesForTenantAsync(tenantId);
@@ -16,32 +16,21 @@ namespace AutopilotMonitor.Functions.Functions.Rules
{
private readonly ILogger<GatherRulesFunction> _logger;
private readonly GatherRuleService _ruleService;
private readonly TenantAdminsService _tenantAdminsService;
private readonly GalacticAdminService _galacticAdminService;
public GatherRulesFunction(
ILogger<GatherRulesFunction> logger,
GatherRuleService ruleService,
TenantAdminsService tenantAdminsService,
GalacticAdminService galacticAdminService)
GatherRuleService ruleService)
{
_logger = logger;
_ruleService = ruleService;
_tenantAdminsService = tenantAdminsService;
_galacticAdminService = galacticAdminService;
}
[Function("GetGatherRules")]
public async Task<HttpResponseData> GetRules(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "rules/gather")] HttpRequestData req)
{
var tenantId = TenantHelper.IsAuthenticated(req) ? TenantHelper.GetTenantId(req) : null;
if (string.IsNullOrEmpty(tenantId))
{
var unauthorized = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorized.WriteAsJsonAsync(new { success = false, message = "Authentication required" });
return unauthorized;
}
// Authentication + MemberRead authorization enforced by PolicyEnforcementMiddleware
var tenantId = TenantHelper.GetTenantId(req);
var rules = await _ruleService.GetAllRulesForTenantAsync(tenantId);
@@ -54,24 +43,8 @@ namespace AutopilotMonitor.Functions.Functions.Rules
public async Task<HttpResponseData> CreateRule(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "rules/gather")] HttpRequestData req)
{
var tenantId = TenantHelper.IsAuthenticated(req) ? TenantHelper.GetTenantId(req) : null;
if (string.IsNullOrEmpty(tenantId))
{
var unauthorized = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorized.WriteAsJsonAsync(new { success = false, message = "Authentication required" });
return unauthorized;
}
// Require Tenant Admin or Galactic Admin for rule modifications
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
return forbidden;
}
// Authentication + TenantAdminOrGA authorization enforced by PolicyEnforcementMiddleware
var tenantId = TenantHelper.GetTenantId(req);
if (req.Headers.TryGetValues("Content-Length", out var clValues)
&& long.TryParse(clValues.FirstOrDefault(), out var contentLength)
@@ -103,24 +76,8 @@ namespace AutopilotMonitor.Functions.Functions.Rules
[HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "rules/gather/{ruleId}")] HttpRequestData req,
string ruleId)
{
var tenantId = TenantHelper.IsAuthenticated(req) ? TenantHelper.GetTenantId(req) : null;
if (string.IsNullOrEmpty(tenantId))
{
var unauthorized = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorized.WriteAsJsonAsync(new { success = false, message = "Authentication required" });
return unauthorized;
}
// Require Tenant Admin or Galactic Admin for rule modifications
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
return forbidden;
}
// Authentication + TenantAdminOrGA authorization enforced by PolicyEnforcementMiddleware
var tenantId = TenantHelper.GetTenantId(req);
if (req.Headers.TryGetValues("Content-Length", out var clValues2)
&& long.TryParse(clValues2.FirstOrDefault(), out var contentLength2)
@@ -153,24 +110,8 @@ namespace AutopilotMonitor.Functions.Functions.Rules
[HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "rules/gather/{ruleId}")] HttpRequestData req,
string ruleId)
{
var tenantId = TenantHelper.IsAuthenticated(req) ? TenantHelper.GetTenantId(req) : null;
if (string.IsNullOrEmpty(tenantId))
{
var unauthorized = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorized.WriteAsJsonAsync(new { success = false, message = "Authentication required" });
return unauthorized;
}
// Require Tenant Admin or Galactic Admin for rule modifications
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
return forbidden;
}
// Authentication + TenantAdminOrGA authorization enforced by PolicyEnforcementMiddleware
var tenantId = TenantHelper.GetTenantId(req);
// Load the rule to determine its type (built-in/community vs. custom)
var rules = await _ruleService.GetAllRulesForTenantAsync(tenantId);
@@ -37,13 +37,7 @@ namespace AutopilotMonitor.Functions.Functions.Rules
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "sessions/{sessionId}/analysis")] HttpRequestData req,
string sessionId)
{
if (!TenantHelper.IsAuthenticated(req))
{
var unauthorized = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorized.WriteAsJsonAsync(new { success = false, message = "Authentication required" });
return unauthorized;
}
// Authentication + MemberRead authorization enforced by PolicyEnforcementMiddleware
var userTenantId = TenantHelper.GetTenantId(req);
var userIdentifier = TenantHelper.GetUserIdentifier(req);
@@ -17,26 +17,19 @@ namespace AutopilotMonitor.Functions.Functions.Rules
{
private readonly ILogger<ImeLogPatternsFunction> _logger;
private readonly ImeLogPatternService _patternService;
private readonly GalacticAdminService _galacticAdminService;
public ImeLogPatternsFunction(ILogger<ImeLogPatternsFunction> logger, ImeLogPatternService patternService, GalacticAdminService galacticAdminService)
public ImeLogPatternsFunction(ILogger<ImeLogPatternsFunction> logger, ImeLogPatternService patternService)
{
_logger = logger;
_patternService = patternService;
_galacticAdminService = galacticAdminService;
}
[Function("GetImeLogPatterns")]
public async Task<HttpResponseData> GetPatterns(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "rules/ime-log-patterns")] HttpRequestData req)
{
var tenantId = TenantHelper.IsAuthenticated(req) ? TenantHelper.GetTenantId(req) : null;
if (string.IsNullOrEmpty(tenantId))
{
var unauthorized = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorized.WriteAsJsonAsync(new { success = false, message = "Authentication required" });
return unauthorized;
}
// Authentication + MemberRead authorization enforced by PolicyEnforcementMiddleware
var tenantId = TenantHelper.GetTenantId(req);
var patterns = await _patternService.GetAllPatternsForTenantAsync(tenantId);
@@ -50,29 +43,14 @@ namespace AutopilotMonitor.Functions.Functions.Rules
[HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "rules/ime-log-patterns/{patternId}")] HttpRequestData req,
string patternId)
{
var tenantId = TenantHelper.IsAuthenticated(req) ? TenantHelper.GetTenantId(req) : null;
if (string.IsNullOrEmpty(tenantId))
{
var unauthorized = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorized.WriteAsJsonAsync(new { success = false, message = "Authentication required" });
return unauthorized;
}
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Phase 1: Only global edits by Galactic Admins
// Phase 1: Only global edits
var globalEdit = req.Url.Query.Contains("global=true", StringComparison.OrdinalIgnoreCase);
if (!globalEdit)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { success = false, message = "IME log pattern edits require ?global=true and Galactic Admin privileges" });
return forbidden;
}
var upn = TenantHelper.GetUserIdentifier(req);
var isGalactic = await _galacticAdminService.IsGalacticAdminAsync(upn);
if (!isGalactic)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { success = false, message = "Galactic Admin privileges required" });
await forbidden.WriteAsJsonAsync(new { success = false, message = "IME log pattern edits require ?global=true" });
return forbidden;
}
@@ -108,20 +86,8 @@ namespace AutopilotMonitor.Functions.Functions.Rules
{
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;
}
_logger.LogInformation($"Reseed IME log patterns triggered by Galactic Admin {upn}");
@@ -17,20 +17,17 @@ public class PreviewWhitelistFunction
{
private readonly ILogger<PreviewWhitelistFunction> _logger;
private readonly PreviewWhitelistService _previewWhitelistService;
private readonly GalacticAdminService _galacticAdminService;
private readonly TenantConfigurationService _tenantConfigurationService;
private readonly TenantAdminsService _tenantAdminsService;
public PreviewWhitelistFunction(
ILogger<PreviewWhitelistFunction> logger,
PreviewWhitelistService previewWhitelistService,
GalacticAdminService galacticAdminService,
TenantConfigurationService tenantConfigurationService,
TenantAdminsService tenantAdminsService)
{
_logger = logger;
_previewWhitelistService = previewWhitelistService;
_galacticAdminService = galacticAdminService;
_tenantConfigurationService = tenantConfigurationService;
_tenantAdminsService = tenantAdminsService;
}
@@ -45,17 +42,7 @@ public class PreviewWhitelistFunction
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "preview/whitelist")] HttpRequestData req,
FunctionContext context)
{
var principal = context.GetUser();
if (principal == null)
return req.CreateResponse(HttpStatusCode.Unauthorized);
var upn = principal.GetUserPrincipalName();
if (!await _galacticAdminService.IsGalacticAdminAsync(upn))
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { error = "Only Galactic Admins can access this endpoint" });
return forbidden;
}
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var approved = await _previewWhitelistService.GetAllApprovedAsync();
@@ -75,17 +62,9 @@ public class PreviewWhitelistFunction
string tenantId,
FunctionContext context)
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var principal = context.GetUser();
if (principal == null)
return req.CreateResponse(HttpStatusCode.Unauthorized);
var upn = principal.GetUserPrincipalName();
if (!await _galacticAdminService.IsGalacticAdminAsync(upn))
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { error = "Only Galactic Admins can approve tenants" });
return forbidden;
}
var upn = principal?.GetUserPrincipalName();
if (string.IsNullOrWhiteSpace(tenantId))
{
@@ -156,17 +135,9 @@ public class PreviewWhitelistFunction
string tenantId,
FunctionContext context)
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var principal = context.GetUser();
if (principal == null)
return req.CreateResponse(HttpStatusCode.Unauthorized);
var upn = principal.GetUserPrincipalName();
if (!await _galacticAdminService.IsGalacticAdminAsync(upn))
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { error = "Only Galactic Admins can revoke tenant access" });
return forbidden;
}
var upn = principal?.GetUserPrincipalName();
await _previewWhitelistService.RevokeAsync(tenantId);
@@ -11,19 +11,13 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
{
private readonly ILogger<DeleteSessionFunction> _logger;
private readonly TableStorageService _storageService;
private readonly TenantAdminsService _tenantAdminsService;
private readonly GalacticAdminService _galacticAdminService;
public DeleteSessionFunction(
ILogger<DeleteSessionFunction> logger,
TableStorageService storageService,
TenantAdminsService tenantAdminsService,
GalacticAdminService galacticAdminService)
TableStorageService storageService)
{
_logger = logger;
_storageService = storageService;
_tenantAdminsService = tenantAdminsService;
_galacticAdminService = galacticAdminService;
}
[Function("DeleteSession")]
@@ -35,37 +29,10 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
try
{
// Validate authentication
if (!TenantHelper.IsAuthenticated(req))
{
_logger.LogWarning($"Unauthenticated DeleteSession attempt for session {sessionId}");
var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorizedResponse.WriteAsJsonAsync(new
{
success = false,
message = "Authentication required. Please provide a valid JWT token."
});
return unauthorizedResponse;
}
// Authentication + TenantAdminOrGA authorization enforced by PolicyEnforcementMiddleware
var tenantId = TenantHelper.GetTenantId(req);
var userIdentifier = TenantHelper.GetUserIdentifier(req);
// Require Tenant Admin or Galactic Admin for destructive operations
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
_logger.LogWarning($"Non-admin user {userIdentifier} attempted to delete session {sessionId}");
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($"Deleting session {sessionId} for tenant {tenantId} by user {userIdentifier}");
// Delete all related data for this session
@@ -11,16 +11,13 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
{
private readonly ILogger<GetAllSessionsFunction> _logger;
private readonly TableStorageService _storageService;
private readonly GalacticAdminService _galacticAdminService;
public GetAllSessionsFunction(
ILogger<GetAllSessionsFunction> logger,
TableStorageService storageService,
GalacticAdminService galacticAdminService)
TableStorageService storageService)
{
_logger = logger;
_storageService = storageService;
_galacticAdminService = galacticAdminService;
}
[Function("GetAllSessions")]
@@ -31,34 +28,8 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
try
{
// Validate authentication and Galactic Admin role
if (!TenantHelper.IsAuthenticated(req))
{
_logger.LogWarning("Unauthenticated GetAllSessions 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 access GetAllSessions");
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 sessions across all tenants (User: {userEmail})");
@@ -41,21 +41,7 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
try
{
// Validate authentication
if (!TenantHelper.IsAuthenticated(req))
{
_logger.LogWarning($"{sessionPrefix} Unauthenticated GetSessionEvents attempt");
var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorizedResponse.WriteAsJsonAsync(new
{
success = false,
message = "Authentication required. Please provide a valid JWT token.",
sessionId = sessionId,
count = 0,
events = Array.Empty<object>()
});
return unauthorizedResponse;
}
// Authentication + MemberRead authorization enforced by PolicyEnforcementMiddleware
// Get user's tenant ID and identifier from JWT
var userTenantId = TenantHelper.GetTenantId(req);
@@ -44,18 +44,7 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
try
{
if (!TenantHelper.IsAuthenticated(req))
{
_logger.LogWarning($"{sessionPrefix} Unauthenticated GetSession 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
var userTenantId = TenantHelper.GetTenantId(req);
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var query = HttpUtility.ParseQueryString(req.Url.Query);
@@ -26,21 +26,7 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
try
{
// Validate authentication
if (!TenantHelper.IsAuthenticated(req))
{
_logger.LogWarning("Unauthenticated GetSessions attempt");
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<object>()
});
return unauthorizedResponse;
}
// Authentication + MemberRead authorization enforced by PolicyEnforcementMiddleware
var tenantId = TenantHelper.GetTenantId(req);
_logger.LogInformation($"Fetching sessions for tenant {tenantId}");
@@ -13,19 +13,13 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
{
private readonly ILogger<MarkSessionFailedFunction> _logger;
private readonly TableStorageService _storageService;
private readonly TenantAdminsService _tenantAdminsService;
private readonly GalacticAdminService _galacticAdminService;
public MarkSessionFailedFunction(
ILogger<MarkSessionFailedFunction> logger,
TableStorageService storageService,
TenantAdminsService tenantAdminsService,
GalacticAdminService galacticAdminService)
TableStorageService storageService)
{
_logger = logger;
_storageService = storageService;
_tenantAdminsService = tenantAdminsService;
_galacticAdminService = galacticAdminService;
}
[Function("MarkSessionFailed")]
@@ -37,37 +31,10 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
try
{
// Validate authentication
if (!TenantHelper.IsAuthenticated(req))
{
_logger.LogWarning($"Unauthenticated MarkSessionFailed attempt for session {sessionId}");
var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
await unauthorizedResponse.WriteAsJsonAsync(new
{
success = false,
message = "Authentication required. Please provide a valid JWT token."
});
return new MarkSessionFailedOutput { HttpResponse = unauthorizedResponse };
}
// Authentication + TenantAdminOrGA authorization enforced by PolicyEnforcementMiddleware
string tenantId = TenantHelper.GetTenantId(req);
string userIdentifier = TenantHelper.GetUserIdentifier(req);
// Require Tenant Admin or Galactic Admin for status modifications
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
{
_logger.LogWarning($"Non-admin user {userIdentifier} attempted to mark session {sessionId} as failed");
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new
{
success = false,
message = "Access denied. Tenant Admin or Galactic Admin role required."
});
return new MarkSessionFailedOutput { HttpResponse = forbiddenResponse };
}
_logger.LogInformation($"Marking session {sessionId} as failed for tenant {tenantId} by user {userIdentifier}");
// Update session status to Failed with manual failure reason
@@ -9,27 +9,6 @@ namespace AutopilotMonitor.Functions.Helpers;
/// </summary>
public static class TenantHelper
{
/// <summary>
/// Checks if the request has an authenticated user
/// Azure Functions Isolated Worker compatible
/// </summary>
/// <param name="req">The HTTP request</param>
/// <returns>True if user is authenticated, false otherwise</returns>
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;
}
/// <summary>
/// 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.
@@ -34,9 +34,9 @@ public class AuthenticationMiddleware : IFunctionsWorkerMiddleware
private readonly object _initLock = new object();
/// <summary>
/// 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.
/// </summary>
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",
};
/// <summary>
/// Route prefixes that do not require JWT authentication (prefix match).
/// Used for routes with path parameters where exact match is impossible.
/// </summary>
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;
}
}
@@ -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;
/// <summary>
/// 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.
/// </summary>
public class MemberAuthorizationMiddleware : IFunctionsWorkerMiddleware
{
private readonly ILogger<MemberAuthorizationMiddleware> _logger;
private readonly TenantAdminsService _tenantAdminsService;
private readonly GalacticAdminService _galacticAdminService;
/// <summary>
/// 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.
/// </summary>
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<MemberAuthorizationMiddleware> 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;
}
}
@@ -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;
/// <summary>
/// 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
/// </summary>
public class PolicyEnforcementMiddleware : IFunctionsWorkerMiddleware
{
private readonly ILogger<PolicyEnforcementMiddleware> _logger;
private readonly GalacticAdminService _galacticAdminService;
private readonly TenantAdminsService _tenantAdminsService;
public PolicyEnforcementMiddleware(
ILogger<PolicyEnforcementMiddleware> 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<CatalogDecisionResult> 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<CatalogDecisionResult> 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<CatalogDecisionResult> 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<CatalogDecisionResult> 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<CatalogDecisionResult> 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");
}
/// <summary>
/// Result of evaluating a catalog policy against the current request context.
/// </summary>
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 };
}
}
@@ -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<AuthenticationMiddleware>();
builder.UseMiddleware<MemberAuthorizationMiddleware>();
builder.UseMiddleware<PolicyEnforcementMiddleware>();
// Configure JSON serialization to use camelCase
builder.Services.Configure<JsonSerializerOptions>(options =>
@@ -0,0 +1,230 @@
using System.Text.RegularExpressions;
namespace AutopilotMonitor.Functions.Security;
/// <summary>
/// Authorization policy tiers for endpoint access control.
/// Ordered from least restrictive to most restrictive.
/// </summary>
public enum EndpointPolicy
{
/// <summary>No authentication required. Fully public.</summary>
PublicAnonymous,
/// <summary>Device certificate or bootstrap token auth. No JWT.</summary>
DeviceOrBootstrapAuth,
/// <summary>Valid JWT token required. Any authenticated user.</summary>
AuthenticatedUser,
/// <summary>Tenant member with Admin, Operator, or Viewer role. Tenant-scoped read access.</summary>
MemberRead,
/// <summary>Tenant Admin or Galactic Admin. Tenant-scoped write access.</summary>
TenantAdminOrGA,
/// <summary>Admin (always) or Operator with CanManageBootstrapTokens permission, or Galactic Admin.</summary>
BootstrapManagerOrGA,
/// <summary>Galactic Admin only. Platform-wide access.</summary>
GalacticAdminOnly,
}
/// <summary>
/// A single entry in the endpoint access policy catalog.
/// </summary>
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);
}
/// <summary>
/// Converts a route template like "sessions/{sessionId}/events" into a regex
/// that matches actual paths like "sessions/abc-123/events".
/// </summary>
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);
}
}
/// <summary>
/// Single source of truth for endpoint authorization policies.
/// Every HTTP route must be registered here. Unregistered routes fail closed.
/// </summary>
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),
};
/// <summary>
/// All registered policy entries. Used by completeness tests.
/// </summary>
public static IReadOnlyList<EndpointPolicyEntry> Entries => _entries;
/// <summary>
/// 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).
/// </summary>
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;
}
}