mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 13:10:13 +00:00
Merge branch 'RefactoringSecurityArchitecture'
This commit is contained in:
+26
-2
@@ -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();
|
||||
|
||||
+2
-1
@@ -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();
|
||||
+185
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-7
@@ -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);
|
||||
|
||||
+2
-20
@@ -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);
|
||||
|
||||
|
||||
+2
-31
@@ -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";
|
||||
|
||||
+12
-30
@@ -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();
|
||||
|
||||
+2
-32
@@ -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);
|
||||
|
||||
+2
-21
@@ -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)
|
||||
|
||||
+2
-21
@@ -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)
|
||||
|
||||
+2
-21
@@ -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)
|
||||
|
||||
+2
-32
@@ -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();
|
||||
|
||||
+2
-33
@@ -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();
|
||||
|
||||
+19
-24
@@ -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}");
|
||||
|
||||
+2
-32
@@ -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
|
||||
|
||||
+9
-36
@@ -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)
|
||||
|
||||
+1
-8
@@ -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))
|
||||
|
||||
+2
-36
@@ -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();
|
||||
|
||||
+1
-8
@@ -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)
|
||||
|
||||
+1
-8
@@ -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}");
|
||||
|
||||
|
||||
+2
-19
@@ -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})");
|
||||
|
||||
|
||||
+2
-19
@@ -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);
|
||||
|
||||
|
||||
+2
-35
@@ -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();
|
||||
|
||||
|
||||
+1
-7
@@ -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);
|
||||
|
||||
|
||||
+2
-35
@@ -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);
|
||||
|
||||
|
||||
+2
-27
@@ -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
-19
@@ -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"];
|
||||
|
||||
+2
-31
@@ -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();
|
||||
|
||||
|
||||
+2
-29
@@ -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>();
|
||||
|
||||
+2
-18
@@ -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
|
||||
|
||||
+2
-31
@@ -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})");
|
||||
|
||||
|
||||
+1
-15
@@ -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}");
|
||||
|
||||
+2
-35
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user