From f19a995b0df25fe78fddb26fda125745fd77f082 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Wed, 14 Jan 2026 19:26:26 +0700 Subject: [PATCH] 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. --- docs/vi/architecture/iam-proposal.md | 128 ++++++++++-------- .../Commands/Audit/AuditCommands.cs | 33 +++++ .../Commands/Compliance/ComplianceCommands.cs | 59 ++++++++ .../Application/Queries/Audit/AuditQueries.cs | 45 ++++++ .../Queries/Compliance/ComplianceQueries.cs | 54 ++++++++ .../Controllers/AuditController.cs | 36 +++++ .../Controllers/ComplianceController.cs | 73 ++++++++++ .../AuditAggregate/AuditEventType.cs | 55 ++++++++ .../AuditAggregate/AuditLog.cs | 101 ++++++++++++++ .../AuditAggregate/IAuditLogRepository.cs | 20 +++ .../ComplianceAggregate/ComplianceReport.cs | 94 +++++++++++++ .../ComplianceAggregate/ComplianceTypes.cs | 62 +++++++++ .../ComplianceViolation.cs | 48 +++++++ .../IComplianceReportRepository.cs | 18 +++ .../Events/ComplianceEvents.cs | 21 +++ .../DependencyInjection.cs | 4 + .../AuditLogEntityConfiguration.cs | 42 ++++++ .../ComplianceEntityConfiguration.cs | 60 ++++++++ .../IamServiceContext.cs | 20 +++ .../Repositories/AuditLogRepository.cs | 64 +++++++++ .../ComplianceReportRepository.cs | 40 ++++++ 21 files changed, 1024 insertions(+), 53 deletions(-) create mode 100644 services/iam-service-net/src/IamService.API/Application/Commands/Audit/AuditCommands.cs create mode 100644 services/iam-service-net/src/IamService.API/Application/Commands/Compliance/ComplianceCommands.cs create mode 100644 services/iam-service-net/src/IamService.API/Application/Queries/Audit/AuditQueries.cs create mode 100644 services/iam-service-net/src/IamService.API/Application/Queries/Compliance/ComplianceQueries.cs create mode 100644 services/iam-service-net/src/IamService.API/Controllers/AuditController.cs create mode 100644 services/iam-service-net/src/IamService.API/Controllers/ComplianceController.cs create mode 100644 services/iam-service-net/src/IamService.Domain/AggregatesModel/AuditAggregate/AuditEventType.cs create mode 100644 services/iam-service-net/src/IamService.Domain/AggregatesModel/AuditAggregate/AuditLog.cs create mode 100644 services/iam-service-net/src/IamService.Domain/AggregatesModel/AuditAggregate/IAuditLogRepository.cs create mode 100644 services/iam-service-net/src/IamService.Domain/AggregatesModel/ComplianceAggregate/ComplianceReport.cs create mode 100644 services/iam-service-net/src/IamService.Domain/AggregatesModel/ComplianceAggregate/ComplianceTypes.cs create mode 100644 services/iam-service-net/src/IamService.Domain/AggregatesModel/ComplianceAggregate/ComplianceViolation.cs create mode 100644 services/iam-service-net/src/IamService.Domain/AggregatesModel/ComplianceAggregate/IComplianceReportRepository.cs create mode 100644 services/iam-service-net/src/IamService.Domain/Events/ComplianceEvents.cs create mode 100644 services/iam-service-net/src/IamService.Infrastructure/EntityConfigurations/AuditLogEntityConfiguration.cs create mode 100644 services/iam-service-net/src/IamService.Infrastructure/EntityConfigurations/ComplianceEntityConfiguration.cs create mode 100644 services/iam-service-net/src/IamService.Infrastructure/Repositories/AuditLogRepository.cs create mode 100644 services/iam-service-net/src/IamService.Infrastructure/Repositories/ComplianceReportRepository.cs diff --git a/docs/vi/architecture/iam-proposal.md b/docs/vi/architecture/iam-proposal.md index 6f59e868..078ab05b 100644 --- a/docs/vi/architecture/iam-proposal.md +++ b/docs/vi/architecture/iam-proposal.md @@ -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 diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Audit/AuditCommands.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Audit/AuditCommands.cs new file mode 100644 index 00000000..d7f818ff --- /dev/null +++ b/services/iam-service-net/src/IamService.API/Application/Commands/Audit/AuditCommands.cs @@ -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; + +public class CreateAuditLogCommandHandler : IRequestHandler +{ + private readonly IAuditLogRepository _repository; + public CreateAuditLogCommandHandler(IAuditLogRepository repository) => _repository = repository; + + public async Task 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; + } +} diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Compliance/ComplianceCommands.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Compliance/ComplianceCommands.cs new file mode 100644 index 00000000..ae73e425 --- /dev/null +++ b/services/iam-service-net/src/IamService.API/Application/Commands/Compliance/ComplianceCommands.cs @@ -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; +public record CompleteComplianceReportCommand(Guid ReportId, int TotalChecks, int PassedChecks, string? Summary = null) : IRequest; +public record AddViolationCommand(Guid ReportId, string Rule, int SeverityId, string Description, string? Remediation = null) : IRequest; + +public class GenerateComplianceReportCommandHandler : IRequestHandler +{ + private readonly IComplianceReportRepository _repository; + public GenerateComplianceReportCommandHandler(IComplianceReportRepository repository) => _repository = repository; + + public async Task 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 +{ + private readonly IComplianceReportRepository _repository; + public CompleteComplianceReportCommandHandler(IComplianceReportRepository repository) => _repository = repository; + + public async Task 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 +{ + private readonly IComplianceReportRepository _repository; + public AddViolationCommandHandler(IComplianceReportRepository repository) => _repository = repository; + + public async Task 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; + } +} diff --git a/services/iam-service-net/src/IamService.API/Application/Queries/Audit/AuditQueries.cs b/services/iam-service-net/src/IamService.API/Application/Queries/Audit/AuditQueries.cs new file mode 100644 index 00000000..019a77cf --- /dev/null +++ b/services/iam-service-net/src/IamService.API/Application/Queries/Audit/AuditQueries.cs @@ -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; + +public record AuditLogsResult(IEnumerable 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 +{ + private readonly IAuditLogRepository _repository; + public GetAuditLogsQueryHandler(IAuditLogRepository repository) => _repository = repository; + + public async Task 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); + } +} diff --git a/services/iam-service-net/src/IamService.API/Application/Queries/Compliance/ComplianceQueries.cs b/services/iam-service-net/src/IamService.API/Application/Queries/Compliance/ComplianceQueries.cs new file mode 100644 index 00000000..544104c0 --- /dev/null +++ b/services/iam-service-net/src/IamService.API/Application/Queries/Compliance/ComplianceQueries.cs @@ -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>; +public record GetComplianceReportByIdQuery(Guid Id) : IRequest; +public record GetUnresolvedViolationsQuery() : IRequest>; + +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 Violations); +public record ViolationDto(Guid Id, string Rule, string Severity, string Description, string? Remediation, bool Resolved); + +public class GetComplianceReportsQueryHandler : IRequestHandler> +{ + private readonly IComplianceReportRepository _repository; + public GetComplianceReportsQueryHandler(IComplianceReportRepository repository) => _repository = repository; + + public async Task> 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 +{ + private readonly IComplianceReportRepository _repository; + public GetComplianceReportByIdQueryHandler(IComplianceReportRepository repository) => _repository = repository; + + public async Task 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> +{ + private readonly IComplianceReportRepository _repository; + public GetUnresolvedViolationsQueryHandler(IComplianceReportRepository repository) => _repository = repository; + + public async Task> 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)); + } +} diff --git a/services/iam-service-net/src/IamService.API/Controllers/AuditController.cs b/services/iam-service-net/src/IamService.API/Controllers/AuditController.cs new file mode 100644 index 00000000..bf61bf75 --- /dev/null +++ b/services/iam-service-net/src/IamService.API/Controllers/AuditController.cs @@ -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 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.Ok(result)); + } +} diff --git a/services/iam-service-net/src/IamService.API/Controllers/ComplianceController.cs b/services/iam-service-net/src/IamService.API/Controllers/ComplianceController.cs new file mode 100644 index 00000000..fb20d685 --- /dev/null +++ b/services/iam-service-net/src/IamService.API/Controllers/ComplianceController.cs @@ -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 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.Ok(new { ReportId = id, Status = "Generating" })); + } + catch (Exception ex) { return BadRequest(ApiResponse.Fail("ERROR", ex.Message)); } + } + + [HttpGet("reports")] + [SwaggerOperation(Summary = "Get compliance reports", OperationId = "GetComplianceReports")] + public async Task GetReports([FromQuery] int? reportTypeId, [FromQuery] int take = 20, CancellationToken ct = default) + { + var reports = await _mediator.Send(new GetComplianceReportsQuery(reportTypeId, take), ct); + return Ok(ApiResponse>.Ok(reports)); + } + + [HttpGet("reports/{id:guid}")] + [SwaggerOperation(Summary = "Get compliance report by ID", OperationId = "GetComplianceReportById")] + public async Task GetById([FromRoute] Guid id, CancellationToken ct = default) + { + var report = await _mediator.Send(new GetComplianceReportByIdQuery(id), ct); + if (report == null) return NotFound(ApiResponse.Fail("NOT_FOUND", "Report not found")); + return Ok(ApiResponse.Ok(report)); + } + + [HttpGet("violations")] + [SwaggerOperation(Summary = "Get unresolved violations", OperationId = "GetUnresolvedViolations")] + public async Task GetViolations(CancellationToken ct = default) + { + var violations = await _mediator.Send(new GetUnresolvedViolationsQuery(), ct); + return Ok(ApiResponse>.Ok(violations)); + } + + [HttpPost("reports/{id:guid}/complete")] + [SwaggerOperation(Summary = "Complete compliance report", OperationId = "CompleteComplianceReport")] + public async Task 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.Ok(new { Message = "Report completed" })); + } + catch (Exception ex) { return BadRequest(ApiResponse.Fail("ERROR", ex.Message)); } + } +} + +public record GenerateReportRequest(string Name, int ReportTypeId, Guid GeneratedByUserId); +public record CompleteReportRequest(int TotalChecks, int PassedChecks, string? Summary); diff --git a/services/iam-service-net/src/IamService.Domain/AggregatesModel/AuditAggregate/AuditEventType.cs b/services/iam-service-net/src/IamService.Domain/AggregatesModel/AuditAggregate/AuditEventType.cs new file mode 100644 index 00000000..14f803c6 --- /dev/null +++ b/services/iam-service-net/src/IamService.Domain/AggregatesModel/AuditAggregate/AuditEventType.cs @@ -0,0 +1,55 @@ +using IamService.Domain.SeedWork; + +namespace IamService.Domain.AggregatesModel.AuditAggregate; + +/// +/// EN: Audit event type enumeration. +/// VI: Enumeration loại audit event. +/// +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().FirstOrDefault(e => e.Id == id); + public static AuditEventType? FromName(string name) => GetAll().FirstOrDefault(e => e.Name == name); +} diff --git a/services/iam-service-net/src/IamService.Domain/AggregatesModel/AuditAggregate/AuditLog.cs b/services/iam-service-net/src/IamService.Domain/AggregatesModel/AuditAggregate/AuditLog.cs new file mode 100644 index 00000000..8274288a --- /dev/null +++ b/services/iam-service-net/src/IamService.Domain/AggregatesModel/AuditAggregate/AuditLog.cs @@ -0,0 +1,101 @@ +using IamService.Domain.SeedWork; + +namespace IamService.Domain.AggregatesModel.AuditAggregate; + +/// +/// 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. +/// +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); + } + + /// + /// EN: Create login event. + /// VI: Tạo event đăng nhập. + /// + 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); + + /// + /// EN: Create access granted event. + /// VI: Tạo event cấp quyền truy cập. + /// + 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); +} diff --git a/services/iam-service-net/src/IamService.Domain/AggregatesModel/AuditAggregate/IAuditLogRepository.cs b/services/iam-service-net/src/IamService.Domain/AggregatesModel/AuditAggregate/IAuditLogRepository.cs new file mode 100644 index 00000000..a5ecb36a --- /dev/null +++ b/services/iam-service-net/src/IamService.Domain/AggregatesModel/AuditAggregate/IAuditLogRepository.cs @@ -0,0 +1,20 @@ +using IamService.Domain.SeedWork; + +namespace IamService.Domain.AggregatesModel.AuditAggregate; + +/// +/// EN: Repository interface for AuditLog aggregate. +/// VI: Interface repository cho AuditLog aggregate. +/// +public interface IAuditLogRepository : IRepository +{ + AuditLog Add(AuditLog log); + Task> GetByActorIdAsync(Guid actorId, int take = 100, CancellationToken cancellationToken = default); + Task> GetByResourceAsync(string resourceType, Guid resourceId, int take = 100, CancellationToken cancellationToken = default); + Task> SearchAsync( + DateTime? fromDate, DateTime? toDate, + int? eventTypeId = null, Guid? actorId = null, string? resourceType = null, + int skip = 0, int take = 50, + CancellationToken cancellationToken = default); + Task GetCountAsync(DateTime? fromDate, DateTime? toDate, CancellationToken cancellationToken = default); +} diff --git a/services/iam-service-net/src/IamService.Domain/AggregatesModel/ComplianceAggregate/ComplianceReport.cs b/services/iam-service-net/src/IamService.Domain/AggregatesModel/ComplianceAggregate/ComplianceReport.cs new file mode 100644 index 00000000..0f0cbc0f --- /dev/null +++ b/services/iam-service-net/src/IamService.Domain/AggregatesModel/ComplianceAggregate/ComplianceReport.cs @@ -0,0 +1,94 @@ +using IamService.Domain.Events; +using IamService.Domain.SeedWork; + +namespace IamService.Domain.AggregatesModel.ComplianceAggregate; + +/// +/// EN: Compliance report aggregate root. +/// VI: Aggregate root báo cáo tuân thủ. +/// +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 _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 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"; + } +} diff --git a/services/iam-service-net/src/IamService.Domain/AggregatesModel/ComplianceAggregate/ComplianceTypes.cs b/services/iam-service-net/src/IamService.Domain/AggregatesModel/ComplianceAggregate/ComplianceTypes.cs new file mode 100644 index 00000000..30ef8d9f --- /dev/null +++ b/services/iam-service-net/src/IamService.Domain/AggregatesModel/ComplianceAggregate/ComplianceTypes.cs @@ -0,0 +1,62 @@ +using IamService.Domain.SeedWork; + +namespace IamService.Domain.AggregatesModel.ComplianceAggregate; + +/// +/// EN: Compliance report type enumeration. +/// VI: Enumeration loại báo cáo tuân thủ. +/// +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 + }; +} + +/// +/// EN: Compliance report status enumeration. +/// VI: Enumeration trạng thái báo cáo. +/// +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 + }; +} + +/// +/// EN: Violation severity enumeration. +/// VI: Enumeration mức độ nghiêm trọng vi phạm. +/// +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 }; +} diff --git a/services/iam-service-net/src/IamService.Domain/AggregatesModel/ComplianceAggregate/ComplianceViolation.cs b/services/iam-service-net/src/IamService.Domain/AggregatesModel/ComplianceAggregate/ComplianceViolation.cs new file mode 100644 index 00000000..1d8a7129 --- /dev/null +++ b/services/iam-service-net/src/IamService.Domain/AggregatesModel/ComplianceAggregate/ComplianceViolation.cs @@ -0,0 +1,48 @@ +using IamService.Domain.SeedWork; + +namespace IamService.Domain.AggregatesModel.ComplianceAggregate; + +/// +/// EN: Compliance violation entity. +/// VI: Entity vi phạm tuân thủ. +/// +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; + } +} diff --git a/services/iam-service-net/src/IamService.Domain/AggregatesModel/ComplianceAggregate/IComplianceReportRepository.cs b/services/iam-service-net/src/IamService.Domain/AggregatesModel/ComplianceAggregate/IComplianceReportRepository.cs new file mode 100644 index 00000000..c24531d1 --- /dev/null +++ b/services/iam-service-net/src/IamService.Domain/AggregatesModel/ComplianceAggregate/IComplianceReportRepository.cs @@ -0,0 +1,18 @@ +using IamService.Domain.SeedWork; + +namespace IamService.Domain.AggregatesModel.ComplianceAggregate; + +/// +/// EN: Repository interface for ComplianceReport aggregate. +/// VI: Interface repository cho ComplianceReport aggregate. +/// +public interface IComplianceReportRepository : IRepository +{ + ComplianceReport Add(ComplianceReport report); + void Update(ComplianceReport report); + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task GetByIdWithViolationsAsync(Guid id, CancellationToken cancellationToken = default); + Task> GetByTypeAsync(int reportTypeId, int take = 20, CancellationToken cancellationToken = default); + Task> GetRecentAsync(int take = 20, CancellationToken cancellationToken = default); + Task> GetUnresolvedViolationsAsync(CancellationToken cancellationToken = default); +} diff --git a/services/iam-service-net/src/IamService.Domain/Events/ComplianceEvents.cs b/services/iam-service-net/src/IamService.Domain/Events/ComplianceEvents.cs new file mode 100644 index 00000000..d35907e9 --- /dev/null +++ b/services/iam-service-net/src/IamService.Domain/Events/ComplianceEvents.cs @@ -0,0 +1,21 @@ +using IamService.Domain.SeedWork; + +namespace IamService.Domain.Events; + +/// +/// EN: Event when compliance report is created. +/// VI: Event khi báo cáo tuân thủ được tạo. +/// +public record ComplianceReportCreatedEvent(Guid ReportId, string Name, string ReportType) : IDomainEvent +{ + public DateTime OccurredOn { get; } = DateTime.UtcNow; +} + +/// +/// EN: Event when compliance report is completed. +/// VI: Event khi báo cáo tuân thủ hoàn thành. +/// +public record ComplianceReportCompletedEvent(Guid ReportId, int PassedChecks, int FailedChecks, int ViolationCount) : IDomainEvent +{ + public DateTime OccurredOn { get; } = DateTime.UtcNow; +} diff --git a/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs b/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs index 8899b7c7..b9b0f81c 100644 --- a/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs +++ b/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs @@ -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(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(sp => sp.GetRequiredService()); // EN: Configure Redis caching (skip in Testing environment) diff --git a/services/iam-service-net/src/IamService.Infrastructure/EntityConfigurations/AuditLogEntityConfiguration.cs b/services/iam-service-net/src/IamService.Infrastructure/EntityConfigurations/AuditLogEntityConfiguration.cs new file mode 100644 index 00000000..e0c22c1a --- /dev/null +++ b/services/iam-service-net/src/IamService.Infrastructure/EntityConfigurations/AuditLogEntityConfiguration.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using IamService.Domain.AggregatesModel.AuditAggregate; + +namespace IamService.Infrastructure.EntityConfigurations; + +/// +/// EN: Entity configuration for AuditLog. +/// VI: Cấu hình entity cho AuditLog. +/// +public class AuditLogEntityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("AuditLogs"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Id).ValueGeneratedNever(); + + builder.Property("_actorId").HasColumnName("ActorId"); + builder.Property("_actorEmail").HasColumnName("ActorEmail").HasMaxLength(256); + builder.Property("_resourceType").HasColumnName("ResourceType").HasMaxLength(100).IsRequired(); + builder.Property("_resourceId").HasColumnName("ResourceId"); + builder.Property("_action").HasColumnName("Action").HasMaxLength(200); + builder.Property("_details").HasColumnName("Details").HasMaxLength(4000); + builder.Property("_ipAddress").HasColumnName("IpAddress").HasMaxLength(45); + builder.Property("_userAgent").HasColumnName("UserAgent").HasMaxLength(500); + builder.Property("_success").HasColumnName("Success"); + builder.Property("_timestamp").HasColumnName("Timestamp").IsRequired(); + + builder.Property("_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"); + } +} diff --git a/services/iam-service-net/src/IamService.Infrastructure/EntityConfigurations/ComplianceEntityConfiguration.cs b/services/iam-service-net/src/IamService.Infrastructure/EntityConfigurations/ComplianceEntityConfiguration.cs new file mode 100644 index 00000000..af44cf37 --- /dev/null +++ b/services/iam-service-net/src/IamService.Infrastructure/EntityConfigurations/ComplianceEntityConfiguration.cs @@ -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 +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ComplianceReports"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Id).ValueGeneratedNever(); + + builder.Property("_name").HasColumnName("Name").HasMaxLength(200).IsRequired(); + builder.Property("_generatedByUserId").HasColumnName("GeneratedByUserId").IsRequired(); + builder.Property("_createdAt").HasColumnName("CreatedAt").IsRequired(); + builder.Property("_completedAt").HasColumnName("CompletedAt"); + builder.Property("_summary").HasColumnName("Summary").HasMaxLength(4000); + builder.Property("_totalChecks").HasColumnName("TotalChecks"); + builder.Property("_passedChecks").HasColumnName("PassedChecks"); + builder.Property("_failedChecks").HasColumnName("FailedChecks"); + + builder.Property("_reportType") + .HasColumnName("ReportTypeId") + .HasConversion(v => v.Id, v => ComplianceReportType.FromId(v) ?? ComplianceReportType.GDPR); + + builder.Property("_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 +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ComplianceViolations"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Id).ValueGeneratedNever(); + + builder.Property("_reportId").HasColumnName("ReportId").IsRequired(); + builder.Property("_rule").HasColumnName("Rule").HasMaxLength(200).IsRequired(); + builder.Property("_description").HasColumnName("Description").HasMaxLength(1000).IsRequired(); + builder.Property("_remediation").HasColumnName("Remediation").HasMaxLength(1000); + builder.Property("_affectedResource").HasColumnName("AffectedResource").HasMaxLength(200); + builder.Property("_resolved").HasColumnName("Resolved"); + builder.Property("_resolvedAt").HasColumnName("ResolvedAt"); + + builder.Property("_severity") + .HasColumnName("SeverityId") + .HasConversion(v => v.Id, v => ViolationSeverity.FromId(v) ?? ViolationSeverity.Medium); + + builder.Ignore(x => x.DomainEvents); + } +} diff --git a/services/iam-service-net/src/IamService.Infrastructure/IamServiceContext.cs b/services/iam-service-net/src/IamService.Infrastructure/IamServiceContext.cs index 7ff96226..bb639258 100644 --- a/services/iam-service-net/src/IamService.Infrastructure/IamServiceContext.cs +++ b/services/iam-service-net/src/IamService.Infrastructure/IamServiceContext.cs @@ -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 public DbSet PrivilegedAccessGrants { get; set; } = null!; + /// + /// EN: Audit logs table. + /// VI: Bảng audit logs. + /// + public DbSet AuditLogs { get; set; } = null!; + + /// + /// EN: Compliance reports table. + /// VI: Bảng báo cáo tuân thủ. + /// + public DbSet ComplianceReports { get; set; } = null!; + + /// + /// EN: Compliance violations table. + /// VI: Bảng vi phạm tuân thủ. + /// + public DbSet ComplianceViolations { get; set; } = null!; + /// /// EN: Check if there's an active transaction. /// VI: Kiểm tra xem có transaction đang hoạt động không. diff --git a/services/iam-service-net/src/IamService.Infrastructure/Repositories/AuditLogRepository.cs b/services/iam-service-net/src/IamService.Infrastructure/Repositories/AuditLogRepository.cs new file mode 100644 index 00000000..9ba51aa7 --- /dev/null +++ b/services/iam-service-net/src/IamService.Infrastructure/Repositories/AuditLogRepository.cs @@ -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> GetByActorIdAsync(Guid actorId, int take = 100, CancellationToken cancellationToken = default) + => await _context.AuditLogs + .Where(x => EF.Property(x, "_actorId") == actorId) + .OrderByDescending(x => EF.Property(x, "_timestamp")) + .Take(take) + .ToListAsync(cancellationToken); + + public async Task> GetByResourceAsync(string resourceType, Guid resourceId, int take = 100, CancellationToken cancellationToken = default) + => await _context.AuditLogs + .Where(x => EF.Property(x, "_resourceType") == resourceType && EF.Property(x, "_resourceId") == resourceId) + .OrderByDescending(x => EF.Property(x, "_timestamp")) + .Take(take) + .ToListAsync(cancellationToken); + + public async Task> 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(x, "_timestamp") >= fromDate.Value); + if (toDate.HasValue) + query = query.Where(x => EF.Property(x, "_timestamp") <= toDate.Value); + if (eventTypeId.HasValue) + query = query.Where(x => EF.Property(x, "_eventType").Id == eventTypeId.Value); + if (actorId.HasValue) + query = query.Where(x => EF.Property(x, "_actorId") == actorId.Value); + if (!string.IsNullOrEmpty(resourceType)) + query = query.Where(x => EF.Property(x, "_resourceType") == resourceType); + + return await query + .OrderByDescending(x => EF.Property(x, "_timestamp")) + .Skip(skip).Take(take) + .ToListAsync(cancellationToken); + } + + public async Task GetCountAsync(DateTime? fromDate, DateTime? toDate, CancellationToken cancellationToken = default) + { + var query = _context.AuditLogs.AsQueryable(); + if (fromDate.HasValue) + query = query.Where(x => EF.Property(x, "_timestamp") >= fromDate.Value); + if (toDate.HasValue) + query = query.Where(x => EF.Property(x, "_timestamp") <= toDate.Value); + return await query.LongCountAsync(cancellationToken); + } +} diff --git a/services/iam-service-net/src/IamService.Infrastructure/Repositories/ComplianceReportRepository.cs b/services/iam-service-net/src/IamService.Infrastructure/Repositories/ComplianceReportRepository.cs new file mode 100644 index 00000000..f67bd1f7 --- /dev/null +++ b/services/iam-service-net/src/IamService.Infrastructure/Repositories/ComplianceReportRepository.cs @@ -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 GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + => await _context.ComplianceReports.FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + + public async Task GetByIdWithViolationsAsync(Guid id, CancellationToken cancellationToken = default) + => await _context.ComplianceReports.Include(x => x.Violations).FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + + public async Task> GetByTypeAsync(int reportTypeId, int take = 20, CancellationToken cancellationToken = default) + => await _context.ComplianceReports + .Where(x => EF.Property(x, "_reportType").Id == reportTypeId) + .OrderByDescending(x => EF.Property(x, "_createdAt")) + .Take(take) + .ToListAsync(cancellationToken); + + public async Task> GetRecentAsync(int take = 20, CancellationToken cancellationToken = default) + => await _context.ComplianceReports + .OrderByDescending(x => EF.Property(x, "_createdAt")) + .Take(take) + .ToListAsync(cancellationToken); + + public async Task> GetUnresolvedViolationsAsync(CancellationToken cancellationToken = default) + => await _context.ComplianceViolations + .Where(x => !EF.Property(x, "_resolved")) + .ToListAsync(cancellationToken); +}