blob: 16b66e0f85626511f85227a55503ba0ebe1032ba [file] [log] [blame]
/*
* 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()
}
}