Repository: nifi Updated Branches: refs/heads/master 5e62b4ae7 -> a565484dd
NIFI-2940: - Allowing access to configuration actions through the Controller when the underlying component has been removed. This closes #1648. Signed-off-by: Bryan Bende <[email protected]> Project: http://git-wip-us.apache.org/repos/asf/nifi/repo Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/a565484d Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/a565484d Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/a565484d Branch: refs/heads/master Commit: a565484ddd5600adf9cdd811f7abe4a45cbbfb27 Parents: 5e62b4a Author: Matt Gilman <[email protected]> Authored: Thu Mar 30 08:52:16 2017 -0400 Committer: Bryan Bende <[email protected]> Committed: Tue Apr 4 17:40:09 2017 -0400 ---------------------------------------------------------------------- .../nifi/web/StandardNiFiServiceFacade.java | 6 +- .../org/apache/nifi/web/api/FlowResource.java | 24 +- .../nifi/web/StandardNiFiServiceFacadeTest.java | 311 +++++++++++++++++++ 3 files changed, 337 insertions(+), 4 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/nifi/blob/a565484d/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java index 1f8fd92..2ff0b3b 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java @@ -3150,7 +3150,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade { final String sourceId = action.getSourceId(); final Component type = action.getSourceType(); - final Authorizable authorizable; + Authorizable authorizable; try { switch (type) { case Processor: @@ -3194,8 +3194,8 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade { throw new WebApplicationException(Response.serverError().entity("An unexpected type of component is the source of this action.").build()); } } catch (final ResourceNotFoundException e) { - // if the underlying component is gone, disallow - return AuthorizationResult.denied("The component of this action is no longer in the data flow."); + // if the underlying component is gone, use the controller to see if permissions should be allowed + authorizable = controllerFacade; } // perform the authorization http://git-wip-us.apache.org/repos/asf/nifi/blob/a565484d/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java index bc45118..a380aa7 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java @@ -2224,7 +2224,29 @@ public class FlowResource extends ApplicationResource { // ignore as the component may not be a reporting task } - throw new ResourceNotFoundException(String.format("Unable to find component with id '%s'.", componentId)); + // a component for the specified id could not be found, attempt to authorize based on read to the controller + final Map<String, String> userContext; + if (!StringUtils.isBlank(user.getClientAddress())) { + userContext = new HashMap<>(); + userContext.put(UserContextKeys.CLIENT_ADDRESS.name(), user.getClientAddress()); + } else { + userContext = null; + } + + final AuthorizationRequest request = new AuthorizationRequest.Builder() + .resource(ResourceFactory.getControllerResource()) + .identity(user.getIdentity()) + .anonymous(user.isAnonymous()) + .accessAttempt(true) + .action(RequestAction.READ) + .userContext(userContext) + .explanationSupplier(() -> String.format("Unable to find component with id '%s' and unable to view the controller.", componentId)) + .build(); + + final AuthorizationResult result = authorizer.authorize(request); + if (!Result.Approved.equals(result.getResult())) { + throw new AccessDeniedException(result.getExplanation()); + } }); // Note: History requests are not replicated throughout the cluster and are instead handled by the nodes independently http://git-wip-us.apache.org/repos/asf/nifi/blob/a565484d/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java new file mode 100644 index 0000000..373c318 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java @@ -0,0 +1,311 @@ +/* + * 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.web; + +import org.apache.nifi.action.Component; +import org.apache.nifi.action.FlowChangeAction; +import org.apache.nifi.action.Operation; +import org.apache.nifi.admin.service.AuditService; +import org.apache.nifi.authorization.AccessDeniedException; +import org.apache.nifi.authorization.AuthorizableLookup; +import org.apache.nifi.authorization.AuthorizationRequest; +import org.apache.nifi.authorization.AuthorizationResult; +import org.apache.nifi.authorization.Authorizer; +import org.apache.nifi.authorization.ConfigurableComponentAuthorizable; +import org.apache.nifi.authorization.Resource; +import org.apache.nifi.authorization.resource.Authorizable; +import org.apache.nifi.authorization.resource.ResourceFactory; +import org.apache.nifi.authorization.resource.ResourceType; +import org.apache.nifi.authorization.user.NiFiUserDetails; +import org.apache.nifi.authorization.user.StandardNiFiUser; +import org.apache.nifi.controller.FlowController; +import org.apache.nifi.history.History; +import org.apache.nifi.history.HistoryQuery; +import org.apache.nifi.web.api.dto.DtoFactory; +import org.apache.nifi.web.api.dto.EntityFactory; +import org.apache.nifi.web.api.dto.action.HistoryDTO; +import org.apache.nifi.web.api.dto.action.HistoryQueryDTO; +import org.apache.nifi.web.api.entity.ActionEntity; +import org.apache.nifi.web.controller.ControllerFacade; +import org.apache.nifi.web.security.token.NiFiAuthenticationToken; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.Mockito; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class StandardNiFiServiceFacadeTest { + + private static final String USER_1 = "user-1"; + private static final String USER_2 = "user-2"; + + private static final Integer UNKNOWN_ACTION_ID = 0; + + private static final Integer ACTION_ID_1 = 1; + private static final String PROCESSOR_ID_1 = "processor-1"; + + private static final Integer ACTION_ID_2 = 2; + private static final String PROCESSOR_ID_2 = "processor-2"; + + private StandardNiFiServiceFacade serviceFacade; + private Authorizer authorizer; + + @Before + public void setUp() throws Exception { + // audit service + final AuditService auditService = mock(AuditService.class); + when(auditService.getAction(anyInt())).then(invocation -> { + final Integer actionId = invocation.getArgumentAt(0, Integer.class); + + FlowChangeAction action = null; + if (ACTION_ID_1.equals(actionId)) { + action = getAction(actionId, PROCESSOR_ID_1); + } else if (ACTION_ID_2.equals(actionId)) { + action = getAction(actionId, PROCESSOR_ID_2); + } + + return action; + }); + when(auditService.getActions(any(HistoryQuery.class))).then(invocation -> { + final History history = new History(); + history.setActions(Arrays.asList(getAction(ACTION_ID_1, PROCESSOR_ID_1), getAction(ACTION_ID_2, PROCESSOR_ID_2))); + return history; + }); + + + // authorizable lookup + final AuthorizableLookup authorizableLookup = mock(AuthorizableLookup.class); + when(authorizableLookup.getProcessor(Mockito.anyString())).then(getProcessorInvocation -> { + final String processorId = getProcessorInvocation.getArgumentAt(0, String.class); + + // processor-2 is no longer part of the flow + if (processorId.equals(PROCESSOR_ID_2)) { + throw new ResourceNotFoundException(""); + } + + // component authorizable + final ConfigurableComponentAuthorizable componentAuthorizable = mock(ConfigurableComponentAuthorizable.class); + when(componentAuthorizable.getAuthorizable()).then(getAuthorizableInvocation -> { + + // authorizable + final Authorizable authorizable = new Authorizable() { + @Override + public Authorizable getParentAuthorizable() { + return null; + } + + @Override + public Resource getResource() { + return ResourceFactory.getComponentResource(ResourceType.Processor, processorId, processorId); + } + }; + + return authorizable; + }); + + return componentAuthorizable; + }); + + // authorizer + authorizer = mock(Authorizer.class); + when(authorizer.authorize(any(AuthorizationRequest.class))).then(invocation -> { + final AuthorizationRequest request = invocation.getArgumentAt(0, AuthorizationRequest.class); + + AuthorizationResult result = AuthorizationResult.denied(); + if (request.getResource().getIdentifier().endsWith(PROCESSOR_ID_1)) { + if (USER_1.equals(request.getIdentity())) { + result = AuthorizationResult.approved(); + } + } else if (request.getResource().equals(ResourceFactory.getControllerResource())) { + if (USER_2.equals(request.getIdentity())) { + result = AuthorizationResult.approved(); + } + } + + return result; + }); + + // flow controller + final FlowController controller = mock(FlowController.class); + when(controller.getResource()).thenCallRealMethod(); + when(controller.getParentAuthorizable()).thenCallRealMethod(); + + // controller facade + final ControllerFacade controllerFacade = new ControllerFacade(); + controllerFacade.setFlowController(controller); + + serviceFacade = new StandardNiFiServiceFacade(); + serviceFacade.setAuditService(auditService); + serviceFacade.setAuthorizableLookup(authorizableLookup); + serviceFacade.setAuthorizer(authorizer); + serviceFacade.setEntityFactory(new EntityFactory()); + serviceFacade.setDtoFactory(new DtoFactory()); + serviceFacade.setControllerFacade(controllerFacade); + } + + private FlowChangeAction getAction(final Integer actionId, final String processorId) { + final FlowChangeAction action = new FlowChangeAction(); + action.setId(actionId); + action.setSourceId(processorId); + action.setSourceType(Component.Processor); + action.setOperation(Operation.Add); + return action; + } + + @Test(expected = ResourceNotFoundException.class) + public void testGetUnknownAction() throws Exception { + serviceFacade.getAction(UNKNOWN_ACTION_ID); + } + + @Test + public void testGetActionApprovedThroughAction() throws Exception { + // set the user + final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(new StandardNiFiUser(USER_1))); + SecurityContextHolder.getContext().setAuthentication(authentication); + + // get the action + final ActionEntity entity = serviceFacade.getAction(ACTION_ID_1); + + // verify + assertEquals(ACTION_ID_1, entity.getId()); + assertTrue(entity.getCanRead()); + + // resource exists and is approved, no need to check the controller + verify(authorizer, times(1)).authorize(argThat(new ArgumentMatcher<AuthorizationRequest>() { + @Override + public boolean matches(Object o) { + return ((AuthorizationRequest) o).getResource().getIdentifier().endsWith(PROCESSOR_ID_1); + } + })); + verify(authorizer, times(0)).authorize(argThat(new ArgumentMatcher<AuthorizationRequest>() { + @Override + public boolean matches(Object o) { + return ((AuthorizationRequest) o).getResource().equals(ResourceFactory.getControllerResource()); + } + })); + } + + @Test(expected = AccessDeniedException.class) + public void testGetActionDeniedDespiteControllerAccess() throws Exception { + // set the user + final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(new StandardNiFiUser(USER_2))); + SecurityContextHolder.getContext().setAuthentication(authentication); + + try { + // get the action + serviceFacade.getAction(ACTION_ID_1); + fail(); + } finally { + // resource exists, but should trigger access denied and will not check the controller + verify(authorizer, times(1)).authorize(argThat(new ArgumentMatcher<AuthorizationRequest>() { + @Override + public boolean matches(Object o) { + return ((AuthorizationRequest) o).getResource().getIdentifier().endsWith(PROCESSOR_ID_1); + } + })); + verify(authorizer, times(0)).authorize(argThat(new ArgumentMatcher<AuthorizationRequest>() { + @Override + public boolean matches(Object o) { + return ((AuthorizationRequest) o).getResource().equals(ResourceFactory.getControllerResource()); + } + })); + } + } + + @Test + public void testGetActionApprovedThroughController() throws Exception { + // set the user + final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(new StandardNiFiUser(USER_2))); + SecurityContextHolder.getContext().setAuthentication(authentication); + + // get the action + final ActionEntity entity = serviceFacade.getAction(ACTION_ID_2); + + // verify + assertEquals(ACTION_ID_2, entity.getId()); + assertTrue(entity.getCanRead()); + + // component does not exists, so only checks against the controller + verify(authorizer, times(0)).authorize(argThat(new ArgumentMatcher<AuthorizationRequest>() { + @Override + public boolean matches(Object o) { + return ((AuthorizationRequest) o).getResource().getIdentifier().endsWith(PROCESSOR_ID_2); + } + })); + verify(authorizer, times(1)).authorize(argThat(new ArgumentMatcher<AuthorizationRequest>() { + @Override + public boolean matches(Object o) { + return ((AuthorizationRequest) o).getResource().equals(ResourceFactory.getControllerResource()); + } + })); + } + + @Test + public void testGetActionsForUser1() throws Exception { + // set the user + final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(new StandardNiFiUser(USER_1))); + SecurityContextHolder.getContext().setAuthentication(authentication); + + final HistoryDTO dto = serviceFacade.getActions(new HistoryQueryDTO()); + + // verify user 1 only has access to actions for processor 1 + dto.getActions().forEach(action -> { + if (PROCESSOR_ID_1.equals(action.getSourceId())) { + assertTrue(action.getCanRead()); + } else if (PROCESSOR_ID_2.equals(action.getSourceId())) { + assertFalse(action.getCanRead()); + assertNull(action.getAction()); + } + }); + } + + @Test + public void testGetActionsForUser2() throws Exception { + // set the user + final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(new StandardNiFiUser(USER_2))); + SecurityContextHolder.getContext().setAuthentication(authentication); + + final HistoryDTO dto = serviceFacade.getActions(new HistoryQueryDTO()); + + // verify user 2 only has access to actions for processor 2 + dto.getActions().forEach(action -> { + if (PROCESSOR_ID_1.equals(action.getSourceId())) { + assertFalse(action.getCanRead()); + assertNull(action.getAction()); + } else if (PROCESSOR_ID_2.equals(action.getSourceId())) { + assertTrue(action.getCanRead()); + } + }); + } + +} \ No newline at end of file
