Widen scratch iteration 011 with fixture-backed integrations QA
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
using static StellaOps.Localization.T;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration.Tenancy;
|
||||
using StellaOps.Integrations.Contracts;
|
||||
using StellaOps.Integrations.Contracts.AiCodeGuard;
|
||||
@@ -59,10 +61,11 @@ public static class IntegrationEndpoints
|
||||
// Get integration by ID
|
||||
group.MapGet("/{id:guid}", async (
|
||||
[FromServices] IntegrationService service,
|
||||
[FromServices] IStellaOpsTenantAccessor tenantAccessor,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await service.GetByIdAsync(id, cancellationToken);
|
||||
var result = await service.GetByIdAsync(id, tenantAccessor.TenantId, cancellationToken);
|
||||
return result is null ? Results.NotFound() : Results.Ok(result);
|
||||
})
|
||||
.RequireAuthorization(IntegrationPolicies.Read)
|
||||
@@ -73,10 +76,12 @@ public static class IntegrationEndpoints
|
||||
group.MapPost("/", async (
|
||||
[FromServices] IntegrationService service,
|
||||
[FromServices] IStellaOpsTenantAccessor tenantAccessor,
|
||||
HttpContext httpContext,
|
||||
[FromBody] CreateIntegrationRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await service.CreateAsync(request, tenantAccessor.TenantId, null, cancellationToken);
|
||||
var actorId = ResolveActorId(httpContext);
|
||||
var result = await service.CreateAsync(request, tenantAccessor.TenantId, actorId, cancellationToken);
|
||||
return Results.Created($"/api/v1/integrations/{result.Id}", result);
|
||||
})
|
||||
.RequireAuthorization(IntegrationPolicies.Write)
|
||||
@@ -87,11 +92,13 @@ public static class IntegrationEndpoints
|
||||
group.MapPut("/{id:guid}", async (
|
||||
[FromServices] IntegrationService service,
|
||||
[FromServices] IStellaOpsTenantAccessor tenantAccessor,
|
||||
HttpContext httpContext,
|
||||
Guid id,
|
||||
[FromBody] UpdateIntegrationRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await service.UpdateAsync(id, request, tenantAccessor.TenantId, cancellationToken);
|
||||
var actorId = ResolveActorId(httpContext);
|
||||
var result = await service.UpdateAsync(id, request, tenantAccessor.TenantId, actorId, cancellationToken);
|
||||
return result is null ? Results.NotFound() : Results.Ok(result);
|
||||
})
|
||||
.RequireAuthorization(IntegrationPolicies.Write)
|
||||
@@ -102,10 +109,12 @@ public static class IntegrationEndpoints
|
||||
group.MapDelete("/{id:guid}", async (
|
||||
[FromServices] IntegrationService service,
|
||||
[FromServices] IStellaOpsTenantAccessor tenantAccessor,
|
||||
HttpContext httpContext,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await service.DeleteAsync(id, tenantAccessor.TenantId, cancellationToken);
|
||||
var actorId = ResolveActorId(httpContext);
|
||||
var result = await service.DeleteAsync(id, tenantAccessor.TenantId, actorId, cancellationToken);
|
||||
return result ? Results.NoContent() : Results.NotFound();
|
||||
})
|
||||
.RequireAuthorization(IntegrationPolicies.Write)
|
||||
@@ -116,10 +125,12 @@ public static class IntegrationEndpoints
|
||||
group.MapPost("/{id:guid}/test", async (
|
||||
[FromServices] IntegrationService service,
|
||||
[FromServices] IStellaOpsTenantAccessor tenantAccessor,
|
||||
HttpContext httpContext,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await service.TestConnectionAsync(id, tenantAccessor.TenantId, cancellationToken);
|
||||
var actorId = ResolveActorId(httpContext);
|
||||
var result = await service.TestConnectionAsync(id, tenantAccessor.TenantId, actorId, cancellationToken);
|
||||
return result is null ? Results.NotFound() : Results.Ok(result);
|
||||
})
|
||||
.RequireAuthorization(IntegrationPolicies.Operate)
|
||||
@@ -129,10 +140,11 @@ public static class IntegrationEndpoints
|
||||
// Health check
|
||||
group.MapGet("/{id:guid}/health", async (
|
||||
[FromServices] IntegrationService service,
|
||||
[FromServices] IStellaOpsTenantAccessor tenantAccessor,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await service.CheckHealthAsync(id, cancellationToken);
|
||||
var result = await service.CheckHealthAsync(id, tenantAccessor.TenantId, cancellationToken);
|
||||
return result is null ? Results.NotFound() : Results.Ok(result);
|
||||
})
|
||||
.RequireAuthorization(IntegrationPolicies.Read)
|
||||
@@ -142,10 +154,11 @@ public static class IntegrationEndpoints
|
||||
// Impact map
|
||||
group.MapGet("/{id:guid}/impact", async (
|
||||
[FromServices] IntegrationService service,
|
||||
[FromServices] IStellaOpsTenantAccessor tenantAccessor,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await service.GetImpactAsync(id, cancellationToken);
|
||||
var result = await service.GetImpactAsync(id, tenantAccessor.TenantId, cancellationToken);
|
||||
return result is null ? Results.NotFound() : Results.Ok(result);
|
||||
})
|
||||
.RequireAuthorization(IntegrationPolicies.Read)
|
||||
@@ -162,4 +175,12 @@ public static class IntegrationEndpoints
|
||||
.WithName("GetSupportedProviders")
|
||||
.WithDescription(_t("integrations.integration.get_providers_description"));
|
||||
}
|
||||
|
||||
private static string? ResolveActorId(HttpContext httpContext)
|
||||
{
|
||||
return httpContext.User.FindFirst(StellaOpsClaimTypes.Subject)?.Value
|
||||
?? httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
|
||||
?? httpContext.User.FindFirst("sub")?.Value
|
||||
?? httpContext.User.Identity?.Name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class IntegrationService
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IntegrationResponse> CreateAsync(CreateIntegrationRequest request, string? userId, string? tenantId, CancellationToken cancellationToken = default)
|
||||
public async Task<IntegrationResponse> CreateAsync(CreateIntegrationRequest request, string? tenantId, string? userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var integration = new Integration
|
||||
@@ -78,9 +78,9 @@ public sealed class IntegrationService
|
||||
return MapToResponse(created);
|
||||
}
|
||||
|
||||
public async Task<IntegrationResponse?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
public async Task<IntegrationResponse?> GetByIdAsync(Guid id, string? tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var integration = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
var integration = await GetScopedIntegrationAsync(id, tenantId, cancellationToken);
|
||||
return integration is null ? null : MapToResponse(integration);
|
||||
}
|
||||
|
||||
@@ -110,9 +110,9 @@ public sealed class IntegrationService
|
||||
totalPages);
|
||||
}
|
||||
|
||||
public async Task<IntegrationResponse?> UpdateAsync(Guid id, UpdateIntegrationRequest request, string? userId, CancellationToken cancellationToken = default)
|
||||
public async Task<IntegrationResponse?> UpdateAsync(Guid id, UpdateIntegrationRequest request, string? tenantId, string? userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var integration = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
var integration = await GetScopedIntegrationAsync(id, tenantId, cancellationToken);
|
||||
if (integration is null) return null;
|
||||
|
||||
var oldStatus = integration.Status;
|
||||
@@ -153,9 +153,9 @@ public sealed class IntegrationService
|
||||
return MapToResponse(updated);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id, string? userId, CancellationToken cancellationToken = default)
|
||||
public async Task<bool> DeleteAsync(Guid id, string? tenantId, string? userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var integration = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
var integration = await GetScopedIntegrationAsync(id, tenantId, cancellationToken);
|
||||
if (integration is null) return false;
|
||||
|
||||
await _repository.DeleteAsync(id, cancellationToken);
|
||||
@@ -172,9 +172,9 @@ public sealed class IntegrationService
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<TestConnectionResponse?> TestConnectionAsync(Guid id, string? userId, CancellationToken cancellationToken = default)
|
||||
public async Task<TestConnectionResponse?> TestConnectionAsync(Guid id, string? tenantId, string? userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var integration = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
var integration = await GetScopedIntegrationAsync(id, tenantId, cancellationToken);
|
||||
if (integration is null) return null;
|
||||
|
||||
var plugin = _pluginLoader.GetByProvider(integration.Provider);
|
||||
@@ -227,9 +227,9 @@ public sealed class IntegrationService
|
||||
endTime);
|
||||
}
|
||||
|
||||
public async Task<HealthCheckResponse?> CheckHealthAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
public async Task<HealthCheckResponse?> CheckHealthAsync(Guid id, string? tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var integration = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
var integration = await GetScopedIntegrationAsync(id, tenantId, cancellationToken);
|
||||
if (integration is null) return null;
|
||||
|
||||
var plugin = _pluginLoader.GetByProvider(integration.Provider);
|
||||
@@ -269,9 +269,9 @@ public sealed class IntegrationService
|
||||
result.Duration);
|
||||
}
|
||||
|
||||
public async Task<IntegrationImpactResponse?> GetImpactAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
public async Task<IntegrationImpactResponse?> GetImpactAsync(Guid id, string? tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var integration = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
var integration = await GetScopedIntegrationAsync(id, tenantId, cancellationToken);
|
||||
if (integration is null)
|
||||
{
|
||||
return null;
|
||||
@@ -302,6 +302,27 @@ public sealed class IntegrationService
|
||||
p.Provider)).ToList();
|
||||
}
|
||||
|
||||
private async Task<Integration?> GetScopedIntegrationAsync(Guid id, string? tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var integration = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
if (integration is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.Equals(integration.TenantId, tenantId, StringComparison.Ordinal))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Integration {IntegrationId} was requested outside its tenant scope. requestedTenant={RequestedTenant} actualTenant={ActualTenant}",
|
||||
id,
|
||||
tenantId,
|
||||
integration.TenantId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return integration;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ImpactedWorkflow> BuildImpactedWorkflows(Integration integration)
|
||||
{
|
||||
var blockedByStatus = integration.Status is IntegrationStatus.Failed or IntegrationStatus.Disabled or IntegrationStatus.Archived;
|
||||
|
||||
Reference in New Issue
Block a user