http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java
 
b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java
new file mode 100644
index 0000000..eea6969
--- /dev/null
+++ 
b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java
@@ -0,0 +1,445 @@
+/*
+ * 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 org.apache.nifi.registry.bucket.BucketItemType;
+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.flow.VersionedProcessGroup;
+import org.junit.Assert;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.test.context.jdbc.Sql;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import static 
org.apache.nifi.registry.web.api.IntegrationTestUtils.assertFlowSnapshotMetadataEqual;
+import static 
org.apache.nifi.registry.web.api.IntegrationTestUtils.assertFlowSnapshotsEqual;
+import static 
org.apache.nifi.registry.web.api.IntegrationTestUtils.assertFlowsEqual;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = 
{"classpath:db/clearDB.sql", "classpath:db/FlowsIT.sql"})
+public class FlowsIT extends UnsecuredITBase {
+
+    @Test
+    public void testGetFlowsEmpty() throws Exception {
+
+        // Given: an empty bucket with id "3" (see FlowsIT.sql)
+        final String emptyBucketId = "3";
+
+        // When: the /buckets/{id}/flows endpoint is queried
+
+        final VersionedFlow[] flows = client
+                .target(createURL("buckets/{bucketId}/flows"))
+                .resolveTemplate("bucketId", emptyBucketId)
+                .request()
+                .get(VersionedFlow[].class);
+
+        // Then: an empty array is returned
+
+        assertNotNull(flows);
+        assertEquals(0, flows.length);
+    }
+
+    @Test
+    public void testGetFlows() throws Exception {
+
+        // Given: a few buckets and flows have been populated in the DB (see 
FlowsIT.sql)
+
+        final String prePopulatedBucketId = "1";
+        final String expected = "[" +
+                "{\"identifier\":\"1\"," +
+                "\"name\":\"Flow 1\"," +
+                "\"description\":\"This is flow 1\"," +
+                "\"bucketIdentifier\":\"1\"," +
+                "\"createdTimestamp\":1505091360000," +
+                "\"modifiedTimestamp\":1505091360000," +
+                "\"type\":\"Flow\"," +
+                
"\"permissions\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                
"\"link\":{\"params\":{\"rel\":\"self\"},\"href\":\"buckets/1/flows/1\"}}," +
+                "{\"identifier\":\"2\",\"name\":\"Flow 2\"," +
+                "\"description\":\"This is flow 2\"," +
+                "\"bucketIdentifier\":\"1\"," +
+                "\"createdTimestamp\":1505091360000," +
+                "\"modifiedTimestamp\":1505091360000," +
+                "\"type\":\"Flow\"," +
+                
"\"permissions\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"versionCount\":0," +
+                
"\"link\":{\"params\":{\"rel\":\"self\"},\"href\":\"buckets/1/flows/2\"}}" +
+                "]";
+
+        // When: the /buckets/{id}/flows endpoint is queried
+
+        final String flowsJson = client
+                .target(createURL("buckets/{bucketId}/flows"))
+                .resolveTemplate("bucketId", prePopulatedBucketId)
+                .request()
+                .get(String.class);
+
+        // Then: the pre-populated list of flows is returned
+
+        JSONAssert.assertEquals(expected, flowsJson, false);
+    }
+
+    @Test
+    public void testCreateFlowGetFlow() throws Exception {
+
+        // Given: an empty bucket with id "3" (see FlowsIT.sql)
+
+        long testStartTime = System.currentTimeMillis();
+        final String bucketId = "3";
+
+        // When: a flow is created
+
+        final VersionedFlow flow = new VersionedFlow();
+        flow.setBucketIdentifier(bucketId);
+        flow.setName("Test Flow");
+        flow.setDescription("This is a flow created by an integration test.");
+
+        final VersionedFlow createdFlow = client
+                .target(createURL("buckets/{bucketId}/flows"))
+                .resolveTemplate("bucketId", bucketId)
+                .request()
+                .post(Entity.entity(flow, MediaType.APPLICATION_JSON), 
VersionedFlow.class);
+
+        // Then: the server returns the created flow, with server-set fields 
populated correctly
+
+        assertFlowsEqual(flow, createdFlow, false);
+        assertNotNull(createdFlow.getIdentifier());
+        assertNotNull(createdFlow.getBucketName());
+        assertEquals(0, createdFlow.getVersionCount());
+        assertEquals(createdFlow.getType(), BucketItemType.Flow);
+        assertTrue(createdFlow.getCreatedTimestamp() - testStartTime > 0L); // 
both server and client in same JVM, so there shouldn't be skew
+        assertEquals(createdFlow.getCreatedTimestamp(), 
createdFlow.getModifiedTimestamp());
+        assertNotNull(createdFlow.getLink());
+        assertNotNull(createdFlow.getLink().getUri());
+
+        // And when .../flows is queried, then the newly created flow is 
returned in the list
+
+        final VersionedFlow[] flows = client
+                .target(createURL("buckets/{bucketId}/flows"))
+                .resolveTemplate("bucketId", bucketId)
+                .request()
+                .get(VersionedFlow[].class);
+        assertNotNull(flows);
+        assertEquals(1, flows.length);
+        assertFlowsEqual(createdFlow, flows[0], true);
+
+        // And when the link URI is queried, then the newly created flow is 
returned
+
+        final VersionedFlow flowByLink = client
+                .target(createURL(flows[0].getLink().getUri().toString()))
+                .request()
+                .get(VersionedFlow.class);
+        assertFlowsEqual(createdFlow, flowByLink, true);
+
+        // And when the bucket is queried by .../flows/ID, then the newly 
created flow is returned
+
+        final VersionedFlow flowById = client
+                .target(createURL("buckets/{bucketId}/flows/{flowId}"))
+                .resolveTemplate("bucketId", bucketId)
+                .resolveTemplate("flowId", createdFlow.getIdentifier())
+                .request()
+                .get(VersionedFlow.class);
+        assertFlowsEqual(createdFlow, flowById, true);
+
+    }
+
+    @Test
+    public void testUpdateFlow() throws Exception {
+
+        // Given: a flow exists on the server
+
+        final String bucketId = "3";
+        final VersionedFlow flow = new VersionedFlow();
+        flow.setBucketIdentifier(bucketId);
+        flow.setName("Test Flow");
+        flow.setDescription("This is a flow created by an integration test.");
+        final VersionedFlow createdFlow = client
+                .target(createURL("buckets/{bucketId}/flows"))
+                .resolveTemplate("bucketId", bucketId)
+                .request()
+                .post(Entity.entity(flow, MediaType.APPLICATION_JSON), 
VersionedFlow.class);
+
+        // When: the flow is modified by the client and updated on the server
+
+        createdFlow.setName("Renamed Flow");
+        createdFlow.setDescription("This flow has been updated by an 
integration test.");
+
+        final VersionedFlow updatedFlow = client
+                .target(createURL("buckets/{bucketId}/flows/{flowId}"))
+                .resolveTemplate("bucketId", bucketId)
+                .resolveTemplate("flowId", createdFlow.getIdentifier())
+                .request()
+                .put(Entity.entity(createdFlow, MediaType.APPLICATION_JSON), 
VersionedFlow.class);
+
+        // Then: the server returns the updated flow, with a new modified 
timestamp
+
+        assertTrue(updatedFlow.getModifiedTimestamp() > 
createdFlow.getModifiedTimestamp());
+        createdFlow.setModifiedTimestamp(updatedFlow.getModifiedTimestamp());
+        assertFlowsEqual(createdFlow, updatedFlow, true);
+
+    }
+
+    @Test
+    public void testDeleteBucket() throws Exception {
+
+        // Given: a flow exists on the server
+
+        final String bucketId = "3";
+        final VersionedFlow flow = new VersionedFlow();
+        flow.setBucketIdentifier(bucketId);
+        flow.setName("Test Flow");
+        flow.setDescription("This is a flow created by an integration test.");
+        final VersionedFlow createdFlow = client
+                .target(createURL("buckets/{bucketId}/flows"))
+                .resolveTemplate("bucketId", bucketId)
+                .request()
+                .post(Entity.entity(flow, MediaType.APPLICATION_JSON), 
VersionedFlow.class);
+
+        // When: the flow is deleted
+
+        final VersionedFlow deletedFlow = client
+                .target(createURL("buckets/{bucketId}/flows/{flowId}"))
+                .resolveTemplate("bucketId", bucketId)
+                .resolveTemplate("flowId", createdFlow.getIdentifier())
+                .request()
+                .delete(VersionedFlow.class);
+
+        // Then: the body of the server response matches the flow that was 
deleted
+        //  and: the flow is no longer accessible (resource not found)
+
+        createdFlow.setLink(null); // self URI will not be present in 
deletedBucket
+        assertFlowsEqual(createdFlow, deletedFlow, true);
+
+        final Response response = client
+                .target(createURL("buckets/{bucketId}/flows/{flowId}"))
+                .resolveTemplate("bucketId", bucketId)
+                .resolveTemplate("flowId", createdFlow.getIdentifier())
+                .request()
+                .get();
+        assertEquals(404, response.getStatus());
+
+    }
+
+    @Test
+    public void testGetFlowVersionsEmpty() throws Exception {
+
+        // Given: a Bucket "2" containing a flow "3" with no snapshots (see 
FlowsIT.sql)
+        final String bucketId = "2";
+        final String flowId = "3";
+
+        // When: the /buckets/{id}/flows/{id}/versions endpoint is queried
+
+        final VersionedFlowSnapshot[] flowSnapshots = client
+                
.target(createURL("buckets/{bucketId}/flows/{flowId}/versions"))
+                .resolveTemplate("bucketId", bucketId)
+                .resolveTemplate("flowId", flowId)
+                .request()
+                .get(VersionedFlowSnapshot[].class);
+
+        // Then: an empty array is returned
+
+        assertNotNull(flowSnapshots);
+        assertEquals(0, flowSnapshots.length);
+    }
+
+    @Test
+    public void testGetFlowVersions() throws Exception {
+
+        // Given: a bucket "1" with flow "1" with existing snapshots has been 
populated in the DB (see FlowsIT.sql)
+
+        final String prePopulatedBucketId = "1";
+        final String prePopulatedFlowId = "1";
+        // For this test case, the order of the expected list matters as we 
are asserting a strict equality check
+        final String expected = "[" +
+                "{\"bucketIdentifier\":\"1\"," +
+                "\"flowIdentifier\":\"1\"," +
+                "\"version\":2," +
+                "\"timestamp\":1505091480000," +
+                "\"author\" : \"user2\"," +
+                "\"comments\":\"This is flow 1 snapshot 2\"," +
+                
"\"link\":{\"params\":{\"rel\":\"content\"},\"href\":\"buckets/1/flows/1/versions/2\"}},"
 +
+                "{\"bucketIdentifier\":\"1\"," +
+                "\"flowIdentifier\":\"1\"," +
+                "\"version\":1," +
+                "\"timestamp\":1505091420000," +
+                "\"author\" : \"user1\"," +
+                "\"comments\":\"This is flow 1 snapshot 1\"," +
+                
"\"link\":{\"params\":{\"rel\":\"content\"},\"href\":\"buckets/1/flows/1/versions/1\"}}"
 +
+                "]";
+
+        // When: the /buckets/{id}/flows/{id}/versions endpoint is queried
+        final String flowSnapshotsJson = client
+                
.target(createURL("buckets/{bucketId}/flows/{flowId}/versions"))
+                .resolveTemplate("bucketId", prePopulatedBucketId)
+                .resolveTemplate("flowId", prePopulatedFlowId)
+                .request()
+                .get(String.class);
+
+        // Then: the pre-populated list of flow versions is returned, in 
descending order
+        JSONAssert.assertEquals(expected, flowSnapshotsJson, true);
+
+    }
+
+    @Test
+    public void testCreateFlowVersionGetFlowVersion() throws Exception {
+
+        // Given: an empty Bucket "3" (see FlowsIT.sql) with a newly created 
flow
+
+        long testStartTime = System.currentTimeMillis();
+        final String bucketId = "2";
+        final VersionedFlow flow = new VersionedFlow();
+        flow.setBucketIdentifier(bucketId);
+        flow.setName("Test Flow for creating snapshots");
+        flow.setDescription("This is a randomly named flow created by an 
integration test for the purpose of holding snapshots.");
+        final VersionedFlow createdFlow = client
+                .target(createURL("buckets/{bucketId}/flows"))
+                .resolveTemplate("bucketId", bucketId)
+                .request()
+                .post(Entity.entity(flow, MediaType.APPLICATION_JSON), 
VersionedFlow.class);
+        final String flowId = createdFlow.getIdentifier();
+
+        // When: an initial flow snapshot is created *without* a version
+
+        final VersionedFlowSnapshotMetadata flowSnapshotMetadata = new 
VersionedFlowSnapshotMetadata();
+        flowSnapshotMetadata.setBucketIdentifier("2");
+        flowSnapshotMetadata.setFlowIdentifier(flowId);
+        flowSnapshotMetadata.setComments("This is snapshot 1, created by an 
integration test.");
+        final VersionedFlowSnapshot flowSnapshot = new VersionedFlowSnapshot();
+        flowSnapshot.setSnapshotMetadata(flowSnapshotMetadata);
+        flowSnapshot.setFlowContents(new VersionedProcessGroup()); // an empty 
root process group
+
+        WebTarget clientRequestTarget = client
+                
.target(createURL("buckets/{bucketId}/flows/{flowId}/versions"))
+                .resolveTemplate("bucketId", bucketId)
+                .resolveTemplate("flowId", flowId);
+        final Response response =
+                clientRequestTarget.request().post(Entity.entity(flowSnapshot, 
MediaType.APPLICATION_JSON), Response.class);
+
+        // Then: an error is returned because version != 1
+
+        assertEquals(400, response.getStatus());
+
+        // But When: an initial flow snapshot is created with version == 1
+
+        flowSnapshot.getSnapshotMetadata().setVersion(1);
+        final VersionedFlowSnapshot createdFlowSnapshot =
+                clientRequestTarget.request().post(Entity.entity(flowSnapshot, 
MediaType.APPLICATION_JSON), VersionedFlowSnapshot.class);
+
+        // Then: the server returns the created flow snapshot, with server-set 
fields populated correctly :)
+
+        assertFlowSnapshotsEqual(flowSnapshot, createdFlowSnapshot, false);
+        assertTrue(createdFlowSnapshot.getSnapshotMetadata().getTimestamp() - 
testStartTime > 0L); // both server and client in same JVM, so there shouldn't 
be skew
+        assertEquals("anonymous", 
createdFlowSnapshot.getSnapshotMetadata().getAuthor());
+        assertNotNull(createdFlowSnapshot.getSnapshotMetadata().getLink());
+        
assertNotNull(createdFlowSnapshot.getSnapshotMetadata().getLink().getUri());
+        assertNotNull(createdFlowSnapshot.getFlow());
+        assertEquals(1, createdFlowSnapshot.getFlow().getVersionCount());
+        assertNotNull(createdFlowSnapshot.getBucket());
+
+        // And when .../flows/{id}/versions is queried, then the newly created 
flow snapshot is returned in the list
+
+        final VersionedFlowSnapshotMetadata[] versionedFlowSnapshots =
+                
clientRequestTarget.request().get(VersionedFlowSnapshotMetadata[].class);
+        assertNotNull(versionedFlowSnapshots);
+        assertEquals(1, versionedFlowSnapshots.length);
+        
assertFlowSnapshotMetadataEqual(createdFlowSnapshot.getSnapshotMetadata(), 
versionedFlowSnapshots[0], true);
+
+        // And when the link URI is queried, then the newly created flow 
snapshot is returned
+
+        final VersionedFlowSnapshot flowSnapshotByLink = client
+                
.target(createURL(versionedFlowSnapshots[0].getLink().getUri().toString()))
+                .request()
+                .get(VersionedFlowSnapshot.class);
+        assertFlowSnapshotsEqual(createdFlowSnapshot, flowSnapshotByLink, 
true);
+        assertNotNull(flowSnapshotByLink.getFlow());
+        assertNotNull(flowSnapshotByLink.getBucket());
+
+        // And when the bucket is queried by .../versions/{v}, then the newly 
created flow snapshot is returned
+
+        final VersionedFlowSnapshot flowSnapshotByVersionNumber = 
clientRequestTarget.path("/1").request().get(VersionedFlowSnapshot.class);
+        assertFlowSnapshotsEqual(createdFlowSnapshot, 
flowSnapshotByVersionNumber, true);
+        assertNotNull(flowSnapshotByVersionNumber.getFlow());
+        assertNotNull(flowSnapshotByVersionNumber.getBucket());
+
+        // And when the latest URI is queried, then the newly created flow 
snapshot is returned
+
+        final VersionedFlowSnapshot flowSnapshotByLatest = 
clientRequestTarget.path("/latest").request().get(VersionedFlowSnapshot.class);
+        assertFlowSnapshotsEqual(createdFlowSnapshot, flowSnapshotByLatest, 
true);
+        assertNotNull(flowSnapshotByLatest.getFlow());
+        assertNotNull(flowSnapshotByLatest.getBucket());
+
+    }
+
+    @Test
+    public void testFlowNameUniquePerBucket() throws Exception {
+
+        final String flowName = "Flow 1";
+
+        // verify we have an existing flow with the name "Flow 1" in bucket 1
+        final VersionedFlow existingFlow = client
+                .target(createURL("buckets/1/flows/1"))
+                .request()
+                .get(VersionedFlow.class);
+
+        assertNotNull(existingFlow);
+        assertEquals(flowName, existingFlow.getName());
+
+        // create a new flow with the same name
+
+        final String bucketId = "3";
+
+        final VersionedFlow flow = new VersionedFlow();
+        flow.setBucketIdentifier(bucketId);
+        flow.setName(flowName);
+        flow.setDescription("This is a flow created by an integration test.");
+
+        // saving this flow to bucket 3 should work because bucket 3 is empty
+
+        final VersionedFlow createdFlow = client
+                .target(createURL("buckets/3/flows"))
+                .resolveTemplate("bucketId", bucketId)
+                .request()
+                .post(Entity.entity(flow, MediaType.APPLICATION_JSON), 
VersionedFlow.class);
+
+        assertNotNull(createdFlow);
+
+        // saving the flow to bucket 1 should not work because there is a flow 
with the same name
+        flow.setBucketIdentifier("1");
+        try {
+            client.target(createURL("buckets/1/flows"))
+                    .resolveTemplate("bucketId", bucketId)
+                    .request()
+                    .post(Entity.entity(flow, MediaType.APPLICATION_JSON), 
VersionedFlow.class);
+
+            Assert.fail("Should have thrown exception");
+        } catch (WebApplicationException e) {
+            final String errorMessage = 
e.getResponse().readEntity(String.class);
+            Assert.assertEquals("A versioned flow with the same name already 
exists in the selected bucket", errorMessage);
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestBase.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestBase.java
 
b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestBase.java
new file mode 100644
index 0000000..9f3d439
--- /dev/null
+++ 
b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestBase.java
@@ -0,0 +1,219 @@
+/*
+ * 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 com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
+import org.apache.nifi.registry.client.NiFiRegistryClientConfig;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.glassfish.jersey.client.ClientConfig;
+import 
org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.TestConfiguration;
+import 
org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
+import org.springframework.boot.web.server.LocalServerPort;
+import org.springframework.context.annotation.Bean;
+
+import javax.annotation.PostConstruct;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * A base class to simplify creating integration tests against an API 
application running with an embedded server and volatile DB.
+ */
+public abstract class IntegrationTestBase {
+
+    private static final String CONTEXT_PATH = "/nifi-registry-api";
+
+    @TestConfiguration
+    public static class TestConfigurationClass {
+
+        /* REQUIRED: Any subclass extending IntegrationTestBase must add a 
Spring profile that defines a
+         * property value for this key containing the path to the 
nifi-registy.properties file to use to
+         * create a NiFiRegistryProperties Bean in the ApplicationContext.  */
+        @Value("${nifi.registry.properties.file}")
+        private String propertiesFileLocation;
+
+        private final ReentrantReadWriteLock lock = new 
ReentrantReadWriteLock();
+        private final Lock readLock = lock.readLock();
+        private NiFiRegistryProperties testProperties;
+
+        @Bean
+        public JettyServletWebServerFactory 
jettyEmbeddedServletContainerFactory() {
+            JettyServletWebServerFactory jettyContainerFactory = new 
JettyServletWebServerFactory();
+            jettyContainerFactory.setContextPath(CONTEXT_PATH);
+            return jettyContainerFactory;
+        }
+
+        @Bean
+        public NiFiRegistryProperties getNiFiRegistryProperties() {
+            readLock.lock();
+            try {
+                if (testProperties == null) {
+                    testProperties = 
loadNiFiRegistryProperties(propertiesFileLocation);
+                }
+            } finally {
+                readLock.unlock();
+            }
+            return testProperties;
+        }
+
+    }
+
+    @Autowired
+    private NiFiRegistryProperties properties;
+
+    /* OPTIONAL: Any subclass that extends this base class MAY provide or 
specify a @TestConfiguration that provides a
+     * NiFiRegistryClientConfig @Bean. The properties specified should 
correspond with the integration test cases in
+     * the concrete subclass. See SecureFileIT for an example. */
+    @Autowired(required = false)
+    private NiFiRegistryClientConfig clientConfig;
+
+    /* This will be injected with the random port assigned to the embedded 
Jetty container. */
+    @LocalServerPort
+    private int port;
+
+    /**
+     * Subclasses can access this auto-configured JAX-RS client to communicate 
to the NiFi Registry Server
+     */
+    protected Client client;
+
+    @PostConstruct
+    void initialize() {
+        if (this.clientConfig != null) {
+            this.client = createClientFromConfig(this.clientConfig);
+        } else {
+            this.client = ClientBuilder.newClient();
+        }
+
+    }
+
+    /**
+     * Subclasses can utilize this method to build a URL that has the correct 
protocol, hostname, and port
+     * for a given path.
+     *
+     * @param relativeResourcePath the path component of the resource you wish 
to access, relative to the
+     *                             base API URL, where the base includes the 
servlet context path.
+     *
+     * @return a String containing the absolute URL of the resource.
+     */
+    String createURL(String relativeResourcePath) {
+        if (relativeResourcePath == null) {
+            throw new IllegalArgumentException("Resource path cannot be null");
+        }
+
+        final StringBuilder baseUriBuilder = new 
StringBuilder(createBaseURL()).append(CONTEXT_PATH);
+
+        if (!relativeResourcePath.startsWith("/")) {
+            baseUriBuilder.append('/');
+        }
+        baseUriBuilder.append(relativeResourcePath);
+
+        return baseUriBuilder.toString();
+    }
+
+    /**
+     * Sub-classes can utilize this method to obtain the base-url for a client.
+     *
+     * @return a string containing the base url which includes the scheme, 
host, and port
+     */
+    String createBaseURL() {
+        final boolean isSecure = this.properties.getSslPort() != null;
+        final String protocolSchema = isSecure ? "https" : "http";
+
+        final StringBuilder baseUriBuilder = new StringBuilder()
+                .append(protocolSchema).append("://localhost:").append(port);
+
+        return baseUriBuilder.toString();
+    }
+
+    NiFiRegistryClientConfig createClientConfig(String baseUrl) {
+        final NiFiRegistryClientConfig.Builder builder = new 
NiFiRegistryClientConfig.Builder();
+        builder.baseUrl(baseUrl);
+
+        if (this.clientConfig != null) {
+            if (this.clientConfig.getSslContext() != null) {
+                builder.sslContext(this.clientConfig.getSslContext());
+            }
+
+            if (this.clientConfig.getHostnameVerifier() != null) {
+                
builder.hostnameVerifier(this.clientConfig.getHostnameVerifier());
+            }
+        }
+
+        return builder.build();
+    }
+
+    /**
+     * A helper method for loading NiFiRegistryProperties by reading 
*.properties files from disk.
+     *
+     * @param propertiesFilePath The location of the properties file
+     * @return A NiFIRegistryProperties instance based on the properties file 
contents
+     */
+    static NiFiRegistryProperties loadNiFiRegistryProperties(String 
propertiesFilePath) {
+        NiFiRegistryProperties properties = new NiFiRegistryProperties();
+        try (final FileReader reader = new FileReader(propertiesFilePath)) {
+            properties.load(reader);
+        } catch (final IOException ioe) {
+            throw new RuntimeException("Unable to load properties: " + ioe, 
ioe);
+        }
+        return properties;
+    }
+
+    private static Client createClientFromConfig(NiFiRegistryClientConfig 
registryClientConfig) {
+
+        final ClientConfig clientConfig = new ClientConfig();
+        clientConfig.register(jacksonJaxbJsonProvider());
+
+        final ClientBuilder clientBuilder = 
ClientBuilder.newBuilder().withConfig(clientConfig);
+
+        final SSLContext sslContext = registryClientConfig.getSslContext();
+        if (sslContext != null) {
+            clientBuilder.sslContext(sslContext);
+        }
+
+        final HostnameVerifier hostnameVerifier = 
registryClientConfig.getHostnameVerifier();
+        if (hostnameVerifier != null) {
+            clientBuilder.hostnameVerifier(hostnameVerifier);
+        }
+
+        return clientBuilder.build();
+    }
+
+    private static JacksonJaxbJsonProvider jacksonJaxbJsonProvider() {
+        JacksonJaxbJsonProvider jacksonJaxbJsonProvider = new 
JacksonJaxbJsonProvider();
+
+        ObjectMapper mapper = new ObjectMapper();
+        
mapper.setPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_NULL,
 JsonInclude.Include.NON_NULL));
