refactor: подтянуть Legal NER, переформатировать код и добавить тесты

Слияние с 685ec97 (третья ступень NER для юридических реквизитов),
код приведён к google-java-format, добавлены юнит-тесты
AdaptiveConcurrencyLimiter/SystemsConfig/PayloadCipher.
This commit is contained in:
Максименко Никита Владимирович
2026-09-23 22:02:45 +03:00
parent 84b5adcb3f
commit 573d94cca8
79 changed files with 7385 additions and 6430 deletions
@@ -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<String> types, Set<String> requireCompanion, String key) {
public record SystemPolicy(
String name,
boolean enabled,
boolean demask,
MaskMode maskMode,
Set<String> types,
Set<String> 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);
}
}