using System.Net;
using AutopilotMonitor.Functions.Extensions;
using AutopilotMonitor.Functions.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Functions.Infrastructure;
///
/// Authentication and authorization endpoints
///
public class AuthFunction
{
private readonly ILogger _logger;
private readonly GalacticAdminService _galacticAdminService;
private readonly TenantConfigurationService _tenantConfigService;
private readonly TenantAdminsService _tenantAdminsService;
private readonly TableStorageService _storageService;
private readonly PreviewWhitelistService _previewWhitelistService;
private readonly TelegramNotificationService _telegramNotificationService;
public AuthFunction(
ILogger logger,
GalacticAdminService galacticAdminService,
TenantConfigurationService tenantConfigService,
TenantAdminsService tenantAdminsService,
TableStorageService storageService,
PreviewWhitelistService previewWhitelistService,
TelegramNotificationService telegramNotificationService)
{
_logger = logger;
_galacticAdminService = galacticAdminService;
_tenantConfigService = tenantConfigService;
_tenantAdminsService = tenantAdminsService;
_storageService = storageService;
_previewWhitelistService = previewWhitelistService;
_telegramNotificationService = telegramNotificationService;
}
///
/// GET /api/auth/me
/// Returns information about the currently authenticated user
///
[Function("GetCurrentUser")]
[Authorize]
public async Task GetCurrentUser(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "auth/me")] HttpRequestData req,
FunctionContext context)
{
var principal = context.GetUser();
if (principal == null)
{
_logger.LogWarning("GetCurrentUser - No authentication found");
return req.CreateResponse(HttpStatusCode.Unauthorized);
}
var tenantId = principal.GetTenantId();
var upn = principal.GetUserPrincipalName();
var displayName = principal.GetDisplayName();
var objectId = principal.GetObjectId();
// Validate required claims
if (string.IsNullOrEmpty(tenantId) || string.IsNullOrEmpty(upn))
{
_logger.LogWarning("Missing required claims: tenantId or upn");
var badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await badRequestResponse.WriteAsJsonAsync(new { error = "Missing required claims" });
return badRequestResponse;
}
// Load tenant configuration
var tenantConfig = await _tenantConfigService.GetConfigurationAsync(tenantId);
// Extract and save domain name from first user if not set
if (string.IsNullOrEmpty(tenantConfig.DomainName) && !string.IsNullOrEmpty(upn))
{
var domain = ExtractDomainFromUpn(upn);
if (!string.IsNullOrEmpty(domain))
{
_logger.LogInformation($"Setting domain name for tenant {tenantId}: {domain}");
tenantConfig.DomainName = domain;
tenantConfig.UpdatedBy = upn;
await _tenantConfigService.SaveConfigurationAsync(tenantConfig);
// Notify via Telegram that a new tenant has signed up for Private Preview
_ = _telegramNotificationService.SendNewTenantSignupAsync(tenantId, upn)
.ContinueWith(t => _logger.LogWarning(t.Exception?.InnerException,
"Fire-and-forget Telegram notification failed for tenant {TenantId}", tenantId),
TaskContinuationOptions.OnlyOnFaulted);
}
}
// Check if tenant is disabled/suspended
if (tenantConfig.IsCurrentlyDisabled())
{
_logger.LogWarning($"Login attempt for suspended tenant: {tenantId} by user {upn}");
var suspendedResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await suspendedResponse.WriteAsJsonAsync(new
{
error = "TenantSuspended",
message = !string.IsNullOrEmpty(tenantConfig.DisabledReason)
? tenantConfig.DisabledReason
: "Your tenant has been suspended. Please contact support for more information.",
disabledUntil = tenantConfig.DisabledUntil?.ToString("o"),
contactSupport = true
});
return suspendedResponse;
}
// Auto-re-enable: if Disabled was true but DisabledUntil has expired, persist the re-enable
if (tenantConfig.Disabled && !tenantConfig.IsCurrentlyDisabled())
{
_logger.LogInformation($"Tenant {tenantId} auto-re-enabled: DisabledUntil ({tenantConfig.DisabledUntil:o}) has expired");
tenantConfig.Disabled = false;
tenantConfig.DisabledReason = null;
tenantConfig.DisabledUntil = null;
tenantConfig.UpdatedBy = "System (auto-re-enable)";
await _tenantConfigService.SaveConfigurationAsync(tenantConfig);
}
// Check if user is galactic admin (must happen before preview gate)
var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(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))
{
_logger.LogInformation("Tenant {TenantId} blocked by preview gate (user: {Upn})", tenantId, upn);
var previewResponse = req.CreateResponse(HttpStatusCode.Forbidden);
await previewResponse.WriteAsJsonAsync(new
{
error = "PrivatePreview",
message = "Autopilot Monitor is currently in Private Preview. Your organization is on the waitlist \u2014 we'll notify you when access is granted."
});
return previewResponse;
}
// Check if user is tenant admin
bool isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, upn);
// Auto-admin logic: First user becomes admin
if (!isTenantAdmin)
{
// Check if tenant has any admins at all
var existingAdmins = await _tenantAdminsService.GetTenantAdminsAsync(tenantId);
if (existingAdmins.Count == 0)
{
_logger.LogInformation($"First user login for tenant {tenantId}: {upn} - Auto-assigning as admin");
await _tenantAdminsService.AddTenantAdminAsync(tenantId, upn, "System");
isTenantAdmin = true;
}
}
// Get role info for the user (Admin, Operator, Viewer, or null if not a member)
var memberRole = await _tenantAdminsService.GetMemberRoleAsync(tenantId, upn);
string? role = memberRole?.Role;
bool canManageBootstrapTokens = memberRole?.CanManageBootstrapTokens ?? false;
// Record user login activity for metrics tracking
_ = _storageService.RecordUserLoginAsync(tenantId, upn, displayName, objectId)
.ContinueWith(t => _logger.LogWarning(t.Exception?.InnerException, "Fire-and-forget RecordUserLoginAsync failed"), TaskContinuationOptions.OnlyOnFaulted);
var userInfo = new
{
tenantId,
upn,
displayName,
objectId,
isGalacticAdmin,
isTenantAdmin,
role,
canManageBootstrapTokens
};
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(userInfo);
return response;
}
///
/// GET /api/auth/is-galactic-admin
/// Checks if the current user is a Galactic Admin
///
[Function("IsGalacticAdmin")]
[Authorize]
public async Task IsGalacticAdmin(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "auth/is-galactic-admin")] HttpRequestData req,
FunctionContext context)
{
var principal = context.GetUser();
if (principal == null)
{
return req.CreateResponse(HttpStatusCode.Unauthorized);
}
var upn = principal.GetUserPrincipalName();
var isAdmin = await _galacticAdminService.IsGalacticAdminAsync(upn);
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new { isGalacticAdmin = isAdmin, upn });
return response;
}
///
/// GET /api/auth/galactic-admins
/// Lists all Galactic Admins (only accessible by Galactic Admins)
///
[Function("GetGalacticAdmins")]
[Authorize]
public async Task GetGalacticAdmins(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "auth/galactic-admins")] HttpRequestData req,
FunctionContext context)
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var admins = await _galacticAdminService.GetAllGalacticAdminsAsync();
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new { admins });
return response;
}
///
/// POST /api/auth/galactic-admins
/// Adds a new Galactic Admin (only accessible by existing Galactic Admins)
///
[Function("AddGalacticAdmin")]
[Authorize]
public async Task AddGalacticAdmin(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "auth/galactic-admins")] HttpRequestData req,
FunctionContext context)
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var principal = context.GetUser();
var currentUpn = principal?.GetUserPrincipalName();
// Parse request body
var body = await req.ReadFromJsonAsync();
if (body == null || string.IsNullOrWhiteSpace(body.Upn))
{
var badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await badRequestResponse.WriteAsJsonAsync(new { error = "UPN is required" });
return badRequestResponse;
}
var newAdmin = await _galacticAdminService.AddGalacticAdminAsync(body.Upn, currentUpn!);
_logger.LogInformation($"Galactic Admin added: {body.Upn} by {currentUpn}");
var response = req.CreateResponse(HttpStatusCode.Created);
await response.WriteAsJsonAsync(new { admin = newAdmin });
return response;
}
///
/// DELETE /api/auth/galactic-admins/{upn}
/// Removes a Galactic Admin (only accessible by existing Galactic Admins)
///
[Function("RemoveGalacticAdmin")]
[Authorize]
public async Task RemoveGalacticAdmin(
[HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "auth/galactic-admins/{upn}")] HttpRequestData req,
string upn,
FunctionContext context)
{
// Authentication + GalacticAdminOnly authorization enforced by PolicyEnforcementMiddleware
var principal = context.GetUser();
var currentUpn = principal?.GetUserPrincipalName();
// Prevent self-removal
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" });
return badRequestResponse;
}
await _galacticAdminService.RemoveGalacticAdminAsync(upn);
_logger.LogInformation($"Galactic Admin removed: {upn} by {currentUpn}");
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new { message = "Galactic Admin removed successfully" });
return response;
}
///
/// Extracts domain name from UPN (e.g., user@contoso.com -> contoso.com)
///
private static string ExtractDomainFromUpn(string upn)
{
if (string.IsNullOrEmpty(upn))
return string.Empty;
var atIndex = upn.IndexOf('@');
if (atIndex > 0 && atIndex < upn.Length - 1)
{
return upn.Substring(atIndex + 1);
}
return string.Empty;
}
}
public class AddGalacticAdminRequest
{
public string Upn { get; set; } = string.Empty;
}