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 0000000..a43f2aa --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/config/AuditKafkaConfig.java @@ -0,0 +1,88 @@ +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:org.apache.kafka.common.security.scram.ScramLoginModule}") + 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 0000000..d30e732 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/constants/AuditConstants.java @@ -0,0 +1,43 @@ +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 = "FL"; + public static final String LOGOUT_EVENT_TYPE = "logout"; + public static final String LOGIN_EVENT_TYPE = "login"; + public static final String SUCCESS_STATUS = "success"; + public static final String FAILURE_STATUS = "failure"; + + private static final Map routeDescriptions = Map.of( + "/", "Главная", + "/mydata", "Мои данные", + "/subpoena", "Повестки", + "/restriction", "Временные меры" + ); + + private static final Map downloadTypes = Map.of( + "1", "Выписка из реестра воинского учета ФЛ", + "2", "Выписка из реестра повесток ФЛ" + ); + + public static String getRouteDescription(String route) { + return Optional.ofNullable(routeDescriptions.get(route)) + .orElseThrow(() -> new IllegalArgumentException("Invalid route :" + route)); + } + + public static String getDownloadType(String formatRegistry) { + return Optional.ofNullable(downloadTypes.get(formatRegistry)) + .orElseThrow( + () -> new IllegalArgumentException("Invalid formatRegistry :" + formatRegistry)); + } + + private AuditConstants() { + } + +} 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 0000000..c6e50a2 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/controller/AuditController.java @@ -0,0 +1,31 @@ +package ru.micord.ervu.audit.controller; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +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; + } + + @PostMapping("/action") + 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 0000000..6a2b122 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditActionEvent.java @@ -0,0 +1,53 @@ +package ru.micord.ervu.audit.model; + +/** + * @author Adel Kalimullin + */ +public class AuditActionEvent extends AuditEvent { + private String eventType; + private String description; + private String sourceUrl; + private String fileName; + + public AuditActionEvent( + String esiaPersonId, String eventTime, String eventType, + String description, String sourceUrl, String fileName) { + super(esiaPersonId, eventTime); + this.eventType = eventType; + this.description = description; + this.sourceUrl = sourceUrl; + this.fileName = fileName; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + 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; + } +} 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 0000000..05229a7 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditActionRequest.java @@ -0,0 +1,43 @@ +package ru.micord.ervu.audit.model; + +/** + * @author Adel Kalimullin + */ +public class AuditActionRequest { + private String route; + private String sourceUrl; + private String eventType; + private String fileName; + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + 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 getEventType() { + return eventType; + } + + public void setEventType(String eventType) { + this.eventType = eventType; + } +} 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 0000000..3885db5 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditAuthorizationEvent.java @@ -0,0 +1,73 @@ +package ru.micord.ervu.audit.model; + + +public class AuditAuthorizationEvent extends AuditEvent { + private String status; + private String eventType; + private String firstName; + private String lastName; + private String middleName; + private String snils; + + public AuditAuthorizationEvent( + String esiaPersonId, String eventTime, String firstName, + String lastName, String middleName, String snils, + String status, String eventType) { + super(esiaPersonId, eventTime); + this.status = status; + this.firstName = firstName; + this.lastName = lastName; + this.middleName = middleName; + this.snils = snils; + this.eventType = eventType; + } + + 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 getSnils() { + return snils; + } + + public void setSnils(String snils) { + this.snils = snils; + } + + 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; + } + +} \ 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 0000000..add1d73 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditDownloadEvent.java @@ -0,0 +1,65 @@ +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 esiaPersonId, String eventTime, String downloadType, + String fileName, String s3FileUrl, String fileSize, + String status) { + super(esiaPersonId, eventTime); + this.downloadType = downloadType; + this.fileName = fileName; + this.s3FileUrl = s3FileUrl; + this.fileSize = fileSize; + this.status = status; + } + + public String getS3FileUrl() { + return s3FileUrl; + } + + public void setS3FileUrl(String s3FileUrl) { + this.s3FileUrl = s3FileUrl; + } + + 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 getFileSize() { + return fileSize; + } + + public void setFileSize(String fileSize) { + this.fileSize = fileSize; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} \ No newline at end of file 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 0000000..1659a37 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/model/AuditEvent.java @@ -0,0 +1,38 @@ +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 esiaPersonId; + protected String eventTime; + + protected AuditEvent(String esiaPersonId, String eventTime) { + this.esiaPersonId = esiaPersonId; + this.eventTime = eventTime; + } + + public String getEsiaPersonId() { + return esiaPersonId; + } + + public void setEsiaPersonId(String esiaPersonId) { + this.esiaPersonId = esiaPersonId; + } + + public String getSubsystem() { + return subsystem; + } + + 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/service/AuditKafkaPublisher.java b/backend/src/main/java/ru/micord/ervu/audit/service/AuditKafkaPublisher.java new file mode 100644 index 0000000..bb3786f --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/service/AuditKafkaPublisher.java @@ -0,0 +1,8 @@ +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 0000000..e1b7f9b --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/service/AuditService.java @@ -0,0 +1,19 @@ +package ru.micord.ervu.audit.service; + +import javax.servlet.http.HttpServletRequest; + +import ru.micord.ervu.audit.model.AuditActionRequest; +import ru.micord.ervu.security.esia.model.PersonModel; + +/** + * @author Adel Kalimullin + */ +public interface AuditService { + void processActionEvent(HttpServletRequest request, AuditActionRequest auditActionRequest); + + void processAuthEvent(HttpServletRequest request, PersonModel personModel, String status, + String eventType); + + void processDownloadEvent(HttpServletRequest request, int fileSize, String fileName, + String formatRegistry, String status); +} 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 0000000..075c5f7 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditKafkaPublisher.java @@ -0,0 +1,42 @@ +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 0000000..8f794d2 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditService.java @@ -0,0 +1,104 @@ +package ru.micord.ervu.audit.service.impl; + +import javax.servlet.http.HttpServletRequest; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +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.AuditActionEvent; +import ru.micord.ervu.audit.model.AuditActionRequest; +import ru.micord.ervu.audit.model.AuditAuthorizationEvent; +import ru.micord.ervu.audit.model.AuditDownloadEvent; +import ru.micord.ervu.audit.service.AuditKafkaPublisher; +import ru.micord.ervu.audit.service.AuditService; +import ru.micord.ervu.security.esia.model.PersonModel; +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 auditPublisher; + private final JwtTokenService jwtTokenService; + private final ObjectMapper objectMapper; + @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; + + public BaseAuditService(AuditKafkaPublisher auditPublisher, JwtTokenService jwtTokenService, + ObjectMapper objectMapper) { + this.auditPublisher = auditPublisher; + this.jwtTokenService = jwtTokenService; + this.objectMapper = objectMapper; + } + + @Override + public void processActionEvent( + HttpServletRequest request, AuditActionRequest auditActionRequest) { + String userAccountId = jwtTokenService.getUserAccountId(request); + String description = AuditConstants.getRouteDescription(auditActionRequest.getRoute()); + + AuditActionEvent event = new AuditActionEvent( + userAccountId, + DateUtils.getClientTimeFromRequest(request), + auditActionRequest.getEventType(), + description, + auditActionRequest.getSourceUrl(), + auditActionRequest.getFileName() + ); + String message = convertToMessage(event); + auditPublisher.publishEvent(actionTopic, message); + } + + @Override + public void processAuthEvent(HttpServletRequest request, PersonModel personModel, String status, + String eventType) { + AuditAuthorizationEvent event = new AuditAuthorizationEvent( + personModel.getPrnsId(), + DateUtils.getClientTimeFromRequest(request), + personModel.getFirstName(), + personModel.getLastName(), + personModel.getMiddleName(), + personModel.getSnils(), + status, + eventType + ); + String message = convertToMessage(event); + auditPublisher.publishEvent(authorizationTopic, message); + } + + @Override + public void processDownloadEvent( + HttpServletRequest request, int fileSize, String fileName, String formatRegistry, + String status) { + String userAccountId = jwtTokenService.getUserAccountId(request); + + AuditDownloadEvent event = new AuditDownloadEvent( + userAccountId, + DateUtils.getClientTimeFromRequest(request), + AuditConstants.getDownloadType(formatRegistry), + fileName, + null, + String.valueOf(fileSize), + status + ); + String message = convertToMessage(event); + auditPublisher.publishEvent(fileDownloadTopic, message); + } + + private String convertToMessage(Object event) { + try { + return objectMapper.writeValueAsString(event); + } + catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/backend/src/main/java/ru/micord/ervu/controller/ExtractController.java b/backend/src/main/java/ru/micord/ervu/controller/ExtractController.java index f749eb6..4e7b0b0 100644 --- a/backend/src/main/java/ru/micord/ervu/controller/ExtractController.java +++ b/backend/src/main/java/ru/micord/ervu/controller/ExtractController.java @@ -3,6 +3,7 @@ package ru.micord.ervu.controller; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import org.apache.kafka.common.utils.Bytes; import org.springframework.beans.factory.annotation.Value; @@ -16,29 +17,33 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import rtl.pgs.ervu.proto.ExtractRegistry; import rtl.pgs.ervu.proto.ResponseData; +import ru.micord.ervu.audit.constants.AuditConstants; +import ru.micord.ervu.audit.service.AuditService; import ru.micord.ervu.dto.ExtractRequestDto; import ru.micord.ervu.kafka.service.ReplyingKafkaService; import ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil; +import javax.servlet.http.HttpServletRequest; + /** * @author gulnaz */ @RestController public class ExtractController { - private final ReplyingKafkaService replyingKafkaService; - + private final AuditService auditService; @Value("${ervu.kafka.registry.extract.request.topic}") private String registryExtractRequestTopic; @Value("${ervu.kafka.registry.extract.reply.topic}") private String registryExtractReplyTopic; - public ExtractController(ReplyingKafkaService replyingKafkaService) { + public ExtractController(ReplyingKafkaService replyingKafkaService, AuditService auditService) { this.replyingKafkaService = replyingKafkaService; + this.auditService = auditService; } @GetMapping(value = "/extract/{formatRegistry}") - public ResponseEntity getExtract(@PathVariable String formatRegistry) { + public ResponseEntity getExtract(HttpServletRequest servletRequest, @PathVariable String formatRegistry) { String ervuId = SecurityUtil.getErvuId(); if (ervuId == null) { @@ -48,18 +53,29 @@ public class ExtractController { byte[] reply = replyingKafkaService.sendMessageAndGetReply(registryExtractRequestTopic, registryExtractReplyTopic, request).get(); + int size = 0; + String fileName = null; try { ResponseData responseData = ResponseData.parseFrom(reply); ExtractRegistry extractRegistry = responseData.getDataRegistryInformation() - .getExtractRegistry(); - String encodedFilename = URLEncoder.encode(extractRegistry.getFileName(), StandardCharsets.UTF_8); - InputStreamResource resource = new InputStreamResource(extractRegistry.getFile().newInput()); + .getExtractRegistry(); + ByteString file = extractRegistry.getFile(); + size = file.size(); + fileName = extractRegistry.getFileName(); + String encodedFilename = URLEncoder.encode(fileName, StandardCharsets.UTF_8); + InputStreamResource resource = new InputStreamResource(file.newInput()); + auditService.processDownloadEvent(servletRequest, size, formatRegistry, fileName, + AuditConstants.SUCCESS_STATUS + ); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(resource); } catch (InvalidProtocolBufferException e) { + auditService.processDownloadEvent(servletRequest, size, formatRegistry, fileName, + AuditConstants.FAILURE_STATUS + ); throw new RuntimeException("Failed to parse data", e); } } diff --git a/backend/src/main/java/ru/micord/ervu/converter/SummonsResponseDataConverter.java b/backend/src/main/java/ru/micord/ervu/converter/SummonsResponseDataConverter.java index a523b24..28f73f1 100644 --- a/backend/src/main/java/ru/micord/ervu/converter/SummonsResponseDataConverter.java +++ b/backend/src/main/java/ru/micord/ervu/converter/SummonsResponseDataConverter.java @@ -8,15 +8,14 @@ import java.util.Optional; import ru.micord.ervu.dto.Restriction; import ru.micord.ervu.dto.SubpoenaResponseDto; import org.springframework.stereotype.Component; -import proto.ervu.rp.summons.MeasuresTemporary; import proto.ervu.rp.summons.ResponseDataAddress; import proto.ervu.rp.summons.SummonsInfo; import proto.ervu.rp.summons.SummonsResponseData; -import static ru.micord.ervu.util.DateUtil.convertToLocalDate; +import static ru.micord.ervu.util.DateUtils.convertToLocalDate; import static java.util.Objects.requireNonNull; -import static ru.micord.ervu.util.DateUtil.convertToString; -import static ru.micord.ervu.util.DateUtil.getDaysTill; +import static ru.micord.ervu.util.DateUtils.convertToString; +import static ru.micord.ervu.util.DateUtils.getDaysTill; /** * @author gulnaz diff --git a/backend/src/main/java/ru/micord/ervu/dto/SubpoenaResponseDto.java b/backend/src/main/java/ru/micord/ervu/dto/SubpoenaResponseDto.java index 5d8d442..80c3a38 100644 --- a/backend/src/main/java/ru/micord/ervu/dto/SubpoenaResponseDto.java +++ b/backend/src/main/java/ru/micord/ervu/dto/SubpoenaResponseDto.java @@ -4,8 +4,8 @@ import java.util.ArrayList; import java.util.List; import static org.springframework.util.StringUtils.hasText; -import static ru.micord.ervu.util.DateUtil.convertToLocalDate; -import static ru.micord.ervu.util.DateUtil.convertToString; +import static ru.micord.ervu.util.DateUtils.convertToLocalDate; +import static ru.micord.ervu.util.DateUtils.convertToString; /** * @author gulnaz 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 a3257ac..cb7b108 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 @@ -26,6 +26,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContext; +import ru.micord.ervu.audit.constants.AuditConstants; +import ru.micord.ervu.audit.service.AuditService; import ru.micord.ervu.kafka.model.Document; import ru.micord.ervu.kafka.model.Person; import ru.micord.ervu.kafka.model.Response; @@ -73,6 +75,9 @@ public class EsiaAuthService { @Autowired private PersonalDataService personalDataService; + @Autowired + private AuditService auditService; + @Autowired private SecurityHelper securityHelper; @@ -237,21 +242,40 @@ public class EsiaAuthService { LOGGER.info("Thread {}: SignSecret: {}ms RequestAccessToken: {}ms VerifySecret: {}ms", Thread.currentThread().getId(), signSecret, requestAccessToken, verifySecret); } + long requestPersonData = 0, requestIdERVU = 0; + PersonModel personModel = null; try { - Response ervuIdResponse = getErvuIdResponse(esiaAccessTokenStr); + long startTime = System.currentTimeMillis(); + personModel = personalDataService.getPersonModel(esiaAccessTokenStr); + requestPersonData = System.currentTimeMillis() - startTime; + startTime = System.currentTimeMillis(); + Response ervuIdResponse = getErvuIdResponse(personModel); + requestIdERVU = System.currentTimeMillis() - startTime; createTokenAndAddCookie(response, prnOid, ervuIdResponse.getErvuId(), expiresIn); + auditService.processAuthEvent( + request, personModel, AuditConstants.SUCCESS_STATUS, AuditConstants.LOGIN_EVENT_TYPE + ); return ResponseEntity.ok("Authentication successful"); } catch (Exception e) { createTokenAndAddCookie(response, prnOid, null, expiresIn); String messageId = getMessageId(e); String messageWithId = String.format("[%s] %s", messageId, e.getMessage()); + if (personModel != null) { + auditService.processAuthEvent( + request, personModel, AuditConstants.FAILURE_STATUS, AuditConstants.LOGIN_EVENT_TYPE + ); + } LOGGER.error(messageWithId, e); return new ResponseEntity<>( "Произошла ошибка " + messageId + ". Обратитесь к системному администратору", HttpStatus.FORBIDDEN ); } + finally { + LOGGER.info("Thread {}: RequestPersonData: {}ms RequestIdERVU: {}ms", + Thread.currentThread().getId(), requestPersonData, requestIdERVU); + } } public void getEsiaTokensByRefreshToken(HttpServletRequest request, HttpServletResponse response) { @@ -313,7 +337,8 @@ public class EsiaAuthService { Long expiresIn = tokenResponse.getExpires_in(); EsiaTokensStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn); EsiaTokensStore.addRefreshToken(prnOid, esiaNewRefreshTokenStr, expiresIn); - Response ervuIdResponse = getErvuIdResponse(esiaAccessTokenStr); + PersonModel personModel = personalDataService.getPersonModel(esiaAccessTokenStr); + Response ervuIdResponse = getErvuIdResponse(personModel); createTokenAndAddCookie(response, esiaAccessToken.getSbj_id(), ervuIdResponse.getErvuId(), expiresIn); } catch (Exception e) { @@ -354,9 +379,12 @@ public class EsiaAuthService { } public String logout(HttpServletRequest request, HttpServletResponse response) { + PersonModel personModel = null; try { - securityHelper.clearAccessCookies(response); String userId = jwtTokenService.getUserAccountId(request); + String accessToken = EsiaTokensStore.getAccessToken(userId); + personModel = personalDataService.getPersonModel(accessToken); + securityHelper.clearAccessCookies(response); EsiaTokensStore.removeAccessToken(userId); EsiaTokensStore.removeRefreshToken(userId); String logoutUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaLogoutUrl(); @@ -364,35 +392,34 @@ public class EsiaAuthService { URL url = new URL(logoutUrl); Map params = mapOf( "client_id", esiaConfig.getClientId(), - "redirect_url", redirectUrl); + "redirect_url", redirectUrl + ); + auditService.processAuthEvent( + request, personModel, AuditConstants.SUCCESS_STATUS, AuditConstants.LOGOUT_EVENT_TYPE + ); return buildUrl(url, params); } catch (Exception e) { + if (personModel != null){ + auditService.processAuthEvent( + request, personModel, AuditConstants.FAILURE_STATUS, AuditConstants.LOGOUT_EVENT_TYPE + ); + } throw new EsiaException(e); } } - public Response getErvuIdResponse(String accessToken) { - long requestPersonData = 0, requestIdERVU = 0; + public Response getErvuIdResponse(PersonModel personModel) { try { - long startTime = System.currentTimeMillis(); - PersonModel personModel = personalDataService.getPersonModel(accessToken); - requestPersonData = System.currentTimeMillis() - startTime; Person person = copyToPerson(personModel); - startTime = System.currentTimeMillis(); String kafkaResponse = replyingKafkaService.sendMessageAndGetReply(requestTopic, requestReplyTopic, objectMapper.writeValueAsString(person) ); - requestIdERVU = System.currentTimeMillis() - startTime; return objectMapper.readValue(kafkaResponse, Response.class); } catch (Exception e) { throw new EsiaException(e); } - finally { - LOGGER.info("Thread {}: RequestPersonData: {}ms RequestIdERVU: {}ms", - Thread.currentThread().getId(), requestPersonData, requestIdERVU); - } } private Person copyToPerson(PersonModel personModel) { 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 57% 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 232bae5..614df49 100644 --- a/backend/src/main/java/ru/micord/ervu/util/DateUtil.java +++ b/backend/src/main/java/ru/micord/ervu/util/DateUtils.java @@ -1,19 +1,39 @@ package ru.micord.ervu.util; import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import org.springframework.util.StringUtils; +import javax.servlet.http.HttpServletRequest; + /** * @author gulnaz */ -public final class DateUtil { +public final class DateUtils { public static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy"); - private DateUtil() {} + private static final DateTimeFormatter DATE_TIME_WITH_TIMEZONE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX"); + + private DateUtils() { + } + + public static String getClientTimeFromRequest(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) diff --git a/config.md b/config.md index 0920c80..da15691 100644 --- a/config.md +++ b/config.md @@ -787,6 +787,16 @@ JBPM использует 3 корневых категории логирова - `ERVU_KAFKA_REGISTRY_EXTRACT_REQUEST_TOPIC` - топик для отправки запроса на получение выписки из Реестра воинского учета - `ERVU_KAFKA_REGISTRY_EXTRACT_REPLY_TOPIC` - топик для получения выписки из Реестра воинского учета - `ERVU_KAFKA_EXTRACT_HEADER_CLASS` - класс для идентификации в заголовке запроса на получение выписки из Реестра повесток/Реестра воинского учета +- `AUDIT_KAFKA_AUTHORIZATION_TOPIC` - топик для отправки аудита в журнал авторизации +- `AUDIT_KAFKA_ACTION_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 4d9a355..f57888f 100644 --- a/config/micord.env +++ b/config/micord.env @@ -32,6 +32,16 @@ ERVU_KAFKA_REGISTRY_EXTRACT_REQUEST_TOPIC=ervu.extract.info.request ERVU_KAFKA_REGISTRY_EXTRACT_REPLY_TOPIC=ervu.extract.info.response ERVU_KAFKA_EXTRACT_HEADER_CLASS=request@urn://rostelekom.ru/ERVU-extractFromRegistryTR/1.0.3 ERVU_KAFKA_DOC_LOGIN_MODULE=org.apache.kafka.common.security.scram.ScramLoginModule +AUDIT_KAFKA_AUTHORIZATION_TOPIC=ervu.lkrp.auth.events +AUDIT_KAFKA_ACTION_TOPIC=ervu.lkrp.action.events +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 ESIA_TOKEN_CLEAR_CRON=0 0 */1 * * * COOKIE_PATH=/fl diff --git a/config/standalone/dev/standalone.xml b/config/standalone/dev/standalone.xml index 1428635..7134c4d 100644 --- a/config/standalone/dev/standalone.xml +++ b/config/standalone/dev/standalone.xml @@ -79,6 +79,16 @@ + + + + + + + + + + diff --git a/frontend/src/ts/ervu/LinkClickHandler.ts b/frontend/src/ts/ervu/LinkClickHandler.ts new file mode 100644 index 0000000..d8d3e24 --- /dev/null +++ b/frontend/src/ts/ervu/LinkClickHandler.ts @@ -0,0 +1,42 @@ +import {AnalyticalScope, Behavior, Control, NotNull} from "@webbpm/base-package"; +import {ElementRef} from "@angular/core"; +import {AuditService} from "./service/AuditService"; +import {LinkEventTypeEnum} from "./component/enum/LinkEventTypeEnum"; + +@AnalyticalScope(Control) +export class LinkClickHandler extends Behavior { + @NotNull() + public eventType: LinkEventTypeEnum; + private control: Control; + private auditService: AuditService; + private el: ElementRef; + + initialize() { + this.control = this.getScript(Control); + this.el = this.control.getEl(); + super.initialize(); + } + + 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') { + this.auditService.logActionAudit(this.eventType); + } + } +} \ No newline at end of file diff --git a/frontend/src/ts/ervu/component/button/ExtractLoadService.ts b/frontend/src/ts/ervu/component/button/ExtractLoadService.ts index a9b1feb..f4004ac 100644 --- a/frontend/src/ts/ervu/component/button/ExtractLoadService.ts +++ b/frontend/src/ts/ervu/component/button/ExtractLoadService.ts @@ -27,8 +27,12 @@ export class ExtractLoadService extends Behavior { this.button = this.getScript(AbstractButton); this.httpClient = this.injector.get(HttpClient); this.onClickFunction = () => { + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; this.httpClient.get('extract/' + this.formatRegistry, { responseType: 'blob', + headers: { + "Client-Time-Zone": timeZone, + }, observe: 'response' }).toPromise() .then((response) => { 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 0000000..8d0c27c --- /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/service/AuditService.ts b/frontend/src/ts/ervu/service/AuditService.ts new file mode 100644 index 0000000..a0a4263 --- /dev/null +++ b/frontend/src/ts/ervu/service/AuditService.ts @@ -0,0 +1,43 @@ +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, fileName?: string): void { + const currentRoute = this.router.url; + const sourceUrl = window.location.href; + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + const data: AuditAction = { + eventType: eventType, + sourceUrl: sourceUrl, + route: currentRoute, + fileName: fileName + }; + + this.httpClient.post("audit/action", data, { + headers: { + "Client-Time-Zone": timeZone, + } + }).toPromise(); + } +} + +export interface AuditAction { + eventType: string; + sourceUrl: string; + route: string; + fileName?: string; +} + +export class AuditConstants { + public static readonly OPEN_PAGE_EVENT = "Открытие страницы"; +} \ 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 15297d4..99cd7e7 100644 --- a/frontend/src/ts/modules/app/app.module.ts +++ b/frontend/src/ts/modules/app/app.module.ts @@ -24,6 +24,7 @@ import {LogOutComponent} from "./component/logout.component"; import {LoadForm} from "../../ervu/component/container/LoadForm"; import {InMemoryStaticGrid} from "../../ervu/component/grid/InMemoryStaticGrid"; import {AuthenticationService} from "../security/authentication.service"; +import {AuditService} from "../../ervu/service/AuditService"; import {HomeLandingComponent} from "./component/home-landing.component"; registerLocaleData(localeRu); @@ -64,7 +65,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 fd7e122..8f0e990 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/LK RP FL/mydata.page b/resources/src/main/resources/business-model/LK RP FL/mydata.page index 9f91d51..29f9a6d 100644 --- a/resources/src/main/resources/business-model/LK RP FL/mydata.page +++ b/resources/src/main/resources/business-model/LK RP FL/mydata.page @@ -1034,6 +1034,7 @@ 6f1b4ecf-7311-41ea-87f7-56f77ea66251 Гиперссылка - на Госуслугах false +false false @@ -1072,6 +1073,22 @@ + + + + LinkClickHandler + ervu + + true + true + + + eventType + + "NAVIGATION_TO_SOURCE" + + + diff --git a/resources/src/main/resources/business-model/LK RP FL/restriction.page b/resources/src/main/resources/business-model/LK RP FL/restriction.page index 87e799e..7736bf3 100644 --- a/resources/src/main/resources/business-model/LK RP FL/restriction.page +++ b/resources/src/main/resources/business-model/LK RP FL/restriction.page @@ -223,7 +223,6 @@ a674ce01-eadd-4297-b782-53e45e059310 HB - (сценарий) в связи с неявкой в военкомат без уважительной причины в течение 20 календарных true - false false @@ -325,7 +324,6 @@ 854640d1-9de8-4d5a-8f00-5d33cdf097f3 HB - (сценарий) в качестве ограничения, направленного на обеспечение явки в военкомат true - false false @@ -450,7 +448,6 @@ 0276d338-dbbc-406a-a356-96f7fd02a5a6 Таблица - временные меры true - false false @@ -623,7 +620,6 @@ 040fa808-ccbf-4844-b179-d63f54dc220a HB - запрет на выезд из России true -false false @@ -1683,7 +1679,6 @@ 52dc50c2-9c0c-44a8-94fb-ff190bdce295 VB - правый true - false false @@ -1818,7 +1813,6 @@ ffa21c64-1030-45c8-9901-db59f298fc11 Диалоговые окна (информационные) true - false false @@ -3002,7 +2996,6 @@ 6a606277-7950-41b1-a59f-7d1deb5cccd2 Уважительные причины неявки в военкомат true - false false @@ -3232,6 +3225,22 @@ false + + + LinkClickHandler + ervu + + true + true + + +eventType + + "NAVIGATION_TO_SOURCE" + + + + fd7e47b9-dce1-4d14-9f3a-580c79f59579 @@ -3344,7 +3353,6 @@ 07d89523-77ea-40f3-96d0-952c74b082b0 AC - для вызова диалогов true - false false diff --git a/resources/src/main/resources/business-model/LK RP FL/screen-form-fl.page b/resources/src/main/resources/business-model/LK RP FL/screen-form-fl.page index 099f115..87f8e5b 100644 --- a/resources/src/main/resources/business-model/LK RP FL/screen-form-fl.page +++ b/resources/src/main/resources/business-model/LK RP FL/screen-form-fl.page @@ -513,7 +513,6 @@ 4e247261-22c9-4b75-bc42-1214d6478193 HB заголовок - Повестки true - false false @@ -573,7 +572,6 @@ 3962166c-e12b-4866-b6fc-e53c9f233272 VB - 1.1.1.1 (на имя выписана повестка) сценарий true - false false @@ -640,7 +638,6 @@ 54ec4ded-0f1d-44b1-beea-1d10f2c65d6b HB - явитесь по повестке в военкомат true - false false @@ -783,7 +780,6 @@ 54755bcb-801b-450d-aa2e-046e2a405538 VB - 1.1.1.1 (на ? дату нет сформированных повесток) сценарий true - false false @@ -919,7 +915,6 @@ ae6bbd9c-b3f6-458a-9c19-07e06a4716da VB - 1.1.1.1 (в реестре нет информации о сформированных повестках) сценарий true - false false @@ -1728,7 +1723,6 @@ 8ed0924a-73dc-4732-8426-0a7217654309 HB - данные загружаются true - false false @@ -2404,7 +2398,6 @@ 935a9f9f-4d12-4813-b313-919627c0e642 VB - 1.1.2.2 (применены временные меры) сценарий true - false false @@ -2961,7 +2954,6 @@ 4b46dfb0-e372-48e7-96cb-6d488cbfbc55 VB - 1.1.2.2 (на "дата" нет действующих ограничений) сценарий true - false false @@ -3114,7 +3106,6 @@ 751006da-a8d6-45c6-8eee-0895c30f8035 VB - 1.1.2.2 (В реестре нет информации о примененных временных мерах) сценарий true - false false @@ -3177,7 +3168,6 @@ a4bb1eb3-c05a-4c96-acc4-885a99e4e28d HB - данные загружаются true - false false @@ -4049,7 +4039,6 @@ 304824d5-9f9f-4af9-9b08-6232f7536774 FS - 1.1.3 (Воинский учёт) true - false false @@ -7511,7 +7500,6 @@ cc277790-aa36-4798-9a37-97d2a381e5e8 Реестр воинского учета true - false false @@ -7709,6 +7697,7 @@ 61e47a39-6f30-4d52-92db-fce8f0df062c Текст(гссылка) - Закон о воинской обязанности и военной службе, ст. 8.2. false + false false @@ -7726,6 +7715,22 @@ false + + + LinkClickHandler + ervu + + true + true + + +eventType + + "NAVIGATION_TO_SOURCE" + + + + fd7e47b9-dce1-4d14-9f3a-580c79f59579 @@ -7837,7 +7842,6 @@ 018ebef5-5b82-464e-ae64-b8a38c11a244 Реестр повесток true - false false @@ -8155,6 +8159,22 @@ false + + + LinkClickHandler + ervu + + true + true + + +eventType + + "NAVIGATION_TO_SOURCE" + + + + fd7e47b9-dce1-4d14-9f3a-580c79f59579 @@ -8273,7 +8293,6 @@ e40b27e3-8df0-4e8c-adeb-6e24de6f18d2 Временные меры true - false false @@ -8626,6 +8645,22 @@ false + + + LinkClickHandler + ervu + + true + true + + +eventType + + "NAVIGATION_TO_SOURCE" + + + + fd7e47b9-dce1-4d14-9f3a-580c79f59579 @@ -8751,7 +8786,6 @@ dce4d66d-4b4f-4883-a54c-e44e96a06a53 Уважительные причины неявки в военкомат true - false false @@ -8981,6 +9015,22 @@ false + + + LinkClickHandler + ervu + + true + true + + +eventType + + "NAVIGATION_TO_SOURCE" + + + + fd7e47b9-dce1-4d14-9f3a-580c79f59579 @@ -9092,7 +9142,6 @@ 5777f90d-5b51-4c1b-b0cc-4ace05faa8f3 Диалог - Отрицательное количество дней до явки в военкомат true - false false @@ -9406,7 +9455,6 @@ 2452bb18-95f0-4bfc-a40d-52b59e3c5cd6 AC - на вызов диалоговых окон true - false false @@ -10395,7 +10443,6 @@ 5443b358-365f-4bfd-bb4c-f2854dd96da4 Диалоговые окна отправки на электронную почту true - false false @@ -10558,6 +10605,22 @@ "https://www.gosuslugi.ru" + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "NAVIGATION_TO_SOURCE" + @@ -10692,7 +10755,6 @@ f25ca4de-d55b-4a06-bb4c-5eedd65d4095 Диалог - Отправка выписки на электронную почту - к удалению true - false false @@ -10869,6 +10931,22 @@ "https://www.gosuslugi.ru" + + + + + +LinkClickHandler +ervu + + true + true + + + eventType + + "NAVIGATION_TO_SOURCE" + @@ -11088,7 +11166,6 @@ 673d4fd7-0527-41df-abcd-2ab522bb161c Диалог - Выписка отправлена - к удалению true - false false @@ -11268,7 +11345,6 @@ d9788c7d-ebeb-413a-969a-a33f167a2bd9 Диалог - не удалось отправить выписку true - false false @@ -11461,7 +11537,6 @@ 8aff628b-a0a4-4296-854d-8a03dbef5937 AC - на вызов диалоговых окон true - false false diff --git a/resources/src/main/resources/business-model/LK RP FL/subpoena.page b/resources/src/main/resources/business-model/LK RP FL/subpoena.page index af533a0..56d26d9 100644 --- a/resources/src/main/resources/business-model/LK RP FL/subpoena.page +++ b/resources/src/main/resources/business-model/LK RP FL/subpoena.page @@ -195,7 +195,6 @@ 2d0083b3-0994-4131-9cfc-de84cab46dab HB - явитесь по повестке в военкомат. Ели не придёте в срок, к Вам применят временные меры true - false false @@ -1101,6 +1100,22 @@ false + + + LinkClickHandler + ervu + + true + true + + + eventType + + "NAVIGATION_TO_SOURCE" + + + + @@ -2412,7 +2427,6 @@ c9c74406-f8f4-4a9a-8a62-57fe026f5fa5 FS - (если вы не можете явиться по уважительной причине) true - false false @@ -2505,7 +2519,6 @@ 2f73cbfa-c121-49b0-994a-1bc17d654e1a FS - (дней до применения всех временных мер) true - false false @@ -2838,6 +2851,22 @@ false + + + LinkClickHandler + ervu + + true + true + + +eventType + + "NAVIGATION_TO_SOURCE" + + + + fd7e47b9-dce1-4d14-9f3a-580c79f59579 @@ -3249,6 +3278,22 @@ false + + + LinkClickHandler + ervu + + true + true + + +eventType + + "NAVIGATION_TO_SOURCE" + + + + fd7e47b9-dce1-4d14-9f3a-580c79f59579 @@ -3360,7 +3405,6 @@ 43f57db1-0f42-4d3c-8166-45e7691b124a Временные меры true - false false @@ -3684,6 +3728,22 @@ false + + + LinkClickHandler + ervu + + true + true + + +eventType + + "NAVIGATION_TO_SOURCE" + + + + fd7e47b9-dce1-4d14-9f3a-580c79f59579 @@ -3802,7 +3862,6 @@ 76ac701a-62f6-493e-8ab3-9391e956c3f1 Уважительные причины неявки в военкомат true - false false @@ -4032,6 +4091,22 @@ false + + + LinkClickHandler + ervu + + true + true + + +eventType + + "NAVIGATION_TO_SOURCE" + + + + fd7e47b9-dce1-4d14-9f3a-580c79f59579