You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by kd...@apache.org on 2018/09/22 02:11:15 UTC

[16/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java
new file mode 100644
index 0000000..afb8e11
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java
@@ -0,0 +1,315 @@
+/*
+ * 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.registry.web.api;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
+import io.swagger.annotations.Extension;
+import io.swagger.annotations.ExtensionProperty;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.field.Fields;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.apache.nifi.registry.service.RegistryService;
+import org.apache.nifi.registry.web.link.LinkService;
+import org.apache.nifi.registry.web.security.PermissionsService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.Set;
+import java.util.SortedSet;
+
+@Component
+@Path("/flows")
+@Api(
+        value = "flows",
+        description = "Gets metadata about flows.",
+        authorizations = { @Authorization("Authorization") }
+)
+public class FlowResource extends AuthorizableApplicationResource {
+
+    private final RegistryService registryService;
+    private final LinkService linkService;
+    private final PermissionsService permissionsService;
+
+    @Autowired
+    public FlowResource(final RegistryService registryService,
+                        final LinkService linkService,
+                        final PermissionsService permissionsService,
+                        final AuthorizationService authorizationService,
+                        final EventService eventService) {
+        super(authorizationService, eventService);
+        this.registryService = registryService;
+        this.linkService = linkService;
+        this.permissionsService = permissionsService;
+    }
+
+    @GET
+    @Path("fields")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Retrieves the available field names that can be used for searching or sorting on flows.",
+            response = Fields.class
+    )
+    public Response getAvailableFlowFields() {
+        final Set<String> flowFields = registryService.getFlowFields();
+        final Fields fields = new Fields(flowFields);
+        return Response.status(Response.Status.OK).entity(fields).build();
+    }
+
+    @GET
+    @Path("{flowId}")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Gets a flow",
+            nickname = "globalGetFlow",
+            response = VersionedFlow.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response getFlow(
+            @PathParam("flowId")
+            @ApiParam("The flow identifier")
+            final String flowId) {
+
+        final VersionedFlow flow = registryService.getFlow(flowId);
+
+        // this should never happen, but if somehow the back-end didn't populate the bucket id let's make sure the flow isn't returned
+        if (StringUtils.isBlank(flow.getBucketIdentifier())) {
+            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
+        }
+
+        authorizeBucketAccess(RequestAction.READ, flow.getBucketIdentifier());
+
+        permissionsService.populateItemPermissions(flow);
+        linkService.populateFlowLinks(flow);
+
+        return Response.status(Response.Status.OK).entity(flow).build();
+    }
+
+    @GET
+    @Path("{flowId}/versions")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Gets summary information for all versions of a flow. Versions are ordered newest->oldest.",
+            nickname = "globalGetFlowVersions",
+            response = VersionedFlowSnapshotMetadata.class,
+            responseContainer = "List",
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response getFlowVersions(
+            @PathParam("flowId")
+            @ApiParam("The flow identifier")
+            final String flowId) {
+
+        final VersionedFlow flow = registryService.getFlow(flowId);
+
+        final String bucketId = flow.getBucketIdentifier();
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
+        }
+
+        authorizeBucketAccess(RequestAction.READ, bucketId);
+
+        final SortedSet<VersionedFlowSnapshotMetadata> snapshots = registryService.getFlowSnapshots(bucketId, flowId);
+        if (snapshots != null ) {
+            linkService.populateSnapshotLinks(snapshots);
+        }
+
+        return Response.status(Response.Status.OK).entity(snapshots).build();
+    }
+
+    @GET
+    @Path("{flowId}/versions/{versionNumber: \\d+}")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Gets the given version of a flow",
+            nickname = "globalGetFlowVersion",
+            response = VersionedFlowSnapshot.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response getFlowVersion(
+            @PathParam("flowId")
+            @ApiParam("The flow identifier")
+            final String flowId,
+            @PathParam("versionNumber")
+            @ApiParam("The version number")
+            final Integer versionNumber) {
+
+        final VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(flowId);
+
+        final String bucketId = latestMetadata.getBucketIdentifier();
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
+        }
+
+        authorizeBucketAccess(RequestAction.READ, bucketId);
+
+        final VersionedFlowSnapshot snapshot = registryService.getFlowSnapshot(bucketId, flowId, versionNumber);
+        populateLinksAndPermissions(snapshot);
+        return Response.status(Response.Status.OK).entity(snapshot).build();
+    }
+
+    @GET
+    @Path("{flowId}/versions/latest")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Get the latest version of a flow",
+            nickname = "globalGetLatestFlowVersion",
+            response = VersionedFlowSnapshot.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response getLatestFlowVersion(
+            @PathParam("flowId")
+            @ApiParam("The flow identifier")
+            final String flowId) {
+
+        final VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(flowId);
+
+        final String bucketId = latestMetadata.getBucketIdentifier();
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
+        }
+
+        authorizeBucketAccess(RequestAction.READ, bucketId);
+
+        final VersionedFlowSnapshot lastSnapshot = registryService.getFlowSnapshot(bucketId, flowId, latestMetadata.getVersion());
+        populateLinksAndPermissions(lastSnapshot);
+
+        return Response.status(Response.Status.OK).entity(lastSnapshot).build();
+    }
+
+    @GET
+    @Path("{flowId}/versions/latest/metadata")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Get the metadata for the latest version of a flow",
+            nickname = "globalGetLatestFlowVersionMetadata",
+            response = VersionedFlowSnapshotMetadata.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response getLatestFlowVersionMetadata(
+            @PathParam("flowId")
+            @ApiParam("The flow identifier")
+            final String flowId) {
+
+        final VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(flowId);
+
+        final String bucketId = latestMetadata.getBucketIdentifier();
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
+        }
+
+        authorizeBucketAccess(RequestAction.READ, bucketId);
+
+        linkService.populateSnapshotLinks(latestMetadata);
+        return Response.status(Response.Status.OK).entity(latestMetadata).build();
+    }
+
+    // override the base implementation so we can provide a different error message that doesn't include the bucket id
+    protected void authorizeBucketAccess(RequestAction action, String bucketId) {
+        try {
+            super.authorizeBucketAccess(RequestAction.READ, bucketId);
+        } catch (AccessDeniedException e) {
+            throw new AccessDeniedException("User not authorized to view the specified flow.", e);
+        }
+    }
+
+    private void populateLinksAndPermissions(VersionedFlowSnapshot snapshot) {
+        if (snapshot.getSnapshotMetadata() != null) {
+            linkService.populateSnapshotLinks(snapshot.getSnapshotMetadata());
+        }
+
+        if (snapshot.getFlow() != null) {
+            linkService.populateFlowLinks(snapshot.getFlow());
+        }
+
+        if (snapshot.getBucket() != null) {
+            permissionsService.populateBucketPermissions(snapshot.getBucket());
+            linkService.populateBucketLinks(snapshot.getBucket());
+        }
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java
new file mode 100644
index 0000000..a3ba939
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java
@@ -0,0 +1,30 @@
+/*
+ * 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.registry.web.api;
+
+class HttpStatusMessages {
+
+    /* 4xx messages */
+    static final String MESSAGE_400 = "NiFi Registry was unable to complete the request because it was invalid. The request should not be retried without modification.";
+    static final String MESSAGE_401 = "Client could not be authenticated.";
+    static final String MESSAGE_403 = "Client is not authorized to make this request.";
+    static final String MESSAGE_404 = "The specified resource could not be found.";
+    static final String MESSAGE_409 = "NiFi Registry was unable to complete the request because it assumes a server state that is not valid.";
+
+    /* 5xx messages */
+    static final String MESSAGE_500 = "NiFi Registry was unable to complete the request because an unexpected error occurred.";
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java
new file mode 100644
index 0000000..02b63d2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java
@@ -0,0 +1,171 @@
+/*
+ * 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.registry.web.api;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
+import io.swagger.annotations.Extension;
+import io.swagger.annotations.ExtensionProperty;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.field.Fields;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.apache.nifi.registry.service.RegistryService;
+import org.apache.nifi.registry.web.link.LinkService;
+import org.apache.nifi.registry.web.security.PermissionsService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+@Component
+@Path("/items")
+@Api(
+        value = "items",
+        description = "Retrieve items across all buckets for which the user is authorized.",
+        authorizations = { @Authorization("Authorization") }
+)
+public class ItemResource extends AuthorizableApplicationResource {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(ItemResource.class);
+
+    @Context
+    UriInfo uriInfo;
+
+    private final LinkService linkService;
+    private final PermissionsService permissionsService;
+    private final RegistryService registryService;
+
+    @Autowired
+    public ItemResource(
+            final RegistryService registryService,
+            final LinkService linkService,
+            final PermissionsService permissionsService,
+            final AuthorizationService authorizationService,
+            final EventService eventService) {
+        super(authorizationService, eventService);
+        this.registryService = registryService;
+        this.linkService = linkService;
+        this.permissionsService = permissionsService;
+    }
+
+
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Get items across all buckets",
+            notes = "The returned items will include only items from buckets for which the user is authorized. " +
+                    "If the user is not authorized to any buckets, an empty list will be returned.",
+            response = BucketItem.class,
+            responseContainer = "List"
+    )
+    @ApiResponses({ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401) })
+    public Response getItems() {
+
+        // Note: We don't explicitly check for access to (READ, /buckets) or
+        // (READ, /items ) because a user might have access to individual buckets
+        // without top-level access. For example, a user that has
+        // (READ, /buckets/bucket-id-1) but not access to /buckets should not
+        // get a 403 error returned from this endpoint. This has the side effect
+        // that a user with no access to any buckets gets an empty array returned
+        // from this endpoint instead of 403 as one might expect.
+
+        final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
+        if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
+            // not authorized for any bucket, return empty list of items
+            return Response.status(Response.Status.OK).entity(new ArrayList<BucketItem>()).build();
+        }
+
+        List<BucketItem> items = registryService.getBucketItems(authorizedBucketIds);
+        if (items == null) {
+            items = Collections.emptyList();
+        }
+        permissionsService.populateItemPermissions(items);
+        linkService.populateItemLinks(items);
+
+        return Response.status(Response.Status.OK).entity(items).build();
+    }
+
+    @GET
+    @Path("{bucketId}")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Gets items of the given bucket",
+            response = BucketItem.class,
+            responseContainer = "List",
+            nickname = "getItemsInBucket",
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404) })
+    public Response getItems(
+            @PathParam("bucketId")
+            @ApiParam("The bucket identifier")
+            final String bucketId) {
+
+        authorizeBucketAccess(RequestAction.READ, bucketId);
+
+        final List<BucketItem> items = registryService.getBucketItems(bucketId);
+        permissionsService.populateItemPermissions(items);
+        linkService.populateItemLinks(items);
+
+        return Response.status(Response.Status.OK).entity(items).build();
+    }
+
+    @GET
+    @Path("fields")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Retrieves the available field names for searching or sorting on bucket items.",
+            response = Fields.class
+    )
+    public Response getAvailableBucketItemFields() {
+        final Set<String> bucketFields = registryService.getBucketItemFields();
+        final Fields fields = new Fields(bucketFields);
+        return Response.status(Response.Status.OK).entity(fields).build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java
new file mode 100644
index 0000000..7215ccb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java
@@ -0,0 +1,579 @@
+/*
+ * 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.registry.web.api;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
+import io.swagger.annotations.Extension;
+import io.swagger.annotations.ExtensionProperty;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.authorization.User;
+import org.apache.nifi.registry.authorization.UserGroup;
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.exception.ResourceNotFoundException;
+import org.apache.nifi.registry.security.authorization.Authorizer;
+import org.apache.nifi.registry.security.authorization.AuthorizerCapabilityDetection;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.security.authorization.resource.Authorizable;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+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.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+import java.util.List;
+
+/**
+ * RESTful endpoints for managing tenants, ie, users and user groups.
+ */
+@Component
+@Path("tenants")
+@Api(
+        value = "tenants",
+        description = "Endpoint for managing users and user groups.",
+        authorizations = { @Authorization("Authorization") }
+)
+public class TenantResource extends AuthorizableApplicationResource {
+
+    private static final Logger logger = LoggerFactory.getLogger(TenantResource.class);
+
+    private Authorizer authorizer;
+
+    @Autowired
+    public TenantResource(AuthorizationService authorizationService, EventService eventService) {
+        super(authorizationService, eventService);
+        authorizer = authorizationService.getAuthorizer();
+    }
+
+
+    // ---------- User endpoints --------------------------------------------------------------------------------------
+
+    /**
+     * Creates a new user.
+     *
+     * @param httpServletRequest request
+     * @param requestUser the user to create
+     * @return the user that was created
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("users")
+    @ApiOperation(
+            value = "Creates a user",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = User.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "write"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response createUser(
+            @Context
+            final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The user configuration details.", required = true)
+            final User requestUser) {
+
+        verifyAuthorizerSupportsConfigurableUserGroups();
+
+        if (requestUser == null) {
+            throw new IllegalArgumentException("User details must be specified when creating a new user.");
+        }
+        if (requestUser.getIdentifier() != null) {
+            throw new IllegalArgumentException("User identifier cannot be specified when creating a new user.");
+        }
+        if (StringUtils.isBlank(requestUser.getIdentity())) {
+            throw new IllegalArgumentException("User identity must be specified when creating a new user.");
+        }
+
+        authorizeAccess(RequestAction.WRITE);
+
+        User createdUser = authorizationService.createUser(requestUser);
+
+        String locationUri = generateUserUri(createdUser);
+        return generateCreatedResponse(URI.create(locationUri), createdUser).build();
+    }
+
+    /**
+     * Retrieves all the of users in this NiFi.
+     *
+     * @return a list of users
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("users")
+    @ApiOperation(
+            value = "Gets all users",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = User.class,
+            responseContainer = "List",
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response getUsers() {
+        verifyAuthorizerIsManaged();
+
+        authorizeAccess(RequestAction.READ);
+
+        // get all the users
+        final List<User> users = authorizationService.getUsers();
+
+        // generate the response
+        return generateOkResponse(users).build();
+    }
+
+    /**
+     * Retrieves the specified user.
+     *
+     * @param identifier The id of the user to retrieve
+     * @return An userEntity.
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("users/{id}")
+    @ApiOperation(
+            value = "Gets a user",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = User.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response getUser(
+            @ApiParam(value = "The user id.", required = true)
+            @PathParam("id") final String identifier) {
+        verifyAuthorizerIsManaged();
+        authorizeAccess(RequestAction.READ);
+
+        final User user = authorizationService.getUser(identifier);
+        if (user == null) {
+            logger.warn("The specified user id [{}] does not exist.", identifier);
+
+            throw new ResourceNotFoundException("The specified user ID does not exist in this registry.");
+        }
+        return generateOkResponse(user).build();
+    }
+
+    /**
+     * Updates a user.
+     *
+     * @param httpServletRequest request
+     * @param identifier The id of the user to update
+     * @param requestUser The user with updated fields.
+     * @return The updated user
+     */
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("users/{id}")
+    @ApiOperation(
+            value = "Updates a user",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = User.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "write"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response updateUser(
+            @Context
+            final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The user id.", required = true)
+            @PathParam("id")
+            final String identifier,
+            @ApiParam(value = "The user configuration details.", required = true)
+            final User requestUser) {
+
+        verifyAuthorizerSupportsConfigurableUserGroups();
+        authorizeAccess(RequestAction.WRITE);
+
+        if (requestUser == null) {
+            throw new IllegalArgumentException("User details must be specified when updating a user.");
+        }
+        if (!identifier.equals(requestUser.getIdentifier())) {
+            throw new IllegalArgumentException(String.format("The user id in the request body (%s) does not equal the "
+                    + "user id of the requested resource (%s).", requestUser.getIdentifier(), identifier));
+        }
+
+        final User updatedUser = authorizationService.updateUser(requestUser);
+        if (updatedUser == null) {
+            logger.warn("The specified user id [{}] does not exist.", identifier);
+
+            throw new ResourceNotFoundException("The specified user ID does not exist in this registry.");
+        }
+
+        return generateOkResponse(updatedUser).build();
+    }
+
+    /**
+     * Removes the specified user.
+     *
+     * @param httpServletRequest request
+     * @param identifier         The id of the user to remove.
+     * @return A entity containing the client id and an updated revision.
+     */
+    @DELETE
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("users/{id}")
+    @ApiOperation(
+            value = "Deletes a user",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = User.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "delete"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response removeUser(
+            @Context
+            final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The user id.", required = true)
+            @PathParam("id")
+            final String identifier) {
+
+        verifyAuthorizerSupportsConfigurableUserGroups();
+        authorizeAccess(RequestAction.DELETE);
+
+        final User user = authorizationService.deleteUser(identifier);
+        if (user == null) {
+            logger.warn("The specified user id [{}] does not exist.", identifier);
+
+            throw new ResourceNotFoundException("The specified user ID does not exist in this registry.");
+        }
+        return generateOkResponse(user).build();
+    }
+
+
+    // ---------- User Group endpoints --------------------------------------------------------------------------------
+
+    /**
+     * Creates a new user group.
+     *
+     * @param httpServletRequest request
+     * @param requestUserGroup the user group to create
+     * @return the created user group
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("user-groups")
+    @ApiOperation(
+            value = "Creates a user group",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = UserGroup.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "write"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response createUserGroup(
+            @Context
+            final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The user group configuration details.", required = true)
+            final UserGroup requestUserGroup) {
+
+        verifyAuthorizerSupportsConfigurableUserGroups();
+        authorizeAccess(RequestAction.WRITE);
+
+        if (requestUserGroup == null) {
+            throw new IllegalArgumentException("User group details must be specified when creating a new group.");
+        }
+        if (requestUserGroup.getIdentifier() != null) {
+            throw new IllegalArgumentException("User group ID cannot be specified when creating a new group.");
+        }
+        if (StringUtils.isBlank(requestUserGroup.getIdentity())) {
+            throw new IllegalArgumentException("User group identity must be specified when creating a new group.");
+        }
+
+        UserGroup createdGroup = authorizationService.createUserGroup(requestUserGroup);
+
+        String locationUri = generateUserGroupUri(createdGroup);
+        return generateCreatedResponse(URI.create(locationUri), createdGroup).build();
+    }
+
+    /**
+     * Retrieves all the of user groups in this NiFi.
+     *
+     * @return a list of all user groups in this NiFi.
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("user-groups")
+    @ApiOperation(
+            value = "Gets all user groups",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = UserGroup.class,
+            responseContainer = "List",
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response getUserGroups() {
+        verifyAuthorizerIsManaged();
+        authorizeAccess(RequestAction.READ);
+
+        final List<UserGroup> userGroups = authorizationService.getUserGroups();
+        return generateOkResponse(userGroups).build();
+    }
+
+    /**
+     * Retrieves the specified user group.
+     *
+     * @param identifier The id of the user group to retrieve
+     * @return An userGroupEntity.
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("user-groups/{id}")
+    @ApiOperation(
+            value = "Gets a user group",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = UserGroup.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response getUserGroup(
+            @ApiParam(value = "The user group id.", required = true)
+            @PathParam("id") final String identifier) {
+        verifyAuthorizerIsManaged();
+        authorizeAccess(RequestAction.READ);
+
+        final UserGroup userGroup = authorizationService.getUserGroup(identifier);
+        if (userGroup == null) {
+            logger.warn("The specified user group id [{}] does not exist.", identifier);
+
+            throw new ResourceNotFoundException("The specified user group ID does not exist in this registry.");
+        }
+
+        return generateOkResponse(userGroup).build();
+    }
+
+    /**
+     * Updates a user group.
+     *
+     * @param httpServletRequest request
+     * @param identifier The id of the user group to update.
+     * @param requestUserGroup The user group with updated fields.
+     * @return The resulting, updated user group.
+     */
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("user-groups/{id}")
+    @ApiOperation(
+            value = "Updates a user group",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = UserGroup.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "write"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response updateUserGroup(
+            @Context
+            final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The user group id.", required = true)
+            @PathParam("id")
+            final String identifier,
+            @ApiParam(value = "The user group configuration details.", required = true)
+            final UserGroup requestUserGroup) {
+
+        verifyAuthorizerSupportsConfigurableUserGroups();
+
+        if (requestUserGroup == null) {
+            throw new IllegalArgumentException("User group details must be specified to update a user group.");
+        }
+        if (!identifier.equals(requestUserGroup.getIdentifier())) {
+            throw new IllegalArgumentException(String.format("The user group id in the request body (%s) does not equal the "
+                    + "user group id of the requested resource (%s).", requestUserGroup.getIdentifier(), identifier));
+        }
+
+        authorizeAccess(RequestAction.WRITE);
+
+        UserGroup updatedUserGroup = authorizationService.updateUserGroup(requestUserGroup);
+        if (updatedUserGroup == null) {
+            logger.warn("The specified user group id [{}] does not exist.", identifier);
+
+            throw new ResourceNotFoundException("The specified user group ID does not exist in this registry.");
+        }
+
+        return generateOkResponse(updatedUserGroup).build();
+    }
+
+    /**
+     * Removes the specified user group.
+     *
+     * @param httpServletRequest request
+     * @param identifier                 The id of the user group to remove.
+     * @return The deleted user group.
+     */
+    @DELETE
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("user-groups/{id}")
+    @ApiOperation(
+            value = "Deletes a user group",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = UserGroup.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "delete"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response removeUserGroup(
+            @Context
+            final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The user group id.", required = true)
+            @PathParam("id")
+            final String identifier) {
+        verifyAuthorizerSupportsConfigurableUserGroups();
+        authorizeAccess(RequestAction.DELETE);
+
+        final UserGroup userGroup = authorizationService.deleteUserGroup(identifier);
+        if (userGroup == null) {
+            logger.warn("The specified user group id [{}] does not exist.", identifier);
+
+            throw new ResourceNotFoundException("The specified user group ID does not exist in this registry.");
+        }
+
+        return generateOkResponse(userGroup).build();
+    }
+
+
+    private void verifyAuthorizerIsManaged() {
+        if (!AuthorizerCapabilityDetection.isManagedAuthorizer(authorizer)) {
+            throw new IllegalStateException(AuthorizationService.MSG_NON_MANAGED_AUTHORIZER);
+        }
+    }
+
+    private void verifyAuthorizerSupportsConfigurableUserGroups() {
+        if (!AuthorizerCapabilityDetection.isConfigurableUserGroupProvider(authorizer)) {
+            throw new IllegalStateException(AuthorizationService.MSG_NON_CONFIGURABLE_USERS);
+        }
+    }
+
+    private void authorizeAccess(RequestAction actionType) {
+        final Authorizable tenantsAuthorizable = authorizableLookup.getTenantsAuthorizable();
+        authorizationService.authorize(tenantsAuthorizable, actionType);
+    }
+
+    private String generateUserUri(final User user) {
+        return generateResourceUri("tenants", "users", user.getIdentifier());
+    }
+
+    private String generateUserGroupUri(final UserGroup userGroup) {
+        return generateResourceUri("tenants", "user-groups", userGroup.getIdentifier());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/exception/UnauthorizedException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/exception/UnauthorizedException.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/exception/UnauthorizedException.java
new file mode 100644
index 0000000..46e6fc9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/exception/UnauthorizedException.java
@@ -0,0 +1,74 @@
+/*
+ * 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.registry.web.exception;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.security.authentication.IdentityProviderUsage;
+
+import java.util.List;
+
+/**
+ * An exception for a convenient way to create a 401 Unauthorized response
+ * using an exception mapper
+ */
+public class UnauthorizedException extends RuntimeException {
+
+    private String[] wwwAuthenticateChallenge;
+
+    public UnauthorizedException() {
+    }
+
+    public UnauthorizedException(String message) {
+        super(message);
+    }
+
+    public UnauthorizedException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public UnauthorizedException(Throwable cause) {
+        super(cause);
+    }
+
+    public UnauthorizedException withAuthenticateChallenge(IdentityProviderUsage.AuthType authType) {
+        wwwAuthenticateChallenge = new String[] { authType.getHttpAuthScheme() };
+        return this;
+    }
+
+    public UnauthorizedException withAuthenticateChallenge(List<IdentityProviderUsage.AuthType> authTypes) {
+        wwwAuthenticateChallenge = new String[authTypes.size()];
+        for (int i = 0; i < authTypes.size(); i++) {
+            wwwAuthenticateChallenge[i] = authTypes.get(i).getHttpAuthScheme();
+        }
+        return this;
+    }
+
+    public UnauthorizedException withAuthenticateChallenge(String authType) {
+        wwwAuthenticateChallenge = new String[] { authType };
+        return this;
+    }
+
+    public UnauthorizedException withAuthenticateChallenge(String[] authTypes) {
+        wwwAuthenticateChallenge = authTypes;
+        return this;
+    }
+
+    public String getWwwAuthenticateChallenge() {
+        return StringUtils.join(wwwAuthenticateChallenge, ",");
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkService.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkService.java
new file mode 100644
index 0000000..19e2168
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkService.java
@@ -0,0 +1,110 @@
+/*
+ * 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.registry.web.link;
+
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+import org.apache.nifi.registry.web.link.builder.BucketLinkBuilder;
+import org.apache.nifi.registry.web.link.builder.LinkBuilder;
+import org.apache.nifi.registry.web.link.builder.VersionedFlowLinkBuilder;
+import org.apache.nifi.registry.web.link.builder.VersionedFlowSnapshotLinkBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import javax.ws.rs.core.Link;
+
+@Service
+public class LinkService {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(LinkService.class);
+
+    private final LinkBuilder<Bucket> bucketLinkBuilder = new BucketLinkBuilder();
+
+    private final LinkBuilder<VersionedFlow> versionedFlowLinkBuilder = new VersionedFlowLinkBuilder();
+
+    private final LinkBuilder<VersionedFlowSnapshotMetadata> snapshotMetadataLinkBuilder = new VersionedFlowSnapshotLinkBuilder();
+
+    // ---- Bucket Links
+
+    public void populateBucketLinks(final Iterable<Bucket> buckets) {
+        if (buckets == null) {
+            return;
+        }
+
+        buckets.forEach(b -> populateBucketLinks(b));
+    }
+
+    public void populateBucketLinks(final Bucket bucket) {
+        final Link bucketLink = bucketLinkBuilder.createLink(bucket);
+        bucket.setLink(bucketLink);
+    }
+
+    // ---- Flow Links
+
+    public void populateFlowLinks(final Iterable<VersionedFlow> versionedFlows) {
+        if (versionedFlows == null) {
+            return;
+        }
+
+        versionedFlows.forEach(f  -> populateFlowLinks(f));
+    }
+
+    public void populateFlowLinks(final VersionedFlow versionedFlow) {
+        final Link flowLink = versionedFlowLinkBuilder.createLink(versionedFlow);
+        versionedFlow.setLink(flowLink);
+    }
+
+    // ---- Flow Snapshot Links
+
+    public void populateSnapshotLinks(final Iterable<VersionedFlowSnapshotMetadata> snapshotMetadatas) {
+        if (snapshotMetadatas == null) {
+            return;
+        }
+
+        snapshotMetadatas.forEach(s -> populateSnapshotLinks(s));
+    }
+
+    public void populateSnapshotLinks(final VersionedFlowSnapshotMetadata snapshotMetadata) {
+        final Link snapshotLink = snapshotMetadataLinkBuilder.createLink(snapshotMetadata);
+        snapshotMetadata.setLink(snapshotLink);
+    }
+
+    // ---- BucketItem Links
+
+    public void populateItemLinks(final Iterable<BucketItem> items) {
+        if (items == null) {
+            return;
+        }
+
+        items.forEach(i -> populateItemLinks(i));
+    }
+
+    public void populateItemLinks(final BucketItem bucketItem) {
+        if (bucketItem == null) {
+            return;
+        }
+
+        if (bucketItem instanceof VersionedFlow) {
+            populateFlowLinks((VersionedFlow)bucketItem);
+        } else {
+            LOGGER.error("Unable to create link for BucketItem with type: " + bucketItem.getClass().getCanonicalName());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/BucketLinkBuilder.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/BucketLinkBuilder.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/BucketLinkBuilder.java
new file mode 100644
index 0000000..f0409c7
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/BucketLinkBuilder.java
@@ -0,0 +1,45 @@
+/*
+ * 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.registry.web.link.builder;
+
+import org.apache.nifi.registry.bucket.Bucket;
+
+import javax.ws.rs.core.Link;
+import javax.ws.rs.core.UriBuilder;
+import java.net.URI;
+
+/**
+ * LinkBuilder that builds "self" links for Buckets.
+ */
+public class BucketLinkBuilder implements LinkBuilder<Bucket> {
+
+    private static final String PATH = "buckets/{id}";
+
+    @Override
+    public Link createLink(final Bucket bucket) {
+        if (bucket == null) {
+            return null;
+        }
+
+        final URI uri = UriBuilder.fromPath(PATH)
+                .resolveTemplate("id", bucket.getIdentifier())
+                .build();
+
+        return Link.fromUri(uri).rel("self").build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
new file mode 100644
index 0000000..ec356fd
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
@@ -0,0 +1,30 @@
+/*
+ * 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.registry.web.link.builder;
+
+import javax.ws.rs.core.Link;
+
+/**
+ * Creates a Link for a given type.
+ *
+ * @param <T> the type to create a link for
+ */
+public interface LinkBuilder<T> {
+
+    Link createLink(T t);
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowLinkBuilder.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowLinkBuilder.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowLinkBuilder.java
new file mode 100644
index 0000000..38d3d0e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowLinkBuilder.java
@@ -0,0 +1,46 @@
+/*
+ * 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.registry.web.link.builder;
+
+import org.apache.nifi.registry.flow.VersionedFlow;
+
+import javax.ws.rs.core.Link;
+import javax.ws.rs.core.UriBuilder;
+import java.net.URI;
+
+/**
+ * LinkBuilder that builds "self" links for VersionedFlows.
+ */
+public class VersionedFlowLinkBuilder implements LinkBuilder<VersionedFlow> {
+
+    private static final String PATH = "buckets/{bucketId}/flows/{flowId}";
+
+    @Override
+    public Link createLink(final VersionedFlow versionedFlow) {
+        if (versionedFlow == null) {
+            return null;
+        }
+
+        final URI uri = UriBuilder.fromPath(PATH)
+                .resolveTemplate("bucketId", versionedFlow.getBucketIdentifier())
+                .resolveTemplate("flowId", versionedFlow.getIdentifier())
+                .build();
+
+        return Link.fromUri(uri).rel("self").build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowSnapshotLinkBuilder.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowSnapshotLinkBuilder.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowSnapshotLinkBuilder.java
new file mode 100644
index 0000000..4085c6d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowSnapshotLinkBuilder.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.registry.web.link.builder;
+
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+
+import javax.ws.rs.core.Link;
+import javax.ws.rs.core.UriBuilder;
+import java.net.URI;
+
+/**
+ * LinkBuilder that builds "self" links for VersionedFlowSnapshotMetadata.
+ */
+public class VersionedFlowSnapshotLinkBuilder implements LinkBuilder<VersionedFlowSnapshotMetadata> {
+
+    private static final String PATH = "buckets/{bucketId}/flows/{flowId}/versions/{versionNumber}";
+
+    @Override
+    public Link createLink(final VersionedFlowSnapshotMetadata snapshotMetadata) {
+        if (snapshotMetadata == null) {
+            return null;
+        }
+
+        final URI uri = UriBuilder.fromPath(PATH)
+                .resolveTemplate("bucketId", snapshotMetadata.getBucketIdentifier())
+                .resolveTemplate("flowId", snapshotMetadata.getFlowIdentifier())
+                .resolveTemplate("versionNumber", snapshotMetadata.getVersion())
+                .build();
+
+        return Link.fromUri(uri).rel("content").build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AccessDeniedExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AccessDeniedExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AccessDeniedExceptionMapper.java
new file mode 100644
index 0000000..5b9e3ee
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AccessDeniedExceptionMapper.java
@@ -0,0 +1,75 @@
+/*
+ * 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.registry.web.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
+import org.apache.nifi.registry.security.authorization.user.NiFiUser;
+import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps access denied exceptions into a client response.
+ */
+@Component
+@Provider
+public class AccessDeniedExceptionMapper implements ExceptionMapper<AccessDeniedException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(AccessDeniedExceptionMapper.class);
+
+    @Override
+    public Response toResponse(AccessDeniedException exception) {
+        // get the current user
+        NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+        // if the user was authenticated - forbidden, otherwise unauthorized... the user may be null if the
+        // AccessDeniedException was thrown from a /access endpoint that isn't subject to the security
+        // filter chain. for instance, one that performs kerberos negotiation
+        final Status status;
+        if (user == null || user.isAnonymous()) {
+            status = Status.UNAUTHORIZED;
+        } else {
+            status = Status.FORBIDDEN;
+        }
+
+        final String identity;
+        if (user == null) {
+            identity = "<no user found>";
+        } else {
+            identity = user.toString();
+        }
+
+        logger.info(String.format("%s does not have permission to access the requested resource. %s Returning %s response.", identity, exception.getMessage(), status));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        return Response.status(status)
+                .entity(String.format("%s Contact the system administrator.", exception.getMessage()))
+                .type("text/plain")
+                .build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AdministrationExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AdministrationExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AdministrationExceptionMapper.java
new file mode 100644
index 0000000..b97222c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AdministrationExceptionMapper.java
@@ -0,0 +1,46 @@
+/*
+ * 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.registry.web.mapper;
+
+import org.apache.nifi.registry.exception.AdministrationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps administration exceptions into client responses.
+ */
+@Component
+@Provider
+public class AdministrationExceptionMapper implements ExceptionMapper<AdministrationException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(AdministrationExceptionMapper.class);
+
+    @Override
+    public Response toResponse(AdministrationException exception) {
+        // log the error
+        logger.error(String.format("%s. Returning %s response.", exception, Response.Status.INTERNAL_SERVER_ERROR), exception);
+
+        // generate the response
+        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(exception.getMessage()).type("text/plain").build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthenticationCredentialsNotFoundExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthenticationCredentialsNotFoundExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthenticationCredentialsNotFoundExceptionMapper.java
new file mode 100644
index 0000000..ee7fb74
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthenticationCredentialsNotFoundExceptionMapper.java
@@ -0,0 +1,50 @@
+/*
+ * 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.registry.web.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps exceptions that occur because no valid credentials were found into the corresponding response.
+ */
+@Component
+@Provider
+public class AuthenticationCredentialsNotFoundExceptionMapper implements ExceptionMapper<AuthenticationCredentialsNotFoundException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(AuthenticationCredentialsNotFoundExceptionMapper.class);
+
+    @Override
+    public Response toResponse(AuthenticationCredentialsNotFoundException exception) {
+        // log the error
+        logger.info(String.format("No valid credentials were found in the request: %s. Returning %s response.", exception, Response.Status.FORBIDDEN));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        return Response.status(Response.Status.FORBIDDEN).entity("Access is denied.").type("text/plain").build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthorizationAccessExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthorizationAccessExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthorizationAccessExceptionMapper.java
new file mode 100644
index 0000000..ff4b7ec
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthorizationAccessExceptionMapper.java
@@ -0,0 +1,46 @@
+/*
+ * 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.registry.web.mapper;
+
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps authorization access exceptions into client responses.
+ */
+@Component
+@Provider
+public class AuthorizationAccessExceptionMapper implements ExceptionMapper<AuthorizationAccessException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(AuthorizationAccessExceptionMapper.class);
+
+    @Override
+    public Response toResponse(AuthorizationAccessException e) {
+        // log the error
+        logger.error(String.format("%s. Returning %s response.", e, Response.Status.INTERNAL_SERVER_ERROR), e);
+
+        // generate the response
+        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).type("text/plain").build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/BadRequestExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/BadRequestExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/BadRequestExceptionMapper.java
new file mode 100644
index 0000000..2577fa0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/BadRequestExceptionMapper.java
@@ -0,0 +1,49 @@
+/*
+ * 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.registry.web.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps exceptions into client responses.
+ */
+@Component
+@Provider
+public class BadRequestExceptionMapper implements ExceptionMapper<BadRequestException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(BadRequestExceptionMapper.class);
+
+    @Override
+    public Response toResponse(BadRequestException exception) {
+        logger.info(String.format("%s. Returning %s response.", exception, Response.Status.BAD_REQUEST));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        return Response.status(Response.Status.BAD_REQUEST).entity(exception.getMessage()).type("text/plain").build();
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ConstraintViolationExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ConstraintViolationExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ConstraintViolationExceptionMapper.java
new file mode 100644
index 0000000..b691775
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ConstraintViolationExceptionMapper.java
@@ -0,0 +1,68 @@
+/*
+ * 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.registry.web.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.Path;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+@Component
+@Provider
+public class ConstraintViolationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(ConstraintViolationExceptionMapper.class);
+
+    @Override
+    public Response toResponse(ConstraintViolationException exception) {
+        logger.info(String.format("%s. Returning %s response.", exception, Response.Status.BAD_REQUEST));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        // start with the overall message which will be something like "Cannot create xyz"
+        final StringBuilder errorMessage = new StringBuilder(exception.getMessage()).append(" - ");
+
+        boolean first = true;
+        for (final ConstraintViolation violation : exception.getConstraintViolations()) {
+            if (!first) {
+                errorMessage.append(", ");
+            }
+            first = false;
+
+            // lastNode should end up as the field that failed validation
+            Path.Node lastNode = null;
+            for (final Path.Node node : violation.getPropertyPath()) {
+                lastNode = node;
+            }
+
+            // append something like "xyz must not be..."
+            errorMessage.append(lastNode.getName()).append(" ").append(violation.getMessage());
+        }
+
+        return Response.status(Response.Status.BAD_REQUEST).entity(errorMessage.toString()).type("text/plain").build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/IllegalArgumentExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/IllegalArgumentExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/IllegalArgumentExceptionMapper.java
new file mode 100644
index 0000000..7186c0f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/IllegalArgumentExceptionMapper.java
@@ -0,0 +1,48 @@
+/*
+ * 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.registry.web.mapper;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+/**
+ * Maps exceptions into client responses.
+ */
+@Component
+@Provider
+public class IllegalArgumentExceptionMapper implements ExceptionMapper<IllegalArgumentException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(IllegalArgumentExceptionMapper.class);
+
+    @Override
+    public Response toResponse(IllegalArgumentException exception) {
+        logger.info(String.format("%s. Returning %s response.", exception, Response.Status.BAD_REQUEST));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).type("text/plain").build();
+    }
+
+}