/** * Copyright (c) 2016 Evolveum * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.evolveum.midpoint.model.impl.controller; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.xml.datatype.XMLGregorianCalendar; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.evolveum.midpoint.audit.api.AuditEventRecord; import com.evolveum.midpoint.audit.api.AuditEventStage; import com.evolveum.midpoint.audit.api.AuditService; import com.evolveum.midpoint.model.api.ModelAuditService; import com.evolveum.midpoint.model.api.ModelAuthorizationAction; import com.evolveum.midpoint.model.impl.ModelObjectResolver; import com.evolveum.midpoint.prism.PrismObject; import com.evolveum.midpoint.prism.delta.ObjectDelta; import com.evolveum.midpoint.prism.xml.XmlTypeConverter; import com.evolveum.midpoint.schema.ObjectDeltaOperation; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.result.OperationResultStatus; import com.evolveum.midpoint.security.api.SecurityEnforcer; import com.evolveum.midpoint.task.api.Task; import com.evolveum.midpoint.util.DebugUtil; import com.evolveum.midpoint.util.exception.ObjectNotFoundException; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.exception.SecurityViolationException; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.midpoint.xml.ns._public.common.common_3.AuthorizationPhaseType; import com.evolveum.midpoint.xml.ns._public.common.common_3.CleanupPolicyType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType; /** * @author semancik * */ @Component public class AuditController implements ModelAuditService { private static final Trace LOGGER = TraceManager.getTrace(AuditController.class); @Autowired(required=true) private AuditService auditService; @Autowired(required=true) private ModelObjectResolver objectResolver; @Autowired(required = true) private SecurityEnforcer securityEnforcer; /* (non-Javadoc) * @see com.evolveum.midpoint.audit.api.AuditService#audit(com.evolveum.midpoint.audit.api.AuditEventRecord, com.evolveum.midpoint.task.api.Task) */ @Override public void audit(AuditEventRecord record, Task task, OperationResult result) throws SecurityViolationException, SchemaException { authorize(ModelAuthorizationAction.AUDIT_RECORD, result); auditService.audit(record, task); } @Override public List<AuditEventRecord> listRecords(String query, Map<String, Object> params, OperationResult result) throws SecurityViolationException, SchemaException { authorize(ModelAuthorizationAction.AUDIT_READ, result); return auditService.listRecords(query, params); } /* (non-Javadoc) * @see com.evolveum.midpoint.audit.api.AuditService#countObjects(java.lang.String, java.util.Map) */ @Override public long countObjects(String query, Map<String, Object> params, OperationResult result) throws SecurityViolationException, SchemaException { authorize(ModelAuthorizationAction.AUDIT_READ, result); return auditService.countObjects(query, params); } @Override public void cleanupAudit(CleanupPolicyType policy, OperationResult parentResult) throws SecurityViolationException, SchemaException { authorize(ModelAuthorizationAction.AUDIT_MANAGE, parentResult); auditService.cleanupAudit(policy, parentResult); } /* (non-Javadoc) * @see com.evolveum.midpoint.audit.api.AuditService#supportsRetrieval() */ @Override public boolean supportsRetrieval() { return auditService.supportsRetrieval(); } @Override public <O extends ObjectType> PrismObject<O> reconstructObject(Class<O> type, String oid, String eventIdentifier, Task task, OperationResult result) throws ObjectNotFoundException, SchemaException { // TODO: authorizations O currentObjectType = objectResolver.getObjectSimple(type, oid, null, task, result); PrismObject<O> currentObject = (PrismObject<O>) currentObjectType.asPrismObject(); List<AuditEventRecord> changeTrail = getChangeTrail(oid, eventIdentifier); LOGGER.trace("Found change trail for {} containing {} events", oid, changeTrail.size()); LOGGER.info("TRAIL:\n{}", DebugUtil.debugDump(changeTrail, 1)); PrismObject<O> objectFromLastEvent = getObjectFromLastEvent(currentObject, changeTrail, eventIdentifier); if (objectFromLastEvent != null) { return objectFromLastEvent; } PrismObject<O> reconstructedObject = rollBackTime(currentObject.clone(), changeTrail); return reconstructedObject; } private List<AuditEventRecord> getChangeTrail(String targetOid, String finalEventIdentifier) throws ObjectNotFoundException { AuditEventRecord finalEvent = findEvent(finalEventIdentifier); if (finalEvent == null) { throw new ObjectNotFoundException("Audit event ID "+finalEventIdentifier+" was not found"); } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Final event:\n{}", finalEvent.debugDump(1)); } List<AuditEventRecord> changeTrail = getChangeTrail(targetOid, XmlTypeConverter.createXMLGregorianCalendar(finalEvent.getTimestamp())); // The search may have returned more events that we want to, e.g. if two // events happened in the same millisecond. Iterator<AuditEventRecord> iterator = changeTrail.iterator(); boolean foundFinalEvent = false; while (iterator.hasNext()) { AuditEventRecord event = iterator.next(); if (foundFinalEvent) { iterator.remove(); } else if (finalEventIdentifier.equals(event.getEventIdentifier())) { foundFinalEvent = true; } } return changeTrail; } private List<AuditEventRecord> getChangeTrail(String targetOid, XMLGregorianCalendar from) { Map<String,Object> params = new HashMap<>(); params.put("from", from); params.put("targetOid", targetOid); params.put("stage", AuditEventStage.EXECUTION); return auditService.listRecords( "from RAuditEventRecord as aer where (aer.timestamp >= :from) and (aer.targetOid = :targetOid) and (aer.eventStage = :stage) order by aer.timestamp desc", params); } private AuditEventRecord findEvent(String eventIdentifier) { Map<String,Object> params = new HashMap<>(); params.put("eventIdentifier", eventIdentifier); List<AuditEventRecord> listRecords = auditService.listRecords("from RAuditEventRecord as aer where (aer.eventIdentifier = :eventIdentifier)", params); if (listRecords == null || listRecords.isEmpty()) { return null; } if (listRecords.size() > 1) { LOGGER.error("Found "+listRecords.size()+" audit records for event ID "+eventIdentifier+" (expecting just one)"); } return listRecords.get(0); } private <O extends ObjectType> PrismObject<O> getObjectFromLastEvent(PrismObject<O> object, List<AuditEventRecord> changeTrail, String eventIdentifier) { if (changeTrail.isEmpty()) { return object; } AuditEventRecord lastEvent = changeTrail.remove(changeTrail.size() - 1); if (!eventIdentifier.equals(lastEvent.getEventIdentifier())) { throw new IllegalStateException("Wrong last event identifier, expected " + eventIdentifier+" but was " + lastEvent.getEventIdentifier()); } Collection<ObjectDeltaOperation<? extends ObjectType>> lastEventDeltasOperations = lastEvent.getDeltas(); for (ObjectDeltaOperation<? extends ObjectType> lastEventDeltasOperation: lastEventDeltasOperations) { ObjectDelta<O> objectDelta = (ObjectDelta<O>) lastEventDeltasOperation.getObjectDelta(); if (!isApplicable(lastEventDeltasOperation, object, lastEvent)) { continue; } if (objectDelta.isAdd()) { // We are lucky. This is object add, so we have complete object there. No need to roll back // the operations. PrismObject<O> objectToAdd = objectDelta.getObjectToAdd(); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Taking object from add delta in last event {}:\n{}", lastEvent.getEventIdentifier(), objectToAdd.debugDump(1)); } return objectToAdd; } } return null; } private <O extends ObjectType> PrismObject<O> rollBackTime(PrismObject<O> object, List<AuditEventRecord> changeTrail) throws SchemaException { for (AuditEventRecord event: changeTrail) { if (LOGGER.isTraceEnabled()) { LOGGER.trace("Applying event {} ({})", event.getEventIdentifier(), XmlTypeConverter.createXMLGregorianCalendar(event.getTimestamp())); } Collection<ObjectDeltaOperation<? extends ObjectType>> deltaOperations = event.getDeltas(); if (deltaOperations != null) { for (ObjectDeltaOperation<? extends ObjectType> deltaOperation: deltaOperations) { ObjectDelta<O> objectDelta = (ObjectDelta<O>) deltaOperation.getObjectDelta(); if (!isApplicable(deltaOperation, object, event)) { continue; } if (objectDelta.isDelete()) { throw new SchemaException("Delete delta found in the audit trail. Object history cannot be reconstructed."); } if (objectDelta.isAdd()) { throw new SchemaException("Add delta found in the audit trail. Object history cannot be reconstructed."); } ObjectDelta<O> reverseDelta = objectDelta.createReverseDelta(); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Applying delta (reverse):\n{}", reverseDelta.debugDump(1)); } reverseDelta.applyTo(object); } } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Object after application of event {} ({}):\n{}", event.getEventIdentifier(), XmlTypeConverter.createXMLGregorianCalendar(event.getTimestamp()), object.debugDump(1)); } } return object; } private <O extends ObjectType> boolean isApplicable(ObjectDeltaOperation<? extends ObjectType> lastEventDeltasOperation, PrismObject<O> object, AuditEventRecord lastEvent) { OperationResult executionResult = lastEventDeltasOperation.getExecutionResult(); ObjectDelta<O> objectDelta = (ObjectDelta<O>) lastEventDeltasOperation.getObjectDelta(); if (executionResult.getStatus() == OperationResultStatus.FATAL_ERROR) { LOGGER.trace("Skipping delta {} in event {} because it is {}", objectDelta, lastEvent.getEventIdentifier(), executionResult.getStatus()); return false; } if (!object.getOid().equals(objectDelta.getOid())) { if (LOGGER.isTraceEnabled()) { LOGGER.trace("Skipping delta {} in event {} because OID does not match ({} vs {})", objectDelta, lastEvent.getEventIdentifier(), object.getOid(), objectDelta.getOid()); } return false; } return true; } private void authorize(ModelAuthorizationAction action, OperationResult result) throws SecurityViolationException, SchemaException { securityEnforcer.authorize(action.getUrl(), AuthorizationPhaseType.REQUEST, null, null, null, null, result); securityEnforcer.authorize(action.getUrl(), AuthorizationPhaseType.EXECUTION, null, null, null, null, result); } }