NIFI-2389 Refactoring identity mapping and applying it to FileAuthorizer for initial admin, cluster nodes, and legacy authorized users. This closes #719

This commit is contained in:
Bryan Bende 2016-07-25 16:15:49 -04:00 committed by Matt Gilman
parent 52bc23f5db
commit c3b4872b55
10 changed files with 402 additions and 144 deletions

View File

@ -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.file.generated.Users;
import org.apache.nifi.authorization.resource.ResourceFactory; import org.apache.nifi.authorization.resource.ResourceFactory;
import org.apache.nifi.authorization.resource.ResourceType; 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.components.PropertyValue;
import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.util.file.FileUtils; import org.apache.nifi.util.file.FileUtils;
@ -106,6 +108,7 @@ public class FileAuthorizer extends AbstractPolicyBasedAuthorizer {
private String legacyAuthorizedUsersFile; private String legacyAuthorizedUsersFile;
private Set<String> nodeIdentities; private Set<String> nodeIdentities;
private List<PortDTO> ports = new ArrayList<>(); private List<PortDTO> ports = new ArrayList<>();
private List<IdentityMapping> identityMappings;
private final AtomicReference<AuthorizationsHolder> authorizationsHolder = new AtomicReference<>(); private final AtomicReference<AuthorizationsHolder> 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 // get the value of the initial admin identity
final PropertyValue initialAdminIdentityProp = configurationContext.getProperty(PROP_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 // get the value of the legacy authorized users file
final PropertyValue legacyAuthorizedUsersProp = configurationContext.getProperty(PROP_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<String,String> entry : configurationContext.getProperties().entrySet()) { for (Map.Entry<String,String> entry : configurationContext.getProperties().entrySet()) {
Matcher matcher = NODE_IDENTITY_PATTERN.matcher(entry.getKey()); Matcher matcher = NODE_IDENTITY_PATTERN.matcher(entry.getKey());
if (matcher.matches() && !StringUtils.isBlank(entry.getValue())) { 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 // get all the user DNs into a list
List<String> userIdentities = new ArrayList<>(); List<String> userIdentities = new ArrayList<>();
for (org.apache.nifi.user.generated.User legacyUser : users.getUser()) { 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 // 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()) { for (org.apache.nifi.user.generated.User legacyUser : users.getUser()) {
// create the identifier of the new user based on the DN // 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(); final String userIdentifier = UUID.nameUUIDFromBytes(legacyUserDn.getBytes(StandardCharsets.UTF_8)).toString();
// create the new User and add it to the list of users // 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) { if (portDTO.getUserAccessControl() != null) {
for (String userAccessControl : portDTO.getUserAccessControl()) { 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 // find a user where the identity is the userAccessControl
org.apache.nifi.authorization.file.generated.User foundUser = null; org.apache.nifi.authorization.file.generated.User foundUser = null;
for (org.apache.nifi.authorization.file.generated.User jaxbUser : authorizations.getUsers().getUser()) { for (org.apache.nifi.authorization.file.generated.User jaxbUser : authorizations.getUsers().getUser()) {
if (jaxbUser.getIdentity().equals(userAccessControl)) { if (jaxbUser.getIdentity().equals(mappedUserAccessControl)) {
foundUser = jaxbUser; foundUser = jaxbUser;
break; break;
} }

View File

@ -28,6 +28,8 @@ import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -36,6 +38,7 @@ import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Properties;
import java.util.Set; import java.util.Set;
import static org.junit.Assert.assertEquals; 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.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -112,6 +116,7 @@ public class FileAuthorizerTest {
private File restore; private File restore;
private File flow; private File flow;
private File flowNoPorts; private File flowNoPorts;
private File flowWithDns;
private AuthorizerConfigurationContext configurationContext; private AuthorizerConfigurationContext configurationContext;
@ -131,6 +136,9 @@ public class FileAuthorizerTest {
flowNoPorts = new File("src/test/resources/flow-no-ports.xml.gz"); flowNoPorts = new File("src/test/resources/flow-no-ports.xml.gz");
FileUtils.ensureDirectoryExistAndCanAccess(flowNoPorts.getParentFile()); FileUtils.ensureDirectoryExistAndCanAccess(flowNoPorts.getParentFile());
flowWithDns = new File("src/test/resources/flow-with-dns.xml.gz");
FileUtils.ensureDirectoryExistAndCanAccess(flowWithDns.getParentFile());
properties = mock(NiFiProperties.class); properties = mock(NiFiProperties.class);
when(properties.getRestoreDirectory()).thenReturn(restore.getParentFile()); when(properties.getRestoreDirectory()).thenReturn(restore.getParentFile());
when(properties.getFlowConfigurationFile()).thenReturn(flow); when(properties.getFlowConfigurationFile()).thenReturn(flow);
@ -332,6 +340,62 @@ public class FileAuthorizerTest {
return resourceActionMap; 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<Group> 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) @Test(expected = AuthorizerCreationException.class)
public void testOnConfiguredWhenBadLegacyUsersFileProvided() throws Exception { public void testOnConfiguredWhenBadLegacyUsersFileProvided() throws Exception {
when(configurationContext.getProperty(Mockito.eq(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE))) when(configurationContext.getProperty(Mockito.eq(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE)))
@ -473,6 +537,31 @@ public class FileAuthorizerTest {
assertFalse(foundRootGroupPolicy); 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<User> users = authorizer.getUsers();
assertEquals(1, users.size());
final User adminUser = users.iterator().next();
assertEquals("localhost_Apache NiFi_Apache", adminUser.getIdentity());
}
@Test @Test
public void testOnConfiguredWhenNodeIdentitiesProvided() throws Exception { public void testOnConfiguredWhenNodeIdentitiesProvided() throws Exception {
final String adminIdentity = "admin-user"; final String adminIdentity = "admin-user";
@ -513,6 +602,43 @@ public class FileAuthorizerTest {
assertTrue(proxyWritePolicy.getUsers().contains(nodeUser2.getIdentifier())); 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<String,String> 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 @Test
public void testOnConfiguredWhenAuthorizationsFileDoesNotExist() { public void testOnConfiguredWhenAuthorizationsFileDoesNotExist() {
authorizer.onConfigured(configurationContext); authorizer.onConfigured(configurationContext);
@ -1178,4 +1304,18 @@ public class FileAuthorizerTest {
} }
return FileUtils.deleteFile(file, null, 10); 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<String>() {
@Override
public String answer(InvocationOnMock invocationOnMock) throws Throwable {
return properties.getProperty((String)invocationOnMock.getArguments()[0]);
}
});
return nifiProperties;
}
} }

View File

@ -0,0 +1,35 @@
<?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.
-->
<users>
<user dn="CN=user1, OU=Apache NiFi, O=Apache, L=Santa Monica, ST=CA, C=US" group="group1">
<role name="ROLE_MONITOR"/>
</user>
<user dn="CN=user2, OU=Apache NiFi, O=Apache, L=Santa Monica, ST=CA, C=US">
<role name="ROLE_PROVENANCE"/>
</user>
<user dn="CN=user3, OU=Apache NiFi, O=Apache, L=Santa Monica, ST=CA, C=US">
<role name="ROLE_DFM"/>
</user>
<user dn="CN=user4, OU=Apache NiFi, O=Apache, L=Santa Monica, ST=CA, C=US">
<role name="ROLE_ADMIN"/>
</user>
<user dn="CN=user5, OU=Apache NiFi, O=Apache, L=Santa Monica, ST=CA, C=US">
<role name="ROLE_PROXY"/>
</user>
<user dn="CN=user6, OU=Apache NiFi, O=Apache, L=Santa Monica, ST=CA, C=US">
<role name="ROLE_NIFI"/>
</user>
</users>

View File

@ -35,6 +35,10 @@
<groupId>org.apache.nifi</groupId> <groupId>org.apache.nifi</groupId>
<artifactId>nifi-expression-language</artifactId> <artifactId>nifi-expression-language</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-properties</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId> <artifactId>spring-security-core</artifactId>

View File

@ -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;
}
}

View File

@ -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<IdentityMapping> getIdentityMappings(final NiFiProperties properties) {
final List<IdentityMapping> 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<IdentityMapping>() {
@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<IdentityMapping> 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;
}
}

View File

@ -31,6 +31,9 @@
are no other users, groups, and policies defined. If this property is specified then a Legacy Authorized are no other users, groups, and policies defined. If this property is specified then a Legacy Authorized
Users File can not be specified. 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 - 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 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. 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. 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: 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" "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).
--> -->
<authorizer> <authorizer>
<identifier>file-provider</identifier> <identifier>file-provider</identifier>

View File

@ -16,18 +16,15 @@
*/ */
package org.apache.nifi.web.security; 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.NiFiProperties;
import org.apache.nifi.util.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* Base AuthenticationProvider that provides common functionality to mapping identities. * Base AuthenticationProvider that provides common functionality to mapping identities.
@ -35,7 +32,6 @@ import java.util.regex.Pattern;
public abstract class NiFiAuthenticationProvider implements AuthenticationProvider { public abstract class NiFiAuthenticationProvider implements AuthenticationProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(NiFiAuthenticationProvider.class); private static final Logger LOGGER = LoggerFactory.getLogger(NiFiAuthenticationProvider.class);
private static final Pattern backReferencePattern = Pattern.compile("\\$(\\d+)");
private NiFiProperties properties; private NiFiProperties properties;
private List<IdentityMapping> mappings; private List<IdentityMapping> mappings;
@ -45,55 +41,7 @@ public abstract class NiFiAuthenticationProvider implements AuthenticationProvid
*/ */
public NiFiAuthenticationProvider(final NiFiProperties properties) { public NiFiAuthenticationProvider(final NiFiProperties properties) {
this.properties = properties; this.properties = properties;
this.mappings = Collections.unmodifiableList(getIdentityMappings(properties)); this.mappings = Collections.unmodifiableList(IdentityMappingUtil.getIdentityMappings(properties));
}
/**
* Builds the identity mappings from NiFiProperties.
*
* @param properties the NiFiProperties instance
* @return a list of identity mappings
*/
private List<IdentityMapping> getIdentityMappings(final NiFiProperties properties) {
final List<IdentityMapping> 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<IdentityMapping>() {
@Override
public int compare(IdentityMapping m1, IdentityMapping m2) {
return m1.getKey().compareTo(m2.getKey());
}
});
return mappings;
} }
public List<IdentityMapping> getMappings() { public List<IdentityMapping> getMappings() {
@ -101,85 +49,7 @@ public abstract class NiFiAuthenticationProvider implements AuthenticationProvid
} }
protected String mapIdentity(final String identity) { protected String mapIdentity(final String identity) {
for (IdentityMapping mapping : mappings) { return IdentityMappingUtil.mapIdentity(identity, 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;
}
} }
} }

View File

@ -16,6 +16,7 @@
*/ */
package org.apache.nifi.web.security; package org.apache.nifi.web.security;
import org.apache.nifi.authorization.util.IdentityMapping;
import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.util.NiFiProperties;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
@ -45,7 +46,7 @@ public class NiFiAuthenticationProviderTest {
final NiFiProperties nifiProperties = getNiFiProperties(properties); final NiFiProperties nifiProperties = getNiFiProperties(properties);
TestableNiFiAuthenticationProvider provider = new TestableNiFiAuthenticationProvider(nifiProperties); TestableNiFiAuthenticationProvider provider = new TestableNiFiAuthenticationProvider(nifiProperties);
List<NiFiAuthenticationProvider.IdentityMapping> mappings = provider.getMappings(); List<IdentityMapping> mappings = provider.getMappings();
assertEquals(1, mappings.size()); assertEquals(1, mappings.size());
assertEquals("dn", mappings.get(0).getKey()); assertEquals("dn", mappings.get(0).getKey());
assertEquals(pattern, mappings.get(0).getPattern().pattern()); assertEquals(pattern, mappings.get(0).getPattern().pattern());
@ -58,7 +59,7 @@ public class NiFiAuthenticationProviderTest {
final NiFiProperties nifiProperties = getNiFiProperties(properties); final NiFiProperties nifiProperties = getNiFiProperties(properties);
TestableNiFiAuthenticationProvider provider = new TestableNiFiAuthenticationProvider(nifiProperties); TestableNiFiAuthenticationProvider provider = new TestableNiFiAuthenticationProvider(nifiProperties);
List<NiFiAuthenticationProvider.IdentityMapping> mappings = provider.getMappings(); List<IdentityMapping> mappings = provider.getMappings();
assertEquals(0, mappings.size()); assertEquals(0, mappings.size());
final String identity = "john"; final String identity = "john";
@ -74,7 +75,7 @@ public class NiFiAuthenticationProviderTest {
final NiFiProperties nifiProperties = getNiFiProperties(properties); final NiFiProperties nifiProperties = getNiFiProperties(properties);
TestableNiFiAuthenticationProvider provider = new TestableNiFiAuthenticationProvider(nifiProperties); TestableNiFiAuthenticationProvider provider = new TestableNiFiAuthenticationProvider(nifiProperties);
List<NiFiAuthenticationProvider.IdentityMapping> mappings = provider.getMappings(); List<IdentityMapping> mappings = provider.getMappings();
assertEquals(0, mappings.size()); assertEquals(0, mappings.size());
} }
@ -86,7 +87,7 @@ public class NiFiAuthenticationProviderTest {
final NiFiProperties nifiProperties = getNiFiProperties(properties); final NiFiProperties nifiProperties = getNiFiProperties(properties);
TestableNiFiAuthenticationProvider provider = new TestableNiFiAuthenticationProvider(nifiProperties); TestableNiFiAuthenticationProvider provider = new TestableNiFiAuthenticationProvider(nifiProperties);
List<NiFiAuthenticationProvider.IdentityMapping> mappings = provider.getMappings(); List<IdentityMapping> mappings = provider.getMappings();
assertEquals(0, mappings.size()); assertEquals(0, mappings.size());
} }
@ -103,7 +104,7 @@ public class NiFiAuthenticationProviderTest {
final NiFiProperties nifiProperties = getNiFiProperties(properties); final NiFiProperties nifiProperties = getNiFiProperties(properties);
TestableNiFiAuthenticationProvider provider = new TestableNiFiAuthenticationProvider(nifiProperties); TestableNiFiAuthenticationProvider provider = new TestableNiFiAuthenticationProvider(nifiProperties);
List<NiFiAuthenticationProvider.IdentityMapping> mappings = provider.getMappings(); List<IdentityMapping> mappings = provider.getMappings();
assertEquals(3, mappings.size()); assertEquals(3, mappings.size());
} }