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