001/* 002 * Licensed to DuraSpace under one or more contributor license agreements. 003 * See the NOTICE file distributed with this work for additional information 004 * regarding copyright ownership. 005 * 006 * DuraSpace licenses this file to you under the Apache License, 007 * Version 2.0 (the "License"); you may not use this file except in 008 * compliance with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.fcrepo.audit; 019 020import static org.apache.jena.datatypes.xsd.XSDDatatype.XSDdateTime; 021import static org.apache.jena.datatypes.xsd.XSDDatatype.XSDstring; 022import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel; 023import static org.apache.jena.rdf.model.ResourceFactory.createProperty; 024import static org.apache.jena.rdf.model.ResourceFactory.createResource; 025import static org.apache.jena.rdf.model.ResourceFactory.createTypedLiteral; 026import static java.util.EnumSet.noneOf; 027import static org.fcrepo.kernel.api.observer.OptionalValues.BASE_URL; 028import static org.fcrepo.kernel.api.observer.OptionalValues.USER_AGENT; 029import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getJcrNode; 030import static org.slf4j.LoggerFactory.getLogger; 031 032import static org.fcrepo.audit.AuditProperties.INTERNAL_EVENT; 033import static org.fcrepo.audit.AuditProperties.PREMIS_AGENT; 034import static org.fcrepo.audit.AuditProperties.PREMIS_EVENT; 035import static org.fcrepo.audit.AuditProperties.PREMIS_TIME; 036import static org.fcrepo.audit.AuditProperties.PREMIS_TYPE; 037import static org.fcrepo.audit.AuditProperties.PROV_EVENT; 038import static org.fcrepo.audit.AuditProperties.RDF_TYPE; 039 040import java.io.IOException; 041import java.net.URI; 042import java.net.URISyntaxException; 043import java.text.DateFormat; 044import java.text.SimpleDateFormat; 045import java.time.Instant; 046import java.util.Set; 047import java.util.TimeZone; 048 049import javax.annotation.PostConstruct; 050import javax.annotation.PreDestroy; 051import javax.inject.Inject; 052import javax.jcr.PropertyType; 053import javax.jcr.RepositoryException; 054 055import org.fcrepo.kernel.api.RequiredRdfContext; 056import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 057import org.fcrepo.kernel.api.identifiers.IdentifierConverter; 058import org.fcrepo.kernel.api.models.FedoraResource; 059import org.fcrepo.kernel.api.observer.FedoraEvent; 060import org.fcrepo.kernel.api.services.ContainerService; 061import org.fcrepo.kernel.modeshape.rdf.impl.PrefixingIdentifierTranslator; 062 063import org.modeshape.jcr.api.JcrTools; 064import org.modeshape.jcr.api.Repository; 065import org.modeshape.jcr.api.Session; 066import org.slf4j.Logger; 067 068import org.apache.jena.rdf.model.Model; 069import org.apache.jena.rdf.model.RDFNode; 070import org.apache.jena.rdf.model.Resource; 071import org.apache.jena.rdf.model.ResourceFactory; 072import org.apache.jena.rdf.model.Statement; 073import com.google.common.annotations.VisibleForTesting; 074import com.google.common.eventbus.EventBus; 075import com.google.common.eventbus.Subscribe; 076 077/** 078 * Auditor implementation that creates audit nodes in the repository. 079 * @author mohideen 080 * @author escowles 081 * @since 2015-04-15 082 */ 083public class InternalAuditor implements Auditor { 084 085 /** 086 * Logger for this class. 087 */ 088 private static final Logger LOGGER = getLogger(InternalAuditor.class); 089 090 private static final String AUDIT_CONTAINER = "fcrepo.audit.container"; 091 092 private static String AUDIT_CONTAINER_LOCATION; 093 094 @Inject 095 private EventBus eventBus; 096 097 @Inject 098 private Repository repository; 099 100 @Inject 101 private ContainerService containerService; 102 103 private static final UuidPathMinter pathMinter = new UuidPathMinter(); 104 105 private Session session; 106 private static JcrTools jcrTools = new JcrTools(true); 107 108 /** 109 * Register with the EventBus to receive events. 110 */ 111 @PostConstruct 112 public void register() { 113 try { 114 AUDIT_CONTAINER_LOCATION = System.getProperty(AUDIT_CONTAINER); 115 if (AUDIT_CONTAINER_LOCATION != null) { 116 LOGGER.info("Initializing: {}, {}", this.getClass().getCanonicalName(), AUDIT_CONTAINER_LOCATION); 117 eventBus.register(this); 118 if (!AUDIT_CONTAINER_LOCATION.startsWith("/")) { 119 AUDIT_CONTAINER_LOCATION = "/" + AUDIT_CONTAINER_LOCATION; 120 } 121 if (AUDIT_CONTAINER_LOCATION.endsWith("/")) { 122 AUDIT_CONTAINER_LOCATION = AUDIT_CONTAINER_LOCATION.substring(0, 123 AUDIT_CONTAINER_LOCATION.length() - 2); 124 } 125 session = repository.login(); 126 containerService.findOrCreate(session, AUDIT_CONTAINER_LOCATION); 127 128 LOGGER.debug("Registering audit CND"); 129 jcrTools.registerNodeTypes(session, "audit.cnd"); 130 131 session.save(); 132 } else { 133 LOGGER.warn("Cannot Initialize: {}", this.getClass().getCanonicalName()); 134 LOGGER.warn("System property not found: " + AUDIT_CONTAINER); 135 } 136 } catch (RepositoryException e) { 137 throw new RepositoryRuntimeException(e); 138 } 139 } 140 141 /** 142 * Fedora internal events are received and processed by this method. 143 * 144 * @param event 145 * The {@link FedoraEvent} to record. 146 */ 147 @Subscribe 148 public void recordEvent(final FedoraEvent event) { 149 LOGGER.debug("Event detected: {} {}", event.getUserID(), event.getPath()); 150 if (!event.getPath().startsWith(AUDIT_CONTAINER_LOCATION) && !event.getPath().isEmpty()) { 151 try { 152 createAuditNode(event); 153 } catch (IOException e) { 154 throw new RepositoryRuntimeException(e); 155 } 156 } 157 } 158 159 /** 160 * Close external connections 161 */ 162 @PreDestroy 163 public void releaseConnections() { 164 LOGGER.debug("Tearing down: {}", this.getClass().getCanonicalName()); 165 eventBus.unregister(this); 166 } 167 168 // JCR property name, not URI 169 private static final String PREMIS_OBJ = "premis:hasEventRelatedObject"; 170 171 /** 172 * Creates a node for the audit event under the configured container. 173 * 174 * @param event to be persisted in the repository 175 * @throws java.io.IOException on json mapping error 176 */ 177 public void createAuditNode(final FedoraEvent event) throws IOException { 178 try { 179 final String userAgent = event.getInfo().get(USER_AGENT); 180 final String baseURL = event.getInfo().get(BASE_URL); 181 final String path = event.getPath(); 182 final String uri = baseURL + path; 183 final Instant timestamp = event.getDate(); 184 final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); 185 df.setTimeZone(TimeZone.getTimeZone("UTC")); 186 final String eventDate = df.format(timestamp.toEpochMilli()); 187 final String userID = event.getUserID(); 188 final Set<String> eventTypes = AuditUtils.getEventURIs(event.getTypes()); 189 final Set<String> resourceTypes = event.getResourceTypes(); 190 final String auditEventType = AuditUtils.getAuditEventType(eventTypes, resourceTypes); 191 192 final String eventPath = getEventPath(event.getEventID()); 193 final FedoraResource auditResource = containerService.findOrCreate(session, 194 AUDIT_CONTAINER_LOCATION + "/" + eventPath); 195 196 LOGGER.debug("Audit node {} created for event.", event.getEventID()); 197 198 final Model m = createDefaultModel(); 199 final String auditResourceURI = baseURL + AUDIT_CONTAINER_LOCATION + "/" + eventPath; 200 final Resource s = createResource(auditResourceURI); 201 m.add(createStatement(s, RDF_TYPE, createResource(INTERNAL_EVENT))); 202 m.add(createStatement(s, RDF_TYPE, createResource(PREMIS_EVENT))); 203 m.add(createStatement(s, RDF_TYPE, createResource(PROV_EVENT))); 204 m.add(createStatement(s, PREMIS_TIME, createTypedLiteral(eventDate, XSDdateTime))); 205 m.add(createStatement(s, PREMIS_AGENT, createTypedLiteral(userID, XSDstring))); 206 m.add(createStatement(s, PREMIS_AGENT, createTypedLiteral(userAgent, XSDstring))); 207 if (auditEventType != null) { 208 m.add(createStatement(s, PREMIS_TYPE, createResource(auditEventType))); 209 } 210 211 final IdentifierConverter<Resource, FedoraResource> translator = 212 new PrefixingIdentifierTranslator(session, baseURL + "/"); 213 auditResource.replaceProperties(translator, m, 214 auditResource.getTriples(translator, noneOf(RequiredRdfContext.class))); 215 216 // set link to impacted object using a URI property to preserve the link if it's deleted 217 try { 218 getJcrNode(auditResource).setProperty(PREMIS_OBJ, new URI(uri).toString(), PropertyType.URI); 219 } catch (URISyntaxException e) { 220 LOGGER.warn("Error creating URI for repository resource {}", uri); 221 } 222 223 session.save(); 224 } catch (RepositoryException e) { 225 throw new RepositoryRuntimeException(e); 226 } 227 } 228 229 @VisibleForTesting 230 protected String getEventPath(final String eventID) { 231 if (!eventID.startsWith("urn:uuid:")) { 232 throw new IllegalArgumentException("Event ID must be a 'urn:uuid:'" + eventID); 233 } 234 return pathMinter.get(eventID.substring("urn:uuid:".length())); 235 } 236 237 @VisibleForTesting 238 protected Statement createStatement(final Resource subject, final String property, final RDFNode object) { 239 return ResourceFactory.createStatement(subject, createProperty(property), object); 240 } 241 242}