feat: Introduce new Access Management and Governance APIs in IAM Service
- Added Access Requests, Access Reviews, Privileged Access Management, Audit Log, and Compliance APIs to enhance access management and governance capabilities. - Updated the DbContext to include new entities for AuditLog and ComplianceReport, improving data handling for compliance and auditing. - Enhanced Dependency Injection to support new repositories for the added functionalities, streamlining service operations.
This commit is contained in:
@@ -290,57 +290,72 @@ graph TD
|
||||
| `POST` | `/api/v1/verifications/email` | Yêu cầu xác thực email | ✅ |
|
||||
| `POST` | `/api/v1/verifications/{id}/confirm` | Xác nhận với OTP code | ✅ |
|
||||
|
||||
### 4.10 Access Management APIs (Planned)
|
||||
### 4.10 Access Requests APIs ✅ (New in Phase 3A)
|
||||
|
||||
| Method | Endpoint | Mô tả | Auth |
|
||||
|--------|----------|-------|------|
|
||||
| `POST` | `/api/v1/access-requests` | Tạo yêu cầu truy cập mới | ✅ |
|
||||
| `GET` | `/api/v1/access-requests` | Lấy danh sách requests của user | ✅ |
|
||||
| `GET` | `/api/v1/access-requests/{id}` | Lấy request theo ID | ✅ |
|
||||
| `POST` | `/api/v1/access-requests/{id}/submit` | Submit request để phê duyệt | ✅ |
|
||||
| `POST` | `/api/v1/access-requests/{id}/approve` | Phê duyệt request | ✅ |
|
||||
| `POST` | `/api/v1/access-requests/{id}/reject` | Từ chối request | ✅ |
|
||||
| `DELETE` | `/api/v1/access-requests/{id}` | Hủy request | ✅ |
|
||||
| `GET` | `/api/v1/access-requests/pending` | Lấy requests đang chờ phê duyệt | ✅ |
|
||||
|
||||
### 4.11 Access Reviews APIs ✅ (New in Phase 3B)
|
||||
|
||||
| Method | Endpoint | Mô tả | Auth |
|
||||
|--------|----------|-------|------|
|
||||
| `POST` | `/api/v1/access-reviews` | Tạo access review mới | ✅ |
|
||||
| `GET` | `/api/v1/access-reviews/{id}` | Lấy review theo ID | ✅ |
|
||||
| `POST` | `/api/v1/access-reviews/{id}/items` | Thêm item vào review | ✅ |
|
||||
| `POST` | `/api/v1/access-reviews/{id}/start` | Bắt đầu review campaign | ✅ |
|
||||
| `POST` | `/api/v1/access-reviews/{id}/items/{itemId}/review` | Certify/Revoke item | ✅ |
|
||||
| `POST` | `/api/v1/access-reviews/{id}/complete` | Hoàn thành review | ✅ |
|
||||
|
||||
### 4.12 Privileged Access Management (PAM) APIs ✅ (New in Phase 3B)
|
||||
|
||||
| Method | Endpoint | Mô tả | Auth |
|
||||
|--------|----------|-------|------|
|
||||
| `POST` | `/api/v1/privileged-access/request` | Yêu cầu JIT elevated access | ✅ |
|
||||
| `GET` | `/api/v1/privileged-access/active` | Lấy grants đang active | ✅ |
|
||||
| `POST` | `/api/v1/privileged-access/{id}/revoke` | Thu hồi privileged access | ✅ |
|
||||
|
||||
### 4.13 Audit Log APIs ✅ (New in Phase 4A)
|
||||
|
||||
| Method | Endpoint | Mô tả | Auth |
|
||||
|--------|----------|-------|------|
|
||||
| `GET` | `/api/v1/audit/logs` | Lấy audit logs (filtered, paginated) | ✅ |
|
||||
|
||||
### 4.14 Compliance APIs ✅ (New in Phase 4A)
|
||||
|
||||
| Method | Endpoint | Mô tả | Auth |
|
||||
|--------|----------|-------|------|
|
||||
| `POST` | `/api/v1/compliance/reports` | Tạo compliance report mới | ✅ |
|
||||
| `GET` | `/api/v1/compliance/reports` | Lấy danh sách reports | ✅ |
|
||||
| `GET` | `/api/v1/compliance/reports/{id}` | Lấy report chi tiết | ✅ |
|
||||
| `POST` | `/api/v1/compliance/reports/{id}/complete` | Hoàn thành report | ✅ |
|
||||
| `GET` | `/api/v1/compliance/violations` | Lấy violations chưa giải quyết | ✅ |
|
||||
|
||||
### 4.15 Governance APIs (Planned - Phase 4B)
|
||||
|
||||
> [!NOTE]
|
||||
> Các APIs dưới đây là tính năng **đang được lên kế hoạch**, chưa triển khai.
|
||||
> Các APIs dưới đây là tính năng **đang được lên kế hoạch** cho Phase 4B.
|
||||
|
||||
```
|
||||
# Access Requests
|
||||
GET /api/v1/access/requests
|
||||
POST /api/v1/access/requests
|
||||
PUT /api/v1/access/requests/:id/approve
|
||||
PUT /api/v1/access/requests/:id/reject
|
||||
|
||||
# Access Reviews
|
||||
GET /api/v1/access/reviews
|
||||
POST /api/v1/access/reviews
|
||||
POST /api/v1/access/reviews/:id/start
|
||||
POST /api/v1/access/reviews/:id/complete
|
||||
GET /api/v1/access/reviews/:id/items
|
||||
|
||||
# Access Analytics
|
||||
GET /api/v1/access/analytics/usage
|
||||
GET /api/v1/access/analytics/permissions
|
||||
GET /api/v1/access/analytics/risks
|
||||
```
|
||||
|
||||
### 4.8 Governance APIs (Planned)
|
||||
|
||||
> [!NOTE]
|
||||
> Các APIs dưới đây là tính năng **đang được lên kế hoạch**, chưa triển khai.
|
||||
|
||||
```
|
||||
# Compliance Reports
|
||||
GET /api/v1/governance/compliance/reports
|
||||
POST /api/v1/governance/compliance/reports/generate
|
||||
GET /api/v1/governance/compliance/reports/:id/export
|
||||
|
||||
# Policy Governance
|
||||
GET /api/v1/governance/policies/templates
|
||||
POST /api/v1/governance/policies/templates
|
||||
GET /api/v1/governance/policies/:id/versions
|
||||
POST /api/v1/governance/policies/:id/test
|
||||
GET /api/v1/policies
|
||||
POST /api/v1/policies
|
||||
GET /api/v1/policies/:id/versions
|
||||
POST /api/v1/policies/:id/activate
|
||||
|
||||
# Risk Management
|
||||
GET /api/v1/governance/risk/scores
|
||||
GET /api/v1/governance/risk/scores/:userId
|
||||
POST /api/v1/governance/risk/calculate
|
||||
GET /api/v1/risk/scores/:userId
|
||||
POST /api/v1/risk/calculate
|
||||
|
||||
# Reporting
|
||||
GET /api/v1/governance/reports/access-summary
|
||||
GET /api/v1/governance/reports/user-activity
|
||||
GET /api/v1/governance/reports/security-events
|
||||
# Security Dashboard
|
||||
GET /api/v1/dashboard/security
|
||||
```
|
||||
|
||||
---
|
||||
@@ -368,17 +383,24 @@ GET /api/v1/governance/reports/security-events
|
||||
- ✅ Organization & Group management
|
||||
- ✅ Profile management with extended attributes (ProfileAttribute entity)
|
||||
|
||||
### Phase 3: Access Management (Planned)
|
||||
- 🔄 Access request/approval workflows
|
||||
- 🔄 Access review & certification system
|
||||
- 🔄 Access analytics
|
||||
- 🔄 Privileged Access Management (PAM)
|
||||
### Phase 3: Access Management ✅ (Completed)
|
||||
- ✅ Access request/approval workflows (Create → Submit → Approve/Reject)
|
||||
- ✅ Access review & certification system (Certify/Revoke decisions)
|
||||
- ✅ Privileged Access Management (PAM) với JIT elevated access
|
||||
- ✅ Entity configurations với EF Core Value Conversion
|
||||
|
||||
### Phase 4: Governance (Planned)
|
||||
- 🔄 Compliance reporting engine
|
||||
- 🔄 Policy governance & versioning
|
||||
- 🔄 Risk scoring & management
|
||||
- 🔄 Reporting dashboards
|
||||
### Phase 4: Governance (In Progress)
|
||||
|
||||
#### Phase 4A: Audit & Compliance ✅ (Completed)
|
||||
- ✅ `AuditLog` aggregate với 18 event types
|
||||
- ✅ `ComplianceReport` aggregate (GDPR, SOC2, ISO27001, HIPAA)
|
||||
- ✅ Audit log search & filtering
|
||||
- ✅ Compliance report generation & violations tracking
|
||||
|
||||
#### Phase 4B: Policy & Risk (Planned)
|
||||
- 🔄 PolicyTemplate aggregate với versioning
|
||||
- 🔄 RiskScore aggregate & calculation
|
||||
- 🔄 Security posture dashboard
|
||||
|
||||
### Phase 5: Advanced Features (Planned)
|
||||
- 🔄 Workflow engine
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using MediatR;
|
||||
using IamService.Domain.AggregatesModel.AuditAggregate;
|
||||
|
||||
namespace IamService.API.Application.Commands.Audit;
|
||||
|
||||
public record CreateAuditLogCommand(
|
||||
int EventTypeId,
|
||||
string ResourceType,
|
||||
Guid? ActorId = null,
|
||||
string? ActorEmail = null,
|
||||
Guid? ResourceId = null,
|
||||
string? Action = null,
|
||||
string? Details = null,
|
||||
string? IpAddress = null,
|
||||
string? UserAgent = null,
|
||||
bool Success = true) : IRequest<Guid>;
|
||||
|
||||
public class CreateAuditLogCommandHandler : IRequestHandler<CreateAuditLogCommand, Guid>
|
||||
{
|
||||
private readonly IAuditLogRepository _repository;
|
||||
public CreateAuditLogCommandHandler(IAuditLogRepository repository) => _repository = repository;
|
||||
|
||||
public async Task<Guid> Handle(CreateAuditLogCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var eventType = AuditEventType.FromId(request.EventTypeId) ?? AuditEventType.Login;
|
||||
var log = AuditLog.Create(
|
||||
eventType, request.ResourceType, request.ActorId, request.ActorEmail,
|
||||
request.ResourceId, request.Action, request.Details, request.IpAddress, request.UserAgent, request.Success);
|
||||
_repository.Add(log);
|
||||
await _repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
return log.Id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using MediatR;
|
||||
using IamService.Domain.AggregatesModel.ComplianceAggregate;
|
||||
using IamService.Domain.Exceptions;
|
||||
|
||||
namespace IamService.API.Application.Commands.Compliance;
|
||||
|
||||
public record GenerateComplianceReportCommand(string Name, int ReportTypeId, Guid GeneratedByUserId) : IRequest<Guid>;
|
||||
public record CompleteComplianceReportCommand(Guid ReportId, int TotalChecks, int PassedChecks, string? Summary = null) : IRequest<Unit>;
|
||||
public record AddViolationCommand(Guid ReportId, string Rule, int SeverityId, string Description, string? Remediation = null) : IRequest<Guid>;
|
||||
|
||||
public class GenerateComplianceReportCommandHandler : IRequestHandler<GenerateComplianceReportCommand, Guid>
|
||||
{
|
||||
private readonly IComplianceReportRepository _repository;
|
||||
public GenerateComplianceReportCommandHandler(IComplianceReportRepository repository) => _repository = repository;
|
||||
|
||||
public async Task<Guid> Handle(GenerateComplianceReportCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var reportType = ComplianceReportType.FromId(request.ReportTypeId) ?? ComplianceReportType.GDPR;
|
||||
var report = ComplianceReport.Create(request.Name, reportType, request.GeneratedByUserId);
|
||||
report.StartGenerating();
|
||||
_repository.Add(report);
|
||||
await _repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
return report.Id;
|
||||
}
|
||||
}
|
||||
|
||||
public class CompleteComplianceReportCommandHandler : IRequestHandler<CompleteComplianceReportCommand, Unit>
|
||||
{
|
||||
private readonly IComplianceReportRepository _repository;
|
||||
public CompleteComplianceReportCommandHandler(IComplianceReportRepository repository) => _repository = repository;
|
||||
|
||||
public async Task<Unit> Handle(CompleteComplianceReportCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var report = await _repository.GetByIdWithViolationsAsync(request.ReportId, cancellationToken)
|
||||
?? throw new DomainException($"Report {request.ReportId} not found.");
|
||||
report.SetResults(request.TotalChecks, request.PassedChecks, request.Summary);
|
||||
report.Complete();
|
||||
_repository.Update(report);
|
||||
await _repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public class AddViolationCommandHandler : IRequestHandler<AddViolationCommand, Guid>
|
||||
{
|
||||
private readonly IComplianceReportRepository _repository;
|
||||
public AddViolationCommandHandler(IComplianceReportRepository repository) => _repository = repository;
|
||||
|
||||
public async Task<Guid> Handle(AddViolationCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var report = await _repository.GetByIdWithViolationsAsync(request.ReportId, cancellationToken)
|
||||
?? throw new DomainException($"Report {request.ReportId} not found.");
|
||||
var severity = ViolationSeverity.FromId(request.SeverityId) ?? ViolationSeverity.Medium;
|
||||
report.AddViolation(request.Rule, severity, request.Description, request.Remediation);
|
||||
_repository.Update(report);
|
||||
await _repository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
return report.Violations.Last().Id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using MediatR;
|
||||
using IamService.Domain.AggregatesModel.AuditAggregate;
|
||||
|
||||
namespace IamService.API.Application.Queries.Audit;
|
||||
|
||||
public record GetAuditLogsQuery(
|
||||
DateTime? FromDate = null,
|
||||
DateTime? ToDate = null,
|
||||
int? EventTypeId = null,
|
||||
Guid? ActorId = null,
|
||||
string? ResourceType = null,
|
||||
int Skip = 0,
|
||||
int Take = 50) : IRequest<AuditLogsResult>;
|
||||
|
||||
public record AuditLogsResult(IEnumerable<AuditLogDto> Logs, long TotalCount);
|
||||
|
||||
public record AuditLogDto(
|
||||
Guid Id,
|
||||
string EventType,
|
||||
Guid? ActorId,
|
||||
string? ActorEmail,
|
||||
string ResourceType,
|
||||
Guid? ResourceId,
|
||||
string? Action,
|
||||
bool Success,
|
||||
DateTime Timestamp);
|
||||
|
||||
public class GetAuditLogsQueryHandler : IRequestHandler<GetAuditLogsQuery, AuditLogsResult>
|
||||
{
|
||||
private readonly IAuditLogRepository _repository;
|
||||
public GetAuditLogsQueryHandler(IAuditLogRepository repository) => _repository = repository;
|
||||
|
||||
public async Task<AuditLogsResult> Handle(GetAuditLogsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var logs = await _repository.SearchAsync(
|
||||
request.FromDate, request.ToDate, request.EventTypeId, request.ActorId,
|
||||
request.ResourceType, request.Skip, request.Take, cancellationToken);
|
||||
var count = await _repository.GetCountAsync(request.FromDate, request.ToDate, cancellationToken);
|
||||
|
||||
var dtos = logs.Select(l => new AuditLogDto(
|
||||
l.Id, l.EventType.Name, l.ActorId, l.ActorEmail, l.ResourceType,
|
||||
l.ResourceId, l.Action, l.Success, l.Timestamp));
|
||||
return new AuditLogsResult(dtos, count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using MediatR;
|
||||
using IamService.Domain.AggregatesModel.ComplianceAggregate;
|
||||
|
||||
namespace IamService.API.Application.Queries.Compliance;
|
||||
|
||||
public record GetComplianceReportsQuery(int? ReportTypeId = null, int Take = 20) : IRequest<IEnumerable<ComplianceReportDto>>;
|
||||
public record GetComplianceReportByIdQuery(Guid Id) : IRequest<ComplianceReportDetailDto?>;
|
||||
public record GetUnresolvedViolationsQuery() : IRequest<IEnumerable<ViolationDto>>;
|
||||
|
||||
public record ComplianceReportDto(Guid Id, string Name, string ReportType, string Status, DateTime CreatedAt, double CompliancePercentage);
|
||||
public record ComplianceReportDetailDto(Guid Id, string Name, string ReportType, string Status, DateTime CreatedAt, DateTime? CompletedAt,
|
||||
int TotalChecks, int PassedChecks, int FailedChecks, double CompliancePercentage, string? Summary, IEnumerable<ViolationDto> Violations);
|
||||
public record ViolationDto(Guid Id, string Rule, string Severity, string Description, string? Remediation, bool Resolved);
|
||||
|
||||
public class GetComplianceReportsQueryHandler : IRequestHandler<GetComplianceReportsQuery, IEnumerable<ComplianceReportDto>>
|
||||
{
|
||||
private readonly IComplianceReportRepository _repository;
|
||||
public GetComplianceReportsQueryHandler(IComplianceReportRepository repository) => _repository = repository;
|
||||
|
||||
public async Task<IEnumerable<ComplianceReportDto>> Handle(GetComplianceReportsQuery request, CancellationToken ct)
|
||||
{
|
||||
var reports = request.ReportTypeId.HasValue
|
||||
? await _repository.GetByTypeAsync(request.ReportTypeId.Value, request.Take, ct)
|
||||
: await _repository.GetRecentAsync(request.Take, ct);
|
||||
return reports.Select(r => new ComplianceReportDto(r.Id, r.Name, r.ReportType.Name, r.Status.Name, r.CreatedAt, r.CompliancePercentage));
|
||||
}
|
||||
}
|
||||
|
||||
public class GetComplianceReportByIdQueryHandler : IRequestHandler<GetComplianceReportByIdQuery, ComplianceReportDetailDto?>
|
||||
{
|
||||
private readonly IComplianceReportRepository _repository;
|
||||
public GetComplianceReportByIdQueryHandler(IComplianceReportRepository repository) => _repository = repository;
|
||||
|
||||
public async Task<ComplianceReportDetailDto?> Handle(GetComplianceReportByIdQuery request, CancellationToken ct)
|
||||
{
|
||||
var r = await _repository.GetByIdWithViolationsAsync(request.Id, ct);
|
||||
if (r == null) return null;
|
||||
var violations = r.Violations.Select(v => new ViolationDto(v.Id, v.Rule, v.Severity.Name, v.Description, v.Remediation, v.Resolved));
|
||||
return new ComplianceReportDetailDto(r.Id, r.Name, r.ReportType.Name, r.Status.Name, r.CreatedAt, r.CompletedAt,
|
||||
r.TotalChecks, r.PassedChecks, r.FailedChecks, r.CompliancePercentage, r.Summary, violations);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetUnresolvedViolationsQueryHandler : IRequestHandler<GetUnresolvedViolationsQuery, IEnumerable<ViolationDto>>
|
||||
{
|
||||
private readonly IComplianceReportRepository _repository;
|
||||
public GetUnresolvedViolationsQueryHandler(IComplianceReportRepository repository) => _repository = repository;
|
||||
|
||||
public async Task<IEnumerable<ViolationDto>> Handle(GetUnresolvedViolationsQuery request, CancellationToken ct)
|
||||
{
|
||||
var violations = await _repository.GetUnresolvedViolationsAsync(ct);
|
||||
return violations.Select(v => new ViolationDto(v.Id, v.Rule, v.Severity.Name, v.Description, v.Remediation, v.Resolved));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using IamService.API.Application.Common;
|
||||
using IamService.API.Application.Queries.Audit;
|
||||
|
||||
namespace IamService.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/audit")]
|
||||
[Authorize(AuthenticationSchemes = "Bearer")]
|
||||
[SwaggerTag("Audit log management")]
|
||||
public class AuditController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
public AuditController(IMediator mediator) => _mediator = mediator;
|
||||
|
||||
[HttpGet("logs")]
|
||||
[SwaggerOperation(Summary = "Get audit logs", OperationId = "GetAuditLogs")]
|
||||
public async Task<IActionResult> GetLogs(
|
||||
[FromQuery] DateTime? fromDate,
|
||||
[FromQuery] DateTime? toDate,
|
||||
[FromQuery] int? eventTypeId,
|
||||
[FromQuery] Guid? actorId,
|
||||
[FromQuery] string? resourceType,
|
||||
[FromQuery] int skip = 0,
|
||||
[FromQuery] int take = 50,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var result = await _mediator.Send(new GetAuditLogsQuery(fromDate, toDate, eventTypeId, actorId, resourceType, skip, take), ct);
|
||||
return Ok(ApiResponse<AuditLogsResult>.Ok(result));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using Asp.Versioning;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using IamService.API.Application.Common;
|
||||
using IamService.API.Application.Commands.Compliance;
|
||||
using IamService.API.Application.Queries.Compliance;
|
||||
|
||||
namespace IamService.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/compliance")]
|
||||
[Authorize(AuthenticationSchemes = "Bearer")]
|
||||
[SwaggerTag("Compliance reporting")]
|
||||
public class ComplianceController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
public ComplianceController(IMediator mediator) => _mediator = mediator;
|
||||
|
||||
[HttpPost("reports")]
|
||||
[SwaggerOperation(Summary = "Generate compliance report", OperationId = "GenerateComplianceReport")]
|
||||
public async Task<IActionResult> Generate([FromBody] GenerateReportRequest request, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var id = await _mediator.Send(new GenerateComplianceReportCommand(request.Name, request.ReportTypeId, request.GeneratedByUserId), ct);
|
||||
return Ok(ApiResponse<object>.Ok(new { ReportId = id, Status = "Generating" }));
|
||||
}
|
||||
catch (Exception ex) { return BadRequest(ApiResponse<object>.Fail("ERROR", ex.Message)); }
|
||||
}
|
||||
|
||||
[HttpGet("reports")]
|
||||
[SwaggerOperation(Summary = "Get compliance reports", OperationId = "GetComplianceReports")]
|
||||
public async Task<IActionResult> GetReports([FromQuery] int? reportTypeId, [FromQuery] int take = 20, CancellationToken ct = default)
|
||||
{
|
||||
var reports = await _mediator.Send(new GetComplianceReportsQuery(reportTypeId, take), ct);
|
||||
return Ok(ApiResponse<IEnumerable<ComplianceReportDto>>.Ok(reports));
|
||||
}
|
||||
|
||||
[HttpGet("reports/{id:guid}")]
|
||||
[SwaggerOperation(Summary = "Get compliance report by ID", OperationId = "GetComplianceReportById")]
|
||||
public async Task<IActionResult> GetById([FromRoute] Guid id, CancellationToken ct = default)
|
||||
{
|
||||
var report = await _mediator.Send(new GetComplianceReportByIdQuery(id), ct);
|
||||
if (report == null) return NotFound(ApiResponse<object>.Fail("NOT_FOUND", "Report not found"));
|
||||
return Ok(ApiResponse<ComplianceReportDetailDto>.Ok(report));
|
||||
}
|
||||
|
||||
[HttpGet("violations")]
|
||||
[SwaggerOperation(Summary = "Get unresolved violations", OperationId = "GetUnresolvedViolations")]
|
||||
public async Task<IActionResult> GetViolations(CancellationToken ct = default)
|
||||
{
|
||||
var violations = await _mediator.Send(new GetUnresolvedViolationsQuery(), ct);
|
||||
return Ok(ApiResponse<IEnumerable<ViolationDto>>.Ok(violations));
|
||||
}
|
||||
|
||||
[HttpPost("reports/{id:guid}/complete")]
|
||||
[SwaggerOperation(Summary = "Complete compliance report", OperationId = "CompleteComplianceReport")]
|
||||
public async Task<IActionResult> Complete([FromRoute] Guid id, [FromBody] CompleteReportRequest request, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _mediator.Send(new CompleteComplianceReportCommand(id, request.TotalChecks, request.PassedChecks, request.Summary), ct);
|
||||
return Ok(ApiResponse<object>.Ok(new { Message = "Report completed" }));
|
||||
}
|
||||
catch (Exception ex) { return BadRequest(ApiResponse<object>.Fail("ERROR", ex.Message)); }
|
||||
}
|
||||
}
|
||||
|
||||
public record GenerateReportRequest(string Name, int ReportTypeId, Guid GeneratedByUserId);
|
||||
public record CompleteReportRequest(int TotalChecks, int PassedChecks, string? Summary);
|
||||
@@ -0,0 +1,55 @@
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Domain.AggregatesModel.AuditAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Audit event type enumeration.
|
||||
/// VI: Enumeration loại audit event.
|
||||
/// </summary>
|
||||
public class AuditEventType : Enumeration
|
||||
{
|
||||
// Authentication events
|
||||
public static readonly AuditEventType Login = new(1, nameof(Login));
|
||||
public static readonly AuditEventType Logout = new(2, nameof(Logout));
|
||||
public static readonly AuditEventType LoginFailed = new(3, nameof(LoginFailed));
|
||||
public static readonly AuditEventType PasswordChanged = new(4, nameof(PasswordChanged));
|
||||
public static readonly AuditEventType PasswordResetRequested = new(5, nameof(PasswordResetRequested));
|
||||
public static readonly AuditEventType TwoFactorEnabled = new(6, nameof(TwoFactorEnabled));
|
||||
public static readonly AuditEventType TwoFactorDisabled = new(7, nameof(TwoFactorDisabled));
|
||||
|
||||
// User events
|
||||
public static readonly AuditEventType UserCreated = new(10, nameof(UserCreated));
|
||||
public static readonly AuditEventType UserUpdated = new(11, nameof(UserUpdated));
|
||||
public static readonly AuditEventType UserDeleted = new(12, nameof(UserDeleted));
|
||||
public static readonly AuditEventType UserLocked = new(13, nameof(UserLocked));
|
||||
public static readonly AuditEventType UserUnlocked = new(14, nameof(UserUnlocked));
|
||||
|
||||
// Access events
|
||||
public static readonly AuditEventType AccessRequested = new(20, nameof(AccessRequested));
|
||||
public static readonly AuditEventType AccessGranted = new(21, nameof(AccessGranted));
|
||||
public static readonly AuditEventType AccessRevoked = new(22, nameof(AccessRevoked));
|
||||
public static readonly AuditEventType AccessDenied = new(23, nameof(AccessDenied));
|
||||
public static readonly AuditEventType PrivilegedAccessGranted = new(24, nameof(PrivilegedAccessGranted));
|
||||
public static readonly AuditEventType PrivilegedAccessRevoked = new(25, nameof(PrivilegedAccessRevoked));
|
||||
|
||||
// Organization events
|
||||
public static readonly AuditEventType OrganizationCreated = new(30, nameof(OrganizationCreated));
|
||||
public static readonly AuditEventType OrganizationUpdated = new(31, nameof(OrganizationUpdated));
|
||||
public static readonly AuditEventType GroupMemberAdded = new(32, nameof(GroupMemberAdded));
|
||||
public static readonly AuditEventType GroupMemberRemoved = new(33, nameof(GroupMemberRemoved));
|
||||
|
||||
// Policy events
|
||||
public static readonly AuditEventType PolicyCreated = new(40, nameof(PolicyCreated));
|
||||
public static readonly AuditEventType PolicyActivated = new(41, nameof(PolicyActivated));
|
||||
public static readonly AuditEventType PolicyDeactivated = new(42, nameof(PolicyDeactivated));
|
||||
|
||||
// Compliance events
|
||||
public static readonly AuditEventType ComplianceReportGenerated = new(50, nameof(ComplianceReportGenerated));
|
||||
public static readonly AuditEventType ViolationDetected = new(51, nameof(ViolationDetected));
|
||||
public static readonly AuditEventType ViolationResolved = new(52, nameof(ViolationResolved));
|
||||
|
||||
public AuditEventType(int id, string name) : base(id, name) { }
|
||||
|
||||
public static AuditEventType? FromId(int id) => GetAll<AuditEventType>().FirstOrDefault(e => e.Id == id);
|
||||
public static AuditEventType? FromName(string name) => GetAll<AuditEventType>().FirstOrDefault(e => e.Name == name);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Domain.AggregatesModel.AuditAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Audit log aggregate root for system-wide event logging.
|
||||
/// VI: Aggregate root audit log cho ghi nhận sự kiện toàn hệ thống.
|
||||
/// </summary>
|
||||
public class AuditLog : Entity, IAggregateRoot
|
||||
{
|
||||
private AuditEventType _eventType = null!;
|
||||
private Guid? _actorId;
|
||||
private string? _actorEmail;
|
||||
private string _resourceType = null!;
|
||||
private Guid? _resourceId;
|
||||
private string? _action;
|
||||
private string? _details;
|
||||
private string? _ipAddress;
|
||||
private string? _userAgent;
|
||||
private bool _success;
|
||||
private DateTime _timestamp;
|
||||
|
||||
#region Properties
|
||||
public AuditEventType EventType => _eventType;
|
||||
public Guid? ActorId => _actorId;
|
||||
public string? ActorEmail => _actorEmail;
|
||||
public string ResourceType => _resourceType;
|
||||
public Guid? ResourceId => _resourceId;
|
||||
public string? Action => _action;
|
||||
public string? Details => _details;
|
||||
public string? IpAddress => _ipAddress;
|
||||
public string? UserAgent => _userAgent;
|
||||
public bool Success => _success;
|
||||
public DateTime Timestamp => _timestamp;
|
||||
#endregion
|
||||
|
||||
protected AuditLog() { }
|
||||
|
||||
private AuditLog(
|
||||
AuditEventType eventType,
|
||||
Guid? actorId,
|
||||
string? actorEmail,
|
||||
string resourceType,
|
||||
Guid? resourceId,
|
||||
string? action,
|
||||
string? details,
|
||||
string? ipAddress,
|
||||
string? userAgent,
|
||||
bool success)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
_eventType = eventType;
|
||||
_actorId = actorId;
|
||||
_actorEmail = actorEmail;
|
||||
_resourceType = resourceType;
|
||||
_resourceId = resourceId;
|
||||
_action = action;
|
||||
_details = details;
|
||||
_ipAddress = ipAddress;
|
||||
_userAgent = userAgent;
|
||||
_success = success;
|
||||
_timestamp = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public static AuditLog Create(
|
||||
AuditEventType eventType,
|
||||
string resourceType,
|
||||
Guid? actorId = null,
|
||||
string? actorEmail = null,
|
||||
Guid? resourceId = null,
|
||||
string? action = null,
|
||||
string? details = null,
|
||||
string? ipAddress = null,
|
||||
string? userAgent = null,
|
||||
bool success = true)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(resourceType))
|
||||
throw new ArgumentException("Resource type is required", nameof(resourceType));
|
||||
|
||||
return new AuditLog(eventType, actorId, actorEmail, resourceType, resourceId, action, details, ipAddress, userAgent, success);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create login event.
|
||||
/// VI: Tạo event đăng nhập.
|
||||
/// </summary>
|
||||
public static AuditLog LoginEvent(Guid userId, string email, string? ipAddress, string? userAgent, bool success = true)
|
||||
=> Create(
|
||||
success ? AuditEventType.Login : AuditEventType.LoginFailed,
|
||||
"User", userId, email, userId,
|
||||
success ? "User logged in" : "Login attempt failed",
|
||||
null, ipAddress, userAgent, success);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create access granted event.
|
||||
/// VI: Tạo event cấp quyền truy cập.
|
||||
/// </summary>
|
||||
public static AuditLog AccessGrantedEvent(Guid actorId, string actorEmail, string resourceType, Guid resourceId, string permission)
|
||||
=> Create(AuditEventType.AccessGranted, resourceType, actorId, actorEmail, resourceId,
|
||||
$"Access granted: {permission}", null, null, null);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Domain.AggregatesModel.AuditAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Repository interface for AuditLog aggregate.
|
||||
/// VI: Interface repository cho AuditLog aggregate.
|
||||
/// </summary>
|
||||
public interface IAuditLogRepository : IRepository<AuditLog>
|
||||
{
|
||||
AuditLog Add(AuditLog log);
|
||||
Task<IEnumerable<AuditLog>> GetByActorIdAsync(Guid actorId, int take = 100, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<AuditLog>> GetByResourceAsync(string resourceType, Guid resourceId, int take = 100, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<AuditLog>> SearchAsync(
|
||||
DateTime? fromDate, DateTime? toDate,
|
||||
int? eventTypeId = null, Guid? actorId = null, string? resourceType = null,
|
||||
int skip = 0, int take = 50,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<long> GetCountAsync(DateTime? fromDate, DateTime? toDate, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using IamService.Domain.Events;
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Domain.AggregatesModel.ComplianceAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Compliance report aggregate root.
|
||||
/// VI: Aggregate root báo cáo tuân thủ.
|
||||
/// </summary>
|
||||
public class ComplianceReport : Entity, IAggregateRoot
|
||||
{
|
||||
private string _name = null!;
|
||||
private ComplianceReportType _reportType = null!;
|
||||
private ComplianceReportStatus _status = null!;
|
||||
private Guid _generatedByUserId;
|
||||
private DateTime _createdAt;
|
||||
private DateTime? _completedAt;
|
||||
private string? _summary;
|
||||
private int _totalChecks;
|
||||
private int _passedChecks;
|
||||
private int _failedChecks;
|
||||
|
||||
private readonly List<ComplianceViolation> _violations = [];
|
||||
|
||||
#region Properties
|
||||
public string Name => _name;
|
||||
public ComplianceReportType ReportType => _reportType;
|
||||
public ComplianceReportStatus Status => _status;
|
||||
public Guid GeneratedByUserId => _generatedByUserId;
|
||||
public DateTime CreatedAt => _createdAt;
|
||||
public DateTime? CompletedAt => _completedAt;
|
||||
public string? Summary => _summary;
|
||||
public int TotalChecks => _totalChecks;
|
||||
public int PassedChecks => _passedChecks;
|
||||
public int FailedChecks => _failedChecks;
|
||||
public IReadOnlyCollection<ComplianceViolation> Violations => _violations.AsReadOnly();
|
||||
public double CompliancePercentage => _totalChecks > 0 ? (double)_passedChecks / _totalChecks * 100 : 0;
|
||||
#endregion
|
||||
|
||||
protected ComplianceReport() { }
|
||||
|
||||
private ComplianceReport(string name, ComplianceReportType reportType, Guid generatedByUserId)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
_name = name;
|
||||
_reportType = reportType;
|
||||
_status = ComplianceReportStatus.Pending;
|
||||
_generatedByUserId = generatedByUserId;
|
||||
_createdAt = DateTime.UtcNow;
|
||||
|
||||
AddDomainEvent(new ComplianceReportCreatedEvent(Id, name, reportType.Name));
|
||||
}
|
||||
|
||||
public static ComplianceReport Create(string name, ComplianceReportType reportType, Guid generatedByUserId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("Name is required", nameof(name));
|
||||
return new ComplianceReport(name, reportType, generatedByUserId);
|
||||
}
|
||||
|
||||
public void StartGenerating()
|
||||
{
|
||||
if (_status != ComplianceReportStatus.Pending)
|
||||
throw new InvalidOperationException("Only pending reports can be started");
|
||||
_status = ComplianceReportStatus.Generating;
|
||||
}
|
||||
|
||||
public void SetResults(int totalChecks, int passedChecks, string? summary = null)
|
||||
{
|
||||
_totalChecks = totalChecks;
|
||||
_passedChecks = passedChecks;
|
||||
_failedChecks = totalChecks - passedChecks;
|
||||
_summary = summary;
|
||||
}
|
||||
|
||||
public void AddViolation(string rule, ViolationSeverity severity, string description, string? remediation = null)
|
||||
{
|
||||
var violation = new ComplianceViolation(Id, rule, severity, description, remediation);
|
||||
_violations.Add(violation);
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
_status = ComplianceReportStatus.Completed;
|
||||
_completedAt = DateTime.UtcNow;
|
||||
AddDomainEvent(new ComplianceReportCompletedEvent(Id, _passedChecks, _failedChecks, _violations.Count));
|
||||
}
|
||||
|
||||
public void Fail(string? reason = null)
|
||||
{
|
||||
_status = ComplianceReportStatus.Failed;
|
||||
_summary = reason ?? "Report generation failed";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Domain.AggregatesModel.ComplianceAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Compliance report type enumeration.
|
||||
/// VI: Enumeration loại báo cáo tuân thủ.
|
||||
/// </summary>
|
||||
public class ComplianceReportType : Enumeration
|
||||
{
|
||||
public static readonly ComplianceReportType GDPR = new(1, nameof(GDPR));
|
||||
public static readonly ComplianceReportType SOC2 = new(2, nameof(SOC2));
|
||||
public static readonly ComplianceReportType ISO27001 = new(3, nameof(ISO27001));
|
||||
public static readonly ComplianceReportType HIPAA = new(4, nameof(HIPAA));
|
||||
public static readonly ComplianceReportType AccessReview = new(5, nameof(AccessReview));
|
||||
|
||||
public ComplianceReportType(int id, string name) : base(id, name) { }
|
||||
|
||||
public static ComplianceReportType? FromId(int id) => id switch
|
||||
{
|
||||
1 => GDPR,
|
||||
2 => SOC2,
|
||||
3 => ISO27001,
|
||||
4 => HIPAA,
|
||||
5 => AccessReview,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Compliance report status enumeration.
|
||||
/// VI: Enumeration trạng thái báo cáo.
|
||||
/// </summary>
|
||||
public class ComplianceReportStatus : Enumeration
|
||||
{
|
||||
public static readonly ComplianceReportStatus Pending = new(1, nameof(Pending));
|
||||
public static readonly ComplianceReportStatus Generating = new(2, nameof(Generating));
|
||||
public static readonly ComplianceReportStatus Completed = new(3, nameof(Completed));
|
||||
public static readonly ComplianceReportStatus Failed = new(4, nameof(Failed));
|
||||
|
||||
public ComplianceReportStatus(int id, string name) : base(id, name) { }
|
||||
|
||||
public static ComplianceReportStatus? FromId(int id) => id switch
|
||||
{
|
||||
1 => Pending, 2 => Generating, 3 => Completed, 4 => Failed, _ => null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Violation severity enumeration.
|
||||
/// VI: Enumeration mức độ nghiêm trọng vi phạm.
|
||||
/// </summary>
|
||||
public class ViolationSeverity : Enumeration
|
||||
{
|
||||
public static readonly ViolationSeverity Low = new(1, nameof(Low));
|
||||
public static readonly ViolationSeverity Medium = new(2, nameof(Medium));
|
||||
public static readonly ViolationSeverity High = new(3, nameof(High));
|
||||
public static readonly ViolationSeverity Critical = new(4, nameof(Critical));
|
||||
|
||||
public ViolationSeverity(int id, string name) : base(id, name) { }
|
||||
public static ViolationSeverity? FromId(int id) => id switch { 1 => Low, 2 => Medium, 3 => High, 4 => Critical, _ => null };
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Domain.AggregatesModel.ComplianceAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Compliance violation entity.
|
||||
/// VI: Entity vi phạm tuân thủ.
|
||||
/// </summary>
|
||||
public class ComplianceViolation : Entity
|
||||
{
|
||||
private Guid _reportId;
|
||||
private string _rule = null!;
|
||||
private ViolationSeverity _severity = null!;
|
||||
private string _description = null!;
|
||||
private string? _remediation;
|
||||
private string? _affectedResource;
|
||||
private bool _resolved;
|
||||
private DateTime? _resolvedAt;
|
||||
|
||||
public Guid ReportId => _reportId;
|
||||
public string Rule => _rule;
|
||||
public ViolationSeverity Severity => _severity;
|
||||
public string Description => _description;
|
||||
public string? Remediation => _remediation;
|
||||
public string? AffectedResource => _affectedResource;
|
||||
public bool Resolved => _resolved;
|
||||
public DateTime? ResolvedAt => _resolvedAt;
|
||||
|
||||
protected ComplianceViolation() { _severity = ViolationSeverity.Medium; }
|
||||
|
||||
public ComplianceViolation(Guid reportId, string rule, ViolationSeverity severity, string description, string? remediation = null, string? affectedResource = null)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
_reportId = reportId;
|
||||
_rule = rule;
|
||||
_severity = severity;
|
||||
_description = description;
|
||||
_remediation = remediation;
|
||||
_affectedResource = affectedResource;
|
||||
_resolved = false;
|
||||
}
|
||||
|
||||
public void Resolve()
|
||||
{
|
||||
_resolved = true;
|
||||
_resolvedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Domain.AggregatesModel.ComplianceAggregate;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Repository interface for ComplianceReport aggregate.
|
||||
/// VI: Interface repository cho ComplianceReport aggregate.
|
||||
/// </summary>
|
||||
public interface IComplianceReportRepository : IRepository<ComplianceReport>
|
||||
{
|
||||
ComplianceReport Add(ComplianceReport report);
|
||||
void Update(ComplianceReport report);
|
||||
Task<ComplianceReport?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<ComplianceReport?> GetByIdWithViolationsAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<ComplianceReport>> GetByTypeAsync(int reportTypeId, int take = 20, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<ComplianceReport>> GetRecentAsync(int take = 20, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<ComplianceViolation>> GetUnresolvedViolationsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Domain.Events;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Event when compliance report is created.
|
||||
/// VI: Event khi báo cáo tuân thủ được tạo.
|
||||
/// </summary>
|
||||
public record ComplianceReportCreatedEvent(Guid ReportId, string Name, string ReportType) : IDomainEvent
|
||||
{
|
||||
public DateTime OccurredOn { get; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Event when compliance report is completed.
|
||||
/// VI: Event khi báo cáo tuân thủ hoàn thành.
|
||||
/// </summary>
|
||||
public record ComplianceReportCompletedEvent(Guid ReportId, int PassedChecks, int FailedChecks, int ViolationCount) : IDomainEvent
|
||||
{
|
||||
public DateTime OccurredOn { get; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -13,6 +13,8 @@ using IamService.Domain.AggregatesModel.VerificationAggregate;
|
||||
using IamService.Domain.AggregatesModel.AccessRequestAggregate;
|
||||
using IamService.Domain.AggregatesModel.AccessReviewAggregate;
|
||||
using IamService.Domain.AggregatesModel.PrivilegedAccessAggregate;
|
||||
using IamService.Domain.AggregatesModel.AuditAggregate;
|
||||
using IamService.Domain.AggregatesModel.ComplianceAggregate;
|
||||
using IamService.Domain.SeedWork;
|
||||
using IamService.Infrastructure.Email;
|
||||
using IamService.Infrastructure.IdentityServer;
|
||||
@@ -159,6 +161,8 @@ public static class DependencyInjection
|
||||
services.AddScoped<IAccessRequestRepository, AccessRequestRepository>();
|
||||
services.AddScoped<IAccessReviewRepository, AccessReviewRepository>();
|
||||
services.AddScoped<IPrivilegedAccessRepository, PrivilegedAccessRepository>();
|
||||
services.AddScoped<IAuditLogRepository, AuditLogRepository>();
|
||||
services.AddScoped<IComplianceReportRepository, ComplianceReportRepository>();
|
||||
services.AddScoped<IUnitOfWork>(sp => sp.GetRequiredService<IamServiceContext>());
|
||||
|
||||
// EN: Configure Redis caching (skip in Testing environment)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using IamService.Domain.AggregatesModel.AuditAggregate;
|
||||
|
||||
namespace IamService.Infrastructure.EntityConfigurations;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Entity configuration for AuditLog.
|
||||
/// VI: Cấu hình entity cho AuditLog.
|
||||
/// </summary>
|
||||
public class AuditLogEntityConfiguration : IEntityTypeConfiguration<AuditLog>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AuditLog> builder)
|
||||
{
|
||||
builder.ToTable("AuditLogs");
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.Id).ValueGeneratedNever();
|
||||
|
||||
builder.Property<Guid?>("_actorId").HasColumnName("ActorId");
|
||||
builder.Property<string?>("_actorEmail").HasColumnName("ActorEmail").HasMaxLength(256);
|
||||
builder.Property<string>("_resourceType").HasColumnName("ResourceType").HasMaxLength(100).IsRequired();
|
||||
builder.Property<Guid?>("_resourceId").HasColumnName("ResourceId");
|
||||
builder.Property<string?>("_action").HasColumnName("Action").HasMaxLength(200);
|
||||
builder.Property<string?>("_details").HasColumnName("Details").HasMaxLength(4000);
|
||||
builder.Property<string?>("_ipAddress").HasColumnName("IpAddress").HasMaxLength(45);
|
||||
builder.Property<string?>("_userAgent").HasColumnName("UserAgent").HasMaxLength(500);
|
||||
builder.Property<bool>("_success").HasColumnName("Success");
|
||||
builder.Property<DateTime>("_timestamp").HasColumnName("Timestamp").IsRequired();
|
||||
|
||||
builder.Property<AuditEventType>("_eventType")
|
||||
.HasColumnName("EventTypeId")
|
||||
.HasConversion(v => v.Id, v => AuditEventType.FromId(v) ?? AuditEventType.Login);
|
||||
|
||||
builder.Ignore(x => x.DomainEvents);
|
||||
|
||||
// Indexes for common queries
|
||||
builder.HasIndex("_timestamp").HasDatabaseName("IX_AuditLogs_Timestamp");
|
||||
builder.HasIndex("_actorId").HasDatabaseName("IX_AuditLogs_ActorId");
|
||||
builder.HasIndex("_eventType").HasDatabaseName("IX_AuditLogs_EventType");
|
||||
builder.HasIndex("_resourceType", "_resourceId").HasDatabaseName("IX_AuditLogs_Resource");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using IamService.Domain.AggregatesModel.ComplianceAggregate;
|
||||
|
||||
namespace IamService.Infrastructure.EntityConfigurations;
|
||||
|
||||
public class ComplianceReportEntityConfiguration : IEntityTypeConfiguration<ComplianceReport>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ComplianceReport> builder)
|
||||
{
|
||||
builder.ToTable("ComplianceReports");
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.Id).ValueGeneratedNever();
|
||||
|
||||
builder.Property<string>("_name").HasColumnName("Name").HasMaxLength(200).IsRequired();
|
||||
builder.Property<Guid>("_generatedByUserId").HasColumnName("GeneratedByUserId").IsRequired();
|
||||
builder.Property<DateTime>("_createdAt").HasColumnName("CreatedAt").IsRequired();
|
||||
builder.Property<DateTime?>("_completedAt").HasColumnName("CompletedAt");
|
||||
builder.Property<string?>("_summary").HasColumnName("Summary").HasMaxLength(4000);
|
||||
builder.Property<int>("_totalChecks").HasColumnName("TotalChecks");
|
||||
builder.Property<int>("_passedChecks").HasColumnName("PassedChecks");
|
||||
builder.Property<int>("_failedChecks").HasColumnName("FailedChecks");
|
||||
|
||||
builder.Property<ComplianceReportType>("_reportType")
|
||||
.HasColumnName("ReportTypeId")
|
||||
.HasConversion(v => v.Id, v => ComplianceReportType.FromId(v) ?? ComplianceReportType.GDPR);
|
||||
|
||||
builder.Property<ComplianceReportStatus>("_status")
|
||||
.HasColumnName("StatusId")
|
||||
.HasConversion(v => v.Id, v => ComplianceReportStatus.FromId(v) ?? ComplianceReportStatus.Pending);
|
||||
|
||||
builder.HasMany(x => x.Violations).WithOne().HasForeignKey("_reportId").OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Ignore(x => x.DomainEvents);
|
||||
builder.HasIndex("_createdAt").HasDatabaseName("IX_ComplianceReports_CreatedAt");
|
||||
}
|
||||
}
|
||||
|
||||
public class ComplianceViolationEntityConfiguration : IEntityTypeConfiguration<ComplianceViolation>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ComplianceViolation> builder)
|
||||
{
|
||||
builder.ToTable("ComplianceViolations");
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.Id).ValueGeneratedNever();
|
||||
|
||||
builder.Property<Guid>("_reportId").HasColumnName("ReportId").IsRequired();
|
||||
builder.Property<string>("_rule").HasColumnName("Rule").HasMaxLength(200).IsRequired();
|
||||
builder.Property<string>("_description").HasColumnName("Description").HasMaxLength(1000).IsRequired();
|
||||
builder.Property<string?>("_remediation").HasColumnName("Remediation").HasMaxLength(1000);
|
||||
builder.Property<string?>("_affectedResource").HasColumnName("AffectedResource").HasMaxLength(200);
|
||||
builder.Property<bool>("_resolved").HasColumnName("Resolved");
|
||||
builder.Property<DateTime?>("_resolvedAt").HasColumnName("ResolvedAt");
|
||||
|
||||
builder.Property<ViolationSeverity>("_severity")
|
||||
.HasColumnName("SeverityId")
|
||||
.HasConversion(v => v.Id, v => ViolationSeverity.FromId(v) ?? ViolationSeverity.Medium);
|
||||
|
||||
builder.Ignore(x => x.DomainEvents);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ using IamService.Domain.AggregatesModel.VerificationAggregate;
|
||||
using IamService.Domain.AggregatesModel.AccessRequestAggregate;
|
||||
using IamService.Domain.AggregatesModel.AccessReviewAggregate;
|
||||
using IamService.Domain.AggregatesModel.PrivilegedAccessAggregate;
|
||||
using IamService.Domain.AggregatesModel.AuditAggregate;
|
||||
using IamService.Domain.AggregatesModel.ComplianceAggregate;
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Infrastructure;
|
||||
@@ -145,6 +147,24 @@ public class IamServiceContext : IdentityDbContext<ApplicationUser, ApplicationR
|
||||
/// </summary>
|
||||
public DbSet<PrivilegedAccessGrant> PrivilegedAccessGrants { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Audit logs table.
|
||||
/// VI: Bảng audit logs.
|
||||
/// </summary>
|
||||
public DbSet<AuditLog> AuditLogs { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Compliance reports table.
|
||||
/// VI: Bảng báo cáo tuân thủ.
|
||||
/// </summary>
|
||||
public DbSet<ComplianceReport> ComplianceReports { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Compliance violations table.
|
||||
/// VI: Bảng vi phạm tuân thủ.
|
||||
/// </summary>
|
||||
public DbSet<ComplianceViolation> ComplianceViolations { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Check if there's an active transaction.
|
||||
/// VI: Kiểm tra xem có transaction đang hoạt động không.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using IamService.Domain.AggregatesModel.AuditAggregate;
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Infrastructure.Repositories;
|
||||
|
||||
public class AuditLogRepository : IAuditLogRepository
|
||||
{
|
||||
private readonly IamServiceContext _context;
|
||||
public IUnitOfWork UnitOfWork => _context;
|
||||
|
||||
public AuditLogRepository(IamServiceContext context) => _context = context;
|
||||
|
||||
public AuditLog Add(AuditLog log) => _context.AuditLogs.Add(log).Entity;
|
||||
|
||||
public async Task<IEnumerable<AuditLog>> GetByActorIdAsync(Guid actorId, int take = 100, CancellationToken cancellationToken = default)
|
||||
=> await _context.AuditLogs
|
||||
.Where(x => EF.Property<Guid?>(x, "_actorId") == actorId)
|
||||
.OrderByDescending(x => EF.Property<DateTime>(x, "_timestamp"))
|
||||
.Take(take)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
public async Task<IEnumerable<AuditLog>> GetByResourceAsync(string resourceType, Guid resourceId, int take = 100, CancellationToken cancellationToken = default)
|
||||
=> await _context.AuditLogs
|
||||
.Where(x => EF.Property<string>(x, "_resourceType") == resourceType && EF.Property<Guid?>(x, "_resourceId") == resourceId)
|
||||
.OrderByDescending(x => EF.Property<DateTime>(x, "_timestamp"))
|
||||
.Take(take)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
public async Task<IEnumerable<AuditLog>> SearchAsync(
|
||||
DateTime? fromDate, DateTime? toDate,
|
||||
int? eventTypeId = null, Guid? actorId = null, string? resourceType = null,
|
||||
int skip = 0, int take = 50,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.AuditLogs.AsQueryable();
|
||||
|
||||
if (fromDate.HasValue)
|
||||
query = query.Where(x => EF.Property<DateTime>(x, "_timestamp") >= fromDate.Value);
|
||||
if (toDate.HasValue)
|
||||
query = query.Where(x => EF.Property<DateTime>(x, "_timestamp") <= toDate.Value);
|
||||
if (eventTypeId.HasValue)
|
||||
query = query.Where(x => EF.Property<AuditEventType>(x, "_eventType").Id == eventTypeId.Value);
|
||||
if (actorId.HasValue)
|
||||
query = query.Where(x => EF.Property<Guid?>(x, "_actorId") == actorId.Value);
|
||||
if (!string.IsNullOrEmpty(resourceType))
|
||||
query = query.Where(x => EF.Property<string>(x, "_resourceType") == resourceType);
|
||||
|
||||
return await query
|
||||
.OrderByDescending(x => EF.Property<DateTime>(x, "_timestamp"))
|
||||
.Skip(skip).Take(take)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<long> GetCountAsync(DateTime? fromDate, DateTime? toDate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.AuditLogs.AsQueryable();
|
||||
if (fromDate.HasValue)
|
||||
query = query.Where(x => EF.Property<DateTime>(x, "_timestamp") >= fromDate.Value);
|
||||
if (toDate.HasValue)
|
||||
query = query.Where(x => EF.Property<DateTime>(x, "_timestamp") <= toDate.Value);
|
||||
return await query.LongCountAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using IamService.Domain.AggregatesModel.ComplianceAggregate;
|
||||
using IamService.Domain.SeedWork;
|
||||
|
||||
namespace IamService.Infrastructure.Repositories;
|
||||
|
||||
public class ComplianceReportRepository : IComplianceReportRepository
|
||||
{
|
||||
private readonly IamServiceContext _context;
|
||||
public IUnitOfWork UnitOfWork => _context;
|
||||
|
||||
public ComplianceReportRepository(IamServiceContext context) => _context = context;
|
||||
|
||||
public ComplianceReport Add(ComplianceReport report) => _context.ComplianceReports.Add(report).Entity;
|
||||
public void Update(ComplianceReport report) => _context.Entry(report).State = EntityState.Modified;
|
||||
|
||||
public async Task<ComplianceReport?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
=> await _context.ComplianceReports.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
|
||||
public async Task<ComplianceReport?> GetByIdWithViolationsAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
=> await _context.ComplianceReports.Include(x => x.Violations).FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
|
||||
public async Task<IEnumerable<ComplianceReport>> GetByTypeAsync(int reportTypeId, int take = 20, CancellationToken cancellationToken = default)
|
||||
=> await _context.ComplianceReports
|
||||
.Where(x => EF.Property<ComplianceReportType>(x, "_reportType").Id == reportTypeId)
|
||||
.OrderByDescending(x => EF.Property<DateTime>(x, "_createdAt"))
|
||||
.Take(take)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
public async Task<IEnumerable<ComplianceReport>> GetRecentAsync(int take = 20, CancellationToken cancellationToken = default)
|
||||
=> await _context.ComplianceReports
|
||||
.OrderByDescending(x => EF.Property<DateTime>(x, "_createdAt"))
|
||||
.Take(take)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
public async Task<IEnumerable<ComplianceViolation>> GetUnresolvedViolationsAsync(CancellationToken cancellationToken = default)
|
||||
=> await _context.ComplianceViolations
|
||||
.Where(x => !EF.Property<bool>(x, "_resolved"))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
Reference in New Issue
Block a user