using System.Security.Claims; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using TheCr3w.Shared.Crypto; using TheCr3w.Shared.Data; using TheCr3w.Shared.Models; var builder = WebApplication.CreateBuilder(args); // Konfiguracja Bazy Danych var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? "Host=localhost;Port=5432;Database=thecr3w_db;Username=thecr3w_user;Password=thecr3w_pass"; builder.Services.AddDbContext(options => options.UseNpgsql(connectionString)); // Keycloak OIDC var keycloakAuthority = builder.Configuration["Keycloak:Authority"] ?? "https://login.thecr3wnet.com/realms/master"; var keycloakAudience = builder.Configuration["Keycloak:Audience"] ?? "thecr3w-backend"; builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = keycloakAuthority; options.Audience = keycloakAudience; options.RequireHttpsMetadata = !builder.Environment.IsDevelopment(); options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = keycloakAuthority, ValidateAudience = true, ValidAudience = keycloakAudience, ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(1) }; }); builder.Services.AddAuthorization(options => { options.AddPolicy("Approver", policy => policy.RequireClaim(ClaimTypes.Role, "thecr3w:approver", "thecr3w:admin")); options.AddPolicy("Deployer", policy => policy.RequireClaim(ClaimTypes.Role, "thecr3w:deployer", "thecr3w:admin")); options.AddPolicy("AdminOnly", policy => policy.RequireClaim(ClaimTypes.Role, "thecr3w:admin")); }); builder.Services.AddCors(options => { options.AddPolicy("AllowAll", p => p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()); }); var app = builder.Build(); app.UseCors("AllowAll"); app.UseAuthentication(); app.UseAuthorization(); app.MapGet("/health", () => Results.Ok(new { status = "Healthy", service = "DeploymentControl.Api", timestamp = DateTimeOffset.UtcNow })); // 1. Zgłoszenie nowego manifestu wdrożeniowego (np. z Forgejo CI/CD) app.MapPost("/api/v1/deployments/manifests", async (DeploymentManifest manifest, TheCr3wDbContext db) => { if (string.IsNullOrEmpty(manifest.SignatureEd25519)) { return Results.BadRequest(new { error = "Manifest must contain a valid Ed25519 signature" }); } if (!manifest.ImmutableDigest.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)) { return Results.BadRequest(new { error = "Deployments must specify an immutable digest (sha256:...), tag ':latest' is strictly forbidden." }); } // Wyliczenie SHA256 kanonicznej reprezentacji manifestu var canonicalSha256 = ManifestSigner.ComputeManifestSha256(manifest); var entity = new DeploymentManifestEntity { ManifestId = manifest.ManifestId == Guid.Empty ? Guid.NewGuid() : manifest.ManifestId, ProjectId = manifest.Project, EnvironmentId = manifest.Environment.ToLowerInvariant(), ServiceName = manifest.ServiceName, Image = manifest.Image, ImmutableDigest = manifest.ImmutableDigest, ManifestSha256 = canonicalSha256, SignatureEd25519 = manifest.SignatureEd25519, ConfigHash = manifest.ConfigHash, Status = manifest.Environment.Equals("production", StringComparison.OrdinalIgnoreCase) ? "pending" : "approved", CreatedAt = DateTimeOffset.UtcNow, CreatedBy = manifest.CreatedBy }; db.DeploymentManifests.Add(entity); db.AuditEvents.Add(new AuditEventEntity { Actor = manifest.CreatedBy, Action = "MANIFEST_REGISTERED", ResourceType = "deployment_manifest", ResourceId = entity.ManifestId.ToString(), CorrelationId = Guid.NewGuid(), DetailsJson = System.Text.Json.JsonSerializer.Serialize(new { entity.ProjectId, entity.EnvironmentId, entity.ImmutableDigest }) }); await db.SaveChangesAsync(); return Results.Created($"/api/v1/deployments/manifests/{entity.ManifestId}", new { manifest_id = entity.ManifestId, manifest_sha256 = entity.ManifestSha256, project = entity.ProjectId, environment = entity.EnvironmentId, status = entity.Status, requires_approval = entity.EnvironmentId.Equals("production", StringComparison.OrdinalIgnoreCase) }); }); // 2. Bramka zatwierdzania wdrożenia produkcyjnego powiązana z SHA256 manifestu app.MapPost("/api/v1/deployments/approvals", async ( Guid manifest_id, string manifest_sha256, string decision, string? comment, TheCr3wDbContext db) => { var manifest = await db.DeploymentManifests.FirstOrDefaultAsync(m => m.ManifestId == manifest_id); if (manifest == null) return Results.NotFound(new { error = "Manifest not found" }); // Weryfikacja niezmienności manifestu (ochrona przed podmianą parametrów po akceptacji) if (!string.Equals(manifest.ManifestSha256, manifest_sha256, StringComparison.OrdinalIgnoreCase)) { return Results.Conflict(new { error = "Manifest SHA256 mismatch! The manifest has been modified and cannot be approved." }); } var approval = new DeploymentApprovalEntity { ApprovalId = Guid.NewGuid(), ManifestId = manifest.ManifestId, ManifestSha256Verified = manifest_sha256, Approver = "admin", Decision = decision.ToLowerInvariant(), Comment = comment, DecidedAt = DateTimeOffset.UtcNow }; db.DeploymentApprovals.Add(approval); if (decision.Equals("approved", StringComparison.OrdinalIgnoreCase)) { manifest.Status = "approved"; } else { manifest.Status = "rejected"; } db.AuditEvents.Add(new AuditEventEntity { Actor = approval.Approver, Action = $"DEPLOYMENT_{decision.ToUpperInvariant()}", ResourceType = "deployment_manifest", ResourceId = manifest.ManifestId.ToString(), CorrelationId = Guid.NewGuid() }); await db.SaveChangesAsync(); return Results.Ok(new { approval_id = approval.ApprovalId, manifest_id = manifest.ManifestId, decision = approval.Decision, decided_at = approval.DecidedAt }); }); // 3. Wykonanie wdrożenia przez Deployment Agenta app.MapPost("/api/v1/deployments/execute/{manifest_id:guid}", async (Guid manifest_id, TheCr3wDbContext db) => { var manifest = await db.DeploymentManifests.FirstOrDefaultAsync(m => m.ManifestId == manifest_id); if (manifest == null) return Results.NotFound(new { error = "Manifest not found" }); if (manifest.EnvironmentId.Equals("production", StringComparison.OrdinalIgnoreCase) && manifest.Status != "approved") { return Results.StatusCode(412); // Precondition Failed: Wymagana akceptacja } manifest.Status = "in_progress"; var deploymentId = Guid.NewGuid(); db.AuditEvents.Add(new AuditEventEntity { Actor = "deploy-engine", Action = "DEPLOYMENT_EXECUTED", ResourceType = "deployment", ResourceId = deploymentId.ToString(), CorrelationId = Guid.NewGuid(), DetailsJson = System.Text.Json.JsonSerializer.Serialize(new { manifest.ProjectId, manifest.ImmutableDigest }) }); await db.SaveChangesAsync(); return Results.Accepted($"/api/v1/deployments/{deploymentId}/status", new { deployment_id = deploymentId, manifest_id = manifest.ManifestId, status = "executing", started_at = DateTimeOffset.UtcNow }); }); // 4. Zarządzanie serwerami zewnętrznymi (SFTP / FTPS) app.MapGet("/api/v1/external-deployments/servers", async (TheCr3wDbContext db) => { var servers = await db.ExternalServers .Select(s => new { s.ServerId, s.Name, s.Protocol, s.Host, s.Port, s.RemoteRootPath, s.HostKeyFingerprint, s.Status, s.UpdatedAt }) .ToListAsync(); return Results.Ok(servers); }); app.MapPost("/api/v1/external-deployments/servers", async (ExternalServerEntity server, TheCr3wDbContext db) => { if (server.Protocol.Equals("ftp", StringComparison.OrdinalIgnoreCase)) { // Ostrzeżenie i wymóg świadomej konfiguracji nieszyfrowanego FTP app.Logger.LogWarning("Dodano serwer nieszyfrowanego FTP: {Host}. Wskazane użycie SFTP.", server.Host); } server.ServerId = Guid.NewGuid(); server.CreatedAt = DateTimeOffset.UtcNow; server.UpdatedAt = DateTimeOffset.UtcNow; db.ExternalServers.Add(server); await db.SaveChangesAsync(); return Results.Created($"/api/v1/external-deployments/servers/{server.ServerId}", server); }); // 5. Pobranie zdarzeń audytowych app.MapGet("/api/v1/audit/logs", async (string? resource_type, TheCr3wDbContext db) => { var query = db.AuditEvents.AsQueryable(); if (!string.IsNullOrEmpty(resource_type)) { query = query.Where(a => a.ResourceType == resource_type); } var logs = await query .OrderByDescending(a => a.CreatedAt) .Take(100) .ToListAsync(); return Results.Ok(logs); }); app.Run(); namespace DeploymentControl.Api { public partial class Program { } }