using System; using System.IO; using System.Net; using System.Threading.Tasks; using AutopilotMonitor.Functions.Helpers; using AutopilotMonitor.Functions.Services; using AutopilotMonitor.Shared.Models; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace AutopilotMonitor.Functions.Functions.Bootstrap { /// /// POST /api/bootstrap/sessions — Create a new bootstrap session for OOBE agent deployment. /// Requires JWT authentication and TenantAdmin role. /// public class CreateBootstrapSessionFunction { private readonly ILogger _logger; private readonly BootstrapSessionService _bootstrapService; private readonly GlobalAdminService _globalAdminService; private readonly TenantConfigurationService _configService; private readonly TableStorageService _storageService; public CreateBootstrapSessionFunction( ILogger logger, BootstrapSessionService bootstrapService, GlobalAdminService globalAdminService, TenantConfigurationService configService, TableStorageService storageService) { _logger = logger; _bootstrapService = bootstrapService; _globalAdminService = globalAdminService; _configService = configService; _storageService = storageService; } [Function("CreateBootstrapSession")] public async Task Run( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "bootstrap/sessions")] HttpRequestData req) { try { // Authentication + BootstrapManagerOrGA authorization enforced by PolicyEnforcementMiddleware var authenticatedTenantId = TenantHelper.GetTenantId(req); var userIdentifier = TenantHelper.GetUserIdentifier(req); // Read request body string body; using (var reader = new StreamReader(req.Body)) body = await reader.ReadToEndAsync(); var request = JsonConvert.DeserializeObject(body); if (request == null) { var badReq = req.CreateResponse(HttpStatusCode.BadRequest); await badReq.WriteAsJsonAsync(new { error = "Invalid request body" }); return badReq; } // Use tenant from JWT if not specified in body var tenantId = !string.IsNullOrEmpty(request.TenantId) ? request.TenantId : authenticatedTenantId; // Cross-tenant boundary check — only Global Admins may operate on other tenants if (!string.Equals(authenticatedTenantId, tenantId, StringComparison.OrdinalIgnoreCase)) { var isGlobal = await _globalAdminService.IsGlobalAdminAsync(userIdentifier); if (!isGlobal) { var forbidden = req.CreateResponse(HttpStatusCode.Forbidden); await forbidden.WriteAsJsonAsync(new { error = "Access denied: tenant mismatch" }); return forbidden; } } // Check if bootstrap token feature is enabled for this tenant var tenantConfig = await _configService.GetConfigurationAsync(tenantId); if (!tenantConfig.BootstrapTokenEnabled) { var disabled = req.CreateResponse(HttpStatusCode.Forbidden); await disabled.WriteAsJsonAsync(new { error = "Bootstrap token feature is not enabled for this tenant" }); return disabled; } // Validate validity hours var validityHours = request.ValidityHours > 0 ? request.ValidityHours : 8; var session = await _bootstrapService.CreateAsync(tenantId, validityHours, userIdentifier, request.Label); await _storageService.LogAuditEntryAsync( tenantId, "CREATE", "BootstrapSession", session.ShortCode, userIdentifier, new Dictionary { { "ValidityHours", validityHours.ToString() }, { "Label", request.Label ?? string.Empty } } ); var responseData = new CreateBootstrapSessionResponse { Success = true, ShortCode = session.ShortCode, BootstrapUrl = $"https://www.autopilotmonitor.com/go/{session.ShortCode}", ExpiresAt = session.ExpiresAt, Message = $"Bootstrap session created. Valid for {validityHours} hours." }; var response = req.CreateResponse(HttpStatusCode.Created); await response.WriteAsJsonAsync(responseData); return response; } catch (Exception ex) { _logger.LogError(ex, "Error creating bootstrap session"); var error = req.CreateResponse(HttpStatusCode.InternalServerError); await error.WriteAsJsonAsync(new { error = "Failed to create bootstrap session" }); return error; } } } }