diff --git a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/web/AuditAdminController.java b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/web/AuditAdminController.java index 18eabe5..9b659b0 100644 --- a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/web/AuditAdminController.java +++ b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/web/AuditAdminController.java @@ -1,7 +1,10 @@ package cloud.nstart.terravault.ordinis.restapi.web; +import cloud.nstart.terravault.ordinis.auth.ScopeContext; +import cloud.nstart.terravault.ordinis.domain.DataScope; import cloud.nstart.terravault.ordinis.domain.audit.AuditLog; import cloud.nstart.terravault.ordinis.domain.audit.AuditLogRepository; +import cloud.nstart.terravault.ordinis.restapi.error.OrdinisException; import com.fasterxml.jackson.databind.JsonNode; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -20,9 +23,11 @@ import java.util.UUID; * Admin endpoints для audit log. Read-only, фильтры опциональны. * * */ @RestController @@ -32,9 +37,29 @@ public class AuditAdminController { private static final int MAX_EXPORT_ROWS = 10_000; private final AuditLogRepository repository; + private final ScopeContext scopeContext; - public AuditAdminController(AuditLogRepository repository) { + public AuditAdminController(AuditLogRepository repository, ScopeContext scopeContext) { this.repository = repository; + this.scopeContext = scopeContext; + } + + /** INTERNAL+ scope для просмотра audit log. PUBLIC отказывается. */ + private void requireInternal() { + if (!scopeContext.canAccess(DataScope.INTERNAL)) { + throw OrdinisException.forbidden( + "audit_internal_required", + "Audit log доступен только с INTERNAL или RESTRICTED scope."); + } + } + + /** RESTRICTED scope для bulk-операций (CSV экспорт). */ + private void requireRestricted() { + if (!scopeContext.canAccess(DataScope.RESTRICTED)) { + throw OrdinisException.forbidden( + "audit_restricted_required", + "Audit bulk export требует RESTRICTED scope (admin role)."); + } } @GetMapping @@ -47,6 +72,7 @@ public class AuditAdminController { @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime to, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "50") int size) { + requireInternal(); Page result = repository.search( nullIfBlank(dictionaryName), nullIfBlank(userId), @@ -66,6 +92,7 @@ public class AuditAdminController { @RequestParam(required = false) String businessKey, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime from, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime to) { + requireRestricted(); Page result = repository.search( nullIfBlank(dictionaryName), nullIfBlank(userId), diff --git a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/web/OutboxAdminController.java b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/web/OutboxAdminController.java index 145999b..e234d11 100644 --- a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/web/OutboxAdminController.java +++ b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/web/OutboxAdminController.java @@ -1,7 +1,10 @@ package cloud.nstart.terravault.ordinis.restapi.web; +import cloud.nstart.terravault.ordinis.auth.ScopeContext; +import cloud.nstart.terravault.ordinis.domain.DataScope; import cloud.nstart.terravault.ordinis.domain.outbox.OutboxEvent; import cloud.nstart.terravault.ordinis.domain.outbox.OutboxEventRepository; +import cloud.nstart.terravault.ordinis.restapi.error.OrdinisException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; @@ -18,19 +21,45 @@ import java.util.Map; * Admin endpoints для outbox observability. Только чтение — replay делается * вручную через SQL (UPDATE outbox_events SET dlq_at=NULL, attempt_count=0 WHERE id=...). * Авто-replay сделаем когда появится первый реальный кейс DLQ. + * + *

Access: + *

*/ @RestController @RequestMapping("/api/v1/admin/outbox") public class OutboxAdminController { private final OutboxEventRepository repository; + private final ScopeContext scopeContext; - public OutboxAdminController(OutboxEventRepository repository) { + public OutboxAdminController(OutboxEventRepository repository, ScopeContext scopeContext) { this.repository = repository; + this.scopeContext = scopeContext; + } + + private void requireInternal() { + if (!scopeContext.canAccess(DataScope.INTERNAL)) { + throw OrdinisException.forbidden( + "outbox_internal_required", + "Outbox stats доступен только с INTERNAL или RESTRICTED scope."); + } + } + + private void requireRestricted() { + if (!scopeContext.canAccess(DataScope.RESTRICTED)) { + throw OrdinisException.forbidden( + "outbox_restricted_required", + "Outbox DLQ payloads требуют RESTRICTED scope (admin role)."); + } } @GetMapping("/stats") public Map stats() { + requireInternal(); return Map.of( "pending", repository.countPending(), "dlq", repository.countByDlqAtIsNotNull() @@ -41,6 +70,7 @@ public class OutboxAdminController { public Page dlq( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "50") int size) { + requireRestricted(); Page events = repository.findByDlqAtIsNotNull( PageRequest.of(page, Math.min(size, 200), Sort.by(Sort.Direction.DESC, "dlqAt"))); return events.map(DlqEntry::from);