NIFIREG-385 Make revision feature configurable

NIFIREG-385 Adding comment to revision property in nifi-registry.properties

Signed-off-by: Nathan Gough <thenatog@gmail.com>

This closes #276.
diff --git a/nifi-registry-assembly/pom.xml b/nifi-registry-assembly/pom.xml
index b4c7acd..ec4bc07 100644
--- a/nifi-registry-assembly/pom.xml
+++ b/nifi-registry-assembly/pom.xml
@@ -197,6 +197,9 @@
         <nifi.registry.kerberos.spnego.keytab.location />
         <nifi.registry.kerberos.spnego.authentication.expiration>12 hours</nifi.registry.kerberos.spnego.authentication.expiration>
 
+        <!-- nifi.registry.properties: revision management properties -->
+        <nifi.registry.revisions.enabled>false</nifi.registry.revisions.enabled>
+
     </properties>
 
     <profiles>
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
index d3b4a25..4700383 100644
--- a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
@@ -87,6 +87,9 @@
     public static final String KERBEROS_SERVICE_PRINCIPAL = "nifi.registry.kerberos.service.principal";
     public static final String KERBEROS_SERVICE_KEYTAB_LOCATION = "nifi.registry.kerberos.service.keytab.location";
 
+    // Revision Management Properties
+    public static final String REVISIONS_ENABLED = "nifi.registry.revisions.enabled";
+
     // Defaults
     public static final String DEFAULT_WEB_WORKING_DIR = "./work/jetty";
     public static final String DEFAULT_WAR_DIR = "./lib";
@@ -273,6 +276,10 @@
         return extensionDirs;
     }
 
