diff --git a/config/systems.json b/config/systems.json
index ace4733..5376fc8 100644
--- a/config/systems.json
+++ b/config/systems.json
@@ -2,7 +2,7 @@
"default": {
"enabled": true,
"demask": true,
- "maskMode": "TOKEN",
+ "maskMode": "MASK",
"types": ["*"],
"requireCompanion": [
"CVV", "PIN", "DATE", "BIRTH_PLACE", "ADDRESS_COUNTRY",
diff --git a/src/main/java/ru/pdguard/PdGuardApplication.java b/src/main/java/ru/pdguard/PdGuardApplication.java
index 99b309a..601b0a0 100644
--- a/src/main/java/ru/pdguard/PdGuardApplication.java
+++ b/src/main/java/ru/pdguard/PdGuardApplication.java
@@ -6,14 +6,13 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Точка входа Spring Boot приложения.
*
- *
Модуль безопасности персональных данных: прокси между системой-потребителем
- * и LLM. Находит персональные данные, маскирует их и восстанавливает исходный
- * текст на обратном шаге.
+ *
Модуль безопасности персональных данных: прокси между системой-потребителем и LLM. Находит
+ * персональные данные, маскирует их и восстанавливает исходный текст на обратном шаге.
*/
@SpringBootApplication
public class PdGuardApplication {
- public static void main(String[] args) {
- SpringApplication.run(PdGuardApplication.class, args);
- }
-}
\ No newline at end of file
+ public static void main(String[] args) {
+ SpringApplication.run(PdGuardApplication.class, args);
+ }
+}
diff --git a/src/main/java/ru/pdguard/api/AdminResource.java b/src/main/java/ru/pdguard/api/AdminResource.java
index a8ab9dd..935c9e2 100644
--- a/src/main/java/ru/pdguard/api/AdminResource.java
+++ b/src/main/java/ru/pdguard/api/AdminResource.java
@@ -1,5 +1,7 @@
package ru.pdguard.api;
+import java.util.List;
+import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -7,34 +9,31 @@ import ru.pdguard.config.SystemPolicy;
import ru.pdguard.config.SystemsConfig;
import ru.pdguard.detect.RuleRegistry;
-import java.util.List;
-import java.util.Map;
-
/** Просмотр действующих настроек и принудительное их перечитывание. */
@RestController
public class AdminResource {
- private final SystemsConfig systems;
- private final RuleRegistry registry;
+ private final SystemsConfig systems;
+ private final RuleRegistry registry;
- public AdminResource(SystemsConfig systems, RuleRegistry registry) {
- this.systems = systems;
- this.registry = registry;
- }
+ public AdminResource(SystemsConfig systems, RuleRegistry registry) {
+ this.systems = systems;
+ this.registry = registry;
+ }
- @GetMapping("/admin/config")
- public Map config() {
- return systems.current();
- }
+ @GetMapping("/admin/config")
+ public Map config() {
+ return systems.current();
+ }
- @GetMapping("/admin/types")
- public List types() {
- return registry.knownTypes();
- }
+ @GetMapping("/admin/types")
+ public List types() {
+ return registry.knownTypes();
+ }
- @PostMapping("/admin/reload")
- public Map reload() {
- systems.reload();
- return systems.current();
- }
-}
\ No newline at end of file
+ @PostMapping("/admin/reload")
+ public Map reload() {
+ systems.reload();
+ return systems.current();
+ }
+}
diff --git a/src/main/java/ru/pdguard/api/HealthResource.java b/src/main/java/ru/pdguard/api/HealthResource.java
index cda0c1b..8ac108e 100644
--- a/src/main/java/ru/pdguard/api/HealthResource.java
+++ b/src/main/java/ru/pdguard/api/HealthResource.java
@@ -7,8 +7,8 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
public class HealthResource {
- @GetMapping("/health")
- public String health() {
- return "OK";
- }
-}
\ No newline at end of file
+ @GetMapping("/health")
+ public String health() {
+ return "OK";
+ }
+}
diff --git a/src/main/java/ru/pdguard/api/ProcessResource.java b/src/main/java/ru/pdguard/api/ProcessResource.java
index f2bebcc..d48e58d 100644
--- a/src/main/java/ru/pdguard/api/ProcessResource.java
+++ b/src/main/java/ru/pdguard/api/ProcessResource.java
@@ -18,116 +18,119 @@ import ru.pdguard.core.AdaptiveConcurrencyLimiter;
import ru.pdguard.core.Pipeline;
/**
- * Единственная точка входа контракта: маскирование и демаскирование по
- * {@code payload_id}.
+ * Единственная точка входа контракта: маскирование и демаскирование по {@code payload_id}.
*
- * Система-потребитель называет себя заголовком {@code X-System-Id}. Заголовка
- * нет или система неизвестна — применяются настройки {@code default}, поэтому
- * контракт работает и без него. Система, выключенная в настройках, получает
- * {@code 403}.
+ *
Система-потребитель называет себя заголовком {@code X-System-Id}. Заголовка нет или система
+ * неизвестна — применяются настройки {@code default}, поэтому контракт работает и без него.
+ * Система, выключенная в настройках, получает {@code 403}.
*
- *
При перегрузке отвечает {@code 429} с {@code Retry-After}. Порог перегрузки —
- * не фиксированное число запросов, а задержка обработки: {@link AdaptiveConcurrencyLimiter}
- * сам находит потолок конкурентности под то, сколько CPU реально досталось контейнеру,
- * вместо того чтобы копить запросы и упереться в таймаут вызывающей стороны.
+ *
При перегрузке отвечает {@code 429} с {@code Retry-After}. Порог перегрузки — не фиксированное
+ * число запросов, а задержка обработки: {@link AdaptiveConcurrencyLimiter} сам находит потолок
+ * конкурентности под то, сколько CPU реально досталось контейнеру, вместо того чтобы копить запросы
+ * и упереться в таймаут вызывающей стороны.
*/
@RestController
public class ProcessResource {
- private static final Logger LOG = LoggerFactory.getLogger(ProcessResource.class);
+ private static final Logger LOG = LoggerFactory.getLogger(ProcessResource.class);
- /** Заголовок, которым система-потребитель себя называет. */
- public static final String SYSTEM_HEADER = "X-System-Id";
+ /** Заголовок, которым система-потребитель себя называет. */
+ public static final String SYSTEM_HEADER = "X-System-Id";
- /** Общий секрет системы. Проверяется, только если он задан в настройках. */
- public static final String KEY_HEADER = "X-System-Key";
+ /** Общий секрет системы. Проверяется, только если он задан в настройках. */
+ public static final String KEY_HEADER = "X-System-Key";
- /** Имя метрики отклонённых запросов и имя её метки причины. */
- private static final String REJECTED_METRIC = "pdguard.requests.rejected";
- private static final String REASON_TAG = "reason";
+ /** Имя метрики отклонённых запросов и имя её метки причины. */
+ private static final String REJECTED_METRIC = "pdguard.requests.rejected";
- /**
- * Что отдаётся при внутреннем сбое. Ни одного знака из запроса: сбой на прямом
- * шаге иначе выпустил бы наружу незамаскированные персональные данные.
- */
- static final String PROCESSING_UNAVAILABLE = "[обработка недоступна]";
+ private static final String REASON_TAG = "reason";
- public record ProcessRequest(
- @JsonProperty("payload") String payload,
- @JsonProperty("payload_id") String payloadId) {
+ /**
+ * Что отдаётся при внутреннем сбое. Ни одного знака из запроса: сбой на прямом шаге иначе
+ * выпустил бы наружу незамаскированные персональные данные.
+ */
+ static final String PROCESSING_UNAVAILABLE = "[обработка недоступна]";
+
+ public record ProcessRequest(
+ @JsonProperty("payload") String payload, @JsonProperty("payload_id") String payloadId) {}
+
+ public record ProcessResponse(@JsonProperty("result") String result) {}
+
+ private final Pipeline pipeline;
+ private final SystemsConfig systems;
+ private final AdaptiveConcurrencyLimiter limiter;
+ private final Counter rejected;
+ private final Counter malformed;
+ private final Counter forbidden;
+ private final Counter failed;
+
+ public ProcessResource(
+ Pipeline pipeline,
+ SystemsConfig systems,
+ MeterRegistry meters,
+ @Value("${pdguard.min-concurrent:8}") int minConcurrent,
+ @Value("${pdguard.max-concurrent:2000}") int maxConcurrent,
+ @Value("${pdguard.target-latency-ms:200}") long targetLatencyMillis) {
+ this.pipeline = pipeline;
+ this.systems = systems;
+ this.limiter =
+ new AdaptiveConcurrencyLimiter(minConcurrent, maxConcurrent, targetLatencyMillis);
+ this.rejected = meters.counter(REJECTED_METRIC, REASON_TAG, "overload");
+ this.malformed = meters.counter(REJECTED_METRIC, REASON_TAG, "malformed");
+ this.forbidden = meters.counter(REJECTED_METRIC, REASON_TAG, "system_disabled");
+ this.failed = meters.counter(REJECTED_METRIC, REASON_TAG, "internal_error");
+ meters.gauge("pdguard.concurrency.limit", limiter, AdaptiveConcurrencyLimiter::limit);
+ meters.gauge("pdguard.concurrency.in.flight", limiter, AdaptiveConcurrencyLimiter::inFlight);
+ }
+
+ @PostMapping("/process")
+ public ResponseEntity process(
+ @RequestBody(required = false) ProcessRequest request,
+ @RequestHeader(value = SYSTEM_HEADER, required = false) String systemId,
+ @RequestHeader(value = KEY_HEADER, required = false) String systemKey) {
+ if (request == null
+ || request.payload() == null
+ || request.payloadId() == null
+ || request.payloadId().isBlank()) {
+ malformed.increment();
+ return ResponseEntity.badRequest()
+ .body(new ProcessResponse("payload и payload_id обязательны"));
}
- public record ProcessResponse(@JsonProperty("result") String result) {
+ SystemPolicy policy = systems.policyFor(systemId);
+ if (!policy.accepts(systemKey)) {
+ forbidden.increment();
+ LOG.warn("Системе {} отказано: неверный ключ", systemId);
+ return ResponseEntity.status(HttpStatus.FORBIDDEN)
+ .body(new ProcessResponse("Неверный ключ системы"));
+ }
+ if (!policy.enabled()) {
+ forbidden.increment();
+ LOG.warn("Системе {} обращение в модуль запрещено настройками", systemId);
+ return ResponseEntity.status(HttpStatus.FORBIDDEN)
+ .body(new ProcessResponse("Системе " + systemId + " обращение в модуль запрещено"));
}
- private final Pipeline pipeline;
- private final SystemsConfig systems;
- private final AdaptiveConcurrencyLimiter limiter;
- private final Counter rejected;
- private final Counter malformed;
- private final Counter forbidden;
- private final Counter failed;
-
- public ProcessResource(Pipeline pipeline, SystemsConfig systems, MeterRegistry meters,
- @Value("${pdguard.min-concurrent:8}") int minConcurrent,
- @Value("${pdguard.max-concurrent:2000}") int maxConcurrent,
- @Value("${pdguard.target-latency-ms:200}") long targetLatencyMillis) {
- this.pipeline = pipeline;
- this.systems = systems;
- this.limiter = new AdaptiveConcurrencyLimiter(minConcurrent, maxConcurrent, targetLatencyMillis);
- this.rejected = meters.counter(REJECTED_METRIC, REASON_TAG, "overload");
- this.malformed = meters.counter(REJECTED_METRIC, REASON_TAG, "malformed");
- this.forbidden = meters.counter(REJECTED_METRIC, REASON_TAG, "system_disabled");
- this.failed = meters.counter(REJECTED_METRIC, REASON_TAG, "internal_error");
- meters.gauge("pdguard.concurrency.limit", limiter, AdaptiveConcurrencyLimiter::limit);
- meters.gauge("pdguard.concurrency.in.flight", limiter, AdaptiveConcurrencyLimiter::inFlight);
+ if (!limiter.tryAcquire()) {
+ rejected.increment();
+ return ResponseEntity.status(429).header("Retry-After", "1").build();
}
-
- @PostMapping("/process")
- public ResponseEntity process(@RequestBody(required = false) ProcessRequest request,
- @RequestHeader(value = SYSTEM_HEADER, required = false) String systemId,
- @RequestHeader(value = KEY_HEADER, required = false) String systemKey) {
- if (request == null || request.payload() == null
- || request.payloadId() == null || request.payloadId().isBlank()) {
- malformed.increment();
- return ResponseEntity.badRequest()
- .body(new ProcessResponse("payload и payload_id обязательны"));
- }
-
- SystemPolicy policy = systems.policyFor(systemId);
- if (!policy.accepts(systemKey)) {
- forbidden.increment();
- LOG.warn("Системе {} отказано: неверный ключ", systemId);
- return ResponseEntity.status(HttpStatus.FORBIDDEN)
- .body(new ProcessResponse("Неверный ключ системы"));
- }
- if (!policy.enabled()) {
- forbidden.increment();
- LOG.warn("Системе {} обращение в модуль запрещено настройками", systemId);
- return ResponseEntity.status(HttpStatus.FORBIDDEN)
- .body(new ProcessResponse("Системе " + systemId + " обращение в модуль запрещено"));
- }
-
- if (!limiter.tryAcquire()) {
- rejected.increment();
- return ResponseEntity.status(429).header("Retry-After", "1").build();
- }
- long started = System.nanoTime();
- try {
- String result = pipeline.process(request.payload(), request.payloadId(), policy);
- return ResponseEntity.ok(new ProcessResponse(result));
- } catch (RuntimeException e) {
- // Ни 5xx, ни исходный текст. Пять подряд невалидных ответов останавливают
- // прогон, поэтому код остаётся 200 — но возвращать при сбое сам payload
- // нельзя: на прямом шаге наружу ушли бы незамаскированные ПД, ровно то,
- // ради чего сервис и существует. Ответ фиксированный: он ничего не
- // раскрывает и не выглядит порчей данных.
- failed.increment();
- LOG.error("payload_id={} обработка не удалась, отдан безопасный ответ",
- request.payloadId(), e);
- return ResponseEntity.ok(new ProcessResponse(PROCESSING_UNAVAILABLE));
- } finally {
- limiter.release(System.nanoTime() - started);
- }
+ long started = System.nanoTime();
+ try {
+ String result = pipeline.process(request.payload(), request.payloadId(), policy);
+ return ResponseEntity.ok(new ProcessResponse(result));
+ } catch (RuntimeException e) {
+ // Ни 5xx, ни исходный текст. Пять подряд невалидных ответов останавливают
+ // прогон, поэтому код остаётся 200 — но возвращать при сбое сам payload
+ // нельзя: на прямом шаге наружу ушли бы незамаскированные ПД, ровно то,
+ // ради чего сервис и существует. Ответ фиксированный: он ничего не
+ // раскрывает и не выглядит порчей данных.
+ failed.increment();
+ LOG.error(
+ "payload_id={} обработка не удалась, отдан безопасный ответ", request.payloadId(), e);
+ return ResponseEntity.ok(new ProcessResponse(PROCESSING_UNAVAILABLE));
+ } finally {
+ limiter.release(System.nanoTime() - started);
}
-}
\ No newline at end of file
+ }
+}
diff --git a/src/main/java/ru/pdguard/api/ProxyResource.java b/src/main/java/ru/pdguard/api/ProxyResource.java
index fd8d227..d697842 100644
--- a/src/main/java/ru/pdguard/api/ProxyResource.java
+++ b/src/main/java/ru/pdguard/api/ProxyResource.java
@@ -1,6 +1,7 @@
package ru.pdguard.api;
import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
@@ -14,8 +15,6 @@ import ru.pdguard.config.SystemsConfig;
import ru.pdguard.core.LlmClient;
import ru.pdguard.core.Pipeline;
-import java.util.Map;
-
/**
* Демонстрационное плечо к языковой модели: показывает всю цепочку целиком.
*
@@ -23,84 +22,94 @@ import java.util.Map;
* потребитель → маскирование → LLM → демаскирование → потребитель
*
*
- * В ответе видны все три текста — что ушло в модель, что она вернула и что
- * получил потребитель. Это и есть доказательство, что в модель не попало ничего
- * незамаскированного, а ответ вернулся с восстановленными значениями.
+ *
В ответе видны все три текста — что ушло в модель, что она вернула и что получил потребитель.
+ * Это и есть доказательство, что в модель не попало ничего незамаскированного, а ответ вернулся с
+ * восстановленными значениями.
*
- *
Ответ модели — другой текст, а не тот же самый, поэтому восстановить его по
- * идентификатору целиком нельзя: замена идёт пофрагментно. Звёздочки для этого не
- * годятся — одна и та же маска отвечала бы разным значениям, — поэтому здесь всегда
- * применяется обратимая подстановка, независимо от режима маскирования системы.
+ *
Ответ модели — другой текст, а не тот же самый, поэтому восстановить его по идентификатору
+ * целиком нельзя: замена идёт пофрагментно. Звёздочки для этого не годятся — одна и та же маска
+ * отвечала бы разным значениям, — поэтому здесь всегда применяется обратимая подстановка,
+ * независимо от режима маскирования системы.
*
- *
Контракт проверяющей системы это плечо не затрагивает: он живёт в
- * {@link ProcessResource}.
+ *
Контракт проверяющей системы это плечо не затрагивает: он живёт в {@link ProcessResource}.
*/
@RestController
public class ProxyResource {
- private static final Logger LOG = LoggerFactory.getLogger(ProxyResource.class);
+ private static final Logger LOG = LoggerFactory.getLogger(ProxyResource.class);
- public record ProxyRequest(@JsonProperty("prompt") String prompt) {
+ public record ProxyRequest(@JsonProperty("prompt") String prompt) {}
+
+ public record ProxyResponse(
+ @JsonProperty("prompt_masked") String promptMasked,
+ @JsonProperty("llm_response_masked") String llmResponseMasked,
+ @JsonProperty("response") String response,
+ @JsonProperty("replaced") Map replaced,
+ @JsonProperty("llm") String llm,
+ @JsonProperty("error") String error) {
+ public ProxyResponse {
+ replaced = replaced == null ? null : Map.copyOf(replaced);
+ }
+ }
+
+ private final Pipeline pipeline;
+ private final SystemsConfig systems;
+ private final LlmClient llm;
+
+ public ProxyResource(Pipeline pipeline, SystemsConfig systems, LlmClient llm) {
+ this.pipeline = pipeline;
+ this.systems = systems;
+ this.llm = llm;
+ }
+
+ @PostMapping("/proxy")
+ public ResponseEntity proxy(
+ @RequestBody(required = false) ProxyRequest request,
+ @RequestHeader(value = ProcessResource.SYSTEM_HEADER, required = false) String systemId,
+ @RequestHeader(value = ProcessResource.KEY_HEADER, required = false) String systemKey) {
+ if (request == null || request.prompt() == null || request.prompt().isBlank()) {
+ return ResponseEntity.badRequest()
+ .body(new ProxyResponse(null, null, null, null, null, "поле prompt обязательно"));
}
- public record ProxyResponse(
- @JsonProperty("prompt_masked") String promptMasked,
- @JsonProperty("llm_response_masked") String llmResponseMasked,
- @JsonProperty("response") String response,
- @JsonProperty("replaced") Map replaced,
- @JsonProperty("llm") String llm,
- @JsonProperty("error") String error) {
- public ProxyResponse {
- replaced = replaced == null ? null : Map.copyOf(replaced);
- }
+ SystemPolicy policy = systems.policyFor(systemId);
+ if (!policy.accepts(systemKey)) {
+ return ResponseEntity.status(HttpStatus.FORBIDDEN)
+ .body(new ProxyResponse(null, null, null, null, null, "Неверный ключ системы"));
+ }
+ if (!policy.enabled()) {
+ return ResponseEntity.status(HttpStatus.FORBIDDEN)
+ .body(
+ new ProxyResponse(
+ null,
+ null,
+ null,
+ null,
+ null,
+ "Системе " + systemId + " обращение в модуль запрещено"));
}
- private final Pipeline pipeline;
- private final SystemsConfig systems;
- private final LlmClient llm;
+ Pipeline.Masked masked = pipeline.maskWithRestorations(request.prompt(), policy);
+ LlmClient.Answer answer = llm.ask(masked.text());
+ String restored =
+ policy.demask() ? restore(answer.text(), masked.restorations()) : answer.text();
- public ProxyResource(Pipeline pipeline, SystemsConfig systems, LlmClient llm) {
- this.pipeline = pipeline;
- this.systems = systems;
- this.llm = llm;
+ LOG.info(
+ "proxy: система={} заменено={} модель={}",
+ policy.name(),
+ masked.restorations().size(),
+ answer.source());
+ return ResponseEntity.ok(
+ new ProxyResponse(
+ masked.text(), answer.text(), restored, masked.restorations(), answer.source(), null));
+ }
+
+ /** Возвращает исходные значения на место подстановок в ответе модели. */
+ private static String restore(String text, Map restorations) {
+ String result = text;
+ for (Map.Entry entry : restorations.entrySet()) {
+ result = result.replace(entry.getKey(), entry.getValue());
}
-
- @PostMapping("/proxy")
- public ResponseEntity proxy(@RequestBody(required = false) ProxyRequest request,
- @RequestHeader(value = ProcessResource.SYSTEM_HEADER, required = false) String systemId,
- @RequestHeader(value = ProcessResource.KEY_HEADER, required = false) String systemKey) {
- if (request == null || request.prompt() == null || request.prompt().isBlank()) {
- return ResponseEntity.badRequest()
- .body(new ProxyResponse(null, null, null, null, null, "поле prompt обязательно"));
- }
-
- SystemPolicy policy = systems.policyFor(systemId);
- if (!policy.accepts(systemKey)) {
- return ResponseEntity.status(HttpStatus.FORBIDDEN)
- .body(new ProxyResponse(null, null, null, null, null, "Неверный ключ системы"));
- }
- if (!policy.enabled()) {
- return ResponseEntity.status(HttpStatus.FORBIDDEN)
- .body(new ProxyResponse(null, null, null, null, null,
- "Системе " + systemId + " обращение в модуль запрещено"));
- }
-
- Pipeline.Masked masked = pipeline.maskWithRestorations(request.prompt(), policy);
- LlmClient.Answer answer = llm.ask(masked.text());
- String restored = policy.demask() ? restore(answer.text(), masked.restorations()) : answer.text();
-
- LOG.info("proxy: система={} заменено={} модель={}",
- policy.name(), masked.restorations().size(), answer.source());
- return ResponseEntity.ok(new ProxyResponse(masked.text(), answer.text(), restored,
- masked.restorations(), answer.source(), null));
- }
-
- /** Возвращает исходные значения на место подстановок в ответе модели. */
- private static String restore(String text, Map restorations) {
- String result = text;
- for (Map.Entry entry : restorations.entrySet()) {
- result = result.replace(entry.getKey(), entry.getValue());
- }
- return result;
- }
-}
\ No newline at end of file
+ return result;
+ }
+}
diff --git a/src/main/java/ru/pdguard/config/DictionaryConfiguration.java b/src/main/java/ru/pdguard/config/DictionaryConfiguration.java
index 748dada..2b01c54 100644
--- a/src/main/java/ru/pdguard/config/DictionaryConfiguration.java
+++ b/src/main/java/ru/pdguard/config/DictionaryConfiguration.java
@@ -8,18 +8,17 @@ import ru.pdguard.detect.NameDictionary;
/**
* Конфигурация словарей распознавания ФИО.
*
- * {@link NameDictionary} работает через статические методы и не требует
- * экземпляра, но путь к внешнему файлу денилиста задаётся из настроек при
- * старте. Бин здесь нужен только для того, чтобы Spring подставил значение
- * {@code pdguard.well-known-file} и передал его словарю.
+ *
{@link NameDictionary} работает через статические методы и не требует экземпляра, но путь к
+ * внешнему файлу денилиста задаётся из настроек при старте. Бин здесь нужен только для того, чтобы
+ * Spring подставил значение {@code pdguard.well-known-file} и передал его словарю.
*/
@Configuration
public class DictionaryConfiguration {
- @Bean
- public NameDictionary nameDictionary(
- @Value("${pdguard.well-known-file:config/well-known.txt}") String wellKnownFile) {
- NameDictionary.configure(wellKnownFile);
- return NameDictionary.create();
- }
-}
\ No newline at end of file
+ @Bean
+ public NameDictionary nameDictionary(
+ @Value("${pdguard.well-known-file:config/well-known.txt}") String wellKnownFile) {
+ NameDictionary.configure(wellKnownFile);
+ return NameDictionary.create();
+ }
+}
diff --git a/src/main/java/ru/pdguard/config/SystemPolicy.java b/src/main/java/ru/pdguard/config/SystemPolicy.java
index 575f2ff..ee84fae 100644
--- a/src/main/java/ru/pdguard/config/SystemPolicy.java
+++ b/src/main/java/ru/pdguard/config/SystemPolicy.java
@@ -1,81 +1,96 @@
package ru.pdguard.config;
+import java.util.Set;
import ru.pdguard.detect.PdTypes;
import ru.pdguard.mask.MaskMode;
-import java.util.Set;
-
/**
* Правила обработки для одной системы-потребителя.
*
- * @param name имя системы; им же разделяется хранилище соответствий,
- * чтобы одна система не могла достать данные другой
- * @param enabled разрешено ли системе обращаться в модуль
- * @param demask выполняется ли для системы обратное преобразование
- * @param maskMode вид замены: звёздочки, токен или синтетическое значение
- * @param types типы ПД к маскированию; {@code "*"} — все известные
- * @param key общий секрет системы; задан — заголовок {@code X-System-Key} обязан
- * совпасть, иначе имя системы можно было бы просто назвать.
- * Только знаки ASCII: заголовки HTTP передаются в Latin-1,
- * и кириллица в ключе до сервиса доедет искажённой
- * @param requireCompanion типы, которые маскируются только вместе с ПД другого типа:
- * пин-код сам по себе безвреден, пин-код рядом с номером
- * карты — уже нет; то же для даты без якорного слова, места
- * рождения («Нижний Новгород» в рассказе о городе — не адрес
- * клиента) и страны («цены выросли в Казахстане» — не гражданство).
- * Сюда же банковские реквизиты — счёт, БИК, ОГРН, ОГРНИП, КПП:
- * сами по себе они опознают организацию или счёт, а не человека,
- * и в перечне типов из задания их нет. Рядом с именем клиента
- * они становятся его данными и маскируются.
- * Сюда же доход и биометрия. Сумма заработка без человека —
- * статистика («доход домохозяйств вырос до 74 500 руб»), а не
- * персональные данные. Биометрия же в тексте не встречается
- * вовсе: это шаблон в базе, и правило маскирует лишь само
- * упоминание, то есть слово, а не данные. Чувствителен здесь
- * факт, что биометрию сдал названный человек, — а он и
- * существует только при имени рядом
+ * @param name имя системы; им же разделяется хранилище соответствий, чтобы одна система не могла
+ * достать данные другой
+ * @param enabled разрешено ли системе обращаться в модуль
+ * @param demask выполняется ли для системы обратное преобразование
+ * @param maskMode вид замены: звёздочки, токен или синтетическое значение
+ * @param types типы ПД к маскированию; {@code "*"} — все известные
+ * @param key общий секрет системы; задан — заголовок {@code X-System-Key} обязан совпасть, иначе
+ * имя системы можно было бы просто назвать. Только знаки ASCII: заголовки HTTP передаются в
+ * Latin-1, и кириллица в ключе до сервиса доедет искажённой
+ * @param requireCompanion типы, которые маскируются только вместе с ПД другого типа: пин-код сам по
+ * себе безвреден, пин-код рядом с номером карты — уже нет; то же для даты без якорного слова,
+ * места рождения («Нижний Новгород» в рассказе о городе — не адрес клиента) и страны («цены
+ * выросли в Казахстане» — не гражданство). Сюда же банковские реквизиты — счёт, БИК, ОГРН,
+ * ОГРНИП, КПП: сами по себе они опознают организацию или счёт, а не человека, и в перечне типов
+ * из задания их нет. Рядом с именем клиента они становятся его данными и маскируются. Сюда же
+ * доход и биометрия. Сумма заработка без человека — статистика («доход домохозяйств вырос до 74
+ * 500 руб»), а не персональные данные. Биометрия же в тексте не встречается вовсе: это шаблон в
+ * базе, и правило маскирует лишь само упоминание, то есть слово, а не данные. Чувствителен
+ * здесь факт, что биометрию сдал названный человек, — а он и существует только при имени рядом
*/
-public record SystemPolicy(String name, boolean enabled, boolean demask, MaskMode maskMode,
- Set types, Set requireCompanion, String key) {
+public record SystemPolicy(
+ String name,
+ boolean enabled,
+ boolean demask,
+ MaskMode maskMode,
+ Set types,
+ Set requireCompanion,
+ String key) {
- public static final String ALL = "*";
+ public static final String ALL = "*";
- /** Имя политики по умолчанию; оно же разделяет хранилище для запросов без заголовка. */
- public static final String DEFAULT_NAME = "default";
+ /** Имя политики по умолчанию; оно же разделяет хранилище для запросов без заголовка. */
+ public static final String DEFAULT_NAME = "default";
- /** Политика по умолчанию: маскируем всё, что умеем, обратное преобразование включено. */
- public static final SystemPolicy DEFAULT = new SystemPolicy(
- DEFAULT_NAME, true, true, MaskMode.MASK, Set.of(ALL),
- Set.of(PdTypes.CVV, PdTypes.PIN, PdTypes.DATE, PdTypes.BIRTH_PLACE, PdTypes.ADDRESS_COUNTRY,
- PdTypes.ACCOUNT_NUMBER, PdTypes.BIK, PdTypes.OGRN, PdTypes.OGRNIP, PdTypes.KPP,
- PdTypes.INCOME, PdTypes.BIOMETRIC), null);
+ /** Политика по умолчанию: маскируем всё, что умеем, обратное преобразование включено. */
+ public static final SystemPolicy DEFAULT =
+ new SystemPolicy(
+ DEFAULT_NAME,
+ true,
+ true,
+ MaskMode.MASK,
+ Set.of(ALL),
+ Set.of(
+ PdTypes.CVV,
+ PdTypes.PIN,
+ PdTypes.DATE,
+ PdTypes.BIRTH_PLACE,
+ PdTypes.ADDRESS_COUNTRY,
+ PdTypes.ACCOUNT_NUMBER,
+ PdTypes.BIK,
+ PdTypes.OGRN,
+ PdTypes.OGRNIP,
+ PdTypes.KPP,
+ PdTypes.INCOME,
+ PdTypes.BIOMETRIC),
+ null);
- public SystemPolicy {
- types = Set.copyOf(types);
- requireCompanion = Set.copyOf(requireCompanion);
+ public SystemPolicy {
+ types = Set.copyOf(types);
+ requireCompanion = Set.copyOf(requireCompanion);
+ }
+
+ /** Политика только для перечисленных типов, с остальными настройками по умолчанию. */
+ public static SystemPolicy forTypes(String... types) {
+ return new SystemPolicy(
+ DEFAULT_NAME, true, true, MaskMode.MASK, Set.of(types), DEFAULT.requireCompanion(), null);
+ }
+
+ /** Совпадает ли предъявленный ключ. Ключ не задан — проверка не применяется. */
+ public boolean accepts(String presentedKey) {
+ if (key == null || key.isBlank()) {
+ return true;
}
+ return java.security.MessageDigest.isEqual(
+ key.getBytes(java.nio.charset.StandardCharsets.UTF_8),
+ (presentedKey == null ? "" : presentedKey)
+ .getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ }
- /** Политика только для перечисленных типов, с остальными настройками по умолчанию. */
- public static SystemPolicy forTypes(String... types) {
- return new SystemPolicy(DEFAULT_NAME, true, true, MaskMode.MASK,
- Set.of(types), DEFAULT.requireCompanion(), null);
- }
+ public boolean allows(String type) {
+ return types.contains(ALL) || types.contains(type);
+ }
- /** Совпадает ли предъявленный ключ. Ключ не задан — проверка не применяется. */
- public boolean accepts(String presentedKey) {
- if (key == null || key.isBlank()) {
- return true;
- }
- return java.security.MessageDigest.isEqual(
- key.getBytes(java.nio.charset.StandardCharsets.UTF_8),
- (presentedKey == null ? "" : presentedKey).getBytes(java.nio.charset.StandardCharsets.UTF_8));
- }
-
- public boolean allows(String type) {
- return types.contains(ALL) || types.contains(type);
- }
-
- public boolean needsCompanion(String type) {
- return requireCompanion.contains(type);
- }
+ public boolean needsCompanion(String type) {
+ return requireCompanion.contains(type);
+ }
}
diff --git a/src/main/java/ru/pdguard/config/SystemsConfig.java b/src/main/java/ru/pdguard/config/SystemsConfig.java
index b9552cb..ca72b5a 100644
--- a/src/main/java/ru/pdguard/config/SystemsConfig.java
+++ b/src/main/java/ru/pdguard/config/SystemsConfig.java
@@ -1,12 +1,6 @@
package ru.pdguard.config;
import com.fasterxml.jackson.databind.ObjectMapper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
-import ru.pdguard.mask.MaskMode;
-
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -14,127 +8,151 @@ import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.concurrent.atomic.AtomicReference;
import java.util.Set;
import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicReference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import ru.pdguard.mask.MaskMode;
/**
* Список систем, которым разрешено обращаться в модуль, и правила для каждой.
*
- * Читается из внешнего файла, чтобы настройки менялись без пересборки. Файл
- * перечитывается сам, когда меняется время его изменения; проверка выполняется
- * не чаще раза в секунду, чтобы не ходить в файловую систему на каждом запросе.
- * Файла нет — работают настройки по умолчанию, и сервис поднимается без него.
+ *
Читается из внешнего файла, чтобы настройки менялись без пересборки. Файл перечитывается сам,
+ * когда меняется время его изменения; проверка выполняется не чаще раза в секунду, чтобы не ходить
+ * в файловую систему на каждом запросе. Файла нет — работают настройки по умолчанию, и сервис
+ * поднимается без него.
*/
@Component
public final class SystemsConfig {
- private static final Logger LOG = LoggerFactory.getLogger(SystemsConfig.class);
+ private static final Logger LOG = LoggerFactory.getLogger(SystemsConfig.class);
- /** Имя политики, которая применяется к запросам без заголовка системы. */
- public static final String DEFAULT_SYSTEM = "default";
+ /** Имя политики, которая применяется к запросам без заголовка системы. */
+ public static final String DEFAULT_SYSTEM = "default";
- private static final long RECHECK_MILLIS = 1000;
+ private static final long RECHECK_MILLIS = 1000;
- /** Описание одной системы в файле настроек. */
- public record SystemEntry(Boolean enabled, Boolean demask, String maskMode,
- List types, List requireCompanion, String key) {
- public SystemEntry {
- types = types == null ? null : List.copyOf(types);
- requireCompanion = requireCompanion == null ? null : List.copyOf(requireCompanion);
- }
+ /** Описание одной системы в файле настроек. */
+ public record SystemEntry(
+ Boolean enabled,
+ Boolean demask,
+ String maskMode,
+ List types,
+ List requireCompanion,
+ String key) {
+ public SystemEntry {
+ types = types == null ? null : List.copyOf(types);
+ requireCompanion = requireCompanion == null ? null : List.copyOf(requireCompanion);
}
+ }
- private final Path file;
- private final ObjectMapper mapper;
+ private final Path file;
+ private final ObjectMapper mapper;
- private final AtomicReference