NIFIREG-415 Add Support for Unicode in X-ProxiedEntitiesChain
- Adds detection and encoding of non-ascii characters to creation of chain
- Adds unit tests and integration tests that use proxied entities with Unicode
- Refactors ProxiedEntityRequestConfig to compute header value in constructor
- Addressed peer review feedback
This closes #302.
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/ProxiedEntityRequestConfig.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/ProxiedEntityRequestConfig.java
index 3a89898..08da6b4 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/ProxiedEntityRequestConfig.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/ProxiedEntityRequestConfig.java
@@ -16,47 +16,32 @@
*/
package org.apache.nifi.registry.client.impl.request;
-import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.nifi.registry.client.RequestConfig;
import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils;
-import java.util.Arrays;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
/**
* Implementation of RequestConfig that produces headers for a request with proxied-entities.
*/
public class ProxiedEntityRequestConfig implements RequestConfig {
- private final String[] proxiedEntities;
+ private final String proxiedEntitiesChain;
public ProxiedEntityRequestConfig(final String... proxiedEntities) {
- this.proxiedEntities = Validate.notNull(proxiedEntities);
+ Validate.notNull(proxiedEntities);
+ this.proxiedEntitiesChain = ProxiedEntitiesUtils.getProxiedEntitiesChain(proxiedEntities);
}
@Override
public Map<String, String> getHeaders() {
- final String proxiedEntitiesValue = getProxiedEntitesValue(proxiedEntities);
-
final Map<String,String> headers = new HashMap<>();
- if (proxiedEntitiesValue != null) {
- headers.put(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxiedEntitiesValue);
+ if (proxiedEntitiesChain != null) {
+ headers.put(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxiedEntitiesChain);
}
return headers;
}
- private String getProxiedEntitesValue(final String[] proxiedEntities) {
- if (proxiedEntities == null) {
- return null;
- }
-
- final List<String> proxiedEntityChain = Arrays.stream(proxiedEntities)
- .map(ProxiedEntitiesUtils::formatProxyDn).collect(Collectors.toList());
- return StringUtils.join(proxiedEntityChain, "");
- }
-
}
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java
index f850341..45a11f4 100644
--- a/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java
+++ b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java
@@ -20,8 +20,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Base64;
import java.util.List;
import java.util.stream.Collectors;
@@ -38,6 +40,63 @@
private static final String ESCAPED_LT = "\\\\<";
private static final String ANONYMOUS_CHAIN = "<>";
+ private static final String ANONYMOUS_IDENTITY = "";
+
+ /**
+ * Formats a list of DN/usernames to be set as a HTTP header using well known conventions.
+ *
+ * @param proxiedEntities the raw identities (usernames and DNs) to be formatted as a chain
+ * @return the value to use in the X-ProxiedEntitiesChain header
+ */
+ public static String getProxiedEntitiesChain(final String... proxiedEntities) {
+ return getProxiedEntitiesChain(Arrays.asList(proxiedEntities));
+ }
+
+ /**
+ * Formats a list of DN/usernames to be set as a HTTP header using well known conventions.
+ *
+ * @param proxiedEntities the raw identities (usernames and DNs) to be formatted as a chain
+ * @return the value to use in the X-ProxiedEntitiesChain header
+ */
+ public static String getProxiedEntitiesChain(final List<String> proxiedEntities) {
+ if (proxiedEntities == null) {
+ return null;
+ }
+
+ final List<String> proxiedEntityChain = proxiedEntities.stream()
+ .map(ProxiedEntitiesUtils::formatProxyDn)
+ .collect(Collectors.toList());
+ return StringUtils.join(proxiedEntityChain, "");
+ }
+
+ /**
+ * Tokenizes the specified proxy chain.
+ *
+ * @param rawProxyChain raw chain
+ * @return tokenized proxy chain
+ */
+ public static List<String> tokenizeProxiedEntitiesChain(final String rawProxyChain) {
+ final List<String> proxyChain = new ArrayList<>();
+ if (!StringUtils.isEmpty(rawProxyChain)) {
+
+ if (!isValidChainFormat(rawProxyChain)) {
+ throw new IllegalArgumentException("Proxy chain format is not recognized and can not safely be converted to a list.");
+ }
+
+ if (rawProxyChain.equals(ANONYMOUS_CHAIN)) {
+ proxyChain.add(ANONYMOUS_IDENTITY);
+ } else {
+ // Split the String on the `><` token, use substring to remove leading `<` and trailing `>`
+ final String[] elements = StringUtils.splitByWholeSeparatorPreserveAllTokens(
+ rawProxyChain.substring(1, rawProxyChain.length() - 1), "><");
+ // Unsanitize each DN and add it to the proxy chain list
+ Arrays.stream(elements)
+ .map(ProxiedEntitiesUtils::unsanitizeDn)
+ .forEach(proxyChain::add);
+ }
+ }
+ return proxyChain;
+ }
/**
* Formats the specified DN to be set as a HTTP header using well known conventions.
@@ -45,29 +104,59 @@
* @param dn raw dn
* @return the dn formatted as an HTTP header
*/
- public static String formatProxyDn(String dn) {
+ public static String formatProxyDn(final String dn) {
return LT + sanitizeDn(dn) + GT;
}
/**
- * If a user provides a DN with the sequence '><', they could escape the tokenization process and impersonate another user.
+ * Sanitizes a DN for safe and lossless transmission.
+ *
+ * Sanitization requires:
+ * <ol>
+ * <li>Encoded so that it can be sent losslessly using US-ASCII (the character set of HTTP Header values)</li>
+ * <li>Resilient to a DN with the sequence '><' to attempt to escape the tokenization process and impersonate another user.</li>
+ * </ol>
+ *
* <p>
* Example:
* <p>
* Provided DN: {@code jdoe><alopresto} -> {@code <jdoe><alopresto><proxy...>} would allow the user to impersonate jdoe
+ * <p>Алйс
+ * Provided DN: {@code Алйс} -> {@code <Алйс>} cannot be encoded/decoded as ASCII
*
* @param rawDn the unsanitized DN
* @return the sanitized DN
*/
- private static String sanitizeDn(String rawDn) {
+ private static String sanitizeDn(final String rawDn) {
if (StringUtils.isEmpty(rawDn)) {
return rawDn;
} else {
- String sanitizedDn = rawDn.replaceAll(GT, ESCAPED_GT).replaceAll(LT, ESCAPED_LT);
- if (!sanitizedDn.equals(rawDn)) {
- logger.warn("The provided DN [" + rawDn + "] contained dangerous characters that were escaped to [" + sanitizedDn + "]");
+
+ // First, escape any GT [>] or LT [<] characters, which are not safe
+ final String escapedDn = rawDn.replaceAll(GT, ESCAPED_GT).replaceAll(LT, ESCAPED_LT);
+ if (!escapedDn.equals(rawDn)) {
+ logger.warn("The provided DN [" + rawDn + "] contained dangerous characters that were escaped to [" + escapedDn + "]");
}
- return sanitizedDn;
+
+ // Second, check for characters outside US-ASCII.
+ // This is necessary because X509 Certs can contain international/Unicode characters,
+ // but this value will be passed in an HTTP Header which must be US-ASCII.
+ // If non-ascii characters are present, base64 encode the DN and wrap in <angled-brackets>,
+ // to indicate to the receiving end that the value must be decoded.
+ // Note: We could have decided to always base64 encode these values,
+ // not only to avoid the isPureAscii(...) check, but also as a
+ // method of sanitizing GT [>] or LT [<] chars. However, there
+ // are advantages to encoding only when necessary, namely:
+ // 1. Backwards compatibility
+ // 2. Debugging this X-ProxiedEntitiesChain headers is easier unencoded.
+ // This algorithm can be revisited as part of the next major version change.
+ if (isPureAscii(escapedDn)) {
+ return escapedDn;
+ } else {
+ final String encodedDn = base64Encode(escapedDn);
+ logger.debug("The provided DN [" + rawDn + "] contained non-ASCII characters and was encoded as [" + encodedDn + "]");
+ return encodedDn;
+ }
}
}
@@ -77,16 +166,25 @@
* Example:
* <p>
* {@code alopresto\>\<proxy1} -> {@code alopresto><proxy1}
+ * <p>
+ * {@code %D0%90%D0%BB%D0%B9%D1%81} -> {@code Алйс}
*
* @param sanitizedDn the sanitized DN
* @return the original DN
*/
- private static String unsanitizeDn(String sanitizedDn) {
+ private static String unsanitizeDn(final String sanitizedDn) {
if (StringUtils.isEmpty(sanitizedDn)) {
return sanitizedDn;
} else {
- String unsanitizedDn = sanitizedDn.replaceAll(ESCAPED_GT, GT).replaceAll(ESCAPED_LT, LT);
- if (!unsanitizedDn.equals(sanitizedDn)) {
+ final String decodedDn;
+ if (isBase64Encoded(sanitizedDn)) {
+ decodedDn = base64Decode(sanitizedDn);
+ logger.debug("The provided DN [" + sanitizedDn + "] had been encoded, and was reconstituted to the original DN [" + decodedDn + "]");
+ } else {
+ decodedDn = sanitizedDn;
+ }
+ final String unsanitizedDn = decodedDn.replaceAll(ESCAPED_GT, GT).replaceAll(ESCAPED_LT, LT);
+ if (!unsanitizedDn.equals(decodedDn)) {
logger.warn("The provided DN [" + sanitizedDn + "] had been escaped, and was reconstituted to the dangerous DN [" + unsanitizedDn + "]");
}
return unsanitizedDn;
@@ -94,34 +192,60 @@
}
/**
- * Tokenizes the specified proxy chain.
+ * Base64 encodes a DN and wraps it in angled brackets to indicate the value is base64 and not a raw DN.
*
- * @param rawProxyChain raw chain
- * @return tokenized proxy chain
+ * @param rawValue The value to encode
+ * @return A string containing a wrapped, encoded value.
*/
- public static List<String> tokenizeProxiedEntitiesChain(String rawProxyChain) {
- final List<String> proxyChain = new ArrayList<>();
- if (!StringUtils.isEmpty(rawProxyChain)) {
- // Split the String on the >< token
- List<String> elements = Arrays.asList(StringUtils.splitByWholeSeparatorPreserveAllTokens(rawProxyChain, "><"));
+ private static String base64Encode(final String rawValue) {
+ final String base64String = Base64.getEncoder().encodeToString(rawValue.getBytes(StandardCharsets.UTF_8));
+ final String wrappedEncodedValue = LT + base64String + GT;
+ return wrappedEncodedValue;
+ }
- // Unsanitize each DN and collect back
- elements = elements.stream().map(ProxiedEntitiesUtils::unsanitizeDn).collect(Collectors.toList());
+ /**
+ * Performs the reverse of ${@link #base64Encode(String)}.
+ *
+ * @param encodedValue the encoded value to decode.
+ * @return The original, decoded string.
+ */
+ private static String base64Decode(final String encodedValue) {
+ final String base64String = encodedValue.substring(1, encodedValue.length() - 1);
+ return new String(Base64.getDecoder().decode(base64String), StandardCharsets.UTF_8);
+ }
- // Remove the leading < from the first element
- elements.set(0, elements.get(0).replaceFirst(LT, ""));
+ /**
+ * Check if a String is in the expected format and can be safely tokenized.
+ *
+ * @param rawProxiedEntitiesChain the value to check
+ * @return true if the value is in the valid format to tokenize, false otherwise.
+ */
+ private static boolean isValidChainFormat(final String rawProxiedEntitiesChain) {
+ return isWrappedInAngleBrackets(rawProxiedEntitiesChain);
+ }
- // Remove the trailing > from the last element
- int last = elements.size() - 1;
- String lastElement = elements.get(last);
- if (lastElement.endsWith(GT)) {
- elements.set(last, lastElement.substring(0, lastElement.length() - 1));
- }
+ /**
+ * Check if a value has been encoded by ${@link #base64Encode(String)}, and therefore needs to be decoded.
+ *
+ * @param token the value to check
+ * @return true if the value is encoded, false otherwise.
+ */
+ private static boolean isBase64Encoded(final String token) {
+ return isWrappedInAngleBrackets(token);
+ }
- proxyChain.addAll(elements);
- }
+ /**
+ * Check if a string is wrapped with <angle brackets>.
+ *
+ * @param string the value to check
+ * @return true if the value starts with < and ends with > - false otherwise
+ */
+ private static boolean isWrappedInAngleBrackets(final String string) {
+ return string.startsWith(LT) && string.endsWith(GT);
+ }
- return proxyChain;
+ private static boolean isPureAscii(final String stringWithUnknownCharacters) {
+ return StandardCharsets.US_ASCII.newEncoder().canEncode(stringWithUnknownCharacters);
}
}
diff --git a/nifi-registry-core/nifi-registry-security-utils/src/test/groovy/org/apache/nifi/registry/security/util/ProxiedEntitiesUtilsTest.groovy b/nifi-registry-core/nifi-registry-security-utils/src/test/groovy/org/apache/nifi/registry/security/util/ProxiedEntitiesUtilsTest.groovy
new file mode 100644
index 0000000..16b66e0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-utils/src/test/groovy/org/apache/nifi/registry/security/util/ProxiedEntitiesUtilsTest.groovy
@@ -0,0 +1,334 @@
+/*
+ * 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.security.util
+
+import org.junit.After
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import java.nio.charset.StandardCharsets
+
+@RunWith(JUnit4.class)
+class ProxiedEntitiesUtilsTest {
+ private static final Logger logger = LoggerFactory.getLogger(ProxiedEntitiesUtils.class)
+
+ private static final String SAFE_USER_NAME_ANDY = "alopresto"
+ private static final String SAFE_USER_DN_ANDY = "CN=${SAFE_USER_NAME_ANDY}, OU=Apache NiFi"
+
+ private static final String SAFE_USER_NAME_JOHN = "jdoe"
+ private static final String SAFE_USER_DN_JOHN = "CN=${SAFE_USER_NAME_JOHN}, OU=Apache NiFi"
+
+ private static final String SAFE_USER_NAME_PROXY_1 = "proxy1.nifi.apache.org"
+ private static final String SAFE_USER_DN_PROXY_1 = "CN=${SAFE_USER_NAME_PROXY_1}, OU=Apache NiFi"
+
+ private static final String SAFE_USER_NAME_PROXY_2 = "proxy2.nifi.apache.org"
+ private static final String SAFE_USER_DN_PROXY_2 = "CN=${SAFE_USER_NAME_PROXY_2}, OU=Apache NiFi"
+
+ private static
+ final String MALICIOUS_USER_NAME_JOHN = "${SAFE_USER_NAME_JOHN}, OU=Apache NiFi><CN=${SAFE_USER_NAME_PROXY_1}"
+ private static final String MALICIOUS_USER_DN_JOHN = "CN=${MALICIOUS_USER_NAME_JOHN}, OU=Apache NiFi"
+
+ private static final String UNICODE_DN_1 = "CN=Алйс, OU=Apache NiFi"
+ private static final String UNICODE_DN_1_ENCODED = "<" + base64Encode(UNICODE_DN_1) + ">"
+
+ private static final String UNICODE_DN_2 = "CN=Боб, OU=Apache NiFi"
+ private static final String UNICODE_DN_2_ENCODED = "<" + base64Encode(UNICODE_DN_2) + ">"
+
+ @BeforeClass
+ static void setUpOnce() throws Exception {
+ logger.metaClass.methodMissing = { String name, args ->
+ logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+ }
+ }
+
+ @Before
+ void setUp() {
+ }
+
+ @After
+ void tearDown() {
+ }
+
+ private static String sanitizeDn(String dn = "") {
+ dn.replaceAll(/>/, '\\\\>').replaceAll('<', '\\\\<')
+ }
+
+ private static String base64Encode(String dn = "") {
+ return Base64.getEncoder().encodeToString(dn.getBytes(StandardCharsets.UTF_8))
+ }
+
+ private static String printUnicodeString(final String raw) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < raw.size(); i++) {
+ int codePoint = Character.codePointAt(raw, i)
+ int charCount = Character.charCount(codePoint)
+ if (charCount > 1) {
+ i += charCount - 1 // 2.
+ if (i >= raw.length()) {
+ throw new IllegalArgumentException("Code point indicated more characters than available")
+ }
+ }
+ sb.append(String.format("\\u%04x ", codePoint))
+ }
+ return sb.toString().trim()
+ }
+
+ @Test
+ void testSanitizeDnShouldHandleFuzzing() throws Exception {
+ // Arrange
+ final String DESIRED_NAME = SAFE_USER_NAME_JOHN
+ logger.info(" Desired name: ${DESIRED_NAME} | ${printUnicodeString(DESIRED_NAME)}")
+
+ // Contains various attempted >< escapes, trailing NULL, and BACKSPACE + 'n'
+ final List MALICIOUS_NAMES = [MALICIOUS_USER_NAME_JOHN,
+ SAFE_USER_NAME_JOHN + ">",
+ SAFE_USER_NAME_JOHN + "><>",
+ SAFE_USER_NAME_JOHN + "\\>",
+ SAFE_USER_NAME_JOHN + "\u003e",
+ SAFE_USER_NAME_JOHN + "\u005c\u005c\u003e",
+ SAFE_USER_NAME_JOHN + "\u0000",
+ SAFE_USER_NAME_JOHN + "\u0008n"]
+
+ // Act
+ MALICIOUS_NAMES.each { String name ->
+ logger.info(" Raw name: ${name} | ${printUnicodeString(name)}")
+ String sanitizedName = ProxiedEntitiesUtils.sanitizeDn(name)
+ logger.info("Sanitized name: ${sanitizedName} | ${printUnicodeString(sanitizedName)}")
+
+ // Assert
+ assert sanitizedName != DESIRED_NAME
+ }
+ }
+
+ @Test
+ void testShouldFormatProxyDn() throws Exception {
+ // Arrange
+ final String DN = SAFE_USER_DN_JOHN
+ logger.info(" Provided proxy DN: ${DN}")
+
+ final String EXPECTED_PROXY_DN = "<${DN}>"
+ logger.info(" Expected proxy DN: ${EXPECTED_PROXY_DN}")
+
+ // Act
+ String forjohnedProxyDn = ProxiedEntitiesUtils.formatProxyDn(DN)
+ logger.info("Forjohned proxy DN: ${forjohnedProxyDn}")
+
+ // Assert
+ assert forjohnedProxyDn == EXPECTED_PROXY_DN
+ }
+
+ @Test
+ void testFormatProxyDnShouldHandleMaliciousInput() throws Exception {
+ // Arrange
+ final String DN = MALICIOUS_USER_DN_JOHN
+ logger.info(" Provided proxy DN: ${DN}")
+
+ final String SANITIZED_DN = sanitizeDn(DN)
+ final String EXPECTED_PROXY_DN = "<${SANITIZED_DN}>"
+ logger.info(" Expected proxy DN: ${EXPECTED_PROXY_DN}")
+
+ // Act
+ String forjohnedProxyDn = ProxiedEntitiesUtils.formatProxyDn(DN)
+ logger.info("Forjohned proxy DN: ${forjohnedProxyDn}")
+
+ // Assert
+ assert forjohnedProxyDn == EXPECTED_PROXY_DN
+ }
+
+ @Test
+ void testFormatProxyDnShouldEncodeNonAsciiCharacters() throws Exception {
+ // Arrange
+ logger.info(" Provided DN: ${UNICODE_DN_1}")
+ final String expectedFormattedDn = "<${UNICODE_DN_1_ENCODED}>"
+ logger.info(" Expected DN: expected")
+
+ // Act
+ String formattedDn = ProxiedEntitiesUtils.formatProxyDn(UNICODE_DN_1)
+ logger.info("Formatted DN: ${formattedDn}")
+
+ // Assert
+ assert formattedDn == expectedFormattedDn
+ }
+
+ @Test
+ void testGetProxiedEntitiesChain() throws Exception {
+ // Arrange
+ String[] input = [SAFE_USER_NAME_JOHN, SAFE_USER_DN_PROXY_1, SAFE_USER_DN_PROXY_2]
+ final String expectedOutput = "<${SAFE_USER_NAME_JOHN}><${SAFE_USER_DN_PROXY_1}><${SAFE_USER_DN_PROXY_2}>"
+
+ // Act
+ def output = ProxiedEntitiesUtils.getProxiedEntitiesChain(input)
+
+ // Assert
+ assert output == expectedOutput
+ }
+
+ @Test
+ void testGetProxiedEntitiesChainShouldHandleMaliciousInput() throws Exception {
+ // Arrange
+ String[] input = [MALICIOUS_USER_DN_JOHN, SAFE_USER_DN_PROXY_1, SAFE_USER_DN_PROXY_2]
+ final String expectedOutput = "<${sanitizeDn(MALICIOUS_USER_DN_JOHN)}><${SAFE_USER_DN_PROXY_1}><${SAFE_USER_DN_PROXY_2}>"
+
+ // Act
+ def output = ProxiedEntitiesUtils.getProxiedEntitiesChain(input)
+
+ // Assert
+ assert output == expectedOutput
+ }
+
+ @Test
+ void testGetProxiedEntitiesChainShouldEncodeUnicode() throws Exception {
+ // Arrange
+ String[] input = [SAFE_USER_NAME_JOHN, UNICODE_DN_1, UNICODE_DN_2]
+ final String expectedOutput = "<${SAFE_USER_NAME_JOHN}><${UNICODE_DN_1_ENCODED}><${UNICODE_DN_2_ENCODED}>"
+
+ // Act
+ def output = ProxiedEntitiesUtils.getProxiedEntitiesChain(input)
+
+ // Assert
+ assert output == expectedOutput
+ }
+
+ @Test
+ void testShouldTokenizeProxiedEntitiesChainWithUserNames() throws Exception {
+ // Arrange
+ final List NAMES = [SAFE_USER_NAME_JOHN, SAFE_USER_NAME_PROXY_1, SAFE_USER_NAME_PROXY_2]
+ final String RAW_PROXY_CHAIN = "<${NAMES.join("><")}>"
+ logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}")
+
+ // Act
+ def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN)
+ logger.info("Tokenized proxy chain: ${tokenizedNames}")
+
+ // Assert
+ assert tokenizedNames == NAMES
+ }
+
+ @Test
+ void testShouldTokenizeAnonymous() throws Exception {
+ // Arrange
+ final List NAMES = [""]
+ final String RAW_PROXY_CHAIN = "<>"
+ logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}")
+
+ // Act
+ def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN)
+ logger.info("Tokenized proxy chain: ${tokenizedNames}")
+
+ // Assert
+ assert tokenizedNames == NAMES
+ }
+
+ @Test
+ void testShouldTokenizeDoubleAnonymous() throws Exception {
+ // Arrange
+ final List NAMES = ["", ""]
+ final String RAW_PROXY_CHAIN = "<><>"
+ logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}")
+
+ // Act
+ def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN)
+ logger.info("Tokenized proxy chain: ${tokenizedNames}")
+
+ // Assert
+ assert tokenizedNames == NAMES
+ }
+
+ @Test
+ void testShouldTokenizeNestedAnonymous() throws Exception {
+ // Arrange
+ final List NAMES = [SAFE_USER_DN_PROXY_1, "", SAFE_USER_DN_PROXY_2]
+ final String RAW_PROXY_CHAIN = "<${SAFE_USER_DN_PROXY_1}><><${SAFE_USER_DN_PROXY_2}>"
+ logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}")
+
+ // Act
+ def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN)
+ logger.info("Tokenized proxy chain: ${tokenizedNames}")
+
+ // Assert
+ assert tokenizedNames == NAMES
+ }
+
+ @Test
+ void testShouldTokenizeProxiedEntitiesChainWithDNs() throws Exception {
+ // Arrange
+ final List DNS = [SAFE_USER_DN_JOHN, SAFE_USER_DN_PROXY_1, SAFE_USER_DN_PROXY_2]
+ final String RAW_PROXY_CHAIN = "<${DNS.join("><")}>"
+ logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}")
+
+ // Act
+ def tokenizedDns = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN)
+ logger.info("Tokenized proxy chain: ${tokenizedDns.collect { "\"${it}\"" }}")
+
+ // Assert
+ assert tokenizedDns == DNS
+ }
+
+ @Test
+ void testShouldTokenizeProxiedEntitiesChainWithAnonymousUser() throws Exception {
+ // Arrange
+ final List NAMES = ["", SAFE_USER_NAME_PROXY_1, SAFE_USER_NAME_PROXY_2]
+ final String RAW_PROXY_CHAIN = "<${NAMES.join("><")}>"
+ logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}")
+
+ // Act
+ def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN)
+ logger.info("Tokenized proxy chain: ${tokenizedNames}")
+
+ // Assert
+ assert tokenizedNames == NAMES
+ }
+
+ @Test
+ void testTokenizeProxiedEntitiesChainShouldHandleMaliciousUser() throws Exception {
+ // Arrange
+ final List NAMES = [MALICIOUS_USER_NAME_JOHN, SAFE_USER_NAME_PROXY_1, SAFE_USER_NAME_PROXY_2]
+ final String RAW_PROXY_CHAIN = "<${NAMES.collect { sanitizeDn(it) }.join("><")}>"
+ logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}")
+
+ // Act
+ def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN)
+ logger.info("Tokenized proxy chain: ${tokenizedNames.collect { "\"${it}\"" }}")
+
+ // Assert
+ assert tokenizedNames == NAMES
+ assert tokenizedNames.size() == NAMES.size()
+ assert !tokenizedNames.contains(SAFE_USER_NAME_JOHN)
+ }
+
+ @Test
+ void testTokenizeProxiedEntitiesChainShouldDecodeNonAsciiValues() throws Exception {
+ // Arrange
+ final String RAW_PROXY_CHAIN = "<${SAFE_USER_NAME_JOHN}><${UNICODE_DN_1_ENCODED}><${UNICODE_DN_2_ENCODED}>"
+ final List TOKENIZED_NAMES = [SAFE_USER_NAME_JOHN, UNICODE_DN_1, UNICODE_DN_2]
+ logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}")
+
+ // Act
+ def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN)
+ logger.info("Tokenized proxy chain: ${tokenizedNames.collect { "\"${it}\"" }}")
+
+ // Assert
+ assert tokenizedNames == TOKENIZED_NAMES
+ assert tokenizedNames.size() == TOKENIZED_NAMES.size()
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
index 3af1a0d..f6d1248 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
@@ -346,6 +346,8 @@
assertEquals(200, logout_response.getStatus());
// Then: the /access endpoint is queried using the logged out JWT
+ LOGGER.info("*** THE FOLLOWING JwtException IS EXPECTED ***");
+ LOGGER.info("*** We are validating the access token no longer works following logout ***");
final Response retryResponse = client
.target(createURL("/access"))
.request()
@@ -365,6 +367,7 @@
public void testLogoutWithJWTSignedByWrongKey() throws Exception {
// Given: use the /access/logout endpoint with the JWT for the nifiadmin LDAP user to log out
+ LOGGER.info("*** THE FOLLOWING JwtException IS EXPECTED ***");
final Response logoutResponse = client
.target(createURL("/access"))
.request()
@@ -652,6 +655,8 @@
accessClient.logout(token);
// check the status of the current user again and should be unauthorized
+ LOGGER.info("*** THE FOLLOWING JwtException IS EXPECTED ***");
+ LOGGER.info("*** We are validating the access token no longer works following logout ***");
try {
userClient.getAccessStatus();
Assert.fail("Should have failed with an unauthorized exception");
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java
index 92f5458..1401838 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java
@@ -107,12 +107,12 @@
assertEquals(INITIAL_ADMIN_IDENTITY, currentUser.getIdentity());
assertFalse(currentUser.isAnonymous());
assertNotNull(currentUser.getResourcePermissions());
- Permissions fullAccess = new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true);
+ final Permissions fullAccess = new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true);
assertEquals(fullAccess, currentUser.getResourcePermissions().getAnyTopLevelResource());
assertEquals(fullAccess, currentUser.getResourcePermissions().getBuckets());
assertEquals(fullAccess, currentUser.getResourcePermissions().getTenants());
assertEquals(fullAccess, currentUser.getResourcePermissions().getPolicies());
- assertEquals(new Permissions().withCanWrite(true).withCanRead(true).withCanDelete(true), currentUser.getResourcePermissions().getProxy());
+ assertEquals(fullAccess, currentUser.getResourcePermissions().getProxy());
}
@Test
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureProxyIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureProxyIT.java
new file mode 100644
index 0000000..0401ece
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureProxyIT.java
@@ -0,0 +1,229 @@
+/*
+ * 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.CurrentUser;
+import org.apache.nifi.registry.authorization.Permissions;
+import org.apache.nifi.registry.client.NiFiRegistryClient;
+import org.apache.nifi.registry.client.NiFiRegistryClientConfig;
+import org.apache.nifi.registry.client.RequestConfig;
+import org.apache.nifi.registry.client.UserClient;
+import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient;
+import org.apache.nifi.registry.client.impl.request.ProxiedEntityRequestConfig;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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.core.Response;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(
+ classes = NiFiRegistryTestApiApplication.class,
+ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+ properties = "spring.profiles.include=ITSecureProxy")
+@Import(SecureITClientConfiguration.class)
+@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:db/clearDB.sql"})
+public class SecureProxyIT extends IntegrationTestBase {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SecureProxyIT.class);
+
+ private static final String INITIAL_ADMIN_IDENTITY = "CN=user1, OU=nifi";
+ private static final String PROXY_IDENTITY = "CN=proxy, OU=nifi";
+ private static final String NEW_USER_IDENTITY = "CN=user2, OU=nifi";
+ private static final String UTF8_USER_IDENTITY = "CN=Алйс, OU=nifi";
+ private static final String ANONYMOUS_USER_IDENTITY = "";
+
+ private NiFiRegistryClient registryClient;
+
+ @Before
+ public void setup() {
+ final String baseUrl = createBaseURL();
+ LOGGER.info("Using base url = " + baseUrl);
+
+ final NiFiRegistryClientConfig clientConfig = createClientConfig(baseUrl);
+ Assert.assertNotNull(clientConfig);
+
+ final NiFiRegistryClient client = new JerseyNiFiRegistryClient.Builder()
+ .config(clientConfig)
+ .build();
+ Assert.assertNotNull(client);
+ this.registryClient = client;
+ }
+
+ @After
+ public void teardown() {
+ try {
+ registryClient.close();
+ } catch (final Exception e) {
+ // do nothing
+ }
+ }
+
+ @Test
+ public void testAccessStatus() throws Exception {
+
+ // Given: the client and server have been configured correctly for two-way TLS
+ final String expectedJson = "{" +
+ "\"identity\":\"CN=proxy, OU=nifi\"," +
+ "\"anonymous\":false," +
+ "\"resourcePermissions\":{" +
+ "\"anyTopLevelResource\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+ "\"buckets\":{\"canRead\":true,\"canWrite\":false,\"canDelete\":false}," +
+ "\"tenants\":{\"canRead\":false,\"canWrite\":false,\"canDelete\":false}," +
+ "\"policies\":{\"canRead\":false,\"canWrite\":false,\"canDelete\":false}," +
+ "\"proxy\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}}" +
+ "}";
+
+ // 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());
+ final String actualJson = response.readEntity(String.class);
+ JSONAssert.assertEquals(expectedJson, actualJson, false);
+ }
+
+ @Test
+ public void testAccessStatusUsingRegistryClient() throws Exception {
+
+ // Given: the client and server have been configured correctly for two-way TLS
+ final Permissions fullAccess = new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true);
+ final Permissions readAccess = new Permissions().withCanRead(true).withCanWrite(false).withCanDelete(false);
+ final Permissions noAccess = new Permissions().withCanRead(false).withCanWrite(false).withCanDelete(false);
+
+ // When: the /access endpoint is queried
+ final UserClient userClient = registryClient.getUserClient();
+ final CurrentUser currentUser = userClient.getAccessStatus();
+
+ // Then: the server returns the proxy identity with default nifi node access
+ assertEquals(PROXY_IDENTITY, currentUser.getIdentity());
+ assertFalse(currentUser.isAnonymous());
+ assertNotNull(currentUser.getResourcePermissions());
+ assertEquals(fullAccess, currentUser.getResourcePermissions().getAnyTopLevelResource());
+ assertEquals(readAccess, currentUser.getResourcePermissions().getBuckets());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getTenants());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getPolicies());
+ assertEquals(fullAccess, currentUser.getResourcePermissions().getProxy());
+ }
+
+ @Test
+ public void testAccessStatusAsProxiedAdmin() throws Exception {
+
+ // Given: the client and server have been configured correctly for two-way TLS
+ final Permissions fullAccess = new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true);
+ final RequestConfig proxiedEntityRequestConfig = new ProxiedEntityRequestConfig(INITIAL_ADMIN_IDENTITY);
+
+ // When: the /access endpoint is queried using X-ProxiedEntitiesChain
+ final UserClient userClient = registryClient.getUserClient(proxiedEntityRequestConfig);
+ final CurrentUser currentUser = userClient.getAccessStatus();
+
+ // Then: the server returns the admin identity and access policies
+ assertEquals(INITIAL_ADMIN_IDENTITY, currentUser.getIdentity());
+ assertFalse(currentUser.isAnonymous());
+ assertNotNull(currentUser.getResourcePermissions());
+ assertEquals(fullAccess, currentUser.getResourcePermissions().getAnyTopLevelResource());
+ assertEquals(fullAccess, currentUser.getResourcePermissions().getBuckets());
+ assertEquals(fullAccess, currentUser.getResourcePermissions().getTenants());
+ assertEquals(fullAccess, currentUser.getResourcePermissions().getPolicies());
+ assertEquals(fullAccess, currentUser.getResourcePermissions().getProxy());
+ }
+
+ @Test
+ public void testAccessStatusAsProxiedUser() throws Exception {
+
+ // Given: the client and server have been configured correctly for two-way TLS
+ final Permissions noAccess = new Permissions().withCanRead(false).withCanWrite(false).withCanDelete(false);
+ final RequestConfig proxiedEntityRequestConfig = new ProxiedEntityRequestConfig(NEW_USER_IDENTITY);
+
+ // When: the /access endpoint is queried using X-ProxiedEntitiesChain
+ final UserClient userClient = registryClient.getUserClient(proxiedEntityRequestConfig);
+ final CurrentUser currentUser = userClient.getAccessStatus();
+
+ // Then: the server returns the user identity ad
+ assertEquals(NEW_USER_IDENTITY, currentUser.getIdentity());
+ assertFalse(currentUser.isAnonymous());
+ assertNotNull(currentUser.getResourcePermissions());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getAnyTopLevelResource());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getBuckets());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getTenants());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getPolicies());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getProxy());
+ }
+
+ @Test
+ public void testAccessStatusAsProxiedAnonymousUser() throws Exception {
+
+ // Given: the client and server have been configured correctly for two-way TLS
+ final Permissions noAccess = new Permissions().withCanRead(false).withCanWrite(false).withCanDelete(false);
+ final RequestConfig proxiedEntityRequestConfig = new ProxiedEntityRequestConfig(ANONYMOUS_USER_IDENTITY);
+
+ // When: the /access endpoint is queried using X-ProxiedEntitiesChain
+ final UserClient userClient = registryClient.getUserClient(proxiedEntityRequestConfig);
+ final CurrentUser currentUser = userClient.getAccessStatus();
+
+ // Then: the server returns the proxy identity with default nifi node access
+ assertEquals("anonymous", currentUser.getIdentity());
+ assertTrue(currentUser.isAnonymous());
+ assertNotNull(currentUser.getResourcePermissions());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getAnyTopLevelResource());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getBuckets());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getTenants());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getPolicies());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getProxy());
+ }
+
+ @Test
+ public void testAccessStatusAsProxiedUtf8User() throws Exception {
+
+ // Given: the client and server have been configured correctly for two-way TLS
+ final Permissions noAccess = new Permissions().withCanRead(false).withCanWrite(false).withCanDelete(false);
+ final RequestConfig proxiedEntityRequestConfig = new ProxiedEntityRequestConfig(UTF8_USER_IDENTITY);
+
+ // When: the /access endpoint is queried using X-ProxiedEntitiesChain
+ final UserClient userClient = registryClient.getUserClient(proxiedEntityRequestConfig);
+ final CurrentUser currentUser = userClient.getAccessStatus();
+
+ // Then: the server returns the proxy identity with default nifi node access
+ assertEquals(UTF8_USER_IDENTITY, currentUser.getIdentity());
+ assertFalse(currentUser.isAnonymous());
+ assertNotNull(currentUser.getResourcePermissions());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getAnyTopLevelResource());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getBuckets());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getTenants());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getPolicies());
+ assertEquals(noAccess, currentUser.getResourcePermissions().getProxy());
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureProxy.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureProxy.properties
new file mode 100644
index 0000000..cab6d41
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureProxy.properties
@@ -0,0 +1,36 @@
+#
+# 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.
+#
+
+
+# Properties for Spring Boot integration tests
+# Documentation for common Spring Boot application properties can be found at:
+# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
+
+
+# Custom (non-standard to Spring Boot) properties
+nifi.registry.properties.file: src/test/resources/conf/secure-proxy/nifi-registry.properties
+nifi.registry.client.properties.file: src/test/resources/conf/secure-proxy/nifi-registry-client.properties
+
+
+# Embedded Server SSL Context Config
+server.ssl.client-auth: need
+server.ssl.key-store: ./target/test-classes/keys/registry-ks.jks
+server.ssl.key-store-password: password
+server.ssl.key-password: password
+server.ssl.protocol: TLS
+server.ssl.trust-store: ./target/test-classes/keys/ca-ts.jks
+server.ssl.trust-store-password: password
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/authorizers.xml b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/authorizers.xml
new file mode 100644
index 0000000..d4fedab
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/authorizers.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+ ~ 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.
+ -->
+<!--
+ This file lists the userGroupProviders, accessPolicyProviders, and authorizers to use when running securely. In order
+ to use a specific authorizer it must be configured here and its identifier must be specified in the nifi-registry.properties file.
+ If the authorizer is a managedAuthorizer, it may need to be configured with an accessPolicyProvider and an userGroupProvider.
+ This file allows for configuration of them, but they must be configured in order:
+
+ ...
+ all userGroupProviders
+ all accessPolicyProviders
+ all Authorizers
+ ...
+-->
+<authorizers>
+
+ <!--
+ The FileUserGroupProvider will provide support for managing users and groups which is backed by a file
+ on the local file system.
+
+ - Users File - The file where the FileUserGroupProvider will store users and groups.
+
+ - Initial User Identity [unique key] - The identity of a users and systems to seed the Users File. The name of
+ each property must be unique, for example: "Initial User Identity A", "Initial User Identity B",
+ "Initial User Identity C" or "Initial User Identity 1", "Initial User Identity 2", "Initial User Identity 3"
+
+ NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the user identities,
+ so the values should be the unmapped identities (i.e. full DN from a certificate).
+ -->
+ <userGroupProvider>
+ <identifier>file-user-group-provider</identifier>
+ <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+ <property name="Users File">./target/test-classes/conf/secure-proxy/users.xml</property>
+ <property name="Initial User Identity 1">CN=user1, OU=nifi</property>
+ <property name="Initial User Identity 2">CN=user2, OU=nifi</property>
+ <property name="Initial User Identity 3">CN=Алйс, OU=nifi</property>
+ <property name="Initial User Identity 4">CN=proxy, OU=nifi</property>
+ </userGroupProvider>
+
+ <!--
+ The FileAccessPolicyProvider will provide support for managing access policies which is backed by a file
+ on the local file system.
+
+ - User Group Provider - The identifier for an User Group Provider defined above that will be used to access
+ users and groups for use in the managed access policies.
+
+ - Authorizations File - The file where the FileAccessPolicyProvider will store policies.
+
+ - Initial Admin Identity - The identity of an initial admin user that will be granted access to the UI and
+ given the ability to create additional users, groups, and policies. The value of this property could be
+ a DN when using certificates or LDAP. This property will only be used when there
+ are no other policies defined.
+
+ NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the initial admin identity,
+ so the value should be the unmapped identity. This identity must be found in the configured User Group Provider.
+
+ - NiFi Identity [unique key] - The identity of a NiFi node that will have access to this NiFi Registry and will be able
+ to act as a proxy on behalf of a NiFi Registry end user. A property should be created for the identity of every NiFi
+ node that needs to access this NiFi Registry.
+
+ NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the nifi identities,
+ so the values should be the unmapped identities (i.e. full DN from a certificate). This identity must be found
+ in the configured User Group Provider.
+ -->
+ <accessPolicyProvider>
+ <identifier>file-access-policy-provider</identifier>
+ <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+ <property name="User Group Provider">file-user-group-provider</property>
+ <property name="Authorizations File">./target/test-classes/conf/secure-proxy/authorizations.xml</property>
+ <property name="Initial Admin Identity">CN=user1, OU=nifi</property>
+ <property name="NiFi Identity 1">CN=proxy, OU=nifi</property>
+ </accessPolicyProvider>
+
+ <!--
+ The StandardManagedAuthorizer. This authorizer implementation must be configured with the
+ Access Policy Provider which it will use to access and manage users, groups, and policies.
+ These users, groups, and policies will be used to make all access decisions during authorization
+ requests.
+
+ - Access Policy Provider - The identifier for an Access Policy Provider defined above.
+ -->
+ <authorizer>
+ <identifier>managed-authorizer</identifier>
+ <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+ <property name="Access Policy Provider">file-access-policy-provider</property>
+ </authorizer>
+
+</authorizers>
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry-client.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry-client.properties
new file mode 100644
index 0000000..b4c1a6a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry-client.properties
@@ -0,0 +1,25 @@
+#
+# 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.
+#
+
+# client security properties #
+nifi.registry.security.keystore=./target/test-classes/keys/proxy-ks.jks
+nifi.registry.security.keystoreType=JKS
+nifi.registry.security.keystorePasswd=password
+nifi.registry.security.keyPasswd=password
+nifi.registry.security.truststore=./target/test-classes/keys/ca-ts.jks
+nifi.registry.security.truststoreType=JKS
+nifi.registry.security.truststorePasswd=password
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry.properties
new file mode 100644
index 0000000..1ae5cc3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry.properties
@@ -0,0 +1,33 @@
+#
+# 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.https.host=localhost
+nifi.registry.web.https.port=0
+
+# security properties #
+#
+# ** Server KeyStore and TrustStore configuration set in Spring profile properties for embedded Jetty **
+#
+nifi.registry.security.authorizers.configuration.file=./target/test-classes/conf/secure-proxy/authorizers.xml
+nifi.registry.security.authorizer=managed-authorizer
+
+# 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/keys/README.md b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/README.md
index 24460cd..129b504 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/README.md
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/README.md
@@ -41,6 +41,10 @@
| registry, localhost | registry-key.pem | NiFi Registry server private key | PEM | password |
| registry, localhost | registry-ks.jks | NiFi Registry server key/cert keystore | JKS | password |
| registry, localhost | registry-ks.p12 | NiFi Registry server key/cert keystore | PKCS12 | password |
+| proxy, localhost | proxy-cert.pem | Proxy server public cert | PEM (unencrypted) | N/A |
+| proxy, localhost | proxy-key.pem | Proxy server private key | PEM | password |
+| proxy, localhost | proxy-ks.jks | Proxy server key/cert keystore | JKS | password |
+| proxy, localhost | proxy-ks.p12 | Proxy server key/cert keystore | PKCS12 | password |
| CN=user1, OU=nifi | user1-cert.pem | client (user="user1") public cert | PEM (unencrypted) | N/A |
| CN=user1, OU=nifi | user1-key.pem | client (user="user1") private key | PEM | password |
| CN=user1, OU=nifi | user1-ks.jks | client (user="user1") key/cert keystore | JKS | password |
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-cert.pem b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-cert.pem
new file mode 100644
index 0000000..67d2960
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-cert.pem
@@ -0,0 +1,51 @@
+Bag Attributes
+ friendlyName: proxy-key
+ localKeyID: 54 69 6D 65 20 31 35 39 39 36 37 38 39 33 36 31 30 34
+subject=/OU=nifi/CN=proxy
+issuer=/OU=nifi/CN=Test CA (Do Not Trust)
+-----BEGIN CERTIFICATE-----
+MIIDbjCCAlagAwIBAgIKAXR0SvpRAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD
+VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTIw
+MDkwOTE5MTUwNFoXDTQ4MDEyNTE5MTUwNFowHzENMAsGA1UECwwEbmlmaTEOMAwG
+A1UEAwwFcHJveHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2CeF8
+tYG/58pjW0nE5Y/JnZzSxQpjs9sFzvJx9V8SEviAPxCSv0tP4PhR09hMLdKA2bTB
+TM1S189fkN09fLLQtFucAJtsqseNFlZh/iJhmX1I1n5MIaLvffFNpXEaMbWOKSX7
+NfTT4mtS0jNJSpkR6bQEVrz8iiMrrMFGz9AiwEba/pg0hUTP+VP4pt7aKFwb8ZyL
+6Wxo+Ny6nMe4M7KHcQi4+Cwmgc5YChMAGfCID+xGyND4vR2WJxNYe1joT+NXtGzY
+zgYDU3NiYJN7lB3NKTZpXz9/Aga+NSo6pFzEFXiFNmFU50O7wkdhlhQfRlBSgjCY
+0vL3X6N1CXSOKCbrAgMBAAGjgZowgZcwHQYDVR0OBBYEFCFvl95yYwhk81eaG4mH
+dd94iKeXMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMA4GA1UdDwEB
+/wQEAwID+DAJBgNVHRMEAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
+ATAbBgNVHREEFDASggVwcm94eYIJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IB
+AQA1rAxZX2ebm40XOFhecxUQhTYJW+VYJUXRoxtjaEL9NwDvTcI8CmmaZ5LNF73E
+doT75HOo+33bQz/Xnp9zFRWmiF4KH3u60cZ0obNpTCtWfY41UbN8La6FPokmaV+L
+7POigubeVZf/6d7Hf8PcBQeXE+CNKJMQ73RoKbMWcdEhEdio1sXdMNPBo4m5SeDe
+T/nbIbLJiFPo20lir3Q4OrzGOUnqwyT+7L64myjVkHgyjOzHC5PH4D5XGMNfQ6tQ
+O2/flOso0ALGTB9VBo20sITS+BnPlMsWGdjQya/d1Oc4CEifRiWFx6H9daCb29/o
+9ReApmPoZV801EkEAhPwqyZd
+-----END CERTIFICATE-----
+Bag Attributes
+ friendlyName: CN=Test CA (Do Not Trust),OU=nifi
+subject=/OU=nifi/CN=Test CA (Do Not Trust)
+issuer=/OU=nifi/CN=Test CA (Do Not Trust)
+-----BEGIN CERTIFICATE-----
+MIIDYzCCAkugAwIBAgIKAWfClyDGAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD
+VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTE4
+MTIxODE4MzIyM1oXDTQ2MDUwNDE4MzIyM1owMDENMAsGA1UECwwEbmlmaTEfMB0G
+A1UEAwwWVGVzdCBDQSAoRG8gTm90IFRydXN0KTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAIv7lVgGRHGaYmKkeTJpFzAp6QA7Anik/u1a+1ngGFWf9e6l
+RkSX6US+nPbDRLJpSkO0c+/v8BwAKBiHUFaGF9XV7YvX92x/Gb3/FidSu+HAW/w/
+keIZ8PHvXbMtTvEur+nY1hSDvssdw1nAYAB9DG26HdRSg5c1DYgHLk9WCDWuIspU
+n31YCb0lStWWbHM53i8xLfeV3IdOw9P3+d8bopzUUjk2quSxxekvzLCC1e14csJG
+GIKLplRUq+zWRgkGYF8Fkx+kYGL62sehAdVcblxjwnXnmlPHvlxeaclsAVn4LCQj
+gQzstzAv+s7sNSCxHba4vAusszWxOFiM1Vk8VvcCAwEAAaN/MH0wDgYDVR0PAQH/
+BAQDAgH+MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFNukt0jKduJKyg8F+c/3j0w2
+AcnHMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMB0GA1UdJQQWMBQG
+CCsGAQUFBwMCBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEAMvNsYLooq3zh
+ts0fPU8dNcfe/NXFK6Uwg0RQPtq/l1ChGnZgXicx+RHMR5Q08pR62e+3gztk+LRE
+iR9PpXqKFLM8slhR1z4sZ+Ja38ZHcOjsDPJeMKjUTrK8MNQN3YPKzoPE0AnLmsZI
+Kf1eUIXXA3uXiXkIIVuxPPK96Q5Rla0xnbOpgejzGJ0BIMFP3odLlSahtT2Gl6wC
+bdyImBkFntRJMoUx1fwUSKvIN5GUpaG6+E3mwgjckTUGZ15WrAllWqzhI06T73Yv
+qR4FsQizqrqLimrIgvCBH6SWbOcsjCH/I58KqMRtG+kmfa/iwMfy0MMzuzx1Kwbr
+qOi08D8F0w==
+-----END CERTIFICATE-----
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-key.pem b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-key.pem
new file mode 100644
index 0000000..1a5417d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-key.pem
@@ -0,0 +1,85 @@
+Bag Attributes
+ friendlyName: proxy-key
+ localKeyID: 54 69 6D 65 20 31 35 39 39 36 37 38 39 33 36 31 30 34
+Key Attributes: <No Attributes>
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIkocYj9mKFyMCAggA
+MBQGCCqGSIb3DQMHBAibNQqgCb2bzQSCBMiTWItLGuguyT/g4eZXZMZvZW2cnZ5+
+heTqXLAWChAt8IwiGYC1K40PcVQvNBPFoLqbdH2EsvmK8l7tNv1tq/kjpkuhVuwL
+06E9m9Y8ucS0Q60WlREaac2kth5rWdx586kwKsM7h79fKSS86F3tCbamIgdst7xQ
+WQ1z7Dcx6Et6VY4F8i6kEgp9b5zIHmbBxB/b9dfHDJSQ5WyzHjc1raQ6XJ/c7DFs
+URkafQbAQ5urHVS9+aWfCPXT9Wjl/4kYqcRWkYua/ulhzcIKyBr1BP+t4Xsoj3da
+4AZzR+ZSQiKj6UkBSssEJbPtOfonhQrFsoTm3sR1k6pXa4uySef42WGoex77ekco
+ReXiAI7Z1tPp3QI6ossYK5R7WY876/fblMNL96hGtCQOM0P9x5L58sVZJ0j2xbJx
+Tvu91yKA3vVn7cI0z99dz3ULuM2bdmKoZbwMlhgk1X7VRg43GHLVd9NlpUido7/Y
+b/KV0aGKrxBr3rdXu+rVZ/7ElhmQ3q7nkU6hxo3Lj8eF9dkJzhxPtRvUAqsRhaN0
+c/iNSNJsDX1g+LOzuGI9xxS8lg61C73jz4Dhfv+CPNZzVXiJctY74hMzTw0gkkSW
+4cN/0UoGdjVpeUScy9+IqF94Hp6w7xDXTJ6egoT60ZtT7Ls8zc6TTNl+qo/IGEnw
+QA+7Z6OY38Z0gMQivtDeOrLFFcTy98ZBibvJlw/LsW2KadXzGooXuJX2AgTfs3U5
+oIBjmubincvVhpcQDAdzifzURZuAnvEb1FvQIAC/jp8U009f2JByycobmviluNSp
+2EirUpA0XsW8B99EUlUfzbCOAb9YS8vesyzevJP43XjKlNNsptG4pe8jiMaxUFGE
+1fdseWJwiRfSXpwQBafovOVXItxZWkrfD+i7h6LfG79hDFNHTcDknoYjJzcAYOYF
+ju9vUhE9dgyZogppOQ32NURkyh8HkHZFGI9MlaoqNXl4z9TRMUVxJET/ZCJ0zsE+
+WV5KL6oiPU5VuGbIReouDtPd6lOtqsaE3PoKfriGhIKSGOHbxOZa3WLs/++BJO8A
+5eNKWuqwUtL4uPw4gnxJFIyK04lzMp5q5BT7fkrlenRvsOn8IGZp6e5Wvsb3dCx1
+LKR8ktyjnxSpSw9dACDeGGxTeCOUxM5ElafI7RKsJ1kCtUpxMyfTru1SmP3Hcepc
+GteHjm/vrv9TW0ZBhGFsuxo5AuZFnsjSAy6JVHRYQaNzdTs8qJTZvYhGjq/XSI34
+Jafugsu1uKMKu+RfvbSM30/70kXu6R0DT55w1j8AMbdchyJDBQT4Ua6zK/N2CHW2
+++jxwM7h7C4imFgsILjo41TT7Ve2vqG3PkLaSIgotZDs9zCYJxN/A9QGHTfzjQvy
+zp2yNCC5ebyGfCgBafdCasyyPuJGVagSvNky7fRlhu0HI+kPIarL6xASFDxDVBEF
+ko5kHDbKZHFp4skdi+Bu/DKtIoGUEJabRkkyOO0rOrq4ldEm3oAnn3PIUaVk0NNb
+pcSx9Hb/M2wWRMHdTpRrBYJ++GlcCn2oaIX4kkxk5ERvm17guHxWATq4spI3GM38
+kCcXwUCEjV1ILpn43m/+1RPTzFKpa7S79HjMWbQEx7VTgtVwxqFq3bGUTV7MtnSQ
+rbg=
+-----END ENCRYPTED PRIVATE KEY-----
+Bag Attributes
+ friendlyName: proxy-key
+ localKeyID: 54 69 6D 65 20 31 35 39 39 36 37 38 39 33 36 31 30 34
+subject=/OU=nifi/CN=proxy
+issuer=/OU=nifi/CN=Test CA (Do Not Trust)
+-----BEGIN CERTIFICATE-----
+MIIDbjCCAlagAwIBAgIKAXR0SvpRAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD
+VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTIw
+MDkwOTE5MTUwNFoXDTQ4MDEyNTE5MTUwNFowHzENMAsGA1UECwwEbmlmaTEOMAwG
+A1UEAwwFcHJveHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2CeF8
+tYG/58pjW0nE5Y/JnZzSxQpjs9sFzvJx9V8SEviAPxCSv0tP4PhR09hMLdKA2bTB
+TM1S189fkN09fLLQtFucAJtsqseNFlZh/iJhmX1I1n5MIaLvffFNpXEaMbWOKSX7
+NfTT4mtS0jNJSpkR6bQEVrz8iiMrrMFGz9AiwEba/pg0hUTP+VP4pt7aKFwb8ZyL
+6Wxo+Ny6nMe4M7KHcQi4+Cwmgc5YChMAGfCID+xGyND4vR2WJxNYe1joT+NXtGzY
+zgYDU3NiYJN7lB3NKTZpXz9/Aga+NSo6pFzEFXiFNmFU50O7wkdhlhQfRlBSgjCY
+0vL3X6N1CXSOKCbrAgMBAAGjgZowgZcwHQYDVR0OBBYEFCFvl95yYwhk81eaG4mH
+dd94iKeXMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMA4GA1UdDwEB
+/wQEAwID+DAJBgNVHRMEAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
+ATAbBgNVHREEFDASggVwcm94eYIJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IB
+AQA1rAxZX2ebm40XOFhecxUQhTYJW+VYJUXRoxtjaEL9NwDvTcI8CmmaZ5LNF73E
+doT75HOo+33bQz/Xnp9zFRWmiF4KH3u60cZ0obNpTCtWfY41UbN8La6FPokmaV+L
+7POigubeVZf/6d7Hf8PcBQeXE+CNKJMQ73RoKbMWcdEhEdio1sXdMNPBo4m5SeDe
+T/nbIbLJiFPo20lir3Q4OrzGOUnqwyT+7L64myjVkHgyjOzHC5PH4D5XGMNfQ6tQ
+O2/flOso0ALGTB9VBo20sITS+BnPlMsWGdjQya/d1Oc4CEifRiWFx6H9daCb29/o
+9ReApmPoZV801EkEAhPwqyZd
+-----END CERTIFICATE-----
+Bag Attributes
+ friendlyName: CN=Test CA (Do Not Trust),OU=nifi
+subject=/OU=nifi/CN=Test CA (Do Not Trust)
+issuer=/OU=nifi/CN=Test CA (Do Not Trust)
+-----BEGIN CERTIFICATE-----
+MIIDYzCCAkugAwIBAgIKAWfClyDGAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD
+VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTE4
+MTIxODE4MzIyM1oXDTQ2MDUwNDE4MzIyM1owMDENMAsGA1UECwwEbmlmaTEfMB0G
+A1UEAwwWVGVzdCBDQSAoRG8gTm90IFRydXN0KTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAIv7lVgGRHGaYmKkeTJpFzAp6QA7Anik/u1a+1ngGFWf9e6l
+RkSX6US+nPbDRLJpSkO0c+/v8BwAKBiHUFaGF9XV7YvX92x/Gb3/FidSu+HAW/w/
+keIZ8PHvXbMtTvEur+nY1hSDvssdw1nAYAB9DG26HdRSg5c1DYgHLk9WCDWuIspU
+n31YCb0lStWWbHM53i8xLfeV3IdOw9P3+d8bopzUUjk2quSxxekvzLCC1e14csJG
+GIKLplRUq+zWRgkGYF8Fkx+kYGL62sehAdVcblxjwnXnmlPHvlxeaclsAVn4LCQj
+gQzstzAv+s7sNSCxHba4vAusszWxOFiM1Vk8VvcCAwEAAaN/MH0wDgYDVR0PAQH/
+BAQDAgH+MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFNukt0jKduJKyg8F+c/3j0w2
+AcnHMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMB0GA1UdJQQWMBQG
+CCsGAQUFBwMCBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEAMvNsYLooq3zh
+ts0fPU8dNcfe/NXFK6Uwg0RQPtq/l1ChGnZgXicx+RHMR5Q08pR62e+3gztk+LRE
+iR9PpXqKFLM8slhR1z4sZ+Ja38ZHcOjsDPJeMKjUTrK8MNQN3YPKzoPE0AnLmsZI
+Kf1eUIXXA3uXiXkIIVuxPPK96Q5Rla0xnbOpgejzGJ0BIMFP3odLlSahtT2Gl6wC
+bdyImBkFntRJMoUx1fwUSKvIN5GUpaG6+E3mwgjckTUGZ15WrAllWqzhI06T73Yv
+qR4FsQizqrqLimrIgvCBH6SWbOcsjCH/I58KqMRtG+kmfa/iwMfy0MMzuzx1Kwbr
+qOi08D8F0w==
+-----END CERTIFICATE-----
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.jks b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.jks
new file mode 100644
index 0000000..444b43d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.jks
Binary files differ
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.p12 b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.p12
new file mode 100644
index 0000000..9eca9d8
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.p12
Binary files differ