+    public boolean areRevisionsEnabled() {
+        return Boolean.parseBoolean(getPropertyAsTrimmedString(REVISIONS_ENABLED));
+    }
+
     /**
      * Retrieves all known property keys.
      *
diff --git a/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties
index 1b62023..bf3e09f 100644
--- a/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties
+++ b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties
@@ -99,3 +99,7 @@
 nifi.registry.kerberos.spnego.principal=${nifi.registry.kerberos.spnego.principal}
 nifi.registry.kerberos.spnego.keytab.location=${nifi.registry.kerberos.spnego.keytab.location}
 nifi.registry.kerberos.spnego.authentication.expiration=${nifi.registry.kerberos.spnego.authentication.expiration}
+
+# revision management #
+# This feature should remain disabled until a future NiFi release that supports the revision API changes
+nifi.registry.revisions.enabled=${nifi.registry.revisions.enabled}
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/RevisionConfiguration.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/RevisionConfiguration.java
index cc0edf9..2b270d6 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/RevisionConfiguration.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/RevisionConfiguration.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.registry.web.service;
 
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
 import org.apache.nifi.registry.revision.api.RevisionManager;
 import org.apache.nifi.registry.revision.entity.RevisableEntityService;
 import org.apache.nifi.registry.revision.entity.StandardRevisableEntityService;
@@ -40,4 +41,11 @@
         return new StandardRevisableEntityService(revisionManager);
     }
 
+    @Bean
+    public synchronized RevisionFeature getRevisionFeature(final NiFiRegistryProperties properties) {
+        return () -> {
+            return properties.areRevisionsEnabled();
+        };
+    }
+
 }
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/RevisionFeature.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/RevisionFeature.java
new file mode 100644
index 0000000..c39f6d1
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/RevisionFeature.java
@@ -0,0 +1,23 @@
+/*
+ * 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.service;
+
+public interface RevisionFeature {
+
+    boolean isEnabled();
+
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java
index 7c908e4..f5bfb01 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java
@@ -105,6 +105,7 @@
     private final AuthorizationService authorizationService;
     private final AuthorizableLookup authorizableLookup;
     private final RevisableEntityService entityService;
+    private final RevisionFeature revisionFeature;
     private final PermissionsService permissionsService;
     private final LinkService linkService;
 
@@ -114,6 +115,7 @@
                                  final AuthorizationService authorizationService,
                                  final AuthorizableLookup authorizableLookup,
                                  final RevisableEntityService entityService,
+                                 final RevisionFeature revisionFeature,
                                  final PermissionsService permissionsService,
                                  final LinkService linkService) {
         this.registryService = registryService;
@@ -121,6 +123,7 @@
         this.authorizationService = authorizationService;
         this.authorizableLookup = authorizableLookup;
         this.entityService = entityService;
+        this.revisionFeature = revisionFeature;
         this.permissionsService = permissionsService;
         this.linkService = linkService;
     }
@@ -135,6 +138,7 @@
     public Bucket createBucket(final Bucket bucket) {
         authorizeBucketsAccess(RequestAction.WRITE);
         validateCreationOfRevisableEntity(bucket, BUCKET_ENTITY_TYPE);
+        validateIdentifierNotPresent(bucket, BUCKET_ENTITY_TYPE);
 
         bucket.setIdentifier(UUID.randomUUID().toString());
 
@@ -231,7 +235,11 @@
         authorizeBucketAccess(RequestAction.WRITE, bucketIdentifier);
         validateCreationOfRevisableEntity(versionedFlow, VERSIONED_FLOW_ENTITY_TYPE);
 
-        versionedFlow.setIdentifier(UUID.randomUUID().toString());
+        // NOTE: Don't validate that identifier is null...
+        // NiFi has been sending an identifier, so we must maintain backwards compatibility
+        if (versionedFlow.getIdentifier() == null) {
+            versionedFlow.setIdentifier(UUID.randomUUID().toString());
+        }
 
         final VersionedFlow createdFlow = createRevisableEntity(versionedFlow, VERSIONED_FLOW_ENTITY_TYPE, currentUserIdentity(),
                 () -> registryService.createFlow(bucketIdentifier, versionedFlow));
@@ -804,6 +812,7 @@
         verifyAuthorizerSupportsConfigurableUserGroups();
         authorizeTenantsAccess(RequestAction.WRITE);
         validateCreationOfRevisableEntity(user, USER_ENTITY_TYPE);
+        validateIdentifierNotPresent(user, USER_ENTITY_TYPE);
 
         user.setIdentifier(UUID.randomUUID().toString());
         return createRevisableEntity(user, USER_ENTITY_TYPE, currentUserIdentity(), () -> authorizationService.createUser(user));
@@ -854,6 +863,7 @@
         verifyAuthorizerSupportsConfigurableUserGroups();
         authorizeTenantsAccess(RequestAction.WRITE);
         validateCreationOfRevisableEntity(userGroup, USER_GROUP_ENTITY_TYPE);
+        validateIdentifierNotPresent(userGroup, USER_GROUP_ENTITY_TYPE);
 
         userGroup.setIdentifier(UUID.randomUUID().toString());
         return createRevisableEntity(userGroup, USER_GROUP_ENTITY_TYPE, currentUserIdentity(),
@@ -907,6 +917,7 @@
         verifyAuthorizerSupportsConfigurablePolicies();
         authorizePoliciesAccess(RequestAction.WRITE);
         validateCreationOfRevisableEntity(accessPolicy, ACCESS_POLICY_ENTITY_TYPE);
+        validateIdentifierNotPresent(accessPolicy, ACCESS_POLICY_ENTITY_TYPE);
 
         accessPolicy.setIdentifier(UUID.randomUUID().toString());
         return createRevisableEntity(accessPolicy, ACCESS_POLICY_ENTITY_TYPE, currentUserIdentity(),
@@ -1111,15 +1122,20 @@
         authorizationService.authorize(tenantsAuthorizable, actionType);
     }
 
+    // ---------------------- Revision Helper Methods -------------------------------------
+
     private void validateCreationOfRevisableEntity(final RevisableEntity entity, final String entityTypeName) {
         if (entity == null) {
             throw new IllegalArgumentException(entityTypeName + " cannot be null");
         }
-        if (entity.getIdentifier() != null) {
-            throw new IllegalArgumentException(entityTypeName + " identifier cannot be specified when creating a new "
-                    + entityTypeName.toLowerCase() + ".");
+
+        // skip checking revision if feature is disabled
+        if (!revisionFeature.isEnabled()) {
+            return;
         }
 
+        // NOT: restore identifier check here when we no longer needs backwards compatibility
+
         if (entity.getRevision() == null
                 || entity.getRevision().getVersion() == null
                 || entity.getRevision().getVersion().longValue() != 0) {
@@ -1127,11 +1143,29 @@
         }
     }
 
+    /**
+     * NOTE: This logic should be moved back to validateCreationOfRevisableEntity once we no longer need to maintain
+     * backwards compatibility (i.e. on a major release like 1.0.0).
+     *
+     * Currently NiFi has been sending an identifier when creating a flow, so we need to continue to allow that.
+     */
+    private void validateIdentifierNotPresent(final RevisableEntity entity, final String entityTypeName) {
+        if (entity.getIdentifier() != null) {
+            throw new IllegalArgumentException(entityTypeName + " identifier cannot be specified when creating a new "
+                    + entityTypeName.toLowerCase() + ".");
+        }
+    }
+
     private void validateUpdateOfRevisableEntity(final RevisableEntity entity, final String entityTypeName) {
         if (entity == null) {
             throw new IllegalArgumentException(entityTypeName + " cannot be null");
         }
 
+        // skip checking revision if feature is disabled
+        if (!revisionFeature.isEnabled()) {
+            return;
+        }
+
         if (entity.getRevision() == null || entity.getRevision().getVersion() == null) {
             throw new IllegalArgumentException("Revision info must be specified.");
         }
@@ -1142,6 +1176,11 @@
             throw new IllegalArgumentException(entityTypeName + " identifier is required");
         }
 
