Files
git.stella-ops.org/src/Registry/StellaOps.Registry.TokenService/Program.cs

200 lines
6.6 KiB
C#

using System.Globalization;
using System.Net;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using OpenTelemetry.Instrumentation.AspNetCore;
using OpenTelemetry.Instrumentation.Runtime;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using Serilog;
using Serilog.Events;
using StellaOps.AirGap.Policy;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Configuration;
using StellaOps.Telemetry.Core;
using StellaOps.Registry.TokenService;
using StellaOps.Registry.TokenService.Admin;
using StellaOps.Registry.TokenService.Observability;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddStellaOpsDefaults(options =>
{
options.BasePath = builder.Environment.ContentRootPath;
options.EnvironmentPrefix = "REGISTRY_TOKEN_";
options.ConfigureBuilder = configurationBuilder =>
{
configurationBuilder.AddYamlFile("../etc/registry-token.yaml", optional: true, reloadOnChange: true);
};
});
var bootstrapOptions = builder.Configuration.BindOptions<RegistryTokenServiceOptions>(
RegistryTokenServiceOptions.SectionName,
(opts, _) => opts.Validate());
builder.Host.UseSerilog((context, services, loggerConfiguration) =>
{
loggerConfiguration
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Console();
});
builder.Services.AddOptions<RegistryTokenServiceOptions>()
.Bind(builder.Configuration.GetSection(RegistryTokenServiceOptions.SectionName))
.PostConfigure(options => options.Validate())
.ValidateOnStart();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<RegistryTokenMetrics>();
builder.Services.AddSingleton<PlanRegistry>(sp =>
{
var options = sp.GetRequiredService<IOptions<RegistryTokenServiceOptions>>().Value;
return new PlanRegistry(options);
});
builder.Services.AddSingleton<RegistryTokenIssuer>();
// Plan Admin API dependencies
builder.Services.AddSingleton<IPlanRuleStore, InMemoryPlanRuleStore>();
builder.Services.AddSingleton<PlanValidator>();
builder.Services.AddHealthChecks().AddCheck("self", () => Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult.Healthy());
builder.Services.AddAirGapEgressPolicy(builder.Configuration);
builder.Services.AddStellaOpsTelemetry(
builder.Configuration,
serviceName: "StellaOps.Registry.TokenService",
configureMetrics: metricsBuilder =>
{
metricsBuilder.AddRuntimeInstrumentation();
metricsBuilder.AddMeter(RegistryTokenMetrics.MeterName);
},
configureTracing: tracerBuilder =>
{
tracerBuilder.AddAspNetCoreInstrumentation();
tracerBuilder.AddHttpClientInstrumentation();
});
builder.Services.AddStellaOpsResourceServerAuthentication(
builder.Configuration,
configurationSection: null,
configure: resourceOptions =>
{
resourceOptions.Authority = bootstrapOptions.Authority.Issuer;
resourceOptions.RequireHttpsMetadata = bootstrapOptions.Authority.RequireHttpsMetadata;
resourceOptions.MetadataAddress = bootstrapOptions.Authority.MetadataAddress;
resourceOptions.Audiences.Clear();
foreach (var audience in bootstrapOptions.Authority.Audiences)
{
resourceOptions.Audiences.Add(audience);
}
});
builder.Services.AddAuthorization(options =>
{
var scopes = bootstrapOptions.Authority.RequiredScopes.Count == 0
? new[] { "registry.token.issue" }
: bootstrapOptions.Authority.RequiredScopes.ToArray();
options.AddPolicy("registry.token.issue", policy =>
{
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new StellaOpsScopeRequirement(scopes));
policy.AddAuthenticationSchemes(StellaOpsAuthenticationDefaults.AuthenticationScheme);
});
// Admin policy for plan management
options.AddPolicy("registry.admin", policy =>
{
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new StellaOpsScopeRequirement(["registry.admin"]));
policy.AddAuthenticationSchemes(StellaOpsAuthenticationDefaults.AuthenticationScheme);
});
});
var app = builder.Build();
app.UseSerilogRequestLogging();
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz");
// Plan Admin API endpoints
app.MapPlanAdminEndpoints();
app.MapGet("/token", (
HttpContext context,
[FromServices] IOptions<RegistryTokenServiceOptions> options,
[FromServices] RegistryTokenIssuer issuer) =>
{
var serviceOptions = options.Value;
var service = context.Request.Query["service"].FirstOrDefault()?.Trim();
if (string.IsNullOrWhiteSpace(service))
{
return Results.Problem(
detail: "The 'service' query parameter is required.",
statusCode: StatusCodes.Status400BadRequest);
}
if (serviceOptions.Registry.AllowedServices.Count > 0 &&
!serviceOptions.Registry.AllowedServices.Contains(service, StringComparer.OrdinalIgnoreCase))
{
return Results.Problem(
detail: "The requested registry service is not permitted for this installation.",
statusCode: StatusCodes.Status403Forbidden);
}
IReadOnlyList<RegistryAccessRequest> accessRequests;
try
{
accessRequests = RegistryScopeParser.Parse(context.Request.Query);
}
catch (InvalidScopeException ex)
{
return Results.Problem(
detail: ex.Message,
statusCode: StatusCodes.Status400BadRequest);
}
if (accessRequests.Count == 0)
{
return Results.Problem(
detail: "At least one scope must be requested.",
statusCode: StatusCodes.Status400BadRequest);
}
try
{
var response = issuer.IssueToken(context.User, service, accessRequests);
return Results.Json(new
{
token = response.Token,
expires_in = response.ExpiresIn,
issued_at = response.IssuedAt.UtcDateTime.ToString("O", CultureInfo.InvariantCulture),
issued_token_type = "urn:ietf:params:oauth:token-type:access_token"
});
}
catch (RegistryTokenException ex)
{
return Results.Problem(
detail: ex.Message,
statusCode: StatusCodes.Status403Forbidden);
}
})
.WithName("GetRegistryToken")
.RequireAuthorization("registry.token.issue")
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status403Forbidden);
app.Run();