http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java ---------------------------------------------------------------------- diff --git a/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java b/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java new file mode 100644 index 0000000..fcd827b --- /dev/null +++ b/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/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.registry.properties.util; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +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 NiFiRegistryProperties. + * + * @param properties the NiFiRegistryProperties instance + * @return a list of identity mappings + */ + public static List<IdentityMapping> getIdentityMappings(final NiFiRegistryProperties properties) { + final List<IdentityMapping> mappings = new ArrayList<>(); + + // go through each property + for (String propertyName : properties.getPropertyKeys()) { + if (StringUtils.startsWith(propertyName, NiFiRegistryProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX)) { + final String key = StringUtils.substringAfter(propertyName, NiFiRegistryProperties.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 = NiFiRegistryProperties.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; + } + +}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-provider-api/pom.xml ---------------------------------------------------------------------- diff --git a/nifi-registry-provider-api/pom.xml b/nifi-registry-provider-api/pom.xml index 294eac4..ecf77bd 100644 --- a/nifi-registry-provider-api/pom.xml +++ b/nifi-registry-provider-api/pom.xml @@ -25,11 +25,5 @@ <packaging>jar</packaging> <dependencies> - <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <version>4.12</version> - <scope>test</scope> - </dependency> </dependencies> </project> http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-resources/src/main/resources/conf/authorized-users.xml ---------------------------------------------------------------------- diff --git a/nifi-registry-resources/src/main/resources/conf/authorized-users.xml b/nifi-registry-resources/src/main/resources/conf/authorized-users.xml deleted file mode 100644 index 7519756..0000000 --- a/nifi-registry-resources/src/main/resources/conf/authorized-users.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?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="[user dn]"></user> - --> -</users> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-resources/src/main/resources/conf/authorizers.xml ---------------------------------------------------------------------- diff --git a/nifi-registry-resources/src/main/resources/conf/authorizers.xml b/nifi-registry-resources/src/main/resources/conf/authorizers.xml new file mode 100644 index 0000000..e4696bb --- /dev/null +++ b/nifi-registry-resources/src/main/resources/conf/authorizers.xml @@ -0,0 +1,141 @@ +<?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.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.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.authorization.file.FileUserGroupProvider</class> + <property name="Users File">./conf/users.xml</property> + <property name="Initial User Identity 1"><!--CN=abc, OU=xyz--></property> + </userGroupProvider> + + <!-- + The CompositeUserGroupProvider will provide support for retrieving users and groups from multiple sources. + + - User Group Provider [unique key] - The identifier of user group providers to load from. The name of + each property must be unique, for example: "User Group Provider A", "User Group Provider B", + "User Group Provider C" or "User Group Provider 1", "User Group Provider 2", "User Group Provider 3" + + NOTE: Any identity mapping rules specified in nifi.properties are not applied in this implementation. This behavior + would need to be applied by the base implementation. + --> + <!-- To enable the composite-user-group-provider remove 2 lines. This is 1 of 2. + <userGroupProvider> + <identifier>composite-user-group-provider</identifier> + <class>org.apache.nifi.authorization.CompositeUserGroupProvider</class> + <property name="User Group Provider 1"></property> + </userGroupProvider> + To enable the composite-user-group-provider remove 2 lines. This is 2 of 2. --> + + <!-- + The CompositeConfigurableUserGroupProvider will provide support for retrieving users and groups from multiple sources. + Additionally, a single configurable user group provider is required. Users from the configurable user group provider + are configurable, however users loaded from one of the User Group Provider [unique key] will not be. + + - Configurable User Group Provider - A configurable user group provider. + + - User Group Provider [unique key] - The identifier of user group providers to load from. The name of + each property must be unique, for example: "User Group Provider A", "User Group Provider B", + "User Group Provider C" or "User Group Provider 1", "User Group Provider 2", "User Group Provider 3" + + NOTE: Any identity mapping rules specified in nifi.properties are not applied in this implementation. This behavior + would need to be applied by the base implementation. + --> + <!-- To enable the composite-configurable-user-group-provider remove 2 lines. This is 1 of 2. + <userGroupProvider> + <identifier>composite-configurable-user-group-provider</identifier> + <class>org.apache.nifi.authorization.CompositeConfigurableUserGroupProvider</class> + <property name="Configurable User Group Provider">file-user-group-provider</property> + <property name="User Group Provider 1"></property> + </userGroupProvider> + To enable the composite-configurable-user-group-provider remove 2 lines. This is 2 of 2. --> + + <!-- + 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.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. + + - Node Identity [unique key] - The identity of a NiFi cluster node. When clustered, a property for each node + 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). This identity must be found + in the configured User Group Provider. + --> + <accessPolicyProvider> + <identifier>file-access-policy-provider</identifier> + <class>org.apache.nifi.registry.authorization.file.FileAccessPolicyProvider</class> + <property name="User Group Provider">file-user-group-provider</property> + <property name="Authorizations File">./conf/authorizations.xml</property> + <property name="Initial Admin Identity"><!-- CN=abc, OU=xyz --></property> + + <!--<property name="Node Identity 1"></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.authorization.StandardManagedAuthorizer</class> + <property name="Access Policy Provider">file-access-policy-provider</property> + </authorizer> + +</authorizers> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties ---------------------------------------------------------------------- diff --git a/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties b/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties index db62768..3b3b2ab 100644 --- a/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties +++ b/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties @@ -31,7 +31,8 @@ nifi.registry.security.truststore=${nifi.registry.security.truststore} nifi.registry.security.truststoreType=${nifi.registry.security.truststoreType} nifi.registry.security.truststorePasswd=${nifi.registry.security.truststorePasswd} nifi.registry.security.needClientAuth=${nifi.registry.security.needClientAuth} -nifi.registry.security.authorized.users=${nifi.registry.security.authorized.users} +nifi.registry.security.authorizers.configuration.file=${nifi.registry.security.authorizers.configuration.file} +nifi.registry.security.authorizer=${nifi.registry.security.authorizer} # providers properties # nifi.registry.providers.configuration.file=${nifi.registry.providers.configuration.file} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-security-api-impl/pom.xml ---------------------------------------------------------------------- diff --git a/nifi-registry-security-api-impl/pom.xml b/nifi-registry-security-api-impl/pom.xml new file mode 100644 index 0000000..4f2e29e --- /dev/null +++ b/nifi-registry-security-api-impl/pom.xml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ 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. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <artifactId>nifi-registry</artifactId> + <groupId>org.apache.nifi.registry</groupId> + <version>0.0.1-SNAPSHOT</version> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>nifi-registry-security-api-impl</artifactId> + <packaging>jar</packaging> + + <build> + <resources> + <resource> + <directory>src/main/xsd</directory> + </resource> + </resources> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>jaxb2-maven-plugin</artifactId> + <executions> + <execution> + <id>authorizations</id> + <goals> + <goal>xjc</goal> + </goals> + <configuration> + <sources> + <source>src/main/xsd/authorizations.xsd</source> + </sources> + <packageName>org.apache.nifi.registry.authorization.file.generated</packageName> + </configuration> + </execution> + <execution> + <id>tenants</id> + <goals> + <goal>xjc</goal> + </goals> + <configuration> + <sources> + <source>src/main/xsd/tenants.xsd</source> + </sources> + <packageName>org.apache.nifi.registry.authorization.file.tenants.generated</packageName> + <clearOutputDir>false</clearOutputDir> + </configuration> + </execution> + </executions> + <configuration> + <outputDirectory>${project.build.directory}/generated-sources/jaxb</outputDirectory> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-checkstyle-plugin</artifactId> + <configuration> + <excludes>**/authorization/file/generated/*.java,**/authorization/file/tenants/generated/*.java</excludes> + </configuration> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>org.apache.nifi.registry</groupId> + <artifactId>nifi-registry-properties</artifactId> + <version>0.0.1-SNAPSHOT</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.nifi.registry</groupId> + <artifactId>nifi-registry-security-api</artifactId> + <version>0.0.1-SNAPSHOT</version> + <scope>provided</scope> + </dependency> + </dependencies> + +</project> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/AbstractPolicyBasedAuthorizer.java ---------------------------------------------------------------------- diff --git a/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/AbstractPolicyBasedAuthorizer.java b/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/AbstractPolicyBasedAuthorizer.java new file mode 100644 index 0000000..5313911 --- /dev/null +++ b/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/AbstractPolicyBasedAuthorizer.java @@ -0,0 +1,822 @@ +/* + * 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.authorization; + +import org.apache.nifi.registry.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.authorization.exception.AuthorizerCreationException; +import org.apache.nifi.registry.authorization.exception.AuthorizerDestructionException; +import org.apache.nifi.registry.authorization.exception.UninheritableAuthorizationsException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +/** + * An Authorizer that provides management of users, groups, and policies. + */ +public abstract class AbstractPolicyBasedAuthorizer implements ManagedAuthorizer { + + static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); + static final XMLOutputFactory XML_OUTPUT_FACTORY = XMLOutputFactory.newInstance(); + + static final String USER_ELEMENT = "user"; + static final String GROUP_USER_ELEMENT = "groupUser"; + static final String GROUP_ELEMENT = "group"; + static final String POLICY_ELEMENT = "policy"; + static final String POLICY_USER_ELEMENT = "policyUser"; + static final String POLICY_GROUP_ELEMENT = "policyGroup"; + static final String IDENTIFIER_ATTR = "identifier"; + static final String IDENTITY_ATTR = "identity"; + static final String NAME_ATTR = "name"; + static final String RESOURCE_ATTR = "resource"; + static final String ACTIONS_ATTR = "actions"; + + @Override + public final void onConfigured(final AuthorizerConfigurationContext configurationContext) throws AuthorizerCreationException { + doOnConfigured(configurationContext); + } + + /** + * Allows sub-classes to take action when onConfigured is called. + * + * @param configurationContext the configuration context + * @throws AuthorizerCreationException if an error occurs during onConfigured process + */ + protected abstract void doOnConfigured(final AuthorizerConfigurationContext configurationContext) throws AuthorizerCreationException; + + @Override + public final AuthorizationResult authorize(AuthorizationRequest request) throws AuthorizationAccessException { + final UsersAndAccessPolicies usersAndAccessPolicies = getUsersAndAccessPolicies(); + final String resourceIdentifier = request.getResource().getIdentifier(); + + final AccessPolicy policy = usersAndAccessPolicies.getAccessPolicy(resourceIdentifier, request.getAction()); + if (policy == null) { + return AuthorizationResult.resourceNotFound(); + } + + final User user = usersAndAccessPolicies.getUser(request.getIdentity()); + if (user == null) { + return AuthorizationResult.denied(String.format("Unknown user with identity '%s'.", request.getIdentity())); + } + + final Set<Group> userGroups = usersAndAccessPolicies.getGroups(user.getIdentity()); + if (policy.getUsers().contains(user.getIdentifier()) || containsGroup(userGroups, policy)) { + return AuthorizationResult.approved(); + } + + return AuthorizationResult.denied(request.getExplanationSupplier().get()); + } + + /** + * Determines if the policy contains one of the user's groups. + * + * @param userGroups the set of the user's groups + * @param policy the policy + * @return true if one of the Groups in userGroups is contained in the policy + */ + private boolean containsGroup(final Set<Group> userGroups, final AccessPolicy policy) { + if (userGroups.isEmpty() || policy.getGroups().isEmpty()) { + return false; + } + + for (Group userGroup : userGroups) { + if (policy.getGroups().contains(userGroup.getIdentifier())) { + return true; + } + } + + return false; + } + + /** + * Adds a new group. + * + * @param group the Group to add + * @return the added Group + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws IllegalStateException if a group with the same name already exists + */ + public final synchronized Group addGroup(Group group) throws AuthorizationAccessException { + return doAddGroup(group); + } + + /** + * Adds a new group. + * + * @param group the Group to add + * @return the added Group + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract Group doAddGroup(Group group) throws AuthorizationAccessException; + + /** + * Retrieves a Group by id. + * + * @param identifier the identifier of the Group to retrieve + * @return the Group with the given identifier, or null if no matching group was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract Group getGroup(String identifier) throws AuthorizationAccessException; + + /** + * The group represented by the provided instance will be updated based on the provided instance. + * + * @param group an updated group instance + * @return the updated group instance, or null if no matching group was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws IllegalStateException if there is already a group with the same name + */ + public final synchronized Group updateGroup(Group group) throws AuthorizationAccessException { + return doUpdateGroup(group); + } + + /** + * The group represented by the provided instance will be updated based on the provided instance. + * + * @param group an updated group instance + * @return the updated group instance, or null if no matching group was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract Group doUpdateGroup(Group group) throws AuthorizationAccessException; + + /** + * Deletes the given group. + * + * @param group the group to delete + * @return the deleted group, or null if no matching group was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract Group deleteGroup(Group group) throws AuthorizationAccessException; + + /** + * Deletes the group with the given identifier. + * + * @param groupIdentifier the id of the group to delete + * @return the deleted group, or null if no matching group was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract Group deleteGroup(String groupIdentifier) throws AuthorizationAccessException; + + /** + * Retrieves all groups. + * + * @return a list of groups + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract Set<Group> getGroups() throws AuthorizationAccessException; + + + /** + * Adds the given user. + * + * @param user the user to add + * @return the user that was added + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws IllegalStateException if there is already a user with the same identity + */ + public final synchronized User addUser(User user) throws AuthorizationAccessException { + return doAddUser(user); + } + + /** + * Adds the given user. + * + * @param user the user to add + * @return the user that was added + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract User doAddUser(User user) throws AuthorizationAccessException; + + /** + * Retrieves the user with the given identifier. + * + * @param identifier the id of the user to retrieve + * @return the user with the given id, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract User getUser(String identifier) throws AuthorizationAccessException; + + /** + * Retrieves the user with the given identity. + * + * @param identity the identity of the user to retrieve + * @return the user with the given identity, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract User getUserByIdentity(String identity) throws AuthorizationAccessException; + + /** + * The user represented by the provided instance will be updated based on the provided instance. + * + * @param user an updated user instance + * @return the updated user instance, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws IllegalStateException if there is already a user with the same identity + */ + public final synchronized User updateUser(final User user) throws AuthorizationAccessException { + return doUpdateUser(user); + } + + /** + * The user represented by the provided instance will be updated based on the provided instance. + * + * @param user an updated user instance + * @return the updated user instance, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract User doUpdateUser(User user) throws AuthorizationAccessException; + + /** + * Deletes the given user. + * + * @param user the user to delete + * @return the user that was deleted, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract User deleteUser(User user) throws AuthorizationAccessException; + + /** + * Deletes the user with the given id. + * + * @param userIdentifier the identifier of the user to delete + * @return the user that was deleted, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract User deleteUser(String userIdentifier) throws AuthorizationAccessException; + + /** + * Retrieves all users. + * + * @return a list of users + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract Set<User> getUsers() throws AuthorizationAccessException; + + /** + * Adds the given policy ensuring that multiple policies can not be added for the same resource and action. + * + * @param accessPolicy the policy to add + * @return the policy that was added + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public final synchronized AccessPolicy addAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + return doAddAccessPolicy(accessPolicy); + } + + /** + * Adds the given policy. + * + * @param accessPolicy the policy to add + * @return the policy that was added + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + protected abstract AccessPolicy doAddAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException; + + /** + * Retrieves the policy with the given identifier. + * + * @param identifier the id of the policy to retrieve + * @return the policy with the given id, or null if no matching policy exists + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract AccessPolicy getAccessPolicy(String identifier) throws AuthorizationAccessException; + + /** + * The policy represented by the provided instance will be updated based on the provided instance. + * + * @param accessPolicy an updated policy + * @return the updated policy, or null if no matching policy was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException; + + /** + * Deletes the given policy. + * + * @param policy the policy to delete + * @return the deleted policy, or null if no matching policy was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract AccessPolicy deleteAccessPolicy(AccessPolicy policy) throws AuthorizationAccessException; + + /** + * Deletes the policy with the given id. + * + * @param policyIdentifier the id of the policy to delete + * @return the deleted policy, or null if no matching policy was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract AccessPolicy deleteAccessPolicy(String policyIdentifier) throws AuthorizationAccessException; + + /** + * Retrieves all access policies. + * + * @return a list of policies + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract Set<AccessPolicy> getAccessPolicies() throws AuthorizationAccessException; + + /** + * Returns the UserAccessPolicies instance. + * + * @return the UserAccessPolicies instance + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract UsersAndAccessPolicies getUsersAndAccessPolicies() throws AuthorizationAccessException; + + /** + * Returns whether the proposed fingerprint is inheritable. + * + * @param proposedFingerprint the proposed fingerprint + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws UninheritableAuthorizationsException if the proposed fingerprint was uninheritable + */ + @Override + public final void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { + try { + // ensure we understand the proposed fingerprint + parsePoliciesUsersAndGroups(proposedFingerprint); + } catch (final AuthorizationAccessException e) { + throw new UninheritableAuthorizationsException("Unable to parse proposed fingerprint: " + e); + } + + final List<User> users = getSortedUsers(); + final List<Group> groups = getSortedGroups(); + final List<AccessPolicy> accessPolicies = getSortedAccessPolicies(); + + // ensure we're in a state to inherit + if (!users.isEmpty() || !groups.isEmpty() || !accessPolicies.isEmpty()) { + throw new UninheritableAuthorizationsException("Proposed fingerprint is not inheritable because the current Authorizations is not empty.."); + } + } + + /** + * Parses the fingerprint and adds any users, groups, and policies to the current Authorizer. + * + * @param fingerprint the fingerprint that was obtained from calling getFingerprint() on another Authorizer. + */ + @Override + public final void inheritFingerprint(final String fingerprint) throws AuthorizationAccessException { + if (fingerprint == null || fingerprint.trim().isEmpty()) { + return; + } + + final PoliciesUsersAndGroups policiesUsersAndGroups = parsePoliciesUsersAndGroups(fingerprint); + policiesUsersAndGroups.getUsers().forEach(user -> addUser(user)); + policiesUsersAndGroups.getGroups().forEach(group -> addGroup(group)); + policiesUsersAndGroups.getAccessPolicies().forEach(policy -> addAccessPolicy(policy)); + } + + private PoliciesUsersAndGroups parsePoliciesUsersAndGroups(final String fingerprint) { + final List<AccessPolicy> accessPolicies = new ArrayList<>(); + final List<User> users = new ArrayList<>(); + final List<Group> groups = new ArrayList<>(); + + final byte[] fingerprintBytes = fingerprint.getBytes(StandardCharsets.UTF_8); + try (final ByteArrayInputStream in = new ByteArrayInputStream(fingerprintBytes)) { + final DocumentBuilder docBuilder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder(); + final Document document = docBuilder.parse(in); + final Element rootElement = document.getDocumentElement(); + + // parse all the users and add them to the current authorizer + NodeList userNodes = rootElement.getElementsByTagName(USER_ELEMENT); + for (int i=0; i < userNodes.getLength(); i++) { + Node userNode = userNodes.item(i); + users.add(parseUser((Element) userNode)); + } + + // parse all the groups and add them to the current authorizer + NodeList groupNodes = rootElement.getElementsByTagName(GROUP_ELEMENT); + for (int i=0; i < groupNodes.getLength(); i++) { + Node groupNode = groupNodes.item(i); + groups.add(parseGroup((Element) groupNode)); + } + + // parse all the policies and add them to the current authorizer + NodeList policyNodes = rootElement.getElementsByTagName(POLICY_ELEMENT); + for (int i=0; i < policyNodes.getLength(); i++) { + Node policyNode = policyNodes.item(i); + accessPolicies.add(parsePolicy((Element) policyNode)); + } + } catch (SAXException | ParserConfigurationException | IOException e) { + throw new AuthorizationAccessException("Unable to parse fingerprint", e); + } + + return new PoliciesUsersAndGroups(accessPolicies, users, groups); + } + + private User parseUser(final Element element) { + final User.Builder builder = new User.Builder() + .identifier(element.getAttribute(IDENTIFIER_ATTR)) + .identity(element.getAttribute(IDENTITY_ATTR)); + + return builder.build(); + } + + private Group parseGroup(final Element element) { + final Group.Builder builder = new Group.Builder() + .identifier(element.getAttribute(IDENTIFIER_ATTR)) + .name(element.getAttribute(NAME_ATTR)); + + NodeList groupUsers = element.getElementsByTagName(GROUP_USER_ELEMENT); + for (int i=0; i < groupUsers.getLength(); i++) { + Element groupUserNode = (Element) groupUsers.item(i); + builder.addUser(groupUserNode.getAttribute(IDENTIFIER_ATTR)); + } + + return builder.build(); + } + + private AccessPolicy parsePolicy(final Element element) { + final AccessPolicy.Builder builder = new AccessPolicy.Builder() + .identifier(element.getAttribute(IDENTIFIER_ATTR)) + .resource(element.getAttribute(RESOURCE_ATTR)); + + final String actions = element.getAttribute(ACTIONS_ATTR); + if (actions.equals(RequestAction.READ.name())) { + builder.action(RequestAction.READ); + } else if (actions.equals(RequestAction.WRITE.name())) { + builder.action(RequestAction.WRITE); + } else { + throw new IllegalStateException("Unknown Policy Action: " + actions); + } + + NodeList policyUsers = element.getElementsByTagName(POLICY_USER_ELEMENT); + for (int i=0; i < policyUsers.getLength(); i++) { + Element policyUserNode = (Element) policyUsers.item(i); + builder.addUser(policyUserNode.getAttribute(IDENTIFIER_ATTR)); + } + + NodeList policyGroups = element.getElementsByTagName(POLICY_GROUP_ELEMENT); + for (int i=0; i < policyGroups.getLength(); i++) { + Element policyGroupNode = (Element) policyGroups.item(i); + builder.addGroup(policyGroupNode.getAttribute(IDENTIFIER_ATTR)); + } + + return builder.build(); + } + + @Override + public final AccessPolicyProvider getAccessPolicyProvider() { + return new ConfigurableAccessPolicyProvider() { + @Override + public Set<AccessPolicy> getAccessPolicies() throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.getAccessPolicies(); + } + + @Override + public AccessPolicy getAccessPolicy(String identifier) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.getAccessPolicy(identifier); + } + + @Override + public AccessPolicy addAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.addAccessPolicy(accessPolicy); + } + + @Override + public AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.updateAccessPolicy(accessPolicy); + } + + @Override + public AccessPolicy deleteAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.deleteAccessPolicy(accessPolicy); + } + + @Override + public AccessPolicy deleteAccessPolicy(String accessPolicyIdentifier) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.deleteAccessPolicy(accessPolicyIdentifier); + } + + @Override + public AccessPolicy getAccessPolicy(String resourceIdentifier, RequestAction action) throws AuthorizationAccessException { + final UsersAndAccessPolicies usersAndAccessPolicies = AbstractPolicyBasedAuthorizer.this.getUsersAndAccessPolicies(); + return usersAndAccessPolicies.getAccessPolicy(resourceIdentifier, action); + } + + @Override + public String getFingerprint() throws AuthorizationAccessException { + // fingerprint is managed by the encapsulating class + throw new UnsupportedOperationException(); + } + + @Override + public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException { + // fingerprint is managed by the encapsulating class + throw new UnsupportedOperationException(); + } + + @Override + public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { + // fingerprint is managed by the encapsulating class + throw new UnsupportedOperationException(); + } + + @Override + public UserGroupProvider getUserGroupProvider() { + return new ConfigurableUserGroupProvider() { + @Override + public User addUser(User user) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.addUser(user); + } + + @Override + public User updateUser(User user) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.updateUser(user); + } + + @Override + public User deleteUser(User user) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.deleteUser(user); + } + + @Override + public User deleteUser(String userIdentifier) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.deleteUser(userIdentifier); + } + + @Override + public Group addGroup(Group group) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.addGroup(group); + } + + @Override + public Group updateGroup(Group group) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.updateGroup(group); + } + + @Override + public Group deleteGroup(Group group) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.deleteGroup(group); + } + + @Override + public Group deleteGroup(String groupIdentifier) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.deleteGroup(groupIdentifier); + } + + @Override + public Set<User> getUsers() throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.getUsers(); + } + + @Override + public User getUser(String identifier) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.getUser(identifier); + } + + @Override + public User getUserByIdentity(String identity) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.getUserByIdentity(identity); + } + + @Override + public Set<Group> getGroups() throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.getGroups(); + } + + @Override + public Group getGroup(String identifier) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.getGroup(identifier); + } + + @Override + public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException { + final UsersAndAccessPolicies usersAndAccessPolicies = AbstractPolicyBasedAuthorizer.this.getUsersAndAccessPolicies(); + final User user = usersAndAccessPolicies.getUser(identity); + final Set<Group> groups = usersAndAccessPolicies.getGroups(identity); + + return new UserAndGroups() { + @Override + public User getUser() { + return user; + } + + @Override + public Set<Group> getGroups() { + return groups; + } + }; + } + + @Override + public String getFingerprint() throws AuthorizationAccessException { + // fingerprint is managed by the encapsulating class + throw new UnsupportedOperationException(); + } + + @Override + public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException { + // fingerprint is managed by the encapsulating class + throw new UnsupportedOperationException(); + } + + @Override + public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { + // fingerprint is managed by the encapsulating class + throw new UnsupportedOperationException(); + } + + @Override + public void initialize(UserGroupProviderInitializationContext initializationContext) throws AuthorizerCreationException { + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws AuthorizerCreationException { + } + + @Override + public void preDestruction() throws AuthorizerDestructionException { + } + }; + } + + @Override + public void initialize(AccessPolicyProviderInitializationContext initializationContext) throws AuthorizerCreationException { + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws AuthorizerCreationException { + } + + @Override + public void preDestruction() throws AuthorizerDestructionException { + } + }; + } + + /** + * Returns a fingerprint representing the authorizations managed by this authorizer. The fingerprint will be + * used for comparison to determine if two policy-based authorizers represent a compatible set of users, + * groups, and policies. + * + * @return the fingerprint for this Authorizer + */ + @Override + public final String getFingerprint() throws AuthorizationAccessException { + final List<User> users = getSortedUsers(); + final List<Group> groups = getSortedGroups(); + final List<AccessPolicy> policies = getSortedAccessPolicies(); + + XMLStreamWriter writer = null; + final StringWriter out = new StringWriter(); + try { + writer = XML_OUTPUT_FACTORY.createXMLStreamWriter(out); + writer.writeStartDocument(); + writer.writeStartElement("authorizations"); + + for (User user : users) { + writeUser(writer, user); + } + for (Group group : groups) { + writeGroup(writer, group); + } + for (AccessPolicy policy : policies) { + writePolicy(writer, policy); + } + + writer.writeEndElement(); + writer.writeEndDocument(); + writer.flush(); + } catch (XMLStreamException e) { + throw new AuthorizationAccessException("Unable to generate fingerprint", e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (XMLStreamException e) { + // nothing to do here + } + } + } + + return out.toString(); + } + + private void writeUser(final XMLStreamWriter writer, final User user) throws XMLStreamException { + writer.writeStartElement(USER_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, user.getIdentifier()); + writer.writeAttribute(IDENTITY_ATTR, user.getIdentity()); + writer.writeEndElement(); + } + + private void writeGroup(final XMLStreamWriter writer, final Group group) throws XMLStreamException { + List<String> users = new ArrayList<>(group.getUsers()); + Collections.sort(users); + + writer.writeStartElement(GROUP_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, group.getIdentifier()); + writer.writeAttribute(NAME_ATTR, group.getName()); + + for (String user : users) { + writer.writeStartElement(GROUP_USER_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, user); + writer.writeEndElement(); + } + + writer.writeEndElement(); + } + + private void writePolicy(final XMLStreamWriter writer, final AccessPolicy policy) throws XMLStreamException { + // sort the users for the policy + List<String> policyUsers = new ArrayList<>(policy.getUsers()); + Collections.sort(policyUsers); + + // sort the groups for this policy + List<String> policyGroups = new ArrayList<>(policy.getGroups()); + Collections.sort(policyGroups); + + writer.writeStartElement(POLICY_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, policy.getIdentifier()); + writer.writeAttribute(RESOURCE_ATTR, policy.getResource()); + writer.writeAttribute(ACTIONS_ATTR, policy.getAction().name()); + + for (String policyUser : policyUsers) { + writer.writeStartElement(POLICY_USER_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, policyUser); + writer.writeEndElement(); + } + + for (String policyGroup : policyGroups) { + writer.writeStartElement(POLICY_GROUP_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, policyGroup); + writer.writeEndElement(); + } + + writer.writeEndElement(); + } + + private List<AccessPolicy> getSortedAccessPolicies() { + final List<AccessPolicy> policies = new ArrayList<>(getAccessPolicies()); + Collections.sort(policies, Comparator.comparing(AccessPolicy::getIdentifier)); + return policies; + } + + private List<Group> getSortedGroups() { + final List<Group> groups = new ArrayList<>(getGroups()); + Collections.sort(groups, Comparator.comparing(Group::getIdentifier)); + return groups; + } + + private List<User> getSortedUsers() { + final List<User> users = new ArrayList<>(getUsers()); + Collections.sort(users, Comparator.comparing(User::getIdentifier)); + return users; + } + + private static class PoliciesUsersAndGroups { + final List<AccessPolicy> accessPolicies; + final List<User> users; + final List<Group> groups; + + public PoliciesUsersAndGroups(List<AccessPolicy> accessPolicies, List<User> users, List<Group> groups) { + this.accessPolicies = accessPolicies; + this.users = users; + this.groups = groups; + } + + public List<AccessPolicy> getAccessPolicies() { + return accessPolicies; + } + + public List<User> getUsers() { + return users; + } + + public List<Group> getGroups() { + return groups; + } + } +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/StandardAuthorizerConfigurationContext.java ---------------------------------------------------------------------- diff --git a/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/StandardAuthorizerConfigurationContext.java b/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/StandardAuthorizerConfigurationContext.java new file mode 100644 index 0000000..798f974 --- /dev/null +++ b/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/StandardAuthorizerConfigurationContext.java @@ -0,0 +1,54 @@ +/* + * 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.authorization; + +import org.apache.nifi.registry.util.PropertyValue; +import org.apache.nifi.registry.util.StandardPropertyValue; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * + */ +public class StandardAuthorizerConfigurationContext implements AuthorizerConfigurationContext { + + private final String identifier; + private final Map<String, String> properties; + + public StandardAuthorizerConfigurationContext(String identifier, Map<String, String> properties) { + this.identifier = identifier; + this.properties = Collections.unmodifiableMap(new HashMap<String, String>(properties)); + } + + @Override + public String getIdentifier() { + return identifier; + } + + @Override + public Map<String, String> getProperties() { + return properties; + } + + @Override + public PropertyValue getProperty(String property) { + return new StandardPropertyValue(properties.get(property)); + } + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/StandardAuthorizerInitializationContext.java ---------------------------------------------------------------------- diff --git a/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/StandardAuthorizerInitializationContext.java b/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/StandardAuthorizerInitializationContext.java new file mode 100644 index 0000000..e23a93e --- /dev/null +++ b/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/StandardAuthorizerInitializationContext.java @@ -0,0 +1,55 @@ +/* + * 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.authorization; + +/** + * + */ +public class StandardAuthorizerInitializationContext implements AuthorizerInitializationContext { + + private final String identifier; + private final UserGroupProviderLookup userGroupProviderLookup; + private final AccessPolicyProviderLookup accessPolicyProviderLookup; + private final AuthorizerLookup authorizerLookup; + + public StandardAuthorizerInitializationContext(String identifier, UserGroupProviderLookup userGroupProviderLookup, + AccessPolicyProviderLookup accessPolicyProviderLookup, AuthorizerLookup authorizerLookup) { + this.identifier = identifier; + this.userGroupProviderLookup = userGroupProviderLookup; + this.accessPolicyProviderLookup = accessPolicyProviderLookup; + this.authorizerLookup = authorizerLookup; + } + + @Override + public String getIdentifier() { + return identifier; + } + + public AuthorizerLookup getAuthorizerLookup() { + return authorizerLookup; + } + + @Override + public AccessPolicyProviderLookup getAccessPolicyProviderLookup() { + return accessPolicyProviderLookup; + } + + @Override + public UserGroupProviderLookup getUserGroupProviderLookup() { + return userGroupProviderLookup; + } +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/StandardManagedAuthorizer.java ---------------------------------------------------------------------- diff --git a/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/StandardManagedAuthorizer.java b/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/StandardManagedAuthorizer.java new file mode 100644 index 0000000..8cd4fea --- /dev/null +++ b/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/StandardManagedAuthorizer.java @@ -0,0 +1,264 @@ +/* + * 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.authorization; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.authorization.exception.UninheritableAuthorizationsException; +import org.apache.nifi.registry.util.PropertyValue; +import org.apache.nifi.registry.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.authorization.exception.AuthorizerCreationException; +import org.apache.nifi.registry.authorization.exception.AuthorizerDestructionException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.Set; + +public class StandardManagedAuthorizer implements ManagedAuthorizer { + + private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); + private static final XMLOutputFactory XML_OUTPUT_FACTORY = XMLOutputFactory.newInstance(); + + private static final String USER_GROUP_PROVIDER_ELEMENT = "userGroupProvider"; + private static final String ACCESS_POLICY_PROVIDER_ELEMENT = "accessPolicyProvider"; + + private AccessPolicyProviderLookup accessPolicyProviderLookup; + private AccessPolicyProvider accessPolicyProvider; + private UserGroupProvider userGroupProvider; + + public StandardManagedAuthorizer() {} + + // exposed for testing to inject mocks + public StandardManagedAuthorizer(AccessPolicyProvider accessPolicyProvider, UserGroupProvider userGroupProvider) { + this.accessPolicyProvider = accessPolicyProvider; + this.userGroupProvider = userGroupProvider; + } + + @Override + public void initialize(AuthorizerInitializationContext initializationContext) throws AuthorizerCreationException { + accessPolicyProviderLookup = initializationContext.getAccessPolicyProviderLookup(); + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws AuthorizerCreationException { + final PropertyValue accessPolicyProviderKey = configurationContext.getProperty("Access Policy Provider"); + if (!accessPolicyProviderKey.isSet()) { + throw new AuthorizerCreationException("The Access Policy Provider must be set."); + } + + accessPolicyProvider = accessPolicyProviderLookup.getAccessPolicyProvider(accessPolicyProviderKey.getValue()); + + // ensure the desired access policy provider was found + if (accessPolicyProvider == null) { + throw new AuthorizerCreationException(String.format("Unable to locate configured Access Policy Provider: %s", accessPolicyProviderKey)); + } + + userGroupProvider = accessPolicyProvider.getUserGroupProvider(); + + // ensure the desired access policy provider has a user group provider + if (userGroupProvider == null) { + throw new AuthorizerCreationException(String.format("Configured Access Policy Provider %s does not contain a User Group Provider", accessPolicyProviderKey)); + } + } + + @Override + public AuthorizationResult authorize(AuthorizationRequest request) throws AuthorizationAccessException { + final String resourceIdentifier = request.getResource().getIdentifier(); + final AccessPolicy policy = accessPolicyProvider.getAccessPolicy(resourceIdentifier, request.getAction()); + if (policy == null) { + return AuthorizationResult.resourceNotFound(); + } + + final UserAndGroups userAndGroups = userGroupProvider.getUserAndGroups(request.getIdentity()); + + final User user = userAndGroups.getUser(); + if (user == null) { + return AuthorizationResult.denied(String.format("Unknown user with identity '%s'.", request.getIdentity())); + } + + final Set<Group> userGroups = userAndGroups.getGroups(); + if (policy.getUsers().contains(user.getIdentifier()) || containsGroup(userGroups, policy)) { + return AuthorizationResult.approved(); + } + + return AuthorizationResult.denied(request.getExplanationSupplier().get()); + } + + /** + * Determines if the policy contains one of the user's groups. + * + * @param userGroups the set of the user's groups + * @param policy the policy + * @return true if one of the Groups in userGroups is contained in the policy + */ + private boolean containsGroup(final Set<Group> userGroups, final AccessPolicy policy) { + if (userGroups == null || userGroups.isEmpty() || policy.getGroups().isEmpty()) { + return false; + } + + for (Group userGroup : userGroups) { + if (policy.getGroups().contains(userGroup.getIdentifier())) { + return true; + } + } + + return false; + } + + @Override + public String getFingerprint() throws AuthorizationAccessException { + XMLStreamWriter writer = null; + final StringWriter out = new StringWriter(); + try { + writer = XML_OUTPUT_FACTORY.createXMLStreamWriter(out); + writer.writeStartDocument(); + writer.writeStartElement("managedAuthorizations"); + + writer.writeStartElement(ACCESS_POLICY_PROVIDER_ELEMENT); + if (accessPolicyProvider instanceof ConfigurableAccessPolicyProvider) { + writer.writeCharacters(((ConfigurableAccessPolicyProvider) accessPolicyProvider).getFingerprint()); + } + writer.writeEndElement(); + + writer.writeStartElement(USER_GROUP_PROVIDER_ELEMENT); + if (userGroupProvider instanceof ConfigurableUserGroupProvider) { + writer.writeCharacters(((ConfigurableUserGroupProvider) userGroupProvider).getFingerprint()); + } + writer.writeEndElement(); + + writer.writeEndElement(); + writer.writeEndDocument(); + writer.flush(); + } catch (XMLStreamException e) { + throw new AuthorizationAccessException("Unable to generate fingerprint", e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (XMLStreamException e) { + // nothing to do here + } + } + } + + return out.toString(); + } + + @Override + public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException { + if (StringUtils.isBlank(fingerprint)) { + return; + } + + final FingerprintHolder fingerprintHolder = parseFingerprint(fingerprint); + + if (StringUtils.isNotBlank(fingerprintHolder.getPolicyFingerprint()) && accessPolicyProvider instanceof ConfigurableAccessPolicyProvider) { + ((ConfigurableAccessPolicyProvider) accessPolicyProvider).inheritFingerprint(fingerprintHolder.getPolicyFingerprint()); + } + + if (StringUtils.isNotBlank(fingerprintHolder.getUserGroupFingerprint()) && userGroupProvider instanceof ConfigurableUserGroupProvider) { + ((ConfigurableUserGroupProvider) userGroupProvider).inheritFingerprint(fingerprintHolder.getUserGroupFingerprint()); + } + } + + @Override + public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { + final FingerprintHolder fingerprintHolder = parseFingerprint(proposedFingerprint); + + if (StringUtils.isNotBlank(fingerprintHolder.getPolicyFingerprint())) { + if (accessPolicyProvider instanceof ConfigurableAccessPolicyProvider) { + ((ConfigurableAccessPolicyProvider) accessPolicyProvider).checkInheritability(fingerprintHolder.getPolicyFingerprint()); + } else { + throw new UninheritableAuthorizationsException("Policy fingerprint is not blank and the configured AccessPolicyProvider does not support fingerprinting."); + } + } + + if (StringUtils.isNotBlank(fingerprintHolder.getUserGroupFingerprint())) { + if (userGroupProvider instanceof ConfigurableUserGroupProvider) { + ((ConfigurableUserGroupProvider) userGroupProvider).checkInheritability(fingerprintHolder.getUserGroupFingerprint()); + } else { + throw new UninheritableAuthorizationsException("User/Group fingerprint is not blank and the configured UserGroupProvider does not support fingerprinting."); + } + } + } + + private final FingerprintHolder parseFingerprint(final String fingerprint) throws AuthorizationAccessException { + final byte[] fingerprintBytes = fingerprint.getBytes(StandardCharsets.UTF_8); + + try (final ByteArrayInputStream in = new ByteArrayInputStream(fingerprintBytes)) { + final DocumentBuilder docBuilder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder(); + final Document document = docBuilder.parse(in); + final Element rootElement = document.getDocumentElement(); + + final NodeList accessPolicyProviderList = rootElement.getElementsByTagName(ACCESS_POLICY_PROVIDER_ELEMENT); + if (accessPolicyProviderList.getLength() != 1) { + throw new AuthorizationAccessException(String.format("Only one %s element is allowed: %s", ACCESS_POLICY_PROVIDER_ELEMENT, fingerprint)); + } + + final NodeList userGroupProviderList = rootElement.getElementsByTagName(USER_GROUP_PROVIDER_ELEMENT); + if (userGroupProviderList.getLength() != 1) { + throw new AuthorizationAccessException(String.format("Only one %s element is allowed: %s", USER_GROUP_PROVIDER_ELEMENT, fingerprint)); + } + + final Node accessPolicyProvider = accessPolicyProviderList.item(0); + final Node userGroupProvider = userGroupProviderList.item(0); + return new FingerprintHolder(accessPolicyProvider.getTextContent(), userGroupProvider.getTextContent()); + } catch (SAXException | ParserConfigurationException | IOException e) { + throw new AuthorizationAccessException("Unable to parse fingerprint", e); + } + } + + @Override + public AccessPolicyProvider getAccessPolicyProvider() { + return accessPolicyProvider; + } + + @Override + public void preDestruction() throws AuthorizerDestructionException { + + } + + private static class FingerprintHolder { + private final String policyFingerprint; + private final String userGroupFingerprint; + + public FingerprintHolder(String policyFingerprint, String userGroupFingerprint) { + this.policyFingerprint = policyFingerprint; + this.userGroupFingerprint = userGroupFingerprint; + } + + public String getPolicyFingerprint() { + return policyFingerprint; + } + + public String getUserGroupFingerprint() { + return userGroupFingerprint; + } + } +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/UsersAndAccessPolicies.java ---------------------------------------------------------------------- diff --git a/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/UsersAndAccessPolicies.java b/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/UsersAndAccessPolicies.java new file mode 100644 index 0000000..7e05c9c --- /dev/null +++ b/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/UsersAndAccessPolicies.java @@ -0,0 +1,52 @@ +/* + * 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.authorization; + +import java.util.Set; + +/** + * A holder object to provide atomic access to policies for a given resource and users by + * identity. Implementations must ensure consistent access to the data backing this instance. + */ +public interface UsersAndAccessPolicies { + + /** + * Retrieves the set of access policies for a given resource and action. + * + * @param resourceIdentifier the resource identifier to retrieve policies for + * @param action the action to retrieve policies for + * @return the access policy for the given resource and action + */ + AccessPolicy getAccessPolicy(final String resourceIdentifier, final RequestAction action); + + /** + * Retrieves a user by an identity string. + * + * @param identity the identity of the user to retrieve + * @return the user with the given identity + */ + User getUser(final String identity); + + /** + * Retrieves the groups for a given user identity. + * + * @param userIdentity a user identity + * @return the set of groups for the given user identity + */ + Set<Group> getGroups(final String userIdentity); + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/annotation/AuthorizerContext.java ---------------------------------------------------------------------- diff --git a/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/annotation/AuthorizerContext.java b/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/annotation/AuthorizerContext.java new file mode 100644 index 0000000..131238c --- /dev/null +++ b/nifi-registry-security-api-impl/src/main/java/org/apache/nifi/registry/authorization/annotation/AuthorizerContext.java @@ -0,0 +1,35 @@ +/* + * 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.authorization.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * + */ +@Documented +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface AuthorizerContext { +}
