renaming global admin

This commit is contained in:
Oliver Kieselbach
2026-03-23 06:28:59 +01:00
parent 3ae19f48b2
commit 0fd8ca22d1
116 changed files with 911 additions and 911 deletions
+13 -13
View File
@@ -247,12 +247,12 @@ Monitoring/
| **Sessions** | `GET /sessions`, `GET /sessions/{id}`, `GET /sessions/{id}/events`, `POST /sessions/{id}/analysis`, `DELETE /sessions/{id}`, `POST /sessions/{id}/mark-failed` |
| **Rules** | CRUD for `/rules/gather`, `/rules/analyze`, `/rules/ime-log-patterns`, `POST /rules/reseed-from-github` |
| **Config** | `GET/POST /global/config`, `GET/PUT /config/{tenantId}` |
| **Auth** | `GET /auth/me`, `GET /auth/is-galactic-admin`, CRUD `/auth/galactic-admins` |
| **Auth** | `GET /auth/me`, `GET /auth/is-global-admin`, CRUD `/auth/global-admins` |
| **Tenants** | CRUD `/tenants/{id}/admins`, `POST /tenants/{id}/offboard` |
| **Devices** | `POST /devices/block`, `DELETE /devices/block/{serial}`, `GET /devices/blocked` |
| **Reports** | `POST /sessions/{id}/report`, `GET /galactic/session-reports` |
| **Reports** | `POST /sessions/{id}/report`, `GET /global/session-reports` |
| **Metrics** | `GET /metrics/usage`, `GET /metrics/app`, `GET /metrics/geographic`, `GET /stats/platform` |
| **Galactic** | `/galactic/metrics/*`, `/galactic/audit/logs`, `/galactic/session-reports`, `/galactic/notifications` (CRUD + dismiss) |
| **Global** | `/global/metrics/*`, `/global/audit/logs`, `/global/session-reports`, `/global/notifications` (CRUD + dismiss) |
| **Progress** | `GET /progress/sessions`, `GET /progress/sessions/{id}/events` |
| **SignalR** | `POST /realtime/negotiate`, `POST /realtime/groups/join`, `POST /realtime/groups/leave` |
| **Health** | `GET /health`, `GET /health/detailed` |
@@ -276,7 +276,7 @@ Monitoring/
| `SessionReportService` | Report ZIP generation + Blob upload |
| `GraphTokenService` | MS Graph token acquisition for Autopilot device validation |
| `BlockedVersionService` | Version-based block/kill rules with wildcard and ceiling patterns |
| `GalacticNotificationService` | Persistent in-app notifications for Galactic Admins (survives page reloads) |
| `GlobalNotificationService` | Persistent in-app notifications for Global Admins (survives page reloads) |
| `HealthCheckService` | Health checks for Storage, Processing, and Agent binary availability |
| `PreviewWhitelistService` | Private Preview tenant whitelist with 5-min cache |
| `ResendEmailService` | Transactional emails via Resend.com for Preview notifications |
@@ -339,16 +339,16 @@ Agent POST /api/agent/ingest (NDJSON+gzip)
| `/sessions/[sessionId]` | Session detail, event timeline, analysis | Yes | Any |
| `/diagnosis/[sessionId]` | Rule analysis results | Yes | Any |
| `/settings` | Tenant configuration (10+ sections) | Yes | Tenant Admin |
| `/admin-configuration` | Global admin settings | Yes | Galactic Admin |
| `/admin-configuration` | Global admin settings | Yes | Global Admin |
| `/gather-rules` | Gather rule CRUD | Yes | Tenant Admin |
| `/analyze-rules` | Analyze rule CRUD | Yes | Tenant Admin |
| `/ime-log-patterns` | IME pattern management | Yes | Tenant Admin |
| `/fleet-health` | App metrics, fleet analytics | Yes | Tenant Admin |
| `/usage-metrics` | Tenant usage analytics | Yes | Tenant Admin |
| `/platform-metrics` | Platform-wide metrics | Yes | Galactic Admin |
| `/platform-metrics` | Platform-wide metrics | Yes | Global Admin |
| `/audit` | Audit log viewer | Yes | Tenant Admin |
| `/health-check` | Backend health status | Yes | Galactic Admin |
| `/geographic-performance` | Geo map of deployments | Yes | Galactic Admin |
| `/health-check` | Backend health status | Yes | Global Admin |
| `/geographic-performance` | Geo map of deployments | Yes | Global Admin |
| `/progress` | Real-time enrollment progress tracking | Yes | Tenant Admin |
| `/preview` | Private Preview waitlist/approval page | Yes | Unapproved tenants |
| `/changelog` | Platform change log & known issues | Public | — |
@@ -365,7 +365,7 @@ React Context API (no Redux/Zustand):
| Context | Purpose |
|---------|---------|
| `AuthContext` | MSAL + user info + role detection (galactic/tenant admin) |
| `AuthContext` | MSAL + user info + role detection (global/tenant admin) |
| `SignalRContext` | WebSocket connection, group subscriptions, auto-reconnect |
| `TenantContext` | Current tenant ID (persisted to localStorage) |
| `NotificationContext` | Toast notifications with auto-dismiss + deduplication |
@@ -443,14 +443,14 @@ React Context API (no Redux/Zustand):
| `BootstrapSessions` | TenantId / "CodeLookup" | ShortCode | OOBE bootstrap tokens |
| `BlockedDevices` | TenantId | SerialNumber | Blocked devices |
| `SessionReports` | TenantId | ReportId | User-submitted reports |
| `GalacticAdmins` | "GalacticAdmins" | UPN | Platform-level admins |
| `GlobalAdmins` | "GlobalAdmins" | UPN | Platform-level admins |
| `TenantAdmins` | TenantId | UPN | Tenant-level admins |
| `PreviewWhitelist` | "Preview" | TenantId | Preview access gate |
| `UserActivity` | TenantId | UserId | User login tracking |
| `RuleStates` | TenantId | RuleId | Rule enable/disable state |
| `PreviewConfig` | "Preview" | "config" | Preview feature config |
| `BlockedVersions` | "BlockedVersions" | Pattern | Version block/kill rules (wildcards, ceilings) |
| `GalacticNotifications` | "GalacticNotifications" | InvertedTicks_Id | Persistent in-app notifications for Galactic Admins |
| `GlobalNotifications` | "GlobalNotifications" | InvertedTicks_Id | Persistent in-app notifications for Global Admins |
### Azure Blob Storage
@@ -521,7 +521,7 @@ ValidateSecurityAsync() (SecurityValidationExtensions.cs)
| Role | Scope | Capabilities |
|------|-------|-------------|
| **Galactic Admin** | Platform-wide | Global config, all tenants, platform metrics, health checks |
| **Global Admin** | Platform-wide | Global config, all tenants, platform metrics, health checks |
| **Tenant Admin** | Single tenant | Tenant config, rules, admin management, device blocking |
| **User** | Single tenant | Read-only dashboard, session detail view |
@@ -540,7 +540,7 @@ ValidateSecurityAsync() (SecurityValidationExtensions.cs)
| Group | Events | Triggered By |
|-------|--------|-------------|
| `tenant-{tenantId}` | `newSession`, `sessionStatusChanged`, `eventReceived`, `ruleResultReceived` | Event ingestion, session registration |
| `galactic-admins` | `newSession` (all tenants), platform stats updates | Cross-tenant broadcasts |
| `global-admins` | `newSession` (all tenants), platform stats updates | Cross-tenant broadcasts |
### Client-Side (SignalRContext)
@@ -1 +1 @@
568
569
@@ -138,7 +138,7 @@ public class EndpointPolicyCatalogCompletenessTests
[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")]
[InlineData("PATCH", "/api/global/session-reports/report-1/note", "global/session-reports/{reportId}/note")]
public void ParameterizedRoutes_MatchCorrectly(string httpMethod, string requestPath, string expectedTemplate)
{
var entry = EndpointAccessPolicyCatalog.FindPolicy(httpMethod, requestPath);
@@ -14,20 +14,20 @@ public class AutopilotDeviceValidationConsentFunction
private readonly ILogger<AutopilotDeviceValidationConsentFunction> _logger;
private readonly IConfiguration _configuration;
private readonly TenantAdminsService _tenantAdminsService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
private readonly GraphTokenService _graphTokenService;
public AutopilotDeviceValidationConsentFunction(
ILogger<AutopilotDeviceValidationConsentFunction> logger,
IConfiguration configuration,
TenantAdminsService tenantAdminsService,
GalacticAdminService galacticAdminService,
GlobalAdminService globalAdminService,
GraphTokenService graphTokenService)
{
_logger = logger;
_configuration = configuration;
_tenantAdminsService = tenantAdminsService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
_graphTokenService = graphTokenService;
}
@@ -108,10 +108,10 @@ public class AutopilotDeviceValidationConsentFunction
return forbidden;
}
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(userIdentifier);
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
if (!isGalacticAdmin && !isTenantAdmin)
if (!isGlobalAdmin && !isTenantAdmin)
{
_logger.LogWarning(
"User {User} attempted autopilot-device-validation consent operation without admin rights for tenant {TenantId}",
@@ -121,7 +121,7 @@ public class AutopilotDeviceValidationConsentFunction
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new
{
error = "Access denied. Tenant Admin or Galactic Admin required."
error = "Access denied. Tenant Admin or Global Admin required."
});
return forbidden;
}
@@ -34,7 +34,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var tenantId = req.Query["tenantId"];
if (string.IsNullOrEmpty(tenantId))
@@ -62,7 +62,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userIdentifier = TenantHelper.GetUserIdentifier(req);
string body;
@@ -110,7 +110,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
var isKill = action == "Kill";
_logger.LogWarning(
"Galactic Admin {User} {Action} device {SerialNumber} in tenant {TenantId} for {Hours}h. Reason: {Reason}",
"Global Admin {User} {Action} device {SerialNumber} in tenant {TenantId} for {Hours}h. Reason: {Reason}",
userIdentifier, isKill ? "issued KILL signal to" : "blocked", serialNumber, tenantId, durationHours, reason);
var response = req.CreateResponse(HttpStatusCode.OK);
@@ -142,7 +142,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var tenantId = req.Query["tenantId"];
@@ -164,7 +164,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
);
_logger.LogInformation(
"Galactic Admin {User} unblocked device {SerialNumber} in tenant {TenantId}",
"Global Admin {User} unblocked device {SerialNumber} in tenant {TenantId}",
userIdentifier, serialNumber, tenantId);
var response = req.CreateResponse(HttpStatusCode.OK);
@@ -21,16 +21,16 @@ namespace AutopilotMonitor.Functions.Functions.Admin
_blockedDeviceService = blockedDeviceService;
}
/// <summary>GET /api/galactic/devices/blocked — list all active blocks across all tenants</summary>
/// <summary>GET /api/global/devices/blocked — list all active blocks across all tenants</summary>
[Function("GetAllBlockedDevices")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "galactic/devices/blocked")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "global/devices/blocked")] HttpRequestData req)
{
_logger.LogInformation("GetAllBlockedDevices function processing request (Galactic Admin Mode)");
_logger.LogInformation("GetAllBlockedDevices function processing request (Global Admin Mode)");
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var blocked = await _blockedDeviceService.GetAllBlockedDevicesAsync();
@@ -7,28 +7,28 @@ using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Functions.Admin
{
public class GetGalacticAuditLogsFunction
public class GetGlobalAuditLogsFunction
{
private readonly ILogger<GetGalacticAuditLogsFunction> _logger;
private readonly ILogger<GetGlobalAuditLogsFunction> _logger;
private readonly TableStorageService _storageService;
public GetGalacticAuditLogsFunction(
ILogger<GetGalacticAuditLogsFunction> logger,
public GetGlobalAuditLogsFunction(
ILogger<GetGlobalAuditLogsFunction> logger,
TableStorageService storageService)
{
_logger = logger;
_storageService = storageService;
}
[Function("GetGalacticAuditLogs")]
[Function("GetGlobalAuditLogs")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "galactic/audit/logs")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "global/audit/logs")] HttpRequestData req)
{
_logger.LogInformation("GetGalacticAuditLogs function processing request (Galactic Admin Mode)");
_logger.LogInformation("GetGlobalAuditLogs function processing request (Global Admin Mode)");
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userEmail = TenantHelper.GetUserIdentifier(req);
_logger.LogInformation($"Fetching all audit logs across all tenants (User: {userEmail})");
@@ -47,7 +47,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting galactic audit logs");
_logger.LogError(ex, "Error getting global audit logs");
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
await errorResponse.WriteAsJsonAsync(new
@@ -45,7 +45,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var upn = TenantHelper.GetUserIdentifier(req);
// Parse ?type= parameter (default: all)
@@ -58,7 +58,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
typeParam = (typeEnd > 0 ? queryString.Substring(typeStart, typeEnd - typeStart) : queryString.Substring(typeStart)).ToLowerInvariant();
}
_logger.LogInformation($"Reseed from GitHub triggered by Galactic Admin {upn}, type={typeParam}");
_logger.LogInformation($"Reseed from GitHub triggered by Global Admin {upn}, type={typeParam}");
var gatherResult = new { deleted = 0, written = 0 };
var analyzeResult = new { deleted = 0, written = 0 };
@@ -11,31 +11,31 @@ namespace AutopilotMonitor.Functions.Functions.Admin;
/// <summary>
/// Tenant Admin Management endpoints
/// Allows tenant admins and galactic admins to manage admin users for a tenant
/// Allows tenant admins and global admins to manage admin users for a tenant
/// </summary>
public class TenantAdminManagementFunction
{
private readonly ILogger<TenantAdminManagementFunction> _logger;
private readonly TenantAdminsService _tenantAdminsService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
private readonly TableStorageService _storageService;
public TenantAdminManagementFunction(
ILogger<TenantAdminManagementFunction> logger,
TenantAdminsService tenantAdminsService,
GalacticAdminService galacticAdminService,
GlobalAdminService globalAdminService,
TableStorageService storageService)
{
_logger = logger;
_tenantAdminsService = tenantAdminsService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
_storageService = storageService;
}
/// <summary>
/// GET /api/tenants/{tenantId}/admins
/// Gets all admins for a tenant
/// Accessible by: Galactic Admins OR Tenant Admins of the same tenant
/// Accessible by: Global Admins OR Tenant Admins of the same tenant
/// </summary>
[Function("GetTenantAdmins")]
[Authorize]
@@ -50,16 +50,16 @@ public class TenantAdminManagementFunction
var userTenantId = principal.GetTenantId();
var upn = principal.GetUserPrincipalName();
// Check authorization: either Galactic Admin OR Tenant Admin of the same tenant
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(upn);
// Check authorization: either Global Admin OR Tenant Admin of the same tenant
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(upn);
var isTenantAdmin = tenantId.Equals(userTenantId, StringComparison.OrdinalIgnoreCase) &&
await _tenantAdminsService.IsTenantAdminAsync(tenantId, upn);
if (!isGalacticAdmin && !isTenantAdmin)
if (!isGlobalAdmin && !isTenantAdmin)
{
_logger.LogWarning($"User {upn} attempted to access admins for tenant {tenantId} without authorization");
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new { error = "Access denied. You must be a Galactic Admin or a Tenant Admin for this tenant." });
await forbiddenResponse.WriteAsJsonAsync(new { error = "Access denied. You must be a Global Admin or a Tenant Admin for this tenant." });
return forbiddenResponse;
}
@@ -75,7 +75,7 @@ public class TenantAdminManagementFunction
/// <summary>
/// POST /api/tenants/{tenantId}/admins
/// Adds a new admin to a tenant
/// Accessible by: Galactic Admins OR Tenant Admins of the same tenant
/// Accessible by: Global Admins OR Tenant Admins of the same tenant
/// </summary>
[Function("AddTenantAdmin")]
[Authorize]
@@ -90,16 +90,16 @@ public class TenantAdminManagementFunction
var userTenantId = principal.GetTenantId();
var upn = principal.GetUserPrincipalName();
// Check authorization: either Galactic Admin OR Tenant Admin of the same tenant
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(upn);
// Check authorization: either Global Admin OR Tenant Admin of the same tenant
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(upn);
var isTenantAdmin = tenantId.Equals(userTenantId, StringComparison.OrdinalIgnoreCase) &&
await _tenantAdminsService.IsTenantAdminAsync(tenantId, upn);
if (!isGalacticAdmin && !isTenantAdmin)
if (!isGlobalAdmin && !isTenantAdmin)
{
_logger.LogWarning($"User {upn} attempted to add admin to tenant {tenantId} without authorization");
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new { error = "Access denied. You must be a Galactic Admin or a Tenant Admin for this tenant." });
await forbiddenResponse.WriteAsJsonAsync(new { error = "Access denied. You must be a Global Admin or a Tenant Admin for this tenant." });
return forbiddenResponse;
}
@@ -139,7 +139,7 @@ public class TenantAdminManagementFunction
/// <summary>
/// DELETE /api/tenants/{tenantId}/admins/{adminUpn}
/// Removes an admin from a tenant
/// Accessible by: Galactic Admins OR Tenant Admins of the same tenant
/// Accessible by: Global Admins OR Tenant Admins of the same tenant
/// Note: Cannot remove yourself if you're the last admin
/// </summary>
[Function("RemoveTenantAdmin")]
@@ -156,24 +156,24 @@ public class TenantAdminManagementFunction
var userTenantId = principal.GetTenantId();
var upn = principal.GetUserPrincipalName();
// Check authorization: either Galactic Admin OR Tenant Admin of the same tenant
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(upn);
// Check authorization: either Global Admin OR Tenant Admin of the same tenant
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(upn);
var isTenantAdmin = tenantId.Equals(userTenantId, StringComparison.OrdinalIgnoreCase) &&
await _tenantAdminsService.IsTenantAdminAsync(tenantId, upn);
if (!isGalacticAdmin && !isTenantAdmin)
if (!isGlobalAdmin && !isTenantAdmin)
{
_logger.LogWarning($"User {upn} attempted to remove admin from tenant {tenantId} without authorization");
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new { error = "Access denied. You must be a Galactic Admin or a Tenant Admin for this tenant." });
await forbiddenResponse.WriteAsJsonAsync(new { error = "Access denied. You must be a Global Admin or a Tenant Admin for this tenant." });
return forbiddenResponse;
}
// Check if trying to remove self
if (adminUpn.Equals(upn, StringComparison.OrdinalIgnoreCase))
{
// Check if this is the last Admin-role member (only for non-Galactic-Admins)
if (!isGalacticAdmin)
// Check if this is the last Admin-role member (only for non-Global-Admins)
if (!isGlobalAdmin)
{
var members = await _tenantAdminsService.GetTenantAdminsAsync(tenantId);
var adminCount = members.Count(m => m.IsEnabled && (m.Role == null || m.Role == AutopilotMonitor.Shared.Constants.TenantRoles.Admin));
@@ -207,7 +207,7 @@ public class TenantAdminManagementFunction
/// <summary>
/// PATCH /api/tenants/{tenantId}/admins/{adminUpn}/disable
/// Disables an admin for a tenant
/// Accessible by: Galactic Admins OR Tenant Admins of the same tenant
/// Accessible by: Global Admins OR Tenant Admins of the same tenant
/// </summary>
[Function("DisableTenantAdmin")]
[Authorize]
@@ -223,16 +223,16 @@ public class TenantAdminManagementFunction
var userTenantId = principal.GetTenantId();
var upn = principal.GetUserPrincipalName();
// Check authorization: either Galactic Admin OR Tenant Admin of the same tenant
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(upn);
// Check authorization: either Global Admin OR Tenant Admin of the same tenant
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(upn);
var isTenantAdmin = tenantId.Equals(userTenantId, StringComparison.OrdinalIgnoreCase) &&
await _tenantAdminsService.IsTenantAdminAsync(tenantId, upn);
if (!isGalacticAdmin && !isTenantAdmin)
if (!isGlobalAdmin && !isTenantAdmin)
{
_logger.LogWarning($"User {upn} attempted to disable admin for tenant {tenantId} without authorization");
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new { error = "Access denied. You must be a Galactic Admin or a Tenant Admin for this tenant." });
await forbiddenResponse.WriteAsJsonAsync(new { error = "Access denied. You must be a Global Admin or a Tenant Admin for this tenant." });
return forbiddenResponse;
}
@@ -258,7 +258,7 @@ public class TenantAdminManagementFunction
/// <summary>
/// PATCH /api/tenants/{tenantId}/admins/{adminUpn}/enable
/// Enables an admin for a tenant
/// Accessible by: Galactic Admins OR Tenant Admins of the same tenant
/// Accessible by: Global Admins OR Tenant Admins of the same tenant
/// </summary>
[Function("EnableTenantAdmin")]
[Authorize]
@@ -274,16 +274,16 @@ public class TenantAdminManagementFunction
var userTenantId = principal.GetTenantId();
var upn = principal.GetUserPrincipalName();
// Check authorization: either Galactic Admin OR Tenant Admin of the same tenant
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(upn);
// Check authorization: either Global Admin OR Tenant Admin of the same tenant
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(upn);
var isTenantAdmin = tenantId.Equals(userTenantId, StringComparison.OrdinalIgnoreCase) &&
await _tenantAdminsService.IsTenantAdminAsync(tenantId, upn);
if (!isGalacticAdmin && !isTenantAdmin)
if (!isGlobalAdmin && !isTenantAdmin)
{
_logger.LogWarning($"User {upn} attempted to enable admin for tenant {tenantId} without authorization");
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new { error = "Access denied. You must be a Galactic Admin or a Tenant Admin for this tenant." });
await forbiddenResponse.WriteAsJsonAsync(new { error = "Access denied. You must be a Global Admin or a Tenant Admin for this tenant." });
return forbiddenResponse;
}
@@ -308,7 +308,7 @@ public class TenantAdminManagementFunction
/// <summary>
/// PATCH /api/tenants/{tenantId}/admins/{adminUpn}/permissions
/// Updates role and permissions for a tenant member
/// Accessible by: Galactic Admins OR Tenant Admins of the same tenant
/// Accessible by: Global Admins OR Tenant Admins of the same tenant
/// </summary>
[Function("UpdateMemberPermissions")]
[Authorize]
@@ -324,16 +324,16 @@ public class TenantAdminManagementFunction
var userTenantId = principal.GetTenantId();
var upn = principal.GetUserPrincipalName();
// Check authorization: either Galactic Admin OR Tenant Admin of the same tenant
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(upn);
// Check authorization: either Global Admin OR Tenant Admin of the same tenant
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(upn);
var isTenantAdmin = tenantId.Equals(userTenantId, StringComparison.OrdinalIgnoreCase) &&
await _tenantAdminsService.IsTenantAdminAsync(tenantId, upn);
if (!isGalacticAdmin && !isTenantAdmin)
if (!isGlobalAdmin && !isTenantAdmin)
{
_logger.LogWarning("User {Upn} attempted to update member permissions for tenant {TenantId} without authorization", upn, tenantId);
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new { error = "Access denied. You must be a Galactic Admin or a Tenant Admin for this tenant." });
await forbiddenResponse.WriteAsJsonAsync(new { error = "Access denied. You must be a Global Admin or a Tenant Admin for this tenant." });
return forbiddenResponse;
}
@@ -348,7 +348,7 @@ public class TenantAdminManagementFunction
// Prevent demoting yourself if you're the last Admin
if (adminUpn.Equals(upn, StringComparison.OrdinalIgnoreCase) && body.Role != AutopilotMonitor.Shared.Constants.TenantRoles.Admin)
{
if (!isGalacticAdmin)
if (!isGlobalAdmin)
{
var members = await _tenantAdminsService.GetTenantAdminsAsync(tenantId);
var adminCount = members.Count(m => m.IsEnabled && (m.Role == null || m.Role == AutopilotMonitor.Shared.Constants.TenantRoles.Admin));
@@ -22,18 +22,18 @@ public class TenantOffboardFunction
{
private readonly ILogger<TenantOffboardFunction> _logger;
private readonly TenantAdminsService _tenantAdminsService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
private readonly TableServiceClient _tableServiceClient;
public TenantOffboardFunction(
ILogger<TenantOffboardFunction> logger,
TenantAdminsService tenantAdminsService,
GalacticAdminService galacticAdminService,
GlobalAdminService globalAdminService,
IConfiguration configuration)
{
_logger = logger;
_tenantAdminsService = tenantAdminsService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
var connectionString = configuration["AzureTableStorageConnectionString"];
_tableServiceClient = new TableServiceClient(connectionString);
}
@@ -41,7 +41,7 @@ public class TenantOffboardFunction
/// <summary>
/// DELETE /api/tenants/{tenantId}/offboard
/// Permanently deletes ALL data for a tenant across all tables.
/// Accessible by: Tenant Admins of the same tenant OR Galactic Admins.
/// Accessible by: Tenant Admins of the same tenant OR Global Admins.
/// This action is IRREVERSIBLE.
/// </summary>
[Function("OffboardTenant")]
@@ -57,12 +57,12 @@ public class TenantOffboardFunction
var userTenantId = principal.GetTenantId();
var upn = principal.GetUserPrincipalName();
// Only Tenant Admins of the same tenant OR Galactic Admins may offboard
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(upn);
// Only Tenant Admins of the same tenant OR Global Admins may offboard
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(upn);
var isTenantAdmin = tenantId.Equals(userTenantId, StringComparison.OrdinalIgnoreCase) &&
await _tenantAdminsService.IsTenantAdminAsync(tenantId, upn);
if (!isGalacticAdmin && !isTenantAdmin)
if (!isGlobalAdmin && !isTenantAdmin)
{
_logger.LogWarning($"User {upn} attempted to offboard tenant {tenantId} without authorization");
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
@@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Functions.Admin
{
/// <summary>
/// Allows Galactic Admins to manually trigger maintenance tasks
/// Allows Global Admins to manually trigger maintenance tasks
/// </summary>
public class TriggerMaintenanceFunction
{
@@ -27,7 +27,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
/// <summary>
/// POST /api/maintenance/trigger
/// Manually trigger maintenance tasks (Galactic Admin only)
/// Manually trigger maintenance tasks (Global Admin only)
/// Query parameters:
/// - date: Optional date to aggregate (yyyy-MM-dd). If not provided, uses yesterday.
/// - aggregateOnly: If true, only runs aggregation (skips timeout and cleanup)
@@ -39,10 +39,10 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
_logger.LogInformation("Manual maintenance trigger requested");
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userEmail = TenantHelper.GetUserIdentifier(req);
_logger.LogInformation($"Maintenance trigger initiated by Galactic Admin: {userEmail}");
_logger.LogInformation($"Maintenance trigger initiated by Global Admin: {userEmail}");
try
{
@@ -34,7 +34,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var rules = await _blockedVersionService.GetBlockedVersionsAsync();
@@ -58,7 +58,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userIdentifier = TenantHelper.GetUserIdentifier(req);
string body;
@@ -105,7 +105,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
);
_logger.LogWarning(
"Galactic Admin {User} added version {Action} rule: Pattern={Pattern}, Reason={Reason}",
"Global Admin {User} added version {Action} rule: Pattern={Pattern}, Reason={Reason}",
userIdentifier, normalizedAction, versionPattern, reason);
var response = req.CreateResponse(HttpStatusCode.OK);
@@ -135,7 +135,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
{
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var versionPattern = Uri.UnescapeDataString(encodedPattern ?? string.Empty);
@@ -153,7 +153,7 @@ namespace AutopilotMonitor.Functions.Functions.Admin
);
_logger.LogInformation(
"Galactic Admin {User} removed version block rule: Pattern={Pattern}",
"Global Admin {User} removed version block rule: Pattern={Pattern}",
userIdentifier, versionPattern);
var response = req.CreateResponse(HttpStatusCode.OK);
@@ -20,20 +20,20 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
{
private readonly ILogger<CreateBootstrapSessionFunction> _logger;
private readonly BootstrapSessionService _bootstrapService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
private readonly TenantConfigurationService _configService;
private readonly TableStorageService _storageService;
public CreateBootstrapSessionFunction(
ILogger<CreateBootstrapSessionFunction> logger,
BootstrapSessionService bootstrapService,
GalacticAdminService galacticAdminService,
GlobalAdminService globalAdminService,
TenantConfigurationService configService,
TableStorageService storageService)
{
_logger = logger;
_bootstrapService = bootstrapService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
_configService = configService;
_storageService = storageService;
}
@@ -64,11 +64,11 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
// Use tenant from JWT if not specified in body
var tenantId = !string.IsNullOrEmpty(request.TenantId) ? request.TenantId : authenticatedTenantId;
// Cross-tenant boundary check — only Galactic Admins may operate on other tenants
// Cross-tenant boundary check — only Global Admins may operate on other tenants
if (!string.Equals(authenticatedTenantId, tenantId, StringComparison.OrdinalIgnoreCase))
{
var isGalactic = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
if (!isGalactic)
var isGlobal = await _globalAdminService.IsGlobalAdminAsync(userIdentifier);
if (!isGlobal)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { error = "Access denied: tenant mismatch" });
@@ -19,18 +19,18 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
{
private readonly ILogger<ListBootstrapSessionsFunction> _logger;
private readonly BootstrapSessionService _bootstrapService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
private readonly TenantConfigurationService _configService;
public ListBootstrapSessionsFunction(
ILogger<ListBootstrapSessionsFunction> logger,
BootstrapSessionService bootstrapService,
GalacticAdminService galacticAdminService,
GlobalAdminService globalAdminService,
TenantConfigurationService configService)
{
_logger = logger;
_bootstrapService = bootstrapService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
_configService = configService;
}
@@ -46,11 +46,11 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
var tenantId = req.Query["tenantId"] ?? authenticatedTenantId;
// Cross-tenant boundary check — only Galactic Admins may operate on other tenants
// Cross-tenant boundary check — only Global Admins may operate on other tenants
if (!string.Equals(authenticatedTenantId, tenantId, StringComparison.OrdinalIgnoreCase))
{
var isGalactic = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
if (!isGalactic)
var isGlobal = await _globalAdminService.IsGlobalAdminAsync(userIdentifier);
if (!isGlobal)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { error = "Access denied: tenant mismatch" });
@@ -17,20 +17,20 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
{
private readonly ILogger<RevokeBootstrapSessionFunction> _logger;
private readonly BootstrapSessionService _bootstrapService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
private readonly TenantConfigurationService _configService;
private readonly TableStorageService _storageService;
public RevokeBootstrapSessionFunction(
ILogger<RevokeBootstrapSessionFunction> logger,
BootstrapSessionService bootstrapService,
GalacticAdminService galacticAdminService,
GlobalAdminService globalAdminService,
TenantConfigurationService configService,
TableStorageService storageService)
{
_logger = logger;
_bootstrapService = bootstrapService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
_configService = configService;
_storageService = storageService;
}
@@ -48,11 +48,11 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
var tenantId = req.Query["tenantId"] ?? authenticatedTenantId;
// Cross-tenant boundary check — only Galactic Admins may operate on other tenants
// Cross-tenant boundary check — only Global Admins may operate on other tenants
if (!string.Equals(authenticatedTenantId, tenantId, StringComparison.OrdinalIgnoreCase))
{
var isGalactic = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
if (!isGalactic)
var isGlobal = await _globalAdminService.IsGlobalAdminAsync(userIdentifier);
if (!isGlobal)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { error = "Access denied: tenant mismatch" });
@@ -28,10 +28,10 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
string userIdentifier = TenantHelper.GetUserIdentifier(req);
_logger.LogInformation($"GetAdminConfiguration by Galactic Admin user {userIdentifier}");
_logger.LogInformation($"GetAdminConfiguration by Global Admin user {userIdentifier}");
var config = await _adminConfigService.GetConfigurationAsync();
@@ -28,10 +28,10 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
string userIdentifier = TenantHelper.GetUserIdentifier(req);
_logger.LogInformation($"GetAllTenantConfigurations by Galactic Admin {userIdentifier}");
_logger.LogInformation($"GetAllTenantConfigurations by Global Admin {userIdentifier}");
var configurations = await _configService.GetAllConfigurationsAsync();
@@ -13,16 +13,16 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
private readonly ILogger<GetTenantConfigurationFunction> _logger;
private readonly TenantConfigurationService _configService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
public GetTenantConfigurationFunction(
ILogger<GetTenantConfigurationFunction> logger,
TenantConfigurationService configService,
GalacticAdminService galacticAdminService)
GlobalAdminService globalAdminService)
{
_logger = logger;
_configService = configService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
}
[Function("GetTenantConfiguration")]
@@ -36,11 +36,11 @@ namespace AutopilotMonitor.Functions.Functions.Config
string authenticatedTenantId = TenantHelper.GetTenantId(req);
string userIdentifier = TenantHelper.GetUserIdentifier(req);
// Validate tenant access: cross-tenant only for Galactic Admins
// Validate tenant access: cross-tenant only for Global Admins
if (!string.Equals(authenticatedTenantId, tenantId, StringComparison.OrdinalIgnoreCase))
{
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
if (!isGalacticAdmin)
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(userIdentifier);
if (!isGlobalAdmin)
{
_logger.LogWarning("User {User} from tenant {AuthTenant} attempted to access configuration for tenant {TargetTenant}",
userIdentifier, authenticatedTenantId, tenantId);
@@ -34,10 +34,10 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
string userIdentifier = TenantHelper.GetUserIdentifier(req);
_logger.LogInformation($"UpdateAdminConfiguration by Galactic Admin user {userIdentifier}");
_logger.LogInformation($"UpdateAdminConfiguration by Global Admin user {userIdentifier}");
// Parse request body
if (req.Headers.TryGetValues("Content-Length", out var clValues)
@@ -16,18 +16,18 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
private readonly ILogger<UpdateTenantConfigurationFunction> _logger;
private readonly TenantConfigurationService _configService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
private readonly TableStorageService _storageService;
public UpdateTenantConfigurationFunction(
ILogger<UpdateTenantConfigurationFunction> logger,
TenantConfigurationService configService,
GalacticAdminService galacticAdminService,
GlobalAdminService globalAdminService,
TableStorageService storageService)
{
_logger = logger;
_configService = configService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
_storageService = storageService;
}
@@ -43,10 +43,10 @@ namespace AutopilotMonitor.Functions.Functions.Config
string userIdentifier = TenantHelper.GetUserIdentifier(req);
// GA check needed for: cross-tenant bypass + protected field filtering
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(userIdentifier);
// Validate tenant access: cross-tenant only for Galactic Admins
if (!isGalacticAdmin && !string.Equals(authenticatedTenantId, tenantId, StringComparison.OrdinalIgnoreCase))
// Validate tenant access: cross-tenant only for Global Admins
if (!isGlobalAdmin && !string.Equals(authenticatedTenantId, tenantId, StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("User {User} from tenant {AuthTenant} attempted to update configuration for tenant {TargetTenant}",
userIdentifier, authenticatedTenantId, tenantId);
@@ -86,9 +86,9 @@ namespace AutopilotMonitor.Functions.Functions.Config
// Set the actual user identifier for audit logging
config.UpdatedBy = userIdentifier;
// Protect GA-only fields from non-Galactic-Admin callers
// Protect GA-only fields from non-Global-Admin callers
var existingConfig = await _configService.GetConfigurationAsync(tenantId);
if (!isGalacticAdmin)
if (!isGlobalAdmin)
{
if (config.AllowInsecureAgentRequests != existingConfig.AllowInsecureAgentRequests ||
config.BootstrapTokenEnabled != existingConfig.BootstrapTokenEnabled ||
@@ -16,16 +16,16 @@ namespace AutopilotMonitor.Functions.Functions.Diagnostics
{
private readonly ILogger<DiagnosticsDownloadFunction> _logger;
private readonly TenantConfigurationService _configService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
public DiagnosticsDownloadFunction(
ILogger<DiagnosticsDownloadFunction> logger,
TenantConfigurationService configService,
GalacticAdminService galacticAdminService)
GlobalAdminService globalAdminService)
{
_logger = logger;
_configService = configService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
}
[Function("DiagnosticsDownloadUrl")]
@@ -52,8 +52,8 @@ namespace AutopilotMonitor.Functions.Functions.Diagnostics
// Validate tenant access
if (tenantId != userTenantId)
{
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
if (!isGalacticAdmin)
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(userIdentifier);
if (!isGlobalAdmin)
{
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new { success = false, message = "Access denied." });
@@ -195,7 +195,7 @@ namespace AutopilotMonitor.Functions.Functions.Feedback
}
/// <summary>
/// Returns all feedback entries for the Galactic Admin dashboard.
/// Returns all feedback entries for the Global Admin dashboard.
/// </summary>
[Function("GetAllFeedback")]
public async Task<HttpResponseData> GetAll(
@@ -4,32 +4,32 @@ using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Functions.Galactic;
namespace AutopilotMonitor.Functions.Functions.Global;
/// <summary>
/// API endpoints for managing persistent Galactic Admin notifications.
/// All endpoints require GalacticAdminOnly authorization (enforced by PolicyEnforcementMiddleware).
/// API endpoints for managing persistent Global Admin notifications.
/// All endpoints require GlobalAdminOnly authorization (enforced by PolicyEnforcementMiddleware).
/// </summary>
public class GalacticNotificationsFunction
public class GlobalNotificationsFunction
{
private readonly ILogger<GalacticNotificationsFunction> _logger;
private readonly GalacticNotificationService _notificationService;
private readonly ILogger<GlobalNotificationsFunction> _logger;
private readonly GlobalNotificationService _notificationService;
public GalacticNotificationsFunction(
ILogger<GalacticNotificationsFunction> logger,
GalacticNotificationService notificationService)
public GlobalNotificationsFunction(
ILogger<GlobalNotificationsFunction> logger,
GlobalNotificationService notificationService)
{
_logger = logger;
_notificationService = notificationService;
}
/// <summary>
/// GET /api/galactic/notifications
/// GET /api/global/notifications
/// Returns all active (non-dismissed) notifications, newest first.
/// </summary>
[Function("GetGalacticNotifications")]
[Function("GetGlobalNotifications")]
public async Task<HttpResponseData> GetNotifications(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "galactic/notifications")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "global/notifications")] HttpRequestData req)
{
var notifications = await _notificationService.GetActiveNotificationsAsync();
@@ -39,12 +39,12 @@ public class GalacticNotificationsFunction
}
/// <summary>
/// POST /api/galactic/notifications/{notificationId}/dismiss
/// POST /api/global/notifications/{notificationId}/dismiss
/// Dismisses a single notification.
/// </summary>
[Function("DismissGalacticNotification")]
[Function("DismissGlobalNotification")]
public async Task<HttpResponseData> DismissNotification(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "galactic/notifications/{notificationId}/dismiss")] HttpRequestData req,
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "global/notifications/{notificationId}/dismiss")] HttpRequestData req,
string notificationId)
{
var found = await _notificationService.DismissNotificationAsync(notificationId);
@@ -62,12 +62,12 @@ public class GalacticNotificationsFunction
}
/// <summary>
/// POST /api/galactic/notifications/dismiss-all
/// POST /api/global/notifications/dismiss-all
/// Dismisses all active notifications.
/// </summary>
[Function("DismissAllGalacticNotifications")]
[Function("DismissAllGlobalNotifications")]
public async Task<HttpResponseData> DismissAll(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "galactic/notifications/dismiss-all")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "global/notifications/dismiss-all")] HttpRequestData req)
{
var dismissedCount = await _notificationService.DismissAllNotificationsAsync();
@@ -15,32 +15,32 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure;
public class AuthFunction
{
private readonly ILogger<AuthFunction> _logger;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
private readonly TenantConfigurationService _tenantConfigService;
private readonly TenantAdminsService _tenantAdminsService;
private readonly TableStorageService _storageService;
private readonly PreviewWhitelistService _previewWhitelistService;
private readonly TelegramNotificationService _telegramNotificationService;
private readonly GalacticNotificationService _galacticNotificationService;
private readonly GlobalNotificationService _globalNotificationService;
public AuthFunction(
ILogger<AuthFunction> logger,
GalacticAdminService galacticAdminService,
GlobalAdminService globalAdminService,
TenantConfigurationService tenantConfigService,
TenantAdminsService tenantAdminsService,
TableStorageService storageService,
PreviewWhitelistService previewWhitelistService,
TelegramNotificationService telegramNotificationService,
GalacticNotificationService galacticNotificationService)
GlobalNotificationService globalNotificationService)
{
_logger = logger;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
_tenantConfigService = tenantConfigService;
_tenantAdminsService = tenantAdminsService;
_storageService = storageService;
_previewWhitelistService = previewWhitelistService;
_telegramNotificationService = telegramNotificationService;
_galacticNotificationService = galacticNotificationService;
_globalNotificationService = globalNotificationService;
}
/// <summary>
@@ -95,8 +95,8 @@ public class AuthFunction
"Fire-and-forget Telegram notification failed for tenant {TenantId}", tenantId),
TaskContinuationOptions.OnlyOnFaulted);
// Persistent in-app notification for Galactic Admins — best effort
_ = _galacticNotificationService.CreateNotificationAsync(
// Persistent in-app notification for Global Admins — best effort
_ = _globalNotificationService.CreateNotificationAsync(
"preview_signup",
"New Preview Signup",
$"Tenant {tenantId} ({domain}), UPN: {upn}");
@@ -132,12 +132,12 @@ public class AuthFunction
await _tenantConfigService.SaveConfigurationAsync(tenantConfig);
}
// Check if user is galactic admin (must happen before preview gate)
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(upn);
// Check if user is global admin (must happen before preview gate)
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(upn);
// Preview gate: only approved tenants get full portal access.
// Galactic Admins bypass the gate (they need access to manage the whitelist).
if (!isGalacticAdmin && !await _previewWhitelistService.IsApprovedAsync(tenantId))
// Global Admins bypass the gate (they need access to manage the whitelist).
if (!isGlobalAdmin && !await _previewWhitelistService.IsApprovedAsync(tenantId))
{
_logger.LogInformation("Tenant {TenantId} blocked by preview gate (user: {Upn})", tenantId, upn);
@@ -181,7 +181,7 @@ public class AuthFunction
upn,
displayName,
objectId,
isGalacticAdmin,
isGlobalAdmin,
isTenantAdmin,
role,
canManageBootstrapTokens
@@ -193,13 +193,13 @@ public class AuthFunction
}
/// <summary>
/// GET /api/auth/is-galactic-admin
/// Checks if the current user is a Galactic Admin
/// GET /api/auth/is-global-admin
/// Checks if the current user is a Global Admin
/// </summary>
[Function("IsGalacticAdmin")]
[Function("IsGlobalAdmin")]
[Authorize]
public async Task<HttpResponseData> IsGalacticAdmin(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "auth/is-galactic-admin")] HttpRequestData req,
public async Task<HttpResponseData> IsGlobalAdmin(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "auth/is-global-admin")] HttpRequestData req,
FunctionContext context)
{
var principal = context.GetUser();
@@ -209,26 +209,26 @@ public class AuthFunction
}
var upn = principal.GetUserPrincipalName();
var isAdmin = await _galacticAdminService.IsGalacticAdminAsync(upn);
var isAdmin = await _globalAdminService.IsGlobalAdminAsync(upn);
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new { isGalacticAdmin = isAdmin, upn });
await response.WriteAsJsonAsync(new { isGlobalAdmin = isAdmin, upn });
return response;
}
/// <summary>
/// GET /api/auth/galactic-admins
/// Lists all Galactic Admins (only accessible by Galactic Admins)
/// GET /api/auth/global-admins
/// Lists all Global Admins (only accessible by Global Admins)
/// </summary>
[Function("GetGalacticAdmins")]
[Function("GetGlobalAdmins")]
[Authorize]
public async Task<HttpResponseData> GetGalacticAdmins(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "auth/galactic-admins")] HttpRequestData req,
public async Task<HttpResponseData> GetGlobalAdmins(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "auth/global-admins")] HttpRequestData req,
FunctionContext context)
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var admins = await _galacticAdminService.GetAllGalacticAdminsAsync();
var admins = await _globalAdminService.GetAllGlobalAdminsAsync();
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new { admins });
@@ -236,21 +236,21 @@ public class AuthFunction
}
/// <summary>
/// POST /api/auth/galactic-admins
/// Adds a new Galactic Admin (only accessible by existing Galactic Admins)
/// POST /api/auth/global-admins
/// Adds a new Global Admin (only accessible by existing Global Admins)
/// </summary>
[Function("AddGalacticAdmin")]
[Function("AddGlobalAdmin")]
[Authorize]
public async Task<HttpResponseData> AddGalacticAdmin(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "auth/galactic-admins")] HttpRequestData req,
public async Task<HttpResponseData> AddGlobalAdmin(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "auth/global-admins")] HttpRequestData req,
FunctionContext context)
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var principal = context.GetUser();
var currentUpn = principal?.GetUserPrincipalName();
// Parse request body
var body = await req.ReadFromJsonAsync<AddGalacticAdminRequest>();
var body = await req.ReadFromJsonAsync<AddGlobalAdminRequest>();
if (body == null || string.IsNullOrWhiteSpace(body.Upn))
{
var badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
@@ -258,9 +258,9 @@ public class AuthFunction
return badRequestResponse;
}
var newAdmin = await _galacticAdminService.AddGalacticAdminAsync(body.Upn, currentUpn!);
var newAdmin = await _globalAdminService.AddGlobalAdminAsync(body.Upn, currentUpn!);
_logger.LogInformation($"Galactic Admin added: {body.Upn} by {currentUpn}");
_logger.LogInformation($"Global Admin added: {body.Upn} by {currentUpn}");
var response = req.CreateResponse(HttpStatusCode.Created);
await response.WriteAsJsonAsync(new { admin = newAdmin });
@@ -268,17 +268,17 @@ public class AuthFunction
}
/// <summary>
/// DELETE /api/auth/galactic-admins/{upn}
/// Removes a Galactic Admin (only accessible by existing Galactic Admins)
/// DELETE /api/auth/global-admins/{upn}
/// Removes a Global Admin (only accessible by existing Global Admins)
/// </summary>
[Function("RemoveGalacticAdmin")]
[Function("RemoveGlobalAdmin")]
[Authorize]
public async Task<HttpResponseData> RemoveGalacticAdmin(
[HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "auth/galactic-admins/{upn}")] HttpRequestData req,
public async Task<HttpResponseData> RemoveGlobalAdmin(
[HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "auth/global-admins/{upn}")] HttpRequestData req,
string upn,
FunctionContext context)
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var principal = context.GetUser();
var currentUpn = principal?.GetUserPrincipalName();
@@ -286,16 +286,16 @@ public class AuthFunction
if (upn.Equals(currentUpn, StringComparison.OrdinalIgnoreCase))
{
var badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await badRequestResponse.WriteAsJsonAsync(new { error = "You cannot remove yourself as a Galactic Admin" });
await badRequestResponse.WriteAsJsonAsync(new { error = "You cannot remove yourself as a Global Admin" });
return badRequestResponse;
}
await _galacticAdminService.RemoveGalacticAdminAsync(upn);
await _globalAdminService.RemoveGlobalAdminAsync(upn);
_logger.LogInformation($"Galactic Admin removed: {upn} by {currentUpn}");
_logger.LogInformation($"Global Admin removed: {upn} by {currentUpn}");
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new { message = "Galactic Admin removed successfully" });
await response.WriteAsJsonAsync(new { message = "Global Admin removed successfully" });
return response;
}
@@ -317,7 +317,7 @@ public class AuthFunction
}
}
public class AddGalacticAdminRequest
public class AddGlobalAdminRequest
{
public string Upn { get; set; } = string.Empty;
}
@@ -79,7 +79,7 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
/// <summary>
/// GET /api/health/detailed
/// Detailed health check with comprehensive system checks (Galactic Admin only)
/// Detailed health check with comprehensive system checks (Global Admin only)
/// </summary>
[Function("DetailedHealthCheck")]
public async Task<HttpResponseData> GetDetailedHealthCheck(
@@ -88,7 +88,7 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
{
_logger.LogInformation("Detailed health check requested");
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Perform comprehensive health checks
var healthCheckResult = await _healthCheckService.PerformAllChecksAsync();
@@ -12,14 +12,14 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
public class SignalRAddToGroupFunction
{
private readonly ILogger<SignalRAddToGroupFunction> _logger;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
public SignalRAddToGroupFunction(
ILogger<SignalRAddToGroupFunction> logger,
GalacticAdminService galacticAdminService)
GlobalAdminService globalAdminService)
{
_logger = logger;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
}
[Function("AddToGroup")]
@@ -54,21 +54,21 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
var userEmail = TenantHelper.GetUserIdentifier(req);
// Validate tenant access
// Group names are in format: "tenant-{tenantId}", "session-{tenantId}-{sessionId}", or "galactic-admins"
// Users can only join groups for their own tenant (unless they are Galactic Admin)
// Group names are in format: "tenant-{tenantId}", "session-{tenantId}-{sessionId}", or "global-admins"
// Users can only join groups for their own tenant (unless they are Global Admin)
// Explicit validation for the galactic-admins group
if (request.GroupName == "galactic-admins")
// Explicit validation for the global-admins group
if (request.GroupName == "global-admins")
{
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userEmail);
if (!isGalacticAdmin)
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(userEmail);
if (!isGlobalAdmin)
{
_logger.LogWarning($"User {userEmail} (tenant {userTenantId}) attempted to join galactic-admins group without being a Galactic Admin");
_logger.LogWarning($"User {userEmail} (tenant {userTenantId}) attempted to join global-admins group without being a Global Admin");
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new { success = false, message = "Access denied: Only Galactic Admins can join this group" });
await forbiddenResponse.WriteAsJsonAsync(new { success = false, message = "Access denied: Only Global Admins can join this group" });
return new AddToGroupOutput { HttpResponse = forbiddenResponse };
}
_logger.LogInformation($"Galactic Admin {userEmail} joining galactic-admins group");
_logger.LogInformation($"Global Admin {userEmail} joining global-admins group");
}
else
{
@@ -84,10 +84,10 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
// Check if user is allowed to join this tenant's group
if (requestedTenantId != userTenantId)
{
// Check if user is Galactic Admin (they can join any tenant's group)
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userEmail);
// Check if user is Global Admin (they can join any tenant's group)
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(userEmail);
if (!isGalacticAdmin)
if (!isGlobalAdmin)
{
_logger.LogWarning($"User {userEmail} (tenant {userTenantId}) attempted to join group for tenant {requestedTenantId}");
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
@@ -96,7 +96,7 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
}
else
{
_logger.LogInformation($"Galactic Admin {userEmail} joining cross-tenant group: {request.GroupName}");
_logger.LogInformation($"Global Admin {userEmail} joining cross-tenant group: {request.GroupName}");
}
}
}
@@ -12,14 +12,14 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
public class SignalRRemoveFromGroupFunction
{
private readonly ILogger<SignalRRemoveFromGroupFunction> _logger;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
public SignalRRemoveFromGroupFunction(
ILogger<SignalRRemoveFromGroupFunction> logger,
GalacticAdminService galacticAdminService)
GlobalAdminService globalAdminService)
{
_logger = logger;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
}
[Function("RemoveFromGroup")]
@@ -54,21 +54,21 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
var userEmail = TenantHelper.GetUserIdentifier(req);
// Validate tenant access (same logic as AddToGroup)
// Group names are in format: "tenant-{tenantId}", "session-{tenantId}-{sessionId}", or "galactic-admins"
// Users can only leave groups for their own tenant (unless they are Galactic Admin)
// Group names are in format: "tenant-{tenantId}", "session-{tenantId}-{sessionId}", or "global-admins"
// Users can only leave groups for their own tenant (unless they are Global Admin)
// Explicit validation for the galactic-admins group
if (request.GroupName == "galactic-admins")
// Explicit validation for the global-admins group
if (request.GroupName == "global-admins")
{
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userEmail);
if (!isGalacticAdmin)
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(userEmail);
if (!isGlobalAdmin)
{
_logger.LogWarning($"User {userEmail} (tenant {userTenantId}) attempted to leave galactic-admins group without being a Galactic Admin");
_logger.LogWarning($"User {userEmail} (tenant {userTenantId}) attempted to leave global-admins group without being a Global Admin");
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new { success = false, message = "Access denied: Only Galactic Admins can leave this group" });
await forbiddenResponse.WriteAsJsonAsync(new { success = false, message = "Access denied: Only Global Admins can leave this group" });
return new RemoveFromGroupOutput { HttpResponse = forbiddenResponse };
}
_logger.LogInformation($"Galactic Admin {userEmail} leaving galactic-admins group");
_logger.LogInformation($"Global Admin {userEmail} leaving global-admins group");
}
else
{
@@ -84,10 +84,10 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
// Check if user is allowed to leave this tenant's group
if (requestedTenantId != userTenantId)
{
// Check if user is Galactic Admin (they can leave any tenant's group)
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userEmail);
// Check if user is Global Admin (they can leave any tenant's group)
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(userEmail);
if (!isGalacticAdmin)
if (!isGlobalAdmin)
{
_logger.LogWarning($"User {userEmail} (tenant {userTenantId}) attempted to leave group for tenant {requestedTenantId}");
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
@@ -96,7 +96,7 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
}
else
{
_logger.LogInformation($"Galactic Admin {userEmail} leaving cross-tenant group: {request.GroupName}");
_logger.LogInformation($"Global Admin {userEmail} leaving cross-tenant group: {request.GroupName}");
}
}
}
@@ -7,29 +7,29 @@ using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Functions.Metrics
{
public class GetGalacticAppMetricsFunction
public class GetGlobalAppMetricsFunction
{
private readonly ILogger<GetGalacticAppMetricsFunction> _logger;
private readonly ILogger<GetGlobalAppMetricsFunction> _logger;
private readonly TableStorageService _storageService;
public GetGalacticAppMetricsFunction(
ILogger<GetGalacticAppMetricsFunction> logger,
public GetGlobalAppMetricsFunction(
ILogger<GetGlobalAppMetricsFunction> logger,
TableStorageService storageService)
{
_logger = logger;
_storageService = storageService;
}
[Function("GetGalacticAppMetrics")]
[Function("GetGlobalAppMetrics")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "galactic/metrics/app")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "global/metrics/app")] HttpRequestData req)
{
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userEmail = TenantHelper.GetUserIdentifier(req);
_logger.LogInformation($"Fetching galactic app metrics (User: {userEmail})");
_logger.LogInformation($"Fetching global app metrics (User: {userEmail})");
var query = System.Web.HttpUtility.ParseQueryString(req.Url.Query);
var daysParam = query["days"];
@@ -94,7 +94,7 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching galactic app metrics");
_logger.LogError(ex, "Error fetching global app metrics");
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
await errorResponse.WriteAsJsonAsync(new { success = false, message = "Internal server error" });
return errorResponse;
@@ -7,26 +7,26 @@ using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Functions.Metrics
{
public class GetGalacticGeographicLocationSessionsFunction
public class GetGlobalGeographicLocationSessionsFunction
{
private readonly ILogger<GetGalacticGeographicLocationSessionsFunction> _logger;
private readonly ILogger<GetGlobalGeographicLocationSessionsFunction> _logger;
private readonly TableStorageService _storageService;
public GetGalacticGeographicLocationSessionsFunction(
ILogger<GetGalacticGeographicLocationSessionsFunction> logger,
public GetGlobalGeographicLocationSessionsFunction(
ILogger<GetGlobalGeographicLocationSessionsFunction> logger,
TableStorageService storageService)
{
_logger = logger;
_storageService = storageService;
}
[Function("GetGalacticGeographicLocationSessions")]
[Function("GetGlobalGeographicLocationSessions")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "galactic/metrics/geographic/sessions")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "global/metrics/geographic/sessions")] HttpRequestData req)
{
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userEmail = TenantHelper.GetUserIdentifier(req);
var query = System.Web.HttpUtility.ParseQueryString(req.Url.Query);
@@ -45,7 +45,7 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
var groupBy = query["groupBy"] ?? "city";
_logger.LogInformation("Fetching galactic sessions for location '{LocationKey}' ({Days}d, groupBy={GroupBy}) (User: {UserEmail})",
_logger.LogInformation("Fetching global sessions for location '{LocationKey}' ({Days}d, groupBy={GroupBy}) (User: {UserEmail})",
locationKey, days, groupBy, userEmail);
var cutoff = DateTime.UtcNow.AddDays(-days);
@@ -59,7 +59,7 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching galactic geographic location sessions");
_logger.LogError(ex, "Error fetching global geographic location sessions");
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
await errorResponse.WriteAsJsonAsync(new { success = false, message = "Internal server error" });
return errorResponse;
@@ -7,29 +7,29 @@ using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Functions.Metrics
{
public class GetGalacticGeographicMetricsFunction
public class GetGlobalGeographicMetricsFunction
{
private readonly ILogger<GetGalacticGeographicMetricsFunction> _logger;
private readonly ILogger<GetGlobalGeographicMetricsFunction> _logger;
private readonly TableStorageService _storageService;
public GetGalacticGeographicMetricsFunction(
ILogger<GetGalacticGeographicMetricsFunction> logger,
public GetGlobalGeographicMetricsFunction(
ILogger<GetGlobalGeographicMetricsFunction> logger,
TableStorageService storageService)
{
_logger = logger;
_storageService = storageService;
}
[Function("GetGalacticGeographicMetrics")]
[Function("GetGlobalGeographicMetrics")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "galactic/metrics/geographic")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "global/metrics/geographic")] HttpRequestData req)
{
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userEmail = TenantHelper.GetUserIdentifier(req);
_logger.LogInformation("Fetching galactic geographic metrics (User: {UserEmail})", userEmail);
_logger.LogInformation("Fetching global geographic metrics (User: {UserEmail})", userEmail);
var query = System.Web.HttpUtility.ParseQueryString(req.Url.Query);
var daysParam = query["days"];
@@ -52,7 +52,7 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching galactic geographic metrics");
_logger.LogError(ex, "Error fetching global geographic metrics");
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
await errorResponse.WriteAsJsonAsync(new { success = false, message = "Internal server error" });
return errorResponse;
@@ -7,31 +7,31 @@ using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Functions.Metrics
{
/// <summary>
/// Function for retrieving platform agent metrics (Galactic Admin only).
/// Function for retrieving platform agent metrics (Global Admin only).
/// Returns per-session CPU, memory, network metrics with 5-minute backend cache.
/// </summary>
public class GetGalacticPlatformMetricsFunction
public class GetGlobalPlatformMetricsFunction
{
private readonly ILogger<GetGalacticPlatformMetricsFunction> _logger;
private readonly ILogger<GetGlobalPlatformMetricsFunction> _logger;
private readonly PlatformMetricsService _platformMetricsService;
public GetGalacticPlatformMetricsFunction(
ILogger<GetGalacticPlatformMetricsFunction> logger,
public GetGlobalPlatformMetricsFunction(
ILogger<GetGlobalPlatformMetricsFunction> logger,
PlatformMetricsService platformMetricsService)
{
_logger = logger;
_platformMetricsService = platformMetricsService;
}
[Function("GetGalacticPlatformMetrics")]
[Function("GetGlobalPlatformMetrics")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "galactic/metrics/platform")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "global/metrics/platform")] HttpRequestData req)
{
_logger.LogInformation("Platform agent metrics requested");
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var metrics = await _platformMetricsService.ComputePlatformMetricsAsync();
@@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Functions.Metrics
{
/// <summary>
/// Function for retrieving platform usage metrics (Galactic Admin only)
/// Function for retrieving platform usage metrics (Global Admin only)
/// </summary>
public class PlatformUsageMetricsFunction
{
@@ -23,19 +23,19 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
}
/// <summary>
/// GET /api/galactic/metrics/usage - Compute and return platform usage metrics
/// On-demand computation with 5-minute cache (Galactic Admin only)
/// GET /api/global/metrics/usage - Compute and return platform usage metrics
/// On-demand computation with 5-minute cache (Global Admin only)
/// </summary>
[Function("GetPlatformUsageMetrics")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "galactic/metrics/usage")]
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "global/metrics/usage")]
HttpRequestData req)
{
_logger.LogInformation("Platform usage metrics requested");
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Optional tenantId query parameter: when provided, return tenant-specific metrics
var tenantId = req.Url.Query?.Contains("tenantId=") == true
@@ -17,16 +17,16 @@ public class ProgressPortalFunction
{
private readonly ILogger<ProgressPortalFunction> _logger;
private readonly TableStorageService _storageService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
public ProgressPortalFunction(
ILogger<ProgressPortalFunction> logger,
TableStorageService storageService,
GalacticAdminService galacticAdminService)
GlobalAdminService globalAdminService)
{
_logger = logger;
_storageService = storageService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
}
/// <summary>
@@ -75,7 +75,7 @@ public class ProgressPortalFunction
/// <summary>
/// GET /api/progress/sessions/{sessionId}/events
/// Returns events for a specific session. Cross-tenant access only for Galactic Admins.
/// Returns events for a specific session. Cross-tenant access only for Global Admins.
/// </summary>
[Function("ProgressGetSessionEvents")]
public async Task<HttpResponseData> GetSessionEvents(
@@ -115,11 +115,11 @@ public class ProgressPortalFunction
return badRequest;
}
// Cross-tenant access only for Galactic Admins
// Cross-tenant access only for Global Admins
if (requestedTenantId != userTenantId)
{
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
if (!isGalacticAdmin)
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(userIdentifier);
if (!isGlobalAdmin)
{
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await forbiddenResponse.WriteAsJsonAsync(new
@@ -133,7 +133,7 @@ public class ProgressPortalFunction
return forbiddenResponse;
}
_logger.LogInformation("{SessionPrefix} Galactic Admin {User} accessing cross-tenant progress events (tenant: {TenantId})",
_logger.LogInformation("{SessionPrefix} Global Admin {User} accessing cross-tenant progress events (tenant: {TenantId})",
sessionPrefix, userIdentifier, requestedTenantId);
}
@@ -11,7 +11,7 @@ namespace AutopilotMonitor.Functions.Functions.Reports
{
/// <summary>
/// Returns a short-lived SAS download URL for a session report blob stored in central blob storage.
/// Galactic Admin only.
/// Global Admin only.
/// </summary>
public class GetSessionReportDownloadUrlFunction
{
@@ -30,11 +30,11 @@ namespace AutopilotMonitor.Functions.Functions.Reports
[Function("GetSessionReportDownloadUrl")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "galactic/session-reports/download-url")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "global/session-reports/download-url")] HttpRequestData req)
{
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var query = HttpUtility.ParseQueryString(req.Url.Query);
var blobName = query["blobName"];
@@ -21,13 +21,13 @@ namespace AutopilotMonitor.Functions.Functions.Reports
[Function("GetSessionReports")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "galactic/session-reports")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "global/session-reports")] HttpRequestData req)
{
_logger.LogInformation("GetSessionReports function processing request");
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var reports = await _sessionReportService.GetAllReportsAsync();
@@ -13,24 +13,24 @@ namespace AutopilotMonitor.Functions.Functions.Reports
private readonly ILogger<SubmitSessionReportFunction> _logger;
private readonly SessionReportService _sessionReportService;
private readonly TableStorageService _storageService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
private readonly TelegramNotificationService _telegramNotificationService;
private readonly GalacticNotificationService _galacticNotificationService;
private readonly GlobalNotificationService _globalNotificationService;
public SubmitSessionReportFunction(
ILogger<SubmitSessionReportFunction> logger,
SessionReportService sessionReportService,
TableStorageService storageService,
GalacticAdminService galacticAdminService,
GlobalAdminService globalAdminService,
TelegramNotificationService telegramNotificationService,
GalacticNotificationService galacticNotificationService)
GlobalNotificationService globalNotificationService)
{
_logger = logger;
_sessionReportService = sessionReportService;
_storageService = storageService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
_telegramNotificationService = telegramNotificationService;
_galacticNotificationService = galacticNotificationService;
_globalNotificationService = globalNotificationService;
}
[Function("SubmitSessionReport")]
@@ -47,7 +47,7 @@ namespace AutopilotMonitor.Functions.Functions.Reports
string userIdentifier = TenantHelper.GetUserIdentifier(req);
// Still needed: GA check for audit log skip logic
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(userIdentifier);
// Parse request body
var request = await req.ReadFromJsonAsync<SubmitSessionReportRequest>();
@@ -72,8 +72,8 @@ namespace AutopilotMonitor.Functions.Functions.Reports
// Submit report
var metadata = await _sessionReportService.SubmitReportAsync(request, userIdentifier);
// Log audit entry — skip for Galactic Admins
if (!isGalacticAdmin)
// Log audit entry — skip for Global Admins
if (!isGlobalAdmin)
{
await _storageService.LogAuditEntryAsync(
request.TenantId,
@@ -98,8 +98,8 @@ namespace AutopilotMonitor.Functions.Functions.Reports
_ = _telegramNotificationService.SendSessionReportAsync(
request.TenantId, userIdentifier, sessionId, metadata.ReportId, request.Comment ?? string.Empty);
// Persistent in-app notification for Galactic Admins — best effort
_ = _galacticNotificationService.CreateNotificationAsync(
// Persistent in-app notification for Global Admins — best effort
_ = _globalNotificationService.CreateNotificationAsync(
"session_report",
"New Session Report",
$"{userIdentifier} — Session {sessionId} (Tenant: {request.TenantId})");
@@ -10,7 +10,7 @@ using Newtonsoft.Json.Linq;
namespace AutopilotMonitor.Functions.Functions.Reports
{
/// <summary>
/// Allows Galactic Admins to add or update an admin note on a submitted session report.
/// Allows Global Admins to add or update an admin note on a submitted session report.
/// </summary>
public class UpdateSessionReportNoteFunction
{
@@ -27,12 +27,12 @@ namespace AutopilotMonitor.Functions.Functions.Reports
[Function("UpdateSessionReportNote")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "patch", Route = "galactic/session-reports/{reportId}/note")] HttpRequestData req,
[HttpTrigger(AuthorizationLevel.Anonymous, "patch", Route = "global/session-reports/{reportId}/note")] HttpRequestData req,
string reportId)
{
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userIdentifier = TenantHelper.GetUserIdentifier(req);
if (string.IsNullOrEmpty(reportId))
@@ -11,25 +11,25 @@ namespace AutopilotMonitor.Functions.Functions.Rules
/// <summary>
/// Returns analysis results (rule evaluations) for a session.
/// Supports on-demand re-analysis via ?reanalyze=true query parameter.
/// Galactic admins can request analysis for any tenant by passing ?tenantId=...
/// Global admins can request analysis for any tenant by passing ?tenantId=...
/// </summary>
public class GetRuleResultsFunction
{
private readonly ILogger<GetRuleResultsFunction> _logger;
private readonly TableStorageService _storageService;
private readonly AnalyzeRuleService _analyzeRuleService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
public GetRuleResultsFunction(
ILogger<GetRuleResultsFunction> logger,
TableStorageService storageService,
AnalyzeRuleService analyzeRuleService,
GalacticAdminService galacticAdminService)
GlobalAdminService globalAdminService)
{
_logger = logger;
_storageService = storageService;
_analyzeRuleService = analyzeRuleService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
}
[Function("GetRuleResults")]
@@ -46,18 +46,18 @@ namespace AutopilotMonitor.Functions.Functions.Rules
var requestedTenantId = query["tenantId"];
var effectiveTenantId = string.IsNullOrEmpty(requestedTenantId) ? userTenantId : requestedTenantId;
// Cross-tenant access requires Galactic Admin
// Cross-tenant access requires Global Admin
if (effectiveTenantId != userTenantId)
{
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
if (!isGalacticAdmin)
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(userIdentifier);
if (!isGlobalAdmin)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. You can only view analysis results for your own tenant." });
return forbidden;
}
_logger.LogInformation($"Galactic Admin {userIdentifier} accessing cross-tenant analysis results (tenant: {effectiveTenantId})");
_logger.LogInformation($"Global Admin {userIdentifier} accessing cross-tenant analysis results (tenant: {effectiveTenantId})");
}
var reanalyze = string.Equals(query["reanalyze"], "true", StringComparison.OrdinalIgnoreCase);
@@ -7,17 +7,17 @@ using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Functions.Rules
{
/// <summary>
/// Galactic Admin endpoints for viewing rules across tenants.
/// Reads tenantId from query string (not JWT) so galactic admins can inspect any tenant.
/// Global Admin endpoints for viewing rules across tenants.
/// Reads tenantId from query string (not JWT) so global admins can inspect any tenant.
/// </summary>
public class GalacticRulesFunction
public class GlobalRulesFunction
{
private readonly ILogger<GalacticRulesFunction> _logger;
private readonly ILogger<GlobalRulesFunction> _logger;
private readonly GatherRuleService _gatherRuleService;
private readonly AnalyzeRuleService _analyzeRuleService;
public GalacticRulesFunction(
ILogger<GalacticRulesFunction> logger,
public GlobalRulesFunction(
ILogger<GlobalRulesFunction> logger,
GatherRuleService gatherRuleService,
AnalyzeRuleService analyzeRuleService)
{
@@ -27,13 +27,13 @@ namespace AutopilotMonitor.Functions.Functions.Rules
}
/// <summary>
/// GET /api/galactic/rules/gather?tenantId=X - Get gather rules for any tenant (Galactic Admin only)
/// GET /api/global/rules/gather?tenantId=X - Get gather rules for any tenant (Global Admin only)
/// </summary>
[Function("GetGalacticGatherRules")]
[Function("GetGlobalGatherRules")]
public async Task<HttpResponseData> GetGatherRules(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "galactic/rules/gather")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "global/rules/gather")] HttpRequestData req)
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var tenantId = System.Web.HttpUtility.ParseQueryString(req.Url.Query ?? "").Get("tenantId");
if (string.IsNullOrEmpty(tenantId))
@@ -43,7 +43,7 @@ namespace AutopilotMonitor.Functions.Functions.Rules
return badRequest;
}
_logger.LogInformation("Galactic admin requesting gather rules for tenant {TenantId}", tenantId);
_logger.LogInformation("Global admin requesting gather rules for tenant {TenantId}", tenantId);
var rules = await _gatherRuleService.GetAllRulesForTenantAsync(tenantId);
@@ -53,13 +53,13 @@ namespace AutopilotMonitor.Functions.Functions.Rules
}
/// <summary>
/// GET /api/galactic/rules/analyze?tenantId=X - Get analyze rules for any tenant (Galactic Admin only)
/// GET /api/global/rules/analyze?tenantId=X - Get analyze rules for any tenant (Global Admin only)
/// </summary>
[Function("GetGalacticAnalyzeRules")]
[Function("GetGlobalAnalyzeRules")]
public async Task<HttpResponseData> GetAnalyzeRules(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "galactic/rules/analyze")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "global/rules/analyze")] HttpRequestData req)
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var tenantId = System.Web.HttpUtility.ParseQueryString(req.Url.Query ?? "").Get("tenantId");
if (string.IsNullOrEmpty(tenantId))
@@ -69,7 +69,7 @@ namespace AutopilotMonitor.Functions.Functions.Rules
return badRequest;
}
_logger.LogInformation("Galactic admin requesting analyze rules for tenant {TenantId}", tenantId);
_logger.LogInformation("Global admin requesting analyze rules for tenant {TenantId}", tenantId);
var rules = await _analyzeRuleService.GetAllRulesForTenantAsync(tenantId);
@@ -11,7 +11,7 @@ namespace AutopilotMonitor.Functions.Functions.Rules
{
/// <summary>
/// API for managing IME log patterns (portal-facing, JWT auth).
/// Phase 1: Read-only for tenants, edit/reseed for Galactic Admins only.
/// Phase 1: Read-only for tenants, edit/reseed for Global Admins only.
/// </summary>
public class ImeLogPatternsFunction
{
@@ -43,7 +43,7 @@ namespace AutopilotMonitor.Functions.Functions.Rules
[HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "rules/ime-log-patterns/{patternId}")] HttpRequestData req,
string patternId)
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Phase 1: Only global edits
var globalEdit = req.Url.Query.Contains("global=true", StringComparison.OrdinalIgnoreCase);
@@ -86,10 +86,10 @@ namespace AutopilotMonitor.Functions.Functions.Rules
{
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var upn = TenantHelper.GetUserIdentifier(req);
_logger.LogInformation($"Reseed IME log patterns triggered by Galactic Admin {upn}");
_logger.LogInformation($"Reseed IME log patterns triggered by Global Admin {upn}");
var (deleted, written) = await _patternService.ReseedBuiltInPatternsAsync();
@@ -10,7 +10,7 @@ namespace AutopilotMonitor.Functions.Functions.Rules;
/// <summary>
/// CRUD endpoints for managing the Private Preview tenant whitelist.
/// All endpoints are Galactic Admin only (except notification-email which is AuthenticatedUser).
/// All endpoints are Global Admin only (except notification-email which is AuthenticatedUser).
/// Temporary — remove after GA.
/// </summary>
public class PreviewWhitelistFunction
@@ -45,7 +45,7 @@ public class PreviewWhitelistFunction
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "preview/whitelist")] HttpRequestData req,
FunctionContext context)
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var approved = await _previewWhitelistService.GetAllApprovedAsync();
@@ -65,7 +65,7 @@ public class PreviewWhitelistFunction
string tenantId,
FunctionContext context)
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var principal = context.GetUser();
var upn = principal?.GetUserPrincipalName();
@@ -149,7 +149,7 @@ public class PreviewWhitelistFunction
string tenantId,
FunctionContext context)
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var principal = context.GetUser();
var upn = principal?.GetUserPrincipalName();
@@ -164,7 +164,7 @@ public class PreviewWhitelistFunction
/// <summary>
/// GET /api/preview/notification-email/{tenantId}
/// Returns the notification email for a tenant. Galactic Admin only.
/// Returns the notification email for a tenant. Global Admin only.
/// </summary>
[Function("GetPreviewNotificationEmail")]
[Authorize]
@@ -224,7 +224,7 @@ public class PreviewWhitelistFunction
/// <summary>
/// POST /api/preview/send-welcome-email/{tenantId}
/// Sends (or resends) the Private Preview welcome email. Galactic Admin only.
/// Sends (or resends) the Private Preview welcome email. Global Admin only.
/// Accepts optional { email } in body — if provided, saves it to PreviewWhitelist table before sending.
/// </summary>
[Function("SendPreviewWelcomeEmail")]
@@ -234,7 +234,7 @@ public class PreviewWhitelistFunction
string tenantId,
FunctionContext context)
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
// If the caller provides an email in the body, save it first
var body = await req.ReadFromJsonAsync<SaveNotificationEmailRequest>();
@@ -23,13 +23,13 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
[Function("GetAllSessions")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "galactic/sessions")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "global/sessions")] HttpRequestData req)
{
_logger.LogInformation("GetAllSessions function processing request (Galactic Admin Mode)");
_logger.LogInformation("GetAllSessions function processing request (Global Admin Mode)");
try
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
var userEmail = TenantHelper.GetUserIdentifier(req);
var query = System.Web.HttpUtility.ParseQueryString(req.Url.Query);
var cursor = query["cursor"];
@@ -12,16 +12,16 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
{
private readonly ILogger<GetSessionEventsFunction> _logger;
private readonly TableStorageService _storageService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
public GetSessionEventsFunction(
ILogger<GetSessionEventsFunction> logger,
TableStorageService storageService,
GalacticAdminService galacticAdminService)
GlobalAdminService globalAdminService)
{
_logger = logger;
_storageService = storageService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
}
[Function("GetSessionEvents")]
@@ -66,12 +66,12 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
return badRequest;
}
// Validate tenant access: user must either own the tenant or be Galactic Admin
// Validate tenant access: user must either own the tenant or be Global Admin
if (requestedTenantId != userTenantId)
{
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(userIdentifier);
if (!isGalacticAdmin)
if (!isGlobalAdmin)
{
_logger.LogWarning($"{sessionPrefix} User {userIdentifier} (tenant {userTenantId}) attempted to access session events for tenant {requestedTenantId}");
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
@@ -87,7 +87,7 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
}
else
{
_logger.LogInformation($"{sessionPrefix} Galactic Admin {userIdentifier} accessing cross-tenant session events (tenant: {requestedTenantId})");
_logger.LogInformation($"{sessionPrefix} Global Admin {userIdentifier} accessing cross-tenant session events (tenant: {requestedTenantId})");
}
}
@@ -12,16 +12,16 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
{
private readonly ILogger<GetSessionFunction> _logger;
private readonly TableStorageService _storageService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
public GetSessionFunction(
ILogger<GetSessionFunction> logger,
TableStorageService storageService,
GalacticAdminService galacticAdminService)
GlobalAdminService globalAdminService)
{
_logger = logger;
_storageService = storageService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
}
[Function("GetSession")]
@@ -64,8 +64,8 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
if (!string.Equals(effectiveTenantId, userTenantId, StringComparison.OrdinalIgnoreCase))
{
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
if (!isGalacticAdmin)
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(userIdentifier);
if (!isGlobalAdmin)
{
_logger.LogWarning($"{sessionPrefix} User {userIdentifier} (tenant {userTenantId}) attempted to access session in tenant {effectiveTenantId}");
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
@@ -74,8 +74,8 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
});
// Send SignalR notification to update all clients in the tenant
// Only sent to tenant-specific group (not galactic-admins) to avoid flooding
// Galactic Admins can refresh or view session details to see status changes
// Only sent to tenant-specific group (not global-admins) to avoid flooding
// Global Admins can refresh or view session details to see status changes
object? sessionDelta = updatedSession != null ? new {
updatedSession.CurrentPhase,
updatedSession.CurrentPhaseDetail,
@@ -138,8 +138,8 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
await response.WriteAsJsonAsync(responseData);
// Send SignalR notification for new session registration
// This is sent to BOTH tenant-specific group AND galactic-admins group
// so Galactic Admins can see new sessions from all tenants without being
// This is sent to BOTH tenant-specific group AND global-admins group
// so Global Admins can see new sessions from all tenants without being
// flooded with every single event update
var messagePayload = new {
sessionId = registration.SessionId,
@@ -154,17 +154,17 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
Arguments = new[] { messagePayload }
};
// 2. Galactic Admins notification (cross-tenant visibility)
var galacticAdminMessage = new SignalRMessageAction("newSession")
// 2. Global Admins notification (cross-tenant visibility)
var globalAdminMessage = new SignalRMessageAction("newSession")
{
GroupName = "galactic-admins",
GroupName = "global-admins",
Arguments = new[] { messagePayload }
};
return new RegisterSessionOutput
{
HttpResponse = response,
SignalRMessages = new[] { tenantMessage, galacticAdminMessage }
SignalRMessages = new[] { tenantMessage, globalAdminMessage }
};
}
@@ -36,7 +36,7 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability
{
try
{
// Galactic Admin only — enforced by PolicyEnforcementMiddleware
// Global Admin only — enforced by PolicyEnforcementMiddleware
var tableClient = _storageService.GetTableClient(Constants.TableNames.VulnerabilityCache);
// Same range query pattern used in VulnerabilityCorrelationService.EnsureMappingsLoadedAsync
@@ -13,22 +13,22 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability
{
/// <summary>
/// Returns the full software inventory for a tenant from the SoftwareInventory table.
/// Galactic Admin only. Query param: ?tenantId=...
/// Global Admin only. Query param: ?tenantId=...
/// </summary>
public class GetSoftwareInventoryFunction
{
private readonly ILogger<GetSoftwareInventoryFunction> _logger;
private readonly TableStorageService _storageService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
public GetSoftwareInventoryFunction(
ILogger<GetSoftwareInventoryFunction> logger,
TableStorageService storageService,
GalacticAdminService galacticAdminService)
GlobalAdminService globalAdminService)
{
_logger = logger;
_storageService = storageService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
}
[Function("GetSoftwareInventory")]
@@ -37,7 +37,7 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability
{
try
{
// Galactic Admin only — enforced by PolicyEnforcementMiddleware
// Global Admin only — enforced by PolicyEnforcementMiddleware
var userIdentifier = TenantHelper.GetUserIdentifier(req);
var query = HttpUtility.ParseQueryString(req.Url.Query);
@@ -23,16 +23,16 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability
{
private readonly ILogger<GetUnmatchedSoftwareFunction> _logger;
private readonly TableStorageService _storageService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
public GetUnmatchedSoftwareFunction(
ILogger<GetUnmatchedSoftwareFunction> logger,
TableStorageService storageService,
GalacticAdminService galacticAdminService)
GlobalAdminService globalAdminService)
{
_logger = logger;
_storageService = storageService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
}
[Function("GetUnmatchedSoftware")]
@@ -41,7 +41,7 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability
{
try
{
// Galactic Admin only — enforced by PolicyEnforcementMiddleware
// Global Admin only — enforced by PolicyEnforcementMiddleware
var userIdentifier = TenantHelper.GetUserIdentifier(req);
// Query all SoftwareInventory entries across tenants and filter for unmatched (no CpeUri)
@@ -16,24 +16,24 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability
/// Returns the vulnerability correlation report for a session.
/// Supports on-demand re-scan via ?rescan=true query parameter — re-runs the
/// correlation against the stored inventory events with fresh CVE/KEV data.
/// Galactic admins can request reports for any tenant by passing ?tenantId=...
/// Global admins can request reports for any tenant by passing ?tenantId=...
/// </summary>
public class GetVulnerabilityReportFunction
{
private readonly ILogger<GetVulnerabilityReportFunction> _logger;
private readonly TableStorageService _storageService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
private readonly VulnerabilityCorrelationService _correlationService;
public GetVulnerabilityReportFunction(
ILogger<GetVulnerabilityReportFunction> logger,
TableStorageService storageService,
GalacticAdminService galacticAdminService,
GlobalAdminService globalAdminService,
VulnerabilityCorrelationService correlationService)
{
_logger = logger;
_storageService = storageService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
_correlationService = correlationService;
}
@@ -51,18 +51,18 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability
var requestedTenantId = query["tenantId"];
var effectiveTenantId = string.IsNullOrEmpty(requestedTenantId) ? userTenantId : requestedTenantId;
// Cross-tenant access requires Galactic Admin
// Cross-tenant access requires Global Admin
if (effectiveTenantId != userTenantId)
{
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
if (!isGalacticAdmin)
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(userIdentifier);
if (!isGlobalAdmin)
{
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. You can only view vulnerability reports for your own tenant." });
return forbidden;
}
_logger.LogInformation("Galactic Admin {User} accessing cross-tenant vulnerability report (tenant: {TenantId})",
_logger.LogInformation("Global Admin {User} accessing cross-tenant vulnerability report (tenant: {TenantId})",
userIdentifier, effectiveTenantId);
}
@@ -33,7 +33,7 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability
{
try
{
// Galactic Admin only - require Authorization header
// Global Admin only - require Authorization header
var authHeader = req.Headers.Contains("Authorization")
? req.Headers.GetValues("Authorization").FirstOrDefault()
: null;
@@ -13,13 +13,13 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability
/// <summary>
/// HTTP-triggered manual sync for vulnerability data (KEV catalog refresh).
/// Supports ?reseed=true to force re-import of CPE mapping seed (GitHub first, embedded fallback).
/// Galactic Admin only.
/// Global Admin only.
/// </summary>
public class TriggerVulnerabilityDataSyncFunction
{
private readonly KevDataService _kevService;
private readonly AdminConfigurationService _adminConfigService;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
private readonly TableStorageService _storageService;
private readonly GitHubRuleRepository _gitHubRepo;
private readonly ILogger<TriggerVulnerabilityDataSyncFunction> _logger;
@@ -27,14 +27,14 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability
public TriggerVulnerabilityDataSyncFunction(
KevDataService kevService,
AdminConfigurationService adminConfigService,
GalacticAdminService galacticAdminService,
GlobalAdminService globalAdminService,
TableStorageService storageService,
GitHubRuleRepository gitHubRepo,
ILogger<TriggerVulnerabilityDataSyncFunction> logger)
{
_kevService = kevService;
_adminConfigService = adminConfigService;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
_storageService = storageService;
_gitHubRepo = gitHubRepo;
_logger = logger;
@@ -2,7 +2,7 @@ namespace AutopilotMonitor.Functions.Helpers;
/// <summary>
/// Helper class for parsing SignalR group names.
/// Group formats: "tenant-{tenantId}", "session-{tenantId}-{sessionId}", or "galactic-admins"
/// Group formats: "tenant-{tenantId}", "session-{tenantId}-{sessionId}", or "global-admins"
/// </summary>
public static class SignalRGroupHelper
{
@@ -23,16 +23,16 @@ namespace AutopilotMonitor.Functions.Middleware;
public class PolicyEnforcementMiddleware : IFunctionsWorkerMiddleware
{
private readonly ILogger<PolicyEnforcementMiddleware> _logger;
private readonly GalacticAdminService _galacticAdminService;
private readonly GlobalAdminService _globalAdminService;
private readonly TenantAdminsService _tenantAdminsService;
public PolicyEnforcementMiddleware(
ILogger<PolicyEnforcementMiddleware> logger,
GalacticAdminService galacticAdminService,
GlobalAdminService globalAdminService,
TenantAdminsService tenantAdminsService)
{
_logger = logger;
_galacticAdminService = galacticAdminService;
_globalAdminService = globalAdminService;
_tenantAdminsService = tenantAdminsService;
}
@@ -146,8 +146,8 @@ public class PolicyEnforcementMiddleware : IFunctionsWorkerMiddleware
case EndpointPolicy.BootstrapManagerOrGA:
return await EvaluateBootstrapManagerOrGAAsync(tenantId, upn, userIdentifier);
case EndpointPolicy.GalacticAdminOnly:
return await EvaluateGalacticAdminOnlyAsync(upn, userIdentifier);
case EndpointPolicy.GlobalAdminOnly:
return await EvaluateGlobalAdminOnlyAsync(upn, userIdentifier);
default:
return CatalogDecisionResult.Deny(userIdentifier, "N/A", $"UnknownPolicy:{entry.Policy}");
@@ -160,8 +160,8 @@ public class PolicyEnforcementMiddleware : IFunctionsWorkerMiddleware
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 _globalAdminService.IsGlobalAdminAsync(upn))
return CatalogDecisionResult.Allow(userIdentifier, "GlobalAdmin", "GABypass");
var role = await _tenantAdminsService.GetMemberRoleAsync(tenantId, upn);
if (role == null)
@@ -177,8 +177,8 @@ public class PolicyEnforcementMiddleware : IFunctionsWorkerMiddleware
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 _globalAdminService.IsGlobalAdminAsync(upn))
return CatalogDecisionResult.Allow(userIdentifier, "GlobalAdmin", "GABypass");
if (await _tenantAdminsService.IsTenantAdminAsync(tenantId, upn))
return CatalogDecisionResult.Allow(userIdentifier, Constants.TenantRoles.Admin, "TenantAdmin");
@@ -194,8 +194,8 @@ public class PolicyEnforcementMiddleware : IFunctionsWorkerMiddleware
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 _globalAdminService.IsGlobalAdminAsync(upn))
return CatalogDecisionResult.Allow(userIdentifier, "GlobalAdmin", "GABypass");
if (await _tenantAdminsService.CanManageBootstrapAsync(tenantId, upn))
return CatalogDecisionResult.Allow(userIdentifier, "BootstrapManager", "CanManageBootstrap");
@@ -205,16 +205,16 @@ public class PolicyEnforcementMiddleware : IFunctionsWorkerMiddleware
return CatalogDecisionResult.Deny(userIdentifier, roleName, "NoBootstrapPermission");
}
private async Task<CatalogDecisionResult> EvaluateGalacticAdminOnlyAsync(
private async Task<CatalogDecisionResult> EvaluateGlobalAdminOnlyAsync(
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");
if (await _globalAdminService.IsGlobalAdminAsync(upn))
return CatalogDecisionResult.Allow(userIdentifier, "GlobalAdmin", "IsGA");
return CatalogDecisionResult.Deny(userIdentifier, "NonGA", "NotGalacticAdmin");
return CatalogDecisionResult.Deny(userIdentifier, "NonGA", "NotGlobalAdmin");
}
/// <summary>
@@ -104,7 +104,7 @@ builder.Services.AddSingleton<AdminConfigurationService>();
builder.Services.AddSingleton<RateLimitService>();
builder.Services.AddSingleton<UsageMetricsService>();
builder.Services.AddSingleton<PlatformMetricsService>();
builder.Services.AddSingleton<GalacticAdminService>();
builder.Services.AddSingleton<GlobalAdminService>();
builder.Services.AddSingleton<PreviewWhitelistService>();
builder.Services.AddSingleton<TenantAdminsService>();
builder.Services.AddSingleton<HealthCheckService>();
@@ -138,7 +138,7 @@ builder.Services.AddSingleton<AutopilotMonitor.Functions.Security.CorporateIdent
builder.Services.AddHttpClient<AutopilotMonitor.Functions.Services.Notifications.WebhookNotificationService>();
builder.Services.AddHttpClient<TelegramNotificationService>();
builder.Services.AddSingleton<ResendEmailService>();
builder.Services.AddSingleton<GalacticNotificationService>();
builder.Services.AddSingleton<GlobalNotificationService>();
var app = builder.Build();
@@ -20,14 +20,14 @@ public enum EndpointPolicy
/// <summary>Tenant member with Admin, Operator, or Viewer role. Tenant-scoped read access.</summary>
MemberRead,
/// <summary>Tenant Admin or Galactic Admin. Tenant-scoped write access.</summary>
/// <summary>Tenant Admin or Global Admin. Tenant-scoped write access.</summary>
TenantAdminOrGA,
/// <summary>Admin (always) or Operator with CanManageBootstrapTokens permission, or Galactic Admin.</summary>
/// <summary>Admin (always) or Operator with CanManageBootstrapTokens permission, or Global Admin.</summary>
BootstrapManagerOrGA,
/// <summary>Galactic Admin only. Platform-wide access.</summary>
GalacticAdminOnly,
/// <summary>Global Admin only. Platform-wide access.</summary>
GlobalAdminOnly,
}
/// <summary>
@@ -90,7 +90,7 @@ public static class EndpointAccessPolicyCatalog
// ── AuthenticatedUser ───────────────────────────────────────────
new("GET", "auth/me", EndpointPolicy.AuthenticatedUser),
new("GET", "auth/is-galactic-admin", EndpointPolicy.AuthenticatedUser),
new("GET", "auth/is-global-admin", EndpointPolicy.AuthenticatedUser),
new("POST", "realtime/negotiate", EndpointPolicy.AuthenticatedUser),
new("POST", "realtime/groups/join", EndpointPolicy.AuthenticatedUser),
new("POST", "realtime/groups/leave", EndpointPolicy.AuthenticatedUser),
@@ -147,51 +147,51 @@ public static class EndpointAccessPolicyCatalog
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", "preview/notification-email/{tenantId}", EndpointPolicy.GalacticAdminOnly),
new("POST", "preview/send-welcome-email/{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/geographic/sessions", 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/rules/gather", EndpointPolicy.GalacticAdminOnly),
new("GET", "galactic/rules/analyze", 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),
// ── GlobalAdminOnly ────────────────────────────────────────────
new("GET", "global/config", EndpointPolicy.GlobalAdminOnly),
new("PUT", "global/config", EndpointPolicy.GlobalAdminOnly),
new("POST", "global/config", EndpointPolicy.GlobalAdminOnly),
new("GET", "config/all", EndpointPolicy.GlobalAdminOnly),
new("GET", "auth/global-admins", EndpointPolicy.GlobalAdminOnly),
new("POST", "auth/global-admins", EndpointPolicy.GlobalAdminOnly),
new("DELETE", "auth/global-admins/{upn}", EndpointPolicy.GlobalAdminOnly),
new("GET", "preview/whitelist", EndpointPolicy.GlobalAdminOnly),
new("POST", "preview/whitelist/{tenantId}", EndpointPolicy.GlobalAdminOnly),
new("DELETE", "preview/whitelist/{tenantId}", EndpointPolicy.GlobalAdminOnly),
new("GET", "preview/notification-email/{tenantId}", EndpointPolicy.GlobalAdminOnly),
new("POST", "preview/send-welcome-email/{tenantId}", EndpointPolicy.GlobalAdminOnly),
new("GET", "global/sessions", EndpointPolicy.GlobalAdminOnly),
new("GET", "global/audit/logs", EndpointPolicy.GlobalAdminOnly),
new("GET", "global/metrics/platform", EndpointPolicy.GlobalAdminOnly),
new("GET", "global/metrics/app", EndpointPolicy.GlobalAdminOnly),
new("GET", "global/metrics/geographic", EndpointPolicy.GlobalAdminOnly),
new("GET", "global/metrics/geographic/sessions", EndpointPolicy.GlobalAdminOnly),
new("GET", "global/metrics/usage", EndpointPolicy.GlobalAdminOnly),
new("GET", "global/session-reports", EndpointPolicy.GlobalAdminOnly),
new("GET", "global/session-reports/download-url", EndpointPolicy.GlobalAdminOnly),
new("PATCH", "global/session-reports/{reportId}/note", EndpointPolicy.GlobalAdminOnly),
new("GET", "global/rules/gather", EndpointPolicy.GlobalAdminOnly),
new("GET", "global/rules/analyze", EndpointPolicy.GlobalAdminOnly),
new("GET", "global/devices/blocked", EndpointPolicy.GlobalAdminOnly),
new("GET", "devices/blocked", EndpointPolicy.GlobalAdminOnly),
new("POST", "devices/block", EndpointPolicy.GlobalAdminOnly),
new("DELETE", "devices/block/{encodedSerialNumber}", EndpointPolicy.GlobalAdminOnly),
new("GET", "versions/blocked", EndpointPolicy.GlobalAdminOnly),
new("POST", "versions/block", EndpointPolicy.GlobalAdminOnly),
new("DELETE", "versions/block/{encodedPattern}", EndpointPolicy.GlobalAdminOnly),
new("POST", "maintenance/trigger", EndpointPolicy.GlobalAdminOnly),
new("GET", "health/detailed", EndpointPolicy.AuthenticatedUser),
new("POST", "rules/reseed-from-github", EndpointPolicy.GalacticAdminOnly),
new("GET", "vulnerability/unmatched-software", EndpointPolicy.GalacticAdminOnly),
new("GET", "vulnerability/software-inventory", EndpointPolicy.GalacticAdminOnly),
new("POST", "vulnerability/sync", EndpointPolicy.GalacticAdminOnly),
new("POST", "vulnerability/cpe-mapping", EndpointPolicy.GalacticAdminOnly),
new("GET", "vulnerability/cpe-mappings", EndpointPolicy.GalacticAdminOnly),
new("POST", "rules/ime-log-patterns/reseed", EndpointPolicy.GalacticAdminOnly),
new("GET", "feedback/all", EndpointPolicy.GalacticAdminOnly),
new("GET", "galactic/notifications", EndpointPolicy.GalacticAdminOnly),
new("POST", "galactic/notifications/dismiss-all", EndpointPolicy.GalacticAdminOnly),
new("POST", "galactic/notifications/{notificationId}/dismiss", EndpointPolicy.GalacticAdminOnly),
new("POST", "rules/reseed-from-github", EndpointPolicy.GlobalAdminOnly),
new("GET", "vulnerability/unmatched-software", EndpointPolicy.GlobalAdminOnly),
new("GET", "vulnerability/software-inventory", EndpointPolicy.GlobalAdminOnly),
new("POST", "vulnerability/sync", EndpointPolicy.GlobalAdminOnly),
new("POST", "vulnerability/cpe-mapping", EndpointPolicy.GlobalAdminOnly),
new("GET", "vulnerability/cpe-mappings", EndpointPolicy.GlobalAdminOnly),
new("POST", "rules/ime-log-patterns/reseed", EndpointPolicy.GlobalAdminOnly),
new("GET", "feedback/all", EndpointPolicy.GlobalAdminOnly),
new("GET", "global/notifications", EndpointPolicy.GlobalAdminOnly),
new("POST", "global/notifications/dismiss-all", EndpointPolicy.GlobalAdminOnly),
new("POST", "global/notifications/{notificationId}/dismiss", EndpointPolicy.GlobalAdminOnly),
};
/// <summary>
@@ -172,7 +172,7 @@ namespace AutopilotMonitor.Functions.Security
// Security validation is always enforced (no longer configurable per tenant)
// Hard gate: tenant must enable at least one device validation method before agent traffic is accepted.
// Galactic Admins can set AllowInsecureAgentRequests=true in the config row for test tenants.
// Global Admins can set AllowInsecureAgentRequests=true in the config row for test tenants.
if (!config.ValidateAutopilotDevice && !config.ValidateCorporateIdentifier && !config.AllowInsecureAgentRequests)
{
return new SecurityValidationResult
@@ -7,20 +7,20 @@ using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Services;
/// <summary>
/// Service for managing Galactic Admin permissions
/// Galactic Admins can access cross-tenant data and perform platform-wide operations
/// Service for managing Global Admin permissions
/// Global Admins can access cross-tenant data and perform platform-wide operations
/// </summary>
public class GalacticAdminService
public class GlobalAdminService
{
private readonly TableServiceClient _tableServiceClient;
private readonly IMemoryCache _cache;
private readonly ILogger<GalacticAdminService> _logger;
private readonly ILogger<GlobalAdminService> _logger;
private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(5);
public GalacticAdminService(
public GlobalAdminService(
IConfiguration configuration,
IMemoryCache cache,
ILogger<GalacticAdminService> logger)
ILogger<GlobalAdminService> logger)
{
_cache = cache;
_logger = logger;
@@ -30,37 +30,37 @@ public class GalacticAdminService
}
/// <summary>
/// Checks if a UPN is a Galactic Admin
/// Checks if a UPN is a Global Admin
/// Uses caching for performance
/// </summary>
/// <param name="upn">User Principal Name (e.g., oliver@contoso.com)</param>
/// <returns>True if the user is a Galactic Admin</returns>
public async Task<bool> IsGalacticAdminAsync(string? upn)
/// <returns>True if the user is a Global Admin</returns>
public async Task<bool> IsGlobalAdminAsync(string? upn)
{
if (string.IsNullOrWhiteSpace(upn))
{
_logger.LogDebug("IsGalacticAdminAsync: UPN is null or empty");
_logger.LogDebug("IsGlobalAdminAsync: UPN is null or empty");
return false;
}
// Normalize UPN to lowercase for case-insensitive comparison
upn = upn.ToLowerInvariant();
_logger.LogInformation($"Checking if user is Galactic Admin: {upn}");
_logger.LogInformation($"Checking if user is Global Admin: {upn}");
// Check cache first
var cacheKey = $"galactic-admin:{upn}";
var cacheKey = $"global-admin:{upn}";
if (_cache.TryGetValue<bool>(cacheKey, out var isAdmin))
{
_logger.LogInformation($"Galactic Admin check (from cache): {upn} -> {isAdmin}");
_logger.LogInformation($"Global Admin check (from cache): {upn} -> {isAdmin}");
return isAdmin;
}
// Query Table Storage
_logger.LogInformation($"Querying Table Storage for Galactic Admin: {upn}");
var admin = await GetGalacticAdminAsync(upn);
_logger.LogInformation($"Querying Table Storage for Global Admin: {upn}");
var admin = await GetGlobalAdminAsync(upn);
var result = admin != null && admin.IsEnabled;
_logger.LogInformation($"Galactic Admin check result: {upn} -> {result} (Entity found: {admin != null}, IsEnabled: {admin?.IsEnabled})");
_logger.LogInformation($"Global Admin check result: {upn} -> {result} (Entity found: {admin != null}, IsEnabled: {admin?.IsEnabled})");
// Cache the result
_cache.Set(cacheKey, result, _cacheDuration);
@@ -69,52 +69,52 @@ public class GalacticAdminService
}
/// <summary>
/// Gets a Galactic Admin entity by UPN
/// Gets a Global Admin entity by UPN
/// </summary>
private async Task<GalacticAdminEntity?> GetGalacticAdminAsync(string upn)
private async Task<GlobalAdminEntity?> GetGlobalAdminAsync(string upn)
{
try
{
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GalacticAdmins);
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GlobalAdmins);
var normalizedUpn = upn.ToLowerInvariant();
_logger.LogDebug($"Querying Table Storage - PartitionKey: 'GalacticAdmins', RowKey: '{normalizedUpn}'");
_logger.LogDebug($"Querying Table Storage - PartitionKey: 'GlobalAdmins', RowKey: '{normalizedUpn}'");
// PartitionKey = "GalacticAdmins", RowKey = UPN
var entity = await tableClient.GetEntityAsync<GalacticAdminEntity>(
"GalacticAdmins",
// PartitionKey = "GlobalAdmins", RowKey = UPN
var entity = await tableClient.GetEntityAsync<GlobalAdminEntity>(
"GlobalAdmins",
normalizedUpn
);
_logger.LogDebug($"Galactic Admin entity found for {normalizedUpn}");
_logger.LogDebug($"Global Admin entity found for {normalizedUpn}");
return entity?.Value;
}
catch (Azure.RequestFailedException ex) when (ex.Status == 404)
{
// Entity not found
_logger.LogDebug($"Galactic Admin entity NOT found for {upn} (404)");
_logger.LogDebug($"Global Admin entity NOT found for {upn} (404)");
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error querying Galactic Admin for {upn}");
_logger.LogError(ex, $"Error querying Global Admin for {upn}");
return null;
}
}
/// <summary>
/// Adds a user as a Galactic Admin
/// Adds a user as a Global Admin
/// </summary>
/// <param name="upn">User Principal Name</param>
/// <param name="addedBy">UPN of the admin who is adding this user</param>
public async Task<GalacticAdminEntity> AddGalacticAdminAsync(string upn, string addedBy)
public async Task<GlobalAdminEntity> AddGlobalAdminAsync(string upn, string addedBy)
{
upn = upn.ToLowerInvariant();
addedBy = addedBy.ToLowerInvariant();
var entity = new GalacticAdminEntity
var entity = new GlobalAdminEntity
{
PartitionKey = "GalacticAdmins",
PartitionKey = "GlobalAdmins",
RowKey = upn,
Upn = upn,
IsEnabled = true,
@@ -122,59 +122,59 @@ public class GalacticAdminService
AddedBy = addedBy
};
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GalacticAdmins);
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GlobalAdmins);
await tableClient.UpsertEntityAsync(entity);
// Invalidate cache
_cache.Remove($"galactic-admin:{upn}");
_cache.Remove($"global-admin:{upn}");
return entity;
}
/// <summary>
/// Removes a user from Galactic Admins
/// Removes a user from Global Admins
/// </summary>
public async Task RemoveGalacticAdminAsync(string upn)
public async Task RemoveGlobalAdminAsync(string upn)
{
upn = upn.ToLowerInvariant();
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GalacticAdmins);
await tableClient.DeleteEntityAsync("GalacticAdmins", upn);
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GlobalAdmins);
await tableClient.DeleteEntityAsync("GlobalAdmins", upn);
// Invalidate cache
_cache.Remove($"galactic-admin:{upn}");
_cache.Remove($"global-admin:{upn}");
}
/// <summary>
/// Disables (but does not delete) a Galactic Admin
/// Disables (but does not delete) a Global Admin
/// </summary>
public async Task DisableGalacticAdminAsync(string upn)
public async Task DisableGlobalAdminAsync(string upn)
{
upn = upn.ToLowerInvariant();
var admin = await GetGalacticAdminAsync(upn);
var admin = await GetGlobalAdminAsync(upn);
if (admin != null)
{
admin.IsEnabled = false;
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GalacticAdmins);
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GlobalAdmins);
await tableClient.UpdateEntityAsync(admin, Azure.ETag.All);
// Invalidate cache
_cache.Remove($"galactic-admin:{upn}");
_cache.Remove($"global-admin:{upn}");
}
}
/// <summary>
/// Gets all Galactic Admins
/// Gets all Global Admins
/// </summary>
public async Task<List<GalacticAdminEntity>> GetAllGalacticAdminsAsync()
public async Task<List<GlobalAdminEntity>> GetAllGlobalAdminsAsync()
{
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GalacticAdmins);
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GlobalAdmins);
var admins = new List<GalacticAdminEntity>();
await foreach (var entity in tableClient.QueryAsync<GalacticAdminEntity>(
filter: $"PartitionKey eq 'GalacticAdmins'"))
var admins = new List<GlobalAdminEntity>();
await foreach (var entity in tableClient.QueryAsync<GlobalAdminEntity>(
filter: $"PartitionKey eq 'GlobalAdmins'"))
{
admins.Add(entity);
}
@@ -183,7 +183,7 @@ public class GalacticAdminService
}
/// <summary>
/// Clears the cache for all Galactic Admins
/// Clears the cache for all Global Admins
/// Useful after bulk updates
/// </summary>
public void ClearCache()
@@ -195,11 +195,11 @@ public class GalacticAdminService
}
/// <summary>
/// Entity representing a Galactic Admin in Table Storage
/// Entity representing a Global Admin in Table Storage
/// </summary>
public class GalacticAdminEntity : ITableEntity
public class GlobalAdminEntity : ITableEntity
{
public string PartitionKey { get; set; } = "GalacticAdmins";
public string PartitionKey { get; set; } = "GlobalAdmins";
public string RowKey { get; set; } = string.Empty; // UPN in lowercase
public DateTimeOffset? Timestamp { get; set; }
public Azure.ETag ETag { get; set; }
@@ -7,10 +7,10 @@ using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Services;
/// <summary>
/// Entity for persistent Galactic Admin notifications stored in Table Storage.
/// Entity for persistent Global Admin notifications stored in Table Storage.
/// All GAs share the same notification pool; dismiss is global.
/// </summary>
public class GalacticNotificationEntity : ITableEntity
public class GlobalNotificationEntity : ITableEntity
{
public string PartitionKey { get; set; } = "notifications";
public string RowKey { get; set; } = string.Empty; // invertedTicks_notificationId (newest-first)
@@ -27,9 +27,9 @@ public class GalacticNotificationEntity : ITableEntity
}
/// <summary>
/// DTO returned by the API for galactic notifications.
/// DTO returned by the API for global notifications.
/// </summary>
public class GalacticNotificationDto
public class GlobalNotificationDto
{
public string Id { get; set; } = string.Empty;
public string Type { get; set; } = "info";
@@ -40,17 +40,17 @@ public class GalacticNotificationDto
}
/// <summary>
/// Service for managing persistent Galactic Admin in-app notifications.
/// Service for managing persistent Global Admin in-app notifications.
/// Notifications survive page reloads and persist until actively dismissed.
/// </summary>
public class GalacticNotificationService
public class GlobalNotificationService
{
private readonly TableServiceClient _tableServiceClient;
private readonly ILogger<GalacticNotificationService> _logger;
private readonly ILogger<GlobalNotificationService> _logger;
public GalacticNotificationService(
public GlobalNotificationService(
IConfiguration configuration,
ILogger<GalacticNotificationService> logger)
ILogger<GlobalNotificationService> logger)
{
_logger = logger;
var connectionString = configuration["AzureTableStorageConnectionString"];
@@ -65,11 +65,11 @@ public class GalacticNotificationService
{
try
{
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GalacticNotifications);
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GlobalNotifications);
var notificationId = Guid.NewGuid().ToString("N")[..12];
var invertedTicks = (DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks).ToString("D19");
var entity = new GalacticNotificationEntity
var entity = new GlobalNotificationEntity
{
PartitionKey = "notifications",
RowKey = $"{invertedTicks}_{notificationId}",
@@ -83,28 +83,28 @@ public class GalacticNotificationService
};
await tableClient.UpsertEntityAsync(entity);
_logger.LogInformation("Galactic notification created: {NotificationId} ({Type})", notificationId, type);
_logger.LogInformation("Global notification created: {NotificationId} ({Type})", notificationId, type);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to create galactic notification ({Type}: {Title})", type, title);
_logger.LogWarning(ex, "Failed to create global notification ({Type}: {Title})", type, title);
}
}
/// <summary>
/// Returns all active (non-dismissed) notifications, newest first.
/// </summary>
public async Task<List<GalacticNotificationDto>> GetActiveNotificationsAsync()
public async Task<List<GlobalNotificationDto>> GetActiveNotificationsAsync()
{
try
{
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GalacticNotifications);
var notifications = new List<GalacticNotificationDto>();
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GlobalNotifications);
var notifications = new List<GlobalNotificationDto>();
await foreach (var entity in tableClient.QueryAsync<GalacticNotificationEntity>(
await foreach (var entity in tableClient.QueryAsync<GlobalNotificationEntity>(
e => e.PartitionKey == "notifications" && !e.Dismissed))
{
notifications.Add(new GalacticNotificationDto
notifications.Add(new GlobalNotificationDto
{
Id = entity.NotificationId,
Type = entity.Type,
@@ -119,13 +119,13 @@ public class GalacticNotificationService
}
catch (RequestFailedException ex) when (ex.Status == 404)
{
_logger.LogWarning("GalacticNotifications table not found — returning empty list");
return new List<GalacticNotificationDto>();
_logger.LogWarning("GlobalNotifications table not found — returning empty list");
return new List<GlobalNotificationDto>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to retrieve galactic notifications");
return new List<GalacticNotificationDto>();
_logger.LogError(ex, "Failed to retrieve global notifications");
return new List<GlobalNotificationDto>();
}
}
@@ -137,14 +137,14 @@ public class GalacticNotificationService
{
try
{
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GalacticNotifications);
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GlobalNotifications);
await foreach (var entity in tableClient.QueryAsync<GalacticNotificationEntity>(
await foreach (var entity in tableClient.QueryAsync<GlobalNotificationEntity>(
e => e.PartitionKey == "notifications" && e.NotificationId == notificationId))
{
entity.Dismissed = true;
await tableClient.UpdateEntityAsync(entity, entity.ETag, TableUpdateMode.Replace);
_logger.LogInformation("Galactic notification dismissed: {NotificationId}", notificationId);
_logger.LogInformation("Global notification dismissed: {NotificationId}", notificationId);
return true;
}
@@ -152,7 +152,7 @@ public class GalacticNotificationService
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to dismiss galactic notification {NotificationId}", notificationId);
_logger.LogError(ex, "Failed to dismiss global notification {NotificationId}", notificationId);
return false;
}
}
@@ -164,10 +164,10 @@ public class GalacticNotificationService
{
try
{
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GalacticNotifications);
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.GlobalNotifications);
var count = 0;
await foreach (var entity in tableClient.QueryAsync<GalacticNotificationEntity>(
await foreach (var entity in tableClient.QueryAsync<GlobalNotificationEntity>(
e => e.PartitionKey == "notifications" && !e.Dismissed))
{
entity.Dismissed = true;
@@ -175,12 +175,12 @@ public class GalacticNotificationService
count++;
}
_logger.LogInformation("Dismissed {Count} galactic notifications", count);
_logger.LogInformation("Dismissed {Count} global notifications", count);
return count;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to dismiss all galactic notifications");
_logger.LogError(ex, "Failed to dismiss all global notifications");
return 0;
}
}
@@ -109,7 +109,7 @@ namespace AutopilotMonitor.Functions.Services
}
/// <summary>
/// Updates a built-in IME log pattern globally (Galactic Admin only).
/// Updates a built-in IME log pattern globally (Global Admin only).
/// Modifies the global definition that all tenants inherit.
/// </summary>
public async Task<bool> UpdateGlobalPatternAsync(ImeLogPattern pattern)
@@ -127,7 +127,7 @@ namespace AutopilotMonitor.Functions.Services
}
/// <summary>
/// Deletes a built-in IME log pattern from the global partition (Galactic Admin only).
/// Deletes a built-in IME log pattern from the global partition (Global Admin only).
/// </summary>
public async Task<bool> DeleteGlobalPatternAsync(string patternId)
{
@@ -98,7 +98,7 @@ public class PreviewWhitelistService
}
/// <summary>
/// Returns all approved tenants (for Galactic Admin overview).
/// Returns all approved tenants (for Global Admin overview).
/// </summary>
public async Task<List<PreviewWhitelistEntity>> GetAllApprovedAsync()
{
@@ -83,7 +83,7 @@ namespace AutopilotMonitor.Functions.Services
}
/// <summary>
/// Gets audit log entries across all tenants (Galactic Admin Mode)
/// Gets audit log entries across all tenants (Global Admin Mode)
/// </summary>
public async Task<List<AuditLogEntry>> GetAllAuditLogsAsync(int maxResults = 100)
{
@@ -239,7 +239,7 @@ namespace AutopilotMonitor.Functions.Services
}
/// <summary>
/// Gets all app install summaries across all tenants (for galactic admin mode)
/// Gets all app install summaries across all tenants (for global admin mode)
/// </summary>
public async Task<List<AppInstallSummary>> GetAllAppInstallSummariesAsync()
{
@@ -488,7 +488,7 @@ namespace AutopilotMonitor.Functions.Services
}
/// <summary>
/// Gets user activity metrics across all tenants (for galactic admin)
/// Gets user activity metrics across all tenants (for global admin)
/// </summary>
public async Task<UserActivityMetrics> GetAllUserActivityMetricsAsync()
{
@@ -661,7 +661,7 @@ namespace AutopilotMonitor.Functions.Services
}
/// <summary>
/// Gets all sessions across all tenants (galactic admin mode), ordered newest-first,
/// Gets all sessions across all tenants (global admin mode), ordered newest-first,
/// with cursor-based pagination. Queries SessionsIndex cross-partition.
/// Cross-partition queries require in-memory sort since Azure Table Storage only
/// guarantees RowKey order within a single partition.
@@ -672,7 +672,7 @@ namespace AutopilotMonitor.Functions.Services
{
var indexTableClient = _tableServiceClient.GetTableClient(Constants.TableNames.SessionsIndex);
// For galactic queries, use StartedAt filter derived from cursor for efficiency
// For global queries, use StartedAt filter derived from cursor for efficiency
string? filter = null;
if (!string.IsNullOrEmpty(cursor))
{
@@ -745,7 +745,7 @@ namespace AutopilotMonitor.Functions.Services
}
/// <summary>
/// Fallback: queries the Sessions table directly for galactic admin (pre-migration).
/// Fallback: queries the Sessions table directly for global admin (pre-migration).
/// </summary>
private async Task<SessionPage> GetAllSessionsFromPrimaryTableAsync(int maxResults)
{
@@ -165,7 +165,7 @@ namespace AutopilotMonitor.Functions.Services
}
/// <summary>
/// Gets all tenant configurations (for Galactic Admin use)
/// Gets all tenant configurations (for Global Admin use)
/// </summary>
public async Task<List<TenantConfiguration>> GetAllConfigurationsAsync()
{
@@ -231,7 +231,7 @@ namespace AutopilotMonitor.Shared
public const string AdminConfiguration = "AdminConfiguration";
// Admin tables
public const string GalacticAdmins = "GalacticAdmins";
public const string GlobalAdmins = "GlobalAdmins";
public const string TenantAdmins = "TenantAdmins";
// Preview gating (temporary — remove after GA)
@@ -250,8 +250,8 @@ namespace AutopilotMonitor.Shared
// Bootstrap sessions (OOBE pre-enrollment agent deployment)
public const string BootstrapSessions = "BootstrapSessions";
// Galactic Admin in-app notifications (persistent until dismissed)
public const string GalacticNotifications = "GalacticNotifications";
// Global Admin in-app notifications (persistent until dismissed)
public const string GlobalNotifications = "GlobalNotifications";
// Vulnerability data cache (CPE mappings, CVE data)
public const string VulnerabilityCache = "VulnerabilityCache";
@@ -282,7 +282,7 @@ namespace AutopilotMonitor.Shared
PlatformStats,
TenantConfiguration,
AdminConfiguration,
GalacticAdmins,
GlobalAdmins,
TenantAdmins,
PreviewWhitelist,
PreviewConfig,
@@ -290,7 +290,7 @@ namespace AutopilotMonitor.Shared
BlockedVersions,
SessionReports,
BootstrapSessions,
GalacticNotifications,
GlobalNotifications,
VulnerabilityCache,
VulnerabilityReports,
SoftwareInventory
@@ -5,7 +5,7 @@ using System.Text.Json;
namespace AutopilotMonitor.Shared.Models
{
/// <summary>
/// Global platform configuration managed by Galactic Admins
/// Global platform configuration managed by Global Admins
/// Stored in Azure Table Storage with single instance
/// PartitionKey = "GlobalConfig"
/// RowKey = "config"
@@ -28,7 +28,7 @@ namespace AutopilotMonitor.Shared.Models
public DateTime LastUpdated { get; set; }
/// <summary>
/// Updated by (Galactic Admin user email)
/// Updated by (Global Admin user email)
/// </summary>
public string UpdatedBy { get; set; }
@@ -74,7 +74,7 @@ namespace AutopilotMonitor.Shared.Models
/// <summary>
/// Optional custom rate limit for this tenant (overrides RateLimitRequestsPerMinute)
/// If set (not null), this custom value takes precedence over the global default
/// Note: This is only configurable by Galactic Admins directly in the database
/// Note: This is only configurable by Global Admins directly in the database
/// </summary>
public int? CustomRateLimitRequestsPerMinute { get; set; } = null;
@@ -105,7 +105,7 @@ namespace AutopilotMonitor.Shared.Models
public bool ValidateCorporateIdentifier { get; set; } = false;
/// <summary>
/// Emergency bypass for agent security gate (Galactic Admin use only).
/// Emergency bypass for agent security gate (Global Admin use only).
/// If true, agent requests are accepted even when ValidateAutopilotDevice is false.
/// Default: false
/// </summary>
@@ -310,7 +310,7 @@ namespace AutopilotMonitor.Shared.Models
/// Whether OOBE Bootstrap Sessions are enabled for this tenant.
/// When false (default), the Bootstrap Sessions feature is hidden in the UI
/// and all bootstrap API endpoints reject requests for this tenant.
/// Only configurable by Galactic Admins.
/// Only configurable by Global Admins.
/// </summary>
public bool BootstrapTokenEnabled { get; set; } = false;
@@ -320,7 +320,7 @@ namespace AutopilotMonitor.Shared.Models
/// Whether the Unrestricted Mode feature is available for this tenant.
/// When false (default), the Unrestricted Mode section is hidden in the tenant settings UI
/// and UnrestrictedMode cannot be activated by tenant admins.
/// Only configurable by Galactic Admins.
/// Only configurable by Global Admins.
/// </summary>
public bool UnrestrictedModeEnabled { get; set; } = false;
@@ -2,7 +2,7 @@ namespace AutopilotMonitor.Shared.Models
{
/// <summary>
/// Represents a log file path (or wildcard pattern) to include in the diagnostics ZIP package.
/// Global (built-in) entries are defined by Galactic Admins; tenants may add their own.
/// Global (built-in) entries are defined by Global Admins; tenants may add their own.
/// </summary>
public class DiagnosticsLogPath
{
@@ -19,7 +19,7 @@ namespace AutopilotMonitor.Shared.Models
public string Description { get; set; }
/// <summary>
/// True when defined globally by a Galactic Admin — displayed as read-only for tenants.
/// True when defined globally by a Global Admin — displayed as read-only for tenants.
/// False when added by the tenant itself.
/// </summary>
public bool IsBuiltIn { get; set; }
@@ -108,11 +108,11 @@ export function AdminConfigProvider({ children }: { children: React.ReactNode })
const [error, setError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const isGalacticAdmin = user?.isGalacticAdmin === true;
const isGlobalAdmin = user?.isGlobalAdmin === true;
// Fetch admin configuration
useEffect(() => {
if (!isGalacticAdmin) return;
if (!isGlobalAdmin) return;
const fetchAdminConfig = async () => {
try {
@@ -150,11 +150,11 @@ export function AdminConfigProvider({ children }: { children: React.ReactNode })
};
fetchAdminConfig();
}, [isGalacticAdmin, getAccessToken]);
}, [isGlobalAdmin, getAccessToken]);
// Fetch tenants + preview whitelist
const fetchTenants = useCallback(async () => {
if (!isGalacticAdmin) return;
if (!isGlobalAdmin) return;
try {
setLoadingTenants(true);
@@ -187,7 +187,7 @@ export function AdminConfigProvider({ children }: { children: React.ReactNode })
} finally {
setLoadingTenants(false);
}
}, [isGalacticAdmin, getAccessToken]);
}, [isGlobalAdmin, getAccessToken]);
useEffect(() => {
fetchTenants();
@@ -76,7 +76,7 @@ export function AdminPageSections() {
{ id: "software", label: "Software Mapping", href: "/admin/software", group: "Software", groupIcon: <GlobeIcon /> },
], []);
usePageSections(items, "Galactic Admin", "route");
usePageSections(items, "Global Admin", "route");
return null;
}
@@ -68,7 +68,7 @@ export function AdminConfigSettingsSection({
<p className="text-sm text-indigo-800 dark:text-gray-300 mb-2">
Configure default DoS protection limits for all tenants. Normal enrollment generates ~10-30 requests/min.
<br />
<strong className="text-indigo-900 dark:text-indigo-100">Note:</strong> Tenants cannot change this value. Only Galactic Admins can override per tenant in tenant management section.
<strong className="text-indigo-900 dark:text-indigo-100">Note:</strong> Tenants cannot change this value. Only Global Admins can override per tenant in tenant management section.
</p>
<input
type="number"
@@ -78,7 +78,7 @@ export function DeviceBlockSection({
const fetchAllBlockedDevices = async () => {
try {
setLoadingAllBlocked(true);
const response = await authenticatedFetch(`${API_BASE_URL}/api/galactic/devices/blocked`, getAccessToken);
const response = await authenticatedFetch(`${API_BASE_URL}/api/global/devices/blocked`, getAccessToken);
if (!response.ok) throw new Error(`Failed to load blocked devices: ${response.statusText}`);
const data = await response.json();
setBlockedDevices(data.blocked ?? []);
@@ -80,7 +80,7 @@ export function SessionReportsSection({
setDownloadingBlob(blobName);
const res = await authenticatedFetch(
`${API_BASE_URL}/api/galactic/session-reports/download-url?blobName=${encodeURIComponent(blobName)}`,
`${API_BASE_URL}/api/global/session-reports/download-url?blobName=${encodeURIComponent(blobName)}`,
getAccessToken
);
if (!res.ok) throw new Error(`Failed to get download URL: ${res.statusText}`);
@@ -108,7 +108,7 @@ export function SessionReportsSection({
setNoteSaveResult(null);
const res = await authenticatedFetch(
`${API_BASE_URL}/api/galactic/session-reports/${selectedReport.reportId}/note`,
`${API_BASE_URL}/api/global/session-reports/${selectedReport.reportId}/note`,
getAccessToken,
{
method: "PATCH",
@@ -137,7 +137,7 @@ export function SessionReportsSection({
try {
setLoading(true);
const res = await authenticatedFetch(`${API_BASE_URL}/api/galactic/session-reports`, getAccessToken);
const res = await authenticatedFetch(`${API_BASE_URL}/api/global/session-reports`, getAccessToken);
if (res.status === 404) {
// Table/container doesn't exist yet — no reports submitted so far
@@ -587,7 +587,7 @@ export function TenantManagementSection({
) : (editingTenant.dataRetentionDays < 7 || editingTenant.dataRetentionDays > 180) ? (
<p className="text-xs text-amber-600 mt-1 font-medium">⚠ Outside tenant range (7–180) — field will be locked for tenant admins</p>
) : (
<p className="text-xs text-gray-400 mt-1">Tenant range: 7–180. Set 0 for infinite retention. No upper limit for Galactic.</p>
<p className="text-xs text-gray-400 mt-1">Tenant range: 7–180. Set 0 for infinite retention. No upper limit for Global.</p>
)}
</div>
@@ -11,15 +11,15 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
const router = useRouter();
useEffect(() => {
if (user && !user.isGalacticAdmin) {
if (user && !user.isGlobalAdmin) {
router.push("/dashboard");
}
}, [user, router]);
if (!user?.isGalacticAdmin) return null;
if (!user?.isGlobalAdmin) return null;
return (
<ProtectedRoute requireGalacticAdmin>
<ProtectedRoute requireGlobalAdmin>
<AdminConfigProvider>
{children}
</AdminConfigProvider>
@@ -113,11 +113,11 @@ export function SectionAgentMetrics() {
setError(null);
try {
const res = await authenticatedFetch(`${API_BASE_URL}/api/galactic/metrics/platform`, getAccessToken);
const res = await authenticatedFetch(`${API_BASE_URL}/api/global/metrics/platform`, getAccessToken);
if (!res.ok) {
if (res.status === 403) {
setError('Access denied. Galactic Admin privileges required.');
setError('Access denied. Global Admin privileges required.');
} else {
setError(`Failed to fetch platform metrics: ${res.status}`);
}
@@ -100,8 +100,8 @@ export function SectionPlatformUsage() {
}
setError(null);
// Platform-wide metrics - cross-tenant (Galactic Admin only)
const response = await authenticatedFetch(`${API_BASE_URL}/api/galactic/metrics/usage`, getAccessToken);
// Platform-wide metrics - cross-tenant (Global Admin only)
const response = await authenticatedFetch(`${API_BASE_URL}/api/global/metrics/usage`, getAccessToken);
if (!response.ok) {
throw new Error(`Failed to fetch platform usage metrics: ${response.statusText}`);
@@ -321,7 +321,7 @@ export function SectionTenantConfigReport() {
// Fetch tenant list
useEffect(() => {
if (!user?.isGalacticAdmin) return;
if (!user?.isGlobalAdmin) return;
const fetchTenants = async () => {
try {
setLoadingTenants(true);
@@ -349,7 +349,7 @@ export function SectionTenantConfigReport() {
}
};
fetchTenants();
}, [user?.isGalacticAdmin, getAccessToken]);
}, [user?.isGlobalAdmin, getAccessToken]);
// Fetch config for selected tenant
const fetchConfig = useCallback(async () => {
@@ -574,7 +574,7 @@ export function SectionTenantConfigReport() {
<ConfigRow label="Custom Log Paths" value={config.diagnosticsLogPathsJson} configKey="diagnosticsLogPathsJson" defaults={DEFAULTS} />
</Section>
<Section title="Feature Flags (Galactic)">
<Section title="Feature Flags (Global)">
<ConfigRow label="Bootstrap Token Enabled" value={config.bootstrapTokenEnabled} configKey="bootstrapTokenEnabled" defaults={DEFAULTS} />
<ConfigRow label="Unrestricted Mode Enabled" value={config.unrestrictedModeEnabled} configKey="unrestrictedModeEnabled" defaults={DEFAULTS} />
<ConfigRow label="Unrestricted Mode Active" value={config.unrestrictedMode} configKey="unrestrictedMode" defaults={DEFAULTS} />
@@ -71,10 +71,10 @@ export default function AnalyzeRulesPage() {
const [togglingRuleId, setTogglingRuleId] = useState<string | null>(null);
const [deletingRuleId, setDeletingRuleId] = useState<string | null>(null);
// Galactic admin mode
const [galacticAdminMode] = useState(() => {
// Global admin mode
const [globalAdminMode] = useState(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('galacticAdminMode') === 'true';
return localStorage.getItem('globalAdminMode') === 'true';
}
return false;
});
@@ -82,7 +82,7 @@ export default function AnalyzeRulesPage() {
const [selectedTenantId, setSelectedTenantId] = useState<string>('');
useEffect(() => {
if (!galacticAdminMode || !user?.isGalacticAdmin) return;
if (!globalAdminMode || !user?.isGlobalAdmin) return;
const fetchTenants = async () => {
try {
const response = await authenticatedFetch(`${API_BASE_URL}/api/config/all`, getAccessToken);
@@ -104,7 +104,7 @@ export default function AnalyzeRulesPage() {
}
};
fetchTenants();
}, [galacticAdminMode, user?.isGalacticAdmin, getAccessToken]);
}, [globalAdminMode, user?.isGlobalAdmin, getAccessToken]);
useEffect(() => {
if (tenantId && !selectedTenantId) {
@@ -112,21 +112,21 @@ export default function AnalyzeRulesPage() {
}
}, [tenantId]);
const isGalacticOverride = galacticAdminMode && user?.isGalacticAdmin && selectedTenantId && selectedTenantId !== tenantId;
const effectiveTenantId = (galacticAdminMode && user?.isGalacticAdmin && selectedTenantId) ? selectedTenantId : tenantId;
const isReadOnly = !user?.isTenantAdmin && !user?.isGalacticAdmin;
const isGlobalOverride = globalAdminMode && user?.isGlobalAdmin && selectedTenantId && selectedTenantId !== tenantId;
const effectiveTenantId = (globalAdminMode && user?.isGlobalAdmin && selectedTenantId) ? selectedTenantId : tenantId;
const isReadOnly = !user?.isTenantAdmin && !user?.isGlobalAdmin;
const fetchRules = useCallback(async () => {
if (!effectiveTenantId) return;
const url = isGalacticOverride
? `${API_BASE_URL}/api/galactic/rules/analyze?tenantId=${effectiveTenantId}`
const url = isGlobalOverride
? `${API_BASE_URL}/api/global/rules/analyze?tenantId=${effectiveTenantId}`
: `${API_BASE_URL}/api/rules/analyze`;
await fetchRulesExec(
url,
undefined,
{ transform: (d) => { const r = d as { success?: boolean; rules?: AnalyzeRule[] }; return r.success && Array.isArray(r.rules) ? r.rules : []; } }
);
}, [effectiveTenantId, isGalacticOverride, fetchRulesExec]);
}, [effectiveTenantId, isGlobalOverride, fetchRulesExec]);
useEffect(() => {
fetchRules();
@@ -322,12 +322,12 @@ export default function AnalyzeRulesPage() {
return (
<ProtectedRoute>
<div className="min-h-screen bg-gray-50">
{galacticAdminMode && user?.isGalacticAdmin && (
{globalAdminMode && user?.isGlobalAdmin && (
<div className="bg-purple-700 text-white text-sm px-4 py-2 flex items-center justify-center space-x-2">
<svg className="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="font-medium">Galactic Admin View</span>
<span className="font-medium">Global Admin View</span>
<span className="text-purple-300">&mdash; access to all tenants</span>
</div>
)}
@@ -341,7 +341,7 @@ export default function AnalyzeRulesPage() {
<p className="text-sm text-gray-600 mt-1">Manage event analysis rules for issue detection</p>
</div>
</div>
{galacticAdminMode && user?.isGalacticAdmin && tenants.length > 0 && (
{globalAdminMode && user?.isGlobalAdmin && tenants.length > 0 && (
<div className="flex items-center gap-3">
<label className="text-sm text-gray-500 hidden sm:inline">Tenant:</label>
<select
@@ -451,9 +451,9 @@ export default function AnalyzeRulesPage() {
},
]}
onExportAll={isReadOnly ? undefined : handleExportAll}
onCreateNew={isReadOnly || isGalacticOverride ? undefined : () => { setShowCreateForm(!showCreateForm); if (showCreateForm) setNewRule({ ...EMPTY_FORM, conditions: [{ ...EMPTY_CONDITION }] }); }}
onCreateNew={isReadOnly || isGlobalOverride ? undefined : () => { setShowCreateForm(!showCreateForm); if (showCreateForm) setNewRule({ ...EMPTY_FORM, conditions: [{ ...EMPTY_CONDITION }] }); }}
createLabel="Create Custom Rule"
showCreateForm={showCreateForm && !isGalacticOverride && !isReadOnly}
showCreateForm={showCreateForm && !isGlobalOverride && !isReadOnly}
/>
{/* Create Custom Rule Form */}
@@ -41,9 +41,9 @@ export default function AuditPage() {
const [currentPage, setCurrentPage] = useState(1);
const logsPerPage = 12;
const [galacticAdminMode] = useState(() => {
const [globalAdminMode] = useState(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('galacticAdminMode') === 'true';
return localStorage.getItem('globalAdminMode') === 'true';
}
return false;
});
@@ -56,8 +56,8 @@ export default function AuditPage() {
setLoading(true);
}
const endpoint = galacticAdminMode
? `${API_BASE_URL}/api/galactic/audit/logs`
const endpoint = globalAdminMode
? `${API_BASE_URL}/api/global/audit/logs`
: `${API_BASE_URL}/api/audit/logs`;
const response = await authenticatedFetch(endpoint, getAccessToken);
@@ -86,7 +86,7 @@ export default function AuditPage() {
};
useEffect(() => {
if (!galacticAdminMode && !tenantId) return;
if (!globalAdminMode && !tenantId) return;
fetchAuditLogs();
}, [tenantId]);
@@ -166,12 +166,12 @@ export default function AuditPage() {
return (
<ProtectedRoute>
<div className="min-h-screen bg-gray-50">
{galacticAdminMode && (
{globalAdminMode && (
<div className="bg-purple-700 text-white text-sm px-4 py-2 flex items-center justify-center space-x-2">
<svg className="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="font-medium">Galactic Admin View</span>
<span className="font-medium">Global Admin View</span>
<span className="text-purple-300">&mdash; aggregating data across all tenants</span>
</div>
)}
@@ -11,7 +11,7 @@ interface ColumnDef {
label: string;
defaultVisible: boolean;
adminOnly?: boolean;
galacticOnly?: boolean;
globalOnly?: boolean;
sortKey?: keyof Session;
// Session field used for column-level filtering; if set, column is filterable
filterKey?: keyof Session;
@@ -19,7 +19,7 @@ interface ColumnDef {
const ALL_COLUMNS: ColumnDef[] = [
{ key: "device", label: "Device", defaultVisible: true, sortKey: "deviceName" },
{ key: "tenantId", label: "Tenant ID", defaultVisible: false, galacticOnly: true },
{ key: "tenantId", label: "Tenant ID", defaultVisible: false, globalOnly: true },
{ key: "model", label: "Model", defaultVisible: true, sortKey: "model", filterKey: "manufacturer" },
{ key: "status", label: "Status", defaultVisible: true, sortKey: "status", filterKey: "status" },
{ key: "eventCount", label: "Events", defaultVisible: true, sortKey: "eventCount" },
@@ -71,14 +71,14 @@ interface SessionTableProps {
loadingMore: boolean;
onLoadMore: () => void;
adminMode: boolean;
galacticAdminMode: boolean;
globalAdminMode: boolean;
tenantIdFilter: string;
onTenantIdFilterChange: (value: string) => void;
onTenantIdFilterSubmit: () => void;
onTenantIdFilterClear: () => void;
blockedDevicesSet: Set<string>;
isPreviewBlocked: boolean;
user: { isGalacticAdmin?: boolean } | null;
user: { isGlobalAdmin?: boolean } | null;
columnFilters: Record<string, Set<string>>;
onColumnFiltersChange: (filters: Record<string, Set<string>>) => void;
onDeleteSession: (sessionId: string, tenantId: string, deviceName?: string) => void;
@@ -107,7 +107,7 @@ export function SessionTable({
loadingMore,
onLoadMore,
adminMode,
galacticAdminMode,
globalAdminMode,
tenantIdFilter,
onTenantIdFilterChange,
onTenantIdFilterSubmit,
@@ -181,14 +181,14 @@ export function SessionTable({
// Filter columns based on mode and visibility
const activeColumns = ALL_COLUMNS.filter((col) => {
if (col.adminOnly && !adminMode) return false;
if (col.galacticOnly && !galacticAdminMode) return false;
if (col.globalOnly && !globalAdminMode) return false;
return visibleColumns.has(col.key);
});
// Columns available for the selector (exclude mode-gated ones that aren't applicable)
const selectableColumns = ALL_COLUMNS.filter((col) => {
if (col.adminOnly && !adminMode) return false;
if (col.galacticOnly && !galacticAdminMode) return false;
if (col.globalOnly && !globalAdminMode) return false;
return true;
});
@@ -280,8 +280,8 @@ export function SessionTable({
</div>
</div>
{/* Tenant ID Filter (Galactic Admin only) */}
{galacticAdminMode && (
{/* Tenant ID Filter (Global Admin only) */}
{globalAdminMode && (
<div className="mb-3 flex items-center gap-2">
<div className="relative flex-1">
<input
@@ -459,7 +459,7 @@ export function SessionTable({
columnKey={col.key}
session={session}
adminMode={adminMode}
galacticAdminMode={galacticAdminMode}
globalAdminMode={globalAdminMode}
blockedDevicesSet={blockedDevicesSet}
user={user}
onDeleteSession={onDeleteSession}
@@ -528,7 +528,7 @@ function SessionCell({
columnKey,
session,
adminMode,
galacticAdminMode,
globalAdminMode,
blockedDevicesSet,
user,
onDeleteSession,
@@ -537,9 +537,9 @@ function SessionCell({
columnKey: string;
session: Session;
adminMode: boolean;
galacticAdminMode: boolean;
globalAdminMode: boolean;
blockedDevicesSet: Set<string>;
user: { isGalacticAdmin?: boolean } | null;
user: { isGlobalAdmin?: boolean } | null;
onDeleteSession: (sessionId: string, tenantId: string, deviceName?: string) => void;
onBlockDevice: (serialNumber: string, tenantId: string, deviceName?: string) => void;
}) {
@@ -702,7 +702,7 @@ function SessionCell({
return (
<td className="pl-3 pr-4 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex items-center justify-end gap-2">
{galacticAdminMode && user?.isGalacticAdmin && (
{globalAdminMode && user?.isGlobalAdmin && (
<button
onClick={(e) => {
e.stopPropagation();
@@ -5,15 +5,15 @@ import { useEffect, useState, useRef } from "react";
export function SettingsMenu({
adminMode,
onAdminModeChange,
galacticAdminMode,
onGalacticAdminModeChange,
globalAdminMode,
onGlobalAdminModeChange,
user,
}: {
adminMode: boolean;
onAdminModeChange: (enabled: boolean) => void;
galacticAdminMode: boolean;
onGalacticAdminModeChange: (enabled: boolean) => void;
user: { displayName: string; email: string; isGalacticAdmin?: boolean } | null;
globalAdminMode: boolean;
onGlobalAdminModeChange: (enabled: boolean) => void;
user: { displayName: string; email: string; isGlobalAdmin?: boolean } | null;
}) {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
@@ -82,28 +82,28 @@ export function SettingsMenu({
)}
</div>
{/* Galactic Admin Toggle - Only visible to actual galactic admins */}
{user?.isGalacticAdmin && (
{/* Global Admin Toggle - Only visible to actual global admins */}
{user?.isGlobalAdmin && (
<div className="mb-3">
<div className="flex items-center justify-between p-3 rounded-lg bg-purple-50">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-700">Galactic Admin</span>
{galacticAdminMode && <span className="text-xs text-purple-700 font-semibold">ACTIVE</span>}
<span className="text-sm font-medium text-gray-700">Global Admin</span>
{globalAdminMode && <span className="text-xs text-purple-700 font-semibold">ACTIVE</span>}
</div>
<button
onClick={() => onGalacticAdminModeChange(!galacticAdminMode)}
onClick={() => onGlobalAdminModeChange(!globalAdminMode)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
galacticAdminMode ? 'bg-purple-600' : 'bg-gray-300'
globalAdminMode ? 'bg-purple-600' : 'bg-gray-300'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
galacticAdminMode ? 'translate-x-6' : 'translate-x-1'
globalAdminMode ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
{galacticAdminMode && (
{globalAdminMode && (
<p className="mt-2 text-xs text-purple-700 px-3">
Shows ALL sessions across ALL tenants
</p>
@@ -58,9 +58,9 @@ export default function Home() {
}
return false;
});
const [galacticAdminMode, setGalacticAdminMode] = useState(() => {
const [globalAdminMode, setGlobalAdminMode] = useState(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('galacticAdminMode') === 'true';
return localStorage.getItem('globalAdminMode') === 'true';
}
return false;
});
@@ -72,14 +72,14 @@ export default function Home() {
// Track if initial fetch has been done to prevent duplicate calls in React StrictMode
const hasInitialFetch = useRef(false);
// Track if galactic admin mode has been initialized (to skip initial mount in useEffect)
const hasGalacticModeInitialized = useRef(false);
// Track if global admin mode has been initialized (to skip initial mount in useEffect)
const hasGlobalModeInitialized = useRef(false);
// Refs for SignalR handlers to access current filter state (avoids stale closures)
const tenantIdFilterRef = useRef(tenantIdFilter);
tenantIdFilterRef.current = tenantIdFilter;
const galacticAdminModeRef = useRef(galacticAdminMode);
galacticAdminModeRef.current = galacticAdminMode;
const globalAdminModeRef = useRef(globalAdminMode);
globalAdminModeRef.current = globalAdminMode;
const tenantIdRef = useRef(tenantId);
tenantIdRef.current = tenantId;
@@ -89,12 +89,12 @@ export default function Home() {
// Track whether we've been connected at least once — used to detect reconnects
const wasConnectedRef = useRef(false);
const fetchSessions = async (loadMoreCursor?: string, galacticTenantIdOverride?: string) => {
const fetchSessions = async (loadMoreCursor?: string, globalTenantIdOverride?: string) => {
try {
// Use different endpoint based on galactic admin mode
const effectiveTenantFilter = galacticTenantIdOverride !== undefined ? galacticTenantIdOverride : tenantIdFilter.trim();
let endpoint = galacticAdminMode
? `${API_BASE_URL}/api/galactic/sessions${effectiveTenantFilter ? `?tenantId=${encodeURIComponent(effectiveTenantFilter)}` : ''}`
// Use different endpoint based on global admin mode
const effectiveTenantFilter = globalTenantIdOverride !== undefined ? globalTenantIdOverride : tenantIdFilter.trim();
let endpoint = globalAdminMode
? `${API_BASE_URL}/api/global/sessions${effectiveTenantFilter ? `?tenantId=${encodeURIComponent(effectiveTenantFilter)}` : ''}`
: `${API_BASE_URL}/api/sessions?tenantId=${tenantId}`;
// Append cursor for "Load More" requests
@@ -143,13 +143,13 @@ export default function Home() {
};
const fetchBlockedDevices = async (currentSessions: Session[]) => {
if (!adminMode || !galacticAdminMode) {
if (!adminMode || !globalAdminMode) {
setBlockedDevicesSet(new Set());
return;
}
try {
const tenantIds = galacticAdminMode
const tenantIds = globalAdminMode
? [...new Set(currentSessions.map(s => s.tenantId))]
: tenantId ? [tenantId] : [];
@@ -186,7 +186,7 @@ export default function Home() {
// Redirect regular users (non-admin, non-operator) to progress portal – they must never see the session list
useEffect(() => {
if (user && !user.isTenantAdmin && !user.isGalacticAdmin && user.role !== 'Operator') {
if (user && !user.isTenantAdmin && !user.isGlobalAdmin && user.role !== 'Operator') {
router.replace("/progress");
}
}, [user, router]);
@@ -194,7 +194,7 @@ export default function Home() {
useEffect(() => {
const fetchTenantSecurityConfig = async () => {
if (!tenantId) return;
if (user && !user.isTenantAdmin && !user.isGalacticAdmin && user.role !== 'Operator') return;
if (user && !user.isTenantAdmin && !user.isGlobalAdmin && user.role !== 'Operator') return;
try {
const response = await authenticatedFetch(`${API_BASE_URL}/api/config/${tenantId}`, getAccessToken);
@@ -221,10 +221,10 @@ export default function Home() {
// Wait for a real tenantId before fetching — TenantContext initializes to '' and
// updates asynchronously once AuthContext finishes loading the user token.
useEffect(() => {
if (user && !user.isTenantAdmin && !user.isGalacticAdmin && user.role !== 'Operator') {
if (user && !user.isTenantAdmin && !user.isGlobalAdmin && user.role !== 'Operator') {
return; // regular users are being redirected, don't fetch
}
if (!galacticAdminMode && !tenantId) return; // wait for real tenant ID
if (!globalAdminMode && !tenantId) return; // wait for real tenant ID
// Prevent duplicate fetches in React StrictMode (development double-mounting)
if (hasInitialFetch.current) {
return;
@@ -234,7 +234,7 @@ export default function Home() {
// Only fetch sessions on load, no automatic health check
fetchSessions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user, tenantId, galacticAdminMode]); // re-run once user and tenantId are known
}, [user, tenantId, globalAdminMode]); // re-run once user and tenantId are known
// Join tenant group when SignalR is connected (for multi-tenancy)
useEffect(() => {
@@ -266,42 +266,42 @@ export default function Home() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isConnected, tenantId]);
// Join/leave galactic-admins group when Galactic Admin mode changes
// Join/leave global-admins group when Global Admin mode changes
useEffect(() => {
if (!isConnected) return;
if (galacticAdminMode) {
console.log('[Home] Galactic Admin mode enabled: joining galactic-admins group');
joinGroup('galactic-admins');
if (globalAdminMode) {
console.log('[Home] Global Admin mode enabled: joining global-admins group');
joinGroup('global-admins');
} else {
console.log('[Home] Galactic Admin mode disabled: leaving galactic-admins group');
leaveGroup('galactic-admins');
console.log('[Home] Global Admin mode disabled: leaving global-admins group');
leaveGroup('global-admins');
}
return () => {
// Clean up galactic-admins group on unmount if currently in Galactic Admin mode
if (galacticAdminMode) {
console.log('[Home] Component unmounting: leaving galactic-admins group');
leaveGroup('galactic-admins');
// Clean up global-admins group on unmount if currently in Global Admin mode
if (globalAdminMode) {
console.log('[Home] Component unmounting: leaving global-admins group');
leaveGroup('global-admins');
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isConnected, galacticAdminMode]);
}, [isConnected, globalAdminMode]);
// Setup SignalR listener - re-register when connection changes
useEffect(() => {
const handleNewSession = (data: { sessionId: string; tenantId: string; session: Session }) => {
console.log('New session registered', data);
// In non-galactic mode, only accept sessions from the user's own tenant
if (!galacticAdminModeRef.current && tenantIdRef.current && data.tenantId !== tenantIdRef.current) {
console.log(`Ignoring newSession from tenant ${data.tenantId} (not in galactic mode, own tenant: ${tenantIdRef.current})`);
// In non-global mode, only accept sessions from the user's own tenant
if (!globalAdminModeRef.current && tenantIdRef.current && data.tenantId !== tenantIdRef.current) {
console.log(`Ignoring newSession from tenant ${data.tenantId} (not in global mode, own tenant: ${tenantIdRef.current})`);
return;
}
// In galactic admin mode with an active tenant filter, ignore sessions from other tenants
// In global admin mode with an active tenant filter, ignore sessions from other tenants
const activeFilter = tenantIdFilterRef.current.trim();
if (galacticAdminModeRef.current && activeFilter && data.tenantId !== activeFilter) {
if (globalAdminModeRef.current && activeFilter && data.tenantId !== activeFilter) {
console.log(`Ignoring newSession from tenant ${data.tenantId} (filtered to ${activeFilter})`);
return;
}
@@ -363,25 +363,25 @@ export default function Home() {
localStorage.setItem('adminMode', adminMode.toString());
}, [adminMode]);
// Save galactic admin mode to localStorage when it changes
// Save global admin mode to localStorage when it changes
useEffect(() => {
localStorage.setItem('galacticAdminMode', galacticAdminMode.toString());
}, [galacticAdminMode]);
localStorage.setItem('globalAdminMode', globalAdminMode.toString());
}, [globalAdminMode]);
// Clear blocked devices set when admin mode or galactic admin mode is turned off
// Clear blocked devices set when admin mode or global admin mode is turned off
useEffect(() => {
if (!adminMode || !galacticAdminMode) {
if (!adminMode || !globalAdminMode) {
setBlockedDevicesSet(new Set());
}
}, [adminMode, galacticAdminMode]);
}, [adminMode, globalAdminMode]);
// Disable galactic admin mode if user is not a galactic admin
// Disable global admin mode if user is not a global admin
useEffect(() => {
if (user && !user.isGalacticAdmin && galacticAdminMode) {
console.log('[Home] User is not a galactic admin, disabling galactic admin mode');
setGalacticAdminMode(false);
if (user && !user.isGlobalAdmin && globalAdminMode) {
console.log('[Home] User is not a global admin, disabling global admin mode');
setGlobalAdminMode(false);
}
}, [user, galacticAdminMode]);
}, [user, globalAdminMode]);
// Listen for localStorage changes from other components (e.g., Navbar)
useEffect(() => {
@@ -389,20 +389,20 @@ export default function Home() {
if (e.key === 'adminMode' && e.newValue !== null) {
setAdminMode(e.newValue === 'true');
}
if (e.key === 'galacticAdminMode' && e.newValue !== null) {
setGalacticAdminMode(e.newValue === 'true');
if (e.key === 'globalAdminMode' && e.newValue !== null) {
setGlobalAdminMode(e.newValue === 'true');
}
};
const handleCustomStorageChange = () => {
const newAdminMode = localStorage.getItem('adminMode') === 'true';
const newGalacticMode = localStorage.getItem('galacticAdminMode') === 'true';
const newGlobalMode = localStorage.getItem('globalAdminMode') === 'true';
if (newAdminMode !== adminMode) {
setAdminMode(newAdminMode);
}
if (newGalacticMode !== galacticAdminMode) {
setGalacticAdminMode(newGalacticMode);
if (newGlobalMode !== globalAdminMode) {
setGlobalAdminMode(newGlobalMode);
}
};
@@ -413,17 +413,17 @@ export default function Home() {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('localStorageChange', handleCustomStorageChange);
};
}, [adminMode, galacticAdminMode]);
}, [adminMode, globalAdminMode]);
// Refetch sessions when galactic admin mode changes
// Refetch sessions when global admin mode changes
useEffect(() => {
if (!hasGalacticModeInitialized.current) {
hasGalacticModeInitialized.current = true;
if (!hasGlobalModeInitialized.current) {
hasGlobalModeInitialized.current = true;
return;
}
// Clear tenant filter when leaving galactic mode
if (!galacticAdminMode) {
// Clear tenant filter when leaving global mode
if (!globalAdminMode) {
setTenantIdFilter("");
}
@@ -433,7 +433,7 @@ export default function Home() {
setCurrentPage(1);
setLoading(true);
fetchSessions();
}, [galacticAdminMode]);
}, [globalAdminMode]);
// Reset to page 1 when search query, status filter, sort, or page size changes
useEffect(() => {
@@ -520,7 +520,7 @@ export default function Home() {
tenantId: sessionToBlock.tenantId,
serialNumber: sessionToBlock.serialNumber,
durationHours: 24,
reason: `Blocked from dashboard by Galactic Admin`
reason: `Blocked from dashboard by Global Admin`
})
});
@@ -557,7 +557,7 @@ export default function Home() {
// Client-side tenant filter: ensures sessions from other tenants are never displayed,
// regardless of how they entered the sessions state (SignalR, race conditions, etc.)
const effectiveSessions = galacticAdminMode
const effectiveSessions = globalAdminMode
? (tenantIdFilter.trim()
? sessions.filter(s => s.tenantId === tenantIdFilter.trim())
: sessions)
@@ -805,7 +805,7 @@ export default function Home() {
loadingMore={loadingMore}
onLoadMore={loadMore}
adminMode={adminMode}
galacticAdminMode={galacticAdminMode}
globalAdminMode={globalAdminMode}
tenantIdFilter={tenantIdFilter}
onTenantIdFilterChange={applyTenantIdFilter}
onTenantIdFilterSubmit={submitTenantIdFilter}
@@ -38,16 +38,16 @@ export default function DiagnosisPage() {
const { getAccessToken } = useAuth();
const { addNotification } = useNotifications();
const [galacticAdminMode] = useState(() => {
const [globalAdminMode] = useState(() => {
if (typeof window !== "undefined") {
return localStorage.getItem("galacticAdminMode") === "true";
return localStorage.getItem("globalAdminMode") === "true";
}
return false;
});
useEffect(() => {
if (!sessionId) return;
if (!galacticAdminMode && !tenantId) return; // wait for real tenant ID
if (!globalAdminMode && !tenantId) return; // wait for real tenant ID
sessionIdRef.current = sessionId;
if (lastFetchedSessionId.current !== sessionId) {
hasInitialFetch.current = false;
@@ -57,7 +57,7 @@ export default function DiagnosisPage() {
if (hasInitialFetch.current) return;
hasInitialFetch.current = true;
fetchSessionDetails();
}, [sessionId, tenantId, galacticAdminMode]);
}, [sessionId, tenantId, globalAdminMode]);
useEffect(() => {
if (sessionTenantId && sessionId) {
@@ -117,8 +117,8 @@ export default function DiagnosisPage() {
const fetchSessionDetails = async () => {
try {
const endpoint = galacticAdminMode
? `${API_BASE_URL}/api/galactic/sessions`
const endpoint = globalAdminMode
? `${API_BASE_URL}/api/global/sessions`
: `${API_BASE_URL}/api/sessions?tenantId=${tenantId}`;
const response = await authenticatedFetch(endpoint, getAccessToken);
if (response.ok) {
@@ -227,7 +227,7 @@ export function SectionSettings() {
<SettingsRow name="Additional Log Paths">
<span>
Extra log files or wildcard patterns added to the diagnostics ZIP package. Global paths are defined
platform-wide by the Galactic Admin and always included. Tenant Admins can add their own paths in addition
platform-wide by the Global Admin and always included. Tenant Admins can add their own paths in addition
to the global ones (e.g. custom app logs or third-party agent logs).
</span>
<span className="block mt-2 text-xs text-gray-500">
@@ -51,20 +51,20 @@ export default function FleetHealthPage() {
const { getAccessToken } = useAuth();
const { addNotification } = useNotifications();
const [galacticAdminMode] = useState(() => {
const [globalAdminMode] = useState(() => {
if (typeof window !== "undefined") {
return localStorage.getItem("galacticAdminMode") === "true";
return localStorage.getItem("globalAdminMode") === "true";
}
return false;
});
useEffect(() => {
if (!galacticAdminMode && !tenantId) return; // wait for real tenant ID
if (!globalAdminMode && !tenantId) return; // wait for real tenant ID
if (hasInitialFetch.current) return;
hasInitialFetch.current = true;
fetchSessions();
fetchAppMetrics(timeRange);
}, [tenantId, galacticAdminMode]);
}, [tenantId, globalAdminMode]);
useEffect(() => {
if (isTimeRangeMount.current) {
@@ -129,8 +129,8 @@ export default function FleetHealthPage() {
const fetchSessions = async () => {
try {
const endpoint = galacticAdminMode
? `${API_BASE_URL}/api/galactic/sessions`
const endpoint = globalAdminMode
? `${API_BASE_URL}/api/global/sessions`
: `${API_BASE_URL}/api/sessions?tenantId=${tenantId}`;
const response = await authenticatedFetch(endpoint, getAccessToken);
if (response.ok) {
@@ -154,8 +154,8 @@ export default function FleetHealthPage() {
const fetchAppMetrics = async (range: "7d" | "30d" | "90d" = timeRange) => {
try {
const days = range === "7d" ? 7 : range === "30d" ? 30 : 90;
const endpoint = galacticAdminMode
? `${API_BASE_URL}/api/galactic/metrics/app?days=${days}`
const endpoint = globalAdminMode
? `${API_BASE_URL}/api/global/metrics/app?days=${days}`
: `${API_BASE_URL}/api/metrics/app?tenantId=${tenantId}&days=${days}`;
const response = await authenticatedFetch(endpoint, getAccessToken);
if (response.ok) {
@@ -352,12 +352,12 @@ export default function FleetHealthPage() {
return (
<ProtectedRoute>
<div className="min-h-screen bg-gray-50">
{galacticAdminMode && (
{globalAdminMode && (
<div className="bg-purple-700 text-white text-sm px-4 py-2 flex items-center justify-center space-x-2">
<svg className="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="font-medium">Galactic Admin View</span>
<span className="font-medium">Global Admin View</span>
<span className="text-purple-300">&mdash; aggregating data across all tenants</span>
</div>
)}
@@ -72,10 +72,10 @@ export default function GatherRulesPage() {
// Unrestricted mode (fetched from tenant config for validation indicators)
const [unrestrictedMode, setUnrestrictedMode] = useState(false);
// Galactic admin mode
const [galacticAdminMode] = useState(() => {
// Global admin mode
const [globalAdminMode] = useState(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('galacticAdminMode') === 'true';
return localStorage.getItem('globalAdminMode') === 'true';
}
return false;
});
@@ -83,7 +83,7 @@ export default function GatherRulesPage() {
const [selectedTenantId, setSelectedTenantId] = useState<string>('');
useEffect(() => {
if (!galacticAdminMode || !user?.isGalacticAdmin) return;
if (!globalAdminMode || !user?.isGlobalAdmin) return;
const fetchTenants = async () => {
try {
const response = await authenticatedFetch(`${API_BASE_URL}/api/config/all`, getAccessToken);
@@ -105,7 +105,7 @@ export default function GatherRulesPage() {
}
};
fetchTenants();
}, [galacticAdminMode, user?.isGalacticAdmin, getAccessToken]);
}, [globalAdminMode, user?.isGlobalAdmin, getAccessToken]);
useEffect(() => {
if (tenantId && !selectedTenantId) {
@@ -113,21 +113,21 @@ export default function GatherRulesPage() {
}
}, [tenantId]);
const isGalacticOverride = galacticAdminMode && user?.isGalacticAdmin && selectedTenantId && selectedTenantId !== tenantId;
const effectiveTenantId = (galacticAdminMode && user?.isGalacticAdmin && selectedTenantId) ? selectedTenantId : tenantId;
const isReadOnly = !user?.isTenantAdmin && !user?.isGalacticAdmin;
const isGlobalOverride = globalAdminMode && user?.isGlobalAdmin && selectedTenantId && selectedTenantId !== tenantId;
const effectiveTenantId = (globalAdminMode && user?.isGlobalAdmin && selectedTenantId) ? selectedTenantId : tenantId;
const isReadOnly = !user?.isTenantAdmin && !user?.isGlobalAdmin;
const fetchRules = useCallback(async () => {
if (!effectiveTenantId) return;
const url = isGalacticOverride
? `${API_BASE_URL}/api/galactic/rules/gather?tenantId=${effectiveTenantId}`
const url = isGlobalOverride
? `${API_BASE_URL}/api/global/rules/gather?tenantId=${effectiveTenantId}`
: `${API_BASE_URL}/api/rules/gather?tenantId=${effectiveTenantId}`;
await fetchRulesExec(
url,
undefined,
{ transform: (d) => (d as { rules?: GatherRule[] }).rules || [] }
);
}, [effectiveTenantId, isGalacticOverride, fetchRulesExec]);
}, [effectiveTenantId, isGlobalOverride, fetchRulesExec]);
useEffect(() => {
if (effectiveTenantId) {
@@ -140,8 +140,8 @@ export default function GatherRulesPage() {
if (!effectiveTenantId) return;
const fetchConfig = async () => {
try {
const url = isGalacticOverride
? `${API_BASE_URL}/api/galactic/config/${effectiveTenantId}`
const url = isGlobalOverride
? `${API_BASE_URL}/api/global/config/${effectiveTenantId}`
: `${API_BASE_URL}/api/config/${effectiveTenantId}`;
const response = await authenticatedFetch(url, getAccessToken);
if (response.ok) {
@@ -153,7 +153,7 @@ export default function GatherRulesPage() {
}
};
fetchConfig();
}, [effectiveTenantId, isGalacticOverride, getAccessToken]);
}, [effectiveTenantId, isGlobalOverride, getAccessToken]);
const handleToggleRule = async (rule: GatherRule) => {
setTogglingRule(rule.ruleId);
@@ -395,12 +395,12 @@ export default function GatherRulesPage() {
return (
<ProtectedRoute>
<div className="min-h-screen bg-gray-50">
{galacticAdminMode && user?.isGalacticAdmin && (
{globalAdminMode && user?.isGlobalAdmin && (
<div className="bg-purple-700 text-white text-sm px-4 py-2 flex items-center justify-center space-x-2">
<svg className="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="font-medium">Galactic Admin View</span>
<span className="font-medium">Global Admin View</span>
<span className="text-purple-300">&mdash; access to all tenants</span>
</div>
)}
@@ -414,7 +414,7 @@ export default function GatherRulesPage() {
<p className="text-sm text-gray-600 mt-1">Manage data collection rules for device enrollment</p>
</div>
</div>
{galacticAdminMode && user?.isGalacticAdmin && tenants.length > 0 && (
{globalAdminMode && user?.isGlobalAdmin && tenants.length > 0 && (
<div className="flex items-center gap-3">
<label className="text-sm text-gray-500 hidden sm:inline">Tenant:</label>
<select
@@ -519,9 +519,9 @@ export default function GatherRulesPage() {
},
]}
onExportAll={isReadOnly ? undefined : handleExportAll}
onCreateNew={isReadOnly || isGalacticOverride ? undefined : () => { setShowCreateForm(!showCreateForm); if (showCreateForm) setNewRule({ ...EMPTY_FORM }); }}
onCreateNew={isReadOnly || isGlobalOverride ? undefined : () => { setShowCreateForm(!showCreateForm); if (showCreateForm) setNewRule({ ...EMPTY_FORM }); }}
createLabel="Create Custom Rule"
showCreateForm={showCreateForm && !isGalacticOverride && !isReadOnly}
showCreateForm={showCreateForm && !isGlobalOverride && !isReadOnly}
/>
{/* Create Custom Rule Form */}
@@ -108,9 +108,9 @@ export default function GeographicPerformancePage() {
const { tenantId } = useTenant();
const { getAccessToken } = useAuth();
const [galacticAdminMode] = useState(() => {
const [globalAdminMode] = useState(() => {
if (typeof window !== "undefined") {
return localStorage.getItem("galacticAdminMode") === "true";
return localStorage.getItem("globalAdminMode") === "true";
}
return false;
});
@@ -118,8 +118,8 @@ export default function GeographicPerformancePage() {
const fetchGeoMetrics = useCallback(async (range: TimeRange = timeRange, group: GroupBy = groupBy) => {
try {
const days = range === "7d" ? 7 : range === "30d" ? 30 : 90;
const endpoint = galacticAdminMode
? `${API_BASE_URL}/api/galactic/metrics/geographic?days=${days}&groupBy=${group}`
const endpoint = globalAdminMode
? `${API_BASE_URL}/api/global/metrics/geographic?days=${days}&groupBy=${group}`
: `${API_BASE_URL}/api/metrics/geographic?tenantId=${tenantId}&days=${days}&groupBy=${group}`;
const response = await authenticatedFetch(endpoint, getAccessToken);
if (response.ok) {
@@ -135,14 +135,14 @@ export default function GeographicPerformancePage() {
} finally {
setLoading(false);
}
}, [galacticAdminMode, tenantId, getAccessToken, timeRange, groupBy]);
}, [globalAdminMode, tenantId, getAccessToken, timeRange, groupBy]);
useEffect(() => {
if (!galacticAdminMode && !tenantId) return;
if (!globalAdminMode && !tenantId) return;
if (hasInitialFetch.current) return;
hasInitialFetch.current = true;
fetchGeoMetrics();
}, [tenantId, galacticAdminMode, fetchGeoMetrics]);
}, [tenantId, globalAdminMode, fetchGeoMetrics]);
useEffect(() => {
if (isTimeRangeMount.current) {
@@ -201,12 +201,12 @@ export default function GeographicPerformancePage() {
return (
<ProtectedRoute>
<div className="min-h-screen bg-gray-50">
{galacticAdminMode && (
{globalAdminMode && (
<div className="bg-purple-700 text-white text-sm px-4 py-2 flex items-center justify-center space-x-2">
<svg className="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="font-medium">Galactic Admin View</span>
<span className="font-medium">Global Admin View</span>
<span className="text-purple-300">&mdash; aggregating data across all tenants</span>
</div>
)}
@@ -261,7 +261,7 @@ export default function GeographicPerformancePage() {
<main className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{/* Geo Disabled Warning */}
{geoMetrics && !geoMetrics.geoLocationEnabled && !galacticAdminMode && (
{geoMetrics && !geoMetrics.geoLocationEnabled && !globalAdminMode && (
<div className="mb-4 bg-amber-50 border border-amber-200 rounded-lg p-4 flex items-start space-x-3">
<svg className="w-5 h-5 text-amber-500 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
@@ -93,9 +93,9 @@ function LocationSessionsContent() {
const { tenantId } = useTenant();
const { getAccessToken } = useAuth();
const [galacticAdminMode] = useState(() => {
const [globalAdminMode] = useState(() => {
if (typeof window !== "undefined") {
return localStorage.getItem("galacticAdminMode") === "true";
return localStorage.getItem("globalAdminMode") === "true";
}
return false;
});
@@ -103,8 +103,8 @@ function LocationSessionsContent() {
const fetchSessions = useCallback(async () => {
if (!locationKey) return;
try {
const endpoint = galacticAdminMode
? `${API_BASE_URL}/api/galactic/metrics/geographic/sessions?days=${days}&groupBy=${groupBy}&locationKey=${encodeURIComponent(locationKey)}`
const endpoint = globalAdminMode
? `${API_BASE_URL}/api/global/metrics/geographic/sessions?days=${days}&groupBy=${groupBy}&locationKey=${encodeURIComponent(locationKey)}`
: `${API_BASE_URL}/api/metrics/geographic/sessions?tenantId=${tenantId}&days=${days}&groupBy=${groupBy}&locationKey=${encodeURIComponent(locationKey)}`;
const response = await authenticatedFetch(endpoint, getAccessToken);
if (response.ok) {
@@ -120,14 +120,14 @@ function LocationSessionsContent() {
} finally {
setLoading(false);
}
}, [galacticAdminMode, tenantId, getAccessToken, days, groupBy, locationKey]);
}, [globalAdminMode, tenantId, getAccessToken, days, groupBy, locationKey]);
useEffect(() => {
if (!galacticAdminMode && !tenantId) return;
if (!globalAdminMode && !tenantId) return;
if (hasInitialFetch.current) return;
hasInitialFetch.current = true;
fetchSessions();
}, [tenantId, galacticAdminMode, fetchSessions]);
}, [tenantId, globalAdminMode, fetchSessions]);
const timeLabel = days === "7" ? "7 Days" : days === "30" ? "30 Days" : "90 Days";
@@ -157,12 +157,12 @@ function LocationSessionsContent() {
return (
<ProtectedRoute>
<div className="min-h-screen bg-gray-50">
{galacticAdminMode && (
{globalAdminMode && (
<div className="bg-purple-700 text-white text-sm px-4 py-2 flex items-center justify-center space-x-2">
<svg className="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="font-medium">Galactic Admin View</span>
<span className="font-medium">Global Admin View</span>
<span className="text-purple-300">&mdash; aggregating data across all tenants</span>
</div>
)}
@@ -250,7 +250,7 @@ export default function ImePatternCard({
<button
onClick={() => onStartEditing(pattern)}
className="px-4 py-2 text-sm bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors flex items-center space-x-2"
title="Edit pattern (Galactic Admin)"
title="Edit pattern (Global Admin)"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
@@ -289,7 +289,7 @@ export default function ImePatternCard({
</div>
<div className="mb-4 bg-amber-50 border border-amber-200 rounded-lg p-3 text-sm text-amber-800">
<strong>Galactic Admin:</strong> Changes will apply globally to all tenants.
<strong>Global Admin:</strong> Changes will apply globally to all tenants.
</div>
{editForm.category === "always" && (
@@ -22,8 +22,8 @@ export default function ImeLogPatternsPage() {
useEffect(() => { trackEvent("ime_patterns_viewed"); }, []);
const { tenantId } = useTenant();
const { user } = useAuth();
const isGalacticAdmin = user?.isGalacticAdmin ?? false;
const [galacticAdminMode, setGalacticAdminMode] = useState(false);
const isGlobalAdmin = user?.isGlobalAdmin ?? false;
const [globalAdminMode, setGlobalAdminMode] = useState(false);
const { successMessage, error, showSuccess, showError } = useNotificationMessages();
@@ -60,11 +60,11 @@ export default function ImeLogPatternsPage() {
const [newParamKey, setNewParamKey] = useState("");
const [newParamValue, setNewParamValue] = useState("");
// Check galactic admin mode from localStorage
// Check global admin mode from localStorage
useEffect(() => {
const mode = localStorage.getItem("galacticAdminMode") === "true";
setGalacticAdminMode(mode);
const handleStorage = () => setGalacticAdminMode(localStorage.getItem("galacticAdminMode") === "true");
const mode = localStorage.getItem("globalAdminMode") === "true";
setGlobalAdminMode(mode);
const handleStorage = () => setGlobalAdminMode(localStorage.getItem("globalAdminMode") === "true");
window.addEventListener("storage", handleStorage);
return () => window.removeEventListener("storage", handleStorage);
}, []);
@@ -83,9 +83,9 @@ export default function ImeLogPatternsPage() {
fetchPatterns();
}, [fetchPatterns]);
// Toggle enable/disable (Galactic Admin only)
// Toggle enable/disable (Global Admin only)
const handleTogglePattern = async (pattern: ImeLogPattern) => {
if (!isGalacticAdmin || !galacticAdminMode) return;
if (!isGlobalAdmin || !globalAdminMode) return;
setTogglingPattern(pattern.patternId);
const result = await mutate(
@@ -109,7 +109,7 @@ export default function ImeLogPatternsPage() {
setTogglingPattern(null);
};
// Start editing (Galactic Admin only)
// Start editing (Global Admin only)
const startEditing = (pattern: ImeLogPattern) => {
setEditingPatternId(pattern.patternId);
setEditForm({
@@ -126,7 +126,7 @@ export default function ImeLogPatternsPage() {
setNewParamValue("");
};
// Save edit (Galactic Admin only)
// Save edit (Global Admin only)
const handleSaveEdit = async (patternId: string) => {
setSaving(true);
const payload: Partial<ImeLogPattern> = {
@@ -208,8 +208,8 @@ export default function ImeLogPatternsPage() {
// Unique actions in data for filter dropdown
const uniqueActions = [...new Set(patternsList.map((p) => p.action))].sort();
const canEdit = isGalacticAdmin && galacticAdminMode;
const isReadOnly = !user?.isTenantAdmin && !user?.isGalacticAdmin;
const canEdit = isGlobalAdmin && globalAdminMode;
const isReadOnly = !user?.isTenantAdmin && !user?.isGlobalAdmin;
return (
<ProtectedRoute>
+3 -3
View File
@@ -5,7 +5,7 @@ import { AuthProvider } from "../contexts/AuthContext";
import { SignalRProvider } from "../contexts/SignalRContext";
import { TenantProvider } from "../contexts/TenantContext";
import { NotificationProvider } from "../contexts/NotificationContext";
import { GalacticNotificationProvider } from "../contexts/GalacticNotificationContext";
import { GlobalNotificationProvider } from "../contexts/GlobalNotificationContext";
import { ThemeProvider } from "../contexts/ThemeContext";
import Navbar from "../components/Navbar";
import ScrollToTopButton from "../components/ScrollToTopButton";
@@ -118,7 +118,7 @@ export default function RootLayout({
<ThemeProvider>
<AuthProvider>
<NotificationProvider>
<GalacticNotificationProvider>
<GlobalNotificationProvider>
<TenantProvider>
<SignalRProvider>
<SidebarProvider>
@@ -132,7 +132,7 @@ export default function RootLayout({
</SidebarProvider>
</SignalRProvider>
</TenantProvider>
</GalacticNotificationProvider>
</GlobalNotificationProvider>
</NotificationProvider>
</AuthProvider>
</ThemeProvider>
@@ -16,7 +16,7 @@ export default function PreviewPage() {
// If not preview-blocked (e.g. approved tenant navigates here), redirect away
useEffect(() => {
if (!isLoading && isAuthenticated && user && !isPreviewBlocked) {
if (user.isTenantAdmin || user.isGalacticAdmin) {
if (user.isTenantAdmin || user.isGlobalAdmin) {
router.push("/dashboard");
} else {
router.push("/progress");
@@ -81,7 +81,7 @@ export default function PrivacyPage() {
</p>
<ul className="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li>Authenticated users within your tenant</li>
<li>Galactic Administrators (for platform operations and support)</li>
<li>Global Administrators (for platform operations and support)</li>
</ul>
<h2 className="text-xl font-semibold text-gray-900 mt-6">Your Rights</h2>
@@ -175,7 +175,7 @@ export default function EventTimeline({
</div>
</div>
{/* Raw mode — compact flat list by sequence (galactic only) */}
{/* Raw mode — compact flat list by sequence (global only) */}
{rawMode ? (
<div className="bg-white shadow rounded-lg p-4">
<h2 className="text-sm font-semibold text-gray-700 mb-3">Raw Events ({sortedBySequence.length})</h2>
@@ -65,9 +65,9 @@ export default function SessionDetailPage() {
}
return false;
});
const [galacticAdminMode, setGalacticAdminMode] = useState(() => {
const [globalAdminMode, setGlobalAdminMode] = useState(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('galacticAdminMode') === 'true';
return localStorage.getItem('globalAdminMode') === 'true';
}
return false;
});
@@ -80,7 +80,7 @@ export default function SessionDetailPage() {
const lastFetchedSessionId = useRef<string | null>(null);
const tenantIdRef = useRef<string>("");
const sessionTenantIdRef = useRef<string | null>(sessionTenantId);
const galacticAdminModeRef = useRef(galacticAdminMode);
const globalAdminModeRef = useRef(globalAdminMode);
// Track if we've joined groups to prevent duplicate joins
const hasJoinedGroups = useRef(false);
@@ -115,8 +115,8 @@ export default function SessionDetailPage() {
}, [sessionTenantId]);
useEffect(() => {
galacticAdminModeRef.current = galacticAdminMode;
}, [galacticAdminMode]);
globalAdminModeRef.current = globalAdminMode;
}, [globalAdminMode]);
// Auto-scroll: track whether user is near the bottom (only while feature is enabled).
// Uses a ref so scroll events don't cause re-renders.
@@ -167,7 +167,7 @@ export default function SessionDetailPage() {
const resolveEffectiveTenantId = () => {
const knownSessionTenant = sessionTenantIdRef.current || sessionRef.current?.tenantId || null;
if (knownSessionTenant) return knownSessionTenant;
if (galacticAdminModeRef.current) return null;
if (globalAdminModeRef.current) return null;
return tenantIdRef.current || null;
};
@@ -185,7 +185,7 @@ export default function SessionDetailPage() {
// `user` is included so that a retry fires once MSAL settles (token becomes available).
useEffect(() => {
if (!sessionId) return;
if (!galacticAdminMode && !tenantId) return; // wait for real tenant ID
if (!globalAdminMode && !tenantId) return; // wait for real tenant ID
// Update sessionIdRef
sessionIdRef.current = sessionId;
@@ -203,7 +203,7 @@ export default function SessionDetailPage() {
fetchSessionDetails();
// fetchEvents will be called after sessionTenantId is set
}, [sessionId, tenantId, galacticAdminMode, user]);
}, [sessionId, tenantId, globalAdminMode, user]);
// Fetch events, analysis, and vulnerability report when we have the session's tenant ID
useEffect(() => {
@@ -265,7 +265,7 @@ export default function SessionDetailPage() {
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId, isConnected, sessionTenantId, tenantId, session?.tenantId, galacticAdminMode]);
}, [sessionId, isConnected, sessionTenantId, tenantId, session?.tenantId, globalAdminMode]);
// Setup SignalR listener - re-register when connection changes
useEffect(() => {
@@ -342,15 +342,15 @@ export default function SessionDetailPage() {
}, 30_000);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId, sessionTenantId, tenantId, session?.tenantId, galacticAdminMode, isConnected]);
}, [sessionId, sessionTenantId, tenantId, session?.tenantId, globalAdminMode, isConnected]);
const fetchSessionDetails = async () => {
try {
const knownTenantId = resolveEffectiveTenantId();
const endpoint = knownTenantId
? `${API_BASE_URL}/api/sessions/${sessionId}?tenantId=${knownTenantId}`
: galacticAdminMode
? `${API_BASE_URL}/api/galactic/sessions`
: globalAdminMode
? `${API_BASE_URL}/api/global/sessions`
: `${API_BASE_URL}/api/sessions/${sessionId}`;
const response = await authenticatedFetch(endpoint, getAccessToken);
@@ -17,7 +17,7 @@ export function SettingsPageSections() {
const items: PageSectionItem[] = useMemo(() => {
const sections: PageSectionItem[] = [];
const isAdmin = user?.isTenantAdmin || user?.isGalacticAdmin;
const isAdmin = user?.isTenantAdmin || user?.isGlobalAdmin;
// 1. Validation
if (isAdmin) {
@@ -413,9 +413,9 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
useEffect(() => {
if (!tenantId) return;
if (!user?.isTenantAdmin && !user?.isGalacticAdmin) return;
if (!user?.isTenantAdmin && !user?.isGlobalAdmin) return;
fetchAdmins();
}, [tenantId, user?.isTenantAdmin, user?.isGalacticAdmin, fetchAdmins]);
}, [tenantId, user?.isTenantAdmin, user?.isGlobalAdmin, fetchAdmins]);
// -----------------------------------------------------------------------
// Fetch bootstrap sessions
@@ -445,10 +445,10 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
}, [tenantId, config?.bootstrapTokenEnabled, fetchBootstrapSessions]);
// -----------------------------------------------------------------------
// Fetch global diagnostics paths (galactic-admin only)
// Fetch global diagnostics paths (global-admin only)
// -----------------------------------------------------------------------
useEffect(() => {
if (!user?.isGalacticAdmin) return;
if (!user?.isGlobalAdmin) return;
const fetchGlobalDiagPaths = async () => {
try {
const res = await authenticatedFetch(`${API_BASE_URL}/api/global/config`, getAccessToken);
@@ -462,7 +462,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
}
};
fetchGlobalDiagPaths();
}, [user?.isGalacticAdmin, getAccessToken]);
}, [user?.isGlobalAdmin, getAccessToken]);
// -----------------------------------------------------------------------
// Save configuration (shared by all sections)
@@ -7,7 +7,7 @@ interface DataManagementSectionProps {
setDataRetentionDays: (value: number) => void;
sessionTimeoutHours: number;
setSessionTimeoutHours: (value: number) => void;
isGalacticAdmin?: boolean;
isGlobalAdmin?: boolean;
onSave: () => Promise<void> | void;
onReset: () => void;
saving: boolean;
@@ -18,13 +18,13 @@ export default function DataManagementSection({
setDataRetentionDays,
sessionTimeoutHours,
setSessionTimeoutHours,
isGalacticAdmin,
isGlobalAdmin,
onSave,
onReset,
saving,
}: DataManagementSectionProps) {
const isOverridden = dataRetentionDays === 0 || dataRetentionDays < 7 || dataRetentionDays > 180;
const isRetentionDisabled = isOverridden && !isGalacticAdmin;
const isRetentionDisabled = isOverridden && !isGlobalAdmin;
return (
<div className="bg-white rounded-lg shadow">
@@ -68,7 +68,7 @@ export default function DataManagementSection({
{dataRetentionDays === 0
? "Infinite retention is active. Data will never be automatically deleted."
: `Retention is set to ${dataRetentionDays} days (outside the standard 7–180 day range).`}
{" "}Contact your Galactic admin to change this value.
{" "}Contact your Global admin to change this value.
</p>
</div>
) : (

Some files were not shown because too many files have changed in this diff Show More