diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/AttestationEndpoints.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/AttestationEndpoints.cs index 4e8350de8..5d30b1ee6 100644 --- a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/AttestationEndpoints.cs +++ b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/AttestationEndpoints.cs @@ -10,6 +10,7 @@ using StellaOps.AdvisoryAI.Attestation; using StellaOps.AdvisoryAI.Attestation.Models; using StellaOps.AdvisoryAI.Attestation.Storage; using StellaOps.AdvisoryAI.WebService.Security; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.AdvisoryAI.WebService.Endpoints; @@ -34,7 +35,8 @@ public static class AttestationEndpoints .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized) .RequireAuthorization(AdvisoryAIPolicies.ViewPolicy) - .RequireRateLimiting("advisory-ai"); + .RequireRateLimiting("advisory-ai") + .RequireTenant(); // GET /v1/advisory-ai/runs/{runId}/claims app.MapGet("/v1/advisory-ai/runs/{runId}/claims", HandleGetRunClaims) @@ -46,7 +48,8 @@ public static class AttestationEndpoints .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized) .RequireAuthorization(AdvisoryAIPolicies.ViewPolicy) - .RequireRateLimiting("advisory-ai"); + .RequireRateLimiting("advisory-ai") + .RequireTenant(); // GET /v1/advisory-ai/attestations/recent app.MapGet("/v1/advisory-ai/attestations/recent", HandleListRecentAttestations) @@ -57,7 +60,8 @@ public static class AttestationEndpoints .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .RequireAuthorization(AdvisoryAIPolicies.ViewPolicy) - .RequireRateLimiting("advisory-ai"); + .RequireRateLimiting("advisory-ai") + .RequireTenant(); // POST /v1/advisory-ai/attestations/verify app.MapPost("/v1/advisory-ai/attestations/verify", HandleVerifyAttestation) @@ -69,7 +73,8 @@ public static class AttestationEndpoints .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized) .RequireAuthorization(AdvisoryAIPolicies.ViewPolicy) - .RequireRateLimiting("advisory-ai"); + .RequireRateLimiting("advisory-ai") + .RequireTenant(); } private static async Task HandleGetRunAttestation( diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/ChatEndpoints.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/ChatEndpoints.cs index 9bd672221..5e923bbf2 100644 --- a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/ChatEndpoints.cs +++ b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/ChatEndpoints.cs @@ -18,6 +18,7 @@ using StellaOps.AdvisoryAI.Chat.Services; using StellaOps.AdvisoryAI.Chat.Settings; using StellaOps.AdvisoryAI.WebService.Contracts; using StellaOps.AdvisoryAI.WebService.Security; +using StellaOps.Auth.ServerIntegration.Tenancy; using System.Collections.Immutable; using System.Runtime.CompilerServices; using System.Text.Json; @@ -45,7 +46,8 @@ public static class ChatEndpoints { var group = builder.MapGroup("/api/v1/chat") .WithTags("Advisory Chat") - .RequireAuthorization(AdvisoryAIPolicies.OperatePolicy); + .RequireAuthorization(AdvisoryAIPolicies.OperatePolicy) + .RequireTenant(); // Single query endpoint (non-streaming) group.MapPost("/query", ProcessQueryAsync) diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/EvidencePackEndpoints.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/EvidencePackEndpoints.cs index 953ab4ecf..fa60da89e 100644 --- a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/EvidencePackEndpoints.cs +++ b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/EvidencePackEndpoints.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using StellaOps.AdvisoryAI.WebService.Security; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Determinism; using StellaOps.Evidence.Pack; using StellaOps.Evidence.Pack.Models; @@ -35,7 +36,8 @@ public static class EvidencePackEndpoints .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized) .RequireAuthorization(AdvisoryAIPolicies.OperatePolicy) - .RequireRateLimiting("advisory-ai"); + .RequireRateLimiting("advisory-ai") + .RequireTenant(); // GET /v1/evidence-packs/{packId} - Get Evidence Pack app.MapGet("/v1/evidence-packs/{packId}", HandleGetEvidencePack) @@ -47,7 +49,8 @@ public static class EvidencePackEndpoints .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized) .RequireAuthorization(AdvisoryAIPolicies.ViewPolicy) - .RequireRateLimiting("advisory-ai"); + .RequireRateLimiting("advisory-ai") + .RequireTenant(); // POST /v1/evidence-packs/{packId}/sign - Sign Evidence Pack app.MapPost("/v1/evidence-packs/{packId}/sign", HandleSignEvidencePack) @@ -59,7 +62,8 @@ public static class EvidencePackEndpoints .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized) .RequireAuthorization(AdvisoryAIPolicies.OperatePolicy) - .RequireRateLimiting("advisory-ai"); + .RequireRateLimiting("advisory-ai") + .RequireTenant(); // POST /v1/evidence-packs/{packId}/verify - Verify Evidence Pack app.MapPost("/v1/evidence-packs/{packId}/verify", HandleVerifyEvidencePack) @@ -71,7 +75,8 @@ public static class EvidencePackEndpoints .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized) .RequireAuthorization(AdvisoryAIPolicies.ViewPolicy) - .RequireRateLimiting("advisory-ai"); + .RequireRateLimiting("advisory-ai") + .RequireTenant(); // GET /v1/evidence-packs/{packId}/export - Export Evidence Pack app.MapGet("/v1/evidence-packs/{packId}/export", HandleExportEvidencePack) @@ -83,7 +88,8 @@ public static class EvidencePackEndpoints .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized) .RequireAuthorization(AdvisoryAIPolicies.ViewPolicy) - .RequireRateLimiting("advisory-ai"); + .RequireRateLimiting("advisory-ai") + .RequireTenant(); // GET /v1/runs/{runId}/evidence-packs - List Evidence Packs for Run app.MapGet("/v1/runs/{runId}/evidence-packs", HandleListRunEvidencePacks) @@ -94,7 +100,8 @@ public static class EvidencePackEndpoints .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .RequireAuthorization(AdvisoryAIPolicies.ViewPolicy) - .RequireRateLimiting("advisory-ai"); + .RequireRateLimiting("advisory-ai") + .RequireTenant(); // GET /v1/evidence-packs - List Evidence Packs app.MapGet("/v1/evidence-packs", HandleListEvidencePacks) @@ -105,7 +112,8 @@ public static class EvidencePackEndpoints .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .RequireAuthorization(AdvisoryAIPolicies.ViewPolicy) - .RequireRateLimiting("advisory-ai"); + .RequireRateLimiting("advisory-ai") + .RequireTenant(); } private static async Task HandleCreateEvidencePack( diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/KnowledgeSearchEndpoints.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/KnowledgeSearchEndpoints.cs index 98ad4fa99..9ff6bddac 100644 --- a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/KnowledgeSearchEndpoints.cs +++ b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/KnowledgeSearchEndpoints.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using StellaOps.AdvisoryAI.KnowledgeSearch; using StellaOps.AdvisoryAI.WebService.Security; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.AdvisoryAI.WebService.Endpoints; @@ -19,7 +20,8 @@ public static class KnowledgeSearchEndpoints { var group = builder.MapGroup("/v1/advisory-ai") .WithTags("Advisory AI - Knowledge Search") - .RequireAuthorization(AdvisoryAIPolicies.ViewPolicy); + .RequireAuthorization(AdvisoryAIPolicies.ViewPolicy) + .RequireTenant(); group.MapPost("/search", SearchAsync) .WithName("AdvisoryAiKnowledgeSearch") diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/LlmAdapterEndpoints.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/LlmAdapterEndpoints.cs index 09f473f4f..c2a9a5ea5 100644 --- a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/LlmAdapterEndpoints.cs +++ b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/LlmAdapterEndpoints.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Routing; using StellaOps.AdvisoryAI.Inference.LlmProviders; using StellaOps.AdvisoryAI.Plugin.Unified; using StellaOps.AdvisoryAI.WebService.Security; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Plugin.Abstractions.Capabilities; using System.Security.Cryptography; using System.Text; @@ -29,7 +30,8 @@ public static class LlmAdapterEndpoints { var group = builder.MapGroup("/v1/advisory-ai/adapters") .WithTags("Advisory AI - LLM Adapters") - .RequireAuthorization(AdvisoryAIPolicies.ViewPolicy); + .RequireAuthorization(AdvisoryAIPolicies.ViewPolicy) + .RequireTenant(); group.MapGet("/llm/providers", ListProvidersAsync) .WithName("ListLlmProviders") diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/RunEndpoints.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/RunEndpoints.cs index e95d6cb70..716d204db 100644 --- a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/RunEndpoints.cs +++ b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Endpoints/RunEndpoints.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using StellaOps.AdvisoryAI.Runs; using StellaOps.AdvisoryAI.WebService.Security; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Determinism; using System.Collections.Immutable; @@ -29,7 +30,8 @@ public static class RunEndpoints { var group = builder.MapGroup("/api/v1/runs") .WithTags("Runs") - .RequireAuthorization(AdvisoryAIPolicies.ViewPolicy); + .RequireAuthorization(AdvisoryAIPolicies.ViewPolicy) + .RequireTenant(); group.MapPost("/", CreateRunAsync) .WithName("CreateRun") diff --git a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs index 479447c9a..a7b879b5b 100644 --- a/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs +++ b/src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Program.cs @@ -29,6 +29,7 @@ using StellaOps.AdvisoryAI.WebService.Services; using StellaOps.Auth.Abstractions; using StellaOps.Evidence.Pack; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Router.AspNet; using System.Collections.Immutable; using System.Diagnostics; @@ -103,6 +104,7 @@ var routerEnabled = builder.Services.AddRouterMicroservice( version: System.Reflection.CustomAttributeExtensions.GetCustomAttribute(System.Reflection.Assembly.GetExecutingAssembly())?.InformationalVersion ?? "1.0.0", routerOptionsSection: "Router"); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); builder.Services.AddRateLimiter(options => @@ -145,6 +147,7 @@ if (app.Environment.IsDevelopment()) app.UseStellaOpsCors(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.UseRateLimiter(); app.TryUseStellaRouter(routerEnabled); diff --git a/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Companion.Tests/CompanionExplainEndpointTests.cs b/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Companion.Tests/CompanionExplainEndpointTests.cs index 861b8d921..fd5a47fff 100644 --- a/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Companion.Tests/CompanionExplainEndpointTests.cs +++ b/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Companion.Tests/CompanionExplainEndpointTests.cs @@ -41,6 +41,7 @@ public sealed class CompanionExplainEndpointTests using var client = factory.CreateClient(); client.DefaultRequestHeaders.Add("X-StellaOps-Client", "companion-tests"); client.DefaultRequestHeaders.Add("X-StellaOps-Scopes", "advisory:companion"); + client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "test-tenant"); var request = new CompanionExplainRequest { @@ -84,6 +85,7 @@ public sealed class CompanionExplainEndpointTests using var client = factory.CreateClient(); client.DefaultRequestHeaders.Add("X-StellaOps-Client", "companion-tests"); client.DefaultRequestHeaders.Add("X-StellaOps-Scopes", "advisory:companion"); + client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "test-tenant"); var request = new CompanionExplainRequest { diff --git a/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Integration/KnowledgeSearchEndpointsIntegrationTests.cs b/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Integration/KnowledgeSearchEndpointsIntegrationTests.cs index 0f867ce05..a6f81550a 100644 --- a/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Integration/KnowledgeSearchEndpointsIntegrationTests.cs +++ b/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Integration/KnowledgeSearchEndpointsIntegrationTests.cs @@ -49,6 +49,7 @@ public sealed class KnowledgeSearchEndpointsIntegrationTests : IDisposable { using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-StellaOps-Scopes", "advisory:search"); + client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "test-tenant"); var response = await client.PostAsJsonAsync("/v1/advisory-ai/search", new AdvisoryKnowledgeSearchRequest { @@ -75,6 +76,7 @@ public sealed class KnowledgeSearchEndpointsIntegrationTests : IDisposable { using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-StellaOps-Scopes", "advisory:index:write"); + client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "test-tenant"); var response = await client.PostAsync("/v1/advisory-ai/index/rebuild", content: null); response.StatusCode.Should().Be(HttpStatusCode.OK); diff --git a/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Integration/LlmAdapterEndpointsIntegrationTests.cs b/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Integration/LlmAdapterEndpointsIntegrationTests.cs index 53572de03..6cd2c16ba 100644 --- a/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Integration/LlmAdapterEndpointsIntegrationTests.cs +++ b/src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/Integration/LlmAdapterEndpointsIntegrationTests.cs @@ -40,6 +40,7 @@ public sealed class LlmAdapterEndpointsIntegrationTests { using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-StellaOps-Scopes", "advisory:adapter:read"); + client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "test-tenant"); var response = await client.GetAsync("/v1/advisory-ai/adapters/llm/providers"); response.StatusCode.Should().Be(HttpStatusCode.OK); @@ -57,6 +58,7 @@ public sealed class LlmAdapterEndpointsIntegrationTests { using var client = _factory.CreateClient(); client.DefaultRequestHeaders.Add("X-StellaOps-Scopes", "advisory:adapter:invoke"); + client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "test-tenant"); var request = new OpenAiChatCompletionRequest { diff --git a/src/AirGap/StellaOps.AirGap.Controller/Endpoints/AirGapEndpoints.cs b/src/AirGap/StellaOps.AirGap.Controller/Endpoints/AirGapEndpoints.cs index adcd0d7c7..afc58cf74 100644 --- a/src/AirGap/StellaOps.AirGap.Controller/Endpoints/AirGapEndpoints.cs +++ b/src/AirGap/StellaOps.AirGap.Controller/Endpoints/AirGapEndpoints.cs @@ -6,6 +6,7 @@ using StellaOps.AirGap.Time.Models; using StellaOps.AirGap.Time.Services; using StellaOps.Auth.Abstractions; using System.Security.Claims; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.AirGap.Controller.Endpoints; @@ -14,7 +15,8 @@ internal static class AirGapEndpoints public static RouteGroupBuilder MapAirGapEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/system/airgap") - .RequireAuthorization(AirGapPolicies.StatusRead); + .RequireAuthorization(AirGapPolicies.StatusRead) + .RequireTenant(); group.MapGet("/status", HandleStatus) .RequireAuthorization(AirGapPolicies.StatusRead) diff --git a/src/AirGap/StellaOps.AirGap.Controller/Program.cs b/src/AirGap/StellaOps.AirGap.Controller/Program.cs index 999940514..c3f94881b 100644 --- a/src/AirGap/StellaOps.AirGap.Controller/Program.cs +++ b/src/AirGap/StellaOps.AirGap.Controller/Program.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authentication; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.AirGap.Controller.Auth; using StellaOps.AirGap.Controller.DependencyInjection; using StellaOps.AirGap.Controller.Endpoints; @@ -29,6 +30,7 @@ builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddAirGapController(builder.Configuration); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); // Stella Router integration @@ -44,6 +46,7 @@ app.LogStellaOpsLocalHostname("airgap-controller"); app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); app.MapAirGapEndpoints(); diff --git a/src/Attestor/StellaOps.Attestor.TileProxy/Endpoints/TileEndpoints.cs b/src/Attestor/StellaOps.Attestor.TileProxy/Endpoints/TileEndpoints.cs index 4e30cdd1c..589a52516 100644 --- a/src/Attestor/StellaOps.Attestor.TileProxy/Endpoints/TileEndpoints.cs +++ b/src/Attestor/StellaOps.Attestor.TileProxy/Endpoints/TileEndpoints.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using StellaOps.Attestor.TileProxy.Services; using System.Text.Json; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.Attestor.TileProxy.Endpoints; @@ -48,7 +49,8 @@ public static class TileEndpoints .Produces(StatusCodes.Status502BadGateway); // Admin endpoints - var admin = endpoints.MapGroup("/_admin"); + var admin = endpoints.MapGroup("/_admin") + .RequireTenant(); admin.MapGet("/cache/stats", GetCacheStats) .WithName("GetCacheStats") diff --git a/src/Attestor/StellaOps.Attestor.WebService/Endpoints/VerdictEndpoints.cs b/src/Attestor/StellaOps.Attestor.WebService/Endpoints/VerdictEndpoints.cs index 88878cf28..366155d19 100644 --- a/src/Attestor/StellaOps.Attestor.WebService/Endpoints/VerdictEndpoints.cs +++ b/src/Attestor/StellaOps.Attestor.WebService/Endpoints/VerdictEndpoints.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using StellaOps.Attestor.Persistence.Entities; using StellaOps.Attestor.Services; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.Attestor.WebService.Endpoints; @@ -25,7 +26,8 @@ public static class VerdictEndpoints { var group = app.MapGroup("/api/v1/verdicts") .WithTags("Verdicts") - .WithOpenApi(); + .WithOpenApi() + .RequireTenant(); group.MapPost("/", CreateVerdict) .WithName("CreateVerdict") diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/Api/ProofsApiContractTests.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/Api/ProofsApiContractTests.cs index c7785de60..1808a1452 100644 --- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/Api/ProofsApiContractTests.cs +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/Api/ProofsApiContractTests.cs @@ -26,6 +26,7 @@ public class ProofsApiContractTests : IClassFixture", Encoding.UTF8, "application/xml") @@ -370,6 +381,7 @@ public sealed class AttestorContractSnapshotTests : IClassFixture10MB) @@ -287,6 +295,7 @@ public sealed class AttestorNegativeTests : IClassFixture { options.TimeProvider ??= TimeProvider.System; }); } + builder.Services.AddStellaOpsTenantServices(); builder.Services.AddAuthorization(options => { options.AddPolicy("attestor:write", policy => @@ -411,6 +413,7 @@ internal static class AttestorWebServiceComposition app.UseAuthentication(); app.UseAuthorization(); + app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); app.MapHealthChecks("/health/ready"); diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Endpoints/PredicateRegistryEndpoints.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Endpoints/PredicateRegistryEndpoints.cs index d752e67c8..ab6f7a7d8 100644 --- a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Endpoints/PredicateRegistryEndpoints.cs +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/Endpoints/PredicateRegistryEndpoints.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using StellaOps.Attestor.Persistence.Repositories; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.Attestor.WebService.Endpoints; @@ -24,7 +25,8 @@ public static class PredicateRegistryEndpoints public static void MapPredicateRegistryEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/api/v1/attestor/predicates") - .WithTags("Predicate Registry"); + .WithTags("Predicate Registry") + .RequireTenant(); group.MapGet("/", ListPredicateTypes) .WithName("ListPredicateTypes") diff --git a/src/BinaryIndex/StellaOps.BinaryIndex.WebService/Program.cs b/src/BinaryIndex/StellaOps.BinaryIndex.WebService/Program.cs index 1a5bea503..f9f6ec5ad 100644 --- a/src/BinaryIndex/StellaOps.BinaryIndex.WebService/Program.cs +++ b/src/BinaryIndex/StellaOps.BinaryIndex.WebService/Program.cs @@ -12,6 +12,7 @@ using StellaOps.BinaryIndex.VexBridge; using StellaOps.BinaryIndex.WebService.Middleware; using StellaOps.BinaryIndex.WebService.Services; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.BinaryIndex.WebService.Telemetry; using StellaOps.Router.AspNet; @@ -65,6 +66,7 @@ builder.Services.AddResolutionRateLimiting(options => builder.Services.AddHealthChecks() .AddRedis(redisConnectionString, name: "redis"); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); // Stella Router integration @@ -88,6 +90,7 @@ app.UseStellaOpsCors(); // HTTPS redirection removed — the gateway handles TLS termination. app.UseResolutionRateLimiting(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); app.MapControllers(); app.MapHealthChecks("/health"); diff --git a/src/Cartographer/StellaOps.Cartographer/Program.cs b/src/Cartographer/StellaOps.Cartographer/Program.cs index 87a2941ce..534a313f6 100644 --- a/src/Cartographer/StellaOps.Cartographer/Program.cs +++ b/src/Cartographer/StellaOps.Cartographer/Program.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Cartographer.Options; using StellaOps.Router.AspNet; @@ -71,6 +72,7 @@ if (authorityOptions.Enabled) builder.Services.AddHealthChecks() .AddCheck("cartographer_ready", () => HealthCheckResult.Healthy(), tags: new[] { "ready" }); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); // Stella Router integration @@ -99,6 +101,7 @@ if (authorityOptions.Enabled) app.UseAuthorization(); app.TryUseStellaRouter(routerEnabled); } +app.UseStellaOpsTenantMiddleware(); app.MapHealthChecks("/healthz").AllowAnonymous(); app.MapHealthChecks("/readyz", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions diff --git a/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceAuditEndpointsTests.cs b/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceAuditEndpointsTests.cs index d09a6d38f..fd1f16b28 100644 --- a/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceAuditEndpointsTests.cs +++ b/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceAuditEndpointsTests.cs @@ -100,6 +100,7 @@ public sealed class EvidenceAuditEndpointsTests : IDisposable client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(EvidenceLockerTestAuthHandler.SchemeName); client.DefaultRequestHeaders.Add("X-Test-Tenant", tenantId); client.DefaultRequestHeaders.Add("X-Test-Scopes", scopes); + client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId); } public void Dispose() diff --git a/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceLockerIntegrationTests.cs b/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceLockerIntegrationTests.cs index 0b69670b5..ce78b54be 100644 --- a/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceLockerIntegrationTests.cs +++ b/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceLockerIntegrationTests.cs @@ -412,6 +412,7 @@ public sealed class EvidenceLockerIntegrationTests : IDisposable client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "test-token"); client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId); client.DefaultRequestHeaders.Add("X-Scopes", scopes); + client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId); } public void Dispose() diff --git a/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceLockerWebServiceContractTests.cs b/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceLockerWebServiceContractTests.cs index 09082f5e9..80ea31d15 100644 --- a/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceLockerWebServiceContractTests.cs +++ b/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceLockerWebServiceContractTests.cs @@ -467,6 +467,7 @@ public sealed class EvidenceLockerWebServiceContractTests : IDisposable client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "test-token"); client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId); client.DefaultRequestHeaders.Add("X-Scopes", scopes); + client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId); } public void Dispose() diff --git a/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceReindexIntegrationTests.cs b/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceReindexIntegrationTests.cs index bb258421c..72fdbbdf9 100644 --- a/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceReindexIntegrationTests.cs +++ b/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/EvidenceReindexIntegrationTests.cs @@ -308,6 +308,7 @@ public sealed class EvidenceReindexIntegrationTests : IDisposable client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId); client.DefaultRequestHeaders.Add("X-Test-Subject", "test-user@example.com"); client.DefaultRequestHeaders.Add("X-Scopes", scopes); + client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId); } private static string ComputeSha256(string input) diff --git a/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/ExportEndpointsTests.cs b/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/ExportEndpointsTests.cs index 1dd43a000..86b4ca090 100644 --- a/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/ExportEndpointsTests.cs +++ b/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/ExportEndpointsTests.cs @@ -32,6 +32,8 @@ public sealed class ExportEndpointsTests : IClassFixture +/// Repository for managing export distributions. +/// +public interface IExportDistributionRepository +{ + /// + /// Gets a distribution by ID. + /// + Task GetByIdAsync( + Guid tenantId, + Guid distributionId, + CancellationToken cancellationToken = default); + + /// + /// Gets a distribution by idempotency key. + /// + Task GetByIdempotencyKeyAsync( + Guid tenantId, + string idempotencyKey, + CancellationToken cancellationToken = default); + + /// + /// Lists distributions for a run. + /// + Task> ListByRunAsync( + Guid tenantId, + Guid runId, + CancellationToken cancellationToken = default); + + /// + /// Lists distributions by status. + /// + Task> ListByStatusAsync( + Guid tenantId, + ExportDistributionStatus status, + int limit = 100, + CancellationToken cancellationToken = default); + + /// + /// Lists distributions due for retention deletion. + /// + Task> ListExpiredAsync( + DateTimeOffset asOf, + int limit = 100, + CancellationToken cancellationToken = default); + + /// + /// Creates a new distribution record. + /// + Task CreateAsync( + ExportDistribution distribution, + CancellationToken cancellationToken = default); + + /// + /// Updates a distribution record. + /// Returns the updated record, or null if not found. + /// + Task UpdateAsync( + ExportDistribution distribution, + CancellationToken cancellationToken = default); + + /// + /// Performs an idempotent upsert based on idempotency key. + /// Returns existing distribution if key matches, otherwise creates new. + /// + Task<(ExportDistribution Distribution, bool WasCreated)> UpsertByIdempotencyKeyAsync( + ExportDistribution distribution, + CancellationToken cancellationToken = default); + + /// + /// Marks a distribution for deletion. + /// + Task MarkForDeletionAsync( + Guid tenantId, + Guid distributionId, + CancellationToken cancellationToken = default); + + /// + /// Deletes a distribution record and returns whether it existed. + /// + Task DeleteAsync( + Guid tenantId, + Guid distributionId, + CancellationToken cancellationToken = default); + + /// + /// Gets distribution statistics for a run. + /// + Task GetStatsAsync( + Guid tenantId, + Guid runId, + CancellationToken cancellationToken = default); +} + +/// +/// Statistics for distributions of a run. +/// +public sealed record ExportDistributionStats +{ + public int Total { get; init; } + public int Pending { get; init; } + public int Distributing { get; init; } + public int Distributed { get; init; } + public int Verified { get; init; } + public int Failed { get; init; } + public int Cancelled { get; init; } + public long TotalSizeBytes { get; init; } +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Persistence/IExportProfileRepository.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Persistence/IExportProfileRepository.cs new file mode 100644 index 000000000..fd8ed707a --- /dev/null +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Persistence/IExportProfileRepository.cs @@ -0,0 +1,66 @@ +using StellaOps.ExportCenter.Core.Domain; + +namespace StellaOps.ExportCenter.Core.Persistence; + +/// +/// Repository for managing export profiles. +/// +public interface IExportProfileRepository +{ + /// + /// Gets a profile by ID for a tenant. + /// + Task GetByIdAsync( + Guid tenantId, + Guid profileId, + CancellationToken cancellationToken = default); + + /// + /// Lists profiles for a tenant with optional filtering. + /// + Task<(IReadOnlyList Items, int TotalCount)> ListAsync( + Guid tenantId, + ExportProfileStatus? status = null, + ExportProfileKind? kind = null, + string? search = null, + int offset = 0, + int limit = 50, + CancellationToken cancellationToken = default); + + /// + /// Creates a new profile. + /// + Task CreateAsync( + ExportProfile profile, + CancellationToken cancellationToken = default); + + /// + /// Updates an existing profile. + /// + Task UpdateAsync( + ExportProfile profile, + CancellationToken cancellationToken = default); + + /// + /// Archives a profile (soft delete). + /// + Task ArchiveAsync( + Guid tenantId, + Guid profileId, + CancellationToken cancellationToken = default); + + /// + /// Checks if a profile name is unique within a tenant. + /// + Task IsNameUniqueAsync( + Guid tenantId, + string name, + Guid? excludeProfileId = null, + CancellationToken cancellationToken = default); + + /// + /// Gets active scheduled profiles for processing. + /// + Task> GetScheduledProfilesAsync( + CancellationToken cancellationToken = default); +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Persistence/IExportRunRepository.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Persistence/IExportRunRepository.cs new file mode 100644 index 000000000..c1e5a1de9 --- /dev/null +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Persistence/IExportRunRepository.cs @@ -0,0 +1,133 @@ +using StellaOps.ExportCenter.Core.Domain; + +namespace StellaOps.ExportCenter.Core.Persistence; + +/// +/// Repository for managing export runs. +/// +public interface IExportRunRepository +{ + /// + /// Gets a run by ID for a tenant. + /// + Task GetByIdAsync( + Guid tenantId, + Guid runId, + CancellationToken cancellationToken = default); + + /// + /// Lists runs for a tenant with optional filtering. + /// + Task<(IReadOnlyList Items, int TotalCount)> ListAsync( + Guid tenantId, + Guid? profileId = null, + ExportRunStatus? status = null, + ExportRunTrigger? trigger = null, + DateTimeOffset? createdAfter = null, + DateTimeOffset? createdBefore = null, + string? correlationId = null, + int offset = 0, + int limit = 50, + CancellationToken cancellationToken = default); + + /// + /// Creates a new run. + /// + Task CreateAsync( + ExportRun run, + CancellationToken cancellationToken = default); + + /// + /// Updates run status and progress. + /// + Task UpdateAsync( + ExportRun run, + CancellationToken cancellationToken = default); + + /// + /// Cancels a run if it's in a cancellable state. + /// + Task CancelAsync( + Guid tenantId, + Guid runId, + CancellationToken cancellationToken = default); + + /// + /// Gets active runs count for concurrency checks. + /// + Task GetActiveRunsCountAsync( + Guid tenantId, + Guid? profileId = null, + CancellationToken cancellationToken = default); + + /// + /// Gets queued runs count. + /// + Task GetQueuedRunsCountAsync( + Guid tenantId, + CancellationToken cancellationToken = default); + + /// + /// Gets the next queued run to execute. + /// + Task DequeueNextRunAsync( + Guid tenantId, + CancellationToken cancellationToken = default); +} + +/// +/// Repository for managing export artifacts. +/// +public interface IExportArtifactRepository +{ + /// + /// Gets an artifact by ID. + /// + Task GetByIdAsync( + Guid tenantId, + Guid artifactId, + CancellationToken cancellationToken = default); + + /// + /// Lists artifacts for a run. + /// + Task> ListByRunAsync( + Guid tenantId, + Guid runId, + CancellationToken cancellationToken = default); + + /// + /// Creates a new artifact record. + /// + Task CreateAsync( + ExportArtifact artifact, + CancellationToken cancellationToken = default); + + /// + /// Deletes artifacts for a run. + /// + Task DeleteByRunAsync( + Guid tenantId, + Guid runId, + CancellationToken cancellationToken = default); +} + +/// +/// Represents an export artifact. +/// +public sealed record ExportArtifact +{ + public required Guid ArtifactId { get; init; } + public required Guid RunId { get; init; } + public required Guid TenantId { get; init; } + public required string Name { get; init; } + public required string Kind { get; init; } + public required string Path { get; init; } + public long SizeBytes { get; init; } + public string? ContentType { get; init; } + public required string Checksum { get; init; } + public string ChecksumAlgorithm { get; init; } = "SHA-256"; + public IReadOnlyDictionary? Metadata { get; init; } + public DateTimeOffset CreatedAt { get; init; } + public DateTimeOffset? ExpiresAt { get; init; } +} diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Postgres/Repositories/PostgresExportDistributionRepository.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Postgres/Repositories/PostgresExportDistributionRepository.cs index 7c829c08e..def0e9686 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Postgres/Repositories/PostgresExportDistributionRepository.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Postgres/Repositories/PostgresExportDistributionRepository.cs @@ -4,7 +4,7 @@ using Npgsql; using StellaOps.ExportCenter.Core.Domain; using StellaOps.ExportCenter.Infrastructure.Db; using StellaOps.ExportCenter.Infrastructure.EfCore.Models; -using StellaOps.ExportCenter.WebService.Distribution; +using StellaOps.ExportCenter.Core.Persistence; namespace StellaOps.ExportCenter.Infrastructure.Postgres.Repositories; diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Postgres/Repositories/PostgresExportProfileRepository.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Postgres/Repositories/PostgresExportProfileRepository.cs index cf55aee20..07b9e9477 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Postgres/Repositories/PostgresExportProfileRepository.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Postgres/Repositories/PostgresExportProfileRepository.cs @@ -4,7 +4,7 @@ using Npgsql; using StellaOps.ExportCenter.Core.Domain; using StellaOps.ExportCenter.Infrastructure.Db; using StellaOps.ExportCenter.Infrastructure.EfCore.Models; -using StellaOps.ExportCenter.WebService.Api; +using StellaOps.ExportCenter.Core.Persistence; namespace StellaOps.ExportCenter.Infrastructure.Postgres.Repositories; diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Postgres/Repositories/PostgresExportRunRepository.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Postgres/Repositories/PostgresExportRunRepository.cs index 33cdaedf7..aaa468a46 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Postgres/Repositories/PostgresExportRunRepository.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Postgres/Repositories/PostgresExportRunRepository.cs @@ -4,7 +4,7 @@ using Npgsql; using StellaOps.ExportCenter.Core.Domain; using StellaOps.ExportCenter.Infrastructure.Db; using StellaOps.ExportCenter.Infrastructure.EfCore.Models; -using StellaOps.ExportCenter.WebService.Api; +using StellaOps.ExportCenter.Core.Persistence; namespace StellaOps.ExportCenter.Infrastructure.Postgres.Repositories; diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiRepositoryTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiRepositoryTests.cs index f5ae6e2ea..14b5e4f0c 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiRepositoryTests.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiRepositoryTests.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging.Abstractions; using StellaOps.ExportCenter.Core.Domain; +using StellaOps.ExportCenter.Core.Persistence; using StellaOps.ExportCenter.WebService.Api; using StellaOps.TestKit; diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiServiceCollectionExtensionsTests.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiServiceCollectionExtensionsTests.cs index dfe5a78ea..5273d73f7 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiServiceCollectionExtensionsTests.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Tests/Api/ExportApiServiceCollectionExtensionsTests.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.ExportCenter.Core.Persistence; using StellaOps.ExportCenter.WebService.Api; using Xunit; diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiEndpoints.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiEndpoints.cs index 6ebe3809c..6badcc476 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiEndpoints.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiEndpoints.cs @@ -6,7 +6,9 @@ using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; using StellaOps.Determinism; using StellaOps.ExportCenter.Core.Domain; +using StellaOps.ExportCenter.Core.Persistence; using StellaOps.ExportCenter.Core.Planner; +using IExportProfileRepository = StellaOps.ExportCenter.Core.Persistence.IExportProfileRepository; using StellaOps.ExportCenter.WebService.Telemetry; using System.Runtime.CompilerServices; using System.Security.Claims; diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiServiceCollectionExtensions.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiServiceCollectionExtensions.cs index de8184a9b..ba8ac7b75 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiServiceCollectionExtensions.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using StellaOps.Determinism; +using StellaOps.ExportCenter.Core.Persistence; namespace StellaOps.ExportCenter.WebService.Api; diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/IExportProfileRepository.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/IExportProfileRepository.cs index 44a0434ed..c398a083f 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/IExportProfileRepository.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/IExportProfileRepository.cs @@ -1,66 +1,3 @@ -using StellaOps.ExportCenter.Core.Domain; - -namespace StellaOps.ExportCenter.WebService.Api; - -/// -/// Repository for managing export profiles. -/// -public interface IExportProfileRepository -{ - /// - /// Gets a profile by ID for a tenant. - /// - Task GetByIdAsync( - Guid tenantId, - Guid profileId, - CancellationToken cancellationToken = default); - - /// - /// Lists profiles for a tenant with optional filtering. - /// - Task<(IReadOnlyList Items, int TotalCount)> ListAsync( - Guid tenantId, - ExportProfileStatus? status = null, - ExportProfileKind? kind = null, - string? search = null, - int offset = 0, - int limit = 50, - CancellationToken cancellationToken = default); - - /// - /// Creates a new profile. - /// - Task CreateAsync( - ExportProfile profile, - CancellationToken cancellationToken = default); - - /// - /// Updates an existing profile. - /// - Task UpdateAsync( - ExportProfile profile, - CancellationToken cancellationToken = default); - - /// - /// Archives a profile (soft delete). - /// - Task ArchiveAsync( - Guid tenantId, - Guid profileId, - CancellationToken cancellationToken = default); - - /// - /// Checks if a profile name is unique within a tenant. - /// - Task IsNameUniqueAsync( - Guid tenantId, - string name, - Guid? excludeProfileId = null, - CancellationToken cancellationToken = default); - - /// - /// Gets active scheduled profiles for processing. - /// - Task> GetScheduledProfilesAsync( - CancellationToken cancellationToken = default); -} +// This interface has been moved to StellaOps.ExportCenter.Core.Persistence. +// Import that namespace instead of StellaOps.ExportCenter.WebService.Api for IExportProfileRepository. +// This file is kept for reference only. diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/IExportRunRepository.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/IExportRunRepository.cs index 76c61b172..3fd41327b 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/IExportRunRepository.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/IExportRunRepository.cs @@ -1,133 +1,4 @@ -using StellaOps.ExportCenter.Core.Domain; - -namespace StellaOps.ExportCenter.WebService.Api; - -/// -/// Repository for managing export runs. -/// -public interface IExportRunRepository -{ - /// - /// Gets a run by ID for a tenant. - /// - Task GetByIdAsync( - Guid tenantId, - Guid runId, - CancellationToken cancellationToken = default); - - /// - /// Lists runs for a tenant with optional filtering. - /// - Task<(IReadOnlyList Items, int TotalCount)> ListAsync( - Guid tenantId, - Guid? profileId = null, - ExportRunStatus? status = null, - ExportRunTrigger? trigger = null, - DateTimeOffset? createdAfter = null, - DateTimeOffset? createdBefore = null, - string? correlationId = null, - int offset = 0, - int limit = 50, - CancellationToken cancellationToken = default); - - /// - /// Creates a new run. - /// - Task CreateAsync( - ExportRun run, - CancellationToken cancellationToken = default); - - /// - /// Updates run status and progress. - /// - Task UpdateAsync( - ExportRun run, - CancellationToken cancellationToken = default); - - /// - /// Cancels a run if it's in a cancellable state. - /// - Task CancelAsync( - Guid tenantId, - Guid runId, - CancellationToken cancellationToken = default); - - /// - /// Gets active runs count for concurrency checks. - /// - Task GetActiveRunsCountAsync( - Guid tenantId, - Guid? profileId = null, - CancellationToken cancellationToken = default); - - /// - /// Gets queued runs count. - /// - Task GetQueuedRunsCountAsync( - Guid tenantId, - CancellationToken cancellationToken = default); - - /// - /// Gets the next queued run to execute. - /// - Task DequeueNextRunAsync( - Guid tenantId, - CancellationToken cancellationToken = default); -} - -/// -/// Repository for managing export artifacts. -/// -public interface IExportArtifactRepository -{ - /// - /// Gets an artifact by ID. - /// - Task GetByIdAsync( - Guid tenantId, - Guid artifactId, - CancellationToken cancellationToken = default); - - /// - /// Lists artifacts for a run. - /// - Task> ListByRunAsync( - Guid tenantId, - Guid runId, - CancellationToken cancellationToken = default); - - /// - /// Creates a new artifact record. - /// - Task CreateAsync( - ExportArtifact artifact, - CancellationToken cancellationToken = default); - - /// - /// Deletes artifacts for a run. - /// - Task DeleteByRunAsync( - Guid tenantId, - Guid runId, - CancellationToken cancellationToken = default); -} - -/// -/// Represents an export artifact. -/// -public sealed record ExportArtifact -{ - public required Guid ArtifactId { get; init; } - public required Guid RunId { get; init; } - public required Guid TenantId { get; init; } - public required string Name { get; init; } - public required string Kind { get; init; } - public required string Path { get; init; } - public long SizeBytes { get; init; } - public string? ContentType { get; init; } - public required string Checksum { get; init; } - public string ChecksumAlgorithm { get; init; } = "SHA-256"; - public IReadOnlyDictionary? Metadata { get; init; } - public DateTimeOffset CreatedAt { get; init; } - public DateTimeOffset? ExpiresAt { get; init; } -} +// The IExportRunRepository, IExportArtifactRepository interfaces and ExportArtifact record +// have been moved to StellaOps.ExportCenter.Core.Persistence. +// Import that namespace instead of StellaOps.ExportCenter.WebService.Api. +// This file is kept for reference only. diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/InMemoryExportRepositories.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/InMemoryExportRepositories.cs index 72717d021..2ca60ae29 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/InMemoryExportRepositories.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/InMemoryExportRepositories.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using StellaOps.ExportCenter.Core.Domain; +using StellaOps.ExportCenter.Core.Persistence; using System.Collections.Concurrent; namespace StellaOps.ExportCenter.WebService.Api; diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/ExportDistributionLifecycle.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/ExportDistributionLifecycle.cs index 38d3b88f5..d4528d3c1 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/ExportDistributionLifecycle.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/ExportDistributionLifecycle.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using StellaOps.Determinism; using StellaOps.ExportCenter.Core.Domain; +using StellaOps.ExportCenter.Core.Persistence; using System.Globalization; using System.Text.Json; diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/ExportDistributionServiceCollectionExtensions.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/ExportDistributionServiceCollectionExtensions.cs index 0427d359b..49efe5071 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/ExportDistributionServiceCollectionExtensions.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/ExportDistributionServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using StellaOps.Determinism; +using StellaOps.ExportCenter.Core.Persistence; using StellaOps.ExportCenter.WebService.Distribution.Oci; namespace StellaOps.ExportCenter.WebService.Distribution; diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/IExportDistributionLifecycle.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/IExportDistributionLifecycle.cs index 85bbba0f7..7e91e3d3d 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/IExportDistributionLifecycle.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/IExportDistributionLifecycle.cs @@ -1,4 +1,5 @@ using StellaOps.ExportCenter.Core.Domain; +using StellaOps.ExportCenter.Core.Persistence; namespace StellaOps.ExportCenter.WebService.Distribution; diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/IExportDistributionRepository.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/IExportDistributionRepository.cs index 2d42e2239..dece59a0e 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/IExportDistributionRepository.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/IExportDistributionRepository.cs @@ -1,112 +1,4 @@ -using StellaOps.ExportCenter.Core.Domain; - -namespace StellaOps.ExportCenter.WebService.Distribution; - -/// -/// Repository for managing export distributions. -/// -public interface IExportDistributionRepository -{ - /// - /// Gets a distribution by ID. - /// - Task GetByIdAsync( - Guid tenantId, - Guid distributionId, - CancellationToken cancellationToken = default); - - /// - /// Gets a distribution by idempotency key. - /// - Task GetByIdempotencyKeyAsync( - Guid tenantId, - string idempotencyKey, - CancellationToken cancellationToken = default); - - /// - /// Lists distributions for a run. - /// - Task> ListByRunAsync( - Guid tenantId, - Guid runId, - CancellationToken cancellationToken = default); - - /// - /// Lists distributions by status. - /// - Task> ListByStatusAsync( - Guid tenantId, - ExportDistributionStatus status, - int limit = 100, - CancellationToken cancellationToken = default); - - /// - /// Lists distributions due for retention deletion. - /// - Task> ListExpiredAsync( - DateTimeOffset asOf, - int limit = 100, - CancellationToken cancellationToken = default); - - /// - /// Creates a new distribution record. - /// - Task CreateAsync( - ExportDistribution distribution, - CancellationToken cancellationToken = default); - - /// - /// Updates a distribution record. - /// Returns the updated record, or null if not found. - /// - Task UpdateAsync( - ExportDistribution distribution, - CancellationToken cancellationToken = default); - - /// - /// Performs an idempotent upsert based on idempotency key. - /// Returns existing distribution if key matches, otherwise creates new. - /// - Task<(ExportDistribution Distribution, bool WasCreated)> UpsertByIdempotencyKeyAsync( - ExportDistribution distribution, - CancellationToken cancellationToken = default); - - /// - /// Marks a distribution for deletion. - /// - Task MarkForDeletionAsync( - Guid tenantId, - Guid distributionId, - CancellationToken cancellationToken = default); - - /// - /// Deletes a distribution record and returns whether it existed. - /// - Task DeleteAsync( - Guid tenantId, - Guid distributionId, - CancellationToken cancellationToken = default); - - /// - /// Gets distribution statistics for a run. - /// - Task GetStatsAsync( - Guid tenantId, - Guid runId, - CancellationToken cancellationToken = default); -} - -/// -/// Statistics for distributions of a run. -/// -public sealed record ExportDistributionStats -{ - public int Total { get; init; } - public int Pending { get; init; } - public int Distributing { get; init; } - public int Distributed { get; init; } - public int Verified { get; init; } - public int Failed { get; init; } - public int Cancelled { get; init; } - public long TotalSizeBytes { get; init; } -} +// The IExportDistributionRepository interface and ExportDistributionStats record +// have been moved to StellaOps.ExportCenter.Core.Persistence. +// Import that namespace instead of StellaOps.ExportCenter.WebService.Distribution. +// This file is kept for reference only. diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/InMemoryExportDistributionRepository.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/InMemoryExportDistributionRepository.cs index 263fe03c7..cfe8dbf9b 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/InMemoryExportDistributionRepository.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Distribution/InMemoryExportDistributionRepository.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Options; using StellaOps.ExportCenter.Core.Domain; +using StellaOps.ExportCenter.Core.Persistence; using System.Collections.Concurrent; namespace StellaOps.ExportCenter.WebService.Distribution; diff --git a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Program.cs b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Program.cs index 730783fd4..c15c02302 100644 --- a/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Program.cs +++ b/src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Program.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authorization; using StellaOps.AirGap.Policy; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.ExportCenter.WebService; using StellaOps.ExportCenter.WebService.Api; using StellaOps.ExportCenter.WebService.Attestation; @@ -104,6 +105,7 @@ builder.Services.AddExportApiServices(options => builder.Services.AddOpenApi(); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); // Stella Router integration @@ -125,6 +127,7 @@ if (app.Environment.IsDevelopment()) app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); // OpenAPI discovery endpoints (anonymous) @@ -162,19 +165,22 @@ app.MapGet("/exports", () => Results.Ok(Array.Empty())) .RequireAuthorization(StellaOpsResourceServerPolicies.ExportViewer) .WithDeprecation(DeprecatedEndpointsRegistry.ListExports) .WithSummary("List exports (DEPRECATED)") - .WithDescription("This endpoint is deprecated. Use GET /v1/exports/profiles instead."); + .WithDescription("This endpoint is deprecated. Use GET /v1/exports/profiles instead.") + .RequireTenant(); app.MapPost("/exports", () => Results.Accepted("/exports", new { status = "scheduled" })) .RequireAuthorization(StellaOpsResourceServerPolicies.ExportOperator) .WithDeprecation(DeprecatedEndpointsRegistry.CreateExport) .WithSummary("Create export (DEPRECATED)") - .WithDescription("This endpoint is deprecated. Use POST /v1/exports/evidence or /v1/exports/attestations instead."); + .WithDescription("This endpoint is deprecated. Use POST /v1/exports/evidence or /v1/exports/attestations instead.") + .RequireTenant(); app.MapDelete("/exports/{id}", (string id) => Results.NoContent()) .RequireAuthorization(StellaOpsResourceServerPolicies.ExportAdmin) .WithDeprecation(DeprecatedEndpointsRegistry.DeleteExport) .WithSummary("Delete export (DEPRECATED)") - .WithDescription("This endpoint is deprecated. Use POST /v1/exports/runs/{id}/cancel instead."); + .WithDescription("This endpoint is deprecated. Use POST /v1/exports/runs/{id}/cancel instead.") + .RequireTenant(); // Refresh Router endpoint cache app.TryRefreshStellaRouterEndpoints(routerEnabled); diff --git a/src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/EvidenceDecisionApiIntegrationTests.cs b/src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/EvidenceDecisionApiIntegrationTests.cs index 0ea31ca12..e2eca58a3 100644 --- a/src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/EvidenceDecisionApiIntegrationTests.cs +++ b/src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/EvidenceDecisionApiIntegrationTests.cs @@ -5,6 +5,7 @@ // ============================================================================= using System.Net; +using System.Net.Http.Headers; using System.Net.Http.Json; using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; @@ -27,6 +28,8 @@ public sealed class EvidenceDecisionApiIntegrationTests : IClassFixture GraphScopeClaimReader.HasAnyScope(context.User, GraphPolicies.ExportScopes)); }); }); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); // Stella Router integration @@ -73,6 +75,7 @@ app.UseRouting(); app.TryUseStellaRouter(routerEnabled); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.MapPost("/graph/search", async (HttpContext context, GraphSearchRequest request, IGraphSearchService service, CancellationToken ct) => { @@ -109,7 +112,8 @@ app.MapPost("/graph/search", async (HttpContext context, GraphSearchRequest requ LogAudit(context, "/graph/search", StatusCodes.Status200OK, sw.ElapsedMilliseconds); return Results.Empty; -}); +}) +.RequireTenant(); app.MapPost("/graph/query", async (HttpContext context, GraphQueryRequest request, IGraphQueryService service, CancellationToken ct) => { @@ -146,7 +150,8 @@ app.MapPost("/graph/query", async (HttpContext context, GraphQueryRequest reques LogAudit(context, "/graph/query", StatusCodes.Status200OK, sw.ElapsedMilliseconds); return Results.Empty; -}); +}) +.RequireTenant(); app.MapPost("/graph/paths", async (HttpContext context, GraphPathRequest request, IGraphPathService service, CancellationToken ct) => { @@ -183,7 +188,8 @@ app.MapPost("/graph/paths", async (HttpContext context, GraphPathRequest request LogAudit(context, "/graph/paths", StatusCodes.Status200OK, sw.ElapsedMilliseconds); return Results.Empty; -}); +}) +.RequireTenant(); app.MapPost("/graph/diff", async (HttpContext context, GraphDiffRequest request, IGraphDiffService service, CancellationToken ct) => { @@ -220,7 +226,8 @@ app.MapPost("/graph/diff", async (HttpContext context, GraphDiffRequest request, LogAudit(context, "/graph/diff", StatusCodes.Status200OK, sw.ElapsedMilliseconds); return Results.Empty; -}); +}) +.RequireTenant(); app.MapPost("/graph/lineage", async (HttpContext context, GraphLineageRequest request, IGraphLineageService service, CancellationToken ct) => { @@ -249,7 +256,8 @@ app.MapPost("/graph/lineage", async (HttpContext context, GraphLineageRequest re var response = await service.GetLineageAsync(tenantId, request, ct); LogAudit(context, "/graph/lineage", StatusCodes.Status200OK, sw.ElapsedMilliseconds); return Results.Ok(response); -}); +}) +.RequireTenant(); app.MapPost("/graph/export", async (HttpContext context, GraphExportRequest request, IGraphExportService service, CancellationToken ct) => { @@ -288,7 +296,8 @@ app.MapPost("/graph/export", async (HttpContext context, GraphExportRequest requ }; LogAudit(context, "/graph/export", StatusCodes.Status200OK, sw.ElapsedMilliseconds); return Results.Ok(manifest); -}); +}) +.RequireTenant(); app.MapGet("/graph/export/{jobId}", async (string jobId, HttpContext context, IGraphExportService service, CancellationToken ct) => { @@ -316,11 +325,12 @@ app.MapGet("/graph/export/{jobId}", async (string jobId, HttpContext context, IG context.Response.Headers["X-Content-SHA256"] = job.Sha256; LogAudit(context, "/graph/export/download", StatusCodes.Status200OK, sw.ElapsedMilliseconds); return Results.File(job.Payload, job.ContentType, $"graph-export-{job.JobId}.{job.Format}"); -}); +}) +.RequireTenant(); -// ──────────────────────────────────────────────────────────────────────────────── +// â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€ // Edge Metadata API -// ──────────────────────────────────────────────────────────────────────────────── +// â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€â"€ app.MapPost("/graph/edges/metadata", async (EdgeMetadataRequest request, HttpContext context, IEdgeMetadataService service, CancellationToken ct) => { @@ -340,7 +350,8 @@ app.MapPost("/graph/edges/metadata", async (EdgeMetadataRequest request, HttpCon var response = await service.GetEdgeMetadataAsync(auth.TenantId!, request, ct); LogAudit(context, "/graph/edges/metadata", StatusCodes.Status200OK, sw.ElapsedMilliseconds); return Results.Ok(response); -}); +}) +.RequireTenant(); app.MapGet("/graph/edges/{edgeId}/metadata", async (string edgeId, HttpContext context, IEdgeMetadataService service, CancellationToken ct) => { @@ -366,7 +377,8 @@ app.MapGet("/graph/edges/{edgeId}/metadata", async (string edgeId, HttpContext c LogAudit(context, "/graph/edges/metadata", StatusCodes.Status200OK, sw.ElapsedMilliseconds); return Results.Ok(result); -}); +}) +.RequireTenant(); app.MapGet("/graph/edges/path/{sourceNodeId}/{targetNodeId}", async (string sourceNodeId, string targetNodeId, HttpContext context, IEdgeMetadataService service, CancellationToken ct) => { @@ -386,7 +398,8 @@ app.MapGet("/graph/edges/path/{sourceNodeId}/{targetNodeId}", async (string sour var edges = await service.GetPathEdgesWithMetadataAsync(auth.TenantId!, sourceNodeId, targetNodeId, ct); LogAudit(context, "/graph/edges/path", StatusCodes.Status200OK, sw.ElapsedMilliseconds); return Results.Ok(new { sourceNodeId, targetNodeId, edges = edges.ToList() }); -}); +}) +.RequireTenant(); app.MapGet("/graph/edges/by-reason/{reason}", async (string reason, int? limit, string? cursor, HttpContext context, IEdgeMetadataService service, CancellationToken ct) => { @@ -412,7 +425,8 @@ app.MapGet("/graph/edges/by-reason/{reason}", async (string reason, int? limit, var response = await service.QueryByReasonAsync(auth.TenantId!, edgeReason, limit ?? 100, cursor, ct); LogAudit(context, "/graph/edges/by-reason", StatusCodes.Status200OK, sw.ElapsedMilliseconds); return Results.Ok(response); -}); +}) +.RequireTenant(); app.MapGet("/graph/edges/by-evidence", async (string evidenceType, string evidenceRef, HttpContext context, IEdgeMetadataService service, CancellationToken ct) => { @@ -432,7 +446,8 @@ app.MapGet("/graph/edges/by-evidence", async (string evidenceType, string eviden var edges = await service.QueryByEvidenceAsync(auth.TenantId!, evidenceType, evidenceRef, ct); LogAudit(context, "/graph/edges/by-evidence", StatusCodes.Status200OK, sw.ElapsedMilliseconds); return Results.Ok(edges); -}); +}) +.RequireTenant(); app.MapGet("/healthz", () => Results.Ok(new { status = "ok" })); diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Endpoints/IssuerEndpoints.cs b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Endpoints/IssuerEndpoints.cs index c3d4719cb..258a8ac9c 100644 --- a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Endpoints/IssuerEndpoints.cs +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Endpoints/IssuerEndpoints.cs @@ -6,6 +6,7 @@ using StellaOps.IssuerDirectory.WebService.Contracts; using StellaOps.IssuerDirectory.WebService.Options; using StellaOps.IssuerDirectory.WebService.Security; using StellaOps.IssuerDirectory.WebService.Services; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.IssuerDirectory.WebService.Endpoints; @@ -14,7 +15,8 @@ public static class IssuerEndpoints public static RouteGroupBuilder MapIssuerEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/issuer-directory/issuers") - .WithTags("Issuer Directory"); + .WithTags("Issuer Directory") + .RequireTenant(); group.MapGet(string.Empty, ListIssuers) .RequireAuthorization(IssuerDirectoryPolicies.Reader) diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Endpoints/IssuerKeyEndpoints.cs b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Endpoints/IssuerKeyEndpoints.cs index 5f7e83d15..1e987040b 100644 --- a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Endpoints/IssuerKeyEndpoints.cs +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Endpoints/IssuerKeyEndpoints.cs @@ -5,6 +5,7 @@ using StellaOps.IssuerDirectory.WebService.Constants; using StellaOps.IssuerDirectory.WebService.Contracts; using StellaOps.IssuerDirectory.WebService.Security; using StellaOps.IssuerDirectory.WebService.Services; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.IssuerDirectory.WebService.Endpoints; @@ -12,7 +13,8 @@ internal static class IssuerKeyEndpoints { public static void MapIssuerKeyEndpoints(this RouteGroupBuilder group) { - var keysGroup = group.MapGroup("{issuerId}/keys"); + var keysGroup = group.MapGroup("{issuerId}/keys") + .RequireTenant(); keysGroup.MapGet(string.Empty, ListKeys) .RequireAuthorization(IssuerDirectoryPolicies.Reader) diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Endpoints/IssuerTrustEndpoints.cs b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Endpoints/IssuerTrustEndpoints.cs index 165cbca10..5df1e22dd 100644 --- a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Endpoints/IssuerTrustEndpoints.cs +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Endpoints/IssuerTrustEndpoints.cs @@ -4,6 +4,7 @@ using StellaOps.IssuerDirectory.WebService.Constants; using StellaOps.IssuerDirectory.WebService.Contracts; using StellaOps.IssuerDirectory.WebService.Security; using StellaOps.IssuerDirectory.WebService.Services; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.IssuerDirectory.WebService.Endpoints; @@ -11,7 +12,8 @@ internal static class IssuerTrustEndpoints { public static void MapIssuerTrustEndpoints(this RouteGroupBuilder group) { - var trustGroup = group.MapGroup("{issuerId}/trust"); + var trustGroup = group.MapGroup("{issuerId}/trust") + .RequireTenant(); trustGroup.MapGet(string.Empty, GetTrust) .RequireAuthorization(IssuerDirectoryPolicies.Reader) diff --git a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Program.cs b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Program.cs index 664dcaf07..fcd1a70f3 100644 --- a/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Program.cs +++ b/src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService/Program.cs @@ -11,6 +11,7 @@ using Serilog; using Serilog.Events; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Configuration; using StellaOps.Infrastructure.Postgres.Options; using StellaOps.IssuerDirectory.Core.Services; @@ -100,6 +101,7 @@ builder.Services.AddOpenTelemetry() .AddRuntimeInstrumentation()) .WithTracing(tracing => tracing.AddAspNetCoreInstrumentation().AddHttpClientInstrumentation()); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); // Stella Router integration @@ -117,6 +119,7 @@ app.UseSerilogRequestLogging(); app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); var issuerGroup = app.MapIssuerEndpoints(); diff --git a/src/Notify/StellaOps.Notify.WebService/Program.cs b/src/Notify/StellaOps.Notify.WebService/Program.cs index 6fb33776e..23277cf1b 100644 --- a/src/Notify/StellaOps.Notify.WebService/Program.cs +++ b/src/Notify/StellaOps.Notify.WebService/Program.cs @@ -398,8 +398,8 @@ static void ConfigureEndpoints(WebApplication app) var options = app.Services.GetRequiredService>().Value; var tenantHeader = options.Api.TenantHeader; var apiBasePath = options.Api.BasePath.TrimEnd('/'); - var apiGroup = app.MapGroup(options.Api.BasePath); - var internalGroup = app.MapGroup(options.Api.InternalBasePath); + var apiGroup = app.MapGroup(options.Api.BasePath).RequireTenant(); + var internalGroup = app.MapGroup(options.Api.InternalBasePath).RequireTenant(); internalGroup.MapPost("/rules/normalize", (JsonNode? body, NotifySchemaMigrationService service) => Normalize(body, service.UpgradeRule)) .WithName("notify.rules.normalize") diff --git a/src/OpsMemory/StellaOps.OpsMemory.WebService/Endpoints/OpsMemoryEndpoints.cs b/src/OpsMemory/StellaOps.OpsMemory.WebService/Endpoints/OpsMemoryEndpoints.cs index 0b6a6401a..2772afe90 100644 --- a/src/OpsMemory/StellaOps.OpsMemory.WebService/Endpoints/OpsMemoryEndpoints.cs +++ b/src/OpsMemory/StellaOps.OpsMemory.WebService/Endpoints/OpsMemoryEndpoints.cs @@ -8,6 +8,7 @@ using StellaOps.OpsMemory.Models; using StellaOps.OpsMemory.Playbook; using StellaOps.OpsMemory.Storage; using StellaOps.OpsMemory.WebService.Security; +using StellaOps.Auth.ServerIntegration.Tenancy; using System.Collections.Immutable; namespace StellaOps.OpsMemory.WebService.Endpoints; @@ -25,7 +26,8 @@ public static class OpsMemoryEndpoints { var group = app.MapGroup("/api/v1/opsmemory") .WithTags("OpsMemory") - .RequireAuthorization(OpsMemoryPolicies.Read); + .RequireAuthorization(OpsMemoryPolicies.Read) + .RequireTenant(); group.MapPost("/decisions", RecordDecisionAsync) .WithName("RecordDecision") diff --git a/src/OpsMemory/StellaOps.OpsMemory.WebService/Program.cs b/src/OpsMemory/StellaOps.OpsMemory.WebService/Program.cs index b31d4063a..39642272f 100644 --- a/src/OpsMemory/StellaOps.OpsMemory.WebService/Program.cs +++ b/src/OpsMemory/StellaOps.OpsMemory.WebService/Program.cs @@ -2,6 +2,7 @@ using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using Npgsql; using StellaOps.Determinism; using StellaOps.OpsMemory.Playbook; @@ -47,6 +48,7 @@ builder.Services.AddAuthorization(options => options.AddStellaOpsScopePolicy(OpsMemoryPolicies.Write, StellaOpsScopes.OpsMemoryWrite); }); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); // Stella Router integration @@ -69,6 +71,7 @@ if (app.Environment.IsDevelopment()) app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); // Map endpoints diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ApprovalEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ApprovalEndpoints.cs index f04afa0b5..9673c436f 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ApprovalEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ApprovalEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.WebService.Contracts; using StellaOps.Orchestrator.WebService.Services; @@ -25,7 +26,8 @@ public static class ApprovalEndpoints { var group = app.MapGroup(prefix) .WithTags("Approvals") - .RequireAuthorization(OrchestratorPolicies.ReleaseRead); + .RequireAuthorization(OrchestratorPolicies.ReleaseRead) + .RequireTenant(); var list = group.MapGet(string.Empty, ListApprovals) .WithDescription("Return a list of release approval requests for the calling tenant, optionally filtered by status (Pending, Approved, Rejected), urgency level, and target environment. Each record includes the associated release, requester identity, SLA deadline, and policy gate context."); diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/AuditEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/AuditEndpoints.cs index 63652946b..6a308e77d 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/AuditEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/AuditEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.Core.Domain; using StellaOps.Orchestrator.Infrastructure.Repositories; using StellaOps.Orchestrator.WebService.Contracts; @@ -18,7 +19,8 @@ public static class AuditEndpoints { var group = app.MapGroup("/api/v1/orchestrator/audit") .WithTags("Orchestrator Audit") - .RequireAuthorization(OrchestratorPolicies.Read); + .RequireAuthorization(OrchestratorPolicies.Read) + .RequireTenant(); // List and get operations group.MapGet(string.Empty, ListAuditEntries) diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/CircuitBreakerEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/CircuitBreakerEndpoints.cs index 0fb917a55..888160252 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/CircuitBreakerEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/CircuitBreakerEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.Core.Domain; using StellaOps.Orchestrator.Core.Services; using StellaOps.Orchestrator.WebService.Contracts; @@ -18,7 +19,8 @@ public static class CircuitBreakerEndpoints { var group = app.MapGroup("/api/v1/orchestrator/circuit-breakers") .WithTags("Orchestrator Circuit Breakers") - .RequireAuthorization(OrchestratorPolicies.Read); + .RequireAuthorization(OrchestratorPolicies.Read) + .RequireTenant(); // List circuit breakers group.MapGet(string.Empty, ListCircuitBreakers) diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/DagEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/DagEndpoints.cs index bf40f6a9e..32c8fecd2 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/DagEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/DagEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.Core.Scheduling; using StellaOps.Orchestrator.Infrastructure.Repositories; using StellaOps.Orchestrator.WebService.Contracts; @@ -18,7 +19,8 @@ public static class DagEndpoints { var group = app.MapGroup("/api/v1/orchestrator/dag") .WithTags("Orchestrator DAG") - .RequireAuthorization(OrchestratorPolicies.Read); + .RequireAuthorization(OrchestratorPolicies.Read) + .RequireTenant(); group.MapGet("run/{runId:guid}", GetRunDag) .WithName("Orchestrator_GetRunDag") diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/DeadLetterEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/DeadLetterEndpoints.cs index 49a0d73e9..be802474c 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/DeadLetterEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/DeadLetterEndpoints.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Npgsql; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.Core.DeadLetter; using StellaOps.Orchestrator.Core.Domain; using StellaOps.Orchestrator.WebService.Services; @@ -22,7 +23,8 @@ public static class DeadLetterEndpoints { var group = app.MapGroup("/api/v1/orchestrator/deadletter") .WithTags("Orchestrator Dead-Letter") - .RequireAuthorization(OrchestratorPolicies.Read); + .RequireAuthorization(OrchestratorPolicies.Read) + .RequireTenant(); // Entry management group.MapGet(string.Empty, ListEntries) diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ExportJobEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ExportJobEndpoints.cs index c1aea9332..bad6297dc 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ExportJobEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ExportJobEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Http.HttpResults; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.Core.Domain; using StellaOps.Orchestrator.Core.Domain.Export; using StellaOps.Orchestrator.Core.Services; @@ -18,7 +19,8 @@ public static class ExportJobEndpoints { var group = app.MapGroup("/api/v1/orchestrator/export") .WithTags("Export Jobs") - .RequireAuthorization(OrchestratorPolicies.ExportViewer); + .RequireAuthorization(OrchestratorPolicies.ExportViewer) + .RequireTenant(); group.MapPost("jobs", CreateExportJob) .WithName("Orchestrator_CreateExportJob") diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/FirstSignalEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/FirstSignalEndpoints.cs index eec9b24eb..ddd8eef1e 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/FirstSignalEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/FirstSignalEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.Core.Services; using StellaOps.Orchestrator.WebService.Contracts; using StellaOps.Orchestrator.WebService.Services; @@ -14,7 +15,8 @@ public static class FirstSignalEndpoints { var group = app.MapGroup("/api/v1/orchestrator/runs") .WithTags("Orchestrator Runs") - .RequireAuthorization(OrchestratorPolicies.Read); + .RequireAuthorization(OrchestratorPolicies.Read) + .RequireTenant(); group.MapGet("{runId:guid}/first-signal", GetFirstSignal) .WithName("Orchestrator_GetFirstSignal") diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/JobEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/JobEndpoints.cs index fb114e5a2..f9bb68528 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/JobEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/JobEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.Infrastructure.Repositories; using StellaOps.Orchestrator.WebService.Contracts; using StellaOps.Orchestrator.WebService.Services; @@ -17,7 +18,8 @@ public static class JobEndpoints { var group = app.MapGroup("/api/v1/orchestrator/jobs") .WithTags("Orchestrator Jobs") - .RequireAuthorization(OrchestratorPolicies.Read); + .RequireAuthorization(OrchestratorPolicies.Read) + .RequireTenant(); group.MapGet(string.Empty, ListJobs) .WithName("Orchestrator_ListJobs") diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/KpiEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/KpiEndpoints.cs index b76fae7c0..333e5bc37 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/KpiEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/KpiEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Metrics.Kpi; namespace StellaOps.Orchestrator.WebService.Endpoints; @@ -16,7 +17,8 @@ public static class KpiEndpoints { var group = app.MapGroup("/api/v1/metrics/kpis") .WithTags("Quality KPIs") - .RequireAuthorization(OrchestratorPolicies.ObservabilityRead); + .RequireAuthorization(OrchestratorPolicies.ObservabilityRead) + .RequireTenant(); // GET /api/v1/metrics/kpis group.MapGet("/", GetQualityKpis) diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/LedgerEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/LedgerEndpoints.cs index 52833a4b6..a3ba3baa8 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/LedgerEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/LedgerEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.Core.Domain; using StellaOps.Orchestrator.Infrastructure.Repositories; using StellaOps.Orchestrator.WebService.Contracts; @@ -18,7 +19,8 @@ public static class LedgerEndpoints { var group = app.MapGroup("/api/v1/orchestrator/ledger") .WithTags("Orchestrator Ledger") - .RequireAuthorization(OrchestratorPolicies.Read); + .RequireAuthorization(OrchestratorPolicies.Read) + .RequireTenant(); // Ledger entry operations group.MapGet(string.Empty, ListLedgerEntries) diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/PackRegistryEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/PackRegistryEndpoints.cs index fd4da3850..81f5a1e55 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/PackRegistryEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/PackRegistryEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.Core.Domain; using StellaOps.Orchestrator.Infrastructure.Repositories; using StellaOps.Orchestrator.WebService.Contracts; @@ -22,7 +23,8 @@ public static class PackRegistryEndpoints { var group = app.MapGroup("/api/v1/orchestrator/registry/packs") .WithTags("Orchestrator Pack Registry") - .RequireAuthorization(OrchestratorPolicies.PacksRead); + .RequireAuthorization(OrchestratorPolicies.PacksRead) + .RequireTenant(); // Pack CRUD endpoints group.MapPost("", CreatePack) diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/PackRunEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/PackRunEndpoints.cs index ac20a6826..103b13098 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/PackRunEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/PackRunEndpoints.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using PackLogLevel = StellaOps.Orchestrator.Core.Domain.LogLevel; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Cryptography; using StellaOps.Orchestrator.Core.Domain; using StellaOps.Orchestrator.Core.Domain.Events; @@ -38,7 +39,8 @@ public static class PackRunEndpoints { var group = app.MapGroup("/api/v1/orchestrator/pack-runs") .WithTags("Orchestrator Pack Runs") - .RequireAuthorization(OrchestratorPolicies.PacksRead); + .RequireAuthorization(OrchestratorPolicies.PacksRead) + .RequireTenant(); // Scheduling endpoints group.MapPost("", SchedulePackRun) diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/QuotaEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/QuotaEndpoints.cs index 25654a575..e36a55143 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/QuotaEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/QuotaEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.Core.Domain; using StellaOps.Orchestrator.Infrastructure.Postgres; using StellaOps.Orchestrator.Infrastructure.Repositories; @@ -19,7 +20,8 @@ public static class QuotaEndpoints { var group = app.MapGroup("/api/v1/orchestrator/quotas") .WithTags("Orchestrator Quotas") - .RequireAuthorization(OrchestratorPolicies.Quota); + .RequireAuthorization(OrchestratorPolicies.Quota) + .RequireTenant(); // Quota CRUD operations group.MapGet(string.Empty, ListQuotas) diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/QuotaGovernanceEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/QuotaGovernanceEndpoints.cs index 25aa86dd2..2aeb7408c 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/QuotaGovernanceEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/QuotaGovernanceEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.Core.Domain; using StellaOps.Orchestrator.Core.Services; using StellaOps.Orchestrator.WebService.Contracts; @@ -18,7 +19,8 @@ public static class QuotaGovernanceEndpoints { var group = app.MapGroup("/api/v1/orchestrator/quota-governance") .WithTags("Orchestrator Quota Governance") - .RequireAuthorization(OrchestratorPolicies.Read); + .RequireAuthorization(OrchestratorPolicies.Read) + .RequireTenant(); // Policy management group.MapGet("policies", ListPolicies) diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ReleaseControlV2Endpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ReleaseControlV2Endpoints.cs index 559854877..2ba25f10e 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ReleaseControlV2Endpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ReleaseControlV2Endpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.WebService.Contracts; using StellaOps.Orchestrator.WebService.Services; @@ -21,7 +22,8 @@ public static class ReleaseControlV2Endpoints { var approvals = app.MapGroup("/api/v1/approvals") .WithTags("Approvals v2") - .RequireAuthorization(OrchestratorPolicies.ReleaseRead); + .RequireAuthorization(OrchestratorPolicies.ReleaseRead) + .RequireTenant(); approvals.MapGet(string.Empty, ListApprovals) .WithName("ApprovalsV2_List") @@ -73,13 +75,15 @@ public static class ReleaseControlV2Endpoints var apiRuns = app.MapGroup("/api/v1/runs") .WithTags("Runs v2") - .RequireAuthorization(OrchestratorPolicies.ReleaseRead); + .RequireAuthorization(OrchestratorPolicies.ReleaseRead) + .RequireTenant(); MapRunGroup(apiRuns); apiRuns.WithGroupName("runs-v2"); var legacyV1Runs = app.MapGroup("/v1/runs") .WithTags("Runs v2") - .RequireAuthorization(OrchestratorPolicies.ReleaseRead); + .RequireAuthorization(OrchestratorPolicies.ReleaseRead) + .RequireTenant(); MapRunGroup(legacyV1Runs); legacyV1Runs.WithGroupName("runs-v1-compat"); } @@ -88,7 +92,8 @@ public static class ReleaseControlV2Endpoints { var environments = app.MapGroup("/api/v1/environments") .WithTags("Environments v2") - .RequireAuthorization(OrchestratorPolicies.ReleaseRead); + .RequireAuthorization(OrchestratorPolicies.ReleaseRead) + .RequireTenant(); environments.MapGet("/{id}", GetEnvironmentDetail) .WithName("EnvironmentsV2_Get") diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ReleaseDashboardEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ReleaseDashboardEndpoints.cs index bf26b7c45..2417756ef 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ReleaseDashboardEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ReleaseDashboardEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.WebService.Services; namespace StellaOps.Orchestrator.WebService.Endpoints; @@ -19,7 +20,8 @@ public static class ReleaseDashboardEndpoints { var group = app.MapGroup(prefix) .WithTags("ReleaseDashboard") - .RequireAuthorization(OrchestratorPolicies.ReleaseRead); + .RequireAuthorization(OrchestratorPolicies.ReleaseRead) + .RequireTenant(); var dashboard = group.MapGet("/dashboard", GetDashboard) .WithDescription("Return a consolidated release dashboard snapshot for the Console control plane, including pending approvals, active promotions, recent deployments, and environment health indicators. Used by the UI to populate the main release management view."); diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ReleaseEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ReleaseEndpoints.cs index 566cf14e7..305c3e988 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ReleaseEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/ReleaseEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.WebService.Services; namespace StellaOps.Orchestrator.WebService.Endpoints; @@ -27,7 +28,8 @@ public static class ReleaseEndpoints { var group = app.MapGroup(prefix) .WithTags("Releases") - .RequireAuthorization(OrchestratorPolicies.ReleaseRead); + .RequireAuthorization(OrchestratorPolicies.ReleaseRead) + .RequireTenant(); var list = group.MapGet(string.Empty, ListReleases) .WithDescription("Return a paginated list of releases for the calling tenant, optionally filtered by status, environment, project, and creation time window. Each release record includes its name, version, current status, component count, and lifecycle timestamps."); diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/RunEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/RunEndpoints.cs index 37f3911fe..6e8cf6924 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/RunEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/RunEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.Infrastructure.Repositories; using StellaOps.Orchestrator.WebService.Contracts; using StellaOps.Orchestrator.WebService.Services; @@ -18,7 +19,8 @@ public static class RunEndpoints { var group = app.MapGroup("/api/v1/orchestrator/runs") .WithTags("Orchestrator Runs") - .RequireAuthorization(OrchestratorPolicies.Read); + .RequireAuthorization(OrchestratorPolicies.Read) + .RequireTenant(); group.MapGet(string.Empty, ListRuns) .WithName("Orchestrator_ListRuns") diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/SloEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/SloEndpoints.cs index 9d4157a74..bc35b2901 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/SloEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/SloEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.Core.Domain; using StellaOps.Orchestrator.Core.SloManagement; using StellaOps.Orchestrator.WebService.Contracts; @@ -18,7 +19,8 @@ public static class SloEndpoints { var group = app.MapGroup("/api/v1/orchestrator/slos") .WithTags("Orchestrator SLOs") - .RequireAuthorization(OrchestratorPolicies.Read); + .RequireAuthorization(OrchestratorPolicies.Read) + .RequireTenant(); // SLO CRUD operations group.MapGet(string.Empty, ListSlos) diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/SourceEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/SourceEndpoints.cs index cb397572e..112f0aed7 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/SourceEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/SourceEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.Infrastructure.Repositories; using StellaOps.Orchestrator.WebService.Contracts; using StellaOps.Orchestrator.WebService.Services; @@ -17,7 +18,8 @@ public static class SourceEndpoints { var group = app.MapGroup("/api/v1/orchestrator/sources") .WithTags("Orchestrator Sources") - .RequireAuthorization(OrchestratorPolicies.Read); + .RequireAuthorization(OrchestratorPolicies.Read) + .RequireTenant(); group.MapGet(string.Empty, ListSources) .WithName("Orchestrator_ListSources") diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/StreamEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/StreamEndpoints.cs index 43aa0dfdf..e985adb7a 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/StreamEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/StreamEndpoints.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.Infrastructure.Repositories; using StellaOps.Orchestrator.WebService.Services; using StellaOps.Orchestrator.WebService.Streaming; @@ -18,7 +19,8 @@ public static class StreamEndpoints { var group = app.MapGroup("/api/v1/orchestrator/stream") .WithTags("Orchestrator Streams") - .RequireAuthorization(OrchestratorPolicies.Read); + .RequireAuthorization(OrchestratorPolicies.Read) + .RequireTenant(); group.MapGet("jobs/{jobId:guid}", StreamJob) .WithName("Orchestrator_StreamJob") diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/WorkerEndpoints.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/WorkerEndpoints.cs index e9bd1f291..cd73f863f 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/WorkerEndpoints.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Endpoints/WorkerEndpoints.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Orchestrator.Core.Domain; using StellaOps.Orchestrator.Infrastructure; using StellaOps.Orchestrator.Infrastructure.Repositories; @@ -24,7 +25,8 @@ public static class WorkerEndpoints { var group = app.MapGroup("/api/v1/orchestrator/worker") .WithTags("Orchestrator Workers") - .RequireAuthorization(OrchestratorPolicies.Operate); + .RequireAuthorization(OrchestratorPolicies.Operate) + .RequireTenant(); group.MapPost("claim", ClaimJob) .WithName("Orchestrator_ClaimJob") diff --git a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Program.cs b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Program.cs index ff3833691..b4b119da8 100644 --- a/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Program.cs +++ b/src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService/Program.cs @@ -9,11 +9,13 @@ using StellaOps.Orchestrator.WebService.Endpoints; using StellaOps.Orchestrator.WebService.Services; using StellaOps.Orchestrator.WebService.Streaming; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Router.AspNet; using StellaOps.Telemetry.Core; var builder = WebApplication.CreateBuilder(args); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); builder.Services.AddRouting(options => options.LowercaseUrls = true); builder.Services.AddEndpointsApiExplorer(); @@ -139,6 +141,7 @@ if (app.Environment.IsDevelopment()) } app.UseStellaOpsCors(); +app.UseStellaOpsTenantMiddleware(); // Enable telemetry context propagation (extracts tenant/actor/correlation from headers) // Per ORCH-OBS-50-001 diff --git a/src/PacksRegistry/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/Program.cs b/src/PacksRegistry/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/Program.cs index faabbd161..4c16a4a0f 100644 --- a/src/PacksRegistry/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/Program.cs +++ b/src/PacksRegistry/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService/Program.cs @@ -9,6 +9,7 @@ using StellaOps.PacksRegistry.WebService; using StellaOps.PacksRegistry.WebService.Contracts; using StellaOps.PacksRegistry.WebService.Options; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Router.AspNet; using System.Text.Json.Serialization; @@ -57,6 +58,7 @@ builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddHealthChecks(); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); // Stella Router integration @@ -76,6 +78,7 @@ if (app.Environment.IsDevelopment()) } app.UseStellaOpsCors(); +app.UseStellaOpsTenantMiddleware(); app.MapHealthChecks("/healthz"); app.TryUseStellaRouter(routerEnabled); @@ -160,7 +163,8 @@ app.MapPost("/api/v1/packs", async (PackUploadRequest request, PackService servi .WithDescription("Uploads a new policy pack as base64-encoded content with optional signature and provenance attachment. Returns 201 Created with the registered pack record and assigned pack ID. Requires the X-StellaOps-Tenant header or a tenantId body field.") .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status400BadRequest) -.Produces(StatusCodes.Status401Unauthorized); +.Produces(StatusCodes.Status401Unauthorized) +.RequireTenant(); app.MapGet("/api/v1/packs", async (string? tenant, bool? includeDeprecated, PackService service, LifecycleService lifecycleService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -197,7 +201,8 @@ app.MapGet("/api/v1/packs", async (string? tenant, bool? includeDeprecated, Pack .WithName("ListPacks") .WithDescription("Returns the list of policy packs for the specified tenant, optionally excluding deprecated packs. When tenant allowlists are configured, a tenant query parameter or X-StellaOps-Tenant header is required.") .Produces>(StatusCodes.Status200OK) -.Produces(StatusCodes.Status401Unauthorized); +.Produces(StatusCodes.Status401Unauthorized) +.RequireTenant(); app.MapGet("/api/v1/packs/{packId}", async (string packId, PackService service, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -226,7 +231,8 @@ app.MapGet("/api/v1/packs/{packId}", async (string packId, PackService service, .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) -.Produces(StatusCodes.Status404NotFound); +.Produces(StatusCodes.Status404NotFound) +.RequireTenant(); app.MapGet("/api/v1/packs/{packId}/content", async (string packId, PackService service, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -262,7 +268,8 @@ app.MapGet("/api/v1/packs/{packId}/content", async (string packId, PackService s .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) -.Produces(StatusCodes.Status404NotFound); +.Produces(StatusCodes.Status404NotFound) +.RequireTenant(); app.MapGet("/api/v1/packs/{packId}/provenance", async (string packId, PackService service, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -302,7 +309,8 @@ app.MapGet("/api/v1/packs/{packId}/provenance", async (string packId, PackServic .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) -.Produces(StatusCodes.Status404NotFound); +.Produces(StatusCodes.Status404NotFound) +.RequireTenant(); app.MapGet("/api/v1/packs/{packId}/manifest", async (string packId, PackService service, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -344,7 +352,8 @@ app.MapGet("/api/v1/packs/{packId}/manifest", async (string packId, PackService .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) -.Produces(StatusCodes.Status404NotFound); +.Produces(StatusCodes.Status404NotFound) +.RequireTenant(); app.MapPost("/api/v1/packs/{packId}/signature", async (string packId, RotateSignatureRequest request, PackService service, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -391,7 +400,8 @@ app.MapPost("/api/v1/packs/{packId}/signature", async (string packId, RotateSign .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) -.Produces(StatusCodes.Status404NotFound); +.Produces(StatusCodes.Status404NotFound) +.RequireTenant(); app.MapPost("/api/v1/packs/{packId}/attestations", async (string packId, AttestationUploadRequest request, AttestationService attestationService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -433,7 +443,8 @@ app.MapPost("/api/v1/packs/{packId}/attestations", async (string packId, Attesta .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) -.Produces(StatusCodes.Status404NotFound); +.Produces(StatusCodes.Status404NotFound) +.RequireTenant(); app.MapGet("/api/v1/packs/{packId}/attestations", async (string packId, AttestationService attestationService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -461,7 +472,8 @@ app.MapGet("/api/v1/packs/{packId}/attestations", async (string packId, Attestat .Produces>(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) -.Produces(StatusCodes.Status404NotFound); +.Produces(StatusCodes.Status404NotFound) +.RequireTenant(); app.MapGet("/api/v1/packs/{packId}/attestations/{type}", async (string packId, string type, AttestationService attestationService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -496,7 +508,8 @@ app.MapGet("/api/v1/packs/{packId}/attestations/{type}", async (string packId, s .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) -.Produces(StatusCodes.Status404NotFound); +.Produces(StatusCodes.Status404NotFound) +.RequireTenant(); app.MapGet("/api/v1/packs/{packId}/parity", async (string packId, ParityService parityService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -525,7 +538,8 @@ app.MapGet("/api/v1/packs/{packId}/parity", async (string packId, ParityService .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) -.Produces(StatusCodes.Status404NotFound); +.Produces(StatusCodes.Status404NotFound) +.RequireTenant(); app.MapGet("/api/v1/packs/{packId}/lifecycle", async (string packId, LifecycleService lifecycleService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -554,7 +568,8 @@ app.MapGet("/api/v1/packs/{packId}/lifecycle", async (string packId, LifecycleSe .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) -.Produces(StatusCodes.Status404NotFound); +.Produces(StatusCodes.Status404NotFound) +.RequireTenant(); app.MapPost("/api/v1/packs/{packId}/lifecycle", async (string packId, LifecycleRequest request, LifecycleService lifecycleService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -596,7 +611,8 @@ app.MapPost("/api/v1/packs/{packId}/lifecycle", async (string packId, LifecycleR .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) -.Produces(StatusCodes.Status404NotFound); +.Produces(StatusCodes.Status404NotFound) +.RequireTenant(); app.MapPost("/api/v1/packs/{packId}/parity", async (string packId, ParityRequest request, ParityService parityService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -638,7 +654,8 @@ app.MapPost("/api/v1/packs/{packId}/parity", async (string packId, ParityRequest .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) -.Produces(StatusCodes.Status404NotFound); +.Produces(StatusCodes.Status404NotFound) +.RequireTenant(); app.MapPost("/api/v1/export/offline-seed", async (OfflineSeedRequest request, ExportService exportService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -667,7 +684,8 @@ app.MapPost("/api/v1/export/offline-seed", async (OfflineSeedRequest request, Ex .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized) -.Produces(StatusCodes.Status403Forbidden); +.Produces(StatusCodes.Status403Forbidden) +.RequireTenant(); app.MapPost("/api/v1/mirrors", async (MirrorRequest request, MirrorService mirrorService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -700,7 +718,8 @@ app.MapPost("/api/v1/mirrors", async (MirrorRequest request, MirrorService mirro .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized) -.Produces(StatusCodes.Status403Forbidden); +.Produces(StatusCodes.Status403Forbidden) +.RequireTenant(); app.MapGet("/api/v1/mirrors", async (string? tenant, MirrorService mirrorService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -723,7 +742,8 @@ app.MapGet("/api/v1/mirrors", async (string? tenant, MirrorService mirrorService .WithDescription("Returns all mirror registrations for the specified tenant, or all mirrors if no tenant filter is applied. Returns 403 if the caller's tenant allowlist excludes the requested tenant.") .Produces>(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) -.Produces(StatusCodes.Status403Forbidden); +.Produces(StatusCodes.Status403Forbidden) +.RequireTenant(); app.MapPost("/api/v1/mirrors/{id}/sync", async (string id, MirrorSyncRequest request, MirrorService mirrorService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -752,7 +772,8 @@ app.MapPost("/api/v1/mirrors/{id}/sync", async (string id, MirrorSyncRequest req .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status401Unauthorized) .Produces(StatusCodes.Status403Forbidden) -.Produces(StatusCodes.Status404NotFound); +.Produces(StatusCodes.Status404NotFound) +.RequireTenant(); app.MapGet("/api/v1/compliance/summary", async (string? tenant, ComplianceService complianceService, HttpContext context, AuthOptions auth, CancellationToken cancellationToken) => { @@ -775,7 +796,8 @@ app.MapGet("/api/v1/compliance/summary", async (string? tenant, ComplianceServic .WithDescription("Returns a compliance summary for the specified tenant's pack collection including signed pack count, unsigned count, packs with attestations, deprecated packs, and mirror sync status breakdown. Returns 403 if the tenant is not allowed.") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) -.Produces(StatusCodes.Status403Forbidden); +.Produces(StatusCodes.Status403Forbidden) +.RequireTenant(); // Refresh Router endpoint cache app.TryRefreshStellaRouterEndpoints(routerEnabled); diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/AdministrationTrustSigningMutationEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/AdministrationTrustSigningMutationEndpoints.cs index 9f4bd6c6a..36b4bd3a2 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/AdministrationTrustSigningMutationEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/AdministrationTrustSigningMutationEndpoints.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; @@ -20,7 +21,8 @@ public static class AdministrationTrustSigningMutationEndpoints { var group = app.MapGroup("/api/v1/administration/trust-signing") .WithTags("Administration") - .RequireAuthorization(PlatformPolicies.TrustRead); + .RequireAuthorization(PlatformPolicies.TrustRead) + .RequireTenant(); group.MapGet("/keys", async Task( HttpContext context, diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/AnalyticsEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/AnalyticsEndpoints.cs index 7aacd372e..67d14ee82 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/AnalyticsEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/AnalyticsEndpoints.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; @@ -18,7 +19,8 @@ public static class AnalyticsEndpoints { var analytics = app.MapGroup("/api/analytics") .WithTags("Analytics") - .RequireAuthorization(PlatformPolicies.AnalyticsRead); + .RequireAuthorization(PlatformPolicies.AnalyticsRead) + .RequireTenant(); analytics.MapGet("/suppliers", async Task ( HttpContext context, diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/ContextEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/ContextEndpoints.cs index bf9345ba0..f59f1633c 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/ContextEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/ContextEndpoints.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; @@ -16,7 +17,8 @@ public static class ContextEndpoints { var context = app.MapGroup("/api/v2/context") .WithTags("Platform Context") - .RequireAuthorization(PlatformPolicies.ContextRead); + .RequireAuthorization(PlatformPolicies.ContextRead) + .RequireTenant(); context.MapGet("/regions", async Task( HttpContext httpContext, diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/EnvironmentSettingsAdminEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/EnvironmentSettingsAdminEndpoints.cs index d7abd13a5..d8f9a7db3 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/EnvironmentSettingsAdminEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/EnvironmentSettingsAdminEndpoints.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Services; @@ -19,7 +20,8 @@ public static class EnvironmentSettingsAdminEndpoints { var group = app.MapGroup("/platform/envsettings/db") .WithTags("Environment Settings Admin") - .RequireAuthorization(PlatformPolicies.SetupRead); + .RequireAuthorization(PlatformPolicies.SetupRead) + .RequireTenant(); group.MapGet("/", async (IEnvironmentSettingsStore store, CancellationToken ct) => { diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/EnvironmentSettingsEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/EnvironmentSettingsEndpoints.cs index 156914bda..ec95a6c44 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/EnvironmentSettingsEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/EnvironmentSettingsEndpoints.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Options; using StellaOps.Platform.WebService.Services; diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/EvidenceThreadEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/EvidenceThreadEndpoints.cs index 6e7d3124a..73a8a20e5 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/EvidenceThreadEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/EvidenceThreadEndpoints.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Services; using StellaOps.ReleaseOrchestrator.EvidenceThread.Export; @@ -33,7 +34,8 @@ public static class EvidenceThreadEndpoints { var evidence = app.MapGroup("/api/v1/evidence") .WithTags("Evidence Thread") - .RequireAuthorization(PlatformPolicies.ContextRead); + .RequireAuthorization(PlatformPolicies.ContextRead) + .RequireTenant(); // GET /api/v1/evidence/{artifactDigest} - Get evidence thread for artifact evidence.MapGet("/{artifactDigest}", GetEvidenceThread) diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/FederationTelemetryEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/FederationTelemetryEndpoints.cs index 6034d0944..c411440e6 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/FederationTelemetryEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/FederationTelemetryEndpoints.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; @@ -21,7 +22,8 @@ public static class FederationTelemetryEndpoints { var group = app.MapGroup("/api/v1/telemetry/federation") .WithTags("Federated Telemetry") - .RequireAuthorization(PlatformPolicies.FederationRead); + .RequireAuthorization(PlatformPolicies.FederationRead) + .RequireTenant(); // GET /consent — get consent state group.MapGet("/consent", async Task( diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/FunctionMapEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/FunctionMapEndpoints.cs index f77a6477f..b9fd26924 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/FunctionMapEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/FunctionMapEndpoints.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; @@ -25,7 +26,8 @@ public static class FunctionMapEndpoints { var maps = app.MapGroup("/api/v1/function-maps") .WithTags("Function Maps") - .RequireAuthorization(PlatformPolicies.FunctionMapRead); + .RequireAuthorization(PlatformPolicies.FunctionMapRead) + .RequireTenant(); MapCrudEndpoints(maps); MapVerifyEndpoints(maps); diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/IntegrationReadModelEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/IntegrationReadModelEndpoints.cs index 063c6592c..024664b1b 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/IntegrationReadModelEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/IntegrationReadModelEndpoints.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; @@ -16,7 +17,8 @@ public static class IntegrationReadModelEndpoints { var integrations = app.MapGroup("/api/v2/integrations") .WithTags("Integrations V2") - .RequireAuthorization(PlatformPolicies.IntegrationsRead); + .RequireAuthorization(PlatformPolicies.IntegrationsRead) + .RequireTenant(); integrations.MapGet("/feeds", async Task( HttpContext context, diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/LegacyAliasEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/LegacyAliasEndpoints.cs index ba5ca501c..f8570f204 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/LegacyAliasEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/LegacyAliasEndpoints.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; @@ -17,7 +18,8 @@ public static class LegacyAliasEndpoints { var legacy = app.MapGroup("/api/v1") .WithTags("Pack22 Legacy Aliases") - .RequireAuthorization(PlatformPolicies.ContextRead); + .RequireAuthorization(PlatformPolicies.ContextRead) + .RequireTenant(); legacy.MapGet("/context/regions", async Task( HttpContext context, diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/PackAdapterEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/PackAdapterEndpoints.cs index 6a822099b..379474fae 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/PackAdapterEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/PackAdapterEndpoints.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; @@ -44,7 +45,8 @@ public static class PackAdapterEndpoints var platform = app.MapGroup("/api/v1/platform") .WithTags("Platform Ops") - .RequireAuthorization(PlatformPolicies.HealthRead); + .RequireAuthorization(PlatformPolicies.HealthRead) + .RequireTenant(); platform.MapGet("/data-integrity/summary", ( HttpContext context, @@ -158,7 +160,8 @@ public static class PackAdapterEndpoints .RequireAuthorization(PlatformPolicies.HealthRead); var administration = app.MapGroup("/api/v1/administration") - .WithTags("Administration"); + .WithTags("Administration") + .RequireTenant(); administration.MapGet("/summary", ( HttpContext context, diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/PlatformEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/PlatformEndpoints.cs index 10632385f..fdcd5c79c 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/PlatformEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/PlatformEndpoints.cs @@ -8,6 +8,7 @@ using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; using System; using System.Linq; +using StellaOps.Auth.ServerIntegration.Tenancy; using System.Threading; using System.Threading.Tasks; @@ -19,7 +20,8 @@ public static class PlatformEndpoints { var platform = app.MapGroup("/api/v1/platform") .WithTags("Platform") - .RequireAuthorization(PlatformPolicies.HealthRead); + .RequireAuthorization(PlatformPolicies.HealthRead) + .RequireTenant(); MapHealthEndpoints(platform); MapQuotaEndpoints(platform); @@ -478,7 +480,8 @@ public static class PlatformEndpoints { var quotas = app.MapGroup("/api/v1/authority/quotas") .WithTags("Platform Quotas Compatibility") - .RequireAuthorization(PlatformPolicies.QuotaRead); + .RequireAuthorization(PlatformPolicies.QuotaRead) + .RequireTenant(); quotas.MapGet(string.Empty, async Task ( HttpContext context, @@ -715,7 +718,8 @@ public static class PlatformEndpoints var rateLimits = app.MapGroup("/api/v1/gateway/rate-limits") .WithTags("Platform Gateway Compatibility") - .RequireAuthorization(PlatformPolicies.QuotaRead); + .RequireAuthorization(PlatformPolicies.QuotaRead) + .RequireTenant(); rateLimits.MapGet(string.Empty, (HttpContext context, PlatformRequestContextResolver resolver) => { diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/PolicyInteropEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/PolicyInteropEndpoints.cs index d00c1360a..b5974c929 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/PolicyInteropEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/PolicyInteropEndpoints.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; @@ -26,7 +27,8 @@ public static class PolicyInteropEndpoints { var interop = app.MapGroup("/api/v1/policy/interop") .WithTags("PolicyInterop") - .RequireAuthorization(PlatformPolicies.PolicyRead); + .RequireAuthorization(PlatformPolicies.PolicyRead) + .RequireTenant(); MapExportEndpoint(interop); MapImportEndpoint(interop); diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/ReleaseControlEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/ReleaseControlEndpoints.cs index 5bf5219ba..a39d76153 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/ReleaseControlEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/ReleaseControlEndpoints.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; @@ -20,7 +21,8 @@ public static class ReleaseControlEndpoints { var bundles = app.MapGroup("/api/v1/release-control/bundles") .WithTags("Release Control") - .RequireAuthorization(PlatformPolicies.ReleaseControlRead); + .RequireAuthorization(PlatformPolicies.ReleaseControlRead) + .RequireTenant(); bundles.MapGet(string.Empty, async Task( HttpContext context, diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/ReleaseReadModelEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/ReleaseReadModelEndpoints.cs index 0e1cf018b..2fc425aff 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/ReleaseReadModelEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/ReleaseReadModelEndpoints.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; @@ -17,7 +18,8 @@ public static class ReleaseReadModelEndpoints { var releases = app.MapGroup("/api/v2/releases") .WithTags("Releases V2") - .RequireAuthorization(PlatformPolicies.ReleaseControlRead); + .RequireAuthorization(PlatformPolicies.ReleaseControlRead) + .RequireTenant(); releases.MapGet(string.Empty, async Task( HttpContext context, diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/ScoreEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/ScoreEndpoints.cs index d1b2c40fd..7a1d9658c 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/ScoreEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/ScoreEndpoints.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; @@ -26,7 +27,8 @@ public static class ScoreEndpoints { var score = app.MapGroup("/api/v1/score") .WithTags("Score") - .RequireAuthorization(PlatformPolicies.ScoreRead); + .RequireAuthorization(PlatformPolicies.ScoreRead) + .RequireTenant(); MapEvaluateEndpoints(score); MapHistoryEndpoints(score); diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/SecurityReadModelEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/SecurityReadModelEndpoints.cs index e5eb25fb6..34c7dc061 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/SecurityReadModelEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/SecurityReadModelEndpoints.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; @@ -16,7 +17,8 @@ public static class SecurityReadModelEndpoints { var security = app.MapGroup("/api/v2/security") .WithTags("Security V2") - .RequireAuthorization(PlatformPolicies.SecurityRead); + .RequireAuthorization(PlatformPolicies.SecurityRead) + .RequireTenant(); security.MapGet("/findings", async Task( HttpContext context, diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/TopologyReadModelEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/TopologyReadModelEndpoints.cs index 4bc1f6330..0df589cd0 100644 --- a/src/Platform/StellaOps.Platform.WebService/Endpoints/TopologyReadModelEndpoints.cs +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/TopologyReadModelEndpoints.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Platform.WebService.Constants; using StellaOps.Platform.WebService.Contracts; using StellaOps.Platform.WebService.Services; @@ -16,7 +17,8 @@ public static class TopologyReadModelEndpoints { var topology = app.MapGroup("/api/v2/topology") .WithTags("Topology V2") - .RequireAuthorization(PlatformPolicies.TopologyRead); + .RequireAuthorization(PlatformPolicies.TopologyRead) + .RequireTenant(); topology.MapGet("/regions", async Task( HttpContext context, diff --git a/src/Platform/StellaOps.Platform.WebService/Program.cs b/src/Platform/StellaOps.Platform.WebService/Program.cs index 00c90c568..ed04c2b8f 100644 --- a/src/Platform/StellaOps.Platform.WebService/Program.cs +++ b/src/Platform/StellaOps.Platform.WebService/Program.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Configuration; using StellaOps.Messaging.DependencyInjection; using StellaOps.Platform.Analytics; @@ -46,6 +47,7 @@ builder.Services.AddOptions() // Layer 1: env var post-configure (STELLAOPS_*_URL -> ApiBaseUrls, lowest priority after YAML) builder.Services.AddSingleton, StellaOpsEnvVarPostConfigure>(); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); builder.Services.AddRouting(options => options.LowercaseUrls = true); builder.Services.AddEndpointsApiExplorer(); @@ -255,6 +257,7 @@ app.UseStellaOpsCors(); app.UseStellaOpsTelemetryContext(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); var legacyAliasTelemetry = app.Services.GetRequiredService(); diff --git a/src/Policy/StellaOps.Policy.Engine/Endpoints/RiskProfileAirGapEndpoints.cs b/src/Policy/StellaOps.Policy.Engine/Endpoints/RiskProfileAirGapEndpoints.cs index a34acabe1..6df834e3e 100644 --- a/src/Policy/StellaOps.Policy.Engine/Endpoints/RiskProfileAirGapEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Engine/Endpoints/RiskProfileAirGapEndpoints.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; +using StellaOps.Auth.ServerIntegration; using StellaOps.Policy.Engine.AirGap; using StellaOps.Policy.Engine.Services; using StellaOps.Policy.RiskProfile.Models; diff --git a/src/Policy/StellaOps.Policy.Gateway/Endpoints/AdvisorySourceEndpoints.cs b/src/Policy/StellaOps.Policy.Gateway/Endpoints/AdvisorySourceEndpoints.cs index 555df5f84..5127c2874 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Endpoints/AdvisorySourceEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Endpoints/AdvisorySourceEndpoints.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Policy.Persistence.Postgres.Repositories; using System.Text.Json; @@ -21,7 +22,8 @@ public static class AdvisorySourceEndpoints public static void MapAdvisorySourcePolicyEndpoints(this WebApplication app) { var group = app.MapGroup("/api/v1/advisory-sources") - .WithTags("Advisory Sources"); + .WithTags("Advisory Sources") + .RequireTenant(); group.MapGet("/{sourceId}/impact", GetImpactAsync) .WithName("GetAdvisorySourceImpact") diff --git a/src/Policy/StellaOps.Policy.Gateway/Endpoints/DeltasEndpoints.cs b/src/Policy/StellaOps.Policy.Gateway/Endpoints/DeltasEndpoints.cs index 4c332b796..5ccb501df 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Endpoints/DeltasEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Endpoints/DeltasEndpoints.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Policy.Deltas; using StellaOps.Policy.Gateway.Contracts; @@ -25,7 +26,8 @@ public static class DeltasEndpoints public static void MapDeltasEndpoints(this WebApplication app) { var deltas = app.MapGroup("/api/policy/deltas") - .WithTags("Deltas"); + .WithTags("Deltas") + .RequireTenant(); // POST /api/policy/deltas/compute - Compute a security state delta deltas.MapPost("/compute", async Task( diff --git a/src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionApprovalEndpoints.cs b/src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionApprovalEndpoints.cs index 5829bcda0..e177b8254 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionApprovalEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionApprovalEndpoints.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Policy.Engine.Services; using StellaOps.Policy.Persistence.Postgres.Models; using StellaOps.Policy.Persistence.Postgres.Repositories; @@ -24,7 +25,8 @@ public static class ExceptionApprovalEndpoints public static void MapExceptionApprovalEndpoints(this WebApplication app) { var exceptions = app.MapGroup("/api/v1/policy/exception") - .WithTags("Exception Approvals"); + .WithTags("Exception Approvals") + .RequireTenant(); // POST /api/v1/policy/exception/request - Create a new exception approval request exceptions.MapPost("/request", CreateApprovalRequestAsync) diff --git a/src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionEndpoints.cs b/src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionEndpoints.cs index fcac21931..a2b26bedf 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Endpoints/ExceptionEndpoints.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Policy.Exceptions.Models; using StellaOps.Policy.Exceptions.Repositories; using StellaOps.Policy.Gateway.Contracts; @@ -26,7 +27,8 @@ public static class ExceptionEndpoints public static void MapExceptionEndpoints(this WebApplication app) { var exceptions = app.MapGroup("/api/policy/exceptions") - .WithTags("Exceptions"); + .WithTags("Exceptions") + .RequireTenant(); // GET /api/policy/exceptions - List exceptions with filters exceptions.MapGet(string.Empty, async Task( diff --git a/src/Policy/StellaOps.Policy.Gateway/Endpoints/GateEndpoints.cs b/src/Policy/StellaOps.Policy.Gateway/Endpoints/GateEndpoints.cs index 579177d30..155d840ff 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Endpoints/GateEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Endpoints/GateEndpoints.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Policy.Audit; using StellaOps.Policy.Deltas; using StellaOps.Policy.Engine.Gates; @@ -28,7 +29,8 @@ public static class GateEndpoints public static void MapGateEndpoints(this WebApplication app) { var gates = app.MapGroup("/api/v1/policy/gate") - .WithTags("Gates"); + .WithTags("Gates") + .RequireTenant(); // POST /api/v1/policy/gate/evaluate - Evaluate gate for image gates.MapPost("/evaluate", async Task( diff --git a/src/Policy/StellaOps.Policy.Gateway/Endpoints/GatesEndpoints.cs b/src/Policy/StellaOps.Policy.Gateway/Endpoints/GatesEndpoints.cs index 49c68e2e5..a713fde2b 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Endpoints/GatesEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Endpoints/GatesEndpoints.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Caching.Memory; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Policy.Gates; using StellaOps.Policy.Persistence.Postgres.Repositories; using System.Text.Json.Serialization; @@ -33,7 +34,8 @@ public static class GatesEndpoints public static IEndpointRouteBuilder MapGatesEndpoints(this IEndpointRouteBuilder endpoints) { var group = endpoints.MapGroup("/api/v1/gates") - .WithTags("Gates"); + .WithTags("Gates") + .RequireTenant(); group.MapGet("/{bomRef}", GetGateStatus) .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead)) diff --git a/src/Policy/StellaOps.Policy.Gateway/Endpoints/GovernanceEndpoints.cs b/src/Policy/StellaOps.Policy.Gateway/Endpoints/GovernanceEndpoints.cs index 7fe2d0196..796592005 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Endpoints/GovernanceEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Endpoints/GovernanceEndpoints.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using System.Collections.Concurrent; using System.Globalization; using System.Text.Json; @@ -29,7 +30,8 @@ public static class GovernanceEndpoints public static void MapGovernanceEndpoints(this WebApplication app) { var governance = app.MapGroup("/api/v1/governance") - .WithTags("Governance"); + .WithTags("Governance") + .RequireTenant(); // Sealed Mode endpoints governance.MapGet("/sealed-mode/status", GetSealedModeStatusAsync) diff --git a/src/Policy/StellaOps.Policy.Gateway/Endpoints/RegistryWebhookEndpoints.cs b/src/Policy/StellaOps.Policy.Gateway/Endpoints/RegistryWebhookEndpoints.cs index b1cb4b96b..fc5a25fee 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Endpoints/RegistryWebhookEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Endpoints/RegistryWebhookEndpoints.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Policy.Engine.Gates; using System.Text.Json; using System.Text.Json.Serialization; @@ -23,7 +24,8 @@ internal static class RegistryWebhookEndpoints { var group = endpoints.MapGroup("/api/v1/webhooks/registry") .WithTags("Registry Webhooks") - .AllowAnonymous(); + .AllowAnonymous() + .RequireTenant(); group.MapPost("/docker", HandleDockerRegistryWebhook) .WithName("DockerRegistryWebhook") diff --git a/src/Policy/StellaOps.Policy.Gateway/Endpoints/ScoreGateEndpoints.cs b/src/Policy/StellaOps.Policy.Gateway/Endpoints/ScoreGateEndpoints.cs index 9f75eee96..bdcb9298f 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Endpoints/ScoreGateEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Endpoints/ScoreGateEndpoints.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.DeltaVerdict.Bundles; using StellaOps.Policy.Gateway.Contracts; using StellaOps.Signals.EvidenceWeightedScore; @@ -26,7 +27,8 @@ public static class ScoreGateEndpoints public static void MapScoreGateEndpoints(this WebApplication app) { var gates = app.MapGroup("/api/v1/gate") - .WithTags("Score Gates"); + .WithTags("Score Gates") + .RequireTenant(); // POST /api/v1/gate/evaluate - Evaluate score-based gate for a finding gates.MapPost("/evaluate", async Task( diff --git a/src/Policy/StellaOps.Policy.Gateway/Endpoints/ToolLatticeEndpoints.cs b/src/Policy/StellaOps.Policy.Gateway/Endpoints/ToolLatticeEndpoints.cs index 318d3b634..095f5a0d1 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Endpoints/ToolLatticeEndpoints.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Endpoints/ToolLatticeEndpoints.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Policy.Gateway.Contracts; using StellaOps.Policy.ToolLattice; using System; @@ -16,7 +17,8 @@ public static class ToolLatticeEndpoints public static void MapToolLatticeEndpoints(this WebApplication app) { var tools = app.MapGroup("/api/v1/policy/assistant/tools") - .WithTags("Assistant Tools"); + .WithTags("Assistant Tools") + .RequireTenant(); tools.MapPost("/evaluate", (HttpContext httpContext, ToolAccessRequest request, IToolAccessEvaluator evaluator) => { diff --git a/src/Policy/StellaOps.Policy.Gateway/Program.cs b/src/Policy/StellaOps.Policy.Gateway/Program.cs index 535c55298..1e50efb96 100644 --- a/src/Policy/StellaOps.Policy.Gateway/Program.cs +++ b/src/Policy/StellaOps.Policy.Gateway/Program.cs @@ -12,6 +12,7 @@ using StellaOps.AirGap.Policy; using StellaOps.Auth.Abstractions; using StellaOps.Auth.Client; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Configuration; using StellaOps.Determinism; using StellaOps.Policy.Deltas; @@ -128,6 +129,7 @@ builder.Services.AddOptions() builder.Services.AddSingleton(sp => sp.GetRequiredService>().Value); builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddSystemGuidProvider(); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); builder.Services.AddRouting(options => options.LowercaseUrls = true); builder.Services.AddProblemDetails(); @@ -323,6 +325,7 @@ app.UseStatusCodePages(); app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); app.MapHealthChecks("/healthz"); diff --git a/src/ReachGraph/StellaOps.ReachGraph.WebService/Program.cs b/src/ReachGraph/StellaOps.ReachGraph.WebService/Program.cs index 4af572bcd..346a3ec44 100644 --- a/src/ReachGraph/StellaOps.ReachGraph.WebService/Program.cs +++ b/src/ReachGraph/StellaOps.ReachGraph.WebService/Program.cs @@ -1,6 +1,7 @@ // Licensed to StellaOps under the BUSL-1.1 license. using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using Microsoft.AspNetCore.RateLimiting; using Npgsql; using StackExchange.Redis; @@ -108,6 +109,7 @@ builder.Services.AddResponseCompression(options => options.EnableForHttps = true; }); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); // Stella Router integration @@ -131,6 +133,7 @@ app.UseResponseCompression(); app.UseStellaOpsCors(); app.UseRateLimiter(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); app.MapControllers(); diff --git a/src/Registry/StellaOps.Registry.TokenService/Program.cs b/src/Registry/StellaOps.Registry.TokenService/Program.cs index 264e8d987..69e3f50a9 100644 --- a/src/Registry/StellaOps.Registry.TokenService/Program.cs +++ b/src/Registry/StellaOps.Registry.TokenService/Program.cs @@ -13,6 +13,7 @@ using Serilog.Events; using StellaOps.AirGap.Policy; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Configuration; using StellaOps.Registry.TokenService; using StellaOps.Registry.TokenService.Admin; @@ -98,6 +99,7 @@ builder.Services.AddStellaOpsResourceServerAuthentication( } }); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); builder.Services.AddAuthorization(options => { @@ -135,6 +137,7 @@ app.UseSerilogRequestLogging(); app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); app.MapHealthChecks("/healthz"); @@ -207,7 +210,8 @@ app.MapGet("/token", ( .RequireAuthorization("registry.token.issue") .Produces(StatusCodes.Status200OK) .ProducesProblem(StatusCodes.Status400BadRequest) -.ProducesProblem(StatusCodes.Status403Forbidden); +.ProducesProblem(StatusCodes.Status403Forbidden) +.RequireTenant(); app.TryRefreshStellaRouterEndpoints(routerEnabled); app.Run(); diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.PolicyGate/Endpoints/PolicyGateEndpoints.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.PolicyGate/Endpoints/PolicyGateEndpoints.cs index 6a41b5b62..09504fb39 100644 --- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.PolicyGate/Endpoints/PolicyGateEndpoints.cs +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.PolicyGate/Endpoints/PolicyGateEndpoints.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using StellaOps.ReleaseOrchestrator.PolicyGate.Models; using StellaOps.ReleaseOrchestrator.PolicyGate.Services; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.ReleaseOrchestrator.PolicyGate.Endpoints; @@ -22,7 +23,8 @@ public static class PolicyGateEndpoints public static RouteGroupBuilder MapPolicyGateEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/api/v1/policy-gates") - .WithTags("Policy Gates"); + .WithTags("Policy Gates") + .RequireTenant(); // Profile endpoints group.MapGet("profiles", ListProfiles) diff --git a/src/Remediation/StellaOps.Remediation.WebService/Endpoints/RemediationMatchEndpoints.cs b/src/Remediation/StellaOps.Remediation.WebService/Endpoints/RemediationMatchEndpoints.cs index 29b7f9d4a..dc76f805e 100644 --- a/src/Remediation/StellaOps.Remediation.WebService/Endpoints/RemediationMatchEndpoints.cs +++ b/src/Remediation/StellaOps.Remediation.WebService/Endpoints/RemediationMatchEndpoints.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using StellaOps.Remediation.Core.Abstractions; using StellaOps.Remediation.WebService.Contracts; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.Remediation.WebService.Endpoints; @@ -12,7 +13,8 @@ public static class RemediationMatchEndpoints public static IEndpointRouteBuilder MapRemediationMatchEndpoints(this IEndpointRouteBuilder app) { var match = app.MapGroup("/api/v1/remediation/match") - .WithTags("Remediation"); + .WithTags("Remediation") + .RequireTenant(); match.MapGet(string.Empty, async Task( IRemediationMatcher matcher, diff --git a/src/Remediation/StellaOps.Remediation.WebService/Endpoints/RemediationRegistryEndpoints.cs b/src/Remediation/StellaOps.Remediation.WebService/Endpoints/RemediationRegistryEndpoints.cs index e0939c786..6702e0dba 100644 --- a/src/Remediation/StellaOps.Remediation.WebService/Endpoints/RemediationRegistryEndpoints.cs +++ b/src/Remediation/StellaOps.Remediation.WebService/Endpoints/RemediationRegistryEndpoints.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Routing; using StellaOps.Remediation.Core.Abstractions; using StellaOps.Remediation.Core.Models; using StellaOps.Remediation.WebService.Contracts; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.Remediation.WebService.Endpoints; @@ -16,7 +17,8 @@ public static class RemediationRegistryEndpoints public static IEndpointRouteBuilder MapRemediationRegistryEndpoints(this IEndpointRouteBuilder app) { var templates = app.MapGroup("/api/v1/remediation/templates") - .WithTags("Remediation"); + .WithTags("Remediation") + .RequireTenant(); templates.MapGet(string.Empty, async Task( IRemediationRegistry registry, @@ -69,7 +71,8 @@ public static class RemediationRegistryEndpoints .RequireAuthorization("remediation.submit"); var submissions = app.MapGroup("/api/v1/remediation/submissions") - .WithTags("Remediation"); + .WithTags("Remediation") + .RequireTenant(); submissions.MapGet(string.Empty, async Task( IRemediationRegistry registry, @@ -136,7 +139,8 @@ public static class RemediationRegistryEndpoints .RequireAuthorization("remediation.read"); var contributors = app.MapGroup("/api/v1/remediation/contributors") - .WithTags("Remediation"); + .WithTags("Remediation") + .RequireTenant(); contributors.MapGet(string.Empty, () => { diff --git a/src/Remediation/StellaOps.Remediation.WebService/Endpoints/RemediationSourceEndpoints.cs b/src/Remediation/StellaOps.Remediation.WebService/Endpoints/RemediationSourceEndpoints.cs index 4939201e4..8a17623ee 100644 --- a/src/Remediation/StellaOps.Remediation.WebService/Endpoints/RemediationSourceEndpoints.cs +++ b/src/Remediation/StellaOps.Remediation.WebService/Endpoints/RemediationSourceEndpoints.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.Remediation.WebService.Endpoints; @@ -9,7 +10,8 @@ public static class RemediationSourceEndpoints public static IEndpointRouteBuilder MapRemediationSourceEndpoints(this IEndpointRouteBuilder app) { var sources = app.MapGroup("/api/v1/remediation/sources") - .WithTags("Remediation"); + .WithTags("Remediation") + .RequireTenant(); sources.MapGet(string.Empty, () => { diff --git a/src/Remediation/StellaOps.Remediation.WebService/Program.cs b/src/Remediation/StellaOps.Remediation.WebService/Program.cs index da0fcee3a..1588eb943 100644 --- a/src/Remediation/StellaOps.Remediation.WebService/Program.cs +++ b/src/Remediation/StellaOps.Remediation.WebService/Program.cs @@ -1,6 +1,7 @@ using StellaOps.Remediation.Core.Abstractions; using StellaOps.Remediation.Core.Services; using StellaOps.Remediation.Persistence.Repositories; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Remediation.WebService.Endpoints; var builder = WebApplication.CreateBuilder(args); @@ -15,6 +16,7 @@ builder.Services.AddAuthorization(options => options.AddPolicy("remediation.manage", policy => policy.RequireAssertion(_ => true)); }); builder.Services.AddAuthentication(); +builder.Services.AddStellaOpsTenantServices(); // Core services builder.Services.AddSingleton(); @@ -38,6 +40,7 @@ var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.MapHealthChecks("/healthz").AllowAnonymous(); diff --git a/src/Remediation/StellaOps.Remediation.WebService/StellaOps.Remediation.WebService.csproj b/src/Remediation/StellaOps.Remediation.WebService/StellaOps.Remediation.WebService.csproj index 2b7b011ea..f5e89e2e0 100644 --- a/src/Remediation/StellaOps.Remediation.WebService/StellaOps.Remediation.WebService.csproj +++ b/src/Remediation/StellaOps.Remediation.WebService/StellaOps.Remediation.WebService.csproj @@ -7,5 +7,6 @@ + diff --git a/src/Replay/StellaOps.Replay.WebService/Program.cs b/src/Replay/StellaOps.Replay.WebService/Program.cs index 5e8e98942..e58bba749 100644 --- a/src/Replay/StellaOps.Replay.WebService/Program.cs +++ b/src/Replay/StellaOps.Replay.WebService/Program.cs @@ -12,6 +12,7 @@ using Serilog.Events; using StellaOps.Audit.ReplayToken; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Router.AspNet; using StellaOps.AuditPack.Services; using StellaOps.Configuration; @@ -110,6 +111,7 @@ builder.Services.AddAuthorization(options => }); }); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); // Stella Router integration @@ -144,6 +146,7 @@ app.UseExceptionHandler(exceptionApp => app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); app.MapHealthChecks("/healthz"); @@ -199,7 +202,8 @@ app.MapPost("/v1/replay/tokens", Task, Pr .Produces(StatusCodes.Status201Created) .ProducesProblem(StatusCodes.Status400BadRequest) .ProducesProblem(StatusCodes.Status401Unauthorized) -.ProducesProblem(StatusCodes.Status403Forbidden); +.ProducesProblem(StatusCodes.Status403Forbidden) +.RequireTenant(); // POST /v1/replay/tokens/verify - Verify a replay token app.MapPost("/v1/replay/tokens/verify", Task, ProblemHttpResult>> ( @@ -263,7 +267,8 @@ app.MapPost("/v1/replay/tokens/verify", Task, Pr .Produces(StatusCodes.Status200OK) .ProducesProblem(StatusCodes.Status400BadRequest) .ProducesProblem(StatusCodes.Status401Unauthorized) -.ProducesProblem(StatusCodes.Status403Forbidden); +.ProducesProblem(StatusCodes.Status403Forbidden) +.RequireTenant(); // GET /v1/replay/tokens/{tokenValue} - Get token details (parse only) app.MapGet("/v1/replay/tokens/{tokenCanonical}", Task, NotFound, ProblemHttpResult>> ( @@ -297,7 +302,8 @@ app.MapGet("/v1/replay/tokens/{tokenCanonical}", Task diff --git a/src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.WebService/Endpoints/ExploitMaturityEndpoints.cs b/src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.WebService/Endpoints/ExploitMaturityEndpoints.cs index d9eaa7c10..20ebf1068 100644 --- a/src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.WebService/Endpoints/ExploitMaturityEndpoints.cs +++ b/src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.WebService/Endpoints/ExploitMaturityEndpoints.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Routing; using StellaOps.RiskEngine.Core.Contracts; using StellaOps.RiskEngine.Core.Providers; using StellaOps.RiskEngine.WebService.Security; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.RiskEngine.WebService.Endpoints; @@ -20,7 +21,8 @@ public static class ExploitMaturityEndpoints { var group = app.MapGroup("/exploit-maturity") .WithTags("ExploitMaturity") - .RequireAuthorization(RiskEnginePolicies.Read); + .RequireAuthorization(RiskEnginePolicies.Read) + .RequireTenant(); // GET /exploit-maturity/{cveId} - Assess exploit maturity for a CVE group.MapGet("/{cveId}", async ( diff --git a/src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.WebService/Program.cs b/src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.WebService/Program.cs index c7ec0f2a3..c452e2dd5 100644 --- a/src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.WebService/Program.cs +++ b/src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.WebService/Program.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.RiskEngine.Core.Contracts; using StellaOps.RiskEngine.Core.Providers; using StellaOps.RiskEngine.Core.Services; @@ -35,6 +36,7 @@ builder.Services.AddSingleton() // Authentication and authorization builder.Services.AddStellaOpsResourceServerAuthentication(builder.Configuration); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddAuthorization(options => { options.AddStellaOpsScopePolicy(RiskEnginePolicies.Read, StellaOpsScopes.RiskEngineRead); @@ -62,6 +64,7 @@ if (app.Environment.IsDevelopment()) app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); // Map exploit maturity endpoints @@ -71,7 +74,8 @@ app.MapGet("/risk-scores/providers", (IRiskScoreProviderRegistry registry) => Results.Ok(new { providers = registry.ProviderNames.OrderBy(n => n, StringComparer.OrdinalIgnoreCase) })) .WithName("ListRiskScoreProviders") .WithDescription("Returns the sorted list of registered risk score provider names. Use this to discover which scoring strategies are available before submitting job or simulation requests.") - .RequireAuthorization(RiskEnginePolicies.Read); + .RequireAuthorization(RiskEnginePolicies.Read) + .RequireTenant(); app.MapPost("/risk-scores/jobs", async ( ScoreRequest request, @@ -92,7 +96,8 @@ app.MapPost("/risk-scores/jobs", async ( }) .WithName("CreateRiskScoreJob") .WithDescription("Enqueues a risk scoring job for the specified subject and provider, immediately executes it synchronously, and returns a 202 Accepted response with the job ID and computed result. The provider must be registered or the job will fail with an error in the result payload.") -.RequireAuthorization(RiskEnginePolicies.Operate); +.RequireAuthorization(RiskEnginePolicies.Operate) +.RequireTenant(); app.MapGet("/risk-scores/jobs/{jobId:guid}", ( Guid jobId, @@ -102,7 +107,8 @@ app.MapGet("/risk-scores/jobs/{jobId:guid}", ( : Results.NotFound()) .WithName("GetRiskScoreJob") .WithDescription("Returns the stored risk score result for the specified job ID. Returns 404 if the job ID is not found in the result store, which may occur if the store has been cleared or the ID is invalid.") - .RequireAuthorization(RiskEnginePolicies.Read); + .RequireAuthorization(RiskEnginePolicies.Read) + .RequireTenant(); app.MapPost("/risk-scores/simulations", async ( IReadOnlyCollection requests, @@ -114,7 +120,8 @@ app.MapPost("/risk-scores/simulations", async ( }) .WithName("RunRiskScoreSimulation") .WithDescription("Evaluates a collection of risk score requests against the registered providers and returns the full result list. Unlike the job endpoint, simulations do not persist results. Requests for unregistered providers are returned with a failure flag and error message.") -.RequireAuthorization(RiskEnginePolicies.Operate); +.RequireAuthorization(RiskEnginePolicies.Operate) +.RequireTenant(); app.MapPost("/risk-scores/simulations/summary", async ( IReadOnlyCollection requests, @@ -140,7 +147,8 @@ app.MapPost("/risk-scores/simulations/summary", async ( }) .WithName("GetRiskScoreSimulationSummary") .WithDescription("Evaluates a collection of risk score requests and returns both the full result list and an aggregate summary including average, minimum, and maximum scores plus the top-three highest-scoring subjects. Use this variant when a dashboard-style overview is required alongside per-subject detail.") -.RequireAuthorization(RiskEnginePolicies.Operate); +.RequireAuthorization(RiskEnginePolicies.Operate) +.RequireTenant(); // Refresh Router endpoint cache app.TryRefreshStellaRouterEndpoints(routerEnabled); diff --git a/src/SbomService/StellaOps.SbomService/Program.cs b/src/SbomService/StellaOps.SbomService/Program.cs index 2251c78f5..3a03994a7 100644 --- a/src/SbomService/StellaOps.SbomService/Program.cs +++ b/src/SbomService/StellaOps.SbomService/Program.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using Microsoft.Extensions.Options; using StellaOps.SbomService.Auth; using StellaOps.SbomService.Models; @@ -27,6 +28,7 @@ builder.Services.AddSingleton(SystemGuidProvider.Instance); builder.Services.AddAuthentication(HeaderAuthenticationHandler.SchemeName) .AddScheme(HeaderAuthenticationHandler.SchemeName, _ => { }); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddAuthorization(options => { // SbomService uses HeaderAuthenticationHandler (x-tenant-id). Policies require authenticated tenant context. @@ -257,6 +259,7 @@ if (app.Environment.IsDevelopment()) app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); app.MapGet("/healthz", () => Results.Ok(new { status = "ok" })) @@ -287,7 +290,8 @@ app.MapGet("/entrypoints", async Task ( }) .WithName("ListSbomEntrypoints") .WithDescription("Returns all registered service entrypoints for the tenant, listing artifact, service, path, scope, and runtime flag for each. The tenant query parameter is required.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); app.MapPost("/entrypoints", async Task ( [FromServices] IEntrypointRepository repo, @@ -324,7 +328,8 @@ app.MapPost("/entrypoints", async Task ( }) .WithName("UpsertSbomEntrypoint") .WithDescription("Creates or updates a service entrypoint for the tenant linking an artifact to a service path. Returns the full updated entrypoint list for the tenant. Requires tenant, artifact, service, and path fields.") - .RequireAuthorization(SbomPolicies.Write); + .RequireAuthorization(SbomPolicies.Write) + .RequireTenant(); app.MapGet("/console/sboms", async Task ( [FromServices] ISbomQueryService service, @@ -372,7 +377,8 @@ app.MapGet("/console/sboms", async Task ( }) .WithName("ListConsoleSboms") .WithDescription("Returns a paginated SBOM catalog for the console UI, optionally filtered by artifact name, license, scope, and asset tag. Supports cursor-based pagination. Limit must be between 1 and 200.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); app.MapGet("/components/lookup", async Task ( [FromServices] ISbomQueryService service, @@ -424,7 +430,8 @@ app.MapGet("/components/lookup", async Task ( }) .WithName("LookupSbomComponent") .WithDescription("Looks up all SBOM entries that include a specific component PURL, optionally filtered by artifact. Returns paginated results with cursor support. Requires purl query parameter. Limit must be between 1 and 200.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); app.MapGet("/sbom/context", async Task ( [FromServices] ISbomQueryService service, @@ -489,7 +496,8 @@ app.MapGet("/sbom/context", async Task ( }) .WithName("GetSbomContext") .WithDescription("Returns an assembled SBOM context for an artifact including version timeline and dependency paths for a specific PURL. Combines timeline and path data into a single response for UI rendering. Requires artifactId query parameter.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); app.MapGet("/sbom/paths", async Task ( [FromServices] IServiceProvider services, @@ -541,7 +549,8 @@ app.MapGet("/sbom/paths", async Task ( }) .WithName("GetSbomPaths") .WithDescription("Returns paginated dependency paths for a specific component PURL across SBOMs, optionally filtered by artifact, scope, and environment. Requires purl query parameter. Limit must be between 1 and 200.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); app.MapGet("/sbom/versions", async Task ( [FromServices] ISbomQueryService service, @@ -588,7 +597,8 @@ app.MapGet("/sbom/versions", async Task ( }) .WithName("GetSbomVersions") .WithDescription("Returns the paginated version timeline for a specific artifact, listing SBOM snapshots in chronological order. Requires artifact query parameter. Limit must be between 1 and 200.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); var sbomUploadHandler = async Task ( [FromBody] SbomUploadRequest request, @@ -613,11 +623,13 @@ var sbomUploadHandler = async Task ( app.MapPost("/sbom/upload", sbomUploadHandler) .WithName("UploadSbom") .WithDescription("Uploads and ingests a new SBOM for the specified artifact, validating the payload and persisting it to the ledger. Returns 202 Accepted with the artifact reference and ledger entry on success. Returns 400 if validation fails.") - .RequireAuthorization(SbomPolicies.Write); + .RequireAuthorization(SbomPolicies.Write) + .RequireTenant(); app.MapPost("/api/v1/sbom/upload", sbomUploadHandler) .WithName("UploadSbomV1") .WithDescription("Canonical v1 API path alias for UploadSbom. Uploads and ingests a new SBOM for the specified artifact, validating the payload and persisting it to the ledger. Returns 202 Accepted with the artifact reference and ledger entry on success.") - .RequireAuthorization(SbomPolicies.Write); + .RequireAuthorization(SbomPolicies.Write) + .RequireTenant(); app.MapGet("/sbom/ledger/history", async Task ( [FromServices] ISbomLedgerService ledgerService, @@ -648,7 +660,8 @@ app.MapGet("/sbom/ledger/history", async Task ( }) .WithName("GetSbomLedgerHistory") .WithDescription("Returns the paginated ledger history for a specific artifact, listing SBOM versions in chronological order with ledger metadata. Requires artifact query parameter. Returns 404 if no history is found.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); app.MapGet("/sbom/ledger/point", async Task ( [FromServices] ISbomLedgerService ledgerService, @@ -676,7 +689,8 @@ app.MapGet("/sbom/ledger/point", async Task ( }) .WithName("GetSbomLedgerPoint") .WithDescription("Returns the SBOM ledger entry for a specific artifact at a given point in time. Requires artifact and at (ISO-8601 timestamp) query parameters. Returns 404 if no ledger entry exists for the specified time.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); app.MapGet("/sbom/ledger/range", async Task ( [FromServices] ISbomLedgerService ledgerService, @@ -719,7 +733,8 @@ app.MapGet("/sbom/ledger/range", async Task ( }) .WithName("GetSbomLedgerRange") .WithDescription("Returns paginated SBOM ledger entries for a specific artifact within a time range defined by start and end ISO-8601 timestamps. Requires artifact, start, and end query parameters. Returns 404 if no data is found for the range.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); app.MapGet("/sbom/ledger/diff", async Task ( [FromServices] ISbomLedgerService ledgerService, @@ -743,7 +758,8 @@ app.MapGet("/sbom/ledger/diff", async Task ( }) .WithName("GetSbomLedgerDiff") .WithDescription("Returns a component-level diff between two SBOM ledger entries identified by their GUIDs (before and after). Highlights added, removed, and changed components between two SBOM versions. Returns 404 if either entry is not found.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); app.MapGet("/sbom/ledger/lineage", async Task ( [FromServices] ISbomLedgerService ledgerService, @@ -765,7 +781,8 @@ app.MapGet("/sbom/ledger/lineage", async Task ( }) .WithName("GetSbomLedgerLineage") .WithDescription("Returns the full artifact lineage chain from the SBOM ledger for a specific artifact, showing the provenance ancestry of SBOM versions. Requires artifact query parameter. Returns 404 if lineage is not found.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); // ----------------------------------------------------------------------------- // Lineage Graph API Endpoints (LIN-BE-013/014) @@ -817,7 +834,8 @@ app.MapGet("/api/v1/lineage/{artifactDigest}", async Task ( }) .WithName("GetLineageGraph") .WithDescription("Returns the lineage graph for a specific artifact by digest for the given tenant, including upstream provenance nodes up to maxDepth levels, optional trust badges, and an optional deterministic replay hash. Returns 404 if the graph is not found.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); app.MapGet("/api/v1/lineage/diff", async Task ( [FromServices] ISbomLineageGraphService lineageService, @@ -857,7 +875,8 @@ app.MapGet("/api/v1/lineage/diff", async Task ( }) .WithName("GetLineageDiff") .WithDescription("Returns a graph-level diff between two artifact lineage graphs identified by their digests (from and to) for the given tenant. Highlights added and removed nodes and edges between two artifact versions. Returns 404 if either graph is not found.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); app.MapGet("/api/v1/lineage/hover", async Task ( [FromServices] ISbomLineageGraphService lineageService, @@ -894,7 +913,8 @@ app.MapGet("/api/v1/lineage/hover", async Task ( }) .WithName("GetLineageHoverCard") .WithDescription("Returns a lightweight hover card summary of the lineage relationship between two artifact digests for the given tenant. Used for fast UI hover popups. Cached for low-latency responses. Returns 404 if no hover card data is available.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); app.MapGet("/api/v1/lineage/{artifactDigest}/children", async Task ( [FromServices] ISbomLineageGraphService lineageService, @@ -925,7 +945,8 @@ app.MapGet("/api/v1/lineage/{artifactDigest}/children", async Task ( }) .WithName("GetLineageChildren") .WithDescription("Returns the direct child artifacts in the lineage graph for a specific artifact digest and tenant. Lists artifacts that were built from or derived from the specified artifact.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); app.MapGet("/api/v1/lineage/{artifactDigest}/parents", async Task ( [FromServices] ISbomLineageGraphService lineageService, @@ -956,7 +977,8 @@ app.MapGet("/api/v1/lineage/{artifactDigest}/parents", async Task ( }) .WithName("GetLineageParents") .WithDescription("Returns the direct parent artifacts in the lineage graph for a specific artifact digest and tenant. Lists artifacts from which the specified artifact was built or derived.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); app.MapPost("/api/v1/lineage/export", async Task ( [FromServices] ILineageExportService exportService, @@ -994,7 +1016,8 @@ app.MapPost("/api/v1/lineage/export", async Task ( }) .WithName("ExportLineage") .WithDescription("Exports the lineage evidence pack between two artifact digests for the given tenant as a structured bundle. Enforces a 50 MB size limit on the export payload. Returns 413 if the export exceeds the size limit. Requires fromDigest, toDigest, and tenantId in the request body.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); // ----------------------------------------------------------------------------- // Lineage Compare API (LIN-BE-028) @@ -1053,7 +1076,8 @@ app.MapGet("/api/v1/lineage/compare", async Task ( }) .WithName("CompareLineage") .WithDescription("Returns a rich comparison between two artifact versions by lineage digest (a and b) for the given tenant. Optionally includes SBOM diff, VEX deltas, reachability deltas, attestations, and replay hashes. Returns 404 if comparison data is not found.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); // ----------------------------------------------------------------------------- // Replay Verification API (LIN-BE-033) @@ -1098,7 +1122,8 @@ app.MapPost("/api/v1/lineage/verify", async Task ( }) .WithName("VerifyLineageReplay") .WithDescription("Verifies a deterministic replay hash against the current policy and SBOM state to confirm the release decision is reproducible. Optionally re-evaluates the policy against current feeds. Requires replayHash and tenantId in the request body.") - .RequireAuthorization(SbomPolicies.Write); + .RequireAuthorization(SbomPolicies.Write) + .RequireTenant(); app.MapPost("/api/v1/lineage/compare-drift", async Task ( [FromServices] IReplayVerificationService verificationService, @@ -1128,7 +1153,8 @@ app.MapPost("/api/v1/lineage/compare-drift", async Task ( }) .WithName("CompareLineageDrift") .WithDescription("Compares two replay hashes (hashA and hashB) for the given tenant to detect drift between two release decision points. Returns a structured drift report indicating whether the two points are equivalent. Requires hashA, hashB, and tenantId.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); app.MapGet("/sboms/{snapshotId}/projection", async Task ( [FromServices] ISbomQueryService service, @@ -1189,7 +1215,8 @@ app.MapGet("/sboms/{snapshotId}/projection", async Task ( }) .WithName("GetSbomProjection") .WithDescription("Returns the structured SBOM projection for a specific snapshot ID and tenant. The projection contains the full normalized component graph with schema version and a deterministic hash. Used by the policy engine and reachability graph for decision-making.") - .RequireAuthorization(SbomPolicies.Read); + .RequireAuthorization(SbomPolicies.Read) + .RequireTenant(); app.MapGet("/internal/sbom/events", async Task ( [FromServices] ISbomEventStore store, @@ -1207,7 +1234,8 @@ app.MapGet("/internal/sbom/events", async Task ( }) .WithName("ListSbomEvents") .WithDescription("Internal endpoint. Returns all SBOM version-created events in the in-memory event store backlog. Logs a warning if the backlog exceeds 100 entries. Used by orchestrators to process pending SBOM ingestion events.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.MapGet("/internal/sbom/asset-events", async Task ( [FromServices] ISbomEventStore store, @@ -1226,7 +1254,8 @@ app.MapGet("/internal/sbom/asset-events", async Task ( }) .WithName("ListSbomAssetEvents") .WithDescription("Internal endpoint. Returns all SBOM asset-level events from the in-memory event store. Used by orchestrators to process asset lifecycle changes associated with SBOM versions.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.MapGet("/internal/sbom/ledger/audit", async Task ( [FromServices] ISbomLedgerService ledgerService, @@ -1243,7 +1272,8 @@ app.MapGet("/internal/sbom/ledger/audit", async Task ( }) .WithName("GetSbomLedgerAudit") .WithDescription("Internal endpoint. Returns the chronologically ordered audit trail for a specific artifact from the SBOM ledger, listing all state transitions and operations. Requires artifact query parameter.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.MapGet("/internal/sbom/analysis/jobs", async Task ( [FromServices] ISbomLedgerService ledgerService, @@ -1260,7 +1290,8 @@ app.MapGet("/internal/sbom/analysis/jobs", async Task ( }) .WithName("ListSbomAnalysisJobs") .WithDescription("Internal endpoint. Returns the chronologically ordered list of SBOM analysis jobs for a specific artifact. Requires artifact query parameter.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.MapPost("/internal/sbom/events/backfill", async Task ( [FromServices] IProjectionRepository repository, @@ -1293,7 +1324,8 @@ app.MapPost("/internal/sbom/events/backfill", async Task ( }) .WithName("BackfillSbomEvents") .WithDescription("Internal endpoint. Replays all known SBOM projections as version-created events into the event store backlog. Used for backfill and recovery scenarios after store resets. Returns the count of successfully published events.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.MapGet("/internal/sbom/inventory", async Task ( [FromServices] ISbomEventStore store, @@ -1305,7 +1337,8 @@ app.MapGet("/internal/sbom/inventory", async Task ( }) .WithName("ListSbomInventory") .WithDescription("Internal endpoint. Returns all SBOM inventory entries from the event store, representing the known set of artifacts and their SBOM state across tenants.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.MapPost("/internal/sbom/inventory/backfill", async Task ( [FromServices] ISbomQueryService service, @@ -1325,7 +1358,8 @@ app.MapPost("/internal/sbom/inventory/backfill", async Task ( }) .WithName("BackfillSbomInventory") .WithDescription("Internal endpoint. Clears and replays the SBOM inventory by re-fetching projections for known snapshot/tenant pairs. Used for recovery after inventory store resets. Returns the count of replayed entries.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.MapGet("/internal/sbom/resolver-feed", async Task ( [FromServices] ISbomEventStore store, @@ -1336,7 +1370,8 @@ app.MapGet("/internal/sbom/resolver-feed", async Task ( }) .WithName("GetSbomResolverFeed") .WithDescription("Internal endpoint. Returns all resolver feed candidates from the event store. The resolver feed is used by the policy engine and scanner to resolve component identities across SBOM versions.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.MapPost("/internal/sbom/resolver-feed/backfill", async Task ( [FromServices] ISbomEventStore store, @@ -1354,7 +1389,8 @@ app.MapPost("/internal/sbom/resolver-feed/backfill", async Task ( }) .WithName("BackfillSbomResolverFeed") .WithDescription("Internal endpoint. Clears and replays the resolver feed by re-fetching projections for known snapshot/tenant pairs. Used for recovery after resolver store resets. Returns the count of re-published resolver feed entries.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.MapGet("/internal/sbom/resolver-feed/export", async Task ( [FromServices] ISbomEventStore store, @@ -1367,7 +1403,8 @@ app.MapGet("/internal/sbom/resolver-feed/export", async Task ( }) .WithName("ExportSbomResolverFeed") .WithDescription("Internal endpoint. Exports all resolver feed candidates as a newline-delimited JSON (NDJSON) stream. Used for bulk export and offline processing of the resolver feed by external consumers.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.MapPost("/internal/sbom/retention/prune", async Task ( [FromServices] ISbomLedgerService ledgerService, @@ -1383,7 +1420,8 @@ app.MapPost("/internal/sbom/retention/prune", async Task ( }) .WithName("PruneSbomRetention") .WithDescription("Internal endpoint. Applies the configured retention policy to the SBOM ledger, pruning old versions beyond the configured min/max version counts. Records pruned version counts in metrics. Returns a retention result summary.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.MapGet("/internal/orchestrator/sources", async Task ( [FromQuery] string? tenant, @@ -1400,7 +1438,8 @@ app.MapGet("/internal/orchestrator/sources", async Task ( }) .WithName("ListOrchestratorSources") .WithDescription("Internal endpoint. Returns all registered orchestrator artifact sources for the given tenant. Requires tenant query parameter.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.MapPost("/internal/orchestrator/sources", async Task ( RegisterOrchestratorSourceRequest request, @@ -1425,7 +1464,8 @@ app.MapPost("/internal/orchestrator/sources", async Task ( }) .WithName("RegisterOrchestratorSource") .WithDescription("Internal endpoint. Registers a new orchestrator artifact source for the given tenant linking an artifact digest to a source type. Requires tenantId, artifactDigest, and sourceType in the request body.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.MapGet("/internal/orchestrator/control", async Task ( [FromQuery] string? tenant, @@ -1442,7 +1482,8 @@ app.MapGet("/internal/orchestrator/control", async Task ( }) .WithName("GetOrchestratorControl") .WithDescription("Internal endpoint. Returns the current orchestrator control state for the given tenant including pause/resume flags and scheduling overrides. Requires tenant query parameter.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.MapPost("/internal/orchestrator/control", async Task ( OrchestratorControlRequest request, @@ -1459,7 +1500,8 @@ app.MapPost("/internal/orchestrator/control", async Task ( }) .WithName("UpdateOrchestratorControl") .WithDescription("Internal endpoint. Updates the orchestrator control state for the given tenant, allowing operators to pause, resume, or adjust scheduling parameters. Requires tenantId in the request body.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.MapGet("/internal/orchestrator/watermarks", async Task ( [FromQuery] string? tenant, @@ -1476,7 +1518,8 @@ app.MapGet("/internal/orchestrator/watermarks", async Task ( }) .WithName("GetOrchestratorWatermarks") .WithDescription("Internal endpoint. Returns the current ingestion watermark state for the given tenant, indicating the last successfully processed position in the artifact stream. Requires tenant query parameter.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.MapPost("/internal/orchestrator/watermarks", async Task ( [FromQuery] string? tenant, @@ -1494,7 +1537,8 @@ app.MapPost("/internal/orchestrator/watermarks", async Task ( }) .WithName("SetOrchestratorWatermark") .WithDescription("Internal endpoint. Sets the ingestion watermark for the given tenant to the specified value, marking the last processed position in the artifact stream. Requires tenant query parameter.") - .RequireAuthorization(SbomPolicies.Internal); + .RequireAuthorization(SbomPolicies.Internal) + .RequireTenant(); app.TryRefreshStellaRouterEndpoints(routerEnabled); app.Run(); diff --git a/src/Scanner/StellaOps.Scanner.WebService/Endpoints/SliceEndpoints.cs b/src/Scanner/StellaOps.Scanner.WebService/Endpoints/SliceEndpoints.cs index b8c5de23a..60097c1f5 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Endpoints/SliceEndpoints.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Endpoints/SliceEndpoints.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Scanner.WebService.Security; using StellaOps.Scanner.WebService.Services; using System.Text.Json; @@ -26,7 +27,8 @@ internal static class SliceEndpoints ArgumentNullException.ThrowIfNull(endpoints); var slicesGroup = endpoints.MapGroup("/api/slices") - .WithTags("Slices"); + .WithTags("Slices") + .RequireTenant(); // POST /api/slices/query - Generate reachability slice on demand slicesGroup.MapPost("/query", HandleQueryAsync) diff --git a/src/Scanner/StellaOps.Scanner.WebService/Program.cs b/src/Scanner/StellaOps.Scanner.WebService/Program.cs index cd61b49ac..b6229c764 100644 --- a/src/Scanner/StellaOps.Scanner.WebService/Program.cs +++ b/src/Scanner/StellaOps.Scanner.WebService/Program.cs @@ -12,6 +12,7 @@ using Serilog.Events; using StellaOps.Auth.Abstractions; using StellaOps.Auth.Client; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Authority.Persistence.Postgres.Repositories; using StellaOps.Concelier.Core.Linksets; using StellaOps.Configuration; @@ -549,6 +550,7 @@ builder.Services.AddSingleton(sp => return new NullAdvisoryLinksetQueryService(); }); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); builder.TryAddStellaOpsLocalBinding("scanner"); @@ -627,6 +629,7 @@ app.UseExceptionHandler(errorApp => app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); // Stella Router integration - enables request dispatch from Router to ASP.NET endpoints app.TryUseStellaRouter(routerEnabled); @@ -641,7 +644,7 @@ app.MapHealthEndpoints(); app.MapObservabilityEndpoints(); app.MapOfflineKitEndpoints(); -var apiGroup = app.MapGroup(resolvedOptions.Api.BasePath); +var apiGroup = app.MapGroup(resolvedOptions.Api.BasePath).RequireTenant(); if (app.Environment.IsEnvironment("Testing")) { diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/EventWebhookEndpointExtensions.cs b/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/EventWebhookEndpointExtensions.cs index e9e2d2e19..ba35ea725 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/EventWebhookEndpointExtensions.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/EventWebhookEndpointExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Scheduler.WebService.Options; using System.ComponentModel.DataAnnotations; using System.IO; @@ -17,7 +18,8 @@ public static class EventWebhookEndpointExtensions public static void MapSchedulerEventWebhookEndpoints(this IEndpointRouteBuilder builder) { var group = builder.MapGroup("/events") - .AllowAnonymous(); + .AllowAnonymous() + .RequireTenant(); group.MapPost("/conselier-export", HandleConselierExportAsync) .WithName("HandleConselierExportWebhook") diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/FailureSignatures/FailureSignatureEndpoints.cs b/src/Scheduler/StellaOps.Scheduler.WebService/FailureSignatures/FailureSignatureEndpoints.cs index 541b33fce..adadea135 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/FailureSignatures/FailureSignatureEndpoints.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/FailureSignatures/FailureSignatureEndpoints.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Scheduler.Persistence.Postgres.Models; using StellaOps.Scheduler.Persistence.Postgres.Repositories; using StellaOps.Scheduler.WebService.Auth; @@ -17,7 +18,8 @@ internal static class FailureSignatureEndpoints public static IEndpointRouteBuilder MapFailureSignatureEndpoints(this IEndpointRouteBuilder routes) { var group = routes.MapGroup("/api/v1/scheduler/failure-signatures") - .RequireAuthorization(SchedulerPolicies.Read); + .RequireAuthorization(SchedulerPolicies.Read) + .RequireTenant(); group.MapGet("/best-match", GetBestMatchAsync) .WithName("GetFailureSignatureBestMatch") diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/GraphJobs/GraphJobEndpointExtensions.cs b/src/Scheduler/StellaOps.Scheduler.WebService/GraphJobs/GraphJobEndpointExtensions.cs index 065e3ba52..971704e60 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/GraphJobs/GraphJobEndpointExtensions.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/GraphJobs/GraphJobEndpointExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Scheduler.Models; using StellaOps.Scheduler.WebService.Auth; using StellaOps.Scheduler.WebService.Security; @@ -13,7 +14,8 @@ public static class GraphJobEndpointExtensions public static void MapGraphJobEndpoints(this IEndpointRouteBuilder builder) { var group = builder.MapGroup("/graphs") - .RequireAuthorization(SchedulerPolicies.Operate); + .RequireAuthorization(SchedulerPolicies.Operate) + .RequireTenant(); group.MapPost("/build", CreateGraphBuildJob) .WithName("CreateGraphBuildJob") diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/PolicyRuns/PolicyRunEndpointExtensions.cs b/src/Scheduler/StellaOps.Scheduler.WebService/PolicyRuns/PolicyRunEndpointExtensions.cs index a0eb989c7..9b4f8f3a6 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/PolicyRuns/PolicyRunEndpointExtensions.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/PolicyRuns/PolicyRunEndpointExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Scheduler.Models; using StellaOps.Scheduler.WebService.Auth; using StellaOps.Scheduler.WebService.Security; @@ -18,7 +19,8 @@ internal static class PolicyRunEndpointExtensions public static void MapPolicyRunEndpoints(this IEndpointRouteBuilder builder) { var group = builder.MapGroup("/api/v1/scheduler/policy/runs") - .RequireAuthorization(SchedulerPolicies.Read); + .RequireAuthorization(SchedulerPolicies.Read) + .RequireTenant(); group.MapGet("/", ListPolicyRunsAsync) .WithName("ListPolicyRuns") diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/PolicySimulations/PolicySimulationEndpointExtensions.cs b/src/Scheduler/StellaOps.Scheduler.WebService/PolicySimulations/PolicySimulationEndpointExtensions.cs index 204c4d897..9c1ea1ca4 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/PolicySimulations/PolicySimulationEndpointExtensions.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/PolicySimulations/PolicySimulationEndpointExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Scheduler.Models; using StellaOps.Scheduler.WebService.Auth; using StellaOps.Scheduler.WebService.PolicyRuns; @@ -21,7 +22,8 @@ internal static class PolicySimulationEndpointExtensions public static void MapPolicySimulationEndpoints(this IEndpointRouteBuilder builder) { var group = builder.MapGroup("/api/v1/scheduler/policies/simulations") - .RequireAuthorization(SchedulerPolicies.Operate); + .RequireAuthorization(SchedulerPolicies.Operate) + .RequireTenant(); group.MapGet("/", ListSimulationsAsync) .WithName("ListPolicySimulations") diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/Program.cs b/src/Scheduler/StellaOps.Scheduler.WebService/Program.cs index 40cae8353..9c3d11023 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/Program.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/Program.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Plugin.DependencyInjection; using StellaOps.Plugin.Hosting; using StellaOps.Router.AspNet; @@ -232,6 +233,7 @@ else builder.Services.AddScoped(); } +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddEndpointsApiExplorer(); // Stella Router integration @@ -248,6 +250,7 @@ app.LogStellaOpsLocalHostname("scheduler"); app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.UseMiddleware(); app.TryUseStellaRouter(routerEnabled); diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/Runs/RunEndpoints.cs b/src/Scheduler/StellaOps.Scheduler.WebService/Runs/RunEndpoints.cs index 99c531066..29a0c9230 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/Runs/RunEndpoints.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/Runs/RunEndpoints.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Primitives; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Scheduler.ImpactIndex; using StellaOps.Scheduler.Models; using StellaOps.Scheduler.Persistence.Postgres.Repositories; @@ -29,7 +30,8 @@ internal static class RunEndpoints public static IEndpointRouteBuilder MapRunEndpoints(this IEndpointRouteBuilder routes) { var group = routes.MapGroup("/api/v1/scheduler/runs") - .RequireAuthorization(SchedulerPolicies.Read); + .RequireAuthorization(SchedulerPolicies.Read) + .RequireTenant(); group.MapGet("/", ListRunsAsync) .WithName("ListSchedulerRuns") diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/Schedules/ScheduleEndpoints.cs b/src/Scheduler/StellaOps.Scheduler.WebService/Schedules/ScheduleEndpoints.cs index 8e6171217..3f41871e4 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/Schedules/ScheduleEndpoints.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/Schedules/ScheduleEndpoints.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Scheduler.Models; using StellaOps.Scheduler.Persistence.Postgres.Repositories; using StellaOps.Scheduler.WebService.Auth; @@ -22,7 +23,8 @@ internal static class ScheduleEndpoints public static IEndpointRouteBuilder MapScheduleEndpoints(this IEndpointRouteBuilder routes) { var group = routes.MapGroup("/api/v1/scheduler/schedules") - .RequireAuthorization(SchedulerPolicies.Read); + .RequireAuthorization(SchedulerPolicies.Read) + .RequireTenant(); group.MapGet("/", ListSchedulesAsync) .WithName("ListSchedules") diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/VulnerabilityResolverJobs/ResolverJobEndpointExtensions.cs b/src/Scheduler/StellaOps.Scheduler.WebService/VulnerabilityResolverJobs/ResolverJobEndpointExtensions.cs index 078599b8a..c52fc8b8b 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/VulnerabilityResolverJobs/ResolverJobEndpointExtensions.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/VulnerabilityResolverJobs/ResolverJobEndpointExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Scheduler.WebService.Auth; using StellaOps.Scheduler.WebService.Security; using System.ComponentModel.DataAnnotations; @@ -15,7 +16,8 @@ public static class ResolverJobEndpointExtensions public static void MapResolverJobEndpoints(this IEndpointRouteBuilder builder) { var group = builder.MapGroup("/api/v1/scheduler/vuln/resolver") - .RequireAuthorization(SchedulerPolicies.Operate); + .RequireAuthorization(SchedulerPolicies.Operate) + .RequireTenant(); group.MapPost("/jobs", CreateJobAsync) .WithName("CreateResolverJob") diff --git a/src/Signals/StellaOps.Signals/Program.cs b/src/Signals/StellaOps.Signals/Program.cs index e103395fc..955129928 100644 --- a/src/Signals/StellaOps.Signals/Program.cs +++ b/src/Signals/StellaOps.Signals/Program.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Options; using NetEscapades.Configuration.Yaml; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Configuration; using StellaOps.Determinism; using StellaOps.Signals.Authentication; @@ -89,6 +90,7 @@ builder.Services.AddTriageSuppressServices(); builder.Services.AddSingleton(); builder.Services.AddProblemDetails(); builder.Services.AddHealthChecks(); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); builder.Services.AddRouting(options => options.LowercaseUrls = true); @@ -310,6 +312,7 @@ if (!bootstrap.Authority.Enabled) app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); app.MapHealthChecks("/healthz").AllowAnonymous(); @@ -330,7 +333,7 @@ app.MapGet("/readyz", (SignalsStartupState state, SignalsSealedModeMonitor seale // SCM/CI webhook endpoints (Sprint: SPRINT_20251229_013) app.MapScmWebhookEndpoints(); -var signalsGroup = app.MapGroup("/signals"); +var signalsGroup = app.MapGroup("/signals").RequireTenant(); signalsGroup.MapGet("/ping", (HttpContext context, SignalsOptions options, SignalsSealedModeMonitor sealedModeMonitor) => { diff --git a/src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/Endpoints/CeremonyEndpoints.cs b/src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/Endpoints/CeremonyEndpoints.cs index a31b656f5..3783fce9d 100644 --- a/src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/Endpoints/CeremonyEndpoints.cs +++ b/src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/Endpoints/CeremonyEndpoints.cs @@ -13,6 +13,7 @@ using StellaOps.Signer.Core.Ceremonies; using StellaOps.Signer.WebService.Contracts; using System.Security.Claims; using System.Text.Json; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.Signer.WebService.Endpoints; @@ -34,7 +35,8 @@ public static class CeremonyEndpoints { var group = endpoints.MapGroup("/api/v1/ceremonies") .WithTags("Ceremonies") - .RequireAuthorization("ceremony:read"); + .RequireAuthorization("ceremony:read") + .RequireTenant(); // Create ceremony group.MapPost("/", CreateCeremonyAsync) diff --git a/src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/Endpoints/KeyRotationEndpoints.cs b/src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/Endpoints/KeyRotationEndpoints.cs index feae7cbf3..98d24a21a 100644 --- a/src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/Endpoints/KeyRotationEndpoints.cs +++ b/src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/Endpoints/KeyRotationEndpoints.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using StellaOps.Signer.KeyManagement; using System.ComponentModel.DataAnnotations; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.Signer.WebService.Endpoints; @@ -27,7 +28,8 @@ public static class KeyRotationEndpoints { var group = endpoints.MapGroup("/api/v1/anchors") .WithTags("KeyRotation", "TrustAnchors") - .RequireAuthorization("KeyManagement"); + .RequireAuthorization("KeyManagement") + .RequireTenant(); // Key management endpoints group.MapPost("/{anchorId:guid}/keys", AddKeyAsync) diff --git a/src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/Endpoints/SignerEndpoints.cs b/src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/Endpoints/SignerEndpoints.cs index 3cd975cea..3927382fa 100644 --- a/src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/Endpoints/SignerEndpoints.cs +++ b/src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/Endpoints/SignerEndpoints.cs @@ -19,6 +19,7 @@ using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.Signer.WebService.Endpoints; @@ -28,7 +29,8 @@ public static class SignerEndpoints { var group = endpoints.MapGroup("/api/v1/signer") .WithTags("Signer") - .RequireAuthorization(SignerPolicies.Verify); + .RequireAuthorization(SignerPolicies.Verify) + .RequireTenant(); group.MapPost("/sign/dsse", SignDsseAsync) .WithName("SignDsse") diff --git a/src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/Program.cs b/src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/Program.cs index 21f6fd84a..e9526bfe9 100644 --- a/src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/Program.cs +++ b/src/Signer/StellaOps.Signer/StellaOps.Signer.WebService/Program.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using StellaOps.Auth.Abstractions; using StellaOps.Cryptography.DependencyInjection; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Router.AspNet; using StellaOps.Signer.Infrastructure; using StellaOps.Signer.Infrastructure.Options; @@ -89,6 +90,7 @@ builder.Services.Configure(options => builder.Services.Configure(_ => { }); builder.Services.AddStellaOpsCryptoRu(builder.Configuration, CryptoProviderRegistryValidator.EnforceRuLinuxDefaults); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); // Stella Router integration @@ -105,6 +107,7 @@ app.LogStellaOpsLocalHostname("signer"); app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); app.MapGet("/", () => Results.Ok("StellaOps Signer service ready.")); diff --git a/src/SmRemote/StellaOps.SmRemote.Service/Program.cs b/src/SmRemote/StellaOps.SmRemote.Service/Program.cs index 504a5d236..501f18fb1 100644 --- a/src/SmRemote/StellaOps.SmRemote.Service/Program.cs +++ b/src/SmRemote/StellaOps.SmRemote.Service/Program.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.SmRemote.Service.Security; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; @@ -40,6 +41,7 @@ builder.Services.AddAuthorization(options => options.AddStellaOpsScopePolicy(SmRemotePolicies.Verify, StellaOpsScopes.SmRemoteVerify); }); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); // Stella Router integration @@ -62,6 +64,7 @@ if (app.Environment.IsDevelopment()) app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); app.MapGet("/health", () => Results.Ok(new SmHealthResponse("ok"))) @@ -99,7 +102,8 @@ app.MapPost("/hash", (HashRequest req) => }) .WithName("SmRemoteHash") .WithDescription("Computes an SM3 hash of the provided base64-encoded payload. Returns the hash as both base64 and lowercase hex. Defaults to SM3 if algorithmId is omitted. Returns 400 if the payload is missing, invalid base64, or an unsupported algorithm is requested.") - .RequireAuthorization(SmRemotePolicies.Sign); + .RequireAuthorization(SmRemotePolicies.Sign) + .RequireTenant(); app.MapPost("/encrypt", (EncryptRequest req) => { @@ -125,7 +129,8 @@ app.MapPost("/encrypt", (EncryptRequest req) => return Results.Ok(new EncryptResponse(algorithmId, Convert.ToBase64String(ciphertext))); }) .WithName("SmRemoteEncrypt") - .WithDescription("Encrypts the provided base64-encoded payload using SM4-ECB with PKCS7 padding and the supplied 128-bit (16-byte) base64-encoded key. Returns the ciphertext as base64. Returns 400 if the key, payload, or algorithm is missing, invalid, or the key length is not 16 bytes."); + .WithDescription("Encrypts the provided base64-encoded payload using SM4-ECB with PKCS7 padding and the supplied 128-bit (16-byte) base64-encoded key. Returns the ciphertext as base64. Returns 400 if the key, payload, or algorithm is missing, invalid, or the key length is not 16 bytes.") + .RequireTenant(); app.MapPost("/decrypt", (DecryptRequest req) => { @@ -157,7 +162,8 @@ app.MapPost("/decrypt", (DecryptRequest req) => } }) .WithName("SmRemoteDecrypt") - .WithDescription("Decrypts the provided base64-encoded SM4-ECB ciphertext using the supplied 128-bit (16-byte) base64-encoded key with PKCS7 unpadding. Returns the plaintext payload as base64. Returns 400 if the key, ciphertext, or algorithm is invalid, or if the ciphertext padding is corrupt."); + .WithDescription("Decrypts the provided base64-encoded SM4-ECB ciphertext using the supplied 128-bit (16-byte) base64-encoded key with PKCS7 unpadding. Returns the plaintext payload as base64. Returns 400 if the key, ciphertext, or algorithm is invalid, or if the ciphertext padding is corrupt.") + .RequireTenant(); app.MapPost("/sign", async (SignRequest req, ICryptoProviderRegistry registry, TimeProvider timeProvider, CancellationToken ct) => { @@ -178,7 +184,8 @@ app.MapPost("/sign", async (SignRequest req, ICryptoProviderRegistry registry, T return Results.Ok(new SignResponse(Convert.ToBase64String(signature))); }) .WithName("SmRemoteSign") - .WithDescription("Signs the provided base64-encoded payload using the SM2 algorithm and the specified key ID. Seeds the key from an ephemeral EC key pair if not already present. Returns the base64-encoded SM2 signature. Returns 400 if the key ID, algorithm, or payload is missing or invalid."); + .WithDescription("Signs the provided base64-encoded payload using the SM2 algorithm and the specified key ID. Seeds the key from an ephemeral EC key pair if not already present. Returns the base64-encoded SM2 signature. Returns 400 if the key ID, algorithm, or payload is missing or invalid.") + .RequireTenant(); app.MapPost("/verify", async (VerifyRequest req, ICryptoProviderRegistry registry, TimeProvider timeProvider, CancellationToken ct) => { @@ -198,7 +205,8 @@ app.MapPost("/verify", async (VerifyRequest req, ICryptoProviderRegistry registr return Results.Ok(new VerifyResponse(ok)); }) .WithName("SmRemoteVerify") - .WithDescription("Verifies an SM2 signature against the provided base64-encoded payload using the specified key ID. Returns a boolean valid field indicating whether the signature matches. Returns 400 if the key ID, algorithm, payload, or signature is missing or invalid base64."); + .WithDescription("Verifies an SM2 signature against the provided base64-encoded payload using the specified key ID. Returns a boolean valid field indicating whether the signature matches. Returns 400 if the key ID, algorithm, payload, or signature is missing or invalid base64.") + .RequireTenant(); app.TryRefreshStellaRouterEndpoints(routerEnabled); app.Run(); diff --git a/src/Symbols/StellaOps.Symbols.Server/Endpoints/SymbolSourceEndpoints.cs b/src/Symbols/StellaOps.Symbols.Server/Endpoints/SymbolSourceEndpoints.cs index 285f45c0f..fc4cb2120 100644 --- a/src/Symbols/StellaOps.Symbols.Server/Endpoints/SymbolSourceEndpoints.cs +++ b/src/Symbols/StellaOps.Symbols.Server/Endpoints/SymbolSourceEndpoints.cs @@ -6,6 +6,7 @@ using StellaOps.Symbols.Marketplace.Models; using StellaOps.Symbols.Marketplace.Repositories; using StellaOps.Symbols.Marketplace.Scoring; using StellaOps.Symbols.Server.Security; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.Symbols.Server.Endpoints; @@ -22,7 +23,8 @@ public static class SymbolSourceEndpoints // --- Symbol Sources --- var sources = app.MapGroup("/api/v1/symbols/sources") .WithTags("Symbol Sources") - .RequireAuthorization(SymbolsPolicies.Read); + .RequireAuthorization(SymbolsPolicies.Read) + .RequireTenant(); sources.MapGet(string.Empty, async ( ISymbolSourceReadRepository repository, @@ -174,7 +176,8 @@ public static class SymbolSourceEndpoints // --- Marketplace Catalog --- var marketplace = app.MapGroup("/api/v1/symbols/marketplace") .WithTags("Symbol Marketplace") - .RequireAuthorization(SymbolsPolicies.Read); + .RequireAuthorization(SymbolsPolicies.Read) + .RequireTenant(); marketplace.MapGet(string.Empty, async ( IMarketplaceCatalogRepository repository, diff --git a/src/Symbols/StellaOps.Symbols.Server/Program.cs b/src/Symbols/StellaOps.Symbols.Server/Program.cs index 29d3bc27e..038386a4d 100644 --- a/src/Symbols/StellaOps.Symbols.Server/Program.cs +++ b/src/Symbols/StellaOps.Symbols.Server/Program.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Symbols.Core.Abstractions; using StellaOps.Symbols.Core.Models; using StellaOps.Symbols.Infrastructure; @@ -20,6 +21,7 @@ builder.Services.AddStellaOpsResourceServerAuthentication( { options.RequiredScopes.Clear(); }); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddAuthorization(options => { @@ -57,6 +59,7 @@ if (app.Environment.IsDevelopment()) app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); // Health endpoint (anonymous) diff --git a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/Program.cs b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/Program.cs index 23dbd00a5..eb5481315 100644 --- a/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/Program.cs +++ b/src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService/Program.cs @@ -6,6 +6,7 @@ using OpenTelemetry.Metrics; using OpenTelemetry.Trace; using StellaOps.AirGap.Policy; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Router.AspNet; using StellaOps.TaskRunner.Core.AirGap; using StellaOps.TaskRunner.Core.Attestation; @@ -113,6 +114,7 @@ builder.Services.AddOpenApi(); // Determinism: TimeProvider injection builder.Services.AddSingleton(TimeProvider.System); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); // Stella Router integration @@ -128,6 +130,7 @@ app.LogStellaOpsLocalHostname("taskrunner"); // Add deprecation middleware for sunset headers (RFC 8594) app.UseStellaOpsCors(); +app.UseStellaOpsTenantMiddleware(); app.UseApiDeprecation(); app.TryUseStellaRouter(routerEnabled); @@ -159,7 +162,8 @@ app.MapGet("/v1/task-runner/deprecations", async ( }) .WithName("GetDeprecations") .WithDescription("Returns a list of deprecated API endpoints with sunset dates, optionally filtered to those expiring within a given number of days. Used for API lifecycle governance and client migration planning.") -.WithTags("API Governance"); +.WithTags("API Governance") +.RequireTenant(); app.MapPost("/v1/task-runner/simulations", async ( [FromBody] SimulationRequest request, @@ -199,107 +203,136 @@ app.MapPost("/v1/task-runner/simulations", async ( return Results.Ok(response); }) .WithName("SimulateTaskPack") -.WithDescription("Simulates a task pack execution plan from a manifest and input map without actually scheduling a run. Returns the execution graph with per-step status, pending approvals, and resolved outputs for pre-flight validation."); +.WithDescription("Simulates a task pack execution plan from a manifest and input map without actually scheduling a run. Returns the execution graph with per-step status, pending approvals, and resolved outputs for pre-flight validation.") +.RequireTenant(); app.MapPost("/v1/task-runner/runs", HandleCreateRun) .WithName("CreatePackRun") - .WithDescription("Creates and schedules a new task pack run from a manifest and optional input overrides. Enforces sealed-install policy before scheduling. Returns 201 Created with the initial run state including step graph. Returns 403 if sealed-install policy is violated."); + .WithDescription("Creates and schedules a new task pack run from a manifest and optional input overrides. Enforces sealed-install policy before scheduling. Returns 201 Created with the initial run state including step graph. Returns 403 if sealed-install policy is violated.") + .RequireTenant(); app.MapPost("/api/runs", HandleCreateRun) .WithName("CreatePackRunApi") - .WithDescription("Legacy path alias for CreatePackRun. Creates and schedules a new task pack run from a manifest and optional input overrides. Returns 201 Created with the initial run state."); + .WithDescription("Legacy path alias for CreatePackRun. Creates and schedules a new task pack run from a manifest and optional input overrides. Returns 201 Created with the initial run state.") + .RequireTenant(); app.MapGet("/v1/task-runner/runs/{runId}", HandleGetRunState) .WithName("GetRunState") - .WithDescription("Returns the current execution state for a task pack run including per-step status, attempt counts, and transition timestamps. Returns 404 if the run is not found."); + .WithDescription("Returns the current execution state for a task pack run including per-step status, attempt counts, and transition timestamps. Returns 404 if the run is not found.") + .RequireTenant(); app.MapGet("/api/runs/{runId}", HandleGetRunState) .WithName("GetRunStateApi") - .WithDescription("Legacy path alias for GetRunState. Returns the current execution state for a task pack run. Returns 404 if the run is not found."); + .WithDescription("Legacy path alias for GetRunState. Returns the current execution state for a task pack run. Returns 404 if the run is not found.") + .RequireTenant(); app.MapGet("/v1/task-runner/runs/{runId}/logs", HandleStreamRunLogs) .WithName("StreamRunLogs") - .WithDescription("Streams the structured log entries for a task pack run as newline-delimited JSON (application/x-ndjson). Returns log lines in chronological order. Returns 404 if the run log is not found."); + .WithDescription("Streams the structured log entries for a task pack run as newline-delimited JSON (application/x-ndjson). Returns log lines in chronological order. Returns 404 if the run log is not found.") + .RequireTenant(); app.MapGet("/api/runs/{runId}/logs", HandleStreamRunLogs) .WithName("StreamRunLogsApi") - .WithDescription("Legacy path alias for StreamRunLogs. Streams the run log entries as newline-delimited JSON."); + .WithDescription("Legacy path alias for StreamRunLogs. Streams the run log entries as newline-delimited JSON.") + .RequireTenant(); app.MapGet("/v1/task-runner/runs/{runId}/artifacts", HandleListArtifacts) .WithName("ListRunArtifacts") - .WithDescription("Lists all artifacts captured during a task pack run including artifact name, type, paths, capture timestamp, and status. Returns 404 if the run is not found."); + .WithDescription("Lists all artifacts captured during a task pack run including artifact name, type, paths, capture timestamp, and status. Returns 404 if the run is not found.") + .RequireTenant(); app.MapGet("/api/runs/{runId}/artifacts", HandleListArtifacts) .WithName("ListRunArtifactsApi") - .WithDescription("Legacy path alias for ListRunArtifacts. Lists all artifacts captured during a task pack run."); + .WithDescription("Legacy path alias for ListRunArtifacts. Lists all artifacts captured during a task pack run.") + .RequireTenant(); app.MapPost("/v1/task-runner/runs/{runId}/approvals/{approvalId}", HandleApplyApprovalDecision) .WithName("ApplyApprovalDecision") - .WithDescription("Submits an approval or rejection decision for a pending approval gate in a task pack run. Validates the planHash to prevent replay attacks. Returns 200 with updated approval status or 409 on plan hash mismatch."); + .WithDescription("Submits an approval or rejection decision for a pending approval gate in a task pack run. Validates the planHash to prevent replay attacks. Returns 200 with updated approval status or 409 on plan hash mismatch.") + .RequireTenant(); app.MapPost("/api/runs/{runId}/approvals/{approvalId}", HandleApplyApprovalDecision) .WithName("ApplyApprovalDecisionApi") - .WithDescription("Legacy path alias for ApplyApprovalDecision. Submits an approval or rejection decision for a pending approval gate."); + .WithDescription("Legacy path alias for ApplyApprovalDecision. Submits an approval or rejection decision for a pending approval gate.") + .RequireTenant(); app.MapPost("/v1/task-runner/runs/{runId}/cancel", HandleCancelRun) .WithName("CancelRun") - .WithDescription("Requests cancellation of an active task pack run. Marks all non-terminal steps as skipped and writes cancellation log entries. Returns 202 Accepted with the cancelled status."); + .WithDescription("Requests cancellation of an active task pack run. Marks all non-terminal steps as skipped and writes cancellation log entries. Returns 202 Accepted with the cancelled status.") + .RequireTenant(); app.MapPost("/api/runs/{runId}/cancel", HandleCancelRun) .WithName("CancelRunApi") - .WithDescription("Legacy path alias for CancelRun. Requests cancellation of an active task pack run and marks remaining steps as skipped."); + .WithDescription("Legacy path alias for CancelRun. Requests cancellation of an active task pack run and marks remaining steps as skipped.") + .RequireTenant(); // Attestation endpoints (TASKRUN-OBS-54-001) app.MapGet("/v1/task-runner/runs/{runId}/attestations", HandleListAttestations) .WithName("ListRunAttestations") - .WithDescription("Lists all attestations generated for a task pack run, including predicate type, subject count, creation timestamp, and whether a DSSE envelope is present."); + .WithDescription("Lists all attestations generated for a task pack run, including predicate type, subject count, creation timestamp, and whether a DSSE envelope is present.") + .RequireTenant(); app.MapGet("/api/runs/{runId}/attestations", HandleListAttestations) .WithName("ListRunAttestationsApi") - .WithDescription("Legacy path alias for ListRunAttestations. Lists all attestations generated for a task pack run."); + .WithDescription("Legacy path alias for ListRunAttestations. Lists all attestations generated for a task pack run.") + .RequireTenant(); app.MapGet("/v1/task-runner/attestations/{attestationId}", HandleGetAttestation) .WithName("GetAttestation") - .WithDescription("Returns the full attestation record for a specific attestation ID, including subjects, predicate type, status, evidence snapshot reference, and metadata. Returns 404 if not found."); + .WithDescription("Returns the full attestation record for a specific attestation ID, including subjects, predicate type, status, evidence snapshot reference, and metadata. Returns 404 if not found.") + .RequireTenant(); app.MapGet("/api/attestations/{attestationId}", HandleGetAttestation) .WithName("GetAttestationApi") - .WithDescription("Legacy path alias for GetAttestation. Returns the full attestation record for a specific attestation ID."); + .WithDescription("Legacy path alias for GetAttestation. Returns the full attestation record for a specific attestation ID.") + .RequireTenant(); app.MapGet("/v1/task-runner/attestations/{attestationId}/envelope", HandleGetAttestationEnvelope) .WithName("GetAttestationEnvelope") - .WithDescription("Returns the DSSE envelope for a signed attestation including payload type, base64-encoded payload, and signatures with key IDs. Returns 404 if no envelope exists."); + .WithDescription("Returns the DSSE envelope for a signed attestation including payload type, base64-encoded payload, and signatures with key IDs. Returns 404 if no envelope exists.") + .RequireTenant(); app.MapGet("/api/attestations/{attestationId}/envelope", HandleGetAttestationEnvelope) .WithName("GetAttestationEnvelopeApi") - .WithDescription("Legacy path alias for GetAttestationEnvelope. Returns the DSSE envelope for a signed attestation."); + .WithDescription("Legacy path alias for GetAttestationEnvelope. Returns the DSSE envelope for a signed attestation.") + .RequireTenant(); app.MapPost("/v1/task-runner/attestations/{attestationId}/verify", HandleVerifyAttestation) .WithName("VerifyAttestation") - .WithDescription("Verifies a task pack attestation against optional expected subjects. Validates signature, subject digest matching, and revocation status. Returns 200 with verification details on success or 400 with error breakdown on failure."); + .WithDescription("Verifies a task pack attestation against optional expected subjects. Validates signature, subject digest matching, and revocation status. Returns 200 with verification details on success or 400 with error breakdown on failure.") + .RequireTenant(); app.MapPost("/api/attestations/{attestationId}/verify", HandleVerifyAttestation) .WithName("VerifyAttestationApi") - .WithDescription("Legacy path alias for VerifyAttestation. Verifies a task pack attestation against expected subjects and returns detailed verification results."); + .WithDescription("Legacy path alias for VerifyAttestation. Verifies a task pack attestation against expected subjects and returns detailed verification results.") + .RequireTenant(); // Incident mode endpoints (TASKRUN-OBS-55-001) app.MapGet("/v1/task-runner/runs/{runId}/incident-mode", HandleGetIncidentModeStatus) .WithName("GetIncidentModeStatus") - .WithDescription("Returns the current incident mode status for a task pack run including activation level, source, expiry, retention policy, telemetry settings, and debug capture configuration."); + .WithDescription("Returns the current incident mode status for a task pack run including activation level, source, expiry, retention policy, telemetry settings, and debug capture configuration.") + .RequireTenant(); app.MapGet("/api/runs/{runId}/incident-mode", HandleGetIncidentModeStatus) .WithName("GetIncidentModeStatusApi") - .WithDescription("Legacy path alias for GetIncidentModeStatus. Returns the current incident mode status for a task pack run."); + .WithDescription("Legacy path alias for GetIncidentModeStatus. Returns the current incident mode status for a task pack run.") + .RequireTenant(); app.MapPost("/v1/task-runner/runs/{runId}/incident-mode/activate", HandleActivateIncidentMode) .WithName("ActivateIncidentMode") - .WithDescription("Activates incident mode for a task pack run at the specified escalation level. Enables extended retention, enhanced telemetry, and optional debug capture. Accepts optional duration and requesting actor."); + .WithDescription("Activates incident mode for a task pack run at the specified escalation level. Enables extended retention, enhanced telemetry, and optional debug capture. Accepts optional duration and requesting actor.") + .RequireTenant(); app.MapPost("/api/runs/{runId}/incident-mode/activate", HandleActivateIncidentMode) .WithName("ActivateIncidentModeApi") - .WithDescription("Legacy path alias for ActivateIncidentMode. Activates incident mode for a task pack run at the specified escalation level."); + .WithDescription("Legacy path alias for ActivateIncidentMode. Activates incident mode for a task pack run at the specified escalation level.") + .RequireTenant(); app.MapPost("/v1/task-runner/runs/{runId}/incident-mode/deactivate", HandleDeactivateIncidentMode) .WithName("DeactivateIncidentMode") - .WithDescription("Deactivates incident mode for a task pack run and restores normal retention and telemetry settings. Returns the updated inactive status."); + .WithDescription("Deactivates incident mode for a task pack run and restores normal retention and telemetry settings. Returns the updated inactive status.") + .RequireTenant(); app.MapPost("/api/runs/{runId}/incident-mode/deactivate", HandleDeactivateIncidentMode) .WithName("DeactivateIncidentModeApi") - .WithDescription("Legacy path alias for DeactivateIncidentMode. Deactivates incident mode for a task pack run."); + .WithDescription("Legacy path alias for DeactivateIncidentMode. Deactivates incident mode for a task pack run.") + .RequireTenant(); app.MapPost("/v1/task-runner/runs/{runId}/incident-mode/escalate", HandleEscalateIncidentMode) .WithName("EscalateIncidentMode") - .WithDescription("Escalates an active incident mode to a higher severity level for a task pack run. Requires a valid escalation level (Low, Medium, High, Critical). Returns the updated incident level."); + .WithDescription("Escalates an active incident mode to a higher severity level for a task pack run. Requires a valid escalation level (Low, Medium, High, Critical). Returns the updated incident level.") + .RequireTenant(); app.MapPost("/api/runs/{runId}/incident-mode/escalate", HandleEscalateIncidentMode) .WithName("EscalateIncidentModeApi") - .WithDescription("Legacy path alias for EscalateIncidentMode. Escalates incident mode to a higher severity level for a task pack run."); + .WithDescription("Legacy path alias for EscalateIncidentMode. Escalates incident mode to a higher severity level for a task pack run.") + .RequireTenant(); app.MapPost("/v1/task-runner/webhooks/slo-breach", HandleSloBreachWebhook) .WithName("SloBreachWebhook") diff --git a/src/Timeline/StellaOps.Timeline.WebService/Endpoints/ExportEndpoints.cs b/src/Timeline/StellaOps.Timeline.WebService/Endpoints/ExportEndpoints.cs index c87a08c45..2dda02d70 100644 --- a/src/Timeline/StellaOps.Timeline.WebService/Endpoints/ExportEndpoints.cs +++ b/src/Timeline/StellaOps.Timeline.WebService/Endpoints/ExportEndpoints.cs @@ -5,6 +5,7 @@ using StellaOps.Timeline.Core; using StellaOps.Timeline.Core.Export; using StellaOps.HybridLogicalClock; using StellaOps.Timeline.WebService.Security; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.Timeline.WebService.Endpoints; @@ -20,7 +21,8 @@ public static class ExportEndpoints { var group = app.MapGroup("/api/v1/timeline") .WithTags("Export") - .RequireAuthorization(TimelinePolicies.Write); + .RequireAuthorization(TimelinePolicies.Write) + .RequireTenant(); group.MapPost("/{correlationId}/export", ExportTimelineAsync) .WithName("ExportTimeline") diff --git a/src/Timeline/StellaOps.Timeline.WebService/Endpoints/ReplayEndpoints.cs b/src/Timeline/StellaOps.Timeline.WebService/Endpoints/ReplayEndpoints.cs index e50616e5a..7c9f946b5 100644 --- a/src/Timeline/StellaOps.Timeline.WebService/Endpoints/ReplayEndpoints.cs +++ b/src/Timeline/StellaOps.Timeline.WebService/Endpoints/ReplayEndpoints.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http.HttpResults; using StellaOps.HybridLogicalClock; using StellaOps.Timeline.Core.Replay; using StellaOps.Timeline.WebService.Security; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.Timeline.WebService.Endpoints; @@ -20,7 +21,8 @@ public static class ReplayEndpoints { var group = app.MapGroup("/api/v1/timeline") .WithTags("Replay") - .RequireAuthorization(TimelinePolicies.Write); + .RequireAuthorization(TimelinePolicies.Write) + .RequireTenant(); group.MapPost("/{correlationId}/replay", InitiateReplayAsync) .WithName("InitiateReplay") diff --git a/src/Timeline/StellaOps.Timeline.WebService/Endpoints/TimelineEndpoints.cs b/src/Timeline/StellaOps.Timeline.WebService/Endpoints/TimelineEndpoints.cs index 71421d91d..38b5d8b62 100644 --- a/src/Timeline/StellaOps.Timeline.WebService/Endpoints/TimelineEndpoints.cs +++ b/src/Timeline/StellaOps.Timeline.WebService/Endpoints/TimelineEndpoints.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http.HttpResults; using StellaOps.HybridLogicalClock; using StellaOps.Timeline.Core; using StellaOps.Timeline.WebService.Security; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.Timeline.WebService.Endpoints; @@ -19,7 +20,8 @@ public static class TimelineEndpoints { var group = app.MapGroup("/api/v1/timeline") .WithTags("Timeline") - .RequireAuthorization(TimelinePolicies.Read); + .RequireAuthorization(TimelinePolicies.Read) + .RequireTenant(); group.MapGet("/{correlationId}", GetTimelineAsync) .WithName("GetTimeline") diff --git a/src/Timeline/StellaOps.Timeline.WebService/Program.cs b/src/Timeline/StellaOps.Timeline.WebService/Program.cs index edd3922ea..6e5c84954 100644 --- a/src/Timeline/StellaOps.Timeline.WebService/Program.cs +++ b/src/Timeline/StellaOps.Timeline.WebService/Program.cs @@ -1,5 +1,6 @@ using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Eventing; using StellaOps.Router.AspNet; using StellaOps.Timeline.Core; @@ -28,6 +29,7 @@ builder.Services.AddHealthChecks() // Authentication and authorization builder.Services.AddStellaOpsResourceServerAuthentication(builder.Configuration); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddAuthorization(options => { options.AddStellaOpsScopePolicy(TimelinePolicies.Read, StellaOpsScopes.TimelineRead); @@ -57,6 +59,7 @@ if (app.Environment.IsDevelopment()) app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); // Map endpoints diff --git a/src/TimelineIndexer/StellaOps.TimelineIndexer/StellaOps.TimelineIndexer.WebService/Program.cs b/src/TimelineIndexer/StellaOps.TimelineIndexer/StellaOps.TimelineIndexer.WebService/Program.cs index af441f1c5..95e93e9c3 100644 --- a/src/TimelineIndexer/StellaOps.TimelineIndexer/StellaOps.TimelineIndexer.WebService/Program.cs +++ b/src/TimelineIndexer/StellaOps.TimelineIndexer/StellaOps.TimelineIndexer.WebService/Program.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Cryptography.Audit; using StellaOps.Router.AspNet; using StellaOps.TimelineIndexer.Core.Abstractions; @@ -39,6 +40,7 @@ builder.Services.AddAuthorization(options => builder.Services.AddOpenApi(); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); // Stella Router integration @@ -60,10 +62,11 @@ if (app.Environment.IsDevelopment()) app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); -MapTimelineEndpoints(app.MapGroup("/api/v1"), routeNamePrefix: "timeline_api_v1"); -MapTimelineEndpoints(app.MapGroup(string.Empty), routeNamePrefix: "timeline"); +MapTimelineEndpoints(app.MapGroup("/api/v1").RequireTenant(), routeNamePrefix: "timeline_api_v1"); +MapTimelineEndpoints(app.MapGroup(string.Empty).RequireTenant(), routeNamePrefix: "timeline"); // Refresh Router endpoint cache app.TryRefreshStellaRouterEndpoints(routerEnabled); diff --git a/src/Unknowns/StellaOps.Unknowns.WebService/Endpoints/GreyQueueEndpoints.cs b/src/Unknowns/StellaOps.Unknowns.WebService/Endpoints/GreyQueueEndpoints.cs index d9204d2e6..a1ed233e1 100644 --- a/src/Unknowns/StellaOps.Unknowns.WebService/Endpoints/GreyQueueEndpoints.cs +++ b/src/Unknowns/StellaOps.Unknowns.WebService/Endpoints/GreyQueueEndpoints.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc; using StellaOps.Unknowns.Core.Models; using StellaOps.Unknowns.Core.Repositories; using StellaOps.Unknowns.WebService.Security; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.Unknowns.WebService.Endpoints; @@ -24,7 +25,8 @@ public static class GreyQueueEndpoints { var group = routes.MapGroup("/api/grey-queue") .WithTags("GreyQueue") - .RequireAuthorization(UnknownsPolicies.Read); + .RequireAuthorization(UnknownsPolicies.Read) + .RequireTenant(); // List and query group.MapGet("/", ListEntries) diff --git a/src/Unknowns/StellaOps.Unknowns.WebService/Endpoints/UnknownsEndpoints.cs b/src/Unknowns/StellaOps.Unknowns.WebService/Endpoints/UnknownsEndpoints.cs index 0e87a8569..5bcd340b6 100644 --- a/src/Unknowns/StellaOps.Unknowns.WebService/Endpoints/UnknownsEndpoints.cs +++ b/src/Unknowns/StellaOps.Unknowns.WebService/Endpoints/UnknownsEndpoints.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc; using StellaOps.Unknowns.Core.Models; using StellaOps.Unknowns.Core.Repositories; using StellaOps.Unknowns.WebService.Security; +using StellaOps.Auth.ServerIntegration.Tenancy; namespace StellaOps.Unknowns.WebService.Endpoints; @@ -25,7 +26,8 @@ public static class UnknownsEndpoints { var group = routes.MapGroup("/api/unknowns") .WithTags("Unknowns") - .RequireAuthorization(UnknownsPolicies.Read); + .RequireAuthorization(UnknownsPolicies.Read) + .RequireTenant(); // WS-004: GET /api/unknowns - List with pagination group.MapGet("/", ListUnknowns) diff --git a/src/Unknowns/StellaOps.Unknowns.WebService/Program.cs b/src/Unknowns/StellaOps.Unknowns.WebService/Program.cs index e233c1e63..9899d4189 100644 --- a/src/Unknowns/StellaOps.Unknowns.WebService/Program.cs +++ b/src/Unknowns/StellaOps.Unknowns.WebService/Program.cs @@ -7,6 +7,7 @@ using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Router.AspNet; using StellaOps.Unknowns.WebService; using StellaOps.Unknowns.WebService.Endpoints; @@ -27,6 +28,7 @@ builder.Services.AddHealthChecks() // Authentication and authorization builder.Services.AddStellaOpsResourceServerAuthentication(builder.Configuration); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddAuthorization(options => { options.AddStellaOpsScopePolicy(UnknownsPolicies.Read, StellaOpsScopes.UnknownsRead); @@ -55,6 +57,7 @@ if (app.Environment.IsDevelopment()) app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); // Map endpoints diff --git a/src/VexHub/StellaOps.VexHub.WebService/Extensions/VexHubEndpointExtensions.cs b/src/VexHub/StellaOps.VexHub.WebService/Extensions/VexHubEndpointExtensions.cs index 73bc74580..36fd198f0 100644 --- a/src/VexHub/StellaOps.VexHub.WebService/Extensions/VexHubEndpointExtensions.cs +++ b/src/VexHub/StellaOps.VexHub.WebService/Extensions/VexHubEndpointExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.VexHub.Core; using StellaOps.VexHub.Core.Export; using StellaOps.VexHub.Core.Models; @@ -23,7 +24,8 @@ public static class VexHubEndpointExtensions { var vexGroup = app.MapGroup("/api/v1/vex") .WithTags("VEX") - .RequireAuthorization(VexHubPolicies.Read); + .RequireAuthorization(VexHubPolicies.Read) + .RequireTenant(); // GET /api/v1/vex/cve/{cve-id} vexGroup.MapGet("/cve/{cveId}", GetByCve) diff --git a/src/VexHub/StellaOps.VexHub.WebService/Program.cs b/src/VexHub/StellaOps.VexHub.WebService/Program.cs index fdd532af5..1f58bc1f4 100644 --- a/src/VexHub/StellaOps.VexHub.WebService/Program.cs +++ b/src/VexHub/StellaOps.VexHub.WebService/Program.cs @@ -1,6 +1,7 @@ using Serilog; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Router.AspNet; using StellaOps.VexHub.Core.Extensions; using StellaOps.VexHub.Persistence.Extensions; @@ -44,6 +45,7 @@ builder.Services.AddAuthentication("ApiKey") } }); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddAuthorization(options => { // VexHub uses API-key authentication; policies require an authenticated API key holder. @@ -82,6 +84,7 @@ app.UseVexHubRateLimiting(); app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); // Map API endpoints diff --git a/src/VexLens/StellaOps.VexLens.WebService/Program.cs b/src/VexLens/StellaOps.VexLens.WebService/Program.cs index 7742be062..f747dd3fd 100644 --- a/src/VexLens/StellaOps.VexLens.WebService/Program.cs +++ b/src/VexLens/StellaOps.VexLens.WebService/Program.cs @@ -5,6 +5,7 @@ using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Serilog; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.VexLens.Api; using StellaOps.VexLens.Consensus; using StellaOps.VexLens.Persistence; @@ -71,6 +72,7 @@ builder.Services.AddRateLimiter(options => }); }); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration); // RASD-03: Register scope-based authorization policies for VexLens endpoints. @@ -100,6 +102,7 @@ if (app.Environment.IsDevelopment()) app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); app.UseRateLimiter(); app.UseSerilogRequestLogging(); diff --git a/src/VulnExplorer/StellaOps.VulnExplorer.Api/Program.cs b/src/VulnExplorer/StellaOps.VulnExplorer.Api/Program.cs index e25c688a4..a2471b3f2 100644 --- a/src/VulnExplorer/StellaOps.VulnExplorer.Api/Program.cs +++ b/src/VulnExplorer/StellaOps.VulnExplorer.Api/Program.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; +using StellaOps.Auth.ServerIntegration.Tenancy; using Microsoft.AspNetCore.OpenApi; using Microsoft.Extensions.DependencyInjection; using StellaOps.VulnExplorer.Api.Data; @@ -36,6 +37,7 @@ builder.Services.AddSingleton(); // Authentication and authorization builder.Services.AddStellaOpsResourceServerAuthentication(builder.Configuration); +builder.Services.AddStellaOpsTenantServices(); builder.Services.AddAuthorization(options => { options.AddStellaOpsScopePolicy(VulnExplorerPolicies.View, StellaOpsScopes.VulnView); @@ -62,6 +64,7 @@ app.UseSwaggerUI(); app.UseStellaOpsCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseStellaOpsTenantMiddleware(); app.TryUseStellaRouter(routerEnabled); app.MapGet("/v1/vulns", ([AsParameters] VulnFilter filter) => @@ -84,7 +87,8 @@ app.MapGet("/v1/vulns", ([AsParameters] VulnFilter filter) => }) .WithName("ListVulns") .WithDescription("Returns a paginated list of vulnerability summaries for the tenant, optionally filtered by CVE IDs, PURLs, severity levels, exploitability, and fix availability. Results are ordered by score descending then ID ascending. Requires x-stella-tenant header.") -.RequireAuthorization(VulnExplorerPolicies.View); +.RequireAuthorization(VulnExplorerPolicies.View) +.RequireTenant(); app.MapGet("/v1/vulns/{id}", ([FromHeader(Name = "x-stella-tenant")] string? tenant, string id) => { @@ -99,7 +103,8 @@ app.MapGet("/v1/vulns/{id}", ([FromHeader(Name = "x-stella-tenant")] string? ten }) .WithName("GetVuln") .WithDescription("Returns the full vulnerability detail record for a specific vulnerability ID including CVE IDs, affected components, severity score, exploitability assessment, and fix availability. Returns 404 if not found. Requires x-stella-tenant header.") -.RequireAuthorization(VulnExplorerPolicies.View); +.RequireAuthorization(VulnExplorerPolicies.View) +.RequireTenant(); // ============================================================================ // VEX Decision Endpoints (API-VEX-06-001, API-VEX-06-002, API-VEX-06-003) @@ -150,7 +155,8 @@ app.MapPost("/v1/vex-decisions", async ( }) .WithName("CreateVexDecision") .WithDescription("Creates a new VEX decision record for a vulnerability and subject artifact, recording the analyst verdict, justification, and optional attestation options. Optionally creates a signed VEX attestation if attestationOptions.createAttestation is true. Returns 201 Created with the VEX decision. Requires x-stella-tenant, x-stella-user-id, and x-stella-user-name headers.") -.RequireAuthorization(VulnExplorerPolicies.Operate); +.RequireAuthorization(VulnExplorerPolicies.Operate) +.RequireTenant(); app.MapPatch("/v1/vex-decisions/{id:guid}", ( [FromHeader(Name = "x-stella-tenant")] string? tenant, @@ -170,7 +176,8 @@ app.MapPatch("/v1/vex-decisions/{id:guid}", ( }) .WithName("UpdateVexDecision") .WithDescription("Partially updates an existing VEX decision record by ID, allowing the analyst to revise the status, justification, or other mutable fields. Returns 200 with the updated decision or 404 if the decision is not found. Requires x-stella-tenant header.") -.RequireAuthorization(VulnExplorerPolicies.Operate); +.RequireAuthorization(VulnExplorerPolicies.Operate) +.RequireTenant(); app.MapGet("/v1/vex-decisions", ([AsParameters] VexDecisionFilter filter, VexDecisionStore store) => { @@ -196,7 +203,8 @@ app.MapGet("/v1/vex-decisions", ([AsParameters] VexDecisionFilter filter, VexDec }) .WithName("ListVexDecisions") .WithDescription("Returns a paginated list of VEX decisions for the tenant, optionally filtered by vulnerability ID, subject artifact name, and decision status. Results are returned in stable order with a page token for continuation. Requires x-stella-tenant header.") -.RequireAuthorization(VulnExplorerPolicies.View); +.RequireAuthorization(VulnExplorerPolicies.View) +.RequireTenant(); app.MapGet("/v1/vex-decisions/{id:guid}", ( [FromHeader(Name = "x-stella-tenant")] string? tenant, @@ -215,7 +223,8 @@ app.MapGet("/v1/vex-decisions/{id:guid}", ( }) .WithName("GetVexDecision") .WithDescription("Returns the full VEX decision record for a specific decision ID including vulnerability reference, subject artifact, analyst verdict, justification, timestamps, and attestation reference if present. Returns 404 if the decision is not found. Requires x-stella-tenant header.") -.RequireAuthorization(VulnExplorerPolicies.View); +.RequireAuthorization(VulnExplorerPolicies.View) +.RequireTenant(); app.MapGet("/v1/evidence-subgraph/{vulnId}", ( [FromHeader(Name = "x-stella-tenant")] string? tenant, @@ -237,7 +246,8 @@ app.MapGet("/v1/evidence-subgraph/{vulnId}", ( }) .WithName("GetEvidenceSubgraph") .WithDescription("Returns the evidence subgraph for a specific vulnerability ID, linking together all related VEX decisions, fix verifications, audit bundles, and attestations that form the traceability chain for the vulnerability disposition. Requires x-stella-tenant header.") -.RequireAuthorization(VulnExplorerPolicies.View); +.RequireAuthorization(VulnExplorerPolicies.View) +.RequireTenant(); app.MapPost("/v1/fix-verifications", ( [FromHeader(Name = "x-stella-tenant")] string? tenant, @@ -259,7 +269,8 @@ app.MapPost("/v1/fix-verifications", ( }) .WithName("CreateFixVerification") .WithDescription("Creates a new fix verification record linking a CVE ID to a component PURL to track the verification status of an applied fix. Returns 201 Created with the verification record. Requires x-stella-tenant header and both cveId and componentPurl in the request body.") -.RequireAuthorization(VulnExplorerPolicies.Operate); +.RequireAuthorization(VulnExplorerPolicies.Operate) +.RequireTenant(); app.MapPatch("/v1/fix-verifications/{cveId}", ( [FromHeader(Name = "x-stella-tenant")] string? tenant, @@ -284,7 +295,8 @@ app.MapPatch("/v1/fix-verifications/{cveId}", ( }) .WithName("UpdateFixVerification") .WithDescription("Updates the verdict for an existing fix verification record, recording the confirmed verification outcome for a CVE fix. Returns 200 with the updated record or 404 if the fix verification is not found. Requires x-stella-tenant header and verdict in the request body.") -.RequireAuthorization(VulnExplorerPolicies.Operate); +.RequireAuthorization(VulnExplorerPolicies.Operate) +.RequireTenant(); app.MapPost("/v1/audit-bundles", ( [FromHeader(Name = "x-stella-tenant")] string? tenant, @@ -318,7 +330,8 @@ app.MapPost("/v1/audit-bundles", ( }) .WithName("CreateAuditBundle") .WithDescription("Creates an immutable audit bundle aggregating a set of VEX decisions by their IDs into a single exportable evidence record for compliance and audit purposes. Returns 201 Created with the bundle ID and included decisions. Returns 404 if none of the requested decision IDs are found. Requires x-stella-tenant header.") -.RequireAuthorization(VulnExplorerPolicies.Audit); +.RequireAuthorization(VulnExplorerPolicies.Audit) +.RequireTenant(); app.TryRefreshStellaRouterEndpoints(routerEnabled); app.Run();