+        // skip checking revision if feature is disabled
+        if (!revisionFeature.isEnabled()) {
+            return;
+        }
+
         if (revision == null || revision.getVersion() == null) {
             throw new IllegalArgumentException("Revision info must be specified.");
         }
@@ -1149,31 +1188,48 @@
 
     private <T extends RevisableEntity> T createRevisableEntity(final T requestEntity, final String entityTypeName,
                                                                 final String creatorIdentity, final Supplier<T> createEntity) {
-        try {
-            return entityService.create(requestEntity, creatorIdentity, createEntity);
-        } catch (InvalidRevisionException e) {
-            final String msg = String.format(INVALID_REVISION_MSG, entityTypeName, "create", requestEntity.getIdentifier());
-            throw new InvalidRevisionException(msg, e);
+
+        // skip using the entity service if revision feature is disabled
+        if (!revisionFeature.isEnabled()) {
+            return createEntity.get();
+        } else {
+            try {
+                return entityService.create(requestEntity, creatorIdentity, createEntity);
+            } catch (InvalidRevisionException e) {
+                final String msg = String.format(INVALID_REVISION_MSG, entityTypeName, "create", requestEntity.getIdentifier());
+                throw new InvalidRevisionException(msg, e);
+            }
         }
     }
 
     private <T extends RevisableEntity> T updateRevisableEntity(final T requestEntity, final String entityTypeName,
                                                                 final String updaterIdentity, final Supplier<T> updateEntity) {
-        try {
-            return entityService.update(requestEntity, updaterIdentity, updateEntity);
-        } catch (InvalidRevisionException e) {
-            final String msg = String.format(INVALID_REVISION_MSG, entityTypeName, "update", requestEntity.getIdentifier());
-            throw new InvalidRevisionException(msg, e);
+
+        // skip using the entity service if revision feature is disabled
+        if (!revisionFeature.isEnabled()) {
+            return updateEntity.get();
+        } else {
+            try {
+                return entityService.update(requestEntity, updaterIdentity, updateEntity);
+            } catch (InvalidRevisionException e) {
+                final String msg = String.format(INVALID_REVISION_MSG, entityTypeName, "update", requestEntity.getIdentifier());
+                throw new InvalidRevisionException(msg, e);
+            }
         }
     }
 
     private <T extends RevisableEntity> T deleteRevisableEntity(final String entityIdentifier, final String entityTypeName,
                                                  final RevisionInfo revisionInfo, final Supplier<T> deleteEntity) {
-        try {
-            return entityService.delete(entityIdentifier, revisionInfo, deleteEntity);
-        } catch (InvalidRevisionException e) {
-            final String msg = String.format(INVALID_REVISION_MSG, entityTypeName, "delete", entityIdentifier);
-            throw new InvalidRevisionException(msg, e);
+        // skip using the entity service if revision feature is disabled
+        if (!revisionFeature.isEnabled()) {
+            return deleteEntity.get();
+        } else {
+            try {
+                return entityService.delete(entityIdentifier, revisionInfo, deleteEntity);
+            } catch (InvalidRevisionException e) {
+                final String msg = String.format(INVALID_REVISION_MSG, entityTypeName, "delete", entityIdentifier);
+                throw new InvalidRevisionException(msg, e);
+            }
         }
     }
 
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/NoRevisionsIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/NoRevisionsIT.java
new file mode 100644
index 0000000..e93b522
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/NoRevisionsIT.java
@@ -0,0 +1,139 @@
+/*
+ * 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.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.Test;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.MediaType;
+
+import java.util.UUID;
+
+import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertBucketsEqual;
+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.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class NoRevisionsIT extends UnsecuredNoRevisionsITBase {
+
+    @Test
+    public void testNoRevisions() {
+        // Create a bucket...
+
+        final Bucket bucket = new Bucket();
+        bucket.setName("Integration Test Bucket");
+        bucket.setDescription("A bucket created by an integration test.");
+
+        final Bucket createdBucket = client
+                .target(createURL("buckets"))
+                .request()
+                .post(Entity.entity(bucket, MediaType.APPLICATION_JSON), Bucket.class);
+
+        assertBucketsEqual(bucket, createdBucket, false);
+        assertNotNull(createdBucket.getIdentifier());
+
+        // Update bucket...
+
+        createdBucket.setName("Renamed Bucket");
+        createdBucket.setDescription("This bucket has been updated by an integration test.");
+
+        final Bucket updatedBucket = client
+                .target(createURL("buckets/" + createdBucket.getIdentifier()))
+                .request()
+                .put(Entity.entity(createdBucket, MediaType.APPLICATION_JSON), Bucket.class);
+
+        assertBucketsEqual(updatedBucket, createdBucket, true);
+
+        // Create a flow...
+
+        final VersionedFlow flow = new VersionedFlow();
+        flow.setIdentifier(UUID.randomUUID().toString()); // Simulate NiFi sending an identifier
+        flow.setBucketIdentifier(createdBucket.getIdentifier());
+        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", flow.getBucketIdentifier())
+                .request()
+                .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class);
+
+        assertFlowsEqual(flow, createdFlow, false);
+        assertNotNull(createdFlow.getIdentifier());
+
+        // Update flow...
+
+        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",flow.getBucketIdentifier())
+                .resolveTemplate("flowId", createdFlow.getIdentifier())
+                .request()
+                .put(Entity.entity(createdFlow, MediaType.APPLICATION_JSON), VersionedFlow.class);
+
+        assertTrue(updatedFlow.getModifiedTimestamp() > createdFlow.getModifiedTimestamp());
+
+        // Create a version of a flow...
+
+        final VersionedFlowSnapshotMetadata flowSnapshotMetadata = new VersionedFlowSnapshotMetadata();
+        flowSnapshotMetadata.setVersion(1);
+        flowSnapshotMetadata.setBucketIdentifier(createdFlow.getBucketIdentifier());
+        flowSnapshotMetadata.setFlowIdentifier(createdFlow.getIdentifier());
+        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
+
+        final VersionedFlowSnapshot createdFlowSnapshot = client
+                .target(createURL("buckets/{bucketId}/flows/{flowId}/versions"))
+                .resolveTemplate("bucketId", flowSnapshotMetadata.getBucketIdentifier())
+                .resolveTemplate("flowId", flowSnapshotMetadata.getFlowIdentifier())
+                .request()
+                .post(Entity.entity(flowSnapshot, MediaType.APPLICATION_JSON), VersionedFlowSnapshot.class);
+
+        assertFlowSnapshotsEqual(flowSnapshot, createdFlowSnapshot, false);
+
+        // Delete flow...
+
+        final VersionedFlow deletedFlow = client
+                .target(createURL("buckets/{bucketId}/flows/{flowId}"))
+                .resolveTemplate("bucketId", createdFlow.getBucketIdentifier())
+                .resolveTemplate("flowId", createdFlow.getIdentifier())
+                .request()
+                .delete(VersionedFlow.class);
+
+        assertNotNull(deletedFlow);
+
+        // Delete bucket...
+
+        final Bucket deletedBucket = client
+                .target(createURL("buckets/" + createdBucket.getIdentifier()))
+                .request()
+                .delete(Bucket.class);
+
+        assertNotNull(deletedBucket);
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNoRevisionsITBase.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNoRevisionsITBase.java
new file mode 100644
index 0000000..cabe25e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNoRevisionsITBase.java
@@ -0,0 +1,42 @@
+/*
+ * 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.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.junit4.SpringRunner;
+
+/**
+ * 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.
+ * - 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=ITUnsecuredNoRevisions")
+@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/clearDB.sql")
+public class UnsecuredNoRevisionsITBase extends IntegrationTestBase {
+
+    // Tests cases defined in subclasses
+
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITUnsecuredNoRevisions.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITUnsecuredNoRevisions.properties
new file mode 100644
index 0000000..5e16e04
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITUnsecuredNoRevisions.properties
@@ -0,0 +1,21 @@
+#
+# 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.
+#
+
+# Integration Test Profile for running an unsecured NiFi Registry instance
+
+# Custom (non-standard to Spring Boot) properties
+nifi.registry.properties.file = src/test/resources/conf/unsecured-no-revisions/nifi-registry.properties
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/db-flow-storage/nifi-registry.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/db-flow-storage/nifi-registry.properties
index dd50ae6..dd13679 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/db-flow-storage/nifi-registry.properties
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/db-flow-storage/nifi-registry.properties
@@ -25,4 +25,7 @@
 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
+nifi.registry.db.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE
+
+# enabled revision checking #
+nifi.registry.revisions.enabled=true
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry.properties
index 408f44d..fa2ad4c 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry.properties
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry.properties
@@ -28,3 +28,6 @@
 
 # providers properties #
 nifi.registry.providers.configuration.file=./target/test-classes/conf/providers.xml
+
+# enabled revision checking #
+nifi.registry.revisions.enabled=true
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry.properties
index 3d5c122..79a41e3 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry.properties
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry.properties
@@ -34,3 +34,6 @@
 nifi.registry.kerberos.spnego.authentication.expiration=12 hours
 nifi.registry.kerberos.spnego.principal=HTTP/localhost@LOCALHOST
 nifi.registry.kerberos.spnego.keytab.location=/path/to/keytab
+
+# enabled revision checking #
+nifi.registry.revisions.enabled=true
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry.properties
index 1b46ac2..b3512f3 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry.properties
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry.properties
@@ -30,3 +30,6 @@
 
 # providers properties #
 nifi.registry.providers.configuration.file=./target/test-classes/conf/providers.xml
+
+# enabled revision checking #
+nifi.registry.revisions.enabled=true
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured-no-revisions/nifi-registry.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured-no-revisions/nifi-registry.properties
new file mode 100644
index 0000000..f87062c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured-no-revisions/nifi-registry.properties
@@ -0,0 +1,31 @@
+#
+# 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.
+#
+
+# web properties #
+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
+
+# disable revision checking #
+nifi.registry.revisions.enabled=false
\ No newline at end of file
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 70ca5e3..451abc0 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
@@ -25,4 +25,7 @@
 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
+nifi.registry.db.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE
+
+# enabled revision checking #
+nifi.registry.revisions.enabled=true
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.html
index b7c37e4..b2aff5a 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.html
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.html
@@ -74,7 +74,7 @@
             <div *ngIf="nfRegistryService.currentUser.identity && nfRegistryService.perspective !== 'login' && nfRegistryService.perspective !== 'not-found'" fxLayout="column" fxLayoutAlign="space-around end" class="push-right-sm">
                 <div id="current-user" matTooltip="{{nfRegistryService.currentUser.identity}}">{{nfRegistryService.currentUser.identity}}</div>
                 <a id="logout-link-container" *ngIf="nfRegistryService.currentUser.canLogout" class="link" (click)="logout()">logout</a>
-                <a id="logout-link-container" *ngIf="!nfRegistryService.currentUser.canLogout && nfRegistryService.currentUser.anonymous && nfRegistryService.currentUser.loginSupported" class="link" (click)="login()">login</a>
+                <a id="login-link-container" *ngIf="!nfRegistryService.currentUser.canLogout && nfRegistryService.currentUser.anonymous && nfRegistryService.currentUser.loginSupported" class="link" (click)="login()">login</a>
             </div>
             <div id="nifi-registry-documentation" *ngIf="nfRegistryService.perspective !== 'login'" class="pad-right-sm">
                 <a matTooltip="Help" href="{{nfRegistryService.documentation.link}}" target="_blank"><i class="fa fa-question-circle help-icon" aria-hidden="true"></i></a>
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/_structureElements.scss b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/_structureElements.scss
index 88ea7df..50ecdf4 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/_structureElements.scss
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/theming/_structureElements.scss
@@ -113,7 +113,8 @@
   color: $grey5;
 }
 
-#logout-link-container {
+#logout-link-container,
+#login-link-container {
   font-size: 12px;
   text-transform: uppercase;
 }