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();
+ }
+
+}