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

View file

@ -3,5 +3,5 @@ package ervu.model.fileupload;
/** /**
* @author r.latypov * @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 * @author Alexandr Shalaginov
*/ */
public class EmployeeInfoKafkaMessage { public class EmployeeInfoKafkaMessage {
private final OrgInfo orgInfo; private final UploadOrgInfo orgInfo;
private final FileInfo fileInfo; private final FileInfo fileInfo;
public EmployeeInfoKafkaMessage(OrgInfo orgInfo, FileInfo fileInfo) { public EmployeeInfoKafkaMessage(UploadOrgInfo orgInfo, FileInfo fileInfo) {
this.orgInfo = orgInfo; this.orgInfo = orgInfo;
this.fileInfo = fileInfo; this.fileInfo = fileInfo;
} }
public OrgInfo getOrgInfo() { public UploadOrgInfo getOrgInfo() {
return orgInfo; return orgInfo;
} }
@ -38,7 +38,7 @@ public class EmployeeInfoKafkaMessage {
@Override @Override
public String toString() { public String toString() {
return "KafkaMessage{" + return "KafkaMessage{" +
"orgInfo=" + orgInfo + "uploadOrgInfo=" + orgInfo +
", fileInfo=" + fileInfo + ", fileInfo=" + fileInfo +
'}'; '}';
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import ervu.model.fileupload.EmployeeInfoFileFormType;
import ervu.model.fileupload.EmployeeInfoKafkaMessage; import ervu.model.fileupload.EmployeeInfoKafkaMessage;
import ervu.model.fileupload.FileInfo; import ervu.model.fileupload.FileInfo;
import ervu.model.fileupload.FileStatus; import ervu.model.fileupload.FileStatus;
import ervu.model.fileupload.OrgInfo; import ervu.model.fileupload.UploadOrgInfo;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ru.micord.ervu.journal.SenderInfo; import ru.micord.ervu.journal.SenderInfo;
import ru.micord.ervu.security.esia.model.OrganizationModel; import ru.micord.ervu.security.esia.model.OrganizationModel;
@ -25,7 +25,8 @@ public class EmployeeInfoKafkaMessageService {
public EmployeeInfoKafkaMessage getKafkaMessage(String fileId, String fileUrl, String fileName, public EmployeeInfoKafkaMessage getKafkaMessage(String fileId, String fileUrl, String fileName,
EmployeeInfoFileFormType formType, String departureDateTime, String accessToken, 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( return new EmployeeInfoKafkaMessage(
getOrgInfo(accessToken, ervuId, prnOid, personModel), getOrgInfo(accessToken, ervuId, prnOid, personModel),
getFileInfo( getFileInfo(
@ -35,33 +36,36 @@ public class EmployeeInfoKafkaMessageService {
formType, formType,
departureDateTime, departureDateTime,
offset, offset,
fileStatus fileStatus,
fileSize
) )
); );
} }
private FileInfo getFileInfo(String fileId, String fileUrl, String fileName, private FileInfo getFileInfo(String fileId, String fileUrl, String fileName,
EmployeeInfoFileFormType formType, String departureDateTime, String offset, EmployeeInfoFileFormType formType, String departureDateTime, String offset,
FileStatus fileStatus) { FileStatus fileStatus, long fileSize) {
return new FileInfo( return new FileInfo(
fileId, fileId,
fileUrl, fileUrl,
fileName, fileName,
formType.getFilePatternCode(), formType.getFilePatternCode(),
formType.getFilePatternName(), formType.getFilePatternName(),
String.valueOf(fileSize),
departureDateTime, departureDateTime,
offset, offset,
fileStatus 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); OrganizationModel organizationModel = ulDataService.getOrganizationModel(accessToken);
SenderInfo senderInfo = new SenderInfo(); SenderInfo senderInfo = new SenderInfo();
senderInfo.setFirstName(personModel.getFirstName()); senderInfo.setFirstName(personModel.getFirstName());
senderInfo.setLastName(personModel.getLastName()); senderInfo.setLastName(personModel.getLastName());
senderInfo.setMiddleName(personModel.getMiddleName()); senderInfo.setMiddleName(personModel.getMiddleName());
senderInfo.setPrnOid(prnOid); 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) @JsonIgnoreProperties(ignoreUnknown = true)
public class SenderInfo { public class SenderInfo {
@JsonProperty("prnOid") @JsonProperty("prnOid")
private String prnOid; // идентификатор сотрудника в ЕСИА private String prnOid; // идентификатор сотрудника в ЕСИА
@JsonProperty("lastName") @JsonProperty("lastName")

View file

@ -6,7 +6,7 @@ import java.time.LocalDateTime;
import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonDeserializer;
import ru.micord.ervu.util.DateUtil; import ru.micord.ervu.util.DateUtils;
public class DepartureDateTimeDeserializer extends JsonDeserializer<LocalDateTime> { public class DepartureDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
@ -14,6 +14,6 @@ public class DepartureDateTimeDeserializer extends JsonDeserializer<LocalDateTim
public LocalDateTime deserialize(JsonParser jsonParser, public LocalDateTime deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException { DeserializationContext deserializationContext) throws IOException {
String dateTimeString = jsonParser.getText(); 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.time.ZonedDateTime;
import java.util.TimeZone; import java.util.TimeZone;
import javax.servlet.http.HttpServletRequest;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import ervu.client.fileupload.WebDavClient; import ervu.client.fileupload.WebDavClient;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity; 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.RequestMapping;
import org.springframework.web.bind.annotation.RestController; 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.Data;
import ru.micord.ervu.kafka.model.ExcerptResponse; import ru.micord.ervu.kafka.model.ExcerptResponse;
import ru.micord.ervu.kafka.service.ReplyingKafkaService; import ru.micord.ervu.kafka.service.ReplyingKafkaService;
import ru.micord.ervu.security.webbpm.jwt.UserIdsPair; import ru.micord.ervu.security.webbpm.jwt.UserIdsPair;
import ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil; import ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil;
import ru.micord.ervu.util.UrlUtils;
/** /**
* @author Eduard Tihomirov * @author Eduard Tihomirov
@ -27,6 +33,9 @@ public class ErvuKafkaController {
@Autowired @Autowired
private ReplyingKafkaService replyingKafkaService; private ReplyingKafkaService replyingKafkaService;
@Autowired
private AuditService auditService;
@Autowired @Autowired
private WebDavClient webDavClient; private WebDavClient webDavClient;
@ -40,10 +49,12 @@ public class ErvuKafkaController {
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
@RequestMapping(value = "/kafka/excerpt") @RequestMapping(value = "/kafka/excerpt")
public ResponseEntity<Resource> getExcerptFile( public ResponseEntity<Resource> getExcerptFile(HttpServletRequest request) {
@RequestHeader("Client-Time-Zone") String clientTimeZone) { String fileUrl = null;
String fileName = null;
long fileSize = 0;
try { try {
String clientTimeZone = request.getHeader("Client-Time-Zone");
UserIdsPair userIdsPair = SecurityUtil.getUserIdsPair(); UserIdsPair userIdsPair = SecurityUtil.getUserIdsPair();
Data data = new Data(); Data data = new Data();
data.setErvuId(userIdsPair.getErvuId()); data.setErvuId(userIdsPair.getErvuId());
@ -56,16 +67,30 @@ public class ErvuKafkaController {
ExcerptResponse excerptResponse = objectMapper.readValue(kafkaResponse, ExcerptResponse.class); ExcerptResponse excerptResponse = objectMapper.readValue(kafkaResponse, ExcerptResponse.class);
if (!excerptResponse.getSuccess()) { 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 else if (excerptResponse.getData() == null || excerptResponse.getData().getFileUrl() == null
|| excerptResponse.getData().getFileUrl().isEmpty()) { || excerptResponse.getData().getFileUrl().isEmpty()) {
auditService.processDownloadEvent(request, fileSize, fileName, 1,
AuditConstants.FAILURE_STATUS_TYPE, fileUrl
);
return ResponseEntity.noContent().build(); 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) { 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 @EnableWebSecurity
public class SecurityConfig { public class SecurityConfig {
private static final String[] PERMIT_ALL = new String[] { 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 @Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter; private JwtAuthenticationFilter jwtAuthenticationFilter;

View file

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

View file

@ -1,9 +1,13 @@
package ru.micord.ervu.util; package ru.micord.ervu.util;
import static org.apache.commons.lang3.StringUtils.capitalize; import static org.apache.commons.lang3.StringUtils.capitalize;
import static org.apache.commons.lang3.StringUtils.substring; 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) { public static String convertToFio(String firstName, String middleName, String lastName) {
String firstNameInitial = substring(firstName, 0, 1).toUpperCase(); 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_REQUEST_TOPIC` - топик для записи запроса для получения выписки по журналу взаимодействия
- `ERVU_KAFKA_EXCERPT_REPLY_TOPIC` - топик для чтения выписки по журналу взаимодействия. Содержит ссылку на S3 с файлом выписки - `ERVU_KAFKA_EXCERPT_REPLY_TOPIC` - топик для чтения выписки по журналу взаимодействия. Содержит ссылку на S3 с файлом выписки
- `DB.JOURNAL.EXCLUDED.STATUSES` - статусы файла, которые необходимо исключить при получении данных по журналу взаимодействия из базы данных приложения - `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_CONNECT_TIMEOUT=2
ESNSI_OKOPF_READ_TIMEOUT=4 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_FILE_SIZE=5242880
ERVU_FILE_UPLOAD_MAX_REQUEST_SIZE=6291456 ERVU_FILE_UPLOAD_MAX_REQUEST_SIZE=6291456
ERVU_FILE_UPLOAD_FILE_SIZE_THRESHOLD=0 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.lifetime.seconds" value="300"/>
<property name="file.webdav.extensions" value="csv,xlsx"/> <property name="file.webdav.extensions" value="csv,xlsx"/>
<property name="webdav.bad_servers.cache.expire.seconds" value="120"/> <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> </system-properties>
<management> <management>
<audit-log> <audit-log>

View file

@ -2462,9 +2462,9 @@
} }
}, },
"cadesplugin_api": { "cadesplugin_api": {
"version": "2.0.4-micord.1", "version": "2.1.1-micord.2",
"resolved": "https://repo.micord.ru/repository/npm-all/cadesplugin_api/-/cadesplugin_api-2.0.4-micord.1.tgz", "resolved": "https://repo.micord.ru/repository/npm-all/cadesplugin_api/-/cadesplugin_api-2.1.1-micord.2.tgz",
"integrity": "sha512-FyGVi1VWIyJOW1zOOQN0IkTH/Z/8g7pNWH7A71nf0h21FCX9SacUfgRwID+gl+NlpYiT3m+yZGdlEJsiDeV8JA==" "integrity": "sha512-+j8RfbL7t2YMlSOC9Oa6+NoNLMYC3ZHkc9W6JQnV5+NBUqKLPAlLL1DF6llmW8coRdmgH6nZU8skvdk6M6qaBg=="
}, },
"calendar-utils": { "calendar-utils": {
"version": "0.8.5", "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, GridRow,
GridRowModelType, GridRowModelType,
GridV2, GridV2,
GridV2Column, Visible GridV2Column,
Visible
} from "@webbpm/base-package"; } from "@webbpm/base-package";
import {ChangeDetectionStrategy, Component} from "@angular/core"; import {ChangeDetectionStrategy, Component} from "@angular/core";
import { import {
ColDef, FilterChangedEvent, ColDef,
GridReadyEvent,
FilterChangedEvent,
ICellRendererParams, ICellRendererParams,
ITooltipParams, ITooltipParams,
ValueFormatterParams, ValueFormatterParams,
@ -17,6 +20,9 @@ import {
import {StaticColumnInitializer} from "./StaticColumnInitializer"; import {StaticColumnInitializer} from "./StaticColumnInitializer";
import {InMemoryStaticGridRpcService} from "../../../generated/ru/micord/ervu/service/rpc/InMemoryStaticGridRpcService"; import {InMemoryStaticGridRpcService} from "../../../generated/ru/micord/ervu/service/rpc/InMemoryStaticGridRpcService";
import {StaticGridColumn} from "../../../generated/ru/micord/ervu/property/grid/StaticGridColumn"; import {StaticGridColumn} from "../../../generated/ru/micord/ervu/property/grid/StaticGridColumn";
import { FilterService } from "../../service/FilterService";
import {AuditConstants, AuditService, FilterInfo} from "../../service/AuditService";
@Component({ @Component({
moduleId: module.id, moduleId: module.id,
@ -30,6 +36,7 @@ export class InMemoryStaticGrid extends GridV2 {
public columnFiltersChanged: Event<any> = new Event<any>(); public columnFiltersChanged: Event<any> = new Event<any>();
private rpcService: InMemoryStaticGridRpcService; private rpcService: InMemoryStaticGridRpcService;
private auditService: AuditService;
getRowModelType(): string { getRowModelType(): string {
return GridRowModelType.CLIENT_SIDE; return GridRowModelType.CLIENT_SIDE;
@ -37,6 +44,7 @@ export class InMemoryStaticGrid extends GridV2 {
protected initGrid() { protected initGrid() {
super.initGrid(); super.initGrid();
this.auditService = this.injector.get(AuditService);
this.rpcService = this.getScript(InMemoryStaticGridRpcService); this.rpcService = this.getScript(InMemoryStaticGridRpcService);
if (this.rpcService) { if (this.rpcService) {
this.rpcService.loadData().then(response => { 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[] { getColumns(): any[] {
return this.getScriptsInChildren(GridV2Column) return this.getScriptsInChildren(GridV2Column)
.map(columnV2 => columnV2.getScript(StaticGridColumn)); .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 {ErvuDownloadFileButton} from "../../ervu/component/button/ErvuDownloadFileButton";
import {AuthenticationService} from "../security/authentication.service"; import {AuthenticationService} from "../security/authentication.service";
import {HomeLandingComponent} from "./component/home-landing.component"; import {HomeLandingComponent} from "./component/home-landing.component";
import {AuditService} from "../../ervu/service/AuditService";
registerLocaleData(localeRu); registerLocaleData(localeRu);
export const DIRECTIVES = [ export const DIRECTIVES = [
@ -67,7 +68,7 @@ export function checkAuthentication(authService: AuthenticationService): () => P
DIRECTIVES DIRECTIVES
], ],
providers: [ providers: [
AuthenticationService, AuthenticationService, AuditService,
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: checkAuthentication, useFactory: checkAuthentication,

View file

@ -8,6 +8,7 @@ import {
Router Router
} from "@angular/router"; } from "@angular/router";
import {ProgressIndicationService} from "@webbpm/base-package"; import {ProgressIndicationService} from "@webbpm/base-package";
import {AuditConstants, AuditService} from "../../../ervu/service/AuditService";
@Component({ @Component({
moduleId: module.id, moduleId: module.id,
@ -21,7 +22,8 @@ export class WebbpmComponent {
constructor(private router: Router, constructor(private router: Router,
private progressIndicationService: ProgressIndicationService, private progressIndicationService: ProgressIndicationService,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef,
private auditService:AuditService) {
router.events.subscribe((event: Event) => { router.events.subscribe((event: Event) => {
if (event instanceof NavigationStart) { if (event instanceof NavigationStart) {
progressIndicationService.showProgressBar(); progressIndicationService.showProgressBar();
@ -29,9 +31,15 @@ export class WebbpmComponent {
this.cd.markForCheck(); this.cd.markForCheck();
} }
else if (event instanceof NavigationEnd else if (event instanceof NavigationEnd
|| event instanceof NavigationError || event instanceof NavigationError
|| event instanceof NavigationCancel) { || event instanceof NavigationCancel) {
progressIndicationService.hideProgressBar(); 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> <componentRootId>3ed7cd92-3c7a-4d6f-a22c-1f3c4031bb61</componentRootId>
<name>VB - левый</name> <name>VB - левый</name>
<container>true</container> <container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="bf098f19-480e-44e4-9084-aa42955c4d0f"> <scripts id="bf098f19-480e-44e4-9084-aa42955c4d0f">
<properties> <properties>
@ -1472,6 +1473,22 @@
</item> </item>
</value> </value>
</entry> </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> </properties>
</scripts> </scripts>
</children> </children>
@ -1495,6 +1512,7 @@
<componentRootId>2f05ef7d-9092-4180-a361-8fecb3dd7542</componentRootId> <componentRootId>2f05ef7d-9092-4180-a361-8fecb3dd7542</componentRootId>
<name>Date time picker</name> <name>Date time picker</name>
<container>false</container> <container>false</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="5274357c-ce77-4621-8965-bd9c80700008"> <scripts id="5274357c-ce77-4621-8965-bd9c80700008">
<properties> <properties>

View file

@ -176,7 +176,6 @@
<componentRootId>3e78f422-3db3-45b9-b531-f4aec5314dab</componentRootId> <componentRootId>3e78f422-3db3-45b9-b531-f4aec5314dab</componentRootId>
<name>Группа полей</name> <name>Группа полей</name>
<container>true</container> <container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f"> <scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f">
<properties> <properties>
@ -207,7 +206,6 @@
<componentRootId>c9898352-a317-4117-bfcc-28b5c4d9a0d1</componentRootId> <componentRootId>c9898352-a317-4117-bfcc-28b5c4d9a0d1</componentRootId>
<name>Горизонтальный контейнер</name> <name>Горизонтальный контейнер</name>
<container>true</container> <container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="bf098f19-480e-44e4-9084-aa42955c4d0f"> <scripts id="bf098f19-480e-44e4-9084-aa42955c4d0f">
<properties> <properties>
@ -312,7 +310,6 @@
<componentRootId>1be5e2cd-f42e-40c6-971c-e92f997a7139</componentRootId> <componentRootId>1be5e2cd-f42e-40c6-971c-e92f997a7139</componentRootId>
<name>Горизонтальный контейнер</name> <name>Горизонтальный контейнер</name>
<container>true</container> <container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="bf098f19-480e-44e4-9084-aa42955c4d0f"/> <scripts id="bf098f19-480e-44e4-9084-aa42955c4d0f"/>
<scripts id="b6068710-0f31-48ec-8e03-c0c1480a40c0"/> <scripts id="b6068710-0f31-48ec-8e03-c0c1480a40c0"/>
@ -480,6 +477,7 @@
<componentRootId>22ffee0b-eb21-48c8-829c-da9f5dfa9459</componentRootId> <componentRootId>22ffee0b-eb21-48c8-829c-da9f5dfa9459</componentRootId>
<name>Inner html_xls</name> <name>Inner html_xls</name>
<container>false</container> <container>false</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380"> <scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380">
<properties> <properties>
@ -500,6 +498,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -509,6 +523,7 @@
<componentRootId>ddd8bddd-4b83-4caa-a29f-7d028aaebdc5</componentRootId> <componentRootId>ddd8bddd-4b83-4caa-a29f-7d028aaebdc5</componentRootId>
<name>Inner html_csv</name> <name>Inner html_csv</name>
<container>false</container> <container>false</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380"> <scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380">
<properties> <properties>
@ -530,6 +545,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -604,6 +635,7 @@
<componentRootId>c341ed51-c07a-4c5b-bc13-42acc337f639</componentRootId> <componentRootId>c341ed51-c07a-4c5b-bc13-42acc337f639</componentRootId>
<name>Inner html_xls</name> <name>Inner html_xls</name>
<container>false</container> <container>false</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380"> <scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380">
<properties> <properties>
@ -624,6 +656,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -633,6 +681,7 @@
<componentRootId>74bf561e-8f86-4dd9-a6ed-eccd89ca0086</componentRootId> <componentRootId>74bf561e-8f86-4dd9-a6ed-eccd89ca0086</componentRootId>
<name>Inner html_csv</name> <name>Inner html_csv</name>
<container>false</container> <container>false</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380"> <scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380">
<properties> <properties>
@ -653,6 +702,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -1155,6 +1220,7 @@
<componentRootId>9b4bd780-47ec-4e2c-b859-33a4100a7539</componentRootId> <componentRootId>9b4bd780-47ec-4e2c-b859-33a4100a7539</componentRootId>
<name>Inner html_xls</name> <name>Inner html_xls</name>
<container>false</container> <container>false</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380"> <scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380">
<properties> <properties>
@ -1170,6 +1236,22 @@
<simple>null</simple> <simple>null</simple>
</value> </value>
</entry> </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> </properties>
</scripts> </scripts>
</children> </children>
@ -1193,6 +1275,22 @@
<simple>null</simple> <simple>null</simple>
</value> </value>
</entry> </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> </properties>
</scripts> </scripts>
</children> </children>
@ -2273,7 +2371,6 @@
<componentRootId>9138d81a-f635-42f6-915c-b3c7be4e2160</componentRootId> <componentRootId>9138d81a-f635-42f6-915c-b3c7be4e2160</componentRootId>
<name>Группа полей</name> <name>Группа полей</name>
<container>true</container> <container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f"> <scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f">
<properties> <properties>
@ -2572,6 +2669,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -2601,6 +2714,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -2695,6 +2824,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -2724,6 +2869,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -3206,6 +3367,22 @@
<simple>null</simple> <simple>null</simple>
</value> </value>
</entry> </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> </properties>
</scripts> </scripts>
</children> </children>
@ -3229,6 +3406,22 @@
<simple>null</simple> <simple>null</simple>
</value> </value>
</entry> </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> </properties>
</scripts> </scripts>
</children> </children>
@ -4309,7 +4502,6 @@
<componentRootId>ae731885-3bdd-433d-a29c-37d5811585a7</componentRootId> <componentRootId>ae731885-3bdd-433d-a29c-37d5811585a7</componentRootId>
<name>Группа полей</name> <name>Группа полей</name>
<container>true</container> <container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f"> <scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f">
<properties> <properties>
@ -4608,6 +4800,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -4637,6 +4845,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -4724,6 +4948,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -4753,6 +4993,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -5185,6 +5441,22 @@
<simple>null</simple> <simple>null</simple>
</value> </value>
</entry> </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> </properties>
</scripts> </scripts>
</children> </children>
@ -5208,6 +5480,22 @@
<simple>null</simple> <simple>null</simple>
</value> </value>
</entry> </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> </properties>
</scripts> </scripts>
</children> </children>
@ -6341,7 +6629,6 @@
<componentRootId>7057bbcb-cff2-4e31-812d-ba7e043a4bcc</componentRootId> <componentRootId>7057bbcb-cff2-4e31-812d-ba7e043a4bcc</componentRootId>
<name>Группа полей</name> <name>Группа полей</name>
<container>true</container> <container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f"> <scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f">
<properties> <properties>
@ -6640,6 +6927,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -6669,6 +6972,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -6756,6 +7075,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -6785,6 +7120,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -7268,6 +7619,22 @@
<simple>null</simple> <simple>null</simple>
</value> </value>
</entry> </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> </properties>
</scripts> </scripts>
</children> </children>
@ -7291,6 +7658,22 @@
<simple>null</simple> <simple>null</simple>
</value> </value>
</entry> </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> </properties>
</scripts> </scripts>
</children> </children>
@ -8371,7 +8754,6 @@
<componentRootId>991237d3-8cb9-48af-8501-030a3c8c6cfc</componentRootId> <componentRootId>991237d3-8cb9-48af-8501-030a3c8c6cfc</componentRootId>
<name>Группа полей</name> <name>Группа полей</name>
<container>true</container> <container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f"> <scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f">
<properties> <properties>
@ -8670,6 +9052,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -8699,6 +9097,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -8786,6 +9200,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -8815,6 +9245,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -9298,6 +9744,22 @@
<simple>null</simple> <simple>null</simple>
</value> </value>
</entry> </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> </properties>
</scripts> </scripts>
</children> </children>
@ -9321,6 +9783,22 @@
<simple>null</simple> <simple>null</simple>
</value> </value>
</entry> </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> </properties>
</scripts> </scripts>
</children> </children>
@ -10401,7 +10879,6 @@
<componentRootId>f4eafa61-1ea3-440a-806b-7b05ec416871</componentRootId> <componentRootId>f4eafa61-1ea3-440a-806b-7b05ec416871</componentRootId>
<name>Диалог - сведения направлены</name> <name>Диалог - сведения направлены</name>
<container>true</container> <container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="cf4526a1-96ab-4820-8aa9-62fb54c2b64c"> <scripts id="cf4526a1-96ab-4820-8aa9-62fb54c2b64c">
<properties> <properties>
@ -10565,7 +11042,6 @@
<componentRootId>894c4e19-0bd3-4e13-9bd6-d40ab378ca21</componentRootId> <componentRootId>894c4e19-0bd3-4e13-9bd6-d40ab378ca21</componentRootId>
<name>Диалог - что-то пошло не так</name> <name>Диалог - что-то пошло не так</name>
<container>true</container> <container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="cf4526a1-96ab-4820-8aa9-62fb54c2b64c"> <scripts id="cf4526a1-96ab-4820-8aa9-62fb54c2b64c">
<properties> <properties>
@ -10759,7 +11235,6 @@
<componentRootId>a2201fe8-183a-40c7-88ed-bbb07bf2c919</componentRootId> <componentRootId>a2201fe8-183a-40c7-88ed-bbb07bf2c919</componentRootId>
<name>Группа полей</name> <name>Группа полей</name>
<container>true</container> <container>true</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f"> <scripts id="46f20297-81d1-4786-bb17-2a78ca6fda6f">
<properties> <properties>
@ -10804,6 +11279,22 @@
<scripts id="f203f156-be32-4131-9c86-4d6bac6d5d56"> <scripts id="f203f156-be32-4131-9c86-4d6bac6d5d56">
<enabled>false</enabled> <enabled>false</enabled>
</scripts> </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>
<children id="8152b078-7230-4fb8-994f-023809b95e44"> <children id="8152b078-7230-4fb8-994f-023809b95e44">
<prototypeId>fe6407f3-4a81-4b9e-8861-49483cb708a4</prototypeId> <prototypeId>fe6407f3-4a81-4b9e-8861-49483cb708a4</prototypeId>
@ -10828,6 +11319,7 @@
<componentRootId>8152b078-7230-4fb8-994f-023809b95e44</componentRootId> <componentRootId>8152b078-7230-4fb8-994f-023809b95e44</componentRootId>
<name>Inner html_xls</name> <name>Inner html_xls</name>
<container>false</container> <container>false</container>
<expanded>false</expanded>
<childrenReordered>false</childrenReordered> <childrenReordered>false</childrenReordered>
<scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380"> <scripts id="2dbb6708-68d5-4eff-a760-a0f091ba1380">
<properties> <properties>
@ -10848,6 +11340,22 @@
<value> <value>
<simple>null</simple> <simple>null</simple>
</value> </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> </entry>
</properties> </properties>
</scripts> </scripts>
@ -10991,6 +11499,22 @@
<scripts id="f203f156-be32-4131-9c86-4d6bac6d5d56"> <scripts id="f203f156-be32-4131-9c86-4d6bac6d5d56">
<enabled>false</enabled> <enabled>false</enabled>
</scripts> </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>
<children id="093b8b33-a959-43e3-9562-249f123585cb"> <children id="093b8b33-a959-43e3-9562-249f123585cb">
<prototypeId>e32ae1f5-5b14-45f1-abb6-f52c34b3b570</prototypeId> <prototypeId>e32ae1f5-5b14-45f1-abb6-f52c34b3b570</prototypeId>