up
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
namespace StellaOps.Policy.Storage.Postgres.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Entity representing an audit log entry for the policy module.
|
||||
/// </summary>
|
||||
public sealed class PolicyAuditEntity
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public required string TenantId { get; init; }
|
||||
public Guid? UserId { get; init; }
|
||||
public required string Action { get; init; }
|
||||
public required string ResourceType { get; init; }
|
||||
public string? ResourceId { get; init; }
|
||||
public string? OldValue { get; init; }
|
||||
public string? NewValue { get; init; }
|
||||
public string? CorrelationId { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for explanation operations.
|
||||
/// </summary>
|
||||
public sealed class ExplanationRepository : RepositoryBase<PolicyDataSource>, IExplanationRepository
|
||||
{
|
||||
public ExplanationRepository(PolicyDataSource dataSource, ILogger<ExplanationRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<ExplanationEntity?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, evaluation_run_id, rule_id, rule_name, result, severity, message, details, remediation, resource_path, line_number, created_at
|
||||
FROM policy.explanations WHERE id = @id
|
||||
""";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
return await reader.ReadAsync(cancellationToken).ConfigureAwait(false) ? MapExplanation(reader) : null;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ExplanationEntity>> GetByEvaluationRunIdAsync(Guid evaluationRunId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, evaluation_run_id, rule_id, rule_name, result, severity, message, details, remediation, resource_path, line_number, created_at
|
||||
FROM policy.explanations WHERE evaluation_run_id = @evaluation_run_id
|
||||
ORDER BY created_at
|
||||
""";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "evaluation_run_id", evaluationRunId);
|
||||
var results = new List<ExplanationEntity>();
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
results.Add(MapExplanation(reader));
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ExplanationEntity>> GetByEvaluationRunIdAndResultAsync(Guid evaluationRunId, RuleResult result, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, evaluation_run_id, rule_id, rule_name, result, severity, message, details, remediation, resource_path, line_number, created_at
|
||||
FROM policy.explanations WHERE evaluation_run_id = @evaluation_run_id AND result = @result
|
||||
ORDER BY severity DESC, created_at
|
||||
""";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "evaluation_run_id", evaluationRunId);
|
||||
AddParameter(command, "result", ResultToString(result));
|
||||
var results = new List<ExplanationEntity>();
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
results.Add(MapExplanation(reader));
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<ExplanationEntity> CreateAsync(ExplanationEntity explanation, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO policy.explanations (id, evaluation_run_id, rule_id, rule_name, result, severity, message, details, remediation, resource_path, line_number)
|
||||
VALUES (@id, @evaluation_run_id, @rule_id, @rule_name, @result, @severity, @message, @details::jsonb, @remediation, @resource_path, @line_number)
|
||||
RETURNING *
|
||||
""";
|
||||
var id = explanation.Id == Guid.Empty ? Guid.NewGuid() : explanation.Id;
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "evaluation_run_id", explanation.EvaluationRunId);
|
||||
AddParameter(command, "rule_id", explanation.RuleId);
|
||||
AddParameter(command, "rule_name", explanation.RuleName);
|
||||
AddParameter(command, "result", ResultToString(explanation.Result));
|
||||
AddParameter(command, "severity", explanation.Severity);
|
||||
AddParameter(command, "message", explanation.Message);
|
||||
AddJsonbParameter(command, "details", explanation.Details);
|
||||
AddParameter(command, "remediation", explanation.Remediation);
|
||||
AddParameter(command, "resource_path", explanation.ResourcePath);
|
||||
AddParameter(command, "line_number", explanation.LineNumber);
|
||||
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
return MapExplanation(reader);
|
||||
}
|
||||
|
||||
public async Task<int> CreateBatchAsync(IEnumerable<ExplanationEntity> explanations, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO policy.explanations (id, evaluation_run_id, rule_id, rule_name, result, severity, message, details, remediation, resource_path, line_number)
|
||||
VALUES (@id, @evaluation_run_id, @rule_id, @rule_name, @result, @severity, @message, @details::jsonb, @remediation, @resource_path, @line_number)
|
||||
""";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
var count = 0;
|
||||
foreach (var explanation in explanations)
|
||||
{
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
var id = explanation.Id == Guid.Empty ? Guid.NewGuid() : explanation.Id;
|
||||
AddParameter(command, "id", id);
|
||||
AddParameter(command, "evaluation_run_id", explanation.EvaluationRunId);
|
||||
AddParameter(command, "rule_id", explanation.RuleId);
|
||||
AddParameter(command, "rule_name", explanation.RuleName);
|
||||
AddParameter(command, "result", ResultToString(explanation.Result));
|
||||
AddParameter(command, "severity", explanation.Severity);
|
||||
AddParameter(command, "message", explanation.Message);
|
||||
AddJsonbParameter(command, "details", explanation.Details);
|
||||
AddParameter(command, "remediation", explanation.Remediation);
|
||||
AddParameter(command, "resource_path", explanation.ResourcePath);
|
||||
AddParameter(command, "line_number", explanation.LineNumber);
|
||||
count += await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteByEvaluationRunIdAsync(Guid evaluationRunId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM policy.explanations WHERE evaluation_run_id = @evaluation_run_id";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "evaluation_run_id", evaluationRunId);
|
||||
var rows = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
private static ExplanationEntity MapExplanation(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
EvaluationRunId = reader.GetGuid(1),
|
||||
RuleId = GetNullableGuid(reader, 2),
|
||||
RuleName = reader.GetString(3),
|
||||
Result = ParseResult(reader.GetString(4)),
|
||||
Severity = reader.GetString(5),
|
||||
Message = GetNullableString(reader, 6),
|
||||
Details = reader.GetString(7),
|
||||
Remediation = GetNullableString(reader, 8),
|
||||
ResourcePath = GetNullableString(reader, 9),
|
||||
LineNumber = reader.IsDBNull(10) ? null : reader.GetInt32(10),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(11)
|
||||
};
|
||||
|
||||
private static string ResultToString(RuleResult result) => result switch
|
||||
{
|
||||
RuleResult.Pass => "pass",
|
||||
RuleResult.Fail => "fail",
|
||||
RuleResult.Skip => "skip",
|
||||
RuleResult.Error => "error",
|
||||
_ => throw new ArgumentException($"Unknown result: {result}")
|
||||
};
|
||||
|
||||
private static RuleResult ParseResult(string result) => result switch
|
||||
{
|
||||
"pass" => RuleResult.Pass,
|
||||
"fail" => RuleResult.Fail,
|
||||
"skip" => RuleResult.Skip,
|
||||
"error" => RuleResult.Error,
|
||||
_ => throw new ArgumentException($"Unknown result: {result}")
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for explanation operations.
|
||||
/// </summary>
|
||||
public interface IExplanationRepository
|
||||
{
|
||||
Task<ExplanationEntity?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<ExplanationEntity>> GetByEvaluationRunIdAsync(Guid evaluationRunId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<ExplanationEntity>> GetByEvaluationRunIdAndResultAsync(Guid evaluationRunId, RuleResult result, CancellationToken cancellationToken = default);
|
||||
Task<ExplanationEntity> CreateAsync(ExplanationEntity explanation, CancellationToken cancellationToken = default);
|
||||
Task<int> CreateBatchAsync(IEnumerable<ExplanationEntity> explanations, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteByEvaluationRunIdAsync(Guid evaluationRunId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for policy audit operations.
|
||||
/// </summary>
|
||||
public interface IPolicyAuditRepository
|
||||
{
|
||||
Task<long> CreateAsync(PolicyAuditEntity audit, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<PolicyAuditEntity>> ListAsync(string tenantId, int limit = 100, int offset = 0, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<PolicyAuditEntity>> GetByResourceAsync(string tenantId, string resourceType, string? resourceId = null, int limit = 100, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<PolicyAuditEntity>> GetByCorrelationIdAsync(string tenantId, string correlationId, CancellationToken cancellationToken = default);
|
||||
Task<int> DeleteOldAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Policy.Storage.Postgres.Models;
|
||||
|
||||
namespace StellaOps.Policy.Storage.Postgres.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL repository for policy audit operations.
|
||||
/// </summary>
|
||||
public sealed class PolicyAuditRepository : RepositoryBase<PolicyDataSource>, IPolicyAuditRepository
|
||||
{
|
||||
public PolicyAuditRepository(PolicyDataSource dataSource, ILogger<PolicyAuditRepository> logger)
|
||||
: base(dataSource, logger) { }
|
||||
|
||||
public async Task<long> CreateAsync(PolicyAuditEntity audit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO policy.audit (tenant_id, user_id, action, resource_type, resource_id, old_value, new_value, correlation_id)
|
||||
VALUES (@tenant_id, @user_id, @action, @resource_type, @resource_id, @old_value::jsonb, @new_value::jsonb, @correlation_id)
|
||||
RETURNING id
|
||||
""";
|
||||
await using var connection = await DataSource.OpenConnectionAsync(audit.TenantId, "writer", cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "tenant_id", audit.TenantId);
|
||||
AddParameter(command, "user_id", audit.UserId);
|
||||
AddParameter(command, "action", audit.Action);
|
||||
AddParameter(command, "resource_type", audit.ResourceType);
|
||||
AddParameter(command, "resource_id", audit.ResourceId);
|
||||
AddJsonbParameter(command, "old_value", audit.OldValue);
|
||||
AddJsonbParameter(command, "new_value", audit.NewValue);
|
||||
AddParameter(command, "correlation_id", audit.CorrelationId);
|
||||
|
||||
var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
|
||||
return (long)result!;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PolicyAuditEntity>> ListAsync(string tenantId, int limit = 100, int offset = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, user_id, action, resource_type, resource_id, old_value, new_value, correlation_id, created_at
|
||||
FROM policy.audit WHERE tenant_id = @tenant_id
|
||||
ORDER BY created_at DESC LIMIT @limit OFFSET @offset
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
AddParameter(cmd, "offset", offset);
|
||||
}, MapAudit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PolicyAuditEntity>> GetByResourceAsync(string tenantId, string resourceType, string? resourceId = null, int limit = 100, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = """
|
||||
SELECT id, tenant_id, user_id, action, resource_type, resource_id, old_value, new_value, correlation_id, created_at
|
||||
FROM policy.audit WHERE tenant_id = @tenant_id AND resource_type = @resource_type
|
||||
""";
|
||||
if (resourceId != null) sql += " AND resource_id = @resource_id";
|
||||
sql += " ORDER BY created_at DESC LIMIT @limit";
|
||||
|
||||
return await QueryAsync(tenantId, sql, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "resource_type", resourceType);
|
||||
if (resourceId != null) AddParameter(cmd, "resource_id", resourceId);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
}, MapAudit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PolicyAuditEntity>> GetByCorrelationIdAsync(string tenantId, string correlationId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, tenant_id, user_id, action, resource_type, resource_id, old_value, new_value, correlation_id, created_at
|
||||
FROM policy.audit WHERE tenant_id = @tenant_id AND correlation_id = @correlation_id
|
||||
ORDER BY created_at
|
||||
""";
|
||||
return await QueryAsync(tenantId, sql,
|
||||
cmd => { AddParameter(cmd, "tenant_id", tenantId); AddParameter(cmd, "correlation_id", correlationId); },
|
||||
MapAudit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<int> DeleteOldAsync(DateTimeOffset cutoff, CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "DELETE FROM policy.audit WHERE created_at < @cutoff";
|
||||
await using var connection = await DataSource.OpenSystemConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var command = CreateCommand(sql, connection);
|
||||
AddParameter(command, "cutoff", cutoff);
|
||||
return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static PolicyAuditEntity MapAudit(NpgsqlDataReader reader) => new()
|
||||
{
|
||||
Id = reader.GetInt64(0),
|
||||
TenantId = reader.GetString(1),
|
||||
UserId = GetNullableGuid(reader, 2),
|
||||
Action = reader.GetString(3),
|
||||
ResourceType = reader.GetString(4),
|
||||
ResourceId = GetNullableString(reader, 5),
|
||||
OldValue = GetNullableString(reader, 6),
|
||||
NewValue = GetNullableString(reader, 7),
|
||||
CorrelationId = GetNullableString(reader, 8),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(9)
|
||||
};
|
||||
}
|
||||
@@ -35,6 +35,8 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IEvaluationRunRepository, EvaluationRunRepository>();
|
||||
services.AddScoped<IExceptionRepository, ExceptionRepository>();
|
||||
services.AddScoped<IReceiptRepository, PostgresReceiptRepository>();
|
||||
services.AddScoped<IExplanationRepository, ExplanationRepository>();
|
||||
services.AddScoped<IPolicyAuditRepository, PolicyAuditRepository>();
|
||||
|
||||
return services;
|
||||
}
|
||||
@@ -60,6 +62,8 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IEvaluationRunRepository, EvaluationRunRepository>();
|
||||
services.AddScoped<IExceptionRepository, ExceptionRepository>();
|
||||
services.AddScoped<IReceiptRepository, PostgresReceiptRepository>();
|
||||
services.AddScoped<IExplanationRepository, ExplanationRepository>();
|
||||
services.AddScoped<IPolicyAuditRepository, PolicyAuditRepository>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user