From 2f7328d6fbc11274efed9861b28abae51b923453 Mon Sep 17 00:00:00 2001 From: "adel.kalimullin" Date: Mon, 20 Jan 2025 09:55:42 +0300 Subject: [PATCH] SUPPORT-8706: add audit --- .../ervu/client/fileupload/WebDavClient.java | 12 +- .../model/fileupload/DownloadResponse.java | 2 +- .../fileupload/EmployeeInfoKafkaMessage.java | 8 +- .../java/ervu/model/fileupload/FileInfo.java | 15 +- .../ervu/model/fileupload/FileStatus.java | 2 - .../{OrgInfo.java => UploadOrgInfo.java} | 26 +- .../EmployeeInfoFileUploadService.java | 48 +- .../EmployeeInfoKafkaMessageService.java | 16 +- .../ervu/audit/config/AuditKafkaConfig.java | 89 +++ .../ervu/audit/constants/AuditConstants.java | 41 ++ .../audit/controller/AuditController.java | 33 ++ .../ervu/audit/model/AuditActionEvent.java | 67 +++ .../ervu/audit/model/AuditActionRequest.java | 56 ++ .../audit/model/AuditAuthorizationEvent.java | 87 +++ .../ervu/audit/model/AuditDownloadEvent.java | 66 +++ .../micord/ervu/audit/model/AuditEvent.java | 48 ++ .../ervu/audit/model/AuditUploadEvent.java | 35 ++ .../ervu/audit/model/FilterCondition.java | 25 + .../micord/ervu/audit/model/FilterInfo.java | 28 + .../ervu/audit/model/SearchCriteria.java | 30 + .../audit/service/AuditKafkaPublisher.java | 9 + .../ervu/audit/service/AuditService.java | 23 + .../service/impl/BaseAuditKafkaPublisher.java | 43 ++ .../audit/service/impl/BaseAuditService.java | 167 ++++++ .../ru/micord/ervu/journal/SenderInfo.java | 1 - .../DepartureDateTimeDeserializer.java | 4 +- .../kafka/controller/ErvuKafkaController.java | 39 +- .../kafka/exception/ExcerptException.java | 18 + .../exception/ExcerptResponseException.java | 10 + .../micord/ervu/security/SecurityConfig.java | 2 +- .../esia/service/EsiaAuthService.java | 83 ++- .../util/{DateUtil.java => DateUtils.java} | 34 +- .../java/ru/micord/ervu/util/StringUtils.java | 6 +- .../java/ru/micord/ervu/util/UrlUtils.java | 18 + config.md | 14 + config/micord.env | 12 + config/standalone/dev/standalone.xml | 11 + frontend/package-lock.json | 6 +- frontend/src/ts/ervu/LinkClickHandler.ts | 61 ++ .../ervu/component/enum/LinkEventTypeEnum.ts | 5 + .../ervu/component/grid/InMemoryStaticGrid.ts | 45 +- frontend/src/ts/ervu/service/AuditService.ts | 54 ++ frontend/src/ts/ervu/service/FilterService.ts | 119 ++++ frontend/src/ts/modules/app/app.module.ts | 3 +- .../webbpm/component/webbpm.component.ts | 14 +- .../Информация об организации.page | 18 + .../Личный кабинет юр лица.page | 544 +++++++++++++++++- 47 files changed, 1989 insertions(+), 108 deletions(-) rename backend/src/main/java/ervu/model/fileupload/{OrgInfo.java => UploadOrgInfo.java} (55%) create mode 100644 backend/src/main/java/ru/micord/ervu/audit/config/AuditKafkaConfig.java create mode 100644 backend/src/main/java/ru/micord/ervu/audit/constants/AuditConstants.java create mode 100644 backend/src/main/java/ru/micord/ervu/audit/controller/AuditController.java create mode 100644 backend/src/main/java/ru/micord/ervu/audit/model/AuditActionEvent.java create mode 100644 backend/src/main/java/ru/micord/ervu/audit/model/AuditActionRequest.java create mode 100644 backend/src/main/java/ru/micord/ervu/audit/model/AuditAuthorizationEvent.java create mode 100644 backend/src/main/java/ru/micord/ervu/audit/model/AuditDownloadEvent.java create mode 100644 backend/src/main/java/ru/micord/ervu/audit/model/AuditEvent.java create mode 100644 backend/src/main/java/ru/micord/ervu/audit/model/AuditUploadEvent.java create mode 100644 backend/src/main/java/ru/micord/ervu/audit/model/FilterCondition.java create mode 100644 backend/src/main/java/ru/micord/ervu/audit/model/FilterInfo.java create mode 100644 backend/src/main/java/ru/micord/ervu/audit/model/SearchCriteria.java create mode 100644 backend/src/main/java/ru/micord/ervu/audit/service/AuditKafkaPublisher.java create mode 100644 backend/src/main/java/ru/micord/ervu/audit/service/AuditService.java create mode 100644 backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditKafkaPublisher.java create mode 100644 backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditService.java create mode 100644 backend/src/main/java/ru/micord/ervu/kafka/exception/ExcerptException.java create mode 100644 backend/src/main/java/ru/micord/ervu/kafka/exception/ExcerptResponseException.java rename backend/src/main/java/ru/micord/ervu/util/{DateUtil.java => DateUtils.java} (52%) create mode 100644 backend/src/main/java/ru/micord/ervu/util/UrlUtils.java create mode 100644 frontend/src/ts/ervu/LinkClickHandler.ts create mode 100644 frontend/src/ts/ervu/component/enum/LinkEventTypeEnum.ts create mode 100644 frontend/src/ts/ervu/service/AuditService.ts create mode 100644 frontend/src/ts/ervu/service/FilterService.ts diff --git a/backend/src/main/java/ervu/client/fileupload/WebDavClient.java b/backend/src/main/java/ervu/client/fileupload/WebDavClient.java index 6e7fee62..cc483713 100644 --- a/backend/src/main/java/ervu/client/fileupload/WebDavClient.java +++ b/backend/src/main/java/ervu/client/fileupload/WebDavClient.java @@ -44,6 +44,7 @@ import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; +import ru.micord.ervu.util.UrlUtils; /** * @author Alexandr Shalaginov @@ -175,14 +176,17 @@ public class WebDavClient { HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofInputStream() ); - if (response.statusCode() == 200) { + long contentLength = response.headers() + .firstValueAsLong(HttpHeaders.CONTENT_LENGTH) + .orElse(0); InputStreamResource resource = new InputStreamResource(response.body()); - String encodedFilename = URLEncoder.encode(getFilenameFromUrl(url), StandardCharsets.UTF_8); + String encodedFilename = URLEncoder.encode(UrlUtils.extractFileNameFromUrl(url), StandardCharsets.UTF_8); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename ) + .contentLength(contentLength) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(resource); } @@ -196,10 +200,6 @@ public class WebDavClient { } } - private String getFilenameFromUrl(String url) { - String path = URI.create(url).getPath(); - return path.substring(path.lastIndexOf('/') + 1); - } @Retryable(value = {IOException.class}, backoff = @Backoff(delayExpression = "${webdav.retry.delay:500}")) public void deleteFilesOlderThan(long seconds, String url, String... extensions) throws IOException { diff --git a/backend/src/main/java/ervu/model/fileupload/DownloadResponse.java b/backend/src/main/java/ervu/model/fileupload/DownloadResponse.java index 25f28c08..883f749b 100644 --- a/backend/src/main/java/ervu/model/fileupload/DownloadResponse.java +++ b/backend/src/main/java/ervu/model/fileupload/DownloadResponse.java @@ -3,5 +3,5 @@ package ervu.model.fileupload; /** * @author r.latypov */ -public record DownloadResponse(OrgInfo orgInfo, FileInfo fileInfo) { +public record DownloadResponse(UploadOrgInfo orgInfo, FileInfo fileInfo) { } diff --git a/backend/src/main/java/ervu/model/fileupload/EmployeeInfoKafkaMessage.java b/backend/src/main/java/ervu/model/fileupload/EmployeeInfoKafkaMessage.java index 682a832f..a6f1e08f 100644 --- a/backend/src/main/java/ervu/model/fileupload/EmployeeInfoKafkaMessage.java +++ b/backend/src/main/java/ervu/model/fileupload/EmployeeInfoKafkaMessage.java @@ -6,15 +6,15 @@ import java.util.Objects; * @author Alexandr Shalaginov */ public class EmployeeInfoKafkaMessage { - private final OrgInfo orgInfo; + private final UploadOrgInfo orgInfo; private final FileInfo fileInfo; - public EmployeeInfoKafkaMessage(OrgInfo orgInfo, FileInfo fileInfo) { + public EmployeeInfoKafkaMessage(UploadOrgInfo orgInfo, FileInfo fileInfo) { this.orgInfo = orgInfo; this.fileInfo = fileInfo; } - public OrgInfo getOrgInfo() { + public UploadOrgInfo getOrgInfo() { return orgInfo; } @@ -38,7 +38,7 @@ public class EmployeeInfoKafkaMessage { @Override public String toString() { return "KafkaMessage{" + - "orgInfo=" + orgInfo + + "uploadOrgInfo=" + orgInfo + ", fileInfo=" + fileInfo + '}'; } diff --git a/backend/src/main/java/ervu/model/fileupload/FileInfo.java b/backend/src/main/java/ervu/model/fileupload/FileInfo.java index 5ee94d48..5141e433 100644 --- a/backend/src/main/java/ervu/model/fileupload/FileInfo.java +++ b/backend/src/main/java/ervu/model/fileupload/FileInfo.java @@ -11,6 +11,7 @@ public class FileInfo { private String fileName; private String filePatternCode; private String filePatternName; + private String fileSize; private String departureDateTime; private String timeZone; private FileStatus fileStatus; @@ -19,12 +20,14 @@ public class FileInfo { } public FileInfo(String fileId, String fileUrl, String fileName, String filePatternCode, - String filePatternName, String departureDateTime, String timeZone, FileStatus fileStatus) { + String filePatternName, String fileSize, String departureDateTime, String timeZone, + FileStatus fileStatus) { this.fileId = fileId; this.fileUrl = fileUrl; this.fileName = fileName; this.filePatternCode = filePatternCode; this.filePatternName = filePatternName; + this.fileSize = fileSize; this.departureDateTime = departureDateTime; this.timeZone = timeZone; this.fileStatus = fileStatus; @@ -50,6 +53,10 @@ public class FileInfo { return filePatternName; } + public String getFileSize() { + return fileSize; + } + public String getDepartureDateTime() { return departureDateTime; } @@ -73,13 +80,14 @@ public class FileInfo { fileName, fileInfo.fileName) && Objects.equals(filePatternCode, fileInfo.filePatternCode ) && Objects.equals(filePatternName, fileInfo.filePatternName) - && Objects.equals(departureDateTime, fileInfo.departureDateTime); + && Objects.equals(departureDateTime, fileInfo.departureDateTime) && + Objects.equals(fileSize, fileInfo.getFileSize()); } @Override public int hashCode() { return Objects.hash(fileId, fileUrl, fileName, filePatternCode, filePatternName, - departureDateTime + departureDateTime, fileSize ); } @@ -92,6 +100,7 @@ public class FileInfo { ", filePatternCode='" + filePatternCode + '\'' + ", filePatternName='" + filePatternName + '\'' + ", departureDateTime='" + departureDateTime + '\'' + + ", fileSize='" + fileSize + '\'' + '}'; } } diff --git a/backend/src/main/java/ervu/model/fileupload/FileStatus.java b/backend/src/main/java/ervu/model/fileupload/FileStatus.java index 0506631b..a34038a2 100644 --- a/backend/src/main/java/ervu/model/fileupload/FileStatus.java +++ b/backend/src/main/java/ervu/model/fileupload/FileStatus.java @@ -1,12 +1,10 @@ package ervu.model.fileupload; -import java.io.Serializable; /** * @author Eduard Tihomirov */ public class FileStatus { - private String code; private String status; private String description; diff --git a/backend/src/main/java/ervu/model/fileupload/OrgInfo.java b/backend/src/main/java/ervu/model/fileupload/UploadOrgInfo.java similarity index 55% rename from backend/src/main/java/ervu/model/fileupload/OrgInfo.java rename to backend/src/main/java/ervu/model/fileupload/UploadOrgInfo.java index 767f3e3c..c35a533a 100644 --- a/backend/src/main/java/ervu/model/fileupload/OrgInfo.java +++ b/backend/src/main/java/ervu/model/fileupload/UploadOrgInfo.java @@ -7,18 +7,24 @@ import ru.micord.ervu.journal.SenderInfo; /** * @author Alexandr Shalaginov */ -public class OrgInfo { +public class UploadOrgInfo { private String orgName; private String orgId; private SenderInfo senderInfo; + private String esiaOrgId; - public OrgInfo() { + public UploadOrgInfo() { } - public OrgInfo(String orgName, String orgId, SenderInfo senderInfo) { + public UploadOrgInfo(String orgName, String orgId, SenderInfo senderInfo, String esiaOrgId) { this.orgName = orgName; this.orgId = orgId; this.senderInfo = senderInfo; + this.esiaOrgId = esiaOrgId; + } + + public String getEsiaOrgId() { + return esiaOrgId; } public String getOrgName() { @@ -37,23 +43,25 @@ public class OrgInfo { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - OrgInfo orgInfo = (OrgInfo) o; - return Objects.equals(orgName, orgInfo.orgName) && Objects.equals(orgId, - orgInfo.orgId - ) && Objects.equals(senderInfo, orgInfo.senderInfo); + UploadOrgInfo uploadOrgInfo = (UploadOrgInfo) o; + return Objects.equals(orgName, uploadOrgInfo.orgName) && Objects.equals(orgId, + uploadOrgInfo.orgId + ) && Objects.equals(senderInfo, uploadOrgInfo.senderInfo) + && Objects.equals(esiaOrgId, uploadOrgInfo.esiaOrgId); } @Override public int hashCode() { - return Objects.hash(orgName, orgId, senderInfo); + return Objects.hash(orgName, orgId, senderInfo, esiaOrgId); } @Override public String toString() { - return "OrgInfo{" + + return "UploadOrgInfo{" + "orgName='" + orgName + '\'' + ", orgId='" + orgId + '\'' + ", senderInfo='" + senderInfo + '\'' + + ", esiaOrgId='" + esiaOrgId + '\'' + '}'; } } diff --git a/backend/src/main/java/ervu/service/fileupload/EmployeeInfoFileUploadService.java b/backend/src/main/java/ervu/service/fileupload/EmployeeInfoFileUploadService.java index 4289785a..953994e2 100644 --- a/backend/src/main/java/ervu/service/fileupload/EmployeeInfoFileUploadService.java +++ b/backend/src/main/java/ervu/service/fileupload/EmployeeInfoFileUploadService.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.sql.Timestamp; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.Locale; import java.util.UUID; @@ -31,6 +30,7 @@ import org.springframework.kafka.annotation.KafkaListener; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import ru.micord.ervu.audit.service.AuditService; import ru.micord.ervu.exception.JsonParsingException; import ru.micord.ervu.security.esia.model.EmployeeModel; import ru.micord.ervu.security.esia.model.PersonModel; @@ -38,6 +38,7 @@ import ru.micord.ervu.security.esia.service.UlDataService; import ru.micord.ervu.security.esia.token.EsiaTokensStore; import ru.micord.ervu.security.webbpm.jwt.UserIdsPair; import ru.micord.ervu.service.InteractionService; +import ru.micord.ervu.util.DateUtils; import static ervu.enums.FileStatusCode.FILE_CLEAN; import static ervu.enums.FileStatusCode.FILE_INFECTED; @@ -51,13 +52,12 @@ import static ru.micord.ervu.util.StringUtils.convertToFio; @Service public class EmployeeInfoFileUploadService { private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeInfoFileUploadService.class); - private static final String FORMAT = "dd.MM.yyyy HH:mm:ss"; - private final WebDavClient webDavClient; private final EmployeeInfoKafkaMessageService employeeInfoKafkaMessageService; private final KafkaTemplate kafkaTemplate; private final InteractionService interactionService; private final UlDataService ulDataService; + private final AuditService auditService; @Value("${av.kafka.message.topic.name}") private String kafkaTopicName; @@ -67,12 +67,14 @@ public class EmployeeInfoFileUploadService { EmployeeInfoKafkaMessageService employeeInfoKafkaMessageService, @Qualifier("avTemplate") KafkaTemplate kafkaTemplate, InteractionService interactionService, - UlDataService ulDataService) { + UlDataService ulDataService, + AuditService auditService) { this.webDavClient = webDavClient; this.employeeInfoKafkaMessageService = employeeInfoKafkaMessageService; this.kafkaTemplate = kafkaTemplate; this.interactionService = interactionService; this.ulDataService = ulDataService; + this.auditService = auditService; } public boolean saveEmployeeInformationFile(MultipartFile multipartFile, String formType, @@ -99,29 +101,34 @@ public class EmployeeInfoFileUploadService { convertToFio(personModel.getFirstName(), personModel.getMiddleName(), personModel.getLastName()), 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) { fileStatus.setCode(FILE_UPLOADED.getCode()); fileStatus.setDescription("Файл принят до проверки на вирусы"); - String departureDateTime = now.format(DateTimeFormatter.ofPattern(FORMAT)); - String jsonMessage = getJsonKafkaMessage( - employeeInfoKafkaMessageService.getKafkaMessage( - fileId, - fileUploadUrl, - fileName, - employeeInfoFileFormType, - departureDateTime, - accessToken, - offset, - fileStatus, - ervuId, - esiaUserId, - personModel - ) - ); + String jsonMessage = getJsonKafkaMessage(kafkaMessage); return sendMessage(jsonMessage); } else { LOGGER.error("Failed to upload file: {}", fileName); + fileStatus.setCode(FILE_NOT_CHECKED.getCode()); + fileStatus.setDescription("Невозможно проверить файл по причине недоступности или ошибки в работе антивируса"); + auditService.processUploadEvent(kafkaMessage.getOrgInfo(), kafkaMessage.getFileInfo()); return false; } } @@ -199,6 +206,7 @@ public class EmployeeInfoFileUploadService { interactionService.delete(fileInfo.getFileId(), downloadResponse.orgInfo().getOrgId()); } else if (statusCode.equals(FILE_NOT_CHECKED.getCode())) { + auditService.processUploadEvent(downloadResponse.orgInfo(), downloadResponse.fileInfo()); interactionService.updateStatus(fileInfo.getFileId(), fileInfo.getFileStatus().getStatus(), downloadResponse.orgInfo().getOrgId() ); diff --git a/backend/src/main/java/ervu/service/fileupload/EmployeeInfoKafkaMessageService.java b/backend/src/main/java/ervu/service/fileupload/EmployeeInfoKafkaMessageService.java index 8afc0242..d8414f2b 100644 --- a/backend/src/main/java/ervu/service/fileupload/EmployeeInfoKafkaMessageService.java +++ b/backend/src/main/java/ervu/service/fileupload/EmployeeInfoKafkaMessageService.java @@ -4,7 +4,7 @@ import ervu.model.fileupload.EmployeeInfoFileFormType; import ervu.model.fileupload.EmployeeInfoKafkaMessage; import ervu.model.fileupload.FileInfo; import ervu.model.fileupload.FileStatus; -import ervu.model.fileupload.OrgInfo; +import ervu.model.fileupload.UploadOrgInfo; import org.springframework.stereotype.Service; import ru.micord.ervu.journal.SenderInfo; import ru.micord.ervu.security.esia.model.OrganizationModel; @@ -25,7 +25,8 @@ public class EmployeeInfoKafkaMessageService { public EmployeeInfoKafkaMessage getKafkaMessage(String fileId, String fileUrl, String fileName, EmployeeInfoFileFormType formType, String departureDateTime, String accessToken, - String offset, FileStatus fileStatus, String ervuId, String prnOid, PersonModel personModel) { + String offset, FileStatus fileStatus, String ervuId, String prnOid, + PersonModel personModel, long fileSize) { return new EmployeeInfoKafkaMessage( getOrgInfo(accessToken, ervuId, prnOid, personModel), getFileInfo( @@ -35,33 +36,36 @@ public class EmployeeInfoKafkaMessageService { formType, departureDateTime, offset, - fileStatus + fileStatus, + fileSize ) ); } private FileInfo getFileInfo(String fileId, String fileUrl, String fileName, EmployeeInfoFileFormType formType, String departureDateTime, String offset, - FileStatus fileStatus) { + FileStatus fileStatus, long fileSize) { return new FileInfo( fileId, fileUrl, fileName, formType.getFilePatternCode(), formType.getFilePatternName(), + String.valueOf(fileSize), departureDateTime, offset, fileStatus ); } - private OrgInfo getOrgInfo(String accessToken, String ervuId, String prnOid, PersonModel personModel) { + private UploadOrgInfo getOrgInfo(String accessToken, String ervuId, String prnOid, PersonModel personModel) { OrganizationModel organizationModel = ulDataService.getOrganizationModel(accessToken); SenderInfo senderInfo = new SenderInfo(); senderInfo.setFirstName(personModel.getFirstName()); senderInfo.setLastName(personModel.getLastName()); senderInfo.setMiddleName(personModel.getMiddleName()); senderInfo.setPrnOid(prnOid); - return new OrgInfo(organizationModel.getFullName(), ervuId, senderInfo); + return new UploadOrgInfo(organizationModel.getFullName(), ervuId, senderInfo, + organizationModel.getOid()); } } diff --git a/backend/src/main/java/ru/micord/ervu/audit/config/AuditKafkaConfig.java b/backend/src/main/java/ru/micord/ervu/audit/config/AuditKafkaConfig.java new file mode 100644 index 00000000..67b8ea0e --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/config/AuditKafkaConfig.java @@ -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 producerFactory() { + Map 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 kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } + + @Bean + public KafkaAdmin auditKafkaAdmin() { + Map 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(); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/constants/AuditConstants.java b/backend/src/main/java/ru/micord/ervu/audit/constants/AuditConstants.java new file mode 100644 index 00000000..1bc3792a --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/constants/AuditConstants.java @@ -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 routeDescriptions = Map.of( + "/", "Личный кабинет ЮР лица", + "/mydata", "Информация об организации", + "/filesentlog", "Журнал взаимодействия" + ); + + private static final Map 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)); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/controller/AuditController.java b/backend/src/main/java/ru/micord/ervu/audit/controller/AuditController.java new file mode 100644 index 00000000..3fe6745c --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/controller/AuditController.java @@ -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 auditAction( + HttpServletRequest request, @RequestBody AuditActionRequest actionEvent) { + auditService.processActionEvent(request, actionEvent); + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/AuditActionEvent.java b/backend/src/main/java/ru/micord/ervu/audit/model/AuditActionEvent.java new file mode 100644 index 00000000..95b1a0d3 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditActionEvent.java @@ -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 searchAttributes; + private String fileName; + + public AuditActionEvent( + String esiaOrgId, String esiaPersonId, String eventTime, + String eventType, String description, String sourceUrl, + List 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 getSearchAttributes() { + return searchAttributes; + } + + public void setSearchAttributes(List searchAttributes) { + this.searchAttributes = searchAttributes; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/AuditActionRequest.java b/backend/src/main/java/ru/micord/ervu/audit/model/AuditActionRequest.java new file mode 100644 index 00000000..7a6bf10f --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditActionRequest.java @@ -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 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 getFilterInfo() { + return filterInfo; + } + + public void setFilterInfo( + Map filterInfo) { + this.filterInfo = filterInfo; + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/AuditAuthorizationEvent.java b/backend/src/main/java/ru/micord/ervu/audit/model/AuditAuthorizationEvent.java new file mode 100644 index 00000000..98d62776 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditAuthorizationEvent.java @@ -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; + } +} \ No newline at end of file diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/AuditDownloadEvent.java b/backend/src/main/java/ru/micord/ervu/audit/model/AuditDownloadEvent.java new file mode 100644 index 00000000..36c22145 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditDownloadEvent.java @@ -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; + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/AuditEvent.java b/backend/src/main/java/ru/micord/ervu/audit/model/AuditEvent.java new file mode 100644 index 00000000..a47ea802 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditEvent.java @@ -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; + } +} \ No newline at end of file diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/AuditUploadEvent.java b/backend/src/main/java/ru/micord/ervu/audit/model/AuditUploadEvent.java new file mode 100644 index 00000000..65270471 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditUploadEvent.java @@ -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; + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/FilterCondition.java b/backend/src/main/java/ru/micord/ervu/audit/model/FilterCondition.java new file mode 100644 index 00000000..631e0508 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/FilterCondition.java @@ -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; + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/FilterInfo.java b/backend/src/main/java/ru/micord/ervu/audit/model/FilterInfo.java new file mode 100644 index 00000000..ec3d9fa5 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/FilterInfo.java @@ -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 conditions; + + public String getConditionOperator() { + return conditionOperator; + } + + public void setConditionOperator(String conditionOperator) { + this.conditionOperator = conditionOperator; + } + + public List getConditions() { + return conditions; + } + + public void setConditions(List conditions) { + this.conditions = conditions; + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/model/SearchCriteria.java b/backend/src/main/java/ru/micord/ervu/audit/model/SearchCriteria.java new file mode 100644 index 00000000..1342bc95 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/SearchCriteria.java @@ -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; + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/service/AuditKafkaPublisher.java b/backend/src/main/java/ru/micord/ervu/audit/service/AuditKafkaPublisher.java new file mode 100644 index 00000000..f8992b44 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/service/AuditKafkaPublisher.java @@ -0,0 +1,9 @@ +package ru.micord.ervu.audit.service; + + +/** + * @author Adel Kalimullin + */ +public interface AuditKafkaPublisher { + void publishEvent(String topicName, String message); +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/service/AuditService.java b/backend/src/main/java/ru/micord/ervu/audit/service/AuditService.java new file mode 100644 index 00000000..d9f9ea8d --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/service/AuditService.java @@ -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); +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditKafkaPublisher.java b/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditKafkaPublisher.java new file mode 100644 index 00000000..9138f48d --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditKafkaPublisher.java @@ -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 kafkaTemplate; + @Value("${audit.kafka.enabled}") + private boolean auditEnabled; + + public BaseAuditKafkaPublisher( + @Qualifier("auditTemplate") KafkaTemplate 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."); + } + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditService.java b/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditService.java new file mode 100644 index 00000000..768d7150 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditService.java @@ -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 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 getSearchCriteriaList(Map filterInfoMap) { + List searchCriteriaList = new ArrayList<>(); + + for (Map.Entry 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); + } + } +} diff --git a/backend/src/main/java/ru/micord/ervu/journal/SenderInfo.java b/backend/src/main/java/ru/micord/ervu/journal/SenderInfo.java index 22b80272..23bf4d2d 100644 --- a/backend/src/main/java/ru/micord/ervu/journal/SenderInfo.java +++ b/backend/src/main/java/ru/micord/ervu/journal/SenderInfo.java @@ -5,7 +5,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) public class SenderInfo { - @JsonProperty("prnOid") private String prnOid; // идентификатор сотрудника в ЕСИА @JsonProperty("lastName") diff --git a/backend/src/main/java/ru/micord/ervu/journal/deserializer/DepartureDateTimeDeserializer.java b/backend/src/main/java/ru/micord/ervu/journal/deserializer/DepartureDateTimeDeserializer.java index 149f720f..e6a2b153 100644 --- a/backend/src/main/java/ru/micord/ervu/journal/deserializer/DepartureDateTimeDeserializer.java +++ b/backend/src/main/java/ru/micord/ervu/journal/deserializer/DepartureDateTimeDeserializer.java @@ -6,7 +6,7 @@ import java.time.LocalDateTime; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; -import ru.micord.ervu.util.DateUtil; +import ru.micord.ervu.util.DateUtils; public class DepartureDateTimeDeserializer extends JsonDeserializer { @@ -14,6 +14,6 @@ public class DepartureDateTimeDeserializer extends JsonDeserializer getExcerptFile( - @RequestHeader("Client-Time-Zone") String clientTimeZone) { - + public ResponseEntity getExcerptFile(HttpServletRequest request) { + String fileUrl = null; + String fileName = null; + long fileSize = 0; try { + String clientTimeZone = request.getHeader("Client-Time-Zone"); UserIdsPair userIdsPair = SecurityUtil.getUserIdsPair(); Data data = new Data(); data.setErvuId(userIdsPair.getErvuId()); @@ -56,16 +67,30 @@ public class ErvuKafkaController { ExcerptResponse excerptResponse = objectMapper.readValue(kafkaResponse, ExcerptResponse.class); if (!excerptResponse.getSuccess()) { - throw new RuntimeException("Error with getting excerpt url " + excerptResponse.getMessage()); + throw new ExcerptResponseException( + "Error with getting excerpt url " + excerptResponse.getMessage()); } else if (excerptResponse.getData() == null || excerptResponse.getData().getFileUrl() == null || excerptResponse.getData().getFileUrl().isEmpty()) { + auditService.processDownloadEvent(request, fileSize, fileName, 1, + AuditConstants.FAILURE_STATUS_TYPE, fileUrl + ); return ResponseEntity.noContent().build(); } - return webDavClient.webDavDownloadFile(excerptResponse.getData().getFileUrl()); + fileUrl = excerptResponse.getData().getFileUrl(); + fileName = UrlUtils.extractFileNameFromUrl(excerptResponse.getData().getFileUrl()); + ResponseEntity responseEntity = webDavClient.webDavDownloadFile(fileUrl); + fileSize = responseEntity.getHeaders().getContentLength(); + auditService.processDownloadEvent(request, fileSize, fileName, 1, + AuditConstants.SUCCESS_STATUS_TYPE, fileUrl + ); + return responseEntity; } catch (Exception e) { - throw new RuntimeException(e); + auditService.processDownloadEvent(request, fileSize, fileName, 1, + AuditConstants.FAILURE_STATUS_TYPE, fileUrl + ); + throw new ExcerptException(e); } } } diff --git a/backend/src/main/java/ru/micord/ervu/kafka/exception/ExcerptException.java b/backend/src/main/java/ru/micord/ervu/kafka/exception/ExcerptException.java new file mode 100644 index 00000000..fbcfb685 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/kafka/exception/ExcerptException.java @@ -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); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/kafka/exception/ExcerptResponseException.java b/backend/src/main/java/ru/micord/ervu/kafka/exception/ExcerptResponseException.java new file mode 100644 index 00000000..ee8cd409 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/kafka/exception/ExcerptResponseException.java @@ -0,0 +1,10 @@ +package ru.micord.ervu.kafka.exception; + +/** + * @author Adel Kalimullin + */ +public class ExcerptResponseException extends RuntimeException { + public ExcerptResponseException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java b/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java index cc1bfd54..43236f99 100644 --- a/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java +++ b/backend/src/main/java/ru/micord/ervu/security/SecurityConfig.java @@ -31,7 +31,7 @@ import static ru.micord.ervu.security.SecurityConstants.ESIA_LOGOUT; @EnableWebSecurity public class SecurityConfig { private static final String[] PERMIT_ALL = new String[] { - "/version", "/esia/url", "/esia/auth", "esia/refresh", "/esia/logout", + "/version", "/esia/url", "/esia/auth", "esia/refresh", "/esia/logout" }; @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; diff --git a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java index 022dedb4..093adcf1 100644 --- a/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java +++ b/backend/src/main/java/ru/micord/ervu/security/esia/service/EsiaAuthService.java @@ -21,7 +21,8 @@ import javax.servlet.http.HttpServletResponse; import com.fasterxml.jackson.databind.ObjectMapper; import ervu.service.okopf.OkopfService; -import org.springframework.stereotype.Service; +import ru.micord.ervu.audit.constants.AuditConstants; +import ru.micord.ervu.audit.service.AuditService; import ru.micord.ervu.security.esia.exception.EsiaException; import ru.micord.ervu.security.esia.model.EmployeeModel; import ru.micord.ervu.security.esia.model.EsiaAccessToken; @@ -47,6 +48,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; import ru.micord.ervu.security.webbpm.jwt.JwtAuthentication; import ru.micord.ervu.security.webbpm.jwt.helper.SecurityHelper; import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService; @@ -74,6 +76,8 @@ public class EsiaAuthService { private OkopfService okopfService; @Autowired private SecurityHelper securityHelper; + @Autowired + private AuditService auditService; @Value("${ervu.kafka.org.reply.topic}") private String requestReplyTopic; @@ -244,20 +248,33 @@ public class EsiaAuthService { LOGGER.info("Thread {}: SignSecret: {}ms RequestAccessToken: {}ms VerifySecret: {}ms", Thread.currentThread().getId(), timeSignSecret, timeRequestAccessToken, timeVerifySecret); } + OrgInfo orgInfo = null; try { + orgInfo = getOrgInfo(esiaAccessTokenStr); hasRole = ulDataService.checkRole(esiaAccessTokenStr); - String ervuId = getErvuId(esiaAccessTokenStr, prnOid); + String ervuId = getErvuId(prnOid, orgInfo); createTokenAndAddCookie(response, prnOid, ervuId, hasRole, expiresIn); if (!hasRole) { LOGGER.error("The user with id = " + prnOid + " does not have the required role"); + auditService.processAuthEvent(request, orgInfo, prnOid, AuditConstants.FAILURE_STATUS_TYPE, + AuditConstants.LOGIN_EVENT_TYPE + ); return new ResponseEntity<>( "Доступ запрещен. Пользователь должен быть включен в группу \"Сотрудник, ответственный за военно-учетную работу\" в ЕСИА", HttpStatus.FORBIDDEN ); } + auditService.processAuthEvent(request, orgInfo, prnOid, AuditConstants.SUCCESS_STATUS_TYPE, + AuditConstants.LOGIN_EVENT_TYPE + ); return ResponseEntity.ok("Authentication successful"); } catch (Exception e) { + if (orgInfo!= null){ + auditService.processAuthEvent(request, orgInfo, prnOid, AuditConstants.FAILURE_STATUS_TYPE, + AuditConstants.LOGIN_EVENT_TYPE + ); + } createTokenAndAddCookie(response, prnOid, null, hasRole , expiresIn); String messageId = getMessageId(e); String messageWithId = String.format("[%s] %s", messageId, e.getMessage()); @@ -333,7 +350,8 @@ public class EsiaAuthService { Long expiresIn = tokenResponse.getExpires_in(); EsiaTokensStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn); EsiaTokensStore.addRefreshToken(prnOid, esiaNewRefreshToken, expiresIn); - String ervuId = getErvuId(esiaAccessTokenStr, prnOid); + OrgInfo orgInfo = getOrgInfo(esiaAccessTokenStr); + String ervuId = getErvuId(prnOid, orgInfo); createTokenAndAddCookie(response, esiaAccessToken.getSbj_id(), ervuId, true, expiresIn); } catch (Exception e) { @@ -374,9 +392,13 @@ public class EsiaAuthService { } public String logout(HttpServletRequest request, HttpServletResponse response) { + OrgInfo orgInfo = null; + String userId = null; try { + userId = jwtTokenService.getUserAccountId(request); + String accessToken = EsiaTokensStore.getAccessToken(userId); + orgInfo = getOrgInfo(accessToken); securityHelper.clearAccessCookies(response); - String userId = jwtTokenService.getUserAccountId(request); EsiaTokensStore.removeAccessToken(userId); EsiaTokensStore.removeRefreshToken(userId); String logoutUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaLogoutUrl(); @@ -385,32 +407,30 @@ public class EsiaAuthService { Map params = mapOf( "client_id", esiaConfig.getClientId(), "redirect_url", redirectUrl); + auditService.processAuthEvent(request, orgInfo, userId, AuditConstants.SUCCESS_STATUS_TYPE, + AuditConstants.LOGOUT_EVENT_TYPE + ); return buildUrl(url, params); } catch (Exception e) { + if (orgInfo != null) { + auditService.processAuthEvent(request, orgInfo, userId, AuditConstants.FAILURE_STATUS_TYPE, + AuditConstants.LOGOUT_EVENT_TYPE + ); + } throw new EsiaException(e); } } - public String getErvuId(String accessToken, String prnOid) { - long timeRequestPersonDataOrg = 0, timeRequestPersonDataEmployee = 0, timeRequestPersonDataChief = 0, timeRequestIdERVU = 0; - 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); + public String getErvuId(String prnOid, OrgInfo orgInfo) { + long timeRequestIdERVU = 0; + try { orgInfo.setOrgTypeName(okopfService.findTitleByLeg(orgInfo.getOrgTypeLeg())); - startTime = System.currentTimeMillis(); + long startTime = System.currentTimeMillis(); String kafkaResponse = replyingKafkaService.sendMessageAndGetReply(requestTopic, requestReplyTopic, objectMapper.writeValueAsString(orgInfo) ); - timeRequestIdERVU = System.currentTimeMillis() - startTime; + timeRequestIdERVU = System.currentTimeMillis() - startTime; ErvuOrgResponse ervuOrgResponse = objectMapper.readValue(kafkaResponse, ErvuOrgResponse.class); String ervuId = ervuOrgResponse.getData().getErvuId(); @@ -423,8 +443,29 @@ public class EsiaAuthService { throw new EsiaException(e); } finally { - LOGGER.info("Thread {}: RequestPersonDataOrg: {}ms RequestPersonDataEmployee: {}ms RequestPersonDataChief: {}ms RequestIdERVU: {}ms", - Thread.currentThread().getId(), timeRequestPersonDataOrg, timeRequestPersonDataEmployee, timeRequestPersonDataChief, timeRequestIdERVU); + LOGGER.info("Thread {}: RequestIdERVU: {}ms", Thread.currentThread().getId(), timeRequestIdERVU); + } + } + + private OrgInfo getOrgInfo(String accessToken) { + long timeRequestPersonDataOrg = 0, timeRequestPersonDataEmployee = 0, timeRequestPersonDataChief = 0; + try { + long startTime = System.currentTimeMillis(); + OrganizationModel organizationModel = ulDataService.getOrganizationModel(accessToken); + timeRequestPersonDataOrg = System.currentTimeMillis() - startTime; + startTime = System.currentTimeMillis(); + EmployeeModel employeeModel = ulDataService.getEmployeeModel(accessToken); + timeRequestPersonDataEmployee = System.currentTimeMillis() - startTime; + startTime = System.currentTimeMillis(); + EmployeeModel chiefModel = ulDataService.getChiefEmployeeModel(accessToken); + timeRequestPersonDataChief = System.currentTimeMillis() - startTime; + return copyToOrgInfo(organizationModel, employeeModel, chiefModel); + } + finally { + LOGGER.info( + "Thread {}: RequestPersonDataOrg: {}ms RequestPersonDataEmployee: {}ms RequestPersonDataChief: {}ms", + Thread.currentThread().getId(), timeRequestPersonDataOrg, timeRequestPersonDataEmployee, timeRequestPersonDataChief + ); } } diff --git a/backend/src/main/java/ru/micord/ervu/util/DateUtil.java b/backend/src/main/java/ru/micord/ervu/util/DateUtils.java similarity index 52% rename from backend/src/main/java/ru/micord/ervu/util/DateUtil.java rename to backend/src/main/java/ru/micord/ervu/util/DateUtils.java index 80644b92..c954a1dc 100644 --- a/backend/src/main/java/ru/micord/ervu/util/DateUtil.java +++ b/backend/src/main/java/ru/micord/ervu/util/DateUtils.java @@ -1,9 +1,10 @@ package ru.micord.ervu.util; -import java.time.LocalDate; -import java.time.LocalDateTime; +import java.time.*; import java.time.format.DateTimeFormatter; +import javax.servlet.http.HttpServletRequest; + import org.springframework.util.StringUtils; import static org.springframework.util.StringUtils.hasText; @@ -11,12 +12,31 @@ import static org.springframework.util.StringUtils.hasText; /** * @author gulnaz */ -public final class DateUtil { - +public final class DateUtils { private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy"); private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"); + private static final DateTimeFormatter DATE_TIME_WITH_TIMEZONE_FORMATTER = DateTimeFormatter.ofPattern( + "yyyy-MM-dd'T'HH:mm:ss.SSSX"); - private DateUtil() {} + private DateUtils() { + } + + public static String getCurrentFormattedDateTimeWithZone(){ + ZonedDateTime now = ZonedDateTime.now(); + return now.format(DATE_TIME_WITH_TIMEZONE_FORMATTER); + } + + public static String getClientDateTimeWithZoneFromRequest(HttpServletRequest request) { + String clientTimeZone = request.getHeader("Client-Time-Zone"); + ZoneId zoneId; + try { + zoneId = ZoneId.of(clientTimeZone); + } + catch (Exception e) { + zoneId = ZoneId.systemDefault(); + } + return ZonedDateTime.now(zoneId).format(DATE_TIME_WITH_TIMEZONE_FORMATTER); + } public static LocalDate convertToLocalDate(String date) { return StringUtils.hasText(date) @@ -30,6 +50,10 @@ public final class DateUtil { : null; } + public static String convertToString(LocalDateTime dateTime) { + return dateTime == null ? "" : dateTime.format(DATE_TIME_FORMATTER); + } + public static String convertToString(LocalDate date) { return date == null ? "" : date.format(DATE_FORMATTER); } diff --git a/backend/src/main/java/ru/micord/ervu/util/StringUtils.java b/backend/src/main/java/ru/micord/ervu/util/StringUtils.java index 2d3cb4a5..0fdfaff0 100644 --- a/backend/src/main/java/ru/micord/ervu/util/StringUtils.java +++ b/backend/src/main/java/ru/micord/ervu/util/StringUtils.java @@ -1,9 +1,13 @@ package ru.micord.ervu.util; + import static org.apache.commons.lang3.StringUtils.capitalize; import static org.apache.commons.lang3.StringUtils.substring; -public class StringUtils { +public final class StringUtils { + + private StringUtils() { + } public static String convertToFio(String firstName, String middleName, String lastName) { String firstNameInitial = substring(firstName, 0, 1).toUpperCase(); diff --git a/backend/src/main/java/ru/micord/ervu/util/UrlUtils.java b/backend/src/main/java/ru/micord/ervu/util/UrlUtils.java new file mode 100644 index 00000000..93982d3f --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/util/UrlUtils.java @@ -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); + } +} diff --git a/config.md b/config.md index 7e14c875..7eac90bb 100644 --- a/config.md +++ b/config.md @@ -828,3 +828,17 @@ JBPM использует 3 корневых категории логирова - `ERVU_KAFKA_EXCERPT_REQUEST_TOPIC` - топик для записи запроса для получения выписки по журналу взаимодействия - `ERVU_KAFKA_EXCERPT_REPLY_TOPIC` - топик для чтения выписки по журналу взаимодействия. Содержит ссылку на S3 с файлом выписки - `DB.JOURNAL.EXCLUDED.STATUSES` - статусы файла, которые необходимо исключить при получении данных по журналу взаимодействия из базы данных приложения + + +#### Взаимодействие с Kafka audit +- `AUDIT_KAFKA_AUTHORIZATION_TOPIC` - топик для отправки аудита в журнал авторизации +- `AUDIT_KAFKA_ACTION_TOPIC` - топик для отправки аудита в журнал действий пользователя +- `AUDIT_KAFKA_FILE_UPLOAD_TOPIC` - топик для отправки аудита в журнал обмена файлами +- `AUDIT_KAFKA_FILE_DOWNLOAD_TOPIC` - топик для отправки аудита в журнал загрузки ЮЛ и ФЛ +- `AUDIT_KAFKA_BOOTSTRAP_SERVERS` - список пар хост:порт, использующихся для установки первоначального соединения с кластером Kafka +- `AUDIT_KAFKA_SECURITY_PROTOCOL` - протокол, используемый для взаимодействия с брокерами +- `AUDIT_KAFKA_DOC_LOGIN_MODULE` - имя класса для входа в систему для SASL-соединений в формате, используемом конфигурационными файлами JAAS +- `AUDIT_KAFKA_USERNAME` - пользователь для подключения к Kafka +- `AUDIT_KAFKA_PASSWORD` - пароль для подключения к Kafka +- `AUDIT_KAFKA_SASL_MECHANISM` - механизм SASL, используемый для клиентских подключений +- `AUDIT_KAFKA_ENABLED` - флажок для включения записи аудита в кафку diff --git a/config/micord.env b/config/micord.env index f097e315..9bb0bd6e 100644 --- a/config/micord.env +++ b/config/micord.env @@ -46,6 +46,18 @@ ESNSI_OKOPF_RETRY_DELAY_LOAD=3000 ESNSI_OKOPF_CONNECT_TIMEOUT=2 ESNSI_OKOPF_READ_TIMEOUT=4 +AUDIT_KAFKA_AUTHORIZATION_TOPIC=ervu.lkrp.auth.events +AUDIT_KAFKA_ACTION_TOPIC=ervu.lkrp.action.events +AUDIT_KAFKA_FILE_UPLOAD_TOPIC=ervu.lkrp.download.request +AUDIT_KAFKA_FILE_DOWNLOAD_TOPIC=ervu.lkrp.import.file +AUDIT_KAFKA_BOOTSTRAP_SERVERS= +AUDIT_KAFKA_SECURITY_PROTOCOL= +AUDIT_KAFKA_DOC_LOGIN_MODULE= +AUDIT_KAFKA_USERNAME= +AUDIT_KAFKA_PASSWORD= +AUDIT_KAFKA_SASL_MECHANISM= +AUDIT_KAFKA_ENABLED=false + ERVU_FILE_UPLOAD_MAX_FILE_SIZE=5242880 ERVU_FILE_UPLOAD_MAX_REQUEST_SIZE=6291456 ERVU_FILE_UPLOAD_FILE_SIZE_THRESHOLD=0 diff --git a/config/standalone/dev/standalone.xml b/config/standalone/dev/standalone.xml index b595148b..c96ccbb8 100644 --- a/config/standalone/dev/standalone.xml +++ b/config/standalone/dev/standalone.xml @@ -100,6 +100,17 @@ + + + + + + + + + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 535bde67..5b6686f2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2462,9 +2462,9 @@ } }, "cadesplugin_api": { - "version": "2.0.4-micord.1", - "resolved": "https://repo.micord.ru/repository/npm-all/cadesplugin_api/-/cadesplugin_api-2.0.4-micord.1.tgz", - "integrity": "sha512-FyGVi1VWIyJOW1zOOQN0IkTH/Z/8g7pNWH7A71nf0h21FCX9SacUfgRwID+gl+NlpYiT3m+yZGdlEJsiDeV8JA==" + "version": "2.1.1-micord.2", + "resolved": "https://repo.micord.ru/repository/npm-all/cadesplugin_api/-/cadesplugin_api-2.1.1-micord.2.tgz", + "integrity": "sha512-+j8RfbL7t2YMlSOC9Oa6+NoNLMYC3ZHkc9W6JQnV5+NBUqKLPAlLL1DF6llmW8coRdmgH6nZU8skvdk6M6qaBg==" }, "calendar-utils": { "version": "0.8.5", diff --git a/frontend/src/ts/ervu/LinkClickHandler.ts b/frontend/src/ts/ervu/LinkClickHandler.ts new file mode 100644 index 00000000..5bb408f9 --- /dev/null +++ b/frontend/src/ts/ervu/LinkClickHandler.ts @@ -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]; + } +} diff --git a/frontend/src/ts/ervu/component/enum/LinkEventTypeEnum.ts b/frontend/src/ts/ervu/component/enum/LinkEventTypeEnum.ts new file mode 100644 index 00000000..8d0c27ce --- /dev/null +++ b/frontend/src/ts/ervu/component/enum/LinkEventTypeEnum.ts @@ -0,0 +1,5 @@ +export enum LinkEventTypeEnum { + NAVIGATION_TO_SOURCE = "Переход на другие источники", + DOWNLOAD_TEMPLATE = "Скачивание шаблона", + DOWNLOAD_EXAMPLE = "Скачивание примера заполнения формы" +} \ No newline at end of file diff --git a/frontend/src/ts/ervu/component/grid/InMemoryStaticGrid.ts b/frontend/src/ts/ervu/component/grid/InMemoryStaticGrid.ts index 0d4b7db6..786135fc 100644 --- a/frontend/src/ts/ervu/component/grid/InMemoryStaticGrid.ts +++ b/frontend/src/ts/ervu/component/grid/InMemoryStaticGrid.ts @@ -4,11 +4,14 @@ import { GridRow, GridRowModelType, GridV2, - GridV2Column, Visible + GridV2Column, + Visible } from "@webbpm/base-package"; import {ChangeDetectionStrategy, Component} from "@angular/core"; import { - ColDef, FilterChangedEvent, + ColDef, + GridReadyEvent, + FilterChangedEvent, ICellRendererParams, ITooltipParams, ValueFormatterParams, @@ -17,6 +20,9 @@ import { import {StaticColumnInitializer} from "./StaticColumnInitializer"; import {InMemoryStaticGridRpcService} from "../../../generated/ru/micord/ervu/service/rpc/InMemoryStaticGridRpcService"; import {StaticGridColumn} from "../../../generated/ru/micord/ervu/property/grid/StaticGridColumn"; +import { FilterService } from "../../service/FilterService"; +import {AuditConstants, AuditService, FilterInfo} from "../../service/AuditService"; + @Component({ moduleId: module.id, @@ -30,6 +36,7 @@ export class InMemoryStaticGrid extends GridV2 { public columnFiltersChanged: Event = new Event(); private rpcService: InMemoryStaticGridRpcService; + private auditService: AuditService; getRowModelType(): string { return GridRowModelType.CLIENT_SIDE; @@ -37,6 +44,7 @@ export class InMemoryStaticGrid extends GridV2 { protected initGrid() { super.initGrid(); + this.auditService = this.injector.get(AuditService); this.rpcService = this.getScript(InMemoryStaticGridRpcService); if (this.rpcService) { this.rpcService.loadData().then(response => { @@ -47,6 +55,39 @@ export class InMemoryStaticGrid extends GridV2 { } } + onGridReady(event: GridReadyEvent) { + super.onGridReady(event); + this.addColumnFilterChangeListener(() => { + this.auditActiveFilters(); + }) + } + + private auditActiveFilters() { + const filterModel = this.gridApi.getFilterModel(); + if (!filterModel || Object.keys(filterModel).length === 0) { + return; + } + + const filterMap: Record = {}; + Object.entries(filterModel).forEach(([column, agFilter]) => { + const columnDef = this.gridApi.getColumnDef(column); + if (!columnDef) { + return; + } + + const data = FilterService.getFilterData(columnDef, agFilter); + if (!data) { + return; + } + + filterMap[agFilter.headerName] = data; + }); + + if (Object.keys(filterMap).length > 0) { + this.auditService.logActionAudit(AuditConstants.FILTER_EVENT, filterMap); + } + } + getColumns(): any[] { return this.getScriptsInChildren(GridV2Column) .map(columnV2 => columnV2.getScript(StaticGridColumn)); diff --git a/frontend/src/ts/ervu/service/AuditService.ts b/frontend/src/ts/ervu/service/AuditService.ts new file mode 100644 index 00000000..9e323de7 --- /dev/null +++ b/frontend/src/ts/ervu/service/AuditService.ts @@ -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, + 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; + fileName?: string +} + +export interface FilterInfo { + conditionOperator?: string; + conditions: FilterCondition[]; +} + +export interface FilterCondition { + filterValue: string; + filterType: string; +} \ No newline at end of file diff --git a/frontend/src/ts/ervu/service/FilterService.ts b/frontend/src/ts/ervu/service/FilterService.ts new file mode 100644 index 00000000..b9a9863d --- /dev/null +++ b/frontend/src/ts/ervu/service/FilterService.ts @@ -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; + } + +} \ No newline at end of file diff --git a/frontend/src/ts/modules/app/app.module.ts b/frontend/src/ts/modules/app/app.module.ts index f6c56163..1302534d 100644 --- a/frontend/src/ts/modules/app/app.module.ts +++ b/frontend/src/ts/modules/app/app.module.ts @@ -27,6 +27,7 @@ import {InMemoryStaticGrid} from "../../ervu/component/grid/InMemoryStaticGrid"; import {ErvuDownloadFileButton} from "../../ervu/component/button/ErvuDownloadFileButton"; import {AuthenticationService} from "../security/authentication.service"; import {HomeLandingComponent} from "./component/home-landing.component"; +import {AuditService} from "../../ervu/service/AuditService"; registerLocaleData(localeRu); export const DIRECTIVES = [ @@ -67,7 +68,7 @@ export function checkAuthentication(authService: AuthenticationService): () => P DIRECTIVES ], providers: [ - AuthenticationService, + AuthenticationService, AuditService, { provide: APP_INITIALIZER, useFactory: checkAuthentication, diff --git a/frontend/src/ts/modules/webbpm/component/webbpm.component.ts b/frontend/src/ts/modules/webbpm/component/webbpm.component.ts index fd7e1222..00b0e73b 100644 --- a/frontend/src/ts/modules/webbpm/component/webbpm.component.ts +++ b/frontend/src/ts/modules/webbpm/component/webbpm.component.ts @@ -8,6 +8,7 @@ import { Router } from "@angular/router"; import {ProgressIndicationService} from "@webbpm/base-package"; +import {AuditConstants, AuditService} from "../../../ervu/service/AuditService"; @Component({ moduleId: module.id, @@ -21,7 +22,8 @@ export class WebbpmComponent { constructor(private router: Router, private progressIndicationService: ProgressIndicationService, - private cd: ChangeDetectorRef) { + private cd: ChangeDetectorRef, + private auditService:AuditService) { router.events.subscribe((event: Event) => { if (event instanceof NavigationStart) { progressIndicationService.showProgressBar(); @@ -29,9 +31,15 @@ export class WebbpmComponent { this.cd.markForCheck(); } else if (event instanceof NavigationEnd - || event instanceof NavigationError - || event instanceof NavigationCancel) { + || event instanceof NavigationError + || event instanceof NavigationCancel) { progressIndicationService.hideProgressBar(); + + if (event instanceof NavigationEnd + && event.url != '/home' + && event.url != '/access-denied') { + this.auditService.logActionAudit(AuditConstants.OPEN_PAGE_EVENT); + } } }) } diff --git a/resources/src/main/resources/business-model/Информация об организации.page b/resources/src/main/resources/business-model/Информация об организации.page index ce712721..8c587663 100644 --- a/resources/src/main/resources/business-model/Информация об организации.page +++ b/resources/src/main/resources/business-model/Информация об организации.page @@ -180,6 +180,7 @@ 3ed7cd92-3c7a-4d6f-a22c-1f3c4031bb61 VB - левый true + false false @@ -1472,6 +1473,22 @@ + + + + + LinkClickHandler + ervu + +true +true + + + eventType + + "NAVIGATION_TO_SOURCE" + + @@ -1495,6 +1512,7 @@ 2f05ef7d-9092-4180-a361-8fecb3dd7542 Date time picker false + false false diff --git a/resources/src/main/resources/business-model/Личный кабинет юр лица.page b/resources/src/main/resources/business-model/Личный кабинет юр лица.page index 9547206b..ad3c27c3 100644 --- a/resources/src/main/resources/business-model/Личный кабинет юр лица.page +++ b/resources/src/main/resources/business-model/Личный кабинет юр лица.page @@ -176,7 +176,6 @@ 3e78f422-3db3-45b9-b531-f4aec5314dab Группа полей true - false false @@ -207,7 +206,6 @@ c9898352-a317-4117-bfcc-28b5c4d9a0d1 Горизонтальный контейнер true - false false @@ -312,7 +310,6 @@ 1be5e2cd-f42e-40c6-971c-e92f997a7139 Горизонтальный контейнер true - false false @@ -480,6 +477,7 @@ 22ffee0b-eb21-48c8-829c-da9f5dfa9459 Inner html_xls false + false false @@ -500,6 +498,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_TEMPLATE" + @@ -509,6 +523,7 @@ ddd8bddd-4b83-4caa-a29f-7d028aaebdc5 Inner html_csv false + false false @@ -530,6 +545,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_TEMPLATE" + @@ -604,6 +635,7 @@ c341ed51-c07a-4c5b-bc13-42acc337f639 Inner html_xls false + false false @@ -624,6 +656,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_EXAMPLE" + @@ -633,6 +681,7 @@ 74bf561e-8f86-4dd9-a6ed-eccd89ca0086 Inner html_csv false + false false @@ -653,6 +702,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_EXAMPLE" + @@ -1155,6 +1220,7 @@ 9b4bd780-47ec-4e2c-b859-33a4100a7539 Inner html_xls false + false false @@ -1170,6 +1236,22 @@ null + + + + + LinkClickHandler + ervu + +true +true + + + eventType + + "DOWNLOAD_EXAMPLE" + + @@ -1193,6 +1275,22 @@ null + + + + + LinkClickHandler + ervu + +true +true + + + eventType + + "DOWNLOAD_EXAMPLE" + + @@ -2273,7 +2371,6 @@ 9138d81a-f635-42f6-915c-b3c7be4e2160 Группа полей true - false false @@ -2572,6 +2669,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_TEMPLATE" + @@ -2601,6 +2714,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_TEMPLATE" + @@ -2695,6 +2824,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_EXAMPLE" + @@ -2724,6 +2869,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_EXAMPLE" + @@ -3206,6 +3367,22 @@ null + + + + + LinkClickHandler + ervu + +true +true + + + eventType + + "DOWNLOAD_EXAMPLE" + + @@ -3229,6 +3406,22 @@ null + + + + + LinkClickHandler + ervu + +true +true + + + eventType + + "DOWNLOAD_EXAMPLE" + + @@ -4309,7 +4502,6 @@ ae731885-3bdd-433d-a29c-37d5811585a7 Группа полей true - false false @@ -4608,6 +4800,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_TEMPLATE" + @@ -4637,6 +4845,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_TEMPLATE" + @@ -4724,6 +4948,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_EXAMPLE" + @@ -4753,6 +4993,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_EXAMPLE" + @@ -5185,6 +5441,22 @@ null + + + + + LinkClickHandler + ervu + +true +true + + + eventType + + "DOWNLOAD_EXAMPLE" + + @@ -5208,6 +5480,22 @@ null + + + + + LinkClickHandler + ervu + +true +true + + + eventType + + "DOWNLOAD_EXAMPLE" + + @@ -6341,7 +6629,6 @@ 7057bbcb-cff2-4e31-812d-ba7e043a4bcc Группа полей true - false false @@ -6640,6 +6927,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_TEMPLATE" + @@ -6669,6 +6972,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_TEMPLATE" + @@ -6756,6 +7075,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_EXAMPLE" + @@ -6785,6 +7120,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_EXAMPLE" + @@ -7268,6 +7619,22 @@ null + + + + + LinkClickHandler + ervu + +true +true + + + eventType + + "DOWNLOAD_EXAMPLE" + + @@ -7291,6 +7658,22 @@ null + + + + + LinkClickHandler + ervu + +true +true + + + eventType + + "DOWNLOAD_EXAMPLE" + + @@ -8371,7 +8754,6 @@ 991237d3-8cb9-48af-8501-030a3c8c6cfc Группа полей true - false false @@ -8670,6 +9052,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_TEMPLATE" + @@ -8699,6 +9097,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_TEMPLATE" + @@ -8786,6 +9200,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_EXAMPLE" + @@ -8815,6 +9245,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_EXAMPLE" + @@ -9298,6 +9744,22 @@ null + + + + + LinkClickHandler + ervu + +true +true + + + eventType + + "DOWNLOAD_EXAMPLE" + + @@ -9321,6 +9783,22 @@ null + + + + + LinkClickHandler + ervu + +true +true + + + eventType + + "DOWNLOAD_EXAMPLE" + + @@ -10401,7 +10879,6 @@ f4eafa61-1ea3-440a-806b-7b05ec416871 Диалог - сведения направлены true - false false @@ -10565,7 +11042,6 @@ 894c4e19-0bd3-4e13-9bd6-d40ab378ca21 Диалог - что-то пошло не так true - false false @@ -10759,7 +11235,6 @@ a2201fe8-183a-40c7-88ed-bbb07bf2c919 Группа полей true - false false @@ -10804,6 +11279,22 @@ false + + + LinkClickHandler + ervu + + true + true + + +eventType + + "NAVIGATION_TO_SOURCE" + + + + fe6407f3-4a81-4b9e-8861-49483cb708a4 @@ -10828,6 +11319,7 @@ 8152b078-7230-4fb8-994f-023809b95e44 Inner html_xls false + false false @@ -10848,6 +11340,22 @@ null + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "DOWNLOAD_EXAMPLE" + @@ -10991,6 +11499,22 @@ false + + + LinkClickHandler + ervu + + true + true + + +eventType + + "NAVIGATION_TO_SOURCE" + + + + e32ae1f5-5b14-45f1-abb6-f52c34b3b570