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

Reply via email to