mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
security: enforce admin authorization, fix error leakage, prevent injection
- H1: Require TenantAdmin or GalacticAdmin for mutative tenant endpoints (DeleteSession, MarkSessionFailed, UpdateTenantConfiguration, AnalyzeRules CRUD, GatherRules CRUD) - M1: Add EnsureValidGuid validation to TenantOffboardFunction to prevent OData injection via tenantId route parameter - M2: Remove ex.Message from error responses in UsageMetrics, PlatformUsageMetrics, TriggerMaintenance, TenantOffboard, ReseedFromGitHub to prevent internal error leakage - M4: Use requireGalacticAdmin on ProtectedRoute and server-derived user.isGalacticAdmin instead of localStorage for admin-configuration - N1: Add 1-second Regex timeout to agent GatherRuleExecutor and ImeLogTracker to prevent ReDoS from malicious patterns Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -667,7 +667,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
|
||||
Regex pattern;
|
||||
try
|
||||
{
|
||||
pattern = new Regex(patternStr, RegexOptions.Compiled);
|
||||
pattern = new Regex(patternStr, RegexOptions.Compiled, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -711,7 +711,15 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
|
||||
if (!CmTraceLogParser.TryParseLine(line, out entry))
|
||||
continue;
|
||||
|
||||
var match = pattern.Match(entry.Message);
|
||||
Match match;
|
||||
try
|
||||
{
|
||||
match = pattern.Match(entry.Message);
|
||||
}
|
||||
catch (RegexMatchTimeoutException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!match.Success)
|
||||
continue;
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
|
||||
{
|
||||
// Replace {GUID} placeholder with actual GUID capture regex
|
||||
var regexStr = pattern.Pattern.Replace("{GUID}", GuidPattern);
|
||||
var regex = new Regex(regexStr, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
var regex = new Regex(regexStr, RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromSeconds(1));
|
||||
|
||||
var compiled = new CompiledPattern
|
||||
{
|
||||
@@ -439,11 +439,18 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
|
||||
// Match against active patterns
|
||||
foreach (var pattern in _activePatterns)
|
||||
{
|
||||
var match = pattern.Regex.Match(messageToMatch);
|
||||
if (match.Success)
|
||||
try
|
||||
{
|
||||
WriteMatchLog(filePath, line, pattern.PatternId);
|
||||
HandlePatternMatch(pattern, match, messageToMatch);
|
||||
var match = pattern.Regex.Match(messageToMatch);
|
||||
if (match.Success)
|
||||
{
|
||||
WriteMatchLog(filePath, line, pattern.PatternId);
|
||||
HandlePatternMatch(pattern, match, messageToMatch);
|
||||
}
|
||||
}
|
||||
catch (RegexMatchTimeoutException)
|
||||
{
|
||||
// Skip this pattern match to prevent ReDoS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,19 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
{
|
||||
private readonly ILogger<AnalyzeRulesFunction> _logger;
|
||||
private readonly AnalyzeRuleService _ruleService;
|
||||
private readonly TenantAdminsService _tenantAdminsService;
|
||||
private readonly GalacticAdminService _galacticAdminService;
|
||||
|
||||
public AnalyzeRulesFunction(ILogger<AnalyzeRulesFunction> logger, AnalyzeRuleService ruleService)
|
||||
public AnalyzeRulesFunction(
|
||||
ILogger<AnalyzeRulesFunction> logger,
|
||||
AnalyzeRuleService ruleService,
|
||||
TenantAdminsService tenantAdminsService,
|
||||
GalacticAdminService galacticAdminService)
|
||||
{
|
||||
_logger = logger;
|
||||
_ruleService = ruleService;
|
||||
_tenantAdminsService = tenantAdminsService;
|
||||
_galacticAdminService = galacticAdminService;
|
||||
}
|
||||
|
||||
[Function("GetAnalyzeRules")]
|
||||
@@ -54,6 +62,17 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
return unauthorized;
|
||||
}
|
||||
|
||||
// Require Tenant Admin or Galactic Admin for rule modifications
|
||||
var userIdentifier = TenantHelper.GetUserIdentifier(req);
|
||||
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
|
||||
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
|
||||
if (!isGalacticAdmin && !isTenantAdmin)
|
||||
{
|
||||
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
|
||||
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
|
||||
return forbidden;
|
||||
}
|
||||
|
||||
if (req.Headers.TryGetValues("Content-Length", out var clValues)
|
||||
&& long.TryParse(clValues.FirstOrDefault(), out var contentLength)
|
||||
&& contentLength > 1_048_576) // 1 MB limit
|
||||
@@ -92,6 +111,17 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
return unauthorized;
|
||||
}
|
||||
|
||||
// Require Tenant Admin or Galactic Admin for rule modifications
|
||||
var userIdentifier = TenantHelper.GetUserIdentifier(req);
|
||||
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
|
||||
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
|
||||
if (!isGalacticAdmin && !isTenantAdmin)
|
||||
{
|
||||
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
|
||||
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
|
||||
return forbidden;
|
||||
}
|
||||
|
||||
if (req.Headers.TryGetValues("Content-Length", out var clValues2)
|
||||
&& long.TryParse(clValues2.FirstOrDefault(), out var contentLength2)
|
||||
&& contentLength2 > 1_048_576) // 1 MB limit
|
||||
@@ -131,6 +161,17 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
return unauthorized;
|
||||
}
|
||||
|
||||
// Require Tenant Admin or Galactic Admin for rule modifications
|
||||
var userIdentifier = TenantHelper.GetUserIdentifier(req);
|
||||
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
|
||||
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
|
||||
if (!isGalacticAdmin && !isTenantAdmin)
|
||||
{
|
||||
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
|
||||
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
|
||||
return forbidden;
|
||||
}
|
||||
|
||||
// Load the rule to determine its type (built-in/community vs. custom)
|
||||
var rules = await _ruleService.GetAllRulesForTenantAsync(tenantId);
|
||||
var rule = rules.FirstOrDefault(r => r.RuleId == ruleId);
|
||||
|
||||
@@ -11,11 +11,19 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
{
|
||||
private readonly ILogger<DeleteSessionFunction> _logger;
|
||||
private readonly TableStorageService _storageService;
|
||||
private readonly TenantAdminsService _tenantAdminsService;
|
||||
private readonly GalacticAdminService _galacticAdminService;
|
||||
|
||||
public DeleteSessionFunction(ILogger<DeleteSessionFunction> logger, TableStorageService storageService)
|
||||
public DeleteSessionFunction(
|
||||
ILogger<DeleteSessionFunction> logger,
|
||||
TableStorageService storageService,
|
||||
TenantAdminsService tenantAdminsService,
|
||||
GalacticAdminService galacticAdminService)
|
||||
{
|
||||
_logger = logger;
|
||||
_storageService = storageService;
|
||||
_tenantAdminsService = tenantAdminsService;
|
||||
_galacticAdminService = galacticAdminService;
|
||||
}
|
||||
|
||||
[Function("DeleteSession")]
|
||||
@@ -43,6 +51,21 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
var tenantId = TenantHelper.GetTenantId(req);
|
||||
var userIdentifier = TenantHelper.GetUserIdentifier(req);
|
||||
|
||||
// Require Tenant Admin or Galactic Admin for destructive operations
|
||||
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
|
||||
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
|
||||
if (!isGalacticAdmin && !isTenantAdmin)
|
||||
{
|
||||
_logger.LogWarning($"Non-admin user {userIdentifier} attempted to delete session {sessionId}");
|
||||
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
|
||||
await forbiddenResponse.WriteAsJsonAsync(new
|
||||
{
|
||||
success = false,
|
||||
message = "Access denied. Tenant Admin or Galactic Admin role required."
|
||||
});
|
||||
return forbiddenResponse;
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Deleting session {sessionId} for tenant {tenantId} by user {userIdentifier}");
|
||||
|
||||
// Delete all related data for this session
|
||||
|
||||
@@ -16,11 +16,19 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
{
|
||||
private readonly ILogger<GatherRulesFunction> _logger;
|
||||
private readonly GatherRuleService _ruleService;
|
||||
private readonly TenantAdminsService _tenantAdminsService;
|
||||
private readonly GalacticAdminService _galacticAdminService;
|
||||
|
||||
public GatherRulesFunction(ILogger<GatherRulesFunction> logger, GatherRuleService ruleService)
|
||||
public GatherRulesFunction(
|
||||
ILogger<GatherRulesFunction> logger,
|
||||
GatherRuleService ruleService,
|
||||
TenantAdminsService tenantAdminsService,
|
||||
GalacticAdminService galacticAdminService)
|
||||
{
|
||||
_logger = logger;
|
||||
_ruleService = ruleService;
|
||||
_tenantAdminsService = tenantAdminsService;
|
||||
_galacticAdminService = galacticAdminService;
|
||||
}
|
||||
|
||||
[Function("GetGatherRules")]
|
||||
@@ -54,6 +62,17 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
return unauthorized;
|
||||
}
|
||||
|
||||
// Require Tenant Admin or Galactic Admin for rule modifications
|
||||
var userIdentifier = TenantHelper.GetUserIdentifier(req);
|
||||
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
|
||||
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
|
||||
if (!isGalacticAdmin && !isTenantAdmin)
|
||||
{
|
||||
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
|
||||
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
|
||||
return forbidden;
|
||||
}
|
||||
|
||||
if (req.Headers.TryGetValues("Content-Length", out var clValues)
|
||||
&& long.TryParse(clValues.FirstOrDefault(), out var contentLength)
|
||||
&& contentLength > 1_048_576) // 1 MB limit
|
||||
@@ -92,6 +111,17 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
return unauthorized;
|
||||
}
|
||||
|
||||
// Require Tenant Admin or Galactic Admin for rule modifications
|
||||
var userIdentifier = TenantHelper.GetUserIdentifier(req);
|
||||
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
|
||||
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
|
||||
if (!isGalacticAdmin && !isTenantAdmin)
|
||||
{
|
||||
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
|
||||
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
|
||||
return forbidden;
|
||||
}
|
||||
|
||||
if (req.Headers.TryGetValues("Content-Length", out var clValues2)
|
||||
&& long.TryParse(clValues2.FirstOrDefault(), out var contentLength2)
|
||||
&& contentLength2 > 1_048_576) // 1 MB limit
|
||||
@@ -131,6 +161,17 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
return unauthorized;
|
||||
}
|
||||
|
||||
// Require Tenant Admin or Galactic Admin for rule modifications
|
||||
var userIdentifier = TenantHelper.GetUserIdentifier(req);
|
||||
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
|
||||
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
|
||||
if (!isGalacticAdmin && !isTenantAdmin)
|
||||
{
|
||||
var forbidden = req.CreateResponse(HttpStatusCode.Forbidden);
|
||||
await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." });
|
||||
return forbidden;
|
||||
}
|
||||
|
||||
// Load the rule to determine its type (built-in/community vs. custom)
|
||||
var rules = await _ruleService.GetAllRulesForTenantAsync(tenantId);
|
||||
var rule = rules.FirstOrDefault(r => r.RuleId == ruleId);
|
||||
|
||||
@@ -13,11 +13,19 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
{
|
||||
private readonly ILogger<MarkSessionFailedFunction> _logger;
|
||||
private readonly TableStorageService _storageService;
|
||||
private readonly TenantAdminsService _tenantAdminsService;
|
||||
private readonly GalacticAdminService _galacticAdminService;
|
||||
|
||||
public MarkSessionFailedFunction(ILogger<MarkSessionFailedFunction> logger, TableStorageService storageService)
|
||||
public MarkSessionFailedFunction(
|
||||
ILogger<MarkSessionFailedFunction> logger,
|
||||
TableStorageService storageService,
|
||||
TenantAdminsService tenantAdminsService,
|
||||
GalacticAdminService galacticAdminService)
|
||||
{
|
||||
_logger = logger;
|
||||
_storageService = storageService;
|
||||
_tenantAdminsService = tenantAdminsService;
|
||||
_galacticAdminService = galacticAdminService;
|
||||
}
|
||||
|
||||
[Function("MarkSessionFailed")]
|
||||
@@ -45,6 +53,21 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
string tenantId = TenantHelper.GetTenantId(req);
|
||||
string userIdentifier = TenantHelper.GetUserIdentifier(req);
|
||||
|
||||
// Require Tenant Admin or Galactic Admin for status modifications
|
||||
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
|
||||
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
|
||||
if (!isGalacticAdmin && !isTenantAdmin)
|
||||
{
|
||||
_logger.LogWarning($"Non-admin user {userIdentifier} attempted to mark session {sessionId} as failed");
|
||||
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
|
||||
await forbiddenResponse.WriteAsJsonAsync(new
|
||||
{
|
||||
success = false,
|
||||
message = "Access denied. Tenant Admin or Galactic Admin role required."
|
||||
});
|
||||
return new MarkSessionFailedOutput { HttpResponse = forbiddenResponse };
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Marking session {sessionId} as failed for tenant {tenantId} by user {userIdentifier}");
|
||||
|
||||
// Update session status to Failed with manual failure reason
|
||||
|
||||
@@ -85,8 +85,7 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
await errorResponse.WriteAsJsonAsync(new
|
||||
{
|
||||
success = false,
|
||||
message = "Failed to compute usage metrics",
|
||||
error = ex.Message
|
||||
message = "Failed to compute usage metrics"
|
||||
});
|
||||
|
||||
return errorResponse;
|
||||
|
||||
@@ -115,7 +115,7 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
await response.WriteAsJsonAsync(new
|
||||
{
|
||||
success = false,
|
||||
message = $"Failed to fetch rules from GitHub: {ex.Message}. GitHub CDN may cache responses for up to 5 minutes after a merge."
|
||||
message = "Failed to fetch rules from GitHub. GitHub CDN may cache responses for up to 5 minutes after a merge."
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Net;
|
||||
using AutopilotMonitor.Functions.Extensions;
|
||||
using AutopilotMonitor.Functions.Helpers;
|
||||
using AutopilotMonitor.Functions.Security;
|
||||
using AutopilotMonitor.Functions.Services;
|
||||
using AutopilotMonitor.Shared;
|
||||
using Azure.Data.Tables;
|
||||
@@ -75,6 +76,9 @@ public class TenantOffboardFunction
|
||||
return forbiddenResponse;
|
||||
}
|
||||
|
||||
// Validate tenantId format to prevent OData injection
|
||||
SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId));
|
||||
|
||||
_logger.LogWarning($"TENANT OFFBOARD initiated for tenant {tenantId} by {upn}");
|
||||
|
||||
var result = new OffboardResult { TenantId = tenantId, InitiatedBy = upn!, InitiatedAt = DateTime.UtcNow };
|
||||
@@ -122,7 +126,7 @@ public class TenantOffboardFunction
|
||||
{
|
||||
_logger.LogError(ex, $"Tenant offboard failed for tenant {tenantId}");
|
||||
result.Success = false;
|
||||
result.Error = ex.Message;
|
||||
result.Error = "Internal server error";
|
||||
|
||||
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
|
||||
await errorResponse.WriteAsJsonAsync(result);
|
||||
|
||||
@@ -121,7 +121,7 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
await errorResponse.WriteAsJsonAsync(new
|
||||
{
|
||||
success = false,
|
||||
error = ex.Message
|
||||
message = "Internal server error"
|
||||
});
|
||||
|
||||
return errorResponse;
|
||||
|
||||
+22
-1
@@ -16,13 +16,19 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
{
|
||||
private readonly ILogger<UpdateTenantConfigurationFunction> _logger;
|
||||
private readonly TenantConfigurationService _configService;
|
||||
private readonly TenantAdminsService _tenantAdminsService;
|
||||
private readonly GalacticAdminService _galacticAdminService;
|
||||
|
||||
public UpdateTenantConfigurationFunction(
|
||||
ILogger<UpdateTenantConfigurationFunction> logger,
|
||||
TenantConfigurationService configService)
|
||||
TenantConfigurationService configService,
|
||||
TenantAdminsService tenantAdminsService,
|
||||
GalacticAdminService galacticAdminService)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_tenantAdminsService = tenantAdminsService;
|
||||
_galacticAdminService = galacticAdminService;
|
||||
}
|
||||
|
||||
[Function("UpdateTenantConfiguration")]
|
||||
@@ -62,6 +68,21 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
return forbiddenResponse;
|
||||
}
|
||||
|
||||
// Require Tenant Admin or Galactic Admin for configuration changes
|
||||
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier);
|
||||
var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier);
|
||||
if (!isGalacticAdmin && !isTenantAdmin)
|
||||
{
|
||||
_logger.LogWarning($"Non-admin user {userIdentifier} attempted to update configuration for tenant {tenantId}");
|
||||
var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden);
|
||||
await forbiddenResponse.WriteAsJsonAsync(new
|
||||
{
|
||||
success = false,
|
||||
message = "Access denied. Tenant Admin or Galactic Admin role required."
|
||||
});
|
||||
return forbiddenResponse;
|
||||
}
|
||||
|
||||
_logger.LogInformation($"UpdateTenantConfiguration: {tenantId} by user {userIdentifier}");
|
||||
|
||||
// Parse request body
|
||||
|
||||
@@ -69,8 +69,7 @@ namespace AutopilotMonitor.Functions.Functions
|
||||
await errorResponse.WriteAsJsonAsync(new
|
||||
{
|
||||
success = false,
|
||||
message = "Failed to compute tenant usage metrics",
|
||||
error = ex.Message
|
||||
message = "Failed to compute tenant usage metrics"
|
||||
});
|
||||
|
||||
return errorResponse;
|
||||
|
||||
@@ -28,7 +28,7 @@ interface AdminConfiguration {
|
||||
|
||||
export default function AdminConfigurationPage() {
|
||||
const router = useRouter();
|
||||
const { getAccessToken } = useAuth();
|
||||
const { getAccessToken, user } = useAuth();
|
||||
const [galacticAdminMode, setGalacticAdminMode] = useState(false);
|
||||
// Diagnostics Log Paths state
|
||||
const [globalDiagPaths, setGlobalDiagPaths] = useState<DiagnosticsLogPath[]>([]);
|
||||
@@ -53,16 +53,16 @@ export default function AdminConfigurationPage() {
|
||||
// Preview Whitelist state
|
||||
const [previewApproved, setPreviewApproved] = useState<Set<string>>(new Set());
|
||||
|
||||
// Load galactic admin mode from localStorage
|
||||
// Load galactic admin mode - use server-derived isGalacticAdmin as authoritative source
|
||||
useEffect(() => {
|
||||
const galacticMode = localStorage.getItem("galacticAdminMode") === "true";
|
||||
const galacticMode = user?.isGalacticAdmin === true;
|
||||
setGalacticAdminMode(galacticMode);
|
||||
|
||||
// Redirect if not in galactic admin mode
|
||||
if (!galacticMode) {
|
||||
// Redirect if not a galactic admin
|
||||
if (user && !galacticMode) {
|
||||
router.push("/dashboard");
|
||||
}
|
||||
}, [router]);
|
||||
}, [router, user]);
|
||||
|
||||
// Fetch admin configuration
|
||||
useEffect(() => {
|
||||
@@ -263,7 +263,7 @@ export default function AdminConfigurationPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<ProtectedRoute requireGalacticAdmin>
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
{/* Header */}
|
||||
<header className="bg-white dark:bg-gray-800 shadow dark:shadow-gray-700">
|
||||
|
||||
Reference in New Issue
Block a user