From c3b4872b552922058c6536e0810dabfe9ca20b10 Mon Sep 17 00:00:00 2001 From: Bryan Bende Date: Mon, 25 Jul 2016 16:15:49 -0400 Subject: [PATCH] NIFI-2389 Refactoring identity mapping and applying it to FileAuthorizer for initial admin, cluster nodes, and legacy authorized users. This closes #719 --- .../nifi/authorization/FileAuthorizer.java | 19 ++- .../authorization/FileAuthorizerTest.java | 140 +++++++++++++++++ .../resources/authorized-users-with-dns.xml | 35 +++++ .../src/test/resources/flow-with-dns.xml.gz | Bin 0 -> 784 bytes .../nifi-framework-authorization/pom.xml | 4 + .../authorization/util/IdentityMapping.java | 48 ++++++ .../util/IdentityMappingUtil.java | 145 ++++++++++++++++++ .../src/main/resources/conf/authorizers.xml | 6 + .../security/NiFiAuthenticationProvider.java | 138 +---------------- .../NiFiAuthenticationProviderTest.java | 11 +- 10 files changed, 402 insertions(+), 144 deletions(-) create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/test/resources/authorized-users-with-dns.xml create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/test/resources/flow-with-dns.xml.gz create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/util/IdentityMapping.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/util/IdentityMappingUtil.java diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/main/java/org/apache/nifi/authorization/FileAuthorizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/main/java/org/apache/nifi/authorization/FileAuthorizer.java index 99672b6f17..3f714d4763 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/main/java/org/apache/nifi/authorization/FileAuthorizer.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/main/java/org/apache/nifi/authorization/FileAuthorizer.java @@ -27,6 +27,8 @@ import org.apache.nifi.authorization.file.generated.Policy; import org.apache.nifi.authorization.file.generated.Users; import org.apache.nifi.authorization.resource.ResourceFactory; import org.apache.nifi.authorization.resource.ResourceType; +import org.apache.nifi.authorization.util.IdentityMapping; +import org.apache.nifi.authorization.util.IdentityMappingUtil; import org.apache.nifi.components.PropertyValue; import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.util.file.FileUtils; @@ -106,6 +108,7 @@ public class FileAuthorizer extends AbstractPolicyBasedAuthorizer { private String legacyAuthorizedUsersFile; private Set nodeIdentities; private List ports = new ArrayList<>(); + private List identityMappings; private final AtomicReference authorizationsHolder = new AtomicReference<>(); @@ -160,9 +163,12 @@ public class FileAuthorizer extends AbstractPolicyBasedAuthorizer { } } + // extract the identity mappings from nifi.properties if any are provided + identityMappings = Collections.unmodifiableList(IdentityMappingUtil.getIdentityMappings(properties)); + // get the value of the initial admin identity final PropertyValue initialAdminIdentityProp = configurationContext.getProperty(PROP_INITIAL_ADMIN_IDENTITY); - initialAdminIdentity = initialAdminIdentityProp == null ? null : initialAdminIdentityProp.getValue(); + initialAdminIdentity = initialAdminIdentityProp == null ? null : IdentityMappingUtil.mapIdentity(initialAdminIdentityProp.getValue(), identityMappings); // get the value of the legacy authorized users file final PropertyValue legacyAuthorizedUsersProp = configurationContext.getProperty(PROP_LEGACY_AUTHORIZED_USERS_FILE); @@ -173,7 +179,7 @@ public class FileAuthorizer extends AbstractPolicyBasedAuthorizer { for (Map.Entry entry : configurationContext.getProperties().entrySet()) { Matcher matcher = NODE_IDENTITY_PATTERN.matcher(entry.getKey()); if (matcher.matches() && !StringUtils.isBlank(entry.getValue())) { - nodeIdentities.add(entry.getValue()); + nodeIdentities.add(IdentityMappingUtil.mapIdentity(entry.getValue(), identityMappings)); } } @@ -362,7 +368,7 @@ public class FileAuthorizer extends AbstractPolicyBasedAuthorizer { // get all the user DNs into a list List userIdentities = new ArrayList<>(); for (org.apache.nifi.user.generated.User legacyUser : users.getUser()) { - userIdentities.add(legacyUser.getDn()); + userIdentities.add(IdentityMappingUtil.mapIdentity(legacyUser.getDn(), identityMappings)); } // sort the list and pull out the first identity @@ -376,7 +382,7 @@ public class FileAuthorizer extends AbstractPolicyBasedAuthorizer { for (org.apache.nifi.user.generated.User legacyUser : users.getUser()) { // create the identifier of the new user based on the DN - final String legacyUserDn = legacyUser.getDn(); + final String legacyUserDn = IdentityMappingUtil.mapIdentity(legacyUser.getDn(), identityMappings); final String userIdentifier = UUID.nameUUIDFromBytes(legacyUserDn.getBytes(StandardCharsets.UTF_8)).toString(); // create the new User and add it to the list of users @@ -421,10 +427,13 @@ public class FileAuthorizer extends AbstractPolicyBasedAuthorizer { if (portDTO.getUserAccessControl() != null) { for (String userAccessControl : portDTO.getUserAccessControl()) { + // need to perform the identity mapping on the access control so it matches the identities in the User objects + final String mappedUserAccessControl = IdentityMappingUtil.mapIdentity(userAccessControl, identityMappings); + // find a user where the identity is the userAccessControl org.apache.nifi.authorization.file.generated.User foundUser = null; for (org.apache.nifi.authorization.file.generated.User jaxbUser : authorizations.getUsers().getUser()) { - if (jaxbUser.getIdentity().equals(userAccessControl)) { + if (jaxbUser.getIdentity().equals(mappedUserAccessControl)) { foundUser = jaxbUser; break; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/test/java/org/apache/nifi/authorization/FileAuthorizerTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/test/java/org/apache/nifi/authorization/FileAuthorizerTest.java index e5d6a54859..0958b27e25 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/test/java/org/apache/nifi/authorization/FileAuthorizerTest.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/test/java/org/apache/nifi/authorization/FileAuthorizerTest.java @@ -28,6 +28,8 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import java.io.File; import java.io.FileOutputStream; @@ -36,6 +38,7 @@ import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Properties; import java.util.Set; import static org.junit.Assert.assertEquals; @@ -43,6 +46,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -112,6 +116,7 @@ public class FileAuthorizerTest { private File restore; private File flow; private File flowNoPorts; + private File flowWithDns; private AuthorizerConfigurationContext configurationContext; @@ -131,6 +136,9 @@ public class FileAuthorizerTest { flowNoPorts = new File("src/test/resources/flow-no-ports.xml.gz"); FileUtils.ensureDirectoryExistAndCanAccess(flowNoPorts.getParentFile()); + flowWithDns = new File("src/test/resources/flow-with-dns.xml.gz"); + FileUtils.ensureDirectoryExistAndCanAccess(flowWithDns.getParentFile()); + properties = mock(NiFiProperties.class); when(properties.getRestoreDirectory()).thenReturn(restore.getParentFile()); when(properties.getFlowConfigurationFile()).thenReturn(flow); @@ -332,6 +340,62 @@ public class FileAuthorizerTest { return resourceActionMap; } + @Test + public void testOnConfiguredWhenLegacyUsersFileProvidedWithIdentityMappings() throws Exception { + final Properties props = new Properties(); + props.setProperty("nifi.security.identity.mapping.pattern.dn1", "^CN=(.*?), OU=(.*?), O=(.*?), L=(.*?), ST=(.*?), C=(.*?)$"); + props.setProperty("nifi.security.identity.mapping.value.dn1", "$1"); + + properties = getNiFiProperties(props); + when(properties.getRestoreDirectory()).thenReturn(restore.getParentFile()); + when(properties.getFlowConfigurationFile()).thenReturn(flowWithDns); + authorizer.setNiFiProperties(properties); + + when(configurationContext.getProperty(Mockito.eq(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE))) + .thenReturn(new StandardPropertyValue("src/test/resources/authorized-users-with-dns.xml", null)); + + writeAuthorizationsFile(primary, EMPTY_AUTHORIZATIONS_CONCISE); + authorizer.onConfigured(configurationContext); + + final User user1 = authorizer.getUserByIdentity("user1"); + assertNotNull(user1); + + final User user2 = authorizer.getUserByIdentity("user2"); + assertNotNull(user2); + + final User user3 = authorizer.getUserByIdentity("user3"); + assertNotNull(user3); + + final User user4 = authorizer.getUserByIdentity("user4"); + assertNotNull(user4); + + final User user5 = authorizer.getUserByIdentity("user5"); + assertNotNull(user5); + + final User user6 = authorizer.getUserByIdentity("user6"); + assertNotNull(user6); + + // verify one group got created + final Set groups = authorizer.getGroups(); + assertEquals(1, groups.size()); + final Group group1 = groups.iterator().next(); + assertEquals("group1", group1.getName()); + + final Resource inputPortResource = ResourceFactory.getDataTransferResource(true, "2f7d1606-b090-4be7-a592-a5b70fb55531", "TCP Input"); + final AccessPolicy inputPortPolicy = authorizer.getUsersAndAccessPolicies().getAccessPolicy(inputPortResource.getIdentifier(), RequestAction.WRITE); + assertNotNull(inputPortPolicy); + assertEquals(1, inputPortPolicy.getUsers().size()); + assertTrue(inputPortPolicy.getUsers().contains(user6.getIdentifier())); + assertEquals(1, inputPortPolicy.getGroups().size()); + assertTrue(inputPortPolicy.getGroups().contains(group1.getIdentifier())); + + final Resource outputPortResource = ResourceFactory.getDataTransferResource(false, "2f7d1606-b090-4be7-a592-a5b70fb55532", "TCP Output"); + final AccessPolicy outputPortPolicy = authorizer.getUsersAndAccessPolicies().getAccessPolicy(outputPortResource.getIdentifier(), RequestAction.WRITE); + assertNotNull(outputPortPolicy); + assertEquals(1, outputPortPolicy.getUsers().size()); + assertTrue(outputPortPolicy.getUsers().contains(user4.getIdentifier())); + } + @Test(expected = AuthorizerCreationException.class) public void testOnConfiguredWhenBadLegacyUsersFileProvided() throws Exception { when(configurationContext.getProperty(Mockito.eq(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE))) @@ -473,6 +537,31 @@ public class FileAuthorizerTest { assertFalse(foundRootGroupPolicy); } + @Test + public void testOnConfiguredWhenInitialAdminProvidedWithIdentityMapping() throws Exception { + final Properties props = new Properties(); + props.setProperty("nifi.security.identity.mapping.pattern.dn1", "^CN=(.*?), OU=(.*?), O=(.*?), L=(.*?), ST=(.*?), C=(.*?)$"); + props.setProperty("nifi.security.identity.mapping.value.dn1", "$1_$2_$3"); + + properties = getNiFiProperties(props); + when(properties.getRestoreDirectory()).thenReturn(restore.getParentFile()); + when(properties.getFlowConfigurationFile()).thenReturn(flow); + authorizer.setNiFiProperties(properties); + + final String adminIdentity = "CN=localhost, OU=Apache NiFi, O=Apache, L=Santa Monica, ST=CA, C=US"; + when(configurationContext.getProperty(Mockito.eq(FileAuthorizer.PROP_INITIAL_ADMIN_IDENTITY))) + .thenReturn(new StandardPropertyValue(adminIdentity, null)); + + writeAuthorizationsFile(primary, EMPTY_AUTHORIZATIONS_CONCISE); + authorizer.onConfigured(configurationContext); + + final Set users = authorizer.getUsers(); + assertEquals(1, users.size()); + + final User adminUser = users.iterator().next(); + assertEquals("localhost_Apache NiFi_Apache", adminUser.getIdentity()); + } + @Test public void testOnConfiguredWhenNodeIdentitiesProvided() throws Exception { final String adminIdentity = "admin-user"; @@ -513,6 +602,43 @@ public class FileAuthorizerTest { assertTrue(proxyWritePolicy.getUsers().contains(nodeUser2.getIdentifier())); } + @Test + public void testOnConfiguredWhenNodeIdentitiesProvidedWithIdentityMappings() throws Exception { + final Properties props = new Properties(); + props.setProperty("nifi.security.identity.mapping.pattern.dn1", "^CN=(.*?), OU=(.*?), O=(.*?), L=(.*?), ST=(.*?), C=(.*?)$"); + props.setProperty("nifi.security.identity.mapping.value.dn1", "$1"); + + properties = getNiFiProperties(props); + when(properties.getRestoreDirectory()).thenReturn(restore.getParentFile()); + when(properties.getFlowConfigurationFile()).thenReturn(flow); + authorizer.setNiFiProperties(properties); + + final String adminIdentity = "CN=user1, OU=Apache NiFi, O=Apache, L=Santa Monica, ST=CA, C=US"; + when(configurationContext.getProperty(Mockito.eq(FileAuthorizer.PROP_INITIAL_ADMIN_IDENTITY))) + .thenReturn(new StandardPropertyValue(adminIdentity, null)); + + final String nodeIdentity1 = "CN=node1, OU=Apache NiFi, O=Apache, L=Santa Monica, ST=CA, C=US"; + final String nodeIdentity2 = "CN=node2, OU=Apache NiFi, O=Apache, L=Santa Monica, ST=CA, C=US"; + + final Map nodeProps = new HashMap<>(); + nodeProps.put("Node Identity 1", nodeIdentity1); + nodeProps.put("Node Identity 2", nodeIdentity2); + + when(configurationContext.getProperties()).thenReturn(nodeProps); + + writeAuthorizationsFile(primary, EMPTY_AUTHORIZATIONS_CONCISE); + authorizer.onConfigured(configurationContext); + + User adminUser = authorizer.getUserByIdentity("user1"); + assertNotNull(adminUser); + + User nodeUser1 = authorizer.getUserByIdentity("node1"); + assertNotNull(nodeUser1); + + User nodeUser2 = authorizer.getUserByIdentity("node2"); + assertNotNull(nodeUser2); + } + @Test public void testOnConfiguredWhenAuthorizationsFileDoesNotExist() { authorizer.onConfigured(configurationContext); @@ -1178,4 +1304,18 @@ public class FileAuthorizerTest { } return FileUtils.deleteFile(file, null, 10); } + + private NiFiProperties getNiFiProperties(final Properties properties) { + final NiFiProperties nifiProperties = Mockito.mock(NiFiProperties.class); + when(nifiProperties.stringPropertyNames()).thenReturn(properties.stringPropertyNames()); + + when(nifiProperties.getProperty(anyString())).then(new Answer() { + @Override + public String answer(InvocationOnMock invocationOnMock) throws Throwable { + return properties.getProperty((String)invocationOnMock.getArguments()[0]); + } + }); + return nifiProperties; + } + } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/test/resources/authorized-users-with-dns.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/test/resources/authorized-users-with-dns.xml new file mode 100644 index 0000000000..7c5d0d05c2 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/test/resources/authorized-users-with-dns.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/test/resources/flow-with-dns.xml.gz b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-file-authorizer/src/test/resources/flow-with-dns.xml.gz new file mode 100644 index 0000000000000000000000000000000000000000..e4b4ed64400864370999b141e7097a5b60edd310 GIT binary patch literal 784 zcmV+r1MmDFiwFp}c$QZH17>V*cP)2mbZ9MPZgVbpZEOJDRojl*Fc5v;udsL~H*y0h z$qE6xLfT~!g!Zv=62n>zc4Rx0?bmk_k}Q|qO0D|T7toyXjL*!Oaf18fK1T;4B;_pZ zS-ROngrz*AY!~)6@q;$(A*nFSaLySCdyMz)qq|_6^B)t=l;C+zL>Ho6WQy((3ikLI z(>)PWK@Wt*uY%xg!b_$i%M6Uqy-n;4V0G(t>(Eb}RjA-xJqlhHl?67C7IjbvP1J`euS)TjO4Ngh*mJ|7IU$U)$97`E-vD0yZ(^sN1x}|zoh179- zztnFCor2p`AQFs8S_%O-#8`fpkyT4~jdtYVzO2k)mX<_No<$~-1Z26vyP65ZIsJ*N zx*M0>#C+gtquwaVvj($Hr_Z7GSW?1xE6Se8f#lKG@oEtmmsoo;=TgQTb^*fxZgDQ5 z6&Ki5fh1ciXk4_8UzW?+v@5X1Z~L)U0vx-yD4svhR^O(p`Ip(^yfNK@Hdd5uT8g^- z1!i1Euy8k6*GIt%B2*XMPTZ70wFGqvOEfzc0tNvxT#cV`nWMJP2S1>!DAke+b-oCL zt)E$*>1l~MGBqb5z6Sd^fS<%Sw~6bzwspG*@nnhSRl47-fHib=+wnZh8x7!ZV0raE zc-~MSTE1!8=D>Fx$L-zh!r69Q>9X1^7W2j9r+WSIz?YJUaay&hc~7IsBCJ5Kk6t$6 zxZXfiy+dF(W*org.apache.nifi nifi-expression-language + + org.apache.nifi + nifi-properties + org.springframework.security spring-security-core diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/util/IdentityMapping.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/util/IdentityMapping.java new file mode 100644 index 0000000000..eb79c3a4df --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/util/IdentityMapping.java @@ -0,0 +1,48 @@ +/* + * 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.authorization.util; + +import java.util.regex.Pattern; + +/** + * Holder to pass around the key, pattern, and replacement from an identity mapping in NiFiProperties. + */ +public class IdentityMapping { + + private final String key; + private final Pattern pattern; + private final String replacementValue; + + public IdentityMapping(String key, Pattern pattern, String replacementValue) { + this.key = key; + this.pattern = pattern; + this.replacementValue = replacementValue; + } + + public String getKey() { + return key; + } + + public Pattern getPattern() { + return pattern; + } + + public String getReplacementValue() { + return replacementValue; + } + +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/util/IdentityMappingUtil.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/util/IdentityMappingUtil.java new file mode 100644 index 0000000000..2485db59f8 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-authorization/src/main/java/org/apache/nifi/authorization/util/IdentityMappingUtil.java @@ -0,0 +1,145 @@ +/* + * 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.authorization.util; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.util.NiFiProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class IdentityMappingUtil { + + private static final Logger LOGGER = LoggerFactory.getLogger(IdentityMappingUtil.class); + private static final Pattern backReferencePattern = Pattern.compile("\\$(\\d+)"); + + /** + * Builds the identity mappings from NiFiProperties. + * + * @param properties the NiFiProperties instance + * @return a list of identity mappings + */ + public static List getIdentityMappings(final NiFiProperties properties) { + final List mappings = new ArrayList<>(); + + // go through each property + for (String propertyName : properties.stringPropertyNames()) { + if (StringUtils.startsWith(propertyName, NiFiProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX)) { + final String key = StringUtils.substringAfter(propertyName, NiFiProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX); + final String identityPattern = properties.getProperty(propertyName); + + if (StringUtils.isBlank(identityPattern)) { + LOGGER.warn("Identity Mapping property {} was found, but was empty", new Object[]{propertyName}); + continue; + } + + final String identityValueProperty = NiFiProperties.SECURITY_IDENTITY_MAPPING_VALUE_PREFIX + key; + final String identityValue = properties.getProperty(identityValueProperty); + + if (StringUtils.isBlank(identityValue)) { + LOGGER.warn("Identity Mapping property {} was found, but corresponding value {} was not found", + new Object[]{propertyName, identityValueProperty}); + continue; + } + + final IdentityMapping identityMapping = new IdentityMapping(key, Pattern.compile(identityPattern), identityValue); + mappings.add(identityMapping); + + LOGGER.debug("Found Identity Mapping with key = {}, pattern = {}, value = {}", + new Object[] {key, identityPattern, identityValue}); + } + } + + // sort the list by the key so users can control the ordering in nifi.properties + Collections.sort(mappings, new Comparator() { + @Override + public int compare(IdentityMapping m1, IdentityMapping m2) { + return m1.getKey().compareTo(m2.getKey()); + } + }); + + return mappings; + } + + /** + * Checks the given identity against each provided mapping and performs the mapping using the first one that matches. + * If none match then the identity is returned as is. + * + * @param identity the identity to map + * @param mappings the mappings + * @return the mapped identity, or the same identity if no mappings matched + */ + public static String mapIdentity(final String identity, List mappings) { + for (IdentityMapping mapping : mappings) { + Matcher m = mapping.getPattern().matcher(identity); + if (m.matches()) { + final String pattern = mapping.getPattern().pattern(); + final String replacementValue = escapeLiteralBackReferences(mapping.getReplacementValue(), m.groupCount()); + return identity.replaceAll(pattern, replacementValue); + } + } + + return identity; + } + + // If we find a back reference that is not valid, then we will treat it as a literal string. For example, if we have 3 capturing + // groups and the Replacement Value has the value is "I owe $8 to him", then we want to treat the $8 as a literal "$8", rather + // than attempting to use it as a back reference. + private static String escapeLiteralBackReferences(final String unescaped, final int numCapturingGroups) { + if (numCapturingGroups == 0) { + return unescaped; + } + + String value = unescaped; + final Matcher backRefMatcher = backReferencePattern.matcher(value); + while (backRefMatcher.find()) { + final String backRefNum = backRefMatcher.group(1); + if (backRefNum.startsWith("0")) { + continue; + } + final int originalBackRefIndex = Integer.parseInt(backRefNum); + int backRefIndex = originalBackRefIndex; + + // if we have a replacement value like $123, and we have less than 123 capturing groups, then + // we want to truncate the 3 and use capturing group 12; if we have less than 12 capturing groups, + // then we want to truncate the 2 and use capturing group 1; if we don't have a capturing group then + // we want to truncate the 1 and get 0. + while (backRefIndex > numCapturingGroups && backRefIndex >= 10) { + backRefIndex /= 10; + } + + if (backRefIndex > numCapturingGroups) { + final StringBuilder sb = new StringBuilder(value.length() + 1); + final int groupStart = backRefMatcher.start(1); + + sb.append(value.substring(0, groupStart - 1)); + sb.append("\\"); + sb.append(value.substring(groupStart - 1)); + value = sb.toString(); + } + } + + return value; + } + +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/authorizers.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/authorizers.xml index 4971d627d5..cc1544d6c5 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/authorizers.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/authorizers.xml @@ -31,6 +31,9 @@ are no other users, groups, and policies defined. If this property is specified then a Legacy Authorized Users File can not be specified. + NOTE: Any identity mapping rules specified in nifi.properties will also be applied to the initial admin identity, + so the value should be the unmapped identity. + - Legacy Authorized Users File - The full path to an existing authorized-users.xml that will be automatically converted to the new authorizations model. If this property is specified then an Initial Admin Identity can not be specified, and this property will only be used when there are no other users, groups, and policies defined. @@ -39,6 +42,9 @@ should be defined, so that every node knows about every other node. If not clustered these properties can be ignored. The name of each property must be unique, for example for a three node cluster: "Node Identity A", "Node Identity B", "Node Identity C" or "Node Identity 1", "Node Identity 2", "Node Identity 3" + + NOTE: Any identity mapping rules specified in nifi.properties will also be applied to the node identities, + so the values should be the unmapped identities (i.e. full DN from a certificate). --> file-provider diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationProvider.java index f3f6bd0bef..62d0858585 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationProvider.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationProvider.java @@ -16,18 +16,15 @@ */ package org.apache.nifi.web.security; +import org.apache.nifi.authorization.util.IdentityMapping; +import org.apache.nifi.authorization.util.IdentityMappingUtil; import org.apache.nifi.util.NiFiProperties; -import org.apache.nifi.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.AuthenticationProvider; -import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * Base AuthenticationProvider that provides common functionality to mapping identities. @@ -35,7 +32,6 @@ import java.util.regex.Pattern; public abstract class NiFiAuthenticationProvider implements AuthenticationProvider { private static final Logger LOGGER = LoggerFactory.getLogger(NiFiAuthenticationProvider.class); - private static final Pattern backReferencePattern = Pattern.compile("\\$(\\d+)"); private NiFiProperties properties; private List mappings; @@ -45,55 +41,7 @@ public abstract class NiFiAuthenticationProvider implements AuthenticationProvid */ public NiFiAuthenticationProvider(final NiFiProperties properties) { this.properties = properties; - this.mappings = Collections.unmodifiableList(getIdentityMappings(properties)); - } - - /** - * Builds the identity mappings from NiFiProperties. - * - * @param properties the NiFiProperties instance - * @return a list of identity mappings - */ - private List getIdentityMappings(final NiFiProperties properties) { - final List mappings = new ArrayList<>(); - - // go through each property - for (String propertyName : properties.stringPropertyNames()) { - if (StringUtils.startsWith(propertyName, NiFiProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX)) { - final String key = StringUtils.substringAfter(propertyName, NiFiProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX); - final String identityPattern = properties.getProperty(propertyName); - - if (StringUtils.isBlank(identityPattern)) { - LOGGER.warn("Identity Mapping property {} was found, but was empty", new Object[]{propertyName}); - continue; - } - - final String identityValueProperty = NiFiProperties.SECURITY_IDENTITY_MAPPING_VALUE_PREFIX + key; - final String identityValue = properties.getProperty(identityValueProperty); - - if (StringUtils.isBlank(identityValue)) { - LOGGER.warn("Identity Mapping property {} was found, but corresponding value {} was not found", - new Object[]{propertyName, identityValueProperty}); - continue; - } - - final IdentityMapping identityMapping = new IdentityMapping(key, Pattern.compile(identityPattern), identityValue); - mappings.add(identityMapping); - - LOGGER.debug("Found Identity Mapping with key = {}, pattern = {}, value = {}", - new Object[] {key, identityPattern, identityValue}); - } - } - - // sort the list by the key so users can control the ordering in nifi.properties - Collections.sort(mappings, new Comparator() { - @Override - public int compare(IdentityMapping m1, IdentityMapping m2) { - return m1.getKey().compareTo(m2.getKey()); - } - }); - - return mappings; + this.mappings = Collections.unmodifiableList(IdentityMappingUtil.getIdentityMappings(properties)); } public List getMappings() { @@ -101,85 +49,7 @@ public abstract class NiFiAuthenticationProvider implements AuthenticationProvid } protected String mapIdentity(final String identity) { - for (IdentityMapping mapping : mappings) { - Matcher m = mapping.getPattern().matcher(identity); - if (m.matches()) { - final String pattern = mapping.getPattern().pattern(); - final String replacementValue = escapeLiteralBackReferences(mapping.getReplacementValue(), m.groupCount()); - return identity.replaceAll(pattern, replacementValue); - } - } - - return identity; - } - - // If we find a back reference that is not valid, then we will treat it as a literal string. For example, if we have 3 capturing - // groups and the Replacement Value has the value is "I owe $8 to him", then we want to treat the $8 as a literal "$8", rather - // than attempting to use it as a back reference. - private static String escapeLiteralBackReferences(final String unescaped, final int numCapturingGroups) { - if (numCapturingGroups == 0) { - return unescaped; - } - - String value = unescaped; - final Matcher backRefMatcher = backReferencePattern.matcher(value); - while (backRefMatcher.find()) { - final String backRefNum = backRefMatcher.group(1); - if (backRefNum.startsWith("0")) { - continue; - } - final int originalBackRefIndex = Integer.parseInt(backRefNum); - int backRefIndex = originalBackRefIndex; - - // if we have a replacement value like $123, and we have less than 123 capturing groups, then - // we want to truncate the 3 and use capturing group 12; if we have less than 12 capturing groups, - // then we want to truncate the 2 and use capturing group 1; if we don't have a capturing group then - // we want to truncate the 1 and get 0. - while (backRefIndex > numCapturingGroups && backRefIndex >= 10) { - backRefIndex /= 10; - } - - if (backRefIndex > numCapturingGroups) { - final StringBuilder sb = new StringBuilder(value.length() + 1); - final int groupStart = backRefMatcher.start(1); - - sb.append(value.substring(0, groupStart - 1)); - sb.append("\\"); - sb.append(value.substring(groupStart - 1)); - value = sb.toString(); - } - } - - return value; - } - - /** - * Holder to pass around the key, pattern, and replacement from an identity mapping in NiFiProperties. - */ - public static final class IdentityMapping { - - private final String key; - private final Pattern pattern; - private final String replacementValue; - - public IdentityMapping(String key, Pattern pattern, String replacementValue) { - this.key = key; - this.pattern = pattern; - this.replacementValue = replacementValue; - } - - public String getKey() { - return key; - } - - public Pattern getPattern() { - return pattern; - } - - public String getReplacementValue() { - return replacementValue; - } - + return IdentityMappingUtil.mapIdentity(identity, mappings); } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/NiFiAuthenticationProviderTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/NiFiAuthenticationProviderTest.java index addd3149cc..0e25747b3e 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/NiFiAuthenticationProviderTest.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/NiFiAuthenticationProviderTest.java @@ -16,6 +16,7 @@ */ package org.apache.nifi.web.security; +import org.apache.nifi.authorization.util.IdentityMapping; import org.apache.nifi.util.NiFiProperties; import org.junit.Test; import org.mockito.Mockito; @@ -45,7 +46,7 @@ public class NiFiAuthenticationProviderTest { final NiFiProperties nifiProperties = getNiFiProperties(properties); TestableNiFiAuthenticationProvider provider = new TestableNiFiAuthenticationProvider(nifiProperties); - List mappings = provider.getMappings(); + List mappings = provider.getMappings(); assertEquals(1, mappings.size()); assertEquals("dn", mappings.get(0).getKey()); assertEquals(pattern, mappings.get(0).getPattern().pattern()); @@ -58,7 +59,7 @@ public class NiFiAuthenticationProviderTest { final NiFiProperties nifiProperties = getNiFiProperties(properties); TestableNiFiAuthenticationProvider provider = new TestableNiFiAuthenticationProvider(nifiProperties); - List mappings = provider.getMappings(); + List mappings = provider.getMappings(); assertEquals(0, mappings.size()); final String identity = "john"; @@ -74,7 +75,7 @@ public class NiFiAuthenticationProviderTest { final NiFiProperties nifiProperties = getNiFiProperties(properties); TestableNiFiAuthenticationProvider provider = new TestableNiFiAuthenticationProvider(nifiProperties); - List mappings = provider.getMappings(); + List mappings = provider.getMappings(); assertEquals(0, mappings.size()); } @@ -86,7 +87,7 @@ public class NiFiAuthenticationProviderTest { final NiFiProperties nifiProperties = getNiFiProperties(properties); TestableNiFiAuthenticationProvider provider = new TestableNiFiAuthenticationProvider(nifiProperties); - List mappings = provider.getMappings(); + List mappings = provider.getMappings(); assertEquals(0, mappings.size()); } @@ -103,7 +104,7 @@ public class NiFiAuthenticationProviderTest { final NiFiProperties nifiProperties = getNiFiProperties(properties); TestableNiFiAuthenticationProvider provider = new TestableNiFiAuthenticationProvider(nifiProperties); - List mappings = provider.getMappings(); + List mappings = provider.getMappings(); assertEquals(3, mappings.size()); }