diff --git a/ordinis-app/src/main/resources/application.yml b/ordinis-app/src/main/resources/application.yml
index 1b5078f..954f913 100644
--- a/ordinis-app/src/main/resources/application.yml
+++ b/ordinis-app/src/main/resources/application.yml
@@ -158,3 +158,11 @@ spring:
hibernate:
ddl-auto: none
open-in-view: false
+
+# Auth (см. ordinis-auth: SecurityConfig). public-key из Vault.
+ordinis:
+ auth:
+ public-key: ${key:}
+ issuer: ${jwt-issuer:}
+ require-authentication: ${ORDINIS_AUTH_REQUIRED:false}
+ allow-query-scope: ${ORDINIS_AUTH_ALLOW_QUERY_SCOPE:true}
diff --git a/ordinis-auth/pom.xml b/ordinis-auth/pom.xml
new file mode 100644
index 0000000..e1d50ba
--- /dev/null
+++ b/ordinis-auth/pom.xml
@@ -0,0 +1,35 @@
+
+
{@code publicKey} — RSA public key (PEM, без BEGIN/END заголовков, base64). + * Подбрасывается через Vault: secret/ordinis/cuod/jwt-public-key.
+ * + *{@code requireAuthentication=true} → 401 на любой запрос без валидного JWT. + * При false работает permissive режим — анонимный запрос имеет только PUBLIC scope, + * что подходит для read-api на raw публичных endpoint'ах.
+ * + *{@code allowQueryScope=true} включает legacy {@code ?as_scope=...} query param + * (для smoke-тестов и dev). В prod рекомендуется false.
+ */ +@ConfigurationProperties(prefix = "ordinis.auth") +public record OrdinisAuthProperties( + String publicKey, + String issuer, + String scopeClaim, + boolean requireAuthentication, + boolean allowQueryScope) { + + public OrdinisAuthProperties { + if (scopeClaim == null) scopeClaim = "scopes"; + if (issuer == null) issuer = ""; + } +} diff --git a/ordinis-auth/src/main/java/cloud/nstart/terravault/ordinis/auth/ScopeContext.java b/ordinis-auth/src/main/java/cloud/nstart/terravault/ordinis/auth/ScopeContext.java new file mode 100644 index 0000000..c386d38 --- /dev/null +++ b/ordinis-auth/src/main/java/cloud/nstart/terravault/ordinis/auth/ScopeContext.java @@ -0,0 +1,97 @@ +package cloud.nstart.terravault.ordinis.auth; + +import cloud.nstart.terravault.ordinis.domain.DataScope; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +/** + * Резолвер активных scope для текущего запроса. + * + *Источник scope (приоритет сверху-вниз): + *
Scope authorisation: в v1 берём из query param {@code as_scope} (для - * dev/curl). В prod — из JWT (@nstart/auth) — сделаем через filter позже. + *
Scope authorisation: JWT claim {@code scopes} через {@link ScopeContext}.
+ * Если запрос без JWT и {@code ordinis.auth.allow-query-scope=true} — fallback
+ * на legacy query param {@code ?as_scope=}; иначе только PUBLIC.
*/
@RestController
@RequestMapping("/api/v1/{dictionaryName}/records")
public class DictionaryReadController {
private final RecordReadService service;
+ private final ScopeContext scopeContext;
private final Counter scopeDeniedCounter;
- public DictionaryReadController(RecordReadService service, MeterRegistry meterRegistry) {
+ public DictionaryReadController(
+ RecordReadService service, ScopeContext scopeContext, MeterRegistry meterRegistry) {
this.service = service;
+ this.scopeContext = scopeContext;
this.scopeDeniedCounter = Counter.builder("ordinis_scope_access_denied_total")
- .description("Попытки чтения недоступного scope (v1 — из query param)")
+ .description("Попытки чтения недоступного scope")
.register(meterRegistry);
}
@@ -45,10 +48,10 @@ public class DictionaryReadController {
@PathVariable String dictionaryName,
@PathVariable String businessKey,
@RequestHeader(value = HttpHeaders.ACCEPT_LANGUAGE, required = false) String acceptLanguage,
- @RequestParam(value = "as_scope", defaultValue = "PUBLIC") String asScope,
+ @RequestParam(value = "as_scope", required = false) String asScope,
@RequestParam(value = "at", required = false) OffsetDateTime at) {
- List> list(
@PathVariable String dictionaryName,
@RequestHeader(value = HttpHeaders.ACCEPT_LANGUAGE, required = false) String acceptLanguage,
- @RequestParam(value = "as_scope", defaultValue = "PUBLIC") String asScope,
+ @RequestParam(value = "as_scope", required = false) String asScope,
@RequestParam(value = "at", required = false) OffsetDateTime at) {
- List