NIFI-11134 Added Label auditing to Flow Configuration History

This closes #6949

Co-authored-by: David Handermann <exceptionfactory@apache.org>
Signed-off-by: David Handermann <exceptionfactory@apache.org>
This commit is contained in:
Mark Bean 2023-02-10 21:09:35 -05:00 committed by exceptionfactory
parent 055cf62c70
commit b08ae75be4
No known key found for this signature in database
GPG Key ID: 29B6A52D2AAE8DBA
6 changed files with 447 additions and 1 deletions

View File

@ -36,5 +36,6 @@ public enum Component {
ParameterProvider,
AccessPolicy,
User,
UserGroup;
UserGroup,
Label;
}

View File

@ -0,0 +1,177 @@
/*
* 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.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.FlowChangeConfigureDetails;
import org.apache.nifi.authorization.user.NiFiUser;
import org.apache.nifi.authorization.user.NiFiUserUtils;
import org.apache.nifi.controller.label.Label;
import org.apache.nifi.web.api.dto.LabelDTO;
import org.apache.nifi.web.dao.LabelDAO;
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.Date;
import java.util.Objects;
@Aspect
public class LabelAuditor extends NiFiAuditor {
private static final Logger logger = LoggerFactory.getLogger(LabelAuditor.class);
/**
* Audits the creation of a Label.
*
* @param proceedingJoinPoint Join Point observed
* @return Label
* @throws Throwable Thrown on failure to proceed with target invocation
*/
@Around("within(org.apache.nifi.web.dao.LabelDAO+) && "
+ "execution(org.apache.nifi.controller.label.Label createLabel(java.lang.String, org.apache.nifi.web.api.dto.LabelDTO))")
public Label createLabelAdvice(final ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
final Label label = (Label) proceedingJoinPoint.proceed();
final Action action = generateAuditRecord(label, Operation.Add);
if (action != null) {
saveAction(action, logger);
}
return label;
}
/**
* Audits the configuration of a Label.
*
* @param proceedingJoinPoint Join Point observed
* @param labelDTO Data Transfer Object
* @param labelDAO Data Access Object
* @return Label
* @throws Throwable Thrown on failure to proceed with target invocation
*/
@Around("within(org.apache.nifi.web.dao.LabelDAO+) && "
+ "execution(org.apache.nifi.controller.label.Label updateLabel(org.apache.nifi.web.api.dto.LabelDTO)) && "
+ "args(labelDTO) && "
+ "target(labelDAO)")
public Label updateLabelAdvice(final ProceedingJoinPoint proceedingJoinPoint, final LabelDTO labelDTO, final LabelDAO labelDAO) throws Throwable {
// determine the initial content of label
final Label label = labelDAO.getLabel(labelDTO.getId());
final String originalLabelValue = label.getValue();
final Label updatedLabel = (Label) proceedingJoinPoint.proceed();
// ensure the user was found
final NiFiUser user = NiFiUserUtils.getNiFiUser();
if (user != null) {
final String updatedLabelValue = updatedLabel.getValue();
if ((originalLabelValue == null && updatedLabelValue != null)
|| !Objects.equals(originalLabelValue, updatedLabelValue)) {
final FlowChangeAction labelAction = new FlowChangeAction();
labelAction.setUserIdentity(user.getIdentity());
labelAction.setTimestamp(new Date());
labelAction.setSourceId(label.getIdentifier());
labelAction.setSourceType(Component.Label);
labelAction.setOperation(Operation.Configure);
// Source Name is a required field for the database but not applicable for a label; use UUID to create a unique name
labelAction.setSourceName(label.getIdentifier());
final FlowChangeConfigureDetails actionDetails = new FlowChangeConfigureDetails();
actionDetails.setName(label.getIdentifier());
actionDetails.setValue(updatedLabelValue);
actionDetails.setPreviousValue(originalLabelValue);
labelAction.setActionDetails(actionDetails);
saveAction(labelAction, logger);
}
}
return updatedLabel;
}
/**
* Audits the removal of a Label.
*
* @param proceedingJoinPoint Join Point observed
* @param labelId Label identifier
* @param labelDAO Label Data Access Object
* @throws Throwable Thrown on failure to proceed with target invocation
*/
@Around("within(org.apache.nifi.web.dao.LabelDAO+) && "
+ "execution(void deleteLabel(java.lang.String)) && "
+ "args(labelId) && "
+ "target(labelDAO)")
public void removeLabelAdvice(final ProceedingJoinPoint proceedingJoinPoint, final String labelId, final LabelDAO labelDAO) throws Throwable {
// get the label before removing it
final Label label = labelDAO.getLabel(labelId);
// remove the label
proceedingJoinPoint.proceed();
// if no exceptions were thrown, add removal actions...
final Action action = generateAuditRecord(label, Operation.Remove);
if (action != null) {
saveAction(action, logger);
}
}
/**
* Generates an audit record for the creation of the specified label.
*
* @param label Label audited
* @param operation Operation audited
* @return Action description
*/
public Action generateAuditRecord(final Label label, final Operation operation) {
return generateAuditRecord(label, operation, null);
}
/**
* Generates an audit record for the creation of the specified label.
*
* @param label Label audited
* @param operation Operation audited
* @param actionDetails Action Details or null when not provided
* @return Action description
*/
public Action generateAuditRecord(final Label label, final Operation operation, final ActionDetails actionDetails) {
FlowChangeAction action = null;
final NiFiUser user = NiFiUserUtils.getNiFiUser();
if (user != null) {
action = new FlowChangeAction();
action.setUserIdentity(user.getIdentity());
action.setOperation(operation);
action.setTimestamp(new Date());
action.setSourceId(label.getIdentifier());
// Labels do not have a Name; use UUID to provide a unique name
action.setSourceName(label.getIdentifier());
action.setSourceType(Component.Label);
if (actionDetails != null) {
action.setActionDetails(actionDetails);
}
}
return action;
}
}

