thecr3w-dev/backend/DeploymentControl.Api/Program.cs

274 lines
9.3 KiB
C#

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<TheCr3wDbContext>(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 { }
}