+        mapper.setAnnotationIntrospector(new 
JaxbAnnotationIntrospector(mapper.getTypeFactory()));
+        // Ignore unknown properties so that deployed client remain compatible 
with future versions of NiFi Registry that add new fields
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 
false);
+
+        jacksonJaxbJsonProvider.setMapper(mapper);
+        return jacksonJaxbJsonProvider;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestUtils.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestUtils.java
 
b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestUtils.java
new file mode 100644
index 0000000..8cfcb38
--- /dev/null
+++ 
b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestUtils.java
@@ -0,0 +1,120 @@
+/*
+ * 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 org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.flow.VersionedComponent;
+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.flow.VersionedProcessGroup;
+
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+class IntegrationTestUtils {
+
+    public static void assertBucketsEqual(Bucket expected, Bucket actual, 
boolean checkServerSetFields) {
+        assertNotNull(actual);
+        assertEquals(expected.getName(), actual.getName());
+        assertEquals(expected.getDescription(), actual.getDescription());
+        if (checkServerSetFields) {
+            assertEquals(expected.getIdentifier(), actual.getIdentifier());
+            assertEquals(expected.getCreatedTimestamp(), 
actual.getCreatedTimestamp());
+            assertEquals(expected.getPermissions(), actual.getPermissions());
+            assertEquals(expected.getLink(), actual.getLink());
+        }
+    }
+
+    public static void assertFlowsEqual(VersionedFlow expected, VersionedFlow 
actual, boolean checkServerSetFields) {
+        assertNotNull(actual);
+        assertEquals(expected.getName(), actual.getName());
+        assertEquals(expected.getDescription(), actual.getDescription());
+        assertEquals(expected.getBucketIdentifier(), 
actual.getBucketIdentifier());
+        if (checkServerSetFields) {
+            assertEquals(expected.getIdentifier(), actual.getIdentifier());
+            assertEquals(expected.getVersionCount(), actual.getVersionCount());
+            assertEquals(expected.getCreatedTimestamp(), 
actual.getCreatedTimestamp());
+            assertEquals(expected.getModifiedTimestamp(), 
actual.getModifiedTimestamp());
+            assertEquals(expected.getType(), actual.getType());
+            assertEquals(expected.getLink(), actual.getLink());
+        }
+    }
+
+    public static void assertFlowSnapshotsEqual(VersionedFlowSnapshot 
expected, VersionedFlowSnapshot actual, boolean checkServerSetFields) {
+
+        assertNotNull(actual);
+
+        if (expected.getSnapshotMetadata() != null) {
+            assertFlowSnapshotMetadataEqual(expected.getSnapshotMetadata(), 
actual.getSnapshotMetadata(), checkServerSetFields);
+        }
+
+        if (expected.getFlowContents() != null) {
+            assertVersionedProcessGroupsEqual(expected.getFlowContents(), 
actual.getFlowContents());
+        }
+
+        if (checkServerSetFields) {
+            assertFlowsEqual(expected.getFlow(), actual.getFlow(), false); // 
false because if we are checking a newly created snapshot, the versionsCount 
won't match
+            assertBucketsEqual(expected.getBucket(), actual.getBucket(), true);
+        }
+
+    }
+
+    public static void assertFlowSnapshotMetadataEqual(
+            VersionedFlowSnapshotMetadata expected, 
VersionedFlowSnapshotMetadata actual, boolean checkServerSetFields) {
+
+        assertNotNull(actual);
+        assertEquals(expected.getBucketIdentifier(), 
actual.getBucketIdentifier());
+        assertEquals(expected.getFlowIdentifier(), actual.getFlowIdentifier());
+        assertEquals(expected.getVersion(), actual.getVersion());
+        assertEquals(expected.getComments(), actual.getComments());
+        if (checkServerSetFields) {
+            assertEquals(expected.getTimestamp(), actual.getTimestamp());
+        }
+    }
+
+    private static void 
assertVersionedProcessGroupsEqual(VersionedProcessGroup expected, 
VersionedProcessGroup actual) {
+        assertNotNull(actual);
+
+        assertEquals(((VersionedComponent)expected), 
((VersionedComponent)actual));
+
+        // Poor man's set equality assertion as we are only checking the base 
type and not doing a recursive check
+        // TODO, this would be a stronger assertion by replacing this with a 
true VersionedProcessGroup.equals() method that does a deep equality check
+        assertSetsEqual(expected.getProcessGroups(), 
actual.getProcessGroups());
+        assertSetsEqual(expected.getRemoteProcessGroups(), 
actual.getRemoteProcessGroups());
+        assertSetsEqual(expected.getProcessors(), actual.getProcessors());
+        assertSetsEqual(expected.getInputPorts(), actual.getInputPorts());
+        assertSetsEqual(expected.getOutputPorts(), actual.getOutputPorts());
+        assertSetsEqual(expected.getConnections(), actual.getConnections());
+        assertSetsEqual(expected.getLabels(), actual.getLabels());
+        assertSetsEqual(expected.getFunnels(), actual.getFunnels());
+        assertSetsEqual(expected.getControllerServices(), 
actual.getControllerServices());
+    }
+
+
+    private static void assertSetsEqual(Set<? extends VersionedComponent> 
expected, Set<? extends VersionedComponent> actual) {
+        if (expected != null) {
+            assertNotNull(actual);
+            assertEquals(expected.size(), actual.size());
+            assertTrue(actual.containsAll(expected));
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java
 
b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java
new file mode 100644
index 0000000..67cb2e2
--- /dev/null
+++ 
b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java
@@ -0,0 +1,172 @@
+/*
+ * 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 org.apache.nifi.registry.NiFiRegistryTestApiApplication;
+import org.apache.nifi.registry.authorization.ResourcePermissions;
+import org.apache.nifi.registry.authorization.Tenant;
+import org.apache.nifi.registry.authorization.User;
+import org.apache.nifi.registry.authorization.UserGroup;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Deploy the Web API Application using an embedded Jetty Server for local 
integration testing, with the follow characteristics:
+ *
+ * - A NiFiRegistryProperties has to be explicitly provided to the 
ApplicationContext using a profile unique to this test suite.
+ * - A NiFiRegistryClientConfig has been configured to create a client capable 
of completing two-way TLS
+ * - The database is embed H2 using volatile (in-memory) persistence
+ * - Custom SQL is clearing the DB before each test method by default, unless 
method overrides this behavior
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest(
+        classes = NiFiRegistryTestApiApplication.class,
+        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+        properties = "spring.profiles.include=ITSecureFile")
+@Import(SecureITClientConfiguration.class)
+@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = 
"classpath:db/clearDB.sql")
+public class SecureFileIT extends IntegrationTestBase {
+
+    @Test
+    public void testAccessStatus() throws Exception {
+
+        // Given: the client and server have been configured correctly for 
two-way TLS
+        String expectedJson = "{" +
+                "\"identity\":\"CN=user1, OU=nifi\"," +
+                "\"anonymous\":false," +
+                "\"resourcePermissions\":{" +
+                
"\"anyTopLevelResource\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true},"
 +
+                
"\"buckets\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                
"\"tenants\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                
"\"policies\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                
"\"proxy\":{\"canRead\":false,\"canWrite\":true,\"canDelete\":false}}" +
+                "}";
+
+        // When: the /access endpoint is queried
+        final Response response = client
+                .target(createURL("access"))
+                .request()
+                .get(Response.class);
+
+        // Then: the server returns 200 OK with the expected client identity
+        assertEquals(200, response.getStatus());
+        String actualJson = response.readEntity(String.class);
+        JSONAssert.assertEquals(expectedJson, actualJson, false);
+    }
+
+    @Test
+    public void testRetrieveResources() throws Exception {
+
+        // Given: an empty registry returns these resources
+        String expected = "[" +
+                "{\"identifier\":\"/actuator\",\"name\":\"Actuator\"}," +
+                "{\"identifier\":\"/swagger\",\"name\":\"Swagger\"}," +
+                "{\"identifier\":\"/policies\",\"name\":\"Access Policies\"}," 
+
+                "{\"identifier\":\"/tenants\",\"name\":\"Tenants\"}," +
+                "{\"identifier\":\"/proxy\",\"name\":\"Proxy User 
Requests\"}," +
+                "{\"identifier\":\"/buckets\",\"name\":\"Buckets\"}" +
+                "]";
+
+        // When: the /resources endpoint is queried
+        final String resourcesJson = client
+                .target(createURL("/policies/resources"))
+                .request()
+                .get(String.class);
+
+        // Then: the expected array of resources is returned
+        JSONAssert.assertEquals(expected, resourcesJson, false);
+    }
+
+    @Test
+    public void testCreateUser() throws Exception {
+
+        // Given: the server has been configured with FileUserGroupProvider, 
which is configurable,
+        //   and: the initial admin client wants to create a tenant
+        Tenant tenant = new Tenant();
+        tenant.setIdentity("New User");
+
+        // When: the POST /tenants/users endpoint is accessed
+        final Response createUserResponse = client
+                .target(createURL("tenants/users"))
+                .request()
+                .post(Entity.entity(tenant, MediaType.APPLICATION_JSON_TYPE), 
Response.class);
+
+        // Then: "201 created" is returned with the expected user
+        assertEquals(201, createUserResponse.getStatus());
+        User actualUser = createUserResponse.readEntity(User.class);
+        assertNotNull(actualUser.getIdentifier());
+        try {
+            assertEquals(tenant.getIdentity(), actualUser.getIdentity());
+            assertEquals(true, actualUser.getConfigurable());
+            assertEquals(0, actualUser.getUserGroups().size());
+            assertEquals(0, actualUser.getAccessPolicies().size());
+            assertEquals(new ResourcePermissions(), 
actualUser.getResourcePermissions());
+        } finally {
+            // cleanup user for other tests
+            client.target(createURL("tenants/users/" + 
actualUser.getIdentifier()))
+                    .request()
+                    .delete();
+        }
+
+    }
+
+    @Test
+    public void testCreateUserGroup() throws Exception {
+
+        // Given: the server has been configured with FileUserGroupProvider, 
which is configurable,
+        //   and: the initial admin client wants to create a tenant
+        Tenant tenant = new Tenant();
+        tenant.setIdentity("New Group");
+
+        // When: the POST /tenants/user-groups endpoint is used
+        final Response createUserGroupResponse = client
+                .target(createURL("tenants/user-groups"))
+                .request()
+                .post(Entity.entity(tenant, MediaType.APPLICATION_JSON_TYPE), 
Response.class);
+
+        // Then: 201 created is returned with the expected group
+        assertEquals(201, createUserGroupResponse.getStatus());
+        UserGroup actualUserGroup = 
createUserGroupResponse.readEntity(UserGroup.class);
+        assertNotNull(actualUserGroup.getIdentifier());
+        try {
+            assertEquals(tenant.getIdentity(), actualUserGroup.getIdentity());
+            assertEquals(true, actualUserGroup.getConfigurable());
+            assertEquals(0, actualUserGroup.getUsers().size());
+            assertEquals(0, actualUserGroup.getAccessPolicies().size());
+            assertEquals(new ResourcePermissions(), 
actualUserGroup.getResourcePermissions());
+        } finally {
+            // cleanup user for other tests
+            client.target(createURL("tenants/user-groups/" + 
actualUserGroup.getIdentifier()))
+                    .request()
+                    .delete();
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureITClientConfiguration.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureITClientConfiguration.java
 
b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureITClientConfiguration.java
new file mode 100644
index 0000000..ab07a08
--- /dev/null
+++ 
b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureITClientConfiguration.java
@@ -0,0 +1,91 @@
+/*
+ * 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 org.apache.nifi.registry.client.NiFiRegistryClientConfig;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.security.util.KeystoreType;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import static 
org.apache.nifi.registry.web.api.IntegrationTestBase.loadNiFiRegistryProperties;
+
+// Do not add Spring annotations that would cause this class to be picked up 
by a ComponentScan. It must be imported manually.
+public class SecureITClientConfiguration {
+
+    @Value("${nifi.registry.client.properties.file}")
+    String clientPropertiesFileLocation;
+
+    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+    private final Lock readLock = lock.readLock();
+    private NiFiRegistryClientConfig clientConfig;
+
+    @Bean
+    public NiFiRegistryClientConfig getNiFiRegistryClientConfig() {
+        readLock.lock();
+        try {
+            if (clientConfig == null) {
+                final NiFiRegistryProperties clientProperties = 
loadNiFiRegistryProperties(clientPropertiesFileLocation);
+                clientConfig = 
createNiFiRegistryClientConfig(clientProperties);
+            }
+        } finally {
+            readLock.unlock();
+        }
+        return clientConfig;
+    }
+
+    /**
+     * A helper method for loading a NiFiRegistryClientConfig corresponding to 
a NiFiRegistryProperties object
+     * holding the values needed to create a client configuration context.
+     *
+     * @param clientProperties A NiFiRegistryProperties object holding the 
config for client keystore, truststore, etc.
+     * @return A NiFiRegistryClientConfig instance based on the properties 
file contents
+     */
+    private static NiFiRegistryClientConfig 
createNiFiRegistryClientConfig(NiFiRegistryProperties clientProperties) {
+
+        NiFiRegistryClientConfig.Builder configBuilder = new 
NiFiRegistryClientConfig.Builder();
+
+        // load keystore/truststore if applicable
+        if (clientProperties.getKeyStorePath() != null) {
+            configBuilder.keystoreFilename(clientProperties.getKeyStorePath());
+        }
+        if (clientProperties.getKeyStoreType() != null) {
+            
configBuilder.keystoreType(KeystoreType.valueOf(clientProperties.getKeyStoreType()));
+        }
+        if (clientProperties.getKeyStorePassword() != null) {
+            
configBuilder.keystorePassword(clientProperties.getKeyStorePassword());
+        }
+        if (clientProperties.getKeyPassword() != null) {
+            configBuilder.keyPassword(clientProperties.getKeyPassword());
+        }
+        if (clientProperties.getTrustStorePath() != null) {
+            
configBuilder.truststoreFilename(clientProperties.getTrustStorePath());
+        }
+        if (clientProperties.getTrustStoreType() != null) {
+            
configBuilder.truststoreType(KeystoreType.valueOf(clientProperties.getTrustStoreType()));
+        }
+        if (clientProperties.getTrustStorePassword() != null) {
+            
configBuilder.truststorePassword(clientProperties.getTrustStorePassword());
+        }
+
+        return configBuilder.build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureKerberosIT.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureKerberosIT.java
 
b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureKerberosIT.java
new file mode 100644
index 0000000..8d8ea97
--- /dev/null
+++ 
b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureKerberosIT.java
@@ -0,0 +1,216 @@
+/*
+ * 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 org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.NiFiRegistryTestApiApplication;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Primary;
+import org.springframework.context.annotation.Profile;
+import org.springframework.security.authentication.BadCredentialsException;
+import 
org.springframework.security.kerberos.authentication.KerberosTicketValidation;
+import 
org.springframework.security.kerberos.authentication.KerberosTicketValidator;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import javax.ws.rs.core.Response;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Base64;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Deploy the Web API Application using an embedded Jetty Server for local 
integration testing, with the follow characteristics:
+ *
+ * - A NiFiRegistryProperties has to be explicitly provided to the 
ApplicationContext using a profile unique to this test suite.
+ * - A NiFiRegistryClientConfig has been configured to create a client capable 
of completing one-way TLS
+ * - The database is embed H2 using volatile (in-memory) persistence
+ * - Custom SQL is clearing the DB before each test method by default, unless 
method overrides this behavior
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest(
+        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+        properties = "spring.profiles.include=ITSecureKerberos")
+@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = 
"classpath:db/clearDB.sql")
+public class SecureKerberosIT extends IntegrationTestBase {
+
+    private static final String validKerberosTicket = "authenticate_me";
+    private static final String invalidKerberosTicket = 
"do_not_authenticate_me";
+
+    public static class MockKerberosTicketValidator implements 
KerberosTicketValidator {
+
+        @Override
+        public KerberosTicketValidation validateTicket(byte[] token) throws 
BadCredentialsException {
+
+            boolean validTicket;
+            try {
+                 validTicket = 
Arrays.equals(validKerberosTicket.getBytes("UTF-8"), token);
+            } catch (UnsupportedEncodingException e) {
+                throw new IllegalStateException(e);
+            }
+
+            if (!validTicket) {
+                throw new 
BadCredentialsException(MockKerberosTicketValidator.class.getSimpleName() + " 
does not validate token");
+            }
+
+            return new KerberosTicketValidation(
+                    "kerberosUser@LOCALHOST",
+                    "HTTP/localhsot@LOCALHOST",
+                    null,
+                    null);
+        }
+    }
+
+    @Configuration
+    @Profile("ITSecureKerberos")
+    @Import({NiFiRegistryTestApiApplication.class, 
SecureITClientConfiguration.class})
+    public static class KerberosSpnegoTestConfiguration {
+
+        @Primary
+        @Bean
+        public static KerberosTicketValidator kerberosTicketValidator() {
+            return new MockKerberosTicketValidator();
+        }
+
+    }
+
+    private String adminAuthToken;
+
+    @Before
+    public void generateAuthToken() {
+        String validTicket = new 
String(Base64.getEncoder().encode(validKerberosTicket.getBytes(Charset.forName("UTF-8"))));
+        final String token = client
+                .target(createURL("/access/token/kerberos"))
+                .request()
+                .header("Authorization", "Negotiate " + validTicket)
+                .post(null, String.class);
+        adminAuthToken = token;
+    }
+
+    @Test
+    public void testTokenGenerationAndAccessStatus() throws Exception {
+
+        // Note: this test intentionally does not use the token generated
+        // for nifiadmin by the @Before method
+
+        // Given: the client and server have been configured correctly for 
Kerberos SPNEGO authentication
+        String expectedJwtPayloadJson = "{" +
+                "\"sub\":\"kerberosUser@LOCALHOST\"," +
+                "\"preferred_username\":\"kerberosUser@LOCALHOST\"," +
+                "\"iss\":\"KerberosSpnegoIdentityProvider\"" +
+                "}";
+        String expectedAccessStatusJson = "{" +
+                "\"identity\":\"kerberosUser@LOCALHOST\"," +
+                "\"anonymous\":false}";
+
+        // When: the /access/token/kerberos endpoint is accessed with no 
credentials
+        final Response tokenResponse1 = client
+                .target(createURL("/access/token/kerberos"))
+                .request()
+                .post(null, Response.class);
+
+        // Then: the server returns 401 Unauthorized with an authenticate 
challenge header
+        assertEquals(401, tokenResponse1.getStatus());
+        assertNotNull(tokenResponse1.getHeaders().get("www-authenticate"));
+        assertEquals(1, 
tokenResponse1.getHeaders().get("www-authenticate").size());
+        assertEquals("Negotiate", 
tokenResponse1.getHeaders().get("www-authenticate").get(0));
+
+        // When: the /access/token/kerberos endpoint is accessed again with an 
invalid ticket
+        String invalidTicket = new 
String(java.util.Base64.getEncoder().encode(invalidKerberosTicket.getBytes(Charset.forName("UTF-8"))));
+        final Response tokenResponse2 = client
+                .target(createURL("/access/token/kerberos"))
+                .request()
+                .header("Authorization", "Negotiate " + invalidTicket)
+                .post(null, Response.class);
+
+        // Then: the server returns 401 Unauthorized
+        assertEquals(401, tokenResponse2.getStatus());
+
+        // When: the /access/token/kerberos endpoint is accessed with a valid 
ticket
+        String validTicket = new 
String(Base64.getEncoder().encode(validKerberosTicket.getBytes(Charset.forName("UTF-8"))));
+        final Response tokenResponse3 = client
+                .target(createURL("/access/token/kerberos"))
+                .request()
+                .header("Authorization", "Negotiate " + validTicket)
+                .post(null, Response.class);
+
+        // Then: the server returns 200 OK with a JWT in the body
+        assertEquals(201, tokenResponse3.getStatus());
+        String token = tokenResponse3.readEntity(String.class);
+        assertTrue(StringUtils.isNotEmpty(token));
+        String[] jwtParts = token.split("\\.");
+        assertEquals(3, jwtParts.length);
+        String jwtPayload = new 
String(Base64.getDecoder().decode(jwtParts[1]), "UTF-8");
+        JSONAssert.assertEquals(expectedJwtPayloadJson, jwtPayload, false);
+
+        // When: the token is returned in the Authorization header
+        final Response accessResponse = client
+                .target(createURL("access"))
+                .request()
+                .header("Authorization", "Bearer " + token)
+                .get(Response.class);
+
+        // Then: the server acknowledges the client has access
+        assertEquals(200, accessResponse.getStatus());
+        String accessStatus = accessResponse.readEntity(String.class);
+        JSONAssert.assertEquals(expectedAccessStatusJson, accessStatus, false);
+
+    }
+
+    @Test
+    public void testGetCurrentUser() throws Exception {
+
+        // Given: the client is connected to an unsecured NiFi Registry
+        String expectedJson = "{" +
+                "\"identity\":\"kerberosUser@LOCALHOST\"," +
+                "\"anonymous\":false," +
+                "\"resourcePermissions\":{" +
+                
"\"anyTopLevelResource\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true},"
 +
+                
"\"buckets\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                
"\"tenants\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                
"\"policies\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                
"\"proxy\":{\"canRead\":false,\"canWrite\":true,\"canDelete\":false}}" +
+                "}";
+
+        // When: the /access endpoint is queried using a JWT for the kerberos 
user
+        final Response response = client
+                .target(createURL("/access"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .get(Response.class);
+
+        // Then: the server returns a 200 OK with the expected current user
+        assertEquals(200, response.getStatus());
+        String actualJson = response.readEntity(String.class);
+        JSONAssert.assertEquals(expectedJson, actualJson, false);
+
+    }
+
+
+}

Reply via email to