mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
renaming global admin
This commit is contained in:
+13
-13
@@ -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
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+6
-6
@@ -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);
|
||||
|
||||
+4
-4
@@ -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();
|
||||
|
||||
|
||||
+9
-9
@@ -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 };
|
||||
|
||||
+37
-37
@@ -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);
|
||||
|
||||
+4
-4
@@ -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);
|
||||
|
||||
+6
-6
@@ -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" });
|
||||
|
||||
+6
-6
@@ -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" });
|
||||
|
||||
+6
-6
@@ -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" });
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
|
||||
+6
-6
@@ -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);
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+8
-8
@@ -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 ||
|
||||
|
||||
+5
-5
@@ -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(
|
||||
|
||||
+18
-18
@@ -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;
|
||||
}
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
+16
-16
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-16
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -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;
|
||||
|
||||
+9
-9
@@ -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;
|
||||
|
||||
+9
-9
@@ -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;
|
||||
|
||||
+8
-8
@@ -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();
|
||||
|
||||
|
||||
+5
-5
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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"];
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
|
||||
+11
-11
@@ -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})");
|
||||
|
||||
+3
-3
@@ -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"];
|
||||
|
||||
+7
-7
@@ -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);
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+6
-6
@@ -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 }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+5
-5
@@ -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);
|
||||
|
||||
+4
-4
@@ -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)
|
||||
|
||||
+8
-8
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+52
-52
@@ -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; }
|
||||
+30
-30
@@ -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}`);
|
||||
|
||||
+3
-3
@@ -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">— 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">— 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">— 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">— 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">— 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">— 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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user