View File

@ -33,6 +33,7 @@ import org.apache.nifi.connectable.Funnel;
import org.apache.nifi.connectable.Port;
import org.apache.nifi.controller.ProcessorNode;
import org.apache.nifi.controller.Snippet;
import org.apache.nifi.controller.label.Label;
import org.apache.nifi.groups.ProcessGroup;
import org.apache.nifi.groups.RemoteProcessGroup;
import org.apache.nifi.authorization.user.NiFiUser;
@ -47,6 +48,7 @@ import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO;
import org.apache.nifi.web.api.dto.SnippetDTO;
import org.apache.nifi.web.dao.ConnectionDAO;
import org.apache.nifi.web.dao.FunnelDAO;
import org.apache.nifi.web.dao.LabelDAO;
import org.apache.nifi.web.dao.PortDAO;
import org.apache.nifi.web.dao.ProcessGroupDAO;
import org.apache.nifi.web.dao.ProcessorDAO;
@ -78,6 +80,7 @@ public class SnippetAuditor extends NiFiAuditor {
private ProcessorDAO processorDAO;
private FunnelDAO funnelDAO;
private ConnectionDAO connectionDAO;
private LabelDAO labelDAO;
private PortAuditor portAuditor;
private RemoteProcessGroupAuditor remoteProcessGroupAuditor;
@ -85,6 +88,7 @@ public class SnippetAuditor extends NiFiAuditor {
private ProcessorAuditor processorAuditor;
private FunnelAuditor funnelAuditor;
private RelationshipAuditor relationshipAuditor;
private LabelAuditor labelAuditor;
/**
* Audits copy/paste.
@ -328,6 +332,14 @@ public class SnippetAuditor extends NiFiAuditor {
}
}
for (String id : snippet.getLabels().keySet()) {
final Label label = labelDAO.getLabel(id);
final Action action = labelAuditor.generateAuditRecord(label, Operation.Move, createMoveDetails(previousGroupId, groupId, logger));
if (action != null) {
actions.add(action);
}
}
// save the actions
if (CollectionUtils.isNotEmpty(actions)) {
saveActions(actions, logger);
@ -390,6 +402,11 @@ public class SnippetAuditor extends NiFiAuditor {
connections.add(connectionDAO.getConnection(id));
}
final Set<Label> labels = new HashSet<>();
for (String id : snippet.getLabels().keySet()) {
labels.add(labelDAO.getLabel(id));
}
// remove the snippet and components
proceedingJoinPoint.proceed();
@ -446,6 +463,13 @@ public class SnippetAuditor extends NiFiAuditor {
}
}
for (Label label : labels) {
final Action action = labelAuditor.generateAuditRecord(label, Operation.Remove);
if (action != null) {
actions.add(action);
}
}
// save the actions
if (CollectionUtils.isNotEmpty(actions)) {
saveActions(actions, logger);
@ -481,6 +505,10 @@ public class SnippetAuditor extends NiFiAuditor {
this.remoteProcessGroupAuditor = remoteProcessGroupAuditor;
}
public void setLabelAuditor(LabelAuditor labelAuditor) {
this.labelAuditor = labelAuditor;
}
public void setRemoteProcessGroupDAO(RemoteProcessGroupDAO remoteProcessGroupDAO) {
this.remoteProcessGroupDAO = remoteProcessGroupDAO;
}

View File

@ -5928,6 +5928,9 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
case UserGroup:
authorizable = authorizableLookup.getTenant();
break;
case Label:
authorizable = authorizableLookup.getLabel(sourceId);
break;
default:
throw new WebApplicationException(Response.serverError().entity("An unexpected type of component is the source of this action.").build());
}

View File

@ -707,6 +707,11 @@
<property name="auditService" ref="auditService"/>
<property name="processGroupDAO" ref="processGroupDAO"/>
</bean>
<bean id="labelAuditor" class="org.apache.nifi.audit.LabelAuditor">
<property name="serviceFacade" ref="serviceFacade"/>
<property name="auditService" ref="auditService"/>
<property name="processGroupDAO" ref="processGroupDAO"/>
</bean>
<bean id="snippetAuditor" class="org.apache.nifi.audit.SnippetAuditor">
<property name="serviceFacade" ref="serviceFacade"/>
<property name="auditService" ref="auditService"/>
@ -723,6 +728,7 @@
<property name="processGroupAuditor" ref="processGroupAuditor"/>
<property name="processorAuditor" ref="processorAuditor"/>
<property name="relationshipAuditor" ref="relationshipAuditor"/>
<property name="labelAuditor" ref="labelAuditor"/>
</bean>
<bean id="controllerServiceAuditor" class="org.apache.nifi.audit.ControllerServiceAuditor">
<property name="serviceFacade" ref="serviceFacade"/>

View File

@ -0,0 +1,231 @@
/*
* 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.nifi.action.Action;
import org.apache.nifi.action.Component;
import org.apache.nifi.action.Operation;
import org.apache.nifi.action.details.ActionDetails;
import org.apache.nifi.action.details.FlowChangeConfigureDetails;
import org.apache.nifi.admin.service.AuditService;
import org.apache.nifi.authorization.user.NiFiUser;
import org.apache.nifi.authorization.user.NiFiUserDetails;
import org.apache.nifi.authorization.user.StandardNiFiUser;
import org.apache.nifi.controller.FlowController;
import org.apache.nifi.controller.flow.FlowManager;
import org.apache.nifi.controller.label.StandardLabel;
import org.apache.nifi.groups.ProcessGroup;
import org.apache.nifi.web.api.dto.LabelDTO;
import org.apache.nifi.web.dao.ProcessGroupDAO;
import org.apache.nifi.web.dao.impl.StandardLabelDAO;
import org.apache.nifi.controller.label.Label;
import org.apache.nifi.web.dao.impl.StandardProcessGroupDAO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith({SpringExtension.class, MockitoExtension.class})
@ContextConfiguration(classes = {TestLabelAuditor.AuditorConfiguration.class})
public class TestLabelAuditor {
private static final String USER_IDENTITY = "username";
private static final String GROUP_ID = "group-1";
private static final String LABEL_ID = "label-1";
private static final String LABEL = "label";
private static final String UPDATED_LABEL = "updated-label";
@Mock
AuditService auditService;
@Mock
Authentication authentication;
@Mock
FlowController flowController;
@Mock
FlowManager flowManager;
@Mock
ProcessGroup processGroup;
@Captor
ArgumentCaptor<List<Action>> actionsCaptor;
@Autowired
StandardLabelDAO labelDao;
@Autowired
LabelAuditor labelAuditor;
@BeforeEach
void setAuditor() {
final SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authentication);
final NiFiUser user = new StandardNiFiUser.Builder().identity(USER_IDENTITY).build();
final NiFiUserDetails userDetail = new NiFiUserDetails(user);
when(authentication.getPrincipal()).thenReturn(userDetail);
when(flowController.getFlowManager()).thenReturn(flowManager);
labelDao.setFlowController(flowController);
labelAuditor.setAuditService(auditService);
}
@Test
void testCreateLabelAdvice() {
final LabelDTO labelDto = getLabelDto();
when(flowManager.getGroup(eq(GROUP_ID))).thenReturn(processGroup);
when(flowManager.createLabel(eq(LABEL_ID), eq(LABEL))).thenReturn(new StandardLabel(LABEL_ID, LABEL));
final Label label = labelDao.createLabel(GROUP_ID, labelDto);
assertNotNull(label);
verify(auditService).addActions(actionsCaptor.capture());
final List<Action> actions = actionsCaptor.getValue();
assertActionFound(actions, Operation.Add);
}
@Test
void testRemoveLabelAdvice() {
setFindLabel();
labelDao.deleteLabel(LABEL_ID);
verify(auditService).addActions(actionsCaptor.capture());
final List<Action> actions = actionsCaptor.getValue();
assertActionFound(actions, Operation.Remove);
}
@Test
void testUpdateLabelAdvice() {
setFindLabel();
final LabelDTO labelDto = getLabelDto();
labelDto.setLabel(UPDATED_LABEL);
labelDao.updateLabel(labelDto);
verify(auditService).addActions(actionsCaptor.capture());
final List<Action> actions = actionsCaptor.getValue();
final Action action = assertActionFound(actions, Operation.Configure);
final ActionDetails actionDetails = action.getActionDetails();
assertActionDetailsFound(actionDetails);
}
@Test
void testUpdateLabelAdviceLabelUnchanged() {
setFindLabel();
final LabelDTO labelDto = getLabelDto();
labelDao.updateLabel(labelDto);
verifyNoInteractions(auditService);
}
private void setFindLabel() {
when(flowManager.getRootGroup()).thenReturn(processGroup);
final Label label = new StandardLabel(LABEL_ID, LABEL);
label.setProcessGroup(processGroup);
when(processGroup.findLabel(eq(LABEL_ID))).thenReturn(label);
}
private void assertActionDetailsFound(final ActionDetails actionDetails) {
assertInstanceOf(FlowChangeConfigureDetails.class, actionDetails);
final FlowChangeConfigureDetails flowChangeConfigureDetails = (FlowChangeConfigureDetails) actionDetails;
assertEquals(LABEL_ID, flowChangeConfigureDetails.getName());
assertEquals(LABEL, flowChangeConfigureDetails.getPreviousValue());
assertEquals(UPDATED_LABEL, flowChangeConfigureDetails.getValue());
}
private Action assertActionFound(final List<Action> actions, final Operation operation) {
assertNotNull(actions);
final Optional<Action> actionFound = actions.stream().findFirst();
assertTrue(actionFound.isPresent());
final Action action = actionFound.get();
assertEquals(USER_IDENTITY, action.getUserIdentity());
assertEquals(operation, action.getOperation());
assertEquals(LABEL_ID, action.getSourceId());
assertEquals(LABEL_ID, action.getSourceName());
assertEquals(Component.Label, action.getSourceType());
assertNotNull(action.getTimestamp());
return action;
}
private LabelDTO getLabelDto() {
final LabelDTO labelDto = new LabelDTO();
labelDto.setLabel(LABEL);
labelDto.setId(LABEL_ID);
labelDto.setStyle(Collections.emptyMap());
return labelDto;
}
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public static class AuditorConfiguration {
@Bean
public LabelAuditor labelAuditor() {
return new LabelAuditor();
}
@Bean
public StandardLabelDAO labelDAO() {
return new StandardLabelDAO();
}
@Bean
public ProcessGroupDAO processGroupDAO() {
return new StandardProcessGroupDAO();
}
}
}