You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by mc...@apache.org on 2016/04/07 22:23:31 UTC

[3/9] nifi git commit: Revert "NIFI-1551:"

http://git-wip-us.apache.org/repos/asf/nifi/blob/3f4ac315/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.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/AccessResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
index 9742011..5ec8d01 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
@@ -47,6 +47,7 @@ import org.apache.nifi.web.security.jwt.JwtService;
 import org.apache.nifi.web.security.kerberos.KerberosService;
 import org.apache.nifi.web.security.otp.OtpService;
 import org.apache.nifi.web.security.token.LoginAuthenticationToken;
+import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
 import org.apache.nifi.web.security.token.OtpAuthenticationToken;
 import org.apache.nifi.web.security.user.NiFiUserUtils;
 import org.apache.nifi.web.security.x509.X509CertificateExtractor;
@@ -58,6 +59,8 @@ import org.springframework.security.authentication.AccountStatusException;
 import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
+import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 
 import javax.servlet.http.HttpServletRequest;
@@ -100,6 +103,8 @@ public class AccessResource extends ApplicationResource {
 
     private KerberosService kerberosService;
 
+    private AuthenticationUserDetailsService<NiFiAuthorizationRequestToken> userDetailsService;
+
     /**
      * Retrieves the access configuration for this NiFi.
      *
@@ -206,12 +211,16 @@ public class AccessResource extends ApplicationResource {
                         // without a certificate, this is not a proxied request
                         final List<String> chain = Arrays.asList(principal);
 
-                        // TODO - ensure the proxy chain is authorized
-//                        final UserDetails userDetails = checkAuthorization(chain);
+                        // ensure the proxy chain is authorized
+                        final UserDetails userDetails = checkAuthorization(chain);
 
                         // no issues with authorization... verify authorities
                         accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
-                        accessStatus.setMessage("Your account is active and you are already logged in.");
+                        if (userDetails.getAuthorities().isEmpty()) {
+                            accessStatus.setMessage("Your account is active but currently does not have any level of access.");
+                        } else {
+                            accessStatus.setMessage("Your account is active and you are already logged in.");
+                        }
                     } catch (JwtException e) {
                         throw new InvalidAuthenticationException(e.getMessage(), e);
                     }
@@ -231,12 +240,16 @@ public class AccessResource extends ApplicationResource {
                     accessStatus.setIdentity(proxyChain.get(0));
                     accessStatus.setUsername(CertificateUtils.extractUsername(proxyChain.get(0)));
 
-                    // TODO - ensure the proxy chain is authorized
-//                    final UserDetails userDetails = checkAuthorization(proxyChain);
+                    // ensure the proxy chain is authorized
+                    final UserDetails userDetails = checkAuthorization(proxyChain);
 
                     // no issues with authorization... verify authorities
                     accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
-                    accessStatus.setMessage("Your account is active and you are already logged in.");
+                    if (userDetails.getAuthorities().isEmpty()) {
+                        accessStatus.setMessage("Your account is active but currently does not have any level of access.");
+                    } else {
+                        accessStatus.setMessage("Your account is active and you are already logged in.");
+                    }
                 } catch (final IllegalArgumentException iae) {
                     throw new InvalidAuthenticationException(iae.getMessage(), iae);
                 }
@@ -271,6 +284,16 @@ public class AccessResource extends ApplicationResource {
     }
 
     /**
+     * Checks the status of the proxy.
+     *
+     * @param proxyChain the proxy chain
+     * @throws AuthenticationException if the proxy chain is not authorized
+     */
+    private UserDetails checkAuthorization(final List<String> proxyChain) throws AuthenticationException {
+        return userDetailsService.loadUserDetails(new NiFiAuthorizationRequestToken(proxyChain));
+    }
+
+    /**
      * Creates a single use access token for downloading FlowFile content.
      *
      * @param httpServletRequest the servlet request
@@ -512,8 +535,8 @@ public class AccessResource extends ApplicationResource {
                 throw new IllegalArgumentException("Unable to determine the user from the incoming request.");
             }
 
-            // TODO - authorize the proxy if necessary
-//            authorizeProxyIfNecessary(proxyChain);
+            // authorize the proxy if necessary
+            authorizeProxyIfNecessary(proxyChain);
 
             // create the authentication token
             loginAuthenticationToken = new LoginAuthenticationToken(proxyChain.get(0), authenticationResponse.getExpiration(), authenticationResponse.getIssuer());
@@ -527,6 +550,30 @@ public class AccessResource extends ApplicationResource {
         return generateCreatedResponse(uri, token).build();
     }
 
+    /**
+     * Ensures the proxyChain is authorized before allowing the user to be authenticated.
+     *
+     * @param proxyChain the proxy chain
+     * @throws AuthenticationException if the proxy chain is not authorized
+     */
+    private void authorizeProxyIfNecessary(final List<String> proxyChain) throws AuthenticationException {
+        if (proxyChain.size() > 1) {
+            try {
+                userDetailsService.loadUserDetails(new NiFiAuthorizationRequestToken(proxyChain));
+            } catch (final UsernameNotFoundException unfe) {
+                // if a username not found exception was thrown, the proxies were authorized and now
+                // we can issue a new token to the end user which they will use to identify themselves
+                // when they enter a new account request
+            } catch (final AuthenticationServiceException ase) {
+                // throw an administration exception which will return a 500
+                throw new AdministrationException(ase.getMessage(), ase);
+            } catch (final Exception e) {
+                // any other issue we're going to treat as access denied exception which will return 403
+                throw new AccessDeniedException(e.getMessage(), e);
+            }
+        }
+    }
+
     private long validateTokenExpiration(long proposedTokenExpiration, String identity) {
         final long maxExpiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS);
         final long minExpiration = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES);
@@ -572,4 +619,9 @@ public class AccessResource extends ApplicationResource {
     public void setCertificateIdentityProvider(X509IdentityProvider certificateIdentityProvider) {
         this.certificateIdentityProvider = certificateIdentityProvider;
     }
+
+    public void setUserDetailsService(AuthenticationUserDetailsService<NiFiAuthorizationRequestToken> userDetailsService) {
+        this.userDetailsService = userDetailsService;
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/3f4ac315/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.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/ControllerResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
index 4fa0b3c..a3d0dc1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerResource.java
@@ -84,7 +84,6 @@ import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import java.net.URI;
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -122,6 +121,34 @@ public class ControllerResource extends ApplicationResource {
     }
 
     /**
+     * Locates the User sub-resource.
+     *
+     * @return the User sub-resource
+     */
+    @Path("/users")
+    @ApiOperation(
+            value = "Gets the user resource",
+            response = UserResource.class
+    )
+    public UserResource getUserResource() {
+        return resourceContext.getResource(UserResource.class);
+    }
+
+    /**
+     * Locates the User sub-resource.
+     *
+     * @return the User sub-resource
+     */
+    @Path("/user-groups")
+    @ApiOperation(
+            value = "Gets the user group resource",
+            response = UserGroupResource.class
+    )
+    public UserGroupResource getUserGroupResource() {
+        return resourceContext.getResource(UserGroupResource.class);
+    }
+
+    /**
      * Locates the History sub-resource.
      *
      * @return the History sub-resource
@@ -905,7 +932,7 @@ public class ControllerResource extends ApplicationResource {
         // create the response entity
         IdentityEntity entity = new IdentityEntity();
         entity.setRevision(revision);
-        entity.setUserId(user.getIdentity());
+        entity.setUserId(user.getId());
         entity.setIdentity(user.getUserName());
 
         // generate the response
@@ -963,8 +990,8 @@ public class ControllerResource extends ApplicationResource {
         // create the response entity
         AuthorityEntity entity = new AuthorityEntity();
         entity.setRevision(revision);
-        entity.setUserId(user.getIdentity());
-        entity.setAuthorities(new HashSet<>(Arrays.asList("ROLE_MONITOR", "ROLE_DFM", "ROLE_ADMIN", "ROLE_PROXY", "ROLE_NIFI", "ROLE_PROVENANCE")));
+        entity.setUserId(user.getId());
+        entity.setAuthorities(NiFiUserUtils.getAuthorities());
 
         // generate the response
         return clusterContext(generateOkResponse(entity)).build();

http://git-wip-us.apache.org/repos/asf/nifi/blob/3f4ac315/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/UserGroupResource.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/UserGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/UserGroupResource.java
new file mode 100644
index 0000000..3a0b596
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/UserGroupResource.java
@@ -0,0 +1,465 @@
+/*
+ * 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.api;
+
+import com.wordnik.swagger.annotations.Api;
+import com.wordnik.swagger.annotations.ApiOperation;
+import com.wordnik.swagger.annotations.ApiParam;
+import com.wordnik.swagger.annotations.ApiResponse;
+import com.wordnik.swagger.annotations.ApiResponses;
+import com.wordnik.swagger.annotations.Authorization;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import org.apache.nifi.cluster.manager.NodeResponse;
+import org.apache.nifi.cluster.manager.impl.WebClusterManager;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.web.api.entity.UserGroupEntity;
+import org.apache.nifi.web.api.request.ClientIdParameter;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.web.NiFiServiceFacade;
+import org.apache.nifi.web.api.dto.RevisionDTO;
+import org.apache.nifi.web.api.dto.UserGroupDTO;
+import org.springframework.security.access.prepost.PreAuthorize;
+
+/**
+ * RESTful endpoint for managing this Controller's user groups.
+ */
+@Api(hidden = true)
+public class UserGroupResource extends ApplicationResource {
+
+    /*
+     * Developer Note: Clustering assumes a centralized security provider. The
+     * cluster manager will manage user accounts when in clustered mode and
+     * interface with the authorization provider. However, when nodes perform
+     * Site-to-Site, the authorization details of the remote NiFi will be cached
+     * locally. These details need to be invalidated when certain actions are
+     * performed (revoking/deleting accounts, changing user authorities, user
+     * group, etc).
+     */
+    private WebClusterManager clusterManager;
+    private NiFiProperties properties;
+    private NiFiServiceFacade serviceFacade;
+
+    /**
+     * Updates a new user group.
+     *
+     * @param httpServletRequest request
+     * @param clientId Optional client id. If the client id is not specified, a
+     * new one will be generated. This value (whether specified or generated) is
+     * included in the response.
+     * @param userIds A collection of user ids to include in this group. If a
+     * user already belongs to another group, they will be placed in this group
+     * instead. Existing users in this group will remain in this group.
+     * @param group The name of the group.
+     * @param rawAuthorities Array of authorities to assign to the specified
+     * user.
+     * @param status The status of the specified users account.
+     * @param formParams form params
+     * @return A userGroupEntity.
+     */
+    @PUT
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
+    @Path("/{group}")
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    public Response updateUserGroup(
+            @Context HttpServletRequest httpServletRequest,
+            @PathParam("group") String group,
+            @FormParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) ClientIdParameter clientId,
+            @FormParam("userIds[]") Set<String> userIds,
+            @FormParam("authorities[]") Set<String> rawAuthorities,
+            @FormParam("status") String status,
+            MultivaluedMap<String, String> formParams) {
+
+        // get the collection of specified authorities
+        final Set<String> authorities = new HashSet<>();
+        for (String authority : rawAuthorities) {
+            if (StringUtils.isNotBlank(authority)) {
+                authorities.add(authority);
+            }
+        }
+
+        // create the user group dto
+        final UserGroupDTO userGroup = new UserGroupDTO();
+        userGroup.setGroup(group);
+        userGroup.setUserIds(userIds);
+        userGroup.setStatus(status);
+
+        // set the authorities
+        if (!authorities.isEmpty() || formParams.containsKey("authorities")) {
+            userGroup.setAuthorities(authorities);
+        }
+
+        // create the revision
+        final RevisionDTO revision = new RevisionDTO();
+        revision.setClientId(clientId.getClientId());
+
+        // create the user group entity
+        final UserGroupEntity entity = new UserGroupEntity();
+        entity.setRevision(revision);
+        entity.setUserGroup(userGroup);
+
+        // create the user group
+        return updateUserGroup(httpServletRequest, group, entity);
+    }
+
+    /**
+     * Creates a new user group with the specified users.
+     *
+     * @param httpServletRequest request
+     * @param group The user group.
+     * @param userGroupEntity A userGroupEntity.
+     * @return A userGroupEntity.
+     */
+    @PUT
+    @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
+    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
+    @Path("/{group}")
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    @ApiOperation(
+            value = "Updates a user group",
+            response = UserGroupEntity.class,
+            authorizations = {
+                @Authorization(value = "Administrator", type = "ROLE_ADMIN")
+            }
+    )
+    @ApiResponses(
+            value = {
+                @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                @ApiResponse(code = 401, message = "Client could not be authenticated."),
+                @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+                @ApiResponse(code = 404, message = "The specified resource could not be found."),
+                @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+            }
+    )
+    public Response updateUserGroup(
+            @Context HttpServletRequest httpServletRequest,
+            @ApiParam(
+                    value = "The name of the user group.",
+                    required = true
+            )
+            @PathParam("group") String group,
+            @ApiParam(
+                    value = "The user group configuration details.",
+                    required = true
+            )
+            UserGroupEntity userGroupEntity) {
+
+        if (userGroupEntity == null || userGroupEntity.getUserGroup() == null) {
+            throw new IllegalArgumentException("User group details must be specified.");
+        }
+
+        // get the user group
+        UserGroupDTO userGroup = userGroupEntity.getUserGroup();
+
+        // ensure the same id is being used
+        if (!group.equals(userGroup.getGroup())) {
+            throw new IllegalArgumentException(String.format("The user group (%s) in the request body does "
+                    + "not equal the user group of the requested resource (%s).", userGroup.getGroup(), group));
+        }
+
+        // the user group must be specified and cannot be blank
+        if (StringUtils.isBlank(userGroup.getGroup())) {
+            throw new IllegalArgumentException("User group must be specified and cannot be blank.");
+        }
+
+        // create the revision
+        final RevisionDTO revision = new RevisionDTO();
+        if (userGroupEntity.getRevision() == null) {
+            revision.setClientId(new ClientIdParameter().getClientId());
+        } else {
+            revision.setClientId(userGroupEntity.getRevision().getClientId());
+        }
+
+        // this user is being modified, replicate to the nodes to invalidate this account
+        // so that it will be re-authorized during the next attempted access - if this wasn't
+        // done the account would remain stale for up to the configured cache duration. this
+        // is acceptable sometimes but when updating a users authorities or groups via the UI
+        // they shouldn't have to wait for the changes to take effect`
+        if (properties.isClusterManager()) {
+            // change content type to JSON for serializing entity
+            final Map<String, String> headersToOverride = new HashMap<>();
+            headersToOverride.put("content-type", MediaType.APPLICATION_JSON);
+
+            // identify yourself as the NCM attempting to invalidate the user
+            final Map<String, String> headers = getHeaders(headersToOverride);
+            headers.put(WebClusterManager.CLUSTER_INVALIDATE_USER_GROUP_HEADER, Boolean.TRUE.toString());
+
+            final RevisionDTO invalidateUserRevision = new RevisionDTO();
+            revision.setClientId(revision.getClientId());
+
+            final UserGroupDTO invalidateUserGroup = new UserGroupDTO();
+            invalidateUserGroup.setGroup(group);
+            invalidateUserGroup.setUserIds(userGroup.getUserIds());
+
+            final UserGroupEntity invalidateUserGroupEntity = new UserGroupEntity();
+            invalidateUserGroupEntity.setRevision(invalidateUserRevision);
+            invalidateUserGroupEntity.setUserGroup(invalidateUserGroup);
+
+            // replicate the invalidate request to each node - if this request is not successful return that fact,
+            // otherwise continue with the desired user modification
+            final NodeResponse response = clusterManager.applyRequest(HttpMethod.PUT, getAbsolutePath(), invalidateUserGroupEntity, headers);
+            if (!response.is2xx()) {
+                return response.getResponse();
+            }
+        }
+
+        // handle expects request (usually from the cluster manager)
+        final String expects = httpServletRequest.getHeader(WebClusterManager.NCM_EXPECTS_HTTP_HEADER);
+        if (expects != null) {
+            return generateContinueResponse().build();
+        }
+
+        // handle an invalidate request from the NCM
+        final String invalidateRequest = httpServletRequest.getHeader(WebClusterManager.CLUSTER_INVALIDATE_USER_GROUP_HEADER);
+        if (invalidateRequest != null) {
+            serviceFacade.invalidateUserGroup(userGroup.getGroup(), userGroup.getUserIds());
+            return generateOkResponse().build();
+        }
+
+        // create the user group
+        userGroup = serviceFacade.updateUserGroup(userGroup);
+
+        // create the response entity
+        final UserGroupEntity entity = new UserGroupEntity();
+        entity.setRevision(revision);
+        entity.setUserGroup(userGroup);
+
+        // generate the URI for this group and return
+        return generateOkResponse(entity).build();
+    }
+
+    /**
+     * Deletes the user from the specified group. The user will not be removed,
+     * just the fact that they were in this group.
+     *
+     * @param httpServletRequest request
+     * @param group The user group.
+     * @param userId The user id to remove.
+     * @param clientId Optional client id. If the client id is not specified, a
+     * new one will be generated. This value (whether specified or generated) is
+     * included in the response.
+     * @return A userGroupEntity.
+     */
+    @DELETE
+    @Consumes(MediaType.WILDCARD)
+    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
+    @Path("/{group}/users/{userId}")
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    @ApiOperation(
+            value = "Removes a user from a user group",
+            notes = "Removes a user from a user group. The will not be deleted, jsut the fact that they were in this group.",
+            response = UserGroupEntity.class,
+            authorizations = {
+                @Authorization(value = "Administrator", type = "ROLE_ADMIN")
+            }
+    )
+    @ApiResponses(
+            value = {
+                @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                @ApiResponse(code = 401, message = "Client could not be authenticated."),
+                @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+                @ApiResponse(code = 404, message = "The specified resource could not be found."),
+                @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+            }
+    )
+    public Response removeUserFromGroup(
+            @Context HttpServletRequest httpServletRequest,
+            @ApiParam(
+                    value = "The name of the user group.",
+                    required = true
+            )
+            @PathParam("group") String group,
+            @ApiParam(
+                    value = "The id of the user to remove from the user group.",
+                    required = true
+            )
+            @PathParam("userId") String userId,
+            @ApiParam(
+                    value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.",
+                    required = false
+            )
+            @QueryParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) ClientIdParameter clientId) {
+
+        // this user is being modified, replicate to the nodes to invalidate this account
+        // so that it will be re-authorized during the next attempted access - if this wasn't
+        // done the account would remain stale for up to the configured cache duration. this
+        // is acceptable sometimes but when removing a user via the UI they shouldn't have to
+        // wait for the changes to take effect
+        if (properties.isClusterManager()) {
+            // identify yourself as the NCM attempting to invalidate the user
+            final Map<String, String> headers = getHeaders();
+            headers.put(WebClusterManager.CLUSTER_INVALIDATE_USER_HEADER, Boolean.TRUE.toString());
+
+            // replicate the invalidate request to each node - if this request is not successful return that fact,
+            // otherwise continue with the desired user modification
+            final NodeResponse response = clusterManager.applyRequest(HttpMethod.DELETE, getAbsolutePath(), getRequestParameters(true), headers);
+            if (!response.is2xx()) {
+                return response.getResponse();
+            }
+        }
+
+        // handle expects request (usually from the cluster manager)
+        final String expects = httpServletRequest.getHeader(WebClusterManager.NCM_EXPECTS_HTTP_HEADER);
+        if (expects != null) {
+            return generateContinueResponse().build();
+        }
+
+        // handle an invalidate request from the NCM
+        final String invalidateRequest = httpServletRequest.getHeader(WebClusterManager.CLUSTER_INVALIDATE_USER_HEADER);
+        if (invalidateRequest != null) {
+            serviceFacade.invalidateUser(userId);
+            return generateOkResponse().build();
+        }
+
+        // ungroup the specified user
+        serviceFacade.removeUserFromGroup(userId);
+
+        // create the revision
+        final RevisionDTO revision = new RevisionDTO();
+        revision.setClientId(clientId.getClientId());
+
+        // create the response entity
+        final UserGroupEntity entity = new UserGroupEntity();
+        entity.setRevision(revision);
+
+        // generate ok response
+        return generateOkResponse(entity).build();
+    }
+
+    /**
+     * Deletes the user group. The users will not be removed, just the fact that
+     * they were grouped.
+     *
+     * @param httpServletRequest request
+     * @param group The user group.
+     * @param clientId Optional client id. If the client id is not specified, a
+     * new one will be generated. This value (whether specified or generated) is
+     * included in the response.
+     * @return A userGroupEntity.
+     */
+    @DELETE
+    @Consumes(MediaType.WILDCARD)
+    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
+    @Path("/{group}")
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    @ApiOperation(
+            value = "Deletes a user group",
+            notes = "Deletes a user group. The users will not be removed, just the fact that they were grouped.",
+            response = UserGroupEntity.class,
+            authorizations = {
+                @Authorization(value = "Administrator", type = "ROLE_ADMIN")
+            }
+    )
+    @ApiResponses(
+            value = {
+                @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                @ApiResponse(code = 401, message = "Client could not be authenticated."),
+                @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+                @ApiResponse(code = 404, message = "The specified resource could not be found."),
+                @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+            }
+    )
+    public Response ungroup(
+            @Context HttpServletRequest httpServletRequest,
+            @ApiParam(
+                    value = "The name of the user group.",
+                    required = true
+            )
+            @PathParam("group") String group,
+            @ApiParam(
+                    value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.",
+                    required = false
+            )
+            @QueryParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) ClientIdParameter clientId) {
+
+        // this user is being modified, replicate to the nodes to invalidate this account
+        // so that it will be re-authorized during the next attempted access - if this wasn't
+        // done the account would remain stale for up to the configured cache duration. this
+        // is acceptable sometimes but when removing a user via the UI they shouldn't have to
+        // wait for the changes to take effect
+        if (properties.isClusterManager()) {
+            // identify yourself as the NCM attempting to invalidate the user
+            final Map<String, String> headers = getHeaders();
+            headers.put(WebClusterManager.CLUSTER_INVALIDATE_USER_GROUP_HEADER, Boolean.TRUE.toString());
+
+            // replicate the invalidate request to each node - if this request is not successful return that fact,
+            // otherwise continue with the desired user modification
+            final NodeResponse response = clusterManager.applyRequest(HttpMethod.DELETE, getAbsolutePath(), getRequestParameters(true), headers);
+            if (!response.is2xx()) {
+                return response.getResponse();
+            }
+        }
+
+        // handle expects request (usually from the cluster manager)
+        final String expects = httpServletRequest.getHeader(WebClusterManager.NCM_EXPECTS_HTTP_HEADER);
+        if (expects != null) {
+            return generateContinueResponse().build();
+        }
+
+        // handle an invalidate request from the NCM
+        final String invalidateRequest = httpServletRequest.getHeader(WebClusterManager.CLUSTER_INVALIDATE_USER_GROUP_HEADER);
+        if (invalidateRequest != null) {
+            serviceFacade.invalidateUserGroup(group, null);
+            return generateOkResponse().build();
+        }
+
+        // delete the user group
+        serviceFacade.removeUserGroup(group);
+
+        // create the revision
+        final RevisionDTO revision = new RevisionDTO();
+        revision.setClientId(clientId.getClientId());
+
+        // create the response entity
+        final UserGroupEntity entity = new UserGroupEntity();
+        entity.setRevision(revision);
+
+        // generate ok response
+        return generateOkResponse(entity).build();
+    }
+
+    /* setters */
+    public void setServiceFacade(NiFiServiceFacade serviceFacade) {
+        this.serviceFacade = serviceFacade;
+    }
+
+    public void setProperties(NiFiProperties properties) {
+        this.properties = properties;
+    }
+
+    public void setClusterManager(WebClusterManager clusterManager) {
+        this.clusterManager = clusterManager;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/3f4ac315/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/UserResource.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/UserResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/UserResource.java
new file mode 100644
index 0000000..1426999
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/UserResource.java
@@ -0,0 +1,617 @@
+/*
+ * 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.api;
+
+import com.sun.jersey.api.Responses;
+import com.wordnik.swagger.annotations.Api;
+import com.wordnik.swagger.annotations.ApiOperation;
+import com.wordnik.swagger.annotations.ApiParam;
+import com.wordnik.swagger.annotations.ApiResponse;
+import com.wordnik.swagger.annotations.ApiResponses;
+import com.wordnik.swagger.annotations.Authorization;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.GET;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import org.apache.nifi.cluster.manager.NodeResponse;
+import org.apache.nifi.cluster.manager.impl.WebClusterManager;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.web.api.dto.UserDTO;
+import org.apache.nifi.web.api.dto.search.UserGroupSearchResultDTO;
+import org.apache.nifi.web.api.dto.search.UserSearchResultDTO;
+import org.apache.nifi.web.api.entity.UserEntity;
+import org.apache.nifi.web.api.entity.UserSearchResultsEntity;
+import org.apache.nifi.web.api.entity.UsersEntity;
+import org.apache.nifi.web.api.request.ClientIdParameter;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.user.NiFiUser;
+import org.apache.nifi.web.NiFiServiceFacade;
+import static org.apache.nifi.web.api.ApplicationResource.CLIENT_ID;
+import org.apache.nifi.web.api.dto.RevisionDTO;
+import org.apache.nifi.web.security.user.NiFiUserUtils;
+import org.springframework.security.access.prepost.PreAuthorize;
+
+/**
+ * RESTful endpoint for managing this Controller's users.
+ */
+@Api(hidden = true)
+public class UserResource extends ApplicationResource {
+
+    /*
+     * Developer Note: Clustering assumes a centralized security provider. The
+     * cluster manager will manage user accounts when in clustered mode and
+     * interface with the authorization provider. However, when nodes perform
+     * Site-to-Site, the authorization details of the remote NiFi will be cached
+     * locally. These details need to be invalidated when certain actions are
+     * performed (revoking/deleting accounts, changing user authorities, user
+     * group, etc).
+     */
+    private WebClusterManager clusterManager;
+    private NiFiProperties properties;
+    private NiFiServiceFacade serviceFacade;
+
+    /**
+     * Creates a new user account request.
+     *
+     * @return A string
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    @Produces(MediaType.TEXT_PLAIN)
+    @Path("") // necessary due to a bug in swagger
+    @ApiOperation(
+            value = "Creates a user",
+            response = String.class
+    )
+    public Response createUser() {
+        if (!properties.getSupportNewAccountRequests()) {
+            return Responses.notFound().entity("This NiFi does not support new account requests.").build();
+        }
+
+        final NiFiUser nifiUser = NiFiUserUtils.getNiFiUser();
+        if (nifiUser != null) {
+            throw new IllegalArgumentException("User account already created " + nifiUser.getIdentity());
+        }
+
+        // create an account request for the current user
+        final UserDTO user = serviceFacade.createUser();
+
+        final String uri = generateResourceUri("controller", "users", user.getId());
+        return generateCreatedResponse(URI.create(uri), "Not authorized. User account created. Authorization pending.").build();
+    }
+
+    /**
+     * Gets all users that are registered within this Controller.
+     *
+     * @param clientId Optional client id. If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response.
+     * @param grouped Whether to return the users in their groups.
+     * @return A usersEntity.
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
+    @Path("") // necessary due to a bug in swagger
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    @ApiOperation(
+            value = "Gets all users",
+            response = UsersEntity.class,
+            authorizations = {
+                @Authorization(value = "Administrator", type = "ROLE_ADMIN")
+            }
+    )
+    @ApiResponses(
+            value = {
+                @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                @ApiResponse(code = 401, message = "Client could not be authenticated."),
+                @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+                @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+            }
+    )
+    public Response getUsers(
+            @ApiParam(
+                    value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.",
+                    required = false
+            )
+            @QueryParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) ClientIdParameter clientId,
+            @ApiParam(
+                    value = "Whether to return the users in their respective groups.",
+                    required = false
+            )
+            @QueryParam("grouped") @DefaultValue("false") Boolean grouped) {
+
+        // get the users
+        final Collection<UserDTO> users = serviceFacade.getUsers(grouped);
+
+        // create the revision
+        final RevisionDTO revision = new RevisionDTO();
+        revision.setClientId(clientId.getClientId());
+
+        // create the response entity
+        final UsersEntity usersEntity = new UsersEntity();
+        usersEntity.setRevision(revision);
+        usersEntity.setUsers(users);
+        usersEntity.setGenerated(new Date());
+
+        // build the response
+        return generateOkResponse(usersEntity).build();
+    }
+
+    /**
+     * Gets the details for the specified user.
+     *
+     * @param clientId Optional client id. If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response.
+     * @param id The user id.
+     * @return A userEntity.
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    @Path("/{id}")
+    @ApiOperation(
+            value = "Gets a user",
+            response = UserEntity.class,
+            authorizations = {
+                @Authorization(value = "Administrator", type = "ROLE_ADMIN")
+            }
+    )
+    @ApiResponses(
+            value = {
+                @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                @ApiResponse(code = 401, message = "Client could not be authenticated."),
+                @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+                @ApiResponse(code = 404, message = "The specified resource could not be found."),
+                @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+            }
+    )
+    public Response getUser(
+            @ApiParam(
+                    value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.",
+                    required = false
+            )
+            @QueryParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) ClientIdParameter clientId,
+            @ApiParam(
+                    value = "The user id.",
+                    required = true
+            )
+            @PathParam("id") String id) {
+
+        // get the specified user
+        final UserDTO userDTO = serviceFacade.getUser(id);
+
+        // create the revision
+        final RevisionDTO revision = new RevisionDTO();
+        revision.setClientId(clientId.getClientId());
+
+        // create the response entity
+        final UserEntity userEntity = new UserEntity();
+        userEntity.setRevision(revision);
+        userEntity.setUser(userDTO);
+
+        // build the response
+        return generateOkResponse(userEntity).build();
+    }
+
+    /**
+     * Searches for users with match the specified query.
+     *
+     * @param value Search value that will be matched against users
+     * @return A userSearchResultsEntity
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
+    @Path("/search-results")
+    @PreAuthorize("hasAnyRole('ROLE_DFM', 'ROLE_ADMIN')")
+    @ApiOperation(
+            value = "Searches for users",
+            response = UserSearchResultsEntity.class,
+            authorizations = {
+                @Authorization(value = "Data Flow Manager", type = "ROLE_DFM"),
+                @Authorization(value = "Administrator", type = "ROLE_ADMIN")
+            }
+    )
+    @ApiResponses(
+            value = {
+                @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                @ApiResponse(code = 401, message = "Client could not be authenticated."),
+                @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+                @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+            }
+    )
+    public Response searchUsers(
+            @ApiParam(
+                    value = "The search terms.",
+                    required = true
+            )
+            @QueryParam("q") @DefaultValue(StringUtils.EMPTY) String value) {
+
+        final List<UserSearchResultDTO> userMatches = new ArrayList<>();
+        final List<UserGroupSearchResultDTO> userGroupMatches = new ArrayList<>();
+
+        // get the users
+        final Collection<UserDTO> users = serviceFacade.getUsers(Boolean.FALSE);
+        final Collection<String> matchedGroups = new HashSet<>();
+
+        // check each to see if it matches the search term
+        for (UserDTO user : users) {
+            // count the user if there is no search or it matches the address
+            if (StringUtils.isBlank(value)) {
+                // record the group match if there is one and it hasn't already been encountered
+                if (user.getUserGroup() != null && !matchedGroups.contains(user.getUserGroup())) {
+                    // add the matched group
+                    matchedGroups.add(user.getUserGroup());
+
+                    // record the group match
+                    final UserGroupSearchResultDTO userGroupMatch = new UserGroupSearchResultDTO();
+                    userGroupMatch.setGroup(user.getUserGroup());
+                    userGroupMatches.add(userGroupMatch);
+                }
+
+                // record the user match
+                final UserSearchResultDTO userMatch = new UserSearchResultDTO();
+                userMatch.setUserDn(user.getDn());
+                userMatch.setUserName(user.getUserName());
+                userMatches.add(userMatch);
+            } else {
+                // look for a user match
+                if (StringUtils.containsIgnoreCase(user.getDn(), value) || StringUtils.containsIgnoreCase(user.getUserName(), value)) {
+                    // record the user match
+                    final UserSearchResultDTO userMatch = new UserSearchResultDTO();
+                    userMatch.setUserDn(user.getDn());
+                    userMatch.setUserName(user.getUserName());
+                    userMatches.add(userMatch);
+                }
+
+                // look for a dn match
+                if (StringUtils.containsIgnoreCase(user.getUserGroup(), value)) {
+                    // record the group match if it hasn't already been encountered
+                    if (!matchedGroups.contains(user.getUserGroup())) {
+                        // add the matched group
+                        matchedGroups.add(user.getUserGroup());
+
+                        // record the group match
+                        final UserGroupSearchResultDTO userGroupMatch = new UserGroupSearchResultDTO();
+                        userGroupMatch.setGroup(user.getUserGroup());
+                        userGroupMatches.add(userGroupMatch);
+                    }
+                }
+            }
+        }
+
+        // sort the user matches
+        Collections.sort(userMatches, new Comparator<UserSearchResultDTO>() {
+            @Override
+            public int compare(UserSearchResultDTO user1, UserSearchResultDTO user2) {
+                return user1.getUserName().compareTo(user2.getUserName());
+            }
+        });
+
+        // sort the user group matches
+        Collections.sort(userGroupMatches, new Comparator<UserGroupSearchResultDTO>() {
+            @Override
+            public int compare(UserGroupSearchResultDTO userGroup1, UserGroupSearchResultDTO userGroup2) {
+                return userGroup1.getGroup().compareTo(userGroup2.getGroup());
+            }
+        });
+
+        // build the response
+        final UserSearchResultsEntity results = new UserSearchResultsEntity();
+        results.setUserResults(userMatches);
+        results.setUserGroupResults(userGroupMatches);
+
+        // generate an 200 - OK response
+        return noCache(Response.ok(results)).build();
+    }
+
+    /**
+     * Updates the specified user.
+     *
+     * @param httpServletRequest request
+     * @param clientId Optional client id. If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response.
+     * @param id The id of the user to update.
+     * @param rawAuthorities Array of authorities to assign to the specified user.
+     * @param status The status of the specified users account.
+     * @param formParams form params
+     * @return A userEntity
+     */
+    @PUT
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    @Path("/{id}")
+    public Response updateUser(
+            @Context HttpServletRequest httpServletRequest,
+            @FormParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) ClientIdParameter clientId,
+            @PathParam("id") String id,
+            @FormParam("authorities[]") Set<String> rawAuthorities,
+            @FormParam("status") String status,
+            MultivaluedMap<String, String> formParams) {
+
+        // create the user
+        final UserDTO userDTO = new UserDTO();
+        userDTO.setId(id);
+        userDTO.setStatus(status);
+
+        // get the collection of specified authorities
+        final Set<String> authorities = new HashSet<>();
+        for (String authority : rawAuthorities) {
+            if (StringUtils.isNotBlank(authority)) {
+                authorities.add(authority);
+            }
+        }
+
+        // set the authorities
+        if (!authorities.isEmpty() || formParams.containsKey("authorities")) {
+            userDTO.setAuthorities(authorities);
+        }
+
+        // create the revision
+        final RevisionDTO revision = new RevisionDTO();
+        revision.setClientId(clientId.getClientId());
+
+        // create the user entity
+        UserEntity userEntity = new UserEntity();
+        userEntity.setRevision(revision);
+        userEntity.setUser(userDTO);
+
+        // update the user
+        return updateUser(httpServletRequest, id, userEntity);
+    }
+
+    /**
+     * Updates the specified user.
+     *
+     * @param httpServletRequest request
+     * @param id The id of the user to update.
+     * @param userEntity A userEntity
+     * @return A userEntity
+     */
+    @PUT
+    @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
+    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    @Path("/{id}")
+    @ApiOperation(
+            value = "Updates a user",
+            response = UserEntity.class,
+            authorizations = {
+                @Authorization(value = "Administrator", type = "ROLE_ADMIN")
+            }
+    )
+    @ApiResponses(
+            value = {
+                @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                @ApiResponse(code = 401, message = "Client could not be authenticated."),
+                @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+                @ApiResponse(code = 404, message = "The specified resource could not be found."),
+                @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+            }
+    )
+    public Response updateUser(
+            @Context HttpServletRequest httpServletRequest,
+            @ApiParam(
+                    value = "The user id.",
+                    required = true
+            )
+            @PathParam("id") String id,
+            @ApiParam(
+                    value = "The user configuration details.",
+                    required = true
+            ) UserEntity userEntity) {
+
+        if (userEntity == null || userEntity.getUser() == null) {
+            throw new IllegalArgumentException("User details must be specified.");
+        }
+
+        // ensure the same user id is being used
+        final UserDTO userDTO = userEntity.getUser();
+        if (!id.equals(userDTO.getId())) {
+            throw new IllegalArgumentException(String.format("The user id (%s) in the request body does "
+                    + "not equal the user id of the requested resource (%s).", userDTO.getId(), id));
+        }
+
+        // create the revision
+        final RevisionDTO revision = new RevisionDTO();
+        if (userEntity.getRevision() == null) {
+            revision.setClientId(new ClientIdParameter().getClientId());
+        } else {
+            revision.setClientId(userEntity.getRevision().getClientId());
+        }
+
+        // this user is being modified, replicate to the nodes to invalidate this account
+        // so that it will be re-authorized during the next attempted access - if this wasn't
+        // done the account would remain stale for up to the configured cache duration. this
+        // is acceptable sometimes but when updating a users authorities or groups via the UI
+        // they shouldn't have to wait for the changes to take effect`
+        if (properties.isClusterManager()) {
+            // change content type to JSON for serializing entity
+            final Map<String, String> headersToOverride = new HashMap<>();
+            headersToOverride.put("content-type", MediaType.APPLICATION_JSON);
+
+            // identify yourself as the NCM attempting to invalidate the user
+            final Map<String, String> headers = getHeaders(headersToOverride);
+            headers.put(WebClusterManager.CLUSTER_INVALIDATE_USER_HEADER, Boolean.TRUE.toString());
+
+            final RevisionDTO invalidateUserRevision = new RevisionDTO();
+            revision.setClientId(revision.getClientId());
+
+            final UserDTO invalidateUser = new UserDTO();
+            invalidateUser.setId(userDTO.getId());
+
+            final UserEntity invalidateUserEntity = new UserEntity();
+            invalidateUserEntity.setRevision(invalidateUserRevision);
+            invalidateUserEntity.setUser(userDTO);
+
+            // replicate the invalidate request to each node - if this request is not successful return that fact,
+            // otherwise continue with the desired user modification
+            final NodeResponse response = clusterManager.applyRequest(HttpMethod.PUT, getAbsolutePath(), invalidateUserEntity, headers);
+            if (!response.is2xx()) {
+                return response.getResponse();
+            }
+        }
+
+        // handle expects request (usually from the cluster manager)
+        final String expects = httpServletRequest.getHeader(WebClusterManager.NCM_EXPECTS_HTTP_HEADER);
+        if (expects != null) {
+            return generateContinueResponse().build();
+        }
+
+        // handle an invalidate request from the NCM
+        final String invalidateRequest = httpServletRequest.getHeader(WebClusterManager.CLUSTER_INVALIDATE_USER_HEADER);
+        if (invalidateRequest != null) {
+            serviceFacade.invalidateUser(id);
+            return generateOkResponse().build();
+        }
+
+        // update the user
+        final UserDTO reponseUserDTO = serviceFacade.updateUser(userDTO);
+
+        // create the response entity
+        UserEntity responseUserEntity = new UserEntity();
+        responseUserEntity.setRevision(revision);
+        responseUserEntity.setUser(reponseUserDTO);
+
+        // build the response
+        return generateOkResponse(responseUserEntity).build();
+    }
+
+    /**
+     * Deletes the specified user.
+     *
+     * @param httpServletRequest request
+     * @param id The user id
+     * @param clientId Optional client id. If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response.
+     * @return A userEntity.
+     */
+    @DELETE
+    @Consumes(MediaType.WILDCARD)
+    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
+    @Path("/{id}")
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    @ApiOperation(
+            value = "Deletes a user",
+            response = UserEntity.class,
+            authorizations = {
+                @Authorization(value = "Administrator", type = "ROLE_ADMIN")
+            }
+    )
+    @ApiResponses(
+            value = {
+                @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
+                @ApiResponse(code = 401, message = "Client could not be authenticated."),
+                @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+                @ApiResponse(code = 404, message = "The specified resource could not be found."),
+                @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
+            }
+    )
+    public Response deleteUser(
+            @Context HttpServletRequest httpServletRequest,
+            @ApiParam(
+                    value = "The user id.",
+                    required = true
+            )
+            @PathParam("id") String id,
+            @ApiParam(
+                    value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.",
+                    required = false
+            )
+            @QueryParam(CLIENT_ID) @DefaultValue(StringUtils.EMPTY) ClientIdParameter clientId) {
+
+        // this user is being modified, replicate to the nodes to invalidate this account
+        // so that it will be re-authorized during the next attempted access - if this wasn't
+        // done the account would remain stale for up to the configured cache duration. this
+        // is acceptable sometimes but when removing a user via the UI they shouldn't have to
+        // wait for the changes to take effect
+        if (properties.isClusterManager()) {
+            // identify yourself as the NCM attempting to invalidate the user
+            final Map<String, String> headers = getHeaders();
+            headers.put(WebClusterManager.CLUSTER_INVALIDATE_USER_HEADER, Boolean.TRUE.toString());
+
+            // replicate the invalidate request to each node - if this request is not successful return that fact,
+            // otherwise continue with the desired user modification
+            final NodeResponse response = clusterManager.applyRequest(HttpMethod.DELETE, getAbsolutePath(), getRequestParameters(true), headers);
+            if (!response.is2xx()) {
+                return response.getResponse();
+            }
+        }
+
+        // handle expects request (usually from the cluster manager)
+        final String expects = httpServletRequest.getHeader(WebClusterManager.NCM_EXPECTS_HTTP_HEADER);
+        if (expects != null) {
+            return generateContinueResponse().build();
+        }
+
+        // handle an invalidate request from the NCM
+        final String invalidateRequest = httpServletRequest.getHeader(WebClusterManager.CLUSTER_INVALIDATE_USER_HEADER);
+        if (invalidateRequest != null) {
+            serviceFacade.invalidateUser(id);
+            return generateOkResponse().build();
+        }
+
+        // ungroup the specified user
+        serviceFacade.deleteUser(id);
+
+        // create the revision
+        final RevisionDTO revision = new RevisionDTO();
+        revision.setClientId(clientId.getClientId());
+
+        // create the response entity
+        final UserEntity entity = new UserEntity();
+        entity.setRevision(revision);
+
+        // generate ok response
+        return generateOkResponse(entity).build();
+    }
+
+    /* setters */
+    public void setServiceFacade(NiFiServiceFacade serviceFacade) {
+        this.serviceFacade = serviceFacade;
+    }
+
+    public void setProperties(NiFiProperties properties) {
+        this.properties = properties;
+    }
+
+    public void setClusterManager(WebClusterManager clusterManager) {
+        this.clusterManager = clusterManager;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/3f4ac315/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/config/AccountNotFoundExceptionMapper.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/config/AccountNotFoundExceptionMapper.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/config/AccountNotFoundExceptionMapper.java
new file mode 100644
index 0000000..8fed1a2
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/config/AccountNotFoundExceptionMapper.java
@@ -0,0 +1,47 @@
+/*
+ * 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.api.config;
+
+import com.sun.jersey.api.Responses;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+import org.apache.nifi.admin.service.AccountNotFoundException;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Maps resource not found exceptions into client responses.
+ */
+@Provider
+public class AccountNotFoundExceptionMapper implements ExceptionMapper<AccountNotFoundException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(AccountNotFoundExceptionMapper.class);
+
+    @Override
+    public Response toResponse(AccountNotFoundException exception) {
+        logger.info(String.format("%s. Returning %s response.", exception, Response.Status.NOT_FOUND));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        return Responses.notFound().entity(exception.getMessage()).type("text/plain").build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/3f4ac315/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.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/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 0ae7649..5e7a902 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -16,6 +16,29 @@
  */
 package org.apache.nifi.web.api.dto;
 
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+
+import javax.ws.rs.WebApplicationException;
+
 import org.apache.nifi.action.Action;
 import org.apache.nifi.action.component.details.ComponentDetails;
 import org.apache.nifi.action.component.details.ExtensionDetails;
@@ -34,6 +57,7 @@ import org.apache.nifi.action.details.PurgeDetails;
 import org.apache.nifi.annotation.behavior.Stateful;
 import org.apache.nifi.annotation.documentation.CapabilityDescription;
 import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.authorization.Authority;
 import org.apache.nifi.cluster.HeartbeatPayload;
 import org.apache.nifi.cluster.event.Event;
 import org.apache.nifi.cluster.manager.StatusMerger;
@@ -98,6 +122,8 @@ import org.apache.nifi.reporting.Bulletin;
 import org.apache.nifi.reporting.BulletinRepository;
 import org.apache.nifi.reporting.ReportingTask;
 import org.apache.nifi.scheduling.SchedulingStrategy;
+import org.apache.nifi.user.NiFiUser;
+import org.apache.nifi.user.NiFiUserGroup;
 import org.apache.nifi.util.FormatUtils;
 import org.apache.nifi.web.FlowModification;
 import org.apache.nifi.web.Revision;
@@ -129,28 +155,6 @@ import org.apache.nifi.web.api.dto.status.ProcessorStatusSnapshotDTO;
 import org.apache.nifi.web.api.dto.status.RemoteProcessGroupStatusDTO;
 import org.apache.nifi.web.api.dto.status.RemoteProcessGroupStatusSnapshotDTO;
 
-import javax.ws.rs.WebApplicationException;
-import java.text.Collator;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.concurrent.TimeUnit;
-
 public final class DtoFactory {
 
     @SuppressWarnings("rawtypes")
@@ -2530,6 +2534,57 @@ public final class DtoFactory {
         return revisionDTO;
     }
 
+    /**
+     * Factory method for creating a new user transfer object.
+     *
+     * @param user user
+     * @return dto
+     */
+    public UserDTO createUserDTO(NiFiUser user) {
+        // convert the users authorities
+        Set<String> authorities = Authority.convertAuthorities(user.getAuthorities());
+
+        // create the user
+        UserDTO userDTO = new UserDTO();
+        userDTO.setId(String.valueOf(user.getId()));
+        userDTO.setDn(user.getIdentity());
+        userDTO.setUserName(user.getUserName());
+        userDTO.setUserGroup(user.getUserGroup());
+        userDTO.setJustification(user.getJustification());
+        userDTO.setAuthorities(authorities);
+
+        // ensure the date fields are not null
+        if (user.getCreation() != null) {
+            userDTO.setCreation(user.getCreation());
+        }
+        if (user.getLastAccessed() != null) {
+            userDTO.setLastAccessed(user.getLastAccessed());
+        }
+        if (user.getLastVerified() != null) {
+            userDTO.setLastVerified(user.getLastVerified());
+        }
+        if (user.getStatus() != null) {
+            userDTO.setStatus(user.getStatus().toString());
+        }
+
+        return userDTO;
+    }
+
+    public UserGroupDTO createUserGroupDTO(NiFiUserGroup userGroup) {
+        UserGroupDTO userGroupDto = new UserGroupDTO();
+        userGroupDto.setGroup(userGroup.getGroup());
+        userGroupDto.setUserIds(new HashSet<String>());
+
+        // set the users if they have been specified
+        if (userGroup.getUsers() != null) {
+            for (NiFiUser user : userGroup.getUsers()) {
+                userGroupDto.getUserIds().add(String.valueOf(user.getId()));
+            }
+        }
+
+        return userGroupDto;
+    }
+
     public NodeDTO createNodeDTO(Node node, List<Event> events, boolean primary) {
 
         final NodeDTO nodeDto = new NodeDTO();

http://git-wip-us.apache.org/repos/asf/nifi/blob/3f4ac315/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
index 1f2beaf..68d0dbe 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
@@ -20,6 +20,7 @@ import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.admin.service.UserService;
+import org.apache.nifi.authorization.DownloadAuthorization;
 import org.apache.nifi.cluster.protocol.NodeIdentifier;
 import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.connectable.Connectable;
@@ -103,6 +104,7 @@ import org.apache.nifi.web.security.ProxiedEntitiesUtils;
 import org.apache.nifi.web.security.user.NiFiUserUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.security.access.AccessDeniedException;
 
 import javax.ws.rs.WebApplicationException;
 import java.io.IOException;
@@ -947,11 +949,11 @@ public class ControllerFacade {
             // calculate the dn chain
             final List<String> dnChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(user);
 
-            // TODO - ensure the users in this chain are allowed to download this content
-//            final DownloadAuthorization downloadAuthorization = userService.authorizeDownload(dnChain, attributes);
-//            if (!downloadAuthorization.isApproved()) {
-//                throw new AccessDeniedException(downloadAuthorization.getExplanation());
-//            }
+            // ensure the users in this chain are allowed to download this content
+            final DownloadAuthorization downloadAuthorization = userService.authorizeDownload(dnChain, attributes);
+            if (!downloadAuthorization.isApproved()) {
+                throw new AccessDeniedException(downloadAuthorization.getExplanation());
+            }
 
             // get the filename and fall back to the identifier (should never happen)
             String filename = attributes.get(CoreAttributes.FILENAME.key());

http://git-wip-us.apache.org/repos/asf/nifi/blob/3f4ac315/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardConnectionDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardConnectionDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardConnectionDAO.java
index 5f0a70c..e1faa14 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardConnectionDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardConnectionDAO.java
@@ -17,6 +17,7 @@
 package org.apache.nifi.web.dao.impl;
 
 import org.apache.nifi.admin.service.UserService;
+import org.apache.nifi.authorization.DownloadAuthorization;
 import org.apache.nifi.connectable.Connectable;
 import org.apache.nifi.connectable.ConnectableType;
 import org.apache.nifi.connectable.Connection;
@@ -47,6 +48,7 @@ import org.apache.nifi.web.security.ProxiedEntitiesUtils;
 import org.apache.nifi.web.security.user.NiFiUserUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.security.access.AccessDeniedException;
 
 import javax.ws.rs.WebApplicationException;
 import java.io.IOException;
@@ -608,12 +610,12 @@ public class StandardConnectionDAO extends ComponentDAO implements ConnectionDAO
             // calculate the dn chain
             final List<String> dnChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(user);
 
-            // TODO - ensure the users in this chain are allowed to download this content
+            // ensure the users in this chain are allowed to download this content
             final Map<String, String> attributes = flowFile.getAttributes();
-//            final DownloadAuthorization downloadAuthorization = userService.authorizeDownload(dnChain, attributes);
-//            if (!downloadAuthorization.isApproved()) {
-//                throw new AccessDeniedException(downloadAuthorization.getExplanation());
-//            }
+            final DownloadAuthorization downloadAuthorization = userService.authorizeDownload(dnChain, attributes);
+            if (!downloadAuthorization.isApproved()) {
+                throw new AccessDeniedException(downloadAuthorization.getExplanation());
+            }
 
             // get the filename and fall back to the identifier (should never happen)
             String filename = attributes.get(CoreAttributes.FILENAME.key());

http://git-wip-us.apache.org/repos/asf/nifi/blob/3f4ac315/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
index 555107f..6c2165f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
@@ -234,6 +234,16 @@
         <property name="properties" ref="nifiProperties"/>
         <property name="clusterManager" ref="clusterManager"/>
     </bean>
+    <bean id="userResource" class="org.apache.nifi.web.api.UserResource" scope="singleton">
+        <property name="serviceFacade" ref="serviceFacade"/>
+        <property name="properties" ref="nifiProperties"/>
+        <property name="clusterManager" ref="clusterManager"/>
+    </bean>
+    <bean id="userGroupResource" class="org.apache.nifi.web.api.UserGroupResource" scope="singleton">
+        <property name="serviceFacade" ref="serviceFacade"/>
+        <property name="properties" ref="nifiProperties"/>
+        <property name="clusterManager" ref="clusterManager"/>
+    </bean>
     <bean id="clusterResource" class="org.apache.nifi.web.api.ClusterResource" scope="singleton">
         <property name="serviceFacade" ref="serviceFacade"/>
         <property name="properties" ref="nifiProperties"/>
@@ -255,6 +265,7 @@
         <property name="jwtService" ref="jwtService"/>
         <property name="otpService" ref="otpService"/>
         <property name="kerberosService" ref="kerberosService"/>
+        <property name="userDetailsService" ref="userDetailsService"/>
     </bean>
 
     <!-- configuration for jaxb serialization -->
@@ -264,6 +275,7 @@
     <bean class="org.apache.nifi.web.api.config.AccessDeniedExceptionMapper" scope="singleton"/>
     <bean class="org.apache.nifi.web.api.config.InvalidAuthenticationExceptionMapper" scope="singleton"/>
     <bean class="org.apache.nifi.web.api.config.AuthenticationCredentialsNotFoundExceptionMapper" scope="singleton"/>
+    <bean class="org.apache.nifi.web.api.config.AccountNotFoundExceptionMapper" scope="singleton"/>
     <bean class="org.apache.nifi.web.api.config.AdministrationExceptionMapper" scope="singleton"/>
     <bean class="org.apache.nifi.web.api.config.ClusterExceptionMapper" scope="singleton"/>
     <bean class="org.apache.nifi.web.api.config.IllegalArgumentExceptionMapper" scope="singleton"/>

http://git-wip-us.apache.org/repos/asf/nifi/blob/3f4ac315/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AccessTokenEndpointTest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AccessTokenEndpointTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AccessTokenEndpointTest.java
index 5b96c6e..fe48490 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AccessTokenEndpointTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AccessTokenEndpointTest.java
@@ -41,13 +41,11 @@ import org.apache.nifi.web.util.WebUtils;
 import org.junit.AfterClass;
 import org.junit.Assert;
 import org.junit.BeforeClass;
-import org.junit.Ignore;
 import org.junit.Test;
 
 /**
  * Access token endpoint test.
  */
-@Ignore
 public class AccessTokenEndpointTest {
 
     private static final String CLIENT_ID = "token-endpoint-id";

http://git-wip-us.apache.org/repos/asf/nifi/blob/3f4ac315/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AdminAccessControlTest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AdminAccessControlTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AdminAccessControlTest.java
index dd69954..8e0efd1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AdminAccessControlTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AdminAccessControlTest.java
@@ -53,13 +53,11 @@ import org.apache.commons.collections4.CollectionUtils;
 import org.junit.AfterClass;
 import org.junit.Assert;
 import org.junit.BeforeClass;
-import org.junit.Ignore;
 import org.junit.Test;
 
 /**
  * Access control test for the admin user.
  */
-@Ignore
 public class AdminAccessControlTest {
 
     public static final String ADMIN_USER_DN = "CN=Lastname Firstname Middlename admin, OU=Unknown, OU=Unknown, OU=Unknown, O=Unknown, C=Unknown";

http://git-wip-us.apache.org/repos/asf/nifi/blob/3f4ac315/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/DfmAccessControlTest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/DfmAccessControlTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/DfmAccessControlTest.java
index 914cf60..283a4a9 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/DfmAccessControlTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/DfmAccessControlTest.java
@@ -78,7 +78,6 @@ import org.junit.Test;
 /**
  * Access control test for the dfm user.
  */
-@Ignore
 public class DfmAccessControlTest {
 
     public static final String DFM_USER_DN = "CN=Lastname Firstname Middlename dfm, OU=Unknown, OU=Unknown, OU=Unknown, O=Unknown, C=Unknown";

http://git-wip-us.apache.org/repos/asf/nifi/blob/3f4ac315/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ReadOnlyAccessControlTest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ReadOnlyAccessControlTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ReadOnlyAccessControlTest.java
index 2ed653a..0ab074f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ReadOnlyAccessControlTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ReadOnlyAccessControlTest.java
@@ -49,13 +49,11 @@ import org.apache.nifi.web.api.entity.ProcessorsEntity;
 import org.junit.AfterClass;
 import org.junit.Assert;
 import org.junit.BeforeClass;
-import org.junit.Ignore;
 import org.junit.Test;
 
 /**
  * Access control test for a read only user.
  */
-@Ignore
 public class ReadOnlyAccessControlTest {
 
     public static final String READ_ONLY_USER_DN = "CN=Lastname Firstname Middlename monitor, OU=Unknown, OU=Unknown, OU=Unknown, O=Unknown, C=Unknown";