SUPPORT-9509: Fix mchd

This commit is contained in:
Eduard Tihomiorv 2025-10-24 15:26:12 +03:00
parent 737e126f39
commit 4590a6f975
9 changed files with 210 additions and 122 deletions

View file

@ -50,12 +50,11 @@ import org.w3c.dom.Document;
import org.w3c.dom.Node;
import ru.micord.ervu.audit.service.AuditService;
import ru.micord.ervu.exception.JsonParsingException;
import ru.micord.ervu.journal.SenderInfo;
import ru.micord.ervu.kafka.service.ReplyingKafkaService;
import ru.micord.ervu.security.esia.config.EsiaConfig;
import ru.micord.ervu.security.esia.model.EmployeeModel;
import ru.micord.ervu.security.esia.model.MchdInfoModel;
import ru.micord.ervu.security.esia.model.PersonModel;
import ru.micord.ervu.security.esia.model.VerifyDocumentSignResponse;
import ru.micord.ervu.security.esia.model.*;
import ru.micord.ervu.security.esia.service.MchdService;
import ru.micord.ervu.security.esia.service.UlDataService;
import ru.micord.ervu.security.esia.EsiaAuthInfoStore;
import ru.micord.ervu.security.webbpm.jwt.UserIdsPair;
@ -91,6 +90,7 @@ public class EmployeeInfoFileUploadService {
private final KafkaTemplate<String, String> kafkaTemplate;
private final InteractionService interactionService;
private final UlDataService ulDataService;
private final MchdService mchdService;
private final AuditService auditService;
private final ObjectMapper objectMapper;
private final EsiaConfig esiaConfig;
@ -116,7 +116,8 @@ public class EmployeeInfoFileUploadService {
AuditService auditService,
ObjectMapper objectMapper,
EsiaConfig esiaConfig,
@Qualifier("fileTemplate") KafkaTemplate<String, String> kafkaTemplate) {
@Qualifier("fileTemplate") KafkaTemplate<String, String> kafkaTemplate,
MchdService mchdService) {
this.webDavClient = webDavClient;
this.employeeInfoKafkaMessageService = employeeInfoKafkaMessageService;
this.replyingKafkaService = replyingKafkaService;
@ -126,6 +127,7 @@ public class EmployeeInfoFileUploadService {
this.objectMapper = objectMapper;
this.esiaConfig = esiaConfig;
this.kafkaTemplate = kafkaTemplate;
this.mchdService = mchdService;
}
public boolean saveEmployeeInformationFiles(MultipartFile multipartFile,
@ -150,10 +152,14 @@ public class EmployeeInfoFileUploadService {
String fio = convertToFio(personModel.getFirstName(), personModel.getMiddleName(),
personModel.getLastName()
);
UploadOrgInfo uploadOrgInfo = employeeInfoKafkaMessageService.getOrgInfo(accessToken, ervuId,
esiaUserId, personModel
);
OrganizationModel organizationModel = ulDataService.getOrganizationModel(accessToken);
SenderInfo senderInfo = new SenderInfo();
senderInfo.setFirstName(personModel.getFirstName());
senderInfo.setLastName(personModel.getLastName());
senderInfo.setMiddleName(personModel.getMiddleName());
senderInfo.setPrnOid(esiaUserId);
UploadOrgInfo uploadOrgInfo = new UploadOrgInfo(organizationModel.getFullName(), ervuId, senderInfo,
organizationModel.getOid());
EmployeeInfoFileFormType employeeInfoFileFormType = EmployeeInfoFileFormType.valueOf(formType);
@ -251,7 +257,7 @@ public class EmployeeInfoFileUploadService {
}
return validateSignerAndMchd(verifyDocumentSignResponse, chiefModel, uploadOrgInfo,
mchdFile, accessToken, fileInfo, signFileInfo, mchdFileInfo, ervuId, response
mchdFile, fileInfo, signFileInfo, mchdFileInfo, ervuId, response, organizationModel.getOgrn()
);
}
@ -321,9 +327,8 @@ public class EmployeeInfoFileUploadService {
}
private boolean validateSignerAndMchd(VerifyDocumentSignResponse verifyDocumentSignResponse,
EmployeeModel chiefModel, UploadOrgInfo uploadOrgInfo, MultipartFile mchdFile,
String accessToken, FileInfo fileInfo, FileInfo signFileInfo,
FileInfo mchdFileInfo, String ervuId, String response) {
EmployeeModel chiefModel, UploadOrgInfo uploadOrgInfo, MultipartFile mchdFile, FileInfo fileInfo, FileInfo signFileInfo,
FileInfo mchdFileInfo, String ervuId, String response, String ogrn) {
String signerInfo = verifyDocumentSignResponse.getSignerInfo();
Map<String, String> signerInfoMap = parseKeyValuePairs(signerInfo);
@ -367,9 +372,7 @@ public class EmployeeInfoFileUploadService {
new FileInfo[] {fileInfo, signFileInfo, mchdFileInfo}, fileInfo.getFileStatus()
);
try {
validateMchd(mchdFile, accessToken, signerInfoMap.get("SN") + " " + signerInfoMap.get("G"),
chiefFirstName, chiefLastName, chiefMiddleName
);
validateMchd(mchdFile, ogrn, signerInfoMap.get("СНИЛС"));
if (sendMessage(fileStatusResponse)) {
interactionService.updateStatus(fileInfo.getFileId(), "Направлено в ЕРВУ", ervuId);
@ -537,8 +540,7 @@ public class EmployeeInfoFileUploadService {
}
}
private void validateMchd(MultipartFile mchdFile, String accessToken, String agentFio,
String chiefFirstName, String chiefLastName, String chiefMiddleName) {
private void validateMchd(MultipartFile mchdFile, String ogrn, String snils) {
String mchdGuid;
try {
mchdGuid = getMchdGuid(mchdFile);
@ -546,31 +548,27 @@ public class EmployeeInfoFileUploadService {
catch (Exception e) {
throw new LocalizedException("mchd_cant_parse", MESSAGE_SOURCE);
}
MchdInfoModel mchdInfoModel = ulDataService.getMchdInfoModel(mchdGuid, accessToken);
MchdInfoModel mchdInfoModel = mchdService.getMchdInfoModel(mchdGuid);
if (!mchdInfoModel.getStatus().equals("A")) {
throw new LocalizedException("mchd_expired", MESSAGE_SOURCE);
}
boolean validAgent = mchdInfoModel.getAgents()
.getElements()
.stream()
.anyMatch(agent1 -> agentFio.equalsIgnoreCase(
agent1.getPerson().getLastName() + " " + agent1.getPerson().getFirstName() + " "
+ agent1.getPerson().getMiddleName()));
.anyMatch(agent1 -> agent1.getPerson().getSnils().equals(snils));
if (!validAgent) {
throw new LocalizedException("mchd_validate_agent", MESSAGE_SOURCE);
}
while (mchdInfoModel.getParentGuid() != null) {
mchdInfoModel = ulDataService.getMchdInfoModel(mchdInfoModel.getParentGuid(), accessToken);
mchdInfoModel = mchdService.getMchdInfoModel(mchdInfoModel.getParentGuid());
if (!mchdInfoModel.getStatus().equals("A")) {
throw new LocalizedException("mchd_tree_expired", MESSAGE_SOURCE);
}
}
MchdInfoModel.Element principal = mchdInfoModel.getPrincipals().getElements().get(0);
MchdInfoModel.Person chief = principal.getOrganization().getChief();
boolean principalFioEquals = chief.getFirstName().equalsIgnoreCase(chiefFirstName) &&
chief.getLastName().equalsIgnoreCase(chiefLastName) &&
chief.getMiddleName().equalsIgnoreCase(chiefMiddleName);
if (!principalFioEquals) {
String ogrnMCHD = principal.getOrganization().getOgrn();
boolean ogrnEqueals = ogrnMCHD.equals(ogrn);
if (!ogrnEqueals) {
throw new LocalizedException("mchd_validate_principal", MESSAGE_SOURCE);
}
}

View file

@ -23,28 +23,6 @@ public class EmployeeInfoKafkaMessageService {
this.ulDataService = ulDataService;
}
public EmployeeInfoKafkaMessage getKafkaMessage(String fileId, String fileUrl, String fileName,
EmployeeInfoFileFormType formType, String departureDateTime, String accessToken,
String offset, FileStatus fileStatus, String ervuId, String prnOid,
PersonModel personModel, long fileSize, String type) {
return new EmployeeInfoKafkaMessage(
getOrgInfo(accessToken, ervuId, prnOid, personModel),
new FileInfo[] {
getFileInfo(
fileId,
fileUrl,
fileName,
formType,
departureDateTime,
offset,
fileStatus,
fileSize,
type
),
}
);
}
public EmployeeInfoKafkaMessage getKafkaMessage(UploadOrgInfo orgInfo, FileInfo[] filesInfo) {
return new EmployeeInfoKafkaMessage(orgInfo, filesInfo);
}
@ -65,15 +43,4 @@ public class EmployeeInfoKafkaMessageService {
type
);
}
public UploadOrgInfo getOrgInfo(String accessToken, String ervuId, String prnOid, PersonModel personModel) {
OrganizationModel organizationModel = ulDataService.getOrganizationModel(accessToken);
SenderInfo senderInfo = new SenderInfo();
senderInfo.setFirstName(personModel.getFirstName());
senderInfo.setLastName(personModel.getLastName());
senderInfo.setMiddleName(personModel.getMiddleName());
senderInfo.setPrnOid(prnOid);
return new UploadOrgInfo(organizationModel.getFullName(), ervuId, senderInfo,
organizationModel.getOid());
}
}

View file

@ -5,6 +5,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -27,6 +28,7 @@ public class EsiaAuthInfoStore {
private static final Map<String, List<ExpiringState>> PRNS_UUID_STATE_MAP = new ConcurrentHashMap<>();
private static final MessageSourceAccessor MESSAGE_SOURCE = MessageBundleUtils.createAccessor(
"messages/common_errors_messages");
private static final AtomicReference<ExpiringToken> SYSTEM_ESIA_TOKEN = new AtomicReference<>();
public static void addAccessToken(String prnOid, String token, long expiresIn) {
if (token != null) {
@ -152,4 +154,20 @@ public class EsiaAuthInfoStore {
});
}
}
public static void setSystemEsiaToken(String token, Long expiresIn) {
if (token != null) {
long expiryTime = System.currentTimeMillis() + 1000L * expiresIn;
SYSTEM_ESIA_TOKEN.set(new ExpiringToken(token, expiryTime));
}
}
public static String getSystemEsiaToken() {
ExpiringToken expiringToken = SYSTEM_ESIA_TOKEN.get();
if (expiringToken == null || expiringToken.getAccessToken() == null
|| expiringToken.isExpired()) {
return null;
}
return expiringToken.getAccessToken();
}
}

View file

@ -74,6 +74,12 @@ public class EsiaConfig {
@Value("${file.sign.verify.url}")
private String fileSignVerifyUrl;
@Value("${esia.system.scope}")
private String esiaSystemScope;
@Value("${esia.system.token.url}")
private String esiaSystemTokenUrl;
public String getEsiaOrgScopes() {
String[] scopeItems = esiaOrgScopes.split(",");
return String.join(" ", Arrays.stream(scopeItems).map(item -> orgScopeUrl + item.trim()).toArray(String[]::new));
@ -153,4 +159,12 @@ public class EsiaConfig {
public String getFileSignVerifyUrl() {
return fileSignVerifyUrl;
}
public String getEsiaSystemScope() {
return esiaSystemScope;
}
public String getEsiaSystemTokenUrl() {
return esiaSystemTokenUrl;
}
}

View file

@ -95,54 +95,27 @@ public class MchdInfoModel implements Serializable {
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Organization implements Serializable {
private String name;
private Person chief;
private String ogrn;
public String getName() {
return name;
public String getOgrn() {
return ogrn;
}
public void setName(String name) {
this.name = name;
}
public Person getChief() {
return chief;
}
public void setChief(Person chief) {
this.chief = chief;
public void setOgrn(String ogrn) {
this.ogrn = ogrn;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Person implements Serializable {
private String firstName;
private String lastName;
private String middleName;
private String snils;
public String getFirstName() {
return firstName;
public String getSnils() {
return snils;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getMiddleName() {
return middleName;
}
public void setMiddleName(String middleName) {
this.middleName = middleName;
public void setSnils(String snils) {
this.snils = snils;
}
}
}

View file

@ -655,4 +655,73 @@ public class EsiaAuthService {
throw new EsiaException("Unknown error in verify module. Error code " + errorCode);
}
}
public String getSystemAccessToken() {
String esiaAccessTokenStr = null;
Long expiresIn = null;
try {
String clientId = esiaConfig.getClientId();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx");
ZonedDateTime dt = ZonedDateTime.now();
String timestamp = dt.format(formatter);
String scope = esiaConfig.getEsiaSystemScope();
Map<String, String> parameters = new LinkedHashMap<String, String>();
parameters.put("client_id", clientId);
parameters.put("scope", scope);
parameters.put("timestamp", timestamp);
parameters.put("state", "%s");
parameters.put("redirect_uri", esiaConfig.getRedirectUrl());
SignResponse signResponse = signMap(parameters);
String newState = signResponse.getState();
String clientSecret = signResponse.getSignature();
String authUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaSystemTokenUrl();
String postBody = new FormUrlencoded()
.setParameter("client_id", clientId)
.setParameter("grant_type", "client_credentials")
.setParameter("client_secret", clientSecret)
.setParameter("state", newState)
.setParameter("scope", scope)
.setParameter("timestamp", timestamp)
.setParameter("token_type", "Bearer")
.setParameter("redirect_uri", esiaConfig.getRedirectUrl())
.toFormUrlencodedString();
HttpRequest postReq = HttpRequest.newBuilder(URI.create(authUrl))
.header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(postBody))
.timeout(Duration.ofSeconds(esiaConfig.getRequestTimeout()))
.build();
HttpResponse<String> postResp = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(esiaConfig.getConnectionTimeout()))
.build()
.send(postReq, HttpResponse.BodyHandlers.ofString());
String responseString = postResp.body();
EsiaTokenResponse tokenResponse = objectMapper.readValue(responseString,
EsiaTokenResponse.class
);
if (tokenResponse == null || tokenResponse.getError() != null) {
String errMsg =
tokenResponse != null ? tokenResponse.getErrorDescription() : "response is empty";
throw new IllegalStateException("Esia response error. " + errMsg);
}
if (!tokenResponse.getState().equals(newState)) {
throw new EsiaException(
"Token invalid. State from request not equals with state from response.");
}
esiaAccessTokenStr = tokenResponse.getAccessToken();
String verifyResult = verifyToken(esiaAccessTokenStr);
if (verifyResult != null) {
throw new EsiaException(verifyResult);
}
expiresIn = tokenResponse.getExpiresIn();
EsiaAuthInfoStore.setSystemEsiaToken(esiaAccessTokenStr, expiresIn);
return esiaAccessTokenStr;
}
catch (Exception e) {
throw new EsiaException(e);
}
}
}

View file

@ -0,0 +1,73 @@
package ru.micord.ervu.security.esia.service;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import ru.micord.ervu.security.esia.EsiaAuthInfoStore;
import ru.micord.ervu.security.esia.config.EsiaConfig;
import ru.micord.ervu.security.esia.exception.EsiaException;
import ru.micord.ervu.security.esia.model.MchdInfoModel;
/**
* @author Eduard Tihomirov
*/
@Service
public class MchdService {
@Autowired
private EsiaConfig esiaConfig;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private EsiaAuthService esiaAuthService;
public MchdInfoModel getMchdInfoModel(String guid) {
String accessToken = EsiaAuthInfoStore.getSystemEsiaToken();
boolean isNewToken = (accessToken == null);
if (isNewToken) {
accessToken = esiaAuthService.getSystemAccessToken();
}
try {
HttpResponse<String> response = executeRequest(accessToken, guid);
if (response.statusCode() != 200) {
if (isNewToken) {
throw new EsiaException(response.statusCode() + " " + response.body());
}
accessToken = esiaAuthService.getSystemAccessToken();
response = executeRequest(accessToken, guid);
if (response.statusCode() != 200) {
throw new EsiaException(response.statusCode() + " " + response.body());
}
}
return objectMapper.readValue(response.body(), MchdInfoModel.class);
}
catch (Exception e) {
throw new EsiaException(e);
}
}
private HttpResponse<String> executeRequest(String accessToken, String guid) throws Exception {
String url = esiaConfig.getEsiaBaseUri() + "/poa-registry/api/public/v1/poa/" + guid;
HttpRequest getReq = HttpRequest.newBuilder(URI.create(url))
.header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
.header("Authorization", "Bearer ".concat(accessToken))
.GET()
.timeout(Duration.ofSeconds(esiaConfig.getRequestTimeout()))
.build();
return HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(esiaConfig.getConnectionTimeout()))
.build()
.send(getReq, HttpResponse.BodyHandlers.ofString());
}
}

View file

@ -22,6 +22,4 @@ public interface UlDataService {
String getAllUserRoles(String accessToken);
EsiaHeader readHeader(String accessToken);
MchdInfoModel getMchdInfoModel(String guid, String accessToken);
}

View file

@ -20,7 +20,6 @@ import ru.micord.ervu.security.esia.model.BrhsModel;
import ru.micord.ervu.security.esia.model.EmployeeModel;
import ru.micord.ervu.security.esia.model.EsiaAccessToken;
import ru.micord.ervu.security.esia.model.EsiaHeader;
import ru.micord.ervu.security.esia.model.MchdInfoModel;
import ru.micord.ervu.security.esia.model.OrganizationModel;
import ru.micord.ervu.security.esia.model.PersonModel;
@ -282,25 +281,4 @@ public class UlDataServiceImpl implements UlDataService {
throw new EsiaException(e);
}
}
public MchdInfoModel getMchdInfoModel(String guid, String accessToken) {
try {
String url = esiaConfig.getEsiaBaseUri() + "/poa-registry/api/public/v1/poa/" + guid;
HttpRequest getReq = HttpRequest.newBuilder(URI.create(url))
.header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
.header("Authorization", "Bearer ".concat(accessToken))
.GET()
.timeout(Duration.ofSeconds(esiaConfig.getRequestTimeout()))
.build();
HttpResponse<String> getResp = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(esiaConfig.getConnectionTimeout()))
.build()
.send(getReq, HttpResponse.BodyHandlers.ofString());
errorHandler(getResp);
return objectMapper.readValue(getResp.body(), MchdInfoModel.class);
}
catch (Exception e) {
throw new EsiaException(e);
}
}
}