SUPPORT-8706: add audit

This commit is contained in:
adel.kalimullin 2025-01-20 09:55:42 +03:00
parent 2451e49530
commit 2f7328d6fb
47 changed files with 1989 additions and 108 deletions

View file

@ -44,6 +44,7 @@ import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import ru.micord.ervu.util.UrlUtils;
/**
* @author Alexandr Shalaginov
@ -175,14 +176,17 @@ public class WebDavClient {
HttpResponse<InputStream> response = httpClient.send(httpRequest,
HttpResponse.BodyHandlers.ofInputStream()
);
if (response.statusCode() == 200) {
long contentLength = response.headers()
.firstValueAsLong(HttpHeaders.CONTENT_LENGTH)
.orElse(0);
InputStreamResource resource = new InputStreamResource(response.body());
String encodedFilename = URLEncoder.encode(getFilenameFromUrl(url), StandardCharsets.UTF_8);
String encodedFilename = URLEncoder.encode(UrlUtils.extractFileNameFromUrl(url), StandardCharsets.UTF_8);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename*=UTF-8''" + encodedFilename
)
.contentLength(contentLength)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
}
@ -196,10 +200,6 @@ public class WebDavClient {
}
}
private String getFilenameFromUrl(String url) {
String path = URI.create(url).getPath();
return path.substring(path.lastIndexOf('/') + 1);
}
@Retryable(value = {IOException.class}, backoff = @Backoff(delayExpression = "${webdav.retry.delay:500}"))
public void deleteFilesOlderThan(long seconds, String url, String... extensions) throws IOException {

View file

@ -3,5 +3,5 @@ package ervu.model.fileupload;
/**
* @author r.latypov
*/
public record DownloadResponse(OrgInfo orgInfo, FileInfo fileInfo) {
public record DownloadResponse(UploadOrgInfo orgInfo, FileInfo fileInfo) {
}

View file

@ -6,15 +6,15 @@ import java.util.Objects;
* @author Alexandr Shalaginov
*/
public class EmployeeInfoKafkaMessage {
private final OrgInfo orgInfo;
private final UploadOrgInfo orgInfo;
private final FileInfo fileInfo;
public EmployeeInfoKafkaMessage(OrgInfo orgInfo, FileInfo fileInfo) {
public EmployeeInfoKafkaMessage(UploadOrgInfo orgInfo, FileInfo fileInfo) {
this.orgInfo = orgInfo;
this.fileInfo = fileInfo;
}
public OrgInfo getOrgInfo() {
public UploadOrgInfo getOrgInfo() {
return orgInfo;
}
@ -38,7 +38,7 @@ public class EmployeeInfoKafkaMessage {
@Override
public String toString() {
return "KafkaMessage{" +
"orgInfo=" + orgInfo +
"uploadOrgInfo=" + orgInfo +
", fileInfo=" + fileInfo +
'}';
}

View file

@ -11,6 +11,7 @@ public class FileInfo {
private String fileName;
private String filePatternCode;
private String filePatternName;
private String fileSize;
private String departureDateTime;
private String timeZone;
private FileStatus fileStatus;
@ -19,12 +20,14 @@ public class FileInfo {
}
public FileInfo(String fileId, String fileUrl, String fileName, String filePatternCode,
String filePatternName, String departureDateTime, String timeZone, FileStatus fileStatus) {
String filePatternName, String fileSize, String departureDateTime, String timeZone,
FileStatus fileStatus) {
this.fileId = fileId;
this.fileUrl = fileUrl;
this.fileName = fileName;
this.filePatternCode = filePatternCode;
this.filePatternName = filePatternName;
this.fileSize = fileSize;
this.departureDateTime = departureDateTime;
this.timeZone = timeZone;
this.fileStatus = fileStatus;
@ -50,6 +53,10 @@ public class FileInfo {
return filePatternName;
}
public String getFileSize() {
return fileSize;
}
public String getDepartureDateTime() {
return departureDateTime;
}
@ -73,13 +80,14 @@ public class FileInfo {
fileName, fileInfo.fileName) && Objects.equals(filePatternCode,
fileInfo.filePatternCode
) && Objects.equals(filePatternName, fileInfo.filePatternName)
&& Objects.equals(departureDateTime, fileInfo.departureDateTime);
&& Objects.equals(departureDateTime, fileInfo.departureDateTime) &&
Objects.equals(fileSize, fileInfo.getFileSize());
}
@Override
public int hashCode() {
return Objects.hash(fileId, fileUrl, fileName, filePatternCode, filePatternName,
departureDateTime
departureDateTime, fileSize
);
}
@ -92,6 +100,7 @@ public class FileInfo {
", filePatternCode='" + filePatternCode + '\'' +
", filePatternName='" + filePatternName + '\'' +
", departureDateTime='" + departureDateTime + '\'' +
", fileSize='" + fileSize + '\'' +
'}';
}
}

View file

@ -1,12 +1,10 @@
package ervu.model.fileupload;
import java.io.Serializable;
/**
* @author Eduard Tihomirov
*/
public class FileStatus {
private String code;
private String status;
private String description;

View file

@ -7,18 +7,24 @@ import ru.micord.ervu.journal.SenderInfo;
/**
* @author Alexandr Shalaginov
*/
public class OrgInfo {
public class UploadOrgInfo {
private String orgName;
private String orgId;
private SenderInfo senderInfo;
private String esiaOrgId;
public OrgInfo() {
public UploadOrgInfo() {
}
public OrgInfo(String orgName, String orgId, SenderInfo senderInfo) {
public UploadOrgInfo(String orgName, String orgId, SenderInfo senderInfo, String esiaOrgId) {
this.orgName = orgName;
this.orgId = orgId;
this.senderInfo = senderInfo;
this.esiaOrgId = esiaOrgId;
}
public String getEsiaOrgId() {
return esiaOrgId;
}
public String getOrgName() {
@ -37,23 +43,25 @@ public class OrgInfo {
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OrgInfo orgInfo = (OrgInfo) o;
return Objects.equals(orgName, orgInfo.orgName) && Objects.equals(orgId,
orgInfo.orgId
) && Objects.equals(senderInfo, orgInfo.senderInfo);
UploadOrgInfo uploadOrgInfo = (UploadOrgInfo) o;
return Objects.equals(orgName, uploadOrgInfo.orgName) && Objects.equals(orgId,
uploadOrgInfo.orgId
) && Objects.equals(senderInfo, uploadOrgInfo.senderInfo)
&& Objects.equals(esiaOrgId, uploadOrgInfo.esiaOrgId);
}
@Override
public int hashCode() {
return Objects.hash(orgName, orgId, senderInfo);
return Objects.hash(orgName, orgId, senderInfo, esiaOrgId);
}
@Override
public String toString() {
return "OrgInfo{" +
return "UploadOrgInfo{" +
"orgName='" + orgName + '\'' +
", orgId='" + orgId + '\'' +
", senderInfo='" + senderInfo + '\'' +
", esiaOrgId='" + esiaOrgId + '\'' +
'}';
}
}

View file

@ -4,7 +4,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Locale;
import java.util.UUID;
@ -31,6 +30,7 @@ import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import ru.micord.ervu.audit.service.AuditService;
import ru.micord.ervu.exception.JsonParsingException;
import ru.micord.ervu.security.esia.model.EmployeeModel;
import ru.micord.ervu.security.esia.model.PersonModel;
@ -38,6 +38,7 @@ import ru.micord.ervu.security.esia.service.UlDataService;
import ru.micord.ervu.security.esia.token.EsiaTokensStore;
import ru.micord.ervu.security.webbpm.jwt.UserIdsPair;
import ru.micord.ervu.service.InteractionService;
import ru.micord.ervu.util.DateUtils;
import static ervu.enums.FileStatusCode.FILE_CLEAN;
import static ervu.enums.FileStatusCode.FILE_INFECTED;
@ -51,13 +52,12 @@ import static ru.micord.ervu.util.StringUtils.convertToFio;
@Service
public class EmployeeInfoFileUploadService {
private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeInfoFileUploadService.class);
private static final String FORMAT = "dd.MM.yyyy HH:mm:ss";
private final WebDavClient webDavClient;
private final EmployeeInfoKafkaMessageService employeeInfoKafkaMessageService;
private final KafkaTemplate<String, String> kafkaTemplate;
private final InteractionService interactionService;
private final UlDataService ulDataService;
private final AuditService auditService;
@Value("${av.kafka.message.topic.name}")
private String kafkaTopicName;
@ -67,12 +67,14 @@ public class EmployeeInfoFileUploadService {
EmployeeInfoKafkaMessageService employeeInfoKafkaMessageService,
@Qualifier("avTemplate") KafkaTemplate<String, String> kafkaTemplate,
InteractionService interactionService,
UlDataService ulDataService) {
UlDataService ulDataService,
AuditService auditService) {
this.webDavClient = webDavClient;
this.employeeInfoKafkaMessageService = employeeInfoKafkaMessageService;
this.kafkaTemplate = kafkaTemplate;
this.interactionService = interactionService;
this.ulDataService = ulDataService;
this.auditService = auditService;
}
public boolean saveEmployeeInformationFile(MultipartFile multipartFile, String formType,
@ -99,12 +101,9 @@ public class EmployeeInfoFileUploadService {
convertToFio(personModel.getFirstName(), personModel.getMiddleName(), personModel.getLastName()),
ervuId);
if (fileUploadUrl != null) {
fileStatus.setCode(FILE_UPLOADED.getCode());
fileStatus.setDescription("Файл принят до проверки на вирусы");
String departureDateTime = now.format(DateTimeFormatter.ofPattern(FORMAT));
String jsonMessage = getJsonKafkaMessage(
employeeInfoKafkaMessageService.getKafkaMessage(
long fileSize = multipartFile.getSize();
String departureDateTime = DateUtils.convertToString(now);
EmployeeInfoKafkaMessage kafkaMessage = employeeInfoKafkaMessageService.getKafkaMessage(
fileId,
fileUploadUrl,
fileName,
@ -115,13 +114,21 @@ public class EmployeeInfoFileUploadService {
fileStatus,
ervuId,
esiaUserId,
personModel
)
personModel,
fileSize
);
if (fileUploadUrl != null) {
fileStatus.setCode(FILE_UPLOADED.getCode());
fileStatus.setDescription("Файл принят до проверки на вирусы");
String jsonMessage = getJsonKafkaMessage(kafkaMessage);
return sendMessage(jsonMessage);
}
else {
LOGGER.error("Failed to upload file: {}", fileName);
fileStatus.setCode(FILE_NOT_CHECKED.getCode());
fileStatus.setDescription("Невозможно проверить файл по причине недоступности или ошибки в работе антивируса");
auditService.processUploadEvent(kafkaMessage.getOrgInfo(), kafkaMessage.getFileInfo());
return false;
}
}
@ -199,6 +206,7 @@ public class EmployeeInfoFileUploadService {
interactionService.delete(fileInfo.getFileId(), downloadResponse.orgInfo().getOrgId());
}
else if (statusCode.equals(FILE_NOT_CHECKED.getCode())) {
auditService.processUploadEvent(downloadResponse.orgInfo(), downloadResponse.fileInfo());
interactionService.updateStatus(fileInfo.getFileId(), fileInfo.getFileStatus().getStatus(),
downloadResponse.orgInfo().getOrgId()
);

View file

@ -4,7 +4,7 @@ import ervu.model.fileupload.EmployeeInfoFileFormType;
import ervu.model.fileupload.EmployeeInfoKafkaMessage;
import ervu.model.fileupload.FileInfo;
import ervu.model.fileupload.FileStatus;
import ervu.model.fileupload.OrgInfo;
import ervu.model.fileupload.UploadOrgInfo;
import org.springframework.stereotype.Service;
import ru.micord.ervu.journal.SenderInfo;
import ru.micord.ervu.security.esia.model.OrganizationModel;
@ -25,7 +25,8 @@ public class EmployeeInfoKafkaMessageService {
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) {
String offset, FileStatus fileStatus, String ervuId, String prnOid,
PersonModel personModel, long fileSize) {
return new EmployeeInfoKafkaMessage(
getOrgInfo(accessToken, ervuId, prnOid, personModel),
getFileInfo(
@ -35,33 +36,36 @@ public class EmployeeInfoKafkaMessageService {
formType,
departureDateTime,
offset,
fileStatus
fileStatus,
fileSize
)
);
}
private FileInfo getFileInfo(String fileId, String fileUrl, String fileName,
EmployeeInfoFileFormType formType, String departureDateTime, String offset,
FileStatus fileStatus) {
FileStatus fileStatus, long fileSize) {
return new FileInfo(
fileId,
fileUrl,
fileName,
formType.getFilePatternCode(),
formType.getFilePatternName(),
String.valueOf(fileSize),
departureDateTime,
offset,
fileStatus
);
}
private OrgInfo getOrgInfo(String accessToken, String ervuId, String prnOid, PersonModel personModel) {
private 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 OrgInfo(organizationModel.getFullName(), ervuId, senderInfo);
return new UploadOrgInfo(organizationModel.getFullName(), ervuId, senderInfo,
organizationModel.getOid());
}
}

View file

@ -0,0 +1,89 @@
package ru.micord.ervu.audit.config;
import java.util.HashMap;
import java.util.Map;
import org.apache.kafka.clients.CommonClientConfigs;
import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.admin.NewTopic;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.config.SaslConfigs;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.TopicBuilder;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaAdmin;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
/**
* @author Adel Kalimullin
*/
@Configuration
public class AuditKafkaConfig {
@Value("${audit.kafka.bootstrap.servers}")
private String bootstrapServers;
@Value("${audit.kafka.security.protocol}")
private String securityProtocol;
@Value("${audit.kafka.login.module}")
private String loginModule;
@Value("${audit.kafka.username}")
private String username;
@Value("${audit.kafka.password}")
private String password;
@Value("${audit.kafka.sasl.mechanism}")
private String saslMechanism;
@Value("${audit.kafka.authorization.topic}")
private String authorizationTopic;
@Value("${audit.kafka.action.topic}")
private String actionTopic;
@Value("${audit.kafka.file.download.topic}")
private String fileDownloadTopic;
@Bean("auditProducerFactory")
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, securityProtocol);
configProps.put(SaslConfigs.SASL_JAAS_CONFIG, loginModule + " required username=\""
+ username + "\" password=\"" + password + "\";");
configProps.put(SaslConfigs.SASL_MECHANISM, saslMechanism);
return new DefaultKafkaProducerFactory<>(configProps);
}
@Bean("auditTemplate")
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
@Bean
public KafkaAdmin auditKafkaAdmin() {
Map<String, Object> configs = new HashMap<>();
configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
configs.put(AdminClientConfig.SECURITY_PROTOCOL_CONFIG, securityProtocol);
configs.put(SaslConfigs.SASL_JAAS_CONFIG, loginModule + " required username=\""
+ username + "\" password=\"" + password + "\";");
configs.put(SaslConfigs.SASL_MECHANISM, saslMechanism);
return new KafkaAdmin(configs);
}
@Bean
public NewTopic auditAuthTopic() {
return TopicBuilder.name(authorizationTopic).build();
}
@Bean
public NewTopic auditActionTopic() {
return TopicBuilder.name(actionTopic).build();
}
@Bean
public NewTopic auditDownloadTopic() {
return TopicBuilder.name(fileDownloadTopic).build();
}
}

View file

@ -0,0 +1,41 @@
package ru.micord.ervu.audit.constants;
import java.util.Map;
import java.util.Optional;
/**
* @author Adel Kalimullin
*/
public final class AuditConstants {
public static final String SUBSYSTEM_TYPE = "UL";
public static final String LOGOUT_EVENT_TYPE = "logout";
public static final String LOGIN_EVENT_TYPE = "login";
public static final String SUCCESS_STATUS_TYPE = "success";
public static final String FAILURE_STATUS_TYPE = "failure";
private static final Map<String, String> routeDescriptions = Map.of(
"/", "Личный кабинет ЮР лица",
"/mydata", "Информация об организации",
"/filesentlog", "Журнал взаимодействия"
);
private static final Map<Integer, String> downloadTypes = Map.of(
1, "Выписка из журнала взаимодействия ЮЛ"
);
private AuditConstants() {
}
public static String getRouteDescription(String route) {
return Optional.ofNullable(routeDescriptions.get(route))
.orElseThrow(() -> new IllegalArgumentException("Invalid route :" + route));
}
public static String getDownloadType(int formatRegistry) {
return Optional.ofNullable(downloadTypes.get(formatRegistry))
.orElseThrow(
() -> new IllegalArgumentException("Invalid formatRegistry :" + formatRegistry));
}
}

View file

@ -0,0 +1,33 @@
package ru.micord.ervu.audit.controller;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import ru.micord.ervu.audit.model.AuditActionRequest;
import ru.micord.ervu.audit.service.AuditService;
/**
* @author Adel Kalimullin
*/
@RestController
@RequestMapping("/audit")
public class AuditController {
private final AuditService auditService;
public AuditController(AuditService auditService) {
this.auditService = auditService;
}
@RequestMapping(value = "/action", method = RequestMethod.POST)
public ResponseEntity<Void> auditAction(
HttpServletRequest request, @RequestBody AuditActionRequest actionEvent) {
auditService.processActionEvent(request, actionEvent);
return ResponseEntity.ok().build();
}
}

View file

@ -0,0 +1,67 @@
package ru.micord.ervu.audit.model;
import java.util.List;
/**
* @author Adel Kalimullin
*/
public class AuditActionEvent extends AuditEvent {
private String eventType;
private String description;
private String sourceUrl;
private List<SearchCriteria> searchAttributes;
private String fileName;
public AuditActionEvent(
String esiaOrgId, String esiaPersonId, String eventTime,
String eventType, String description, String sourceUrl,
List<SearchCriteria> searchAttributes, String fileName) {
super(esiaOrgId, esiaPersonId, eventTime);
this.eventType = eventType;
this.description = description;
this.sourceUrl = sourceUrl;
this.searchAttributes = searchAttributes;
this.fileName = fileName;
}
public String getEventType() {
return eventType;
}
public void setEventType(String eventType) {
this.eventType = eventType;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getSourceUrl() {
return sourceUrl;
}
public void setSourceUrl(String sourceUrl) {
this.sourceUrl = sourceUrl;
}
public List<SearchCriteria> getSearchAttributes() {
return searchAttributes;
}
public void setSearchAttributes(List<SearchCriteria> searchAttributes) {
this.searchAttributes = searchAttributes;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
}

View file

@ -0,0 +1,56 @@
package ru.micord.ervu.audit.model;
import java.util.Map;
/**
* @author Adel Kalimullin
*/
public class AuditActionRequest {
private String eventType;
private String route;
private String sourceUrl;
private Map<String, FilterInfo> filterInfo;
private String fileName;
public String getEventType() {
return eventType;
}
public void setEventType(String eventType) {
this.eventType = eventType;
}
public String getRoute() {
return route;
}
public void setRoute(String route) {
this.route = route;
}
public String getSourceUrl() {
return sourceUrl;
}
public void setSourceUrl(String sourceUrl) {
this.sourceUrl = sourceUrl;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public Map<String, FilterInfo> getFilterInfo() {
return filterInfo;
}
public void setFilterInfo(
Map<String, FilterInfo> filterInfo) {
this.filterInfo = filterInfo;
}
}

View file

@ -0,0 +1,87 @@
package ru.micord.ervu.audit.model;
/**
* @author Adel Kalimullin
*/
public class AuditAuthorizationEvent extends AuditEvent {
private String status;
private String eventType;
private String organizationName;
private String firstName;
private String lastName;
private String middleName;
private String inn;
public AuditAuthorizationEvent(
String esiaOrgId, String esiaPersonId, String eventTime,
String organizationName, String firstName, String lastName,
String middleName, String inn, String status,
String eventType) {
super(esiaOrgId, esiaPersonId, eventTime);
this.status = status;
this.eventType = eventType;
this.organizationName = organizationName;
this.firstName = firstName;
this.lastName = lastName;
this.middleName = middleName;
this.inn = inn;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getEventType() {
return eventType;
}
public void setEventType(String eventType) {
this.eventType = eventType;
}
public String getOrganizationName() {
return organizationName;
}
public void setOrganizationName(String organizationName) {
this.organizationName = organizationName;
}
public String getFirstName() {
return firstName;
}
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 String getInn() {
return inn;
}
public void setInn(String inn) {
this.inn = inn;
}
}

View file

@ -0,0 +1,66 @@
package ru.micord.ervu.audit.model;
/**
* @author Adel Kalimullin
*/
public class AuditDownloadEvent extends AuditEvent {
private String downloadType;
private String fileName;
private String s3FileUrl;
private String fileSize;
private String status;
public AuditDownloadEvent(
String esiaOrgId, String esiaPersonId, String eventTime,
String downloadType, String fileName, String s3FileUrl,
String fileSize, String status) {
super(esiaOrgId, esiaPersonId, eventTime);
this.downloadType = downloadType;
this.fileName = fileName;
this.s3FileUrl = s3FileUrl;
this.fileSize = fileSize;
this.status = status;
}
public String getDownloadType() {
return downloadType;
}
public void setDownloadType(String downloadType) {
this.downloadType = downloadType;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getS3FileUrl() {
return s3FileUrl;
}
public void setS3FileUrl(String s3FileUrl) {
this.s3FileUrl = s3FileUrl;
}
public String getFileSize() {
return fileSize;
}
public void setFileSize(String fileSize) {
this.fileSize = fileSize;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}

View file

@ -0,0 +1,48 @@
package ru.micord.ervu.audit.model;
import ru.micord.ervu.audit.constants.AuditConstants;
/**
* @author Adel Kalimullin
*/
public abstract class AuditEvent {
protected final String subsystem = AuditConstants.SUBSYSTEM_TYPE;
protected String esiaOrgId;
protected String esiaPersonId;
protected String eventTime;
public AuditEvent(
String esiaOrgId, String esiaPersonId, String eventTime) {
this.esiaOrgId = esiaOrgId;
this.esiaPersonId = esiaPersonId;
this.eventTime = eventTime;
}
public String getSubsystem() {
return subsystem;
}
public String getEsiaOrgId() {
return esiaOrgId;
}
public void setEsiaOrgId(String esiaOrgId) {
this.esiaOrgId = esiaOrgId;
}
public String getEsiaPersonId() {
return esiaPersonId;
}
public void setEsiaPersonId(String esiaPersonId) {
this.esiaPersonId = esiaPersonId;
}
public String getEventTime() {
return eventTime;
}
public void setEventTime(String eventTime) {
this.eventTime = eventTime;
}
}

View file

@ -0,0 +1,35 @@
package ru.micord.ervu.audit.model;
import ervu.model.fileupload.FileInfo;
import ervu.model.fileupload.UploadOrgInfo;
/**
* @author Adel Kalimullin
*/
public class AuditUploadEvent {
private UploadOrgInfo orgInfo;
private FileInfo fileInfo;
public AuditUploadEvent(UploadOrgInfo orgInfo, FileInfo fileInfo) {
this.orgInfo = orgInfo;
this.fileInfo = fileInfo;
}
public UploadOrgInfo getOrgInfo() {
return orgInfo;
}
public void setOrgInfo(UploadOrgInfo orgInfo) {
this.orgInfo = orgInfo;
}
public FileInfo getFileInfo() {
return fileInfo;
}
public void setFileInfo(FileInfo fileInfo) {
this.fileInfo = fileInfo;
}
}

View file

@ -0,0 +1,25 @@
package ru.micord.ervu.audit.model;
/**
* @author Adel Kalimullin
*/
public class FilterCondition {
private String filterValue;
private String filterType;
public String getFilterValue() {
return filterValue;
}
public void setFilterValue(String filterValue) {
this.filterValue = filterValue;
}
public String getFilterType() {
return filterType;
}
public void setFilterType(String filterType) {
this.filterType = filterType;
}
}

View file

@ -0,0 +1,28 @@
package ru.micord.ervu.audit.model;
import java.util.List;
/**
* @author Adel Kalimullin
*/
public class FilterInfo {
private String conditionOperator;
private List<FilterCondition> conditions;
public String getConditionOperator() {
return conditionOperator;
}
public void setConditionOperator(String conditionOperator) {
this.conditionOperator = conditionOperator;
}
public List<FilterCondition> getConditions() {
return conditions;
}
public void setConditions(List<FilterCondition> conditions) {
this.conditions = conditions;
}
}

View file

@ -0,0 +1,30 @@
package ru.micord.ervu.audit.model;
/**
* @author Adel Kalimullin
*/
public class SearchCriteria {
private String searchAttribute;
private String searchValue;
public SearchCriteria(String searchAttribute, String searchValue) {
this.searchAttribute = searchAttribute;
this.searchValue = searchValue;
}
public String getSearchAttribute() {
return searchAttribute;
}
public void setSearchAttribute(String searchAttribute) {
this.searchAttribute = searchAttribute;
}
public String getSearchValue() {
return searchValue;
}
public void setSearchValue(String searchValue) {
this.searchValue = searchValue;
}
}

View file

@ -0,0 +1,9 @@
package ru.micord.ervu.audit.service;
/**
* @author Adel Kalimullin
*/
public interface AuditKafkaPublisher {
void publishEvent(String topicName, String message);
}

View file

@ -0,0 +1,23 @@
package ru.micord.ervu.audit.service;
import javax.servlet.http.HttpServletRequest;
import ervu.model.fileupload.FileInfo;
import ervu.model.fileupload.UploadOrgInfo;
import ru.micord.ervu.audit.model.AuditActionRequest;
import ru.micord.ervu.kafka.model.OrgInfo;
/**
* @author Adel Kalimullin
*/
public interface AuditService {
void processActionEvent(HttpServletRequest request, AuditActionRequest auditActionRequest);
void processAuthEvent(HttpServletRequest request, OrgInfo orgInfo, String prnOid, String status,
String eventType);
void processUploadEvent(UploadOrgInfo uploadOrgInfo, FileInfo fileInfo);
void processDownloadEvent(HttpServletRequest request, long fileSize, String fileName,
int formatRegistry, String status, String s3FileUrl);
}

View file

@ -0,0 +1,43 @@
package ru.micord.ervu.audit.service.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import ru.micord.ervu.audit.service.AuditKafkaPublisher;
/**
* @author Adel Kalimullin
*/
@Service
public class BaseAuditKafkaPublisher implements AuditKafkaPublisher {
private static final Logger LOGGER = LoggerFactory.getLogger(BaseAuditKafkaPublisher.class);
private final KafkaTemplate<String, String> kafkaTemplate;
@Value("${audit.kafka.enabled}")
private boolean auditEnabled;
public BaseAuditKafkaPublisher(
@Qualifier("auditTemplate") KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
@Override
public void publishEvent(String topic, String message) {
if (auditEnabled) {
kafkaTemplate.send(topic, message)
.addCallback(
result -> {
},
ex -> LOGGER.error("Failed to send message to topic {}: {}", topic, ex.getMessage(),
ex
)
);
}
else {
LOGGER.info("Audit is disabled. Event not published.");
}
}
}

View file

@ -0,0 +1,167 @@
package ru.micord.ervu.audit.service.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import ervu.model.fileupload.FileInfo;
import ervu.model.fileupload.UploadOrgInfo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import ru.micord.ervu.audit.constants.AuditConstants;
import ru.micord.ervu.audit.model.*;
import ru.micord.ervu.audit.service.AuditKafkaPublisher;
import ru.micord.ervu.audit.service.AuditService;
import ru.micord.ervu.exception.JsonParsingException;
import ru.micord.ervu.kafka.model.OrgInfo;
import ru.micord.ervu.security.esia.model.EsiaAccessToken;
import ru.micord.ervu.security.esia.service.UlDataService;
import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService;
import ru.micord.ervu.util.DateUtils;
/**
* @author Adel Kalimullin
*/
@Service
public class BaseAuditService implements AuditService {
private final AuditKafkaPublisher auditKafkaPublisher;
private final JwtTokenService jwtTokenService;
private final UlDataService ulDataService;
private final ObjectMapper objectMapper;
@Value("${audit.kafka.authorization.topic}")
private String authorizationTopic;
@Value("${audit.kafka.action.topic}")
private String actionTopic;
@Value("${audit.kafka.file.upload.topic}")
private String fileUploadTopic;
@Value("${audit.kafka.file.download.topic}")
private String fileDownloadTopic;
public BaseAuditService(AuditKafkaPublisher auditKafkaPublisher, JwtTokenService jwtTokenService,
UlDataService ulDataService, ObjectMapper objectMapper) {
this.auditKafkaPublisher = auditKafkaPublisher;
this.jwtTokenService = jwtTokenService;
this.ulDataService = ulDataService;
this.objectMapper = objectMapper;
}
@Override
public void processActionEvent(HttpServletRequest request,
AuditActionRequest auditActionRequest) {
String orgId = getEsiaOrgId(request);
String userAccountId = jwtTokenService.getUserAccountId(request);
String description = AuditConstants.getRouteDescription(auditActionRequest.getRoute());
List<SearchCriteria> searchAttributes = null;
if (auditActionRequest.getFilterInfo() != null && !auditActionRequest.getFilterInfo().isEmpty()) {
searchAttributes = getSearchCriteriaList(auditActionRequest.getFilterInfo());
}
AuditActionEvent event = new AuditActionEvent(
orgId,
userAccountId,
DateUtils.getClientDateTimeWithZoneFromRequest(request),
auditActionRequest.getEventType(),
description,
auditActionRequest.getSourceUrl(),
searchAttributes,
auditActionRequest.getFileName()
);
String message = convertToMessage(event);
auditKafkaPublisher.publishEvent(actionTopic, message);
}
@Override
public void processAuthEvent(HttpServletRequest request, OrgInfo orgInfo, String prnOid,
String status, String eventType) {
AuditAuthorizationEvent event = new AuditAuthorizationEvent(
orgInfo.getOrgOid(),
prnOid,
DateUtils.getClientDateTimeWithZoneFromRequest(request),
orgInfo.getOrgFullName(),
orgInfo.getSenderInfo().getFirstName(),
orgInfo.getSenderInfo().getLastName(),
orgInfo.getSenderInfo().getMiddleName(),
orgInfo.getInn(),
status,
eventType
);
String message = convertToMessage(event);
auditKafkaPublisher.publishEvent(authorizationTopic, message);
}
@Override
public void processUploadEvent(UploadOrgInfo orgInfo, FileInfo fileInfo) {
AuditUploadEvent auditUploadEvent = new AuditUploadEvent(
orgInfo,
fileInfo
);
String message = convertToMessage(auditUploadEvent);
auditKafkaPublisher.publishEvent(fileUploadTopic, message);
}
@Override
public void processDownloadEvent(
HttpServletRequest request, long fileSize, String fileName, int formatRegistry,
String status, String s3FileUrl) {
String userAccountId = jwtTokenService.getUserAccountId(request);
AuditDownloadEvent event = new AuditDownloadEvent(
getEsiaOrgId(request),
userAccountId,
DateUtils.getClientDateTimeWithZoneFromRequest(request),
AuditConstants.getDownloadType(formatRegistry),
fileName,
s3FileUrl,
String.valueOf(fileSize),
status
);
String message = convertToMessage(event);
auditKafkaPublisher.publishEvent(fileDownloadTopic, message);
}
private String getEsiaOrgId(HttpServletRequest request) {
String accessToken = jwtTokenService.getAccessToken(request);
EsiaAccessToken esiaAccessToken = ulDataService.readToken(accessToken);
String scope = esiaAccessToken.getScope();
return scope.substring(scope.indexOf('=') + 1, scope.indexOf(' '));
}
public List<SearchCriteria> getSearchCriteriaList(Map<String, FilterInfo> filterInfoMap) {
List<SearchCriteria> searchCriteriaList = new ArrayList<>();
for (Map.Entry<String, FilterInfo> entry : filterInfoMap.entrySet()) {
String searchAttribute = entry.getKey();
FilterInfo filterInfo = entry.getValue();
String searchValue = filterInfo.getConditions().stream()
.map(condition -> condition.getFilterValue() + " " + condition.getFilterType())
.collect(Collectors.joining(", "));
if (filterInfo.getConditionOperator() != null) {
searchValue += " Operator: " + filterInfo.getConditionOperator();
}
SearchCriteria searchCriteria = new SearchCriteria(searchAttribute, searchValue);
searchCriteriaList.add(searchCriteria);
}
return searchCriteriaList;
}
private String convertToMessage(Object event) {
try {
return objectMapper.writeValueAsString(event);
}
catch (JsonProcessingException e) {
throw new JsonParsingException(e);
}
}
}

View file

@ -5,7 +5,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class SenderInfo {
@JsonProperty("prnOid")
private String prnOid; // идентификатор сотрудника в ЕСИА
@JsonProperty("lastName")

View file

@ -6,7 +6,7 @@ import java.time.LocalDateTime;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import ru.micord.ervu.util.DateUtil;
import ru.micord.ervu.util.DateUtils;
public class DepartureDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
@ -14,6 +14,6 @@ public class DepartureDateTimeDeserializer extends JsonDeserializer<LocalDateTim
public LocalDateTime deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException {
String dateTimeString = jsonParser.getText();
return DateUtil.convertToLocalDateTime(dateTimeString);
return DateUtils.convertToLocalDateTime(dateTimeString);
}
}

View file

@ -3,20 +3,26 @@ package ru.micord.ervu.kafka.controller;
import java.time.ZonedDateTime;
import java.util.TimeZone;
import javax.servlet.http.HttpServletRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import ervu.client.fileupload.WebDavClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.micord.ervu.audit.constants.AuditConstants;
import ru.micord.ervu.audit.service.AuditService;
import ru.micord.ervu.kafka.exception.ExcerptException;
import ru.micord.ervu.kafka.exception.ExcerptResponseException;
import ru.micord.ervu.kafka.model.Data;
import ru.micord.ervu.kafka.model.ExcerptResponse;
import ru.micord.ervu.kafka.service.ReplyingKafkaService;
import ru.micord.ervu.security.webbpm.jwt.UserIdsPair;
import ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil;
import ru.micord.ervu.util.UrlUtils;
/**
* @author Eduard Tihomirov
@ -27,6 +33,9 @@ public class ErvuKafkaController {
@Autowired
private ReplyingKafkaService replyingKafkaService;
@Autowired
private AuditService auditService;
@Autowired
private WebDavClient webDavClient;
@ -40,10 +49,12 @@ public class ErvuKafkaController {
private ObjectMapper objectMapper;
@RequestMapping(value = "/kafka/excerpt")
public ResponseEntity<Resource> getExcerptFile(
@RequestHeader("Client-Time-Zone") String clientTimeZone) {
public ResponseEntity<Resource> getExcerptFile(HttpServletRequest request) {
String fileUrl = null;
String fileName = null;
long fileSize = 0;
try {
String clientTimeZone = request.getHeader("Client-Time-Zone");
UserIdsPair userIdsPair = SecurityUtil.getUserIdsPair();
Data data = new Data();
data.setErvuId(userIdsPair.getErvuId());
@ -56,16 +67,30 @@ public class ErvuKafkaController {
ExcerptResponse excerptResponse = objectMapper.readValue(kafkaResponse, ExcerptResponse.class);
if (!excerptResponse.getSuccess()) {
throw new RuntimeException("Error with getting excerpt url " + excerptResponse.getMessage());
throw new ExcerptResponseException(
"Error with getting excerpt url " + excerptResponse.getMessage());
}
else if (excerptResponse.getData() == null || excerptResponse.getData().getFileUrl() == null
|| excerptResponse.getData().getFileUrl().isEmpty()) {
auditService.processDownloadEvent(request, fileSize, fileName, 1,
AuditConstants.FAILURE_STATUS_TYPE, fileUrl
);
return ResponseEntity.noContent().build();
}
return webDavClient.webDavDownloadFile(excerptResponse.getData().getFileUrl());
fileUrl = excerptResponse.getData().getFileUrl();
fileName = UrlUtils.extractFileNameFromUrl(excerptResponse.getData().getFileUrl());
ResponseEntity<Resource> responseEntity = webDavClient.webDavDownloadFile(fileUrl);
fileSize = responseEntity.getHeaders().getContentLength();
auditService.processDownloadEvent(request, fileSize, fileName, 1,
AuditConstants.SUCCESS_STATUS_TYPE, fileUrl
);
return responseEntity;
}
catch (Exception e) {
throw new RuntimeException(e);
auditService.processDownloadEvent(request, fileSize, fileName, 1,
AuditConstants.FAILURE_STATUS_TYPE, fileUrl
);
throw new ExcerptException(e);
}
}
}

View file

@ -0,0 +1,18 @@
package ru.micord.ervu.kafka.exception;
/**
* @author Adel Kalimullin
*/
public class ExcerptException extends RuntimeException {
public ExcerptException(String message, Throwable cause) {
super(message, cause);
}
public ExcerptException(Throwable cause) {
super(cause);
}
public ExcerptException(String message) {
super(message);
}
}

View file

@ -0,0 +1,10 @@
package ru.micord.ervu.kafka.exception;
/**
* @author Adel Kalimullin
*/
public class ExcerptResponseException extends RuntimeException {
public ExcerptResponseException(String message) {
super(message);
}
}

View file

@ -31,7 +31,7 @@ import static ru.micord.ervu.security.SecurityConstants.ESIA_LOGOUT;
@EnableWebSecurity
public class SecurityConfig {
private static final String[] PERMIT_ALL = new String[] {
"/version", "/esia/url", "/esia/auth", "esia/refresh", "/esia/logout",
"/version", "/esia/url", "/esia/auth", "esia/refresh", "/esia/logout"
};
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;

View file

@ -21,7 +21,8 @@ import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import ervu.service.okopf.OkopfService;
import org.springframework.stereotype.Service;
import ru.micord.ervu.audit.constants.AuditConstants;
import ru.micord.ervu.audit.service.AuditService;
import ru.micord.ervu.security.esia.exception.EsiaException;
import ru.micord.ervu.security.esia.model.EmployeeModel;
import ru.micord.ervu.security.esia.model.EsiaAccessToken;
@ -47,6 +48,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import ru.micord.ervu.security.webbpm.jwt.JwtAuthentication;
import ru.micord.ervu.security.webbpm.jwt.helper.SecurityHelper;
import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService;
@ -74,6 +76,8 @@ public class EsiaAuthService {
private OkopfService okopfService;
@Autowired
private SecurityHelper securityHelper;
@Autowired
private AuditService auditService;
@Value("${ervu.kafka.org.reply.topic}")
private String requestReplyTopic;
@ -244,20 +248,33 @@ public class EsiaAuthService {
LOGGER.info("Thread {}: SignSecret: {}ms RequestAccessToken: {}ms VerifySecret: {}ms",
Thread.currentThread().getId(), timeSignSecret, timeRequestAccessToken, timeVerifySecret);
}
OrgInfo orgInfo = null;
try {
orgInfo = getOrgInfo(esiaAccessTokenStr);
hasRole = ulDataService.checkRole(esiaAccessTokenStr);
String ervuId = getErvuId(esiaAccessTokenStr, prnOid);
String ervuId = getErvuId(prnOid, orgInfo);
createTokenAndAddCookie(response, prnOid, ervuId, hasRole, expiresIn);
if (!hasRole) {
LOGGER.error("The user with id = " + prnOid + " does not have the required role");
auditService.processAuthEvent(request, orgInfo, prnOid, AuditConstants.FAILURE_STATUS_TYPE,
AuditConstants.LOGIN_EVENT_TYPE
);
return new ResponseEntity<>(
"Доступ запрещен. Пользователь должен быть включен в группу \"Сотрудник, ответственный за военно-учетную работу\" в ЕСИА",
HttpStatus.FORBIDDEN
);
}
auditService.processAuthEvent(request, orgInfo, prnOid, AuditConstants.SUCCESS_STATUS_TYPE,
AuditConstants.LOGIN_EVENT_TYPE
);
return ResponseEntity.ok("Authentication successful");
}
catch (Exception e) {
if (orgInfo!= null){
auditService.processAuthEvent(request, orgInfo, prnOid, AuditConstants.FAILURE_STATUS_TYPE,
AuditConstants.LOGIN_EVENT_TYPE
);
}
createTokenAndAddCookie(response, prnOid, null, hasRole , expiresIn);
String messageId = getMessageId(e);
String messageWithId = String.format("[%s] %s", messageId, e.getMessage());
@ -333,7 +350,8 @@ public class EsiaAuthService {
Long expiresIn = tokenResponse.getExpires_in();
EsiaTokensStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn);
EsiaTokensStore.addRefreshToken(prnOid, esiaNewRefreshToken, expiresIn);
String ervuId = getErvuId(esiaAccessTokenStr, prnOid);
OrgInfo orgInfo = getOrgInfo(esiaAccessTokenStr);
String ervuId = getErvuId(prnOid, orgInfo);
createTokenAndAddCookie(response, esiaAccessToken.getSbj_id(), ervuId, true, expiresIn);
}
catch (Exception e) {
@ -374,9 +392,13 @@ public class EsiaAuthService {
}
public String logout(HttpServletRequest request, HttpServletResponse response) {
OrgInfo orgInfo = null;
String userId = null;
try {
userId = jwtTokenService.getUserAccountId(request);
String accessToken = EsiaTokensStore.getAccessToken(userId);
orgInfo = getOrgInfo(accessToken);
securityHelper.clearAccessCookies(response);
String userId = jwtTokenService.getUserAccountId(request);
EsiaTokensStore.removeAccessToken(userId);
EsiaTokensStore.removeRefreshToken(userId);
String logoutUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaLogoutUrl();
@ -385,28 +407,26 @@ public class EsiaAuthService {
Map<String, String> params = mapOf(
"client_id", esiaConfig.getClientId(),
"redirect_url", redirectUrl);
auditService.processAuthEvent(request, orgInfo, userId, AuditConstants.SUCCESS_STATUS_TYPE,
AuditConstants.LOGOUT_EVENT_TYPE
);
return buildUrl(url, params);
}
catch (Exception e) {
if (orgInfo != null) {
auditService.processAuthEvent(request, orgInfo, userId, AuditConstants.FAILURE_STATUS_TYPE,
AuditConstants.LOGOUT_EVENT_TYPE
);
}
throw new EsiaException(e);
}
}
public String getErvuId(String accessToken, String prnOid) {
long timeRequestPersonDataOrg = 0, timeRequestPersonDataEmployee = 0, timeRequestPersonDataChief = 0, timeRequestIdERVU = 0;
public String getErvuId(String prnOid, OrgInfo orgInfo) {
long timeRequestIdERVU = 0;
try {
long startTime = System.currentTimeMillis();
OrganizationModel organizationModel = ulDataService.getOrganizationModel(accessToken);
timeRequestPersonDataOrg = System.currentTimeMillis() - startTime;
startTime = System.currentTimeMillis();
EmployeeModel employeeModel = ulDataService.getEmployeeModel(accessToken);
timeRequestPersonDataEmployee = System.currentTimeMillis() - startTime;
startTime = System.currentTimeMillis();
EmployeeModel chiefModel = ulDataService.getChiefEmployeeModel(accessToken);
timeRequestPersonDataChief = System.currentTimeMillis() - startTime;
OrgInfo orgInfo = copyToOrgInfo(organizationModel, employeeModel, chiefModel);
orgInfo.setOrgTypeName(okopfService.findTitleByLeg(orgInfo.getOrgTypeLeg()));
startTime = System.currentTimeMillis();
long startTime = System.currentTimeMillis();
String kafkaResponse = replyingKafkaService.sendMessageAndGetReply(requestTopic,
requestReplyTopic, objectMapper.writeValueAsString(orgInfo)
);
@ -423,8 +443,29 @@ public class EsiaAuthService {
throw new EsiaException(e);
}
finally {
LOGGER.info("Thread {}: RequestPersonDataOrg: {}ms RequestPersonDataEmployee: {}ms RequestPersonDataChief: {}ms RequestIdERVU: {}ms",
Thread.currentThread().getId(), timeRequestPersonDataOrg, timeRequestPersonDataEmployee, timeRequestPersonDataChief, timeRequestIdERVU);
LOGGER.info("Thread {}: RequestIdERVU: {}ms", Thread.currentThread().getId(), timeRequestIdERVU);
}
}
private OrgInfo getOrgInfo(String accessToken) {
long timeRequestPersonDataOrg = 0, timeRequestPersonDataEmployee = 0, timeRequestPersonDataChief = 0;
try {
long startTime = System.currentTimeMillis();
OrganizationModel organizationModel = ulDataService.getOrganizationModel(accessToken);
timeRequestPersonDataOrg = System.currentTimeMillis() - startTime;
startTime = System.currentTimeMillis();
EmployeeModel employeeModel = ulDataService.getEmployeeModel(accessToken);
timeRequestPersonDataEmployee = System.currentTimeMillis() - startTime;
startTime = System.currentTimeMillis();
EmployeeModel chiefModel = ulDataService.getChiefEmployeeModel(accessToken);
timeRequestPersonDataChief = System.currentTimeMillis() - startTime;
return copyToOrgInfo(organizationModel, employeeModel, chiefModel);
}
finally {
LOGGER.info(
"Thread {}: RequestPersonDataOrg: {}ms RequestPersonDataEmployee: {}ms RequestPersonDataChief: {}ms",
Thread.currentThread().getId(), timeRequestPersonDataOrg, timeRequestPersonDataEmployee, timeRequestPersonDataChief
);
}
}

View file

@ -1,9 +1,10 @@
package ru.micord.ervu.util;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.*;
import java.time.format.DateTimeFormatter;
import javax.servlet.http.HttpServletRequest;
import org.springframework.util.StringUtils;
import static org.springframework.util.StringUtils.hasText;
@ -11,12 +12,31 @@ import static org.springframework.util.StringUtils.hasText;
/**
* @author gulnaz
*/
public final class DateUtil {
public final class DateUtils {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy");
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss");
private static final DateTimeFormatter DATE_TIME_WITH_TIMEZONE_FORMATTER = DateTimeFormatter.ofPattern(
"yyyy-MM-dd'T'HH:mm:ss.SSSX");
private DateUtil() {}
private DateUtils() {
}
public static String getCurrentFormattedDateTimeWithZone(){
ZonedDateTime now = ZonedDateTime.now();
return now.format(DATE_TIME_WITH_TIMEZONE_FORMATTER);
}
public static String getClientDateTimeWithZoneFromRequest(HttpServletRequest request) {
String clientTimeZone = request.getHeader("Client-Time-Zone");
ZoneId zoneId;
try {
zoneId = ZoneId.of(clientTimeZone);
}
catch (Exception e) {
zoneId = ZoneId.systemDefault();
}
return ZonedDateTime.now(zoneId).format(DATE_TIME_WITH_TIMEZONE_FORMATTER);
}
public static LocalDate convertToLocalDate(String date) {
return StringUtils.hasText(date)
@ -30,6 +50,10 @@ public final class DateUtil {
: null;
}
public static String convertToString(LocalDateTime dateTime) {
return dateTime == null ? "" : dateTime.format(DATE_TIME_FORMATTER);
}
public static String convertToString(LocalDate date) {
return date == null ? "" : date.format(DATE_FORMATTER);
}

View file

@ -1,9 +1,13 @@
package ru.micord.ervu.util;
import static org.apache.commons.lang3.StringUtils.capitalize;
import static org.apache.commons.lang3.StringUtils.substring;
public class StringUtils {
public final class StringUtils {
private StringUtils() {
}
public static String convertToFio(String firstName, String middleName, String lastName) {
String firstNameInitial = substring(firstName, 0, 1).toUpperCase();

View file

@ -0,0 +1,18 @@
package ru.micord.ervu.util;
import java.net.URI;
/**
* @author Adel Kalimullin
*/
public final class UrlUtils {
private UrlUtils(){
}
public static String extractFileNameFromUrl(String url) {
String path = URI.create(url).getPath();
return path.substring(path.lastIndexOf('/') + 1);
}
}

View file

@ -828,3 +828,17 @@ JBPM использует 3 корневых категории логирова
- `ERVU_KAFKA_EXCERPT_REQUEST_TOPIC` - топик для записи запроса для получения выписки по журналу взаимодействия
- `ERVU_KAFKA_EXCERPT_REPLY_TOPIC` - топик для чтения выписки по журналу взаимодействия. Содержит ссылку на S3 с файлом выписки
- `DB.JOURNAL.EXCLUDED.STATUSES` - статусы файла, которые необходимо исключить при получении данных по журналу взаимодействия из базы данных приложения
#### Взаимодействие с Kafka audit
- `AUDIT_KAFKA_AUTHORIZATION_TOPIC` - топик для отправки аудита в журнал авторизации
- `AUDIT_KAFKA_ACTION_TOPIC` - топик для отправки аудита в журнал действий пользователя
- `AUDIT_KAFKA_FILE_UPLOAD_TOPIC` - топик для отправки аудита в журнал обмена файлами
- `AUDIT_KAFKA_FILE_DOWNLOAD_TOPIC` - топик для отправки аудита в журнал загрузки ЮЛ и ФЛ
- `AUDIT_KAFKA_BOOTSTRAP_SERVERS` - список пар хост:порт, использующихся для установки первоначального соединения с кластером Kafka
- `AUDIT_KAFKA_SECURITY_PROTOCOL` - протокол, используемый для взаимодействия с брокерами
- `AUDIT_KAFKA_DOC_LOGIN_MODULE` - имя класса для входа в систему для SASL-соединений в формате, используемом конфигурационными файлами JAAS
- `AUDIT_KAFKA_USERNAME` - пользователь для подключения к Kafka
- `AUDIT_KAFKA_PASSWORD` - пароль для подключения к Kafka
- `AUDIT_KAFKA_SASL_MECHANISM` - механизм SASL, используемый для клиентских подключений
- `AUDIT_KAFKA_ENABLED` - флажок для включения записи аудита в кафку

View file

@ -46,6 +46,18 @@ ESNSI_OKOPF_RETRY_DELAY_LOAD=3000
ESNSI_OKOPF_CONNECT_TIMEOUT=2
ESNSI_OKOPF_READ_TIMEOUT=4
AUDIT_KAFKA_AUTHORIZATION_TOPIC=ervu.lkrp.auth.events
AUDIT_KAFKA_ACTION_TOPIC=ervu.lkrp.action.events
AUDIT_KAFKA_FILE_UPLOAD_TOPIC=ervu.lkrp.download.request
AUDIT_KAFKA_FILE_DOWNLOAD_TOPIC=ervu.lkrp.import.file
AUDIT_KAFKA_BOOTSTRAP_SERVERS=
AUDIT_KAFKA_SECURITY_PROTOCOL=
AUDIT_KAFKA_DOC_LOGIN_MODULE=
AUDIT_KAFKA_USERNAME=
AUDIT_KAFKA_PASSWORD=
AUDIT_KAFKA_SASL_MECHANISM=
AUDIT_KAFKA_ENABLED=false
ERVU_FILE_UPLOAD_MAX_FILE_SIZE=5242880
ERVU_FILE_UPLOAD_MAX_REQUEST_SIZE=6291456
ERVU_FILE_UPLOAD_FILE_SIZE_THRESHOLD=0

View file

@ -100,6 +100,17 @@
<property name="file.webdav.lifetime.seconds" value="300"/>
<property name="file.webdav.extensions" value="csv,xlsx"/>
<property name="webdav.bad_servers.cache.expire.seconds" value="120"/>
<property name="audit.kafka.bootstrap.servers" value="localhost:9092"/>
<property name="audit.kafka.authorization.topic" value="ervu.lkrp.auth.events"/>
<property name="audit.kafka.file.download.topic" value="ervu.lkrp.import.file"/>
<property name="audit.kafka.file.upload.topic" value="ervu.lkrp.download.request"/>
<property name="audit.kafka.action.topic" value="ervu.lkrp.action.events"/>
<property name="audit.kafka.security.protocol" value="SASL_PLAINTEXT"/>
<property name="audit.kafka.doc.login.module" value="org.apache.kafka.common.security.scram.ScramLoginModule"/>
<property name="audit.kafka.sasl.mechanism" value="SCRAM-SHA-256"/>
<property name="audit.kafka.username" value="user1"/>
<property name="audit.kafka.password" value="Blfi9d2OFG"/>
<property name="audit.kafka.enabled" value="false"/>
</system-properties>
<management>
<audit-log>

View file

@ -2462,9 +2462,9 @@
}
},
"cadesplugin_api": {
"version": "2.0.4-micord.1",
"resolved": "https://repo.micord.ru/repository/npm-all/cadesplugin_api/-/cadesplugin_api-2.0.4-micord.1.tgz",
"integrity": "sha512-FyGVi1VWIyJOW1zOOQN0IkTH/Z/8g7pNWH7A71nf0h21FCX9SacUfgRwID+gl+NlpYiT3m+yZGdlEJsiDeV8JA=="
"version": "2.1.1-micord.2",
"resolved": "https://repo.micord.ru/repository/npm-all/cadesplugin_api/-/cadesplugin_api-2.1.1-micord.2.tgz",
"integrity": "sha512-+j8RfbL7t2YMlSOC9Oa6+NoNLMYC3ZHkc9W6JQnV5+NBUqKLPAlLL1DF6llmW8coRdmgH6nZU8skvdk6M6qaBg=="
},
"calendar-utils": {
"version": "0.8.5",

View file

@ -0,0 +1,61 @@
import {AnalyticalScope, Behavior, Control, NotNull} from "@webbpm/base-package";
import {AuditService} from "./service/AuditService";
import {ElementRef, Input} from "@angular/core";
import {LinkEventTypeEnum} from "./component/enum/LinkEventTypeEnum";
@AnalyticalScope(Control)
export class LinkClickHandler extends Behavior {
@Input()
@NotNull()
public eventType: LinkEventTypeEnum;
private control: Control;
private auditService: AuditService;
private el: ElementRef
public initialize() {
super.initialize();
this.control = this.getScript(Control);
this.injector.get(AuditService);
this.auditService = this.injector.get(AuditService);
this.el = this.control.getEl();
}
bindEvents() {
super.bindEvents();
if (this.el) {
this.el.nativeElement.addEventListener('click',
(event: MouseEvent) => this.onClickFunction(event));
}
}
unbindEvents() {
super.unbindEvents();
if (this.el) {
this.el.nativeElement.removeEventListener('click',
(event: MouseEvent) => this.onClickFunction(event));
}
}
private onClickFunction(event: MouseEvent) {
const target = event.target as HTMLElement;
if (target.tagName === 'A') {
if (this.eventType === LinkEventTypeEnum.DOWNLOAD_EXAMPLE
|| this.eventType === LinkEventTypeEnum.DOWNLOAD_TEMPLATE) {
const href = target.getAttribute('href');
if (href) {
const fileName = this.extractFileNameFromHref(href);
this.auditService.logActionAudit(this.eventType, null, fileName);
}
}
else {
this.auditService.logActionAudit(this.eventType);
}
}
}
private extractFileNameFromHref(href: string): string {
const parts = href.split('/');
const fileNameWithExtension = parts[parts.length - 1];
return fileNameWithExtension.split('?')[0];
}
}

View file

@ -0,0 +1,5 @@
export enum LinkEventTypeEnum {
NAVIGATION_TO_SOURCE = "Переход на другие источники",
DOWNLOAD_TEMPLATE = "Скачивание шаблона",
DOWNLOAD_EXAMPLE = "Скачивание примера заполнения формы"
}

View file

@ -4,11 +4,14 @@ import {
GridRow,
GridRowModelType,
GridV2,
GridV2Column, Visible
GridV2Column,
Visible
} from "@webbpm/base-package";
import {ChangeDetectionStrategy, Component} from "@angular/core";
import {
ColDef, FilterChangedEvent,
ColDef,
GridReadyEvent,
FilterChangedEvent,
ICellRendererParams,
ITooltipParams,
ValueFormatterParams,
@ -17,6 +20,9 @@ import {
import {StaticColumnInitializer} from "./StaticColumnInitializer";
import {InMemoryStaticGridRpcService} from "../../../generated/ru/micord/ervu/service/rpc/InMemoryStaticGridRpcService";
import {StaticGridColumn} from "../../../generated/ru/micord/ervu/property/grid/StaticGridColumn";
import { FilterService } from "../../service/FilterService";
import {AuditConstants, AuditService, FilterInfo} from "../../service/AuditService";
@Component({
moduleId: module.id,
@ -30,6 +36,7 @@ export class InMemoryStaticGrid extends GridV2 {
public columnFiltersChanged: Event<any> = new Event<any>();
private rpcService: InMemoryStaticGridRpcService;
private auditService: AuditService;
getRowModelType(): string {
return GridRowModelType.CLIENT_SIDE;
@ -37,6 +44,7 @@ export class InMemoryStaticGrid extends GridV2 {
protected initGrid() {
super.initGrid();
this.auditService = this.injector.get(AuditService);
this.rpcService = this.getScript(InMemoryStaticGridRpcService);
if (this.rpcService) {
this.rpcService.loadData().then(response => {
@ -47,6 +55,39 @@ export class InMemoryStaticGrid extends GridV2 {
}
}
onGridReady(event: GridReadyEvent) {
super.onGridReady(event);
this.addColumnFilterChangeListener(() => {
this.auditActiveFilters();
})
}
private auditActiveFilters() {
const filterModel = this.gridApi.getFilterModel();
if (!filterModel || Object.keys(filterModel).length === 0) {
return;
}
const filterMap: Record<string, FilterInfo> = {};
Object.entries(filterModel).forEach(([column, agFilter]) => {
const columnDef = this.gridApi.getColumnDef(column);
if (!columnDef) {
return;
}
const data = FilterService.getFilterData(columnDef, agFilter);
if (!data) {
return;
}
filterMap[agFilter.headerName] = data;
});
if (Object.keys(filterMap).length > 0) {
this.auditService.logActionAudit(AuditConstants.FILTER_EVENT, filterMap);
}
}
getColumns(): any[] {
return this.getScriptsInChildren(GridV2Column)
.map(columnV2 => columnV2.getScript(StaticGridColumn));

View file

@ -0,0 +1,54 @@
import {Injectable} from "@angular/core";
import {HttpClient} from "@angular/common/http";
import {Router} from "@angular/router";
@Injectable({
providedIn: "root"
})
export class AuditService {
constructor(private httpClient: HttpClient, private router: Router) {
}
public logActionAudit(eventType: string, filterInfo?: Record<string, FilterInfo>,
fileName?: string): void {
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const route = this.router.url;
const sourceUrl = window.location.href;
const auditEvent: AuditAction = {
eventType: eventType,
route: route,
sourceUrl: sourceUrl,
filterInfo: filterInfo,
fileName: fileName
}
this.httpClient.post("audit/action", auditEvent, {
headers: {
"Client-Time-Zone": timeZone,
}
}).toPromise();
}
}
export class AuditConstants {
public static readonly OPEN_PAGE_EVENT = "Открытие страницы";
public static readonly FILTER_EVENT = "Поиск по фильтру";
}
export interface AuditAction {
eventType: string;
route: string;
sourceUrl: string;
filterInfo?: Record<string, FilterInfo>;
fileName?: string
}
export interface FilterInfo {
conditionOperator?: string;
conditions: FilterCondition[];
}
export interface FilterCondition {
filterValue: string;
filterType: string;
}

View file

@ -0,0 +1,119 @@
import {DateFilter, NumberFilter, TextFilter} from "ag-grid-community";
import {SetFilter} from "../component/grid/filter/SetFilter";
import {FilterInfo} from "./AuditService";
export class FilterService {
static getFilterData(columnDef: any, agFilter: any): FilterInfo {
if (!agFilter) {
return;
}
switch (columnDef.filter) {
case DateFilter:
case NumberFilter:
return this.processDateOrNumberFilter(agFilter);
case SetFilter:
return this.processSetFilter(agFilter);
case TextFilter:
return this.processTextFilter(agFilter);
default:
return;
}
}
private static processDateOrNumberFilter(agFilter: any): FilterInfo {
if (!agFilter.condition1 && !agFilter.condition2) {
if (agFilter.type === "inRange") {
return this.createSingleConditionData(
this.formatFilterValue(agFilter.dateFrom, agFilter.filter, agFilter.filterType),
agFilter.type,
this.formatFilterValue(agFilter.dateTo, agFilter.filterTo, agFilter.filterType)
);
}
if (agFilter.type === "blank" || agFilter.type === "notBlank") {
return this.createSingleConditionData(null, agFilter.type);
}
return this.createSingleConditionData(
this.formatFilterValue(agFilter.dateFrom, agFilter.filter, agFilter.filterType),
agFilter.type,
);
}
return this.createDualConditionData(agFilter);
}
private static processSetFilter(agFilter: any): FilterInfo {
if (agFilter.value) {
return this.createSingleConditionData(
agFilter.value.join(", "),
"in",
);
}
return;
}
private static processTextFilter(agFilter: any): FilterInfo {
if (!agFilter.condition1 && !agFilter.condition2) {
if (agFilter.type === "blank" || agFilter.type === "notBlank") {
return this.createSingleConditionData(null, agFilter.type);
}
return this.createSingleConditionData(
agFilter.filter,
agFilter.type
);
}
return this.createDualConditionData(agFilter);
}
private static createSingleConditionData(
filterValue: string,
filterType: string,
endValue?: string
): FilterInfo {
return {
conditionOperator: undefined,
conditions: [{
filterValue: endValue ? `${filterValue} to ${endValue}` : filterValue,
filterType: filterType,
}]
};
}
private static createDualConditionData(agFilter: any): FilterInfo {
const condition1 = agFilter.condition1
? {
filterValue: this.getConditionValue(agFilter.condition1),
filterType: agFilter.condition1.type,
}
: undefined;
const condition2 = agFilter.condition2
? {
filterValue: this.getConditionValue(agFilter.condition2),
filterType: agFilter.condition2.type,
}
: undefined;
return {
conditionOperator: agFilter.operator,
conditions: [condition1, condition2]
};
}
private static getConditionValue(condition: any): string {
if (condition.type === "inRange") {
return `${this.formatFilterValue(condition.dateFrom, condition.filter, condition.filterType)}
to ${this.formatFilterValue(condition.dateTo, condition.filterTo, condition.filterType)}`;
}
if (condition.type === "blank" || condition.type === "notBlank") {
return null;
}
return this.formatFilterValue(condition.dateFrom, condition.filter, condition.filterType);
}
private static formatFilterValue(dateValue: any, defaultValue: any, filterType: string): string {
if (filterType === "date" && dateValue) {
return new Date(dateValue).toISOString();
}
return defaultValue;
}
}

View file

@ -27,6 +27,7 @@ import {InMemoryStaticGrid} from "../../ervu/component/grid/InMemoryStaticGrid";
import {ErvuDownloadFileButton} from "../../ervu/component/button/ErvuDownloadFileButton";
import {AuthenticationService} from "../security/authentication.service";
import {HomeLandingComponent} from "./component/home-landing.component";
import {AuditService} from "../../ervu/service/AuditService";
registerLocaleData(localeRu);
export const DIRECTIVES = [
@ -67,7 +68,7 @@ export function checkAuthentication(authService: AuthenticationService): () => P
DIRECTIVES
],
providers: [
AuthenticationService,
AuthenticationService, AuditService,
{
provide: APP_INITIALIZER,
useFactory: checkAuthentication,

View file

@ -8,6 +8,7 @@ import {
Router
} from "@angular/router";
import {ProgressIndicationService} from "@webbpm/base-package";
import {AuditConstants, AuditService} from "../../../ervu/service/AuditService";
@Component({
moduleId: module.id,
@ -21,7 +22,8 @@ export class WebbpmComponent {
constructor(private router: Router,
private progressIndicationService: ProgressIndicationService,
private cd: ChangeDetectorRef) {
private cd: ChangeDetectorRef,
private auditService:AuditService) {
router.events.subscribe((event: Event) => {
if (event instanceof NavigationStart) {
progressIndicationService.showProgressBar();
@ -32,6 +34,12 @@ export class WebbpmComponent {
|| event instanceof NavigationError
|| event instanceof NavigationCancel) {
progressIndicationService.hideProgressBar();
if (event instanceof NavigationEnd
&& event.url != '/home'
&& event.url != '/access-denied') {
this.auditService.logActionAudit(AuditConstants.OPEN_PAGE_EVENT);
}
}
})
}

View file

@ -180,6 +180,7 @@
<componentRootId>3ed7cd92-3c7a-4d6f-a22c-1f3c4031bb61</componentRootId>
<name>VB - левый</name>
<container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="bf098f19-480e-44e4-9084-aa42955c4d0f">
<properties>
@ -1472,6 +1473,22 @@
</item>
</value>
</entry>
</properties>
</scripts>
<scripts id="69b73946-ac9d-48be-aaa9-21629ec156b9">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"NAVIGATION_TO_SOURCE"</simple>
</value>
</entry>
</properties>
</scripts>
</children>
@ -1495,6 +1512,7 @@
<componentRootId>2f05ef7d-9092-4180-a361-8fecb3dd7542</componentRootId>
<name>Date time picker</name>
<container>false</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="5274357c-ce77-4621-8965-bd9c80700008">
<properties>

View file

@ -176,7 +176,6 @@
<componentRootId>3e78f422-3db3-45b9-b531-f4aec5314dab</componentRootId>
<name>Группа полей</name>
<container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f">
<properties>
@ -207,7 +206,6 @@
<componentRootId>c9898352-a317-4117-bfcc-28b5c4d9a0d1</componentRootId>
<name>Горизонтальный контейнер</name>
<container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="bf098f19-480e-44e4-9084-aa42955c4d0f">
<properties>
@ -312,7 +310,6 @@
<componentRootId>1be5e2cd-f42e-40c6-971c-e92f997a7139</componentRootId>
<name>Горизонтальный контейнер</name>
<container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="bf098f19-480e-44e4-9084-aa42955c4d0f"/>
<scripts id="b6068710-0f31-48ec-8e03-c0c1480a40c0"/>
@ -480,6 +477,7 @@
<componentRootId>22ffee0b-eb21-48c8-829c-da9f5dfa9459</componentRootId>
<name>Inner html_xls</name>
<container>false</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380">
<properties>
@ -500,6 +498,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="3891ca34-850d-47c8-aa2f-1eda4bbad60e">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_TEMPLATE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -509,6 +523,7 @@
<componentRootId>ddd8bddd-4b83-4caa-a29f-7d028aaebdc5</componentRootId>
<name>Inner html_csv</name>
<container>false</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380">
<properties>
@ -530,6 +545,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="d916fc3c-5e97-4553-8760-db4ca8659e34">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_TEMPLATE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -604,6 +635,7 @@
<componentRootId>c341ed51-c07a-4c5b-bc13-42acc337f639</componentRootId>
<name>Inner html_xls</name>
<container>false</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380">
<properties>
@ -624,6 +656,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="637f4816-331a-4d8b-90e4-b5f090386eea">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -633,6 +681,7 @@
<componentRootId>74bf561e-8f86-4dd9-a6ed-eccd89ca0086</componentRootId>
<name>Inner html_csv</name>
<container>false</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380">
<properties>
@ -653,6 +702,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="c29421c8-b3fa-44de-b823-b4e620eccf2e">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -1155,6 +1220,7 @@
<componentRootId>9b4bd780-47ec-4e2c-b859-33a4100a7539</componentRootId>
<name>Inner html_xls</name>
<container>false</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380">
<properties>
@ -1170,6 +1236,22 @@
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="1b927f46-31ce-4347-a3e7-299b574881cf">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
</children>
@ -1193,6 +1275,22 @@
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="ebddd83d-e10d-4f84-97e9-032cac81992c">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
</children>
@ -2273,7 +2371,6 @@
<componentRootId>9138d81a-f635-42f6-915c-b3c7be4e2160</componentRootId>
<name>Группа полей</name>
<container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f">
<properties>
@ -2572,6 +2669,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="a09fb7b7-808a-4801-8ae8-0c832246ba63">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_TEMPLATE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -2601,6 +2714,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="acf698fd-bcbd-470f-a4fa-280f67c42f86">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_TEMPLATE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -2695,6 +2824,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="19593dfe-ab78-489c-b80a-7007f5991b1b">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -2724,6 +2869,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="f582e649-3607-4179-a37b-082bb7f9177e">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -3206,6 +3367,22 @@
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="212ce5b2-a27b-4d0e-902c-10336f8222d7">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
</children>
@ -3229,6 +3406,22 @@
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="f249497c-7ef5-4558-a31a-55fbeb73a513">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
</children>
@ -4309,7 +4502,6 @@
<componentRootId>ae731885-3bdd-433d-a29c-37d5811585a7</componentRootId>
<name>Группа полей</name>
<container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f">
<properties>
@ -4608,6 +4800,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="747f7ebb-298d-4a8a-a187-ceb32f77f853">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_TEMPLATE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -4637,6 +4845,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="6947e1b6-e289-4319-93ba-0cb2c3480c34">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_TEMPLATE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -4724,6 +4948,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="1c5e65a2-c50e-4314-bc91-e58f919ce658">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -4753,6 +4993,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="8273436d-cfa6-4d84-8992-a0343f3ecf7e">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -5185,6 +5441,22 @@
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="1b9401b0-c043-48bc-adb7-3a5540bcfd82">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
</children>
@ -5208,6 +5480,22 @@
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="6c736c38-7ea1-4e21-8a74-4d8839137c39">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
</children>
@ -6341,7 +6629,6 @@
<componentRootId>7057bbcb-cff2-4e31-812d-ba7e043a4bcc</componentRootId>
<name>Группа полей</name>
<container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f">
<properties>
@ -6640,6 +6927,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="b2eabc2b-e7a8-4965-b7c0-73580f45f5a9">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_TEMPLATE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -6669,6 +6972,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="6bc9f53c-e89b-4923-ad4d-cc301606ec32">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_TEMPLATE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -6756,6 +7075,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="3f1e0672-6273-4451-a9b9-c732419b0aa6">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -6785,6 +7120,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="12c6f230-ed4c-44f5-9918-0d0ca2586050">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -7268,6 +7619,22 @@
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="23d925ca-0f1f-4055-a2e3-43cece1c1d02">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
</children>
@ -7291,6 +7658,22 @@
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="46f4dd2d-a8cc-4eb2-b75e-4aaeb9701f6a">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
</children>
@ -8371,7 +8754,6 @@
<componentRootId>991237d3-8cb9-48af-8501-030a3c8c6cfc</componentRootId>
<name>Группа полей</name>
<container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f">
<properties>
@ -8670,6 +9052,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="2d8f08ad-2b85-40f9-bf1e-5dd557315d8e">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_TEMPLATE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -8699,6 +9097,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="c8cbb214-e1d3-48d0-bf32-d1338bfef1b9">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_TEMPLATE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -8786,6 +9200,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="1d4169a0-5fac-4536-9206-58c559668cd7">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -8815,6 +9245,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="919e9427-c9e6-422b-8aa6-fdfe2a985100">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -9298,6 +9744,22 @@
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="bc0feeb4-3fa0-47f9-9c38-57db8e4c6015">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
</children>
@ -9321,6 +9783,22 @@
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="87030d5b-ea86-459b-8126-4798176a4ab7">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
</children>
@ -10401,7 +10879,6 @@
<componentRootId>f4eafa61-1ea3-440a-806b-7b05ec416871</componentRootId>
<name>Диалог - сведения направлены</name>
<container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="cf4526a1-96ab-4820-8aa9-62fb54c2b64c">
<properties>
@ -10565,7 +11042,6 @@
<componentRootId>894c4e19-0bd3-4e13-9bd6-d40ab378ca21</componentRootId>
<name>Диалог - что-то пошло не так</name>
<container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="cf4526a1-96ab-4820-8aa9-62fb54c2b64c">
<properties>
@ -10759,7 +11235,6 @@
<componentRootId>a2201fe8-183a-40c7-88ed-bbb07bf2c919</componentRootId>
<name>Группа полей</name>
<container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f">
<properties>
@ -10804,6 +11279,22 @@
<scripts id="f203f156-be32-4131-9c86-4d6bac6d5d56">
<enabled>false</enabled>
</scripts>
<scripts id="cbe4f18e-3eb5-4f83-84fb-94e420bada94">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"NAVIGATION_TO_SOURCE"</simple>
</value>
</entry>
</properties>
</scripts>
</children>
<children id="8152b078-7230-4fb8-994f-023809b95e44">
<prototypeId>fe6407f3-4a81-4b9e-8861-49483cb708a4</prototypeId>
@ -10828,6 +11319,7 @@
<componentRootId>8152b078-7230-4fb8-994f-023809b95e44</componentRootId>
<name>Inner html_xls</name>
<container>false</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered>
<scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380">
<properties>
@ -10848,6 +11340,22 @@
<value>
<simple>null</simple>
</value>
</entry>
</properties>
</scripts>
<scripts id="a761cf7a-5fbb-43c0-a136-696c1dd3c77e">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"DOWNLOAD_EXAMPLE"</simple>
</value>
</entry>
</properties>
</scripts>
@ -10991,6 +11499,22 @@
<scripts id="f203f156-be32-4131-9c86-4d6bac6d5d56">
<enabled>false</enabled>
</scripts>
<scripts id="66b61fd0-4da5-44b7-84dc-9f2cfb60e2f1">
<classRef type="TS">
<className>LinkClickHandler</className>
<packageName>ervu</packageName>
</classRef>
<enabled>true</enabled>
<expanded>true</expanded>
<properties>
<entry>
<key>eventType</key>
<value>
<simple>"NAVIGATION_TO_SOURCE"</simple>
</value>
</entry>
</properties>
</scripts>
</children>
<children id="093b8b33-a959-43e3-9562-249f123585cb">
<prototypeId>e32ae1f5-5b14-45f1-abb6-f52c34b3b570</prototypeId>