/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.nifi.audit; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.action.Action; import org.apache.nifi.action.Component; import org.apache.nifi.action.FlowChangeAction; import org.apache.nifi.action.Operation; import org.apache.nifi.action.details.ActionDetails; import org.apache.nifi.action.details.ConnectDetails; import org.apache.nifi.action.details.FlowChangeConfigureDetails; import org.apache.nifi.action.details.FlowChangeConnectDetails; import org.apache.nifi.authorization.user.NiFiUser; import org.apache.nifi.authorization.user.NiFiUserUtils; import org.apache.nifi.connectable.Connectable; import org.apache.nifi.connectable.Connection; import org.apache.nifi.connectable.Funnel; import org.apache.nifi.connectable.Port; import org.apache.nifi.controller.ProcessorNode; import org.apache.nifi.flowfile.FlowFilePrioritizer; import org.apache.nifi.groups.ProcessGroup; import org.apache.nifi.processor.Relationship; import org.apache.nifi.remote.RemoteGroupPort; import org.apache.nifi.remote.TransferDirection; import org.apache.nifi.web.api.dto.ConnectionDTO; import org.apache.nifi.web.dao.ConnectionDAO; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; /** * Audits relationship creation/removal. */ @Aspect public class RelationshipAuditor extends NiFiAuditor { private static final Logger logger = LoggerFactory.getLogger(RelationshipAuditor.class); private static final String NAME = "Name"; private static final String FLOW_FILE_EXPIRATION = "File Expiration"; private static final String BACK_PRESSURE_OBJECT_THRESHOLD = "Back Pressure Object Threshold"; private static final String BACK_PRESSURE_DATA_SIZE_THRESHOLD = "Back Pressure Data Size Threshold"; private static final String PRIORITIZERS = "Prioritizers"; /** * Audits the creation of relationships via createConnection(). * * This method only needs to be run 'after returning'. However, in Java 7 the order in which these methods are returned from Class.getDeclaredMethods (even though there is no order guaranteed) * seems to differ from Java 6. SpringAOP depends on this ordering to determine advice precedence. By normalizing all advice into Around advice we can alleviate this issue. * * @param proceedingJoinPoint join point * @return connection * @throws java.lang.Throwable ex */ @Around("within(org.apache.nifi.web.dao.ConnectionDAO+) && " + "execution(org.apache.nifi.connectable.Connection createConnection(java.lang.String, org.apache.nifi.web.api.dto.ConnectionDTO))") public Connection createConnectionAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { // perform the underlying operation Connection connection = (Connection) proceedingJoinPoint.proceed(); // audit the connection creation final ConnectDetails connectDetails = createConnectDetails(connection, connection.getRelationships()); final Action action = generateAuditRecordForConnection(connection, Operation.Connect, connectDetails); // save the actions if (action != null) { saveAction(action, logger); } return connection; } /** * Audits the creation and removal of relationships via updateConnection(). * * @param proceedingJoinPoint join point * @param connectionDTO dto * @param connectionDAO dao * @return connection * @throws Throwable ex */ @Around("within(org.apache.nifi.web.dao.ConnectionDAO+) && " + "execution(org.apache.nifi.connectable.Connection updateConnection(org.apache.nifi.web.api.dto.ConnectionDTO)) && " + "args(connectionDTO) && " + "target(connectionDAO)") public Connection updateConnectionAdvice(ProceedingJoinPoint proceedingJoinPoint, ConnectionDTO connectionDTO, ConnectionDAO connectionDAO) throws Throwable { // get the previous configuration Connection connection = connectionDAO.getConnection(connectionDTO.getId()); Connectable previousDestination = connection.getDestination(); Collection<Relationship> previousRelationships = connection.getRelationships(); Map<String, String> values = extractConfiguredPropertyValues(connection, connectionDTO); // perform the underlying operation connection = (Connection) proceedingJoinPoint.proceed(); // get the current user NiFiUser user = NiFiUserUtils.getNiFiUser(); // ensure the user was found if (user != null) { Collection<Action> actions = new ArrayList<>(); Map<String, String> updatedValues = extractConfiguredPropertyValues(connection, connectionDTO); // get the source Connectable source = connection.getSource(); // get the current target Connectable destination = connection.getDestination(); // determine if a new target was specified in the initial request if (destination != null && !previousDestination.getIdentifier().equals(destination.getIdentifier())) { // record the removal of all relationships from the previous target final ConnectDetails disconnectDetails = createConnectDetails(connection, source, previousRelationships, previousDestination); actions.add(generateAuditRecordForConnection(connection, Operation.Disconnect, disconnectDetails)); // record the addition of all relationships to the new target final ConnectDetails connectDetails = createConnectDetails(connection, connection.getRelationships()); actions.add(generateAuditRecordForConnection(connection, Operation.Connect, connectDetails)); } // audit and relationships added/removed Collection<Relationship> newRelationships = connection.getRelationships(); // identify any relationships that were added if (newRelationships != null) { List<Relationship> relationshipsToAdd = new ArrayList<>(newRelationships); if (previousRelationships != null) { relationshipsToAdd.removeAll(previousRelationships); } if (!relationshipsToAdd.isEmpty()) { final ConnectDetails connectDetails = createConnectDetails(connection, relationshipsToAdd); actions.add(generateAuditRecordForConnection(connection, Operation.Connect, connectDetails)); } } // identify any relationships that were removed if (previousRelationships != null) { List<Relationship> relationshipsToRemove = new ArrayList<>(previousRelationships); if (newRelationships != null) { relationshipsToRemove.removeAll(newRelationships); } if (!relationshipsToRemove.isEmpty()) { final ConnectDetails connectDetails = createConnectDetails(connection, relationshipsToRemove); actions.add(generateAuditRecordForConnection(connection, Operation.Disconnect, connectDetails)); } } // go through each updated value Date actionTimestamp = new Date(); for (String property : updatedValues.keySet()) { String newValue = updatedValues.get(property); String oldValue = values.get(property); // ensure the value is changing if (oldValue == null || newValue == null || !newValue.equals(oldValue)) { // create the config details FlowChangeConfigureDetails configurationDetails = new FlowChangeConfigureDetails(); configurationDetails.setName(property); configurationDetails.setValue(newValue); configurationDetails.setPreviousValue(oldValue); // create a configuration action FlowChangeAction configurationAction = new FlowChangeAction(); configurationAction.setUserIdentity(user.getIdentity()); configurationAction.setOperation(Operation.Configure); configurationAction.setTimestamp(actionTimestamp); configurationAction.setSourceId(connection.getIdentifier()); configurationAction.setSourceName(connection.getName()); configurationAction.setSourceType(Component.Connection); configurationAction.setActionDetails(configurationDetails); actions.add(configurationAction); } } // save the actions if (!actions.isEmpty()) { saveActions(actions, logger); } } return connection; } /** * Audits the removal of relationships via deleteConnection(). * * @param proceedingJoinPoint join point * @param id id * @param connectionDAO dao * @throws Throwable ex */ @Around("within(org.apache.nifi.web.dao.ConnectionDAO+) && " + "execution(void deleteConnection(java.lang.String)) && " + "args(id) && " + "target(connectionDAO)") public void removeConnectionAdvice(ProceedingJoinPoint proceedingJoinPoint, String id, ConnectionDAO connectionDAO) throws Throwable { // get the connection before performing the update Connection connection = connectionDAO.getConnection(id); // perform the underlying operation proceedingJoinPoint.proceed(); // audit the connection creation final ConnectDetails connectDetails = createConnectDetails(connection, connection.getRelationships()); final Action action = generateAuditRecordForConnection(connection, Operation.Disconnect, connectDetails); // save the actions if (action != null) { saveAction(action, logger); } } public ConnectDetails createConnectDetails(final Connection connection, final Collection<Relationship> relationships) { return createConnectDetails(connection, connection.getSource(), relationships, connection.getDestination()); } /** * Creates action details for connect/disconnect actions. * * @param connection connection * @param source source * @param relationships relationships * @param destination destinations * @return details */ public ConnectDetails createConnectDetails(final Connection connection, final Connectable source, final Collection<Relationship> relationships, final Connectable destination) { final Component sourceType = determineConnectableType(source); final Component destiantionType = determineConnectableType(destination); // format the relationship names Collection<String> relationshipNames = new HashSet<>(connection.getRelationships().size()); for (final Relationship relationship : relationships) { relationshipNames.add(relationship.getName()); } final String formattedRelationships = relationshipNames.isEmpty() ? StringUtils.EMPTY : StringUtils.join(relationshipNames, ", "); // create the connect details final FlowChangeConnectDetails connectDetails = new FlowChangeConnectDetails(); connectDetails.setSourceId(source.getIdentifier()); connectDetails.setSourceName(source.getName()); connectDetails.setSourceType(sourceType); connectDetails.setRelationship(formattedRelationships); connectDetails.setDestinationId(destination.getIdentifier()); connectDetails.setDestinationName(destination.getName()); connectDetails.setDestinationType(destiantionType); return connectDetails; } /** * Extracts configured settings from the specified connection only if they have also been specified in the connectionDTO. * * @param connection connection * @param connectionDTO dto * @return properties */ private Map<String, String> extractConfiguredPropertyValues(Connection connection, ConnectionDTO connectionDTO) { Map<String, String> values = new HashMap<>(); if (connectionDTO.getName() != null) { values.put(NAME, connection.getName()); } if (connectionDTO.getFlowFileExpiration() != null) { values.put(FLOW_FILE_EXPIRATION, String.valueOf(connection.getFlowFileQueue().getFlowFileExpiration())); } if (connectionDTO.getBackPressureObjectThreshold() != null) { values.put(BACK_PRESSURE_OBJECT_THRESHOLD, String.valueOf(connection.getFlowFileQueue().getBackPressureObjectThreshold())); } if (connectionDTO.getBackPressureDataSizeThreshold() != null) { values.put(BACK_PRESSURE_DATA_SIZE_THRESHOLD, String.valueOf(connection.getFlowFileQueue().getBackPressureDataSizeThreshold())); } if (connectionDTO.getPrioritizers() != null) { List<String> prioritizers = new ArrayList<>(); for (FlowFilePrioritizer prioritizer : connection.getFlowFileQueue().getPriorities()) { prioritizers.add(prioritizer.getClass().getCanonicalName()); } values.put(PRIORITIZERS, StringUtils.join(prioritizers, ", ")); } return values; } /** * Generates the audit records for the specified connection. * * @param connection connection * @param operation operation * @return action */ public Action generateAuditRecordForConnection(Connection connection, Operation operation) { return generateAuditRecordForConnection(connection, operation, null); } /** * Generates the audit records for the specified connection. * * @param connection connection * @param operation operation * @param actionDetails details * @return action */ public Action generateAuditRecordForConnection(Connection connection, Operation operation, ActionDetails actionDetails) { FlowChangeAction action = null; // get the current user NiFiUser user = NiFiUserUtils.getNiFiUser(); // ensure the user was found if (user != null) { // determine the source details final String connectionId = connection.getIdentifier(); String connectionName = connection.getName(); if (StringUtils.isBlank(connectionName)) { Collection<String> relationshipNames = new HashSet<>(connection.getRelationships().size()); for (final Relationship relationship : connection.getRelationships()) { relationshipNames.add(relationship.getName()); } connectionName = StringUtils.join(relationshipNames, ", "); } // go through each relationship added Date actionTimestamp = new Date(); // create a new relationship action action = new FlowChangeAction(); action.setUserIdentity(user.getIdentity()); action.setOperation(operation); action.setTimestamp(actionTimestamp); action.setSourceId(connectionId); action.setSourceName(connectionName); action.setSourceType(Component.Connection); if (actionDetails != null) { action.setActionDetails(actionDetails); } } return action; } /** * Determines the type of component the specified connectable is. */ private Component determineConnectableType(Connectable connectable) { String sourceId = connectable.getIdentifier(); Component componentType = Component.Controller; if (connectable instanceof ProcessorNode) { componentType = Component.Processor; } else if (connectable instanceof RemoteGroupPort) { final RemoteGroupPort remoteGroupPort = (RemoteGroupPort) connectable; if (TransferDirection.RECEIVE.equals(remoteGroupPort.getTransferDirection())) { if (remoteGroupPort.getRemoteProcessGroup() == null) { componentType = Component.InputPort; } else { componentType = Component.OutputPort; } } else { if (remoteGroupPort.getRemoteProcessGroup() == null) { componentType = Component.OutputPort; } else { componentType = Component.InputPort; } } } else if (connectable instanceof Port) { ProcessGroup processGroup = connectable.getProcessGroup(); if (processGroup.getInputPort(sourceId) != null) { componentType = Component.InputPort; } else if (processGroup.getOutputPort(sourceId) != null) { componentType = Component.OutputPort; } } else if (connectable instanceof Funnel) { componentType = Component.Funnel; } return componentType; } }