http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionResource.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionResource.java
 
b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionResource.java
new file mode 100644
index 0000000..62c4c08
--- /dev/null
+++ 
b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionResource.java
@@ -0,0 +1,366 @@
+/*
+ * 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.bucket.BucketItem;
+import org.apache.nifi.registry.event.EventFactory;
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.ExtensionBundleVersion;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+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.service.extension.ExtensionBundleVersionCoordinate;
+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.DELETE;
+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 javax.ws.rs.core.StreamingOutput;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+
+@Component
+@Path("/extensions")
+@Api(
+        value = "extensions",
+        description = "Gets metadata about extension bundles and extensions.",
+        authorizations = { @Authorization("Authorization") }
+)
+public class ExtensionResource extends AuthorizableApplicationResource {
+
+    public static final String CONTENT_DISPOSITION_HEADER = 
"content-disposition";
+    private final RegistryService registryService;
+    private final LinkService linkService;
+    private final PermissionsService permissionsService;
+
+    @Autowired
+    public ExtensionResource(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("bundles")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Get extension bundles across all authorized 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 = ExtensionBundle.class,
+            responseContainer = "List"
+    )
+    @ApiResponses({ @ApiResponse(code = 401, message = 
HttpStatusMessages.MESSAGE_401) })
+    public Response getExtensionBundles() {
+
+        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<ExtensionBundle> bundles = 
registryService.getExtensionBundles(authorizedBucketIds);
+        if (bundles == null) {
+            bundles = Collections.emptyList();
+        }
+        permissionsService.populateItemPermissions(bundles);
+        linkService.populateLinks(bundles);
+
+        return Response.status(Response.Status.OK).entity(bundles).build();
+    }
+
+    @GET
+    @Path("bundles/{bundleId}")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Gets the metadata about an extension bundle",
+            nickname = "globalGetExtensionBundle",
+            response = ExtensionBundle.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 getExtensionBundle(
+            @PathParam("bundleId")
+            @ApiParam("The extension bundle identifier")
+                final String bundleId) {
+
+        final ExtensionBundle extensionBundle = 
getExtensionBundleWithBucketReadAuthorization(bundleId);
+
+        permissionsService.populateItemPermissions(extensionBundle);
+        linkService.populateLinks(extensionBundle);
+
+        return 
Response.status(Response.Status.OK).entity(extensionBundle).build();
+    }
+
+    @DELETE
+    @Path("bundles/{bundleId}")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Deletes the given extension bundle and all of it's 
versions",
+            nickname = "globalDeleteExtensionBundle",
+            response = ExtensionBundle.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = 
"write"),
+                            @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 deleteExtensionBundle(
+            @PathParam("bundleId")
+            @ApiParam("The extension bundle identifier")
+                final String bundleId) {
+
+        final ExtensionBundle extensionBundle = 
getExtensionBundleWithBucketReadAuthorization(bundleId);
+
+        final ExtensionBundle deletedExtensionBundle = 
registryService.deleteExtensionBundle(extensionBundle);
+        publish(EventFactory.extensionBundleDeleted(deletedExtensionBundle));
+
+        permissionsService.populateItemPermissions(deletedExtensionBundle);
+        linkService.populateLinks(deletedExtensionBundle);
+
+        return 
Response.status(Response.Status.OK).entity(deletedExtensionBundle).build();
+    }
+
+    @GET
+    @Path("bundles/{bundleId}/versions")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Gets the metadata about the versions of an extension 
bundle",
+            nickname = "globalGetExtensionBundleVersions",
+            response = ExtensionBundleVersionMetadata.class,
+            responseContainer = "List",
+            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 getExtensionBundleVersions(
+            @PathParam("bundleId")
+            @ApiParam("The extension bundle identifier")
+                final String bundleId) {
+
+        final ExtensionBundle extensionBundle = 
getExtensionBundleWithBucketReadAuthorization(bundleId);
+
+        final SortedSet<ExtensionBundleVersionMetadata> bundleVersions = 
registryService.getExtensionBundleVersions(extensionBundle.getIdentifier());
+        linkService.populateLinks(bundleVersions);
+
+        return 
Response.status(Response.Status.OK).entity(bundleVersions).build();
+    }
+
+    @GET
+    @Path("bundles/{bundleId}/versions/{version}")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Gets the descriptor for the specified version of the 
extension bundle",
+            nickname = "globalGetExtensionBundleVersionDescriptor",
+            response = ExtensionBundleVersion.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 getExtensionBundleVersion(
+            @PathParam("bundleId")
+            @ApiParam("The extension bundle identifier")
+                final String bundleId,
+            @PathParam("version")
+            @ApiParam("The version of the bundle")
+                final String version) {
+
+        final ExtensionBundle extensionBundle = 
getExtensionBundleWithBucketReadAuthorization(bundleId);
+
+        final ExtensionBundleVersionCoordinate versionCoordinate = new 
ExtensionBundleVersionCoordinate(
+                extensionBundle.getBucketIdentifier(),
+                extensionBundle.getGroupId(),
+                extensionBundle.getArtifactId(),
+                version);
+
+        final ExtensionBundleVersion bundleVersion = 
registryService.getExtensionBundleVersion(versionCoordinate);
+        linkService.populateLinks(bundleVersion);
+
+        return Response.ok(bundleVersion).build();
+    }
+
+    @GET
+    @Path("bundles/{bundleId}/versions/{version}/content")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_OCTET_STREAM)
+    @ApiOperation(
+            value = "Gets the binary content for the specified version of the 
extension bundle",
+            nickname = "globalGetExtensionBundleVersion",
+            response = byte[].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 getExtensionBundleVersionContent(
+            @PathParam("bundleId")
+            @ApiParam("The extension bundle identifier")
+                final String bundleId,
+            @PathParam("version")
+            @ApiParam("The version of the bundle")
+                final String version) {
+
+        final ExtensionBundle extensionBundle = 
getExtensionBundleWithBucketReadAuthorization(bundleId);
+
+        final ExtensionBundleVersionCoordinate versionCoordinate = new 
ExtensionBundleVersionCoordinate(
+                extensionBundle.getBucketIdentifier(),
+                extensionBundle.getGroupId(),
+                extensionBundle.getArtifactId(),
+                version);
+
+        final ExtensionBundleVersion bundleVersion = 
registryService.getExtensionBundleVersion(versionCoordinate);
+        final StreamingOutput streamingOutput = (output) -> 
registryService.writeExtensionBundleVersionContent(bundleVersion, output);
+
+        return Response.ok(streamingOutput)
+                .header(CONTENT_DISPOSITION_HEADER,"attachment; filename = " + 
bundleVersion.getFilename())
+                .build();
+    }
+
+    @DELETE
+    @Path("bundles/{bundleId}/versions/{version}")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Deletes the given extension bundle version",
+            nickname = "globalDeleteExtensionBundleVersion",
+            response = ExtensionBundleVersion.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = 
"write"),
+                            @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 deleteExtensionBundleVersion(
+            @PathParam("bundleId")
+            @ApiParam("The extension bundle identifier")
+                final String bundleId,
+            @PathParam("version")
+            @ApiParam("The version of the bundle")
+                final String version) {
+
+        final ExtensionBundle extensionBundle = 
getExtensionBundleWithBucketReadAuthorization(bundleId);
+
+        final ExtensionBundleVersionCoordinate versionCoordinate = new 
ExtensionBundleVersionCoordinate(
+                extensionBundle.getBucketIdentifier(),
+                extensionBundle.getGroupId(),
+                extensionBundle.getArtifactId(),
+                version);
+
+        final ExtensionBundleVersion bundleVersion = 
registryService.getExtensionBundleVersion(versionCoordinate);
+
+        final ExtensionBundleVersion deletedBundleVersion = 
registryService.deleteExtensionBundleVersion(bundleVersion);
+        
publish(EventFactory.extensionBundleVersionDeleted(deletedBundleVersion));
+        linkService.populateLinks(deletedBundleVersion);
+
+        return 
Response.status(Response.Status.OK).entity(deletedBundleVersion).build();
+    }
+
+    /**
+     * Retrieves the extension bundle with the given id and ensures the 
current user has authorization to read the bucket it belongs to.
+     *
+     * @param bundleId the bundle id
+     * @return the extension bundle
+     */
+    private ExtensionBundle 
getExtensionBundleWithBucketReadAuthorization(final String bundleId) {
+        final ExtensionBundle extensionBundle = 
registryService.getExtensionBundle(bundleId);
+
+        // 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(extensionBundle.getBucketIdentifier())) {
+            throw new IllegalStateException("Unable to authorize access 
because bucket identifier is null or blank");
+        }
+
+        authorizeBucketAccess(RequestAction.READ, 
extensionBundle.getBucketIdentifier());
+        return extensionBundle;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/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
index afb8e11..9a1fe55 100644
--- 
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
@@ -123,7 +123,7 @@ public class FlowResource extends 
AuthorizableApplicationResource {
         authorizeBucketAccess(RequestAction.READ, flow.getBucketIdentifier());
 
         permissionsService.populateItemPermissions(flow);
-        linkService.populateFlowLinks(flow);
+        linkService.populateLinks(flow);
 
         return Response.status(Response.Status.OK).entity(flow).build();
     }
@@ -164,7 +164,7 @@ public class FlowResource extends 
AuthorizableApplicationResource {
 
         final SortedSet<VersionedFlowSnapshotMetadata> snapshots = 
registryService.getFlowSnapshots(bucketId, flowId);
         if (snapshots != null ) {
-            linkService.populateSnapshotLinks(snapshots);
+            linkService.populateLinks(snapshots);
         }
 
         return Response.status(Response.Status.OK).entity(snapshots).build();
@@ -284,7 +284,7 @@ public class FlowResource extends 
AuthorizableApplicationResource {
 
         authorizeBucketAccess(RequestAction.READ, bucketId);
 
-        linkService.populateSnapshotLinks(latestMetadata);
+        linkService.populateLinks(latestMetadata);
         return 
Response.status(Response.Status.OK).entity(latestMetadata).build();
     }
 
@@ -299,16 +299,16 @@ public class FlowResource extends 
AuthorizableApplicationResource {
 
     private void populateLinksAndPermissions(VersionedFlowSnapshot snapshot) {
         if (snapshot.getSnapshotMetadata() != null) {
-            linkService.populateSnapshotLinks(snapshot.getSnapshotMetadata());
+            linkService.populateLinks(snapshot.getSnapshotMetadata());
         }
 
         if (snapshot.getFlow() != null) {
-            linkService.populateFlowLinks(snapshot.getFlow());
+            linkService.populateLinks(snapshot.getFlow());
         }
 
         if (snapshot.getBucket() != null) {
             permissionsService.populateBucketPermissions(snapshot.getBucket());
-            linkService.populateBucketLinks(snapshot.getBucket());
+            linkService.populateLinks(snapshot.getBucket());
         }
 
     }

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/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
index 02b63d2..b137569 100644
--- 
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
@@ -115,7 +115,7 @@ public class ItemResource extends 
AuthorizableApplicationResource {
             items = Collections.emptyList();
         }
         permissionsService.populateItemPermissions(items);
-        linkService.populateItemLinks(items);
+        linkService.populateLinks(items);
 
         return Response.status(Response.Status.OK).entity(items).build();
     }
@@ -149,7 +149,7 @@ public class ItemResource extends 
AuthorizableApplicationResource {
 
         final List<BucketItem> items = 
registryService.getBucketItems(bucketId);
         permissionsService.populateItemPermissions(items);
-        linkService.populateItemLinks(items);
+        linkService.populateLinks(items);
 
         return Response.status(Response.Status.OK).entity(items).build();
     }

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkBuilder.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkBuilder.java
 
b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkBuilder.java
new file mode 100644
index 0000000..1e2bc29
--- /dev/null
+++ 
b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/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;
+
+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/f1e5aef7/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
index 19e2168..9b2e818 100644
--- 
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
@@ -17,94 +17,239 @@
 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.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.ExtensionBundleVersion;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
 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.apache.nifi.registry.link.LinkableEntity;
 import org.springframework.stereotype.Service;
 
 import javax.ws.rs.core.Link;
+import javax.ws.rs.core.UriBuilder;
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 @Service
 public class LinkService {
 
-    private static final Logger LOGGER = 
LoggerFactory.getLogger(LinkService.class);
+    private static final String BUCKET_PATH = "buckets/{id}";
 
-    private final LinkBuilder<Bucket> bucketLinkBuilder = new 
BucketLinkBuilder();
+    private static final String FLOW_PATH = 
"buckets/{bucketId}/flows/{flowId}";
+    private static final String FLOW_SNAPSHOT_PATH = 
"buckets/{bucketId}/flows/{flowId}/versions/{versionNumber}";
 
-    private final LinkBuilder<VersionedFlow> versionedFlowLinkBuilder = new 
VersionedFlowLinkBuilder();
+    private static final String EXTENSION_BUNDLE_PATH = 
"extensions/bundles/{bundleId}";
+    private static final String EXTENSION_BUNDLE_VERSION_PATH = 
"extensions/bundles/{bundleId}/versions/{version}";
+    private static final String EXTENSION_BUNDLE_VERSION_CONTENT_PATH = 
"extensions/bundles/{bundleId}/versions/{version}/content";
 
-    private final LinkBuilder<VersionedFlowSnapshotMetadata> 
snapshotMetadataLinkBuilder = new VersionedFlowSnapshotLinkBuilder();
+    private static final String EXTENSION_REPO_BUCKET_PATH = 
"extensions/repo/{bucketName}";
+    private static final String EXTENSION_REPO_GROUP_PATH = 
"extensions/repo/{bucketName}/{groupId}";
+    private static final String EXTENSION_REPO_ARTIFACT_PATH = 
"extensions/repo/{bucketName}/{groupId}/{artifactId}";
+    private static final String EXTENSION_REPO_VERSION_PATH = 
"extensions/repo/{bucketName}/{groupId}/{artifactId}/{version}";
 
-    // ---- Bucket Links
 
-    public void populateBucketLinks(final Iterable<Bucket> buckets) {
-        if (buckets == null) {
-            return;
+    private static final LinkBuilder<Bucket> BUCKET_LINK_BUILDER = (bucket) -> 
{
+        if (bucket == null) {
+            return null;
         }
 
-        buckets.forEach(b -> populateBucketLinks(b));
-    }
+        final URI uri = UriBuilder.fromPath(BUCKET_PATH)
+                .resolveTemplate("id", bucket.getIdentifier())
+                .build();
 
-    public void populateBucketLinks(final Bucket bucket) {
-        final Link bucketLink = bucketLinkBuilder.createLink(bucket);
-        bucket.setLink(bucketLink);
-    }
+        return Link.fromUri(uri).rel("self").build();
+    };
 
-    // ---- Flow Links
+    private static final LinkBuilder<VersionedFlow> FLOW_LINK_BUILDER = 
(versionedFlow -> {
+        if (versionedFlow == null) {
+            return null;
+        }
 
-    public void populateFlowLinks(final Iterable<VersionedFlow> 
versionedFlows) {
-        if (versionedFlows == null) {
-            return;
+        final URI uri = UriBuilder.fromPath(FLOW_PATH)
+                .resolveTemplate("bucketId", 
versionedFlow.getBucketIdentifier())
+                .resolveTemplate("flowId", versionedFlow.getIdentifier())
+                .build();
+
+        return Link.fromUri(uri).rel("self").build();
+    });
+
+    private static final LinkBuilder<VersionedFlowSnapshotMetadata> 
FLOW_SNAPSHOT_LINK_BUILDER = (snapshotMetadata) -> {
+        if (snapshotMetadata == null) {
+            return null;
         }
 
-        versionedFlows.forEach(f  -> populateFlowLinks(f));
-    }
+        final URI uri = UriBuilder.fromPath(FLOW_SNAPSHOT_PATH)
+                .resolveTemplate("bucketId", 
snapshotMetadata.getBucketIdentifier())
+                .resolveTemplate("flowId", 
snapshotMetadata.getFlowIdentifier())
+                .resolveTemplate("versionNumber", 
snapshotMetadata.getVersion())
+                .build();
 
-    public void populateFlowLinks(final VersionedFlow versionedFlow) {
-        final Link flowLink = 
versionedFlowLinkBuilder.createLink(versionedFlow);
-        versionedFlow.setLink(flowLink);
-    }
+        return Link.fromUri(uri).rel("content").build();
+    };
 
-    // ---- Flow Snapshot Links
+    private static final LinkBuilder<ExtensionBundle> 
EXTENSION_BUNDLE_LINK_BUILDER = (extensionBundle -> {
+        if (extensionBundle == null) {
+            return null;
+        }
 
-    public void populateSnapshotLinks(final 
Iterable<VersionedFlowSnapshotMetadata> snapshotMetadatas) {
-        if (snapshotMetadatas == null) {
-            return;
+        final URI uri = UriBuilder.fromPath(EXTENSION_BUNDLE_PATH)
+                .resolveTemplate("bundleId", extensionBundle.getIdentifier())
+                .build();
+
+        return Link.fromUri(uri).rel("self").build();
+    });
+
+    private static final LinkBuilder<ExtensionBundleVersionMetadata> 
EXTENSION_BUNDLE_VERSION_LINK_BUILDER = (bundleVersion -> {
+        if (bundleVersion == null) {
+            return null;
         }
 
-        snapshotMetadatas.forEach(s -> populateSnapshotLinks(s));
-    }
+        final URI uri = UriBuilder.fromPath(EXTENSION_BUNDLE_VERSION_PATH)
+                .resolveTemplate("bundleId", 
bundleVersion.getExtensionBundleId())
+                .resolveTemplate("version", bundleVersion.getVersion())
+                .build();
+
+        return Link.fromUri(uri).rel("self").build();
+    });
+
+    private static final LinkBuilder<ExtensionBundleVersion> 
EXTENSION_BUNDLE_VERSION_CONTENT_LINK_BUILDER = (bundleVersion -> {
+        if (bundleVersion == null) {
+            return null;
+        }
+
+        final URI uri = 
UriBuilder.fromPath(EXTENSION_BUNDLE_VERSION_CONTENT_PATH)
+                .resolveTemplate("bundleId", 
bundleVersion.getExtensionBundle().getIdentifier())
+                .resolveTemplate("version", 
bundleVersion.getVersionMetadata().getVersion())
+                .build();
 
-    public void populateSnapshotLinks(final VersionedFlowSnapshotMetadata 
snapshotMetadata) {
-        final Link snapshotLink = 
snapshotMetadataLinkBuilder.createLink(snapshotMetadata);
-        snapshotMetadata.setLink(snapshotLink);
+        return Link.fromUri(uri).rel("self").build();
+    });
+
+    private static final LinkBuilder<ExtensionRepoBucket> 
EXTENSION_REPO_BUCKET_LINK_BUILDER = (extensionRepoBucket -> {
+        if (extensionRepoBucket == null) {
+            return null;
+        }
+
+        final URI uri = UriBuilder.fromPath(EXTENSION_REPO_BUCKET_PATH)
+                .resolveTemplate("bucketName", 
extensionRepoBucket.getBucketName())
+                .build();
+
+        return Link.fromUri(uri).rel("self").build();
+    });
+
+    private static final LinkBuilder<ExtensionRepoGroup> 
EXTENSION_REPO_GROUP_LINK_BUILDER = (extensionRepoGroup -> {
+        if (extensionRepoGroup == null) {
+            return null;
+        }
+
+        final URI uri = UriBuilder.fromPath(EXTENSION_REPO_GROUP_PATH)
+                .resolveTemplate("bucketName", 
extensionRepoGroup.getBucketName())
+                .resolveTemplate("groupId", extensionRepoGroup.getGroupId())
+                .build();
+
+        return Link.fromUri(uri).rel("self").build();
+    });
+
+    private static final LinkBuilder<ExtensionRepoArtifact> 
EXTENSION_REPO_ARTIFACT_LINK_BUILDER = (extensionRepoArtifact -> {
+        if (extensionRepoArtifact == null) {
+            return null;
+        }
+
+        final URI uri = UriBuilder.fromPath(EXTENSION_REPO_ARTIFACT_PATH)
+                .resolveTemplate("bucketName", 
extensionRepoArtifact.getBucketName())
+                .resolveTemplate("groupId", extensionRepoArtifact.getGroupId())
+                .resolveTemplate("artifactId", 
extensionRepoArtifact.getArtifactId())
+                .build();
+
+        return Link.fromUri(uri).rel("self").build();
+    });
+
+    private static final LinkBuilder<ExtensionRepoVersionSummary> 
EXTENSION_REPO_VERSION_LINK_BUILDER = (extensionRepoVersion -> {
+        if (extensionRepoVersion == null) {
+            return null;
+        }
+
+        final URI uri = UriBuilder.fromPath(EXTENSION_REPO_VERSION_PATH)
+                .resolveTemplate("bucketName", 
extensionRepoVersion.getBucketName())
+                .resolveTemplate("groupId", extensionRepoVersion.getGroupId())
+                .resolveTemplate("artifactId", 
extensionRepoVersion.getArtifactId())
+                .resolveTemplate("version", extensionRepoVersion.getVersion())
+                .build();
+
+        return Link.fromUri(uri).rel("self").build();
+    });
+
+
+    private static final Map<Class,LinkBuilder> LINK_BUILDERS;
+    static {
+        final Map<Class,LinkBuilder> builderMap = new HashMap<>();
+        builderMap.put(Bucket.class, BUCKET_LINK_BUILDER);
+        builderMap.put(VersionedFlow.class, FLOW_LINK_BUILDER);
+        builderMap.put(VersionedFlowSnapshotMetadata.class, 
FLOW_SNAPSHOT_LINK_BUILDER);
+        builderMap.put(ExtensionBundle.class, EXTENSION_BUNDLE_LINK_BUILDER);
+        builderMap.put(ExtensionBundleVersionMetadata.class, 
EXTENSION_BUNDLE_VERSION_LINK_BUILDER);
+        builderMap.put(ExtensionBundleVersion.class, 
EXTENSION_BUNDLE_VERSION_CONTENT_LINK_BUILDER);
+        builderMap.put(ExtensionRepoBucket.class, 
EXTENSION_REPO_BUCKET_LINK_BUILDER);
+        builderMap.put(ExtensionRepoGroup.class, 
EXTENSION_REPO_GROUP_LINK_BUILDER);
+        builderMap.put(ExtensionRepoArtifact.class, 
EXTENSION_REPO_ARTIFACT_LINK_BUILDER);
+        builderMap.put(ExtensionRepoVersionSummary.class, 
EXTENSION_REPO_VERSION_LINK_BUILDER);
+        LINK_BUILDERS = Collections.unmodifiableMap(builderMap);
     }
 
-    // ---- BucketItem Links
+    public <E extends LinkableEntity> void populateLinks(final E entity) {
+        final LinkBuilder linkBuilder = LINK_BUILDERS.get(entity.getClass());
+        if (linkBuilder == null) {
+            throw new IllegalArgumentException("No LinkBuilder found for " + 
entity.getClass().getCanonicalName());
+        }
+
+        final Link link = linkBuilder.createLink(entity);
+        entity.setLink(link);
+    }
 
-    public void populateItemLinks(final Iterable<BucketItem> items) {
-        if (items == null) {
+    public <E extends LinkableEntity> void populateLinks(final Iterable<E> 
entities) {
+        if (entities == null) {
             return;
         }
 
-        items.forEach(i -> populateItemLinks(i));
+        entities.forEach(e -> populateLinks(e));
     }
 
-    public void populateItemLinks(final BucketItem bucketItem) {
-        if (bucketItem == null) {
-            return;
+    public <E extends LinkableEntity> void populateFullLinks(final E entity, 
final URI baseUri) {
+        final LinkBuilder linkBuilder = LINK_BUILDERS.get(entity.getClass());
+        if (linkBuilder == null) {
+            throw new IllegalArgumentException("No LinkBuilder found for " + 
entity.getClass().getCanonicalName());
         }
 
-        if (bucketItem instanceof VersionedFlow) {
-            populateFlowLinks((VersionedFlow)bucketItem);
-        } else {
-            LOGGER.error("Unable to create link for BucketItem with type: " + 
bucketItem.getClass().getCanonicalName());
+        if (baseUri == null) {
+            throw new IllegalArgumentException("Base URI cannot be null");
         }
+
+        final Link relativeLink = linkBuilder.createLink(entity);
+        final URI relativeUri = relativeLink.getUri();
+
+        final URI fullUri = UriBuilder.fromUri(baseUri)
+                .path(relativeUri.getPath())
+                .build();
+
+        final Link fullLink = Link.fromUri(fullUri)
+                .rel(relativeLink.getRel())
+                .build();
+
+        entity.setLink(fullLink);
     }
+
+    public <E extends LinkableEntity> void populateFullLinks(final Iterable<E> 
entities, final URI baseUri) {
+        if (entities == null) {
+            return;
+        }
+
+        entities.forEach(e -> populateFullLinks(e, baseUri));
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/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
deleted file mode 100644
index f0409c7..0000000
--- 
a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/BucketLinkBuilder.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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/f1e5aef7/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
deleted file mode 100644
index ec356fd..0000000
--- 
a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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/f1e5aef7/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
deleted file mode 100644
index 38d3d0e..0000000
--- 
a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowLinkBuilder.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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/f1e5aef7/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
deleted file mode 100644
index 4085c6d..0000000
--- 
a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowSnapshotLinkBuilder.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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/f1e5aef7/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoIdentityProvider.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoIdentityProvider.java
 
b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoIdentityProvider.java
index e611b53..f44d766 100644
--- 
a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoIdentityProvider.java
+++ 
b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoIdentityProvider.java
@@ -39,13 +39,11 @@ import org.springframework.security.crypto.codec.Base64;
 import 
org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider;
 import 
org.springframework.security.kerberos.authentication.KerberosServiceRequestToken;
 import 
org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
-import org.springframework.stereotype.Component;
 
 import javax.servlet.http.HttpServletRequest;
 import java.nio.charset.StandardCharsets;
 import java.util.concurrent.TimeUnit;
 
-@Component
 public class KerberosSpnegoIdentityProvider implements IdentityProvider {
 
     private static final Logger logger = 
LoggerFactory.getLogger(KerberosSpnegoIdentityProvider.class);

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
 
b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
index 2410234..2fb2f4a 100644
--- 
a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
+++ 
b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
@@ -16,11 +16,17 @@
  */
 package org.apache.nifi.registry.web.api;
 
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.authorization.CurrentUser;
 import org.apache.nifi.registry.authorization.Permissions;
 import org.apache.nifi.registry.bucket.Bucket;
 import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.bucket.BucketItemType;
 import org.apache.nifi.registry.client.BucketClient;
+import org.apache.nifi.registry.client.ExtensionBundleClient;
+import org.apache.nifi.registry.client.ExtensionBundleVersionClient;
+import org.apache.nifi.registry.client.ExtensionRepoClient;
 import org.apache.nifi.registry.client.FlowClient;
 import org.apache.nifi.registry.client.FlowSnapshotClient;
 import org.apache.nifi.registry.client.ItemsClient;
@@ -30,6 +36,16 @@ import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.registry.client.UserClient;
 import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient;
 import org.apache.nifi.registry.diff.VersionedFlowDifference;
+import org.apache.nifi.registry.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.ExtensionBundleType;
+import org.apache.nifi.registry.extension.ExtensionBundleVersion;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionDependency;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersion;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
 import org.apache.nifi.registry.field.Fields;
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
@@ -37,6 +53,9 @@ import 
org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.registry.flow.VersionedProcessor;
 import org.apache.nifi.registry.flow.VersionedPropertyDescriptor;
+import org.apache.nifi.registry.util.FileUtils;
+import org.bouncycastle.util.encoders.Hex;
+import org.glassfish.jersey.media.multipart.MultiPartFeature;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -44,11 +63,21 @@ import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Test all basic functionality of JerseyNiFiRegistryClient.
@@ -60,7 +89,7 @@ public class UnsecuredNiFiRegistryClientIT extends 
UnsecuredITBase {
     private NiFiRegistryClient client;
 
     @Before
-    public void setup() {
+    public void setup() throws IOException {
         final String baseUrl = createBaseURL();
         LOGGER.info("Using base url = " + baseUrl);
 
@@ -76,6 +105,16 @@ public class UnsecuredNiFiRegistryClientIT extends 
UnsecuredITBase {
 
         Assert.assertNotNull(client);
         this.client = client;
+
+        // Clear the extension bundles storage directory in case previous 
tests left data
+        final File extensionsStorageDir = new 
File("./target/test-classes/extension_bundles");
+        if (extensionsStorageDir.exists()) {
+            try {
+                FileUtils.deleteFile(extensionsStorageDir, true);
+            } catch (Exception e) {
+                LOGGER.warn("Unable to delete extensions storage dir due to: " 
+ e.getMessage(), e);
+            }
+        }
     }
 
     @After
@@ -103,7 +142,7 @@ public class UnsecuredNiFiRegistryClientIT extends 
UnsecuredITBase {
     }
 
     @Test
-    public void testNiFiRegistryClient() throws IOException, 
NiFiRegistryException {
+    public void testNiFiRegistryClient() throws IOException, 
NiFiRegistryException, NoSuchAlgorithmException {
         // ---------------------- TEST BUCKETS --------------------------//
 
         final BucketClient bucketClient = client.getBucketClient();
@@ -281,7 +320,190 @@ public class UnsecuredNiFiRegistryClientIT extends 
UnsecuredITBase {
         Assert.assertEquals(snapshotFlow.getIdentifier(), 
latestMetadataWithoutBucket.getFlowIdentifier());
         Assert.assertEquals(2, latestMetadataWithoutBucket.getVersion());
 
-        // ---------------------- TEST ITEMS --------------------------//
+        // ---------------------- TEST EXTENSIONS ----------------------//
+
+        // verify we have no bundles yet
+        final ExtensionBundleClient bundleClient = 
client.getExtensionBundleClient();
+        final List<ExtensionBundle> allBundles = bundleClient.getAll();
+        Assert.assertEquals(0, allBundles.size());
+
+        final Bucket bundlesBucket = createdBuckets.get(1);
+        final ExtensionBundleVersionClient bundleVersionClient = 
client.getExtensionBundleVersionClient();
+
+        // create version 1.0.0 of nifi-test-nar
+        final String testNar1 = 
"src/test/resources/extensions/nars/nifi-test-nar-1.0.0.nar";
+        final ExtensionBundleVersion createdTestNarV1 = 
createExtensionBundleVersionWithStream(bundlesBucket, bundleVersionClient, 
testNar1, null);
+
+        final ExtensionBundle testNarV1Bundle = 
createdTestNarV1.getExtensionBundle();
+        LOGGER.info("Created bundle with id {}", new 
Object[]{testNarV1Bundle.getIdentifier()});
+
+        Assert.assertEquals("org.apache.nifi", testNarV1Bundle.getGroupId());
+        Assert.assertEquals("nifi-test-nar", testNarV1Bundle.getArtifactId());
+        Assert.assertEquals(ExtensionBundleType.NIFI_NAR, 
testNarV1Bundle.getBundleType());
+        Assert.assertEquals(1, testNarV1Bundle.getVersionCount());
+
+        Assert.assertEquals("org.apache.nifi:nifi-test-nar", 
testNarV1Bundle.getName());
+        Assert.assertEquals(bundlesBucket.getIdentifier(), 
testNarV1Bundle.getBucketIdentifier());
+        Assert.assertEquals(bundlesBucket.getName(), 
testNarV1Bundle.getBucketName());
+        Assert.assertNotNull(testNarV1Bundle.getPermissions());
+        Assert.assertTrue(testNarV1Bundle.getCreatedTimestamp() > 0);
+        Assert.assertTrue(testNarV1Bundle.getModifiedTimestamp() > 0);
+
+        final ExtensionBundleVersionMetadata testNarV1Metadata = 
createdTestNarV1.getVersionMetadata();
+        Assert.assertEquals("1.0.0", testNarV1Metadata.getVersion());
+        Assert.assertNotNull(testNarV1Metadata.getId());
+        Assert.assertNotNull(testNarV1Metadata.getSha256());
+        Assert.assertNotNull(testNarV1Metadata.getAuthor());
+        Assert.assertEquals(testNarV1Bundle.getIdentifier(), 
testNarV1Metadata.getExtensionBundleId());
+        Assert.assertEquals(bundlesBucket.getIdentifier(), 
testNarV1Metadata.getBucketId());
+        Assert.assertTrue(testNarV1Metadata.getTimestamp() > 0);
+        Assert.assertFalse(testNarV1Metadata.getSha256Supplied());
+
+        final Set<ExtensionBundleVersionDependency> dependencies = 
createdTestNarV1.getDependencies();
+        Assert.assertNotNull(dependencies);
+        Assert.assertEquals(1, dependencies.size());
+
+        final ExtensionBundleVersionDependency testNarV1Dependency = 
dependencies.stream().findFirst().get();
+        Assert.assertEquals("org.apache.nifi", 
testNarV1Dependency.getGroupId());
+        Assert.assertEquals("nifi-test-api-nar", 
testNarV1Dependency.getArtifactId());
+        Assert.assertEquals("1.0.0", testNarV1Dependency.getVersion());
+
+        final String testNar2 = 
"src/test/resources/extensions/nars/nifi-test-nar-2.0.0.nar";
+
+        // try to create version 2.0.0 of nifi-test-nar when the supplied 
SHA-256 does not match server's
+        final String madeUpSha256 = "MADE-UP-SHA-256";
+        try {
+            createExtensionBundleVersionWithStream(bundlesBucket, 
bundleVersionClient, testNar2, madeUpSha256);
+            Assert.fail("Should have thrown exception");
+        } catch (Exception e) {
+            // should have thrown exception from mismatched SHA-256
+        }
+
+        // create version 2.0.0 of nifi-test-nar using correct supplied SHA-256
+        final String testNar2Sha256 = calculateSha256Hex(testNar2);
+        final ExtensionBundleVersion createdTestNarV2 = 
createExtensionBundleVersionWithStream(bundlesBucket, bundleVersionClient, 
testNar2, testNar2Sha256);
+        
Assert.assertTrue(createdTestNarV2.getVersionMetadata().getSha256Supplied());
+
+        final ExtensionBundle testNarV2Bundle = 
createdTestNarV2.getExtensionBundle();
+        LOGGER.info("Created bundle with id {}", new 
Object[]{testNarV2Bundle.getIdentifier()});
+
+        // create version 1.0.0 of nifi-foo-nar, use the file variant
+        final String fooNar = 
"src/test/resources/extensions/nars/nifi-foo-nar-1.0.0.nar";
+        final ExtensionBundleVersion createdFooNarV1 = 
createExtensionBundleVersionWithFile(bundlesBucket, bundleVersionClient, 
fooNar, null);
+        
Assert.assertFalse(createdFooNarV1.getVersionMetadata().getSha256Supplied());
+
+        final ExtensionBundle fooNarV1Bundle = 
createdFooNarV1.getExtensionBundle();
+        LOGGER.info("Created bundle with id {}", new 
Object[]{fooNarV1Bundle.getIdentifier()});
+
+        // verify there are 2 bundles now
+        final List<ExtensionBundle> allBundlesAfterCreate = 
bundleClient.getAll();
+        Assert.assertEquals(2, allBundlesAfterCreate.size());
+
+        // verify getting bundles by bucket
+        Assert.assertEquals(2, 
bundleClient.getByBucket(bundlesBucket.getIdentifier()).size());
+        Assert.assertEquals(0, 
bundleClient.getByBucket(flowsBucket.getIdentifier()).size());
+
+        // verify getting bundles by id
+        final ExtensionBundle retrievedBundle = 
bundleClient.get(testNarV1Bundle.getIdentifier());
+        Assert.assertNotNull(retrievedBundle);
+        Assert.assertEquals(testNarV1Bundle.getIdentifier(), 
retrievedBundle.getIdentifier());
+        Assert.assertEquals(testNarV1Bundle.getGroupId(), 
retrievedBundle.getGroupId());
+        Assert.assertEquals(testNarV1Bundle.getArtifactId(), 
retrievedBundle.getArtifactId());
+
+        // verify getting list of version metadata for a bundle
+        final List<ExtensionBundleVersionMetadata> bundleVersions = 
bundleVersionClient.getBundleVersions(testNarV1Bundle.getIdentifier());
+        Assert.assertNotNull(bundleVersions);
+        Assert.assertEquals(2, bundleVersions.size());
+
+        // verify getting a bundle version by the bundle id + version string
+        final ExtensionBundleVersion bundleVersion1 = 
bundleVersionClient.getBundleVersion(testNarV1Bundle.getIdentifier(), "1.0.0");
+        Assert.assertNotNull(bundleVersion1);
+        Assert.assertEquals("1.0.0", 
bundleVersion1.getVersionMetadata().getVersion());
+        Assert.assertNotNull(bundleVersion1.getDependencies());
+        Assert.assertEquals(1, bundleVersion1.getDependencies().size());
+
+        final ExtensionBundleVersion bundleVersion2 = 
bundleVersionClient.getBundleVersion(testNarV1Bundle.getIdentifier(), "2.0.0");
+        Assert.assertNotNull(bundleVersion2);
+        Assert.assertEquals("2.0.0", 
bundleVersion2.getVersionMetadata().getVersion());
+
+        // verify getting the input stream for a bundle version
+        try (final InputStream bundleVersion1InputStream = 
bundleVersionClient.getBundleVersionContent(testNarV1Bundle.getIdentifier(), 
"1.0.0")) {
+            final String sha256Hex = 
DigestUtils.sha256Hex(bundleVersion1InputStream);
+            Assert.assertEquals(testNarV1Metadata.getSha256(), sha256Hex);
+        }
+
+        // verify writing a bundle version to an output stream
+        final File targetDir = new File("./target");
+        final File bundleFile = 
bundleVersionClient.writeBundleVersionContent(testNarV1Bundle.getIdentifier(), 
"1.0.0", targetDir);
+        Assert.assertNotNull(bundleFile);
+
+        try (final InputStream bundleInputStream = new 
FileInputStream(bundleFile)) {
+            final String sha256Hex = DigestUtils.sha256Hex(bundleInputStream);
+            Assert.assertEquals(testNarV1Metadata.getSha256(), sha256Hex);
+        }
+
+        // Verify deleting a bundle version
+        final ExtensionBundleVersion deletedBundleVersion2 = 
bundleVersionClient.delete(testNarV1Bundle.getIdentifier(), "2.0.0");
+        Assert.assertNotNull(deletedBundleVersion2);
+        Assert.assertEquals(testNarV1Bundle.getIdentifier(), 
deletedBundleVersion2.getExtensionBundle().getIdentifier());
+        Assert.assertEquals("2.0.0", 
deletedBundleVersion2.getVersionMetadata().getVersion());
+
+        try {
+            
bundleVersionClient.getBundleVersion(testNarV1Bundle.getIdentifier(), "2.0.0");
+            Assert.fail("Should have thrown exception");
+        } catch (Exception e) {
+            // should catch exception
+        }
+
+        // ---------------------- TEST EXTENSION REPO ----------------------//
+
+        final ExtensionRepoClient extensionRepoClient = 
client.getExtensionRepoClient();
+
+        final List<ExtensionRepoBucket> repoBuckets = 
extensionRepoClient.getBuckets();
+        Assert.assertEquals(createdBuckets.size(), repoBuckets.size());
+
+        final String bundlesBucketName = bundlesBucket.getName();
+        final List<ExtensionRepoGroup> repoGroups = 
extensionRepoClient.getGroups(bundlesBucketName);
+        Assert.assertEquals(1, repoGroups.size());
+
+        final String repoGroupId = "org.apache.nifi";
+        final ExtensionRepoGroup repoGroup = repoGroups.get(0);
+        Assert.assertEquals(repoGroupId, repoGroup.getGroupId());
+
+        final List<ExtensionRepoArtifact> repoArtifacts = 
extensionRepoClient.getArtifacts(bundlesBucketName, repoGroupId);
+        Assert.assertEquals(2, repoArtifacts.size());
+
+        final String repoArtifactId = "nifi-test-nar";
+        final List<ExtensionRepoVersionSummary> repoVersions = 
extensionRepoClient.getVersions(bundlesBucketName, repoGroupId, repoArtifactId);
+        Assert.assertEquals(1, repoVersions.size());
+
+        final String repoVersionString = "1.0.0";
+        final ExtensionRepoVersion repoVersion = 
extensionRepoClient.getVersion(bundlesBucketName, repoGroupId, repoArtifactId, 
repoVersionString);
+        Assert.assertNotNull(repoVersion);
+        Assert.assertNotNull(repoVersion.getDownloadLink());
+        Assert.assertNotNull(repoVersion.getSha256Link());
+
+        // verify the version links for content and sha256
+        final Client jerseyClient = 
ClientBuilder.newBuilder().register(MultiPartFeature.class).build();
+
+        final WebTarget downloadLinkTarget = 
jerseyClient.target(repoVersion.getDownloadLink().getUri());
+        try (final InputStream downloadLinkInputStream = 
downloadLinkTarget.request()
+                
.accept(MediaType.APPLICATION_OCTET_STREAM_TYPE).get().readEntity(InputStream.class))
 {
+            final String sha256DownloadResult = 
DigestUtils.sha256Hex(downloadLinkInputStream);
+
+            final WebTarget sha256LinkTarget = 
jerseyClient.target(repoVersion.getSha256Link().getUri());
+            final String sha256LinkResult = 
sha256LinkTarget.request().get(String.class);
+            Assert.assertEquals(sha256DownloadResult, sha256LinkResult);
+        }
+
+        // verify the client methods for content input stream and content 
sha256
+        try (final InputStream repoVersionInputStream = 
extensionRepoClient.getVersionContent(bundlesBucketName, repoGroupId, 
repoArtifactId, repoVersionString)) {
+            final String sha256Hex = 
DigestUtils.sha256Hex(repoVersionInputStream);
+            final String repoSha256Hex = 
extensionRepoClient.getVersionSha256(bundlesBucketName, repoGroupId, 
repoArtifactId, repoVersionString);
+            Assert.assertEquals(sha256Hex, repoSha256Hex);
+        }
+
+        // ---------------------- TEST ITEMS -------------------------- //
 
         final ItemsClient itemsClient = client.getItemsClient();
 
@@ -292,10 +514,25 @@ public class UnsecuredNiFiRegistryClientIT extends 
UnsecuredITBase {
 
         // get all items
         final List<BucketItem> allItems = itemsClient.getAll();
-        Assert.assertEquals(2, allItems.size());
-        allItems.stream().forEach(i -> 
Assert.assertNotNull(i.getBucketName()));
+        Assert.assertEquals(4, allItems.size());
+        allItems.stream().forEach(i -> {
+            Assert.assertNotNull(i.getBucketName());
+            Assert.assertNotNull(i.getLink());
+        });
         allItems.stream().forEach(i -> LOGGER.info("All items, item " + 
i.getIdentifier()));
 
+        // verify 2 flow items
+        final List<BucketItem> flowItems = allItems.stream()
+                .filter(i -> i.getType() == BucketItemType.Flow)
+                .collect(Collectors.toList());
+        Assert.assertEquals(2, flowItems.size());
+
+        // verify 2 bundle items
+        final List<BucketItem> extensionBundleItems = allItems.stream()
+                .filter(i -> i.getType() == BucketItemType.Extension_Bundle)
+                .collect(Collectors.toList());
+        Assert.assertEquals(2, extensionBundleItems.size());
+
         // get items for bucket
         final List<BucketItem> bucketItems = 
itemsClient.getByBucket(flowsBucket.getIdentifier());
         Assert.assertEquals(2, bucketItems.size());
@@ -325,6 +562,14 @@ public class UnsecuredNiFiRegistryClientIT extends 
UnsecuredITBase {
         Assert.assertNotNull(deletedFlow2);
         LOGGER.info("Deleted flow " + deletedFlow2.getIdentifier());
 
+        final ExtensionBundle deletedBundle1 = 
bundleClient.delete(testNarV1Bundle.getIdentifier());
+        Assert.assertNotNull(deletedBundle1);
+        LOGGER.info("Deleted extension bundle " + 
deletedBundle1.getIdentifier());
+
+        final ExtensionBundle deletedBundle2 = 
bundleClient.delete(fooNarV1Bundle.getIdentifier());
+        Assert.assertNotNull(deletedBundle2);
+        LOGGER.info("Deleted extension bundle " + 
deletedBundle2.getIdentifier());
+
         // delete each bucket
         for (final Bucket bucket : createdBuckets) {
             final Bucket deletedBucket = 
bucketClient.delete(bucket.getIdentifier());
@@ -337,6 +582,58 @@ public class UnsecuredNiFiRegistryClientIT extends 
UnsecuredITBase {
 
     }
 
+    private ExtensionBundleVersion 
createExtensionBundleVersionWithStream(final Bucket bundlesBucket,
+                                                                          
final ExtensionBundleVersionClient bundleVersionClient,
+                                                                          
final String narFile, final String sha256)
+            throws IOException, NiFiRegistryException {
+
+        final ExtensionBundleVersion createdBundleVersion;
+        try (final InputStream bundleInputStream = new 
FileInputStream(narFile)) {
+            if (StringUtils.isBlank(sha256)) {
+                createdBundleVersion = bundleVersionClient.create(
+                        bundlesBucket.getIdentifier(), 
ExtensionBundleType.NIFI_NAR, bundleInputStream);
+            } else {
+                createdBundleVersion = bundleVersionClient.create(
+                        bundlesBucket.getIdentifier(), 
ExtensionBundleType.NIFI_NAR, bundleInputStream, sha256);
+            }
+        }
+
+        Assert.assertNotNull(createdBundleVersion);
+        Assert.assertNotNull(createdBundleVersion.getBucket());
+        Assert.assertNotNull(createdBundleVersion.getExtensionBundle());
+        Assert.assertNotNull(createdBundleVersion.getVersionMetadata());
+
+        return createdBundleVersion;
+    }
+
+    private ExtensionBundleVersion createExtensionBundleVersionWithFile(final 
Bucket bundlesBucket,
+                                                                        final 
ExtensionBundleVersionClient bundleVersionClient,
+                                                                        final 
String narFile, final String sha256)
+            throws IOException, NiFiRegistryException {
+
+        final ExtensionBundleVersion createdBundleVersion;
+        if (StringUtils.isBlank(sha256)) {
+            createdBundleVersion = bundleVersionClient.create(
+                    bundlesBucket.getIdentifier(), 
ExtensionBundleType.NIFI_NAR, new File(narFile));
+        } else {
+            createdBundleVersion = bundleVersionClient.create(
+                    bundlesBucket.getIdentifier(), 
ExtensionBundleType.NIFI_NAR, new File(narFile), sha256);
+        }
+
+        Assert.assertNotNull(createdBundleVersion);
+        Assert.assertNotNull(createdBundleVersion.getBucket());
+        Assert.assertNotNull(createdBundleVersion.getExtensionBundle());
+        Assert.assertNotNull(createdBundleVersion.getVersionMetadata());
+
+        return createdBundleVersion;
+    }
+
+    private String calculateSha256Hex(final String narFile) throws IOException 
{
+        try (final InputStream bundleInputStream = new 
FileInputStream(narFile)) {
+            return Hex.toHexString(DigestUtils.sha256(bundleInputStream));
+        }
+    }
+
     private static Bucket createBucket(BucketClient bucketClient, int num) 
throws IOException, NiFiRegistryException {
         final Bucket bucket = new Bucket();
         bucket.setName("Bucket #" + num);

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java
 
b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java
index bfc9a46..4e1f239 100644
--- 
a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java
+++ 
b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java
@@ -18,17 +18,29 @@ 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.bucket.BucketItemType;
+import org.apache.nifi.registry.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 
+import javax.ws.rs.core.UriBuilder;
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.List;
 
 public class TestLinkService {
 
+    private static final String BASE_URI = 
"http://localhost:18080/nifi-registry-api";;
+    private URI baseUri = UriBuilder.fromUri(BASE_URI).build();
+
     private LinkService linkService;
 
     private List<Bucket> buckets;
@@ -36,6 +48,14 @@ public class TestLinkService {
     private List<VersionedFlowSnapshotMetadata> snapshots;
     private List<BucketItem> items;
 
+    private List<ExtensionBundle> extensionBundles;
+    private List<ExtensionBundleVersionMetadata> 
extensionBundleVersionMetadata;
+
+    private List<ExtensionRepoBucket> extensionRepoBuckets;
+    private List<ExtensionRepoGroup> extensionRepoGroups;
+    private List<ExtensionRepoArtifact> extensionRepoArtifacts;
+    private List<ExtensionRepoVersionSummary> extensionRepoVersions;
+
     @Before
     public void setup() {
         linkService = new LinkService();
@@ -43,11 +63,11 @@ public class TestLinkService {
         // setup buckets
         final Bucket bucket1 = new Bucket();
         bucket1.setIdentifier("b1");
-        bucket1.setName("Bucket 1");
+        bucket1.setName("Bucket_1");
 
         final Bucket bucket2 = new Bucket();
         bucket2.setIdentifier("b2");
-        bucket2.setName("Bucket 2");
+        bucket2.setName("Bucket_2");
 
         buckets = new ArrayList<>();
         buckets.add(bucket1);
@@ -56,12 +76,12 @@ public class TestLinkService {
         // setup flows
         final VersionedFlow flow1 = new VersionedFlow();
         flow1.setIdentifier("f1");
-        flow1.setName("Flow 1");
+        flow1.setName("Flow_1");
         flow1.setBucketIdentifier(bucket1.getIdentifier());
 
         final VersionedFlow flow2 = new VersionedFlow();
         flow2.setIdentifier("f2");
-        flow2.setName("Flow 2");
+        flow2.setName("Flow_2");
         flow2.setBucketIdentifier(bucket1.getIdentifier());
 
         flows = new ArrayList<>();
@@ -83,42 +103,197 @@ public class TestLinkService {
         snapshots.add(snapshotMetadata1);
         snapshots.add(snapshotMetadata2);
 
+        // setup extension bundles
+        final ExtensionBundle bundle1 = new ExtensionBundle();
+        bundle1.setIdentifier("eb1");
+
+        final ExtensionBundle bundle2 = new ExtensionBundle();
+        bundle2.setIdentifier("eb2");
+
+        extensionBundles = new ArrayList<>();
+        extensionBundles.add(bundle1);
+        extensionBundles.add(bundle2);
+
+        // setup extension bundle versions
+        final ExtensionBundleVersionMetadata bundleVersion1 = new 
ExtensionBundleVersionMetadata();
+        bundleVersion1.setExtensionBundleId(bundle1.getIdentifier());
+        bundleVersion1.setVersion("1.0.0");
+
+        final ExtensionBundleVersionMetadata bundleVersion2 = new 
ExtensionBundleVersionMetadata();
+        bundleVersion2.setExtensionBundleId(bundle1.getIdentifier());
+        bundleVersion2.setVersion("2.0.0");
+
+        extensionBundleVersionMetadata = new ArrayList<>();
+        extensionBundleVersionMetadata.add(bundleVersion1);
+        extensionBundleVersionMetadata.add(bundleVersion2);
+
+        // setup extension repo buckets
+        final ExtensionRepoBucket rb1 = new ExtensionRepoBucket();
+        rb1.setBucketName(bucket1.getName());
+
+        final ExtensionRepoBucket rb2 = new ExtensionRepoBucket();
+        rb2.setBucketName(bucket2.getName());
+
+        extensionRepoBuckets = new ArrayList<>();
+        extensionRepoBuckets.add(rb1);
+        extensionRepoBuckets.add(rb2);
+
+        // setup extension repo groups
+        final ExtensionRepoGroup rg1 = new ExtensionRepoGroup();
+        rg1.setBucketName(rb1.getBucketName());
+        rg1.setGroupId("g1");
+
+        final ExtensionRepoGroup rg2 = new ExtensionRepoGroup();
+        rg2.setBucketName(rb1.getBucketName());
+        rg2.setGroupId("g2");
+
+        extensionRepoGroups = new ArrayList<>();
+        extensionRepoGroups.add(rg1);
+        extensionRepoGroups.add(rg2);
+
+        // setup extension repo artifacts
+        final ExtensionRepoArtifact ra1 = new ExtensionRepoArtifact();
+        ra1.setBucketName(rb1.getBucketName());
+        ra1.setGroupId(rg1.getGroupId());
+        ra1.setArtifactId("a1");
+
+        final ExtensionRepoArtifact ra2 = new ExtensionRepoArtifact();
+        ra2.setBucketName(rb1.getBucketName());
+        ra2.setGroupId(rg1.getGroupId());
+        ra2.setArtifactId("a2");
+
+        extensionRepoArtifacts = new ArrayList<>();
+        extensionRepoArtifacts.add(ra1);
+        extensionRepoArtifacts.add(ra2);
+
+        // setup extension repo versions
+        final ExtensionRepoVersionSummary rv1 = new 
ExtensionRepoVersionSummary();
+        rv1.setBucketName(rb1.getBucketName());
+        rv1.setGroupId(rg1.getGroupId());
+        rv1.setArtifactId(ra1.getArtifactId());
+        rv1.setVersion("1.0.0");
+
+        final ExtensionRepoVersionSummary rv2 = new 
ExtensionRepoVersionSummary();
+        rv2.setBucketName(rb1.getBucketName());
+        rv2.setGroupId(rg1.getGroupId());
+        rv2.setArtifactId(ra1.getArtifactId());
+        rv2.setVersion("2.0.0");
+
+        extensionRepoVersions = new ArrayList<>();
+        extensionRepoVersions.add(rv1);
+        extensionRepoVersions.add(rv2);
+
         // setup items
         items = new ArrayList<>();
         items.add(flow1);
         items.add(flow2);
+        items.add(bundle1);
+        items.add(bundle2);
     }
 
     @Test
     public void testPopulateBucketLinks() {
-        buckets.stream().forEach(b -> Assert.assertNull(b.getLink()));
-        linkService.populateBucketLinks(buckets);
-        buckets.stream().forEach(b -> Assert.assertEquals(
+        buckets.forEach(b -> Assert.assertNull(b.getLink()));
+        linkService.populateLinks(buckets);
+        buckets.forEach(b -> Assert.assertEquals(
                 "buckets/" + b.getIdentifier(), 
b.getLink().getUri().toString()));
     }
 
     @Test
     public void testPopulateFlowLinks() {
-        flows.stream().forEach(f -> Assert.assertNull(f.getLink()));
-        linkService.populateFlowLinks(flows);
-        flows.stream().forEach(f -> Assert.assertEquals(
+        flows.forEach(f -> Assert.assertNull(f.getLink()));
+        linkService.populateLinks(flows);
+        flows.forEach(f -> Assert.assertEquals(
                 "buckets/" + f.getBucketIdentifier() + "/flows/" + 
f.getIdentifier(), f.getLink().getUri().toString()));
     }
 
     @Test
     public void testPopulateSnapshotLinks() {
-        snapshots.stream().forEach(s -> Assert.assertNull(s.getLink()));
-        linkService.populateSnapshotLinks(snapshots);
-        snapshots.stream().forEach(s -> Assert.assertEquals(
+        snapshots.forEach(s -> Assert.assertNull(s.getLink()));
+        linkService.populateLinks(snapshots);
+        snapshots.forEach(s -> Assert.assertEquals(
                 "buckets/" + s.getBucketIdentifier() + "/flows/" + 
s.getFlowIdentifier() + "/versions/" + s.getVersion(), 
s.getLink().getUri().toString()));
     }
 
     @Test
     public void testPopulateItemLinks() {
-        items.stream().forEach(i -> Assert.assertNull(i.getLink()));
-        linkService.populateItemLinks(items);
-        items.stream().forEach(i -> Assert.assertEquals(
-                "buckets/" + i.getBucketIdentifier() + "/flows/" + 
i.getIdentifier(), i.getLink().getUri().toString()));
+        items.forEach(i -> Assert.assertNull(i.getLink()));
+        linkService.populateLinks(items);
+        items.forEach(i -> {
+            if (i.getType() == BucketItemType.Flow) {
+                Assert.assertEquals("buckets/" + i.getBucketIdentifier() + 
"/flows/" + i.getIdentifier(), i.getLink().getUri().toString());
+            } else {
+                Assert.assertEquals("extensions/bundles/" + i.getIdentifier(), 
i.getLink().getUri().toString());
+            }
+        });
     }
 
+    @Test
+    public void testPopulateExtensionBundleLinks() {
+        extensionBundles.forEach(i -> Assert.assertNull(i.getLink()));
+        linkService.populateLinks(extensionBundles);
+        extensionBundles.forEach(eb -> 
Assert.assertEquals("extensions/bundles/" + eb.getIdentifier(), 
eb.getLink().getUri().toString()));
+    }
+
+    @Test
+    public void testPopulateExtensionBundleVersionLinks() {
+        extensionBundleVersionMetadata.forEach(i -> 
Assert.assertNull(i.getLink()));
+        linkService.populateLinks(extensionBundleVersionMetadata);
+        extensionBundleVersionMetadata.forEach(eb -> Assert.assertEquals(
+                "extensions/bundles/" + eb.getExtensionBundleId() + 
"/versions/" + eb.getVersion(), eb.getLink().getUri().toString()));
+    }
+
+    @Test
+    public void testPopulateExtensionRepoBucketLinks() {
+        extensionRepoBuckets.forEach(i -> Assert.assertNull(i.getLink()));
+        linkService.populateLinks(extensionRepoBuckets);
+        extensionRepoBuckets.forEach(i -> Assert.assertEquals(
+                "extensions/repo/" + i.getBucketName(),
+                i.getLink().getUri().toString())
+        );
+    }
+
+    @Test
+    public void testPopulateExtensionRepoGroupLinks() {
+        extensionRepoGroups.forEach(i -> Assert.assertNull(i.getLink()));
+        linkService.populateLinks(extensionRepoGroups);
+        extensionRepoGroups.forEach(i -> {
+            Assert.assertEquals(
+                    "extensions/repo/" + i.getBucketName() + "/" + 
i.getGroupId(),
+                    i.getLink().getUri().toString()); }
+        );
+    }
+
+    @Test
+    public void testPopulateExtensionRepoArtifactLinks() {
+        extensionRepoArtifacts.forEach(i -> Assert.assertNull(i.getLink()));
+        linkService.populateLinks(extensionRepoArtifacts);
+        extensionRepoArtifacts.forEach(i -> {
+            Assert.assertEquals(
+                    "extensions/repo/" + i.getBucketName() + "/" + 
i.getGroupId() + "/" + i.getArtifactId(),
+                    i.getLink().getUri().toString()); }
+        );
+    }
+
+    @Test
+    public void testPopulateExtensionRepoVersionLinks() {
+        extensionRepoVersions.forEach(i -> Assert.assertNull(i.getLink()));
+        linkService.populateLinks(extensionRepoVersions);
+        extensionRepoVersions.forEach(i -> {
+            Assert.assertEquals(
+                    "extensions/repo/" + i.getBucketName() + "/" + 
i.getGroupId() + "/" + i.getArtifactId() + "/" + i.getVersion(),
+                    i.getLink().getUri().toString()); }
+        );
+    }
+
+    @Test
+    public void testPopulateExtensionRepoVersionFullLinks() {
+        extensionRepoVersions.forEach(i -> Assert.assertNull(i.getLink()));
+        linkService.populateFullLinks(extensionRepoVersions, baseUri);
+        extensionRepoVersions.forEach(i -> {
+            Assert.assertEquals(
+                    BASE_URI + "/extensions/repo/" + i.getBucketName() + "/" + 
i.getGroupId() + "/" + i.getArtifactId() + "/" + i.getVersion(),
+                    i.getLink().getUri().toString()); }
+        );
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-web-api/src/test/resources/application.properties
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/test/resources/application.properties
 
b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application.properties
index efa0290..721b949 100644
--- 
a/nifi-registry-core/nifi-registry-web-api/src/test/resources/application.properties
+++ 
b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application.properties
@@ -23,3 +23,6 @@
 #logging.level.org.springframework.core.io.support: DEBUG
 #logging.level.org.springframework.context.annotation: DEBUG
 #logging.level.org.springframework.web: DEBUG
+
+# Need to allow overriding of beans in integration tests
+spring.main.allow-bean-definition-overriding=true
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers.xml
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers.xml
 
b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers.xml
index fd002be..c5609ec 100644
--- 
a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers.xml
+++ 
b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers.xml
@@ -22,4 +22,9 @@
         <property name="Flow Storage 
Directory">./target/test-classes/flow_storage</property>
     </flowPersistenceProvider>
 
+    <extensionBundlePersistenceProvider>
+        
<class>org.apache.nifi.registry.provider.extension.FileSystemExtensionBundlePersistenceProvider</class>
+        <property name="Extension Bundle Storage 
Directory">./target/test-classes/extension_bundles</property>
+    </extensionBundlePersistenceProvider>
+
 </providers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured/nifi-registry.properties
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured/nifi-registry.properties
 
b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured/nifi-registry.properties
index 113773c..70ca5e3 100644
--- 
a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured/nifi-registry.properties
+++ 
b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured/nifi-registry.properties
@@ -21,5 +21,8 @@ nifi.registry.web.http.host=localhost
 # providers properties #
 
nifi.registry.providers.configuration.file=./target/test-classes/conf/providers.xml
 
+# extensions working dir #
+nifi.registry.extensions.working.directory=./target/work/extensions
+
 # database properties
 nifi.registry.db.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-1.0.0.nar
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-1.0.0.nar
 
b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-1.0.0.nar
new file mode 100644
index 0000000..4a91f31
Binary files /dev/null and 
b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-1.0.0.nar
 differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-1.0.0.nar
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-1.0.0.nar
 
b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-1.0.0.nar
new file mode 100644
index 0000000..c3fccf3
Binary files /dev/null and 
b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-1.0.0.nar
 differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-bad-manifest.nar
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-bad-manifest.nar
 
b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-bad-manifest.nar
new file mode 100644
index 0000000..4400589
Binary files /dev/null and 
b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-bad-manifest.nar
 differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-diff-checksum.nar
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-diff-checksum.nar
 
b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-diff-checksum.nar
new file mode 100644
index 0000000..7055b64
Binary files /dev/null and 
b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-diff-checksum.nar
 differ

Reply via email to