/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Copyright (c) 2014, MPL CodeInside http://codeinside.ru */ package ru.codeinside.gses.activiti.forms.values; import com.google.common.base.Objects; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import org.activiti.engine.impl.context.Context; import org.activiti.engine.impl.db.DbSqlSession; import org.activiti.engine.impl.form.StartFormVariableScope; import org.activiti.engine.impl.interceptor.CommandContext; import org.activiti.engine.impl.persistence.entity.AttachmentEntity; import org.activiti.engine.impl.persistence.entity.ByteArrayEntity; import org.activiti.engine.impl.persistence.entity.ExecutionEntity; import org.activiti.engine.impl.persistence.entity.HistoricVariableUpdateEntity; import org.activiti.engine.impl.persistence.entity.TaskEntity; import org.activiti.engine.impl.variable.EntityManagerSession; import org.activiti.engine.repository.ProcessDefinition; import org.activiti.engine.task.Task; import org.glassfish.osgicdi.ServiceUnavailableException; import ru.codeinside.adm.database.AuditId; import ru.codeinside.adm.database.AuditSnapshot; import ru.codeinside.adm.database.AuditValue; import ru.codeinside.adm.database.FieldBuffer; import ru.codeinside.adm.database.FormBuffer; import ru.codeinside.gses.activiti.VariableToBytes; import ru.codeinside.gses.activiti.forms.api.definitions.BlockNode; import ru.codeinside.gses.activiti.forms.api.definitions.EnclosureNode; import ru.codeinside.gses.activiti.forms.api.definitions.PropertyNode; import ru.codeinside.gses.activiti.forms.api.definitions.PropertyTree; import ru.codeinside.gses.activiti.forms.api.definitions.ToggleNode; import ru.codeinside.gses.activiti.forms.api.values.FormValue; import ru.codeinside.gses.activiti.forms.api.values.PropertyValue; import ru.codeinside.gses.activiti.history.HistoricDbSqlSession; import ru.codeinside.gses.cert.NameParts; import ru.codeinside.gses.cert.X509; import ru.codeinside.gses.service.CryptoProviderAware; import ru.codeinside.gses.service.Some; import ru.codeinside.gws.api.CryptoProvider; import javax.persistence.EntityManager; import java.io.ByteArrayInputStream; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; // TODO: аудит // TODO: как строится archiveValues для истории ?! // TODO: переключатели тоже должны клонироваться во втором проходе, когда собрано основное дерево! final public class PropertyValuesBuilder { /** * Проверять ли целостность, сверяя значения аудита с текущими значением переменных. */ private static final boolean CHECK_INTEGRITY = true; /** * Проверять ли целостность через equals при true либо через криптопровайдера при false. */ private static final boolean CHECK_EQUALITY = true; final TaskEntity task; final HistoricDbSqlSession session; private final Logger logger = Logger.getLogger(getClass().getName()); private final String taskId; private final ExecutionEntity execution; private final Map<String, String> archiveValues; private final boolean archiveMode; private final FormBuffer formBuffer; private final Map<String, ValueBuilder> valuesMap = new HashMap<String, ValueBuilder>(); private CryptoProvider cryptoProvider; public PropertyValuesBuilder(String taskId, ExecutionEntity execution, Map<String, String> archiveValues) { this.taskId = taskId; this.execution = execution; this.archiveValues = archiveValues; archiveMode = execution == null && archiveValues != null; if (this.taskId != null) { CommandContext commandContext = Context.getCommandContext(); EntityManager em = commandContext.getSession(EntityManagerSession.class).getEntityManager(); List<FormBuffer> formBuffers = em.createQuery("select fb from FormBuffer fb where fb.taskId = :taskId", FormBuffer.class) .setParameter("taskId", this.taskId).getResultList(); if (!formBuffers.isEmpty()) { formBuffer = formBuffers.get(0); } else { formBuffer = null; } task = commandContext.getTaskManager().findTaskById(taskId); session = (HistoricDbSqlSession) commandContext.getSession(DbSqlSession.class); if (Context.getProcessEngineConfiguration() instanceof CryptoProviderAware) { cryptoProvider = ((CryptoProviderAware) Context.getProcessEngineConfiguration()).getCryptoProviderProxy(); } } else { formBuffer = null; task = null; session = null; } } public FormValue build(PropertyTree definition, Task task, ProcessDefinition processDefinition) { ValueBuilder valueBuilder = new ValueBuilder(); valueBuilder.valueBuilders = new ArrayList<ValueBuilder>(definition.getNodes().length); for (PropertyNode node : definition.getNodes()) { build(valueBuilder, node, ""); } return valueBuilder.toValues(task, processDefinition, definition, archiveMode); } public List<PropertyValue<?>> block(BlockNode definition, String path) { ValueBuilder valueBuilder = new ValueBuilder(); valueBuilder.id = definition.getId(); valueBuilder.valueBuilders = new ArrayList<ValueBuilder>(definition.getNodes().length); valuesMap.put(valueBuilder.id, valueBuilder); for (PropertyNode node : definition.getNodes()) { build(valueBuilder, node, path); } return valueBuilder.toCollection(); } void build(ValueBuilder valueBuilderCollection, PropertyNode node, String suffix) { if (!node.isFieldReadable()) { return; } if (node instanceof ToggleNode) { return; } // 1. формируется id // 2. меняется variableName String id = node.getId() + suffix; if (valuesMap.containsKey(id)) { logger.info("duplicate value " + id); return; } // TODO: имя переменной должно быть согласовано с аудитом! String variableName = node.getVariableName() == null ? null : node.getVariableName() + suffix; Object modelValue = null; boolean useBuffer = false; Some<String> userVariable = Some.empty(); if (archiveMode) { Object archiveValue = null; if (variableName != null || node.getVariableExpression() == null) { String varName = variableName != null ? variableName : id; if (archiveValues.containsKey(varName)) { archiveValue = archiveValues.get(varName); userVariable = Some.of(varName); } else if (node.getDefaultExpression() != null) { HistoryScope tracker = new HistoryScope(archiveValues); archiveValue = node.getDefaultExpression().getValue(tracker); userVariable = tracker.getUsedVariable(); } } else { HistoryScope tracker = new HistoryScope(archiveValues); archiveValue = node.getVariableExpression().getValue(tracker); userVariable = tracker.getUsedVariable(); } modelValue = node.getVariableType().convertFormValueToModelValue(archiveValue, node.getPattern(), node.getParams()); } else { if (formBuffer != null) { for (FieldBuffer fieldBuffer : formBuffer.getFields()) { if (id.equals(fieldBuffer.getFieldId())) { useBuffer = true; modelValue = node.getVariableType().convertBufferToModelValue(fieldBuffer); break; } } } if (!useBuffer) { Object formValue = null; boolean readable = node.isVarReadable(); if (!readable) { logger.info("skip variable for " + id + " by #read=false"); } if (readable && execution != null) { if (variableName != null || node.getVariableExpression() == null) { String varName = variableName != null ? variableName : id; if (execution.hasVariable(varName)) { formValue = execution.getVariable(varName); userVariable = Some.of(varName); } else if (node.getDefaultExpression() != null) { VariableTracker tracker = new VariableTracker(execution); formValue = node.getDefaultExpression().getValue(tracker); userVariable = tracker.getUsedVariable(); } } else { VariableTracker tracker = new VariableTracker(execution); formValue = node.getVariableExpression().getValue(tracker); userVariable = tracker.getUsedVariable(); } } else { if (node.getDefaultExpression() != null) { formValue = node.getDefaultExpression().getValue(StartFormVariableScope.getSharedInstance()); } } modelValue = node.getVariableType().convertFormValueToModelValue(formValue, node.getPattern(), node.getParams()); } } ValueBuilder valueBuilder = new ValueBuilder(); valuesMap.put(id, valueBuilder); valueBuilder.id = id; valueBuilder.value = modelValue; valueBuilder.node = node; valueBuilderCollection.valueBuilders.add(valueBuilder); //TODO для истории значения аудита пока не вытаскиваются if (userVariable.isPresent() && userVariable.get() != null && !archiveMode) { String varName = userVariable.get(); long executionId = Long.parseLong(execution.getId()); AuditValue auditValue = getAuditSnapshotValue(executionId, varName); if (auditValue != null) { AuditBuilder info = new AuditBuilder(); info.login = auditValue.getLogin(); info.verified = verifyValue(auditValue, execution, varName); if (auditValue.getSign() != null || auditValue.getCert() != null) { NameParts nameParts = X509.getSubjectParts(auditValue.getCert()); info.ownerName = nameParts.getCommonName(); info.organizationName = nameParts.getOrganization(); } valueBuilder.auditBuilder = info; } } if (node instanceof BlockNode) { if (modelValue != null) { long n = (Long) modelValue; BlockNode block = (BlockNode) node; long items = n < block.getMinimum() ? block.getMinimum() : (n <= block.getMaximum() ? n : block.getMaximum()); // коррекция значения по описателю блока if (n != items) { valueBuilder.value = items; if (valueBuilder.auditBuilder != null) { valueBuilder.auditBuilder.verified = false; } } valueBuilder.valueBuilders = new ArrayList<ValueBuilder>((int) items); for (long i = 1; i <= items; i++) { ValueBuilder clone = new ValueBuilder(); clone.valueBuilders = new ArrayList<ValueBuilder>(block.getNodes().length); valueBuilder.valueBuilders.add(clone); for (PropertyNode child : block.getNodes()) { build(clone, child, suffix + '_' + i); } } } } else if (node instanceof EnclosureNode) { String attachments = (String) modelValue; EnclosureNode enclosureNode = (EnclosureNode) node; Iterable<String> varNamesForRefToAttachment = Splitter.on(';').omitEmptyStrings().trimResults().split(attachments); for (String varName : varNamesForRefToAttachment) { PropertyNode child = enclosureNode.createEnclosure(varName); build(valueBuilderCollection, child, suffix); } } } //TODO: упростить этот ахтунг private boolean verifyValue(final AuditValue auditValue, final ExecutionEntity execution, final String varName) { if (cryptoProvider == null) { return false; } try { cryptoProvider.toString(); } catch (ServiceUnavailableException e) { logger.info("Отсутсвует поддержка ГОСТ"); return false; } boolean verified; final byte[] sign = auditValue.getSign(); final byte[] cert = auditValue.getCert(); if (sign == null && cert == null) { // ok verified = false; } else if (sign == null || cert == null) { logger.log(Level.WARNING, "Нарушение целостности: удалены элементы подписи для переменной " + execution.getId() + ":" + varName); verified = false; } else { try { X509Certificate certificate = X509.decode(cert); boolean attachment = auditValue.isAttachment(); Object varValue = execution.getVariable(varName); verified = verifyValue(sign, certificate, attachment, varValue); if (!verified) { String type = varValue == null ? "null" : varValue.getClass().getName(); logger.log(Level.WARNING, "Нарушение целостности: проверка подписи провалена для переменной " + execution.getId() + ":" + varName + " " + type); } if (verified && CHECK_INTEGRITY) { HistoricVariableUpdateEntity entity = session.selectById(HistoricVariableUpdateEntity.class, Long.toString(auditValue.getHid())); if (entity == null) { logger.log(Level.WARNING, "Нарушение целостности: не найдена запись истории " + auditValue.getHid() + " для переменной " + execution.getId() + ":" + varName); verified = false; } else { Object value = entity.getValue(); if (CHECK_EQUALITY) { // либо через проверку эквивалентности значения if (varValue instanceof byte[]) { verified = value instanceof byte[] && Arrays.equals((byte[]) varValue, (byte[]) value); } else { verified = Objects.equal(varValue, value); } if (!verified) { logger.log(Level.WARNING, "Нарушение целостности: запись в истории " + auditValue.getHid() + " отличается для переменной " + execution.getId() + ":" + varName); } } else { // либо через проверку данных криптопровайдером. Это надежнее но и дольше. verified = verifyValue(sign, certificate, attachment, value); if (!verified) { logger.log(Level.WARNING, "Нарушение целостности: проверка подписи в истории провалена для переменной " + execution.getId() + ":" + varName); } } } } } catch (Exception e) { logger.log(Level.WARNING, "Ошибка при проверке подписи", e); verified = false; } } return verified; } private boolean verifyValue(byte[] sign, X509Certificate certificate, boolean attachment, Object value) { byte[] bytes; if (attachment) { bytes = getAttachmentContent("" + value); } else { bytes = VariableToBytes.toBytes(value); } return cryptoProvider.verifySignature(certificate, new ByteArrayInputStream(bytes), sign); } private byte[] getAttachmentContent(final String attachmentId) { // на самом деле attachmentId имеет формат, из которого надо взять id activiti attachment List<String> parts = ImmutableList.copyOf(Splitter.on(":").split(attachmentId).iterator()); AttachmentEntity attachment = session.selectById(AttachmentEntity.class, parts.get(0)); String contentId = attachment.getContentId(); if (contentId == null) { return new byte[0]; } ByteArrayEntity byteArray = session.selectById(ByteArrayEntity.class, contentId); return byteArray.getBytes(); } private AuditValue getAuditSnapshotValue(long executionId, String varName) { EntityManager em = Context.getCommandContext().getSession(EntityManagerSession.class).getEntityManager(); AuditSnapshot snapshot = em.find(AuditSnapshot.class, new AuditId(executionId, varName)); if (snapshot != null) { return snapshot.getValue(); } logger.log(Level.FINE, "no history for " + executionId + ":" + varName); return null; } }