http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardManagedAuthorizer.java ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardManagedAuthorizer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardManagedAuthorizer.java new file mode 100644 index 0000000..58bcf55 --- /dev/null +++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/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.security.authorization; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.util.PropertyValue; +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 SecurityProviderCreationException { + accessPolicyProviderLookup = initializationContext.getAccessPolicyProviderLookup(); + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + final PropertyValue accessPolicyProviderKey = configurationContext.getProperty("Access Policy Provider"); + if (!accessPolicyProviderKey.isSet()) { + throw new SecurityProviderCreationException("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 SecurityProviderCreationException(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 SecurityProviderCreationException(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 SecurityProviderDestructionException { + + } + + 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/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UsersAndAccessPolicies.java ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UsersAndAccessPolicies.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UsersAndAccessPolicies.java new file mode 100644 index 0000000..7675f27 --- /dev/null +++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/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.security.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/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/AuthorizationsHolder.java ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/AuthorizationsHolder.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/AuthorizationsHolder.java new file mode 100644 index 0000000..6e84f49 --- /dev/null +++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/AuthorizationsHolder.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.security.authorization.file; + + +import org.apache.nifi.registry.security.authorization.file.generated.Authorizations; +import org.apache.nifi.registry.security.authorization.file.generated.Policies; +import org.apache.nifi.registry.security.authorization.AccessPolicy; +import org.apache.nifi.registry.security.authorization.RequestAction; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A holder to provide atomic access to data structures. + */ +public class AuthorizationsHolder { + + private final Authorizations authorizations; + + private final Set<AccessPolicy> allPolicies; + private final Map<String, Set<AccessPolicy>> policiesByResource; + private final Map<String, AccessPolicy> policiesById; + + /** + * Creates a new holder and populates all convenience authorizations data structures. + * + * @param authorizations the current authorizations instance + */ + public AuthorizationsHolder(final Authorizations authorizations) { + this.authorizations = authorizations; + + // load all access policies + final Policies policies = authorizations.getPolicies(); + final Set<AccessPolicy> allPolicies = Collections.unmodifiableSet(createAccessPolicies(policies)); + + // create a convenience map from resource id to policies + final Map<String, Set<AccessPolicy>> policiesByResourceMap = Collections.unmodifiableMap(createResourcePolicyMap(allPolicies)); + + // create a convenience map from policy id to policy + final Map<String, AccessPolicy> policiesByIdMap = Collections.unmodifiableMap(createPoliciesByIdMap(allPolicies)); + + // set all the holders + this.allPolicies = allPolicies; + this.policiesByResource = policiesByResourceMap; + this.policiesById = policiesByIdMap; + } + + /** + * Creates AccessPolicies from the JAXB Policies. + * + * @param policies the JAXB Policies element + * @return a set of AccessPolicies corresponding to the provided Resources + */ + private Set<AccessPolicy> createAccessPolicies(org.apache.nifi.registry.security.authorization.file.generated.Policies policies) { + Set<AccessPolicy> allPolicies = new HashSet<>(); + if (policies == null || policies.getPolicy() == null) { + return allPolicies; + } + + // load the new authorizations + for (final org.apache.nifi.registry.security.authorization.file.generated.Policy policy : policies.getPolicy()) { + final String policyIdentifier = policy.getIdentifier(); + final String resourceIdentifier = policy.getResource(); + + // start a new builder and set the policy and resource identifiers + final AccessPolicy.Builder builder = new AccessPolicy.Builder() + .identifier(policyIdentifier) + .resource(resourceIdentifier); + + // add each user identifier + for (org.apache.nifi.registry.security.authorization.file.generated.Policy.User user : policy.getUser()) { + builder.addUser(user.getIdentifier()); + } + + // add each group identifier + for (org.apache.nifi.registry.security.authorization.file.generated.Policy.Group group : policy.getGroup()) { + builder.addGroup(group.getIdentifier()); + } + + // add the appropriate request actions + final String authorizationCode = policy.getAction(); + if (authorizationCode.equals(FileAccessPolicyProvider.READ_CODE)) { + builder.action(RequestAction.READ); + } else if (authorizationCode.equals(FileAccessPolicyProvider.WRITE_CODE)){ + builder.action(RequestAction.WRITE); + } else if (authorizationCode.equals(FileAccessPolicyProvider.DELETE_CODE)){ + builder.action(RequestAction.DELETE); + } else { + throw new IllegalStateException("Unknown Policy Action: " + authorizationCode); + } + + // build the policy and add it to the map + allPolicies.add(builder.build()); + } + + return allPolicies; + } + + /** + * Creates a map from resource identifier to the set of policies for the given resource. + * + * @param allPolicies the set of all policies + * @return a map from resource identifier to policies + */ + private Map<String, Set<AccessPolicy>> createResourcePolicyMap(final Set<AccessPolicy> allPolicies) { + Map<String, Set<AccessPolicy>> resourcePolicies = new HashMap<>(); + + for (AccessPolicy policy : allPolicies) { + Set<AccessPolicy> policies = resourcePolicies.get(policy.getResource()); + if (policies == null) { + policies = new HashSet<>(); + resourcePolicies.put(policy.getResource(), policies); + } + policies.add(policy); + } + + return resourcePolicies; + } + + /** + * Creates a Map from policy identifier to AccessPolicy. + * + * @param policies the set of all access policies + * @return the Map from policy identifier to AccessPolicy + */ + private Map<String, AccessPolicy> createPoliciesByIdMap(final Set<AccessPolicy> policies) { + Map<String,AccessPolicy> policyMap = new HashMap<>(); + for (AccessPolicy policy : policies) { + policyMap.put(policy.getIdentifier(), policy); + } + return policyMap; + } + + public Authorizations getAuthorizations() { + return authorizations; + } + + public Set<AccessPolicy> getAllPolicies() { + return allPolicies; + } + + public Map<String, Set<AccessPolicy>> getPoliciesByResource() { + return policiesByResource; + } + + public Map<String, AccessPolicy> getPoliciesById() { + return policiesById; + } + + public AccessPolicy getAccessPolicy(final String resourceIdentifier, final RequestAction action) { + if (resourceIdentifier == null) { + throw new IllegalArgumentException("Resource Identifier cannot be null"); + } + + final Set<AccessPolicy> resourcePolicies = policiesByResource.get(resourceIdentifier); + if (resourcePolicies == null) { + return null; + } + + for (AccessPolicy accessPolicy : resourcePolicies) { + if (accessPolicy.getAction() == action) { + return accessPolicy; + } + } + + return null; + } + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java new file mode 100644 index 0000000..c3434c4 --- /dev/null +++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java @@ -0,0 +1,777 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.security.authorization.file; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.properties.util.IdentityMapping; +import org.apache.nifi.registry.properties.util.IdentityMappingUtil; +import org.apache.nifi.registry.security.authorization.AccessPolicy; +import org.apache.nifi.registry.security.authorization.AccessPolicyProviderInitializationContext; +import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.ConfigurableAccessPolicyProvider; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.User; +import org.apache.nifi.registry.security.authorization.UserGroupProvider; +import org.apache.nifi.registry.security.authorization.UserGroupProviderLookup; +import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext; +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException; +import org.apache.nifi.registry.security.authorization.file.generated.Authorizations; +import org.apache.nifi.registry.security.authorization.file.generated.Policies; +import org.apache.nifi.registry.security.authorization.file.generated.Policy; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.util.PropertyValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.XMLConstants; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +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 javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import java.io.ByteArrayInputStream; +import java.io.File; +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.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class FileAccessPolicyProvider implements ConfigurableAccessPolicyProvider { + + private static final Logger logger = LoggerFactory.getLogger(FileAccessPolicyProvider.class); + + private static final String AUTHORIZATIONS_XSD = "/authorizations.xsd"; + private static final String JAXB_AUTHORIZATIONS_PATH = "org.apache.nifi.registry.security.authorization.file.generated"; + + private static final JAXBContext JAXB_AUTHORIZATIONS_CONTEXT = initializeJaxbContext(JAXB_AUTHORIZATIONS_PATH); + + /** + * Load the JAXBContext. + */ + private static JAXBContext initializeJaxbContext(final String contextPath) { + try { + return JAXBContext.newInstance(contextPath, FileAuthorizer.class.getClassLoader()); + } catch (JAXBException e) { + throw new RuntimeException("Unable to create JAXBContext."); + } + } + + private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); + private static final XMLOutputFactory XML_OUTPUT_FACTORY = XMLOutputFactory.newInstance(); + + private static final String POLICY_ELEMENT = "policy"; + private static final String POLICY_USER_ELEMENT = "policyUser"; + private static final String POLICY_GROUP_ELEMENT = "policyGroup"; + private static final String IDENTIFIER_ATTR = "identifier"; + private static final String RESOURCE_ATTR = "resource"; + private static final String ACTIONS_ATTR = "actions"; + + /* These codes must match the enumeration values set in authorizations.xsd */ + static final String READ_CODE = "R"; + static final String WRITE_CODE = "W"; + static final String DELETE_CODE = "D"; + + /* TODO - move this somewhere into nifi-registry-security-framework so it can be applied to any ConfigurableAccessPolicyProvider + * (and also gets us away from requiring magic strings here) */ + private static final ResourceActionPair[] INITIAL_ADMIN_ACCESS_POLICIES = { + new ResourceActionPair("/tenants", READ_CODE), + new ResourceActionPair("/tenants", WRITE_CODE), + new ResourceActionPair("/tenants", DELETE_CODE), + new ResourceActionPair("/policies", READ_CODE), + new ResourceActionPair("/policies", WRITE_CODE), + new ResourceActionPair("/policies", DELETE_CODE), + new ResourceActionPair("/buckets", READ_CODE), + new ResourceActionPair("/buckets", WRITE_CODE), + new ResourceActionPair("/buckets", DELETE_CODE), + new ResourceActionPair("/actuator", READ_CODE), + new ResourceActionPair("/actuator", WRITE_CODE), + new ResourceActionPair("/actuator", DELETE_CODE), + new ResourceActionPair("/swagger", READ_CODE), + new ResourceActionPair("/swagger", WRITE_CODE), + new ResourceActionPair("/swagger", DELETE_CODE), + new ResourceActionPair("/proxy", WRITE_CODE) + }; + + /* TODO - move this somewhere into nifi-registry-security-framework so it can be applied to any ConfigurableAccessPolicyProvider + * (and also gets us away from requiring magic strings here) */ + private static final ResourceActionPair[] NIFI_ACCESS_POLICIES = { + new ResourceActionPair("/buckets", READ_CODE), + new ResourceActionPair("/proxy", WRITE_CODE) + }; + + static final String PROP_NIFI_IDENTITY_PREFIX = "NiFi Identity "; + static final String PROP_USER_GROUP_PROVIDER = "User Group Provider"; + static final String PROP_AUTHORIZATIONS_FILE = "Authorizations File"; + static final String PROP_INITIAL_ADMIN_IDENTITY = "Initial Admin Identity"; + static final Pattern NIFI_IDENTITY_PATTERN = Pattern.compile(PROP_NIFI_IDENTITY_PREFIX + "\\S+"); + + private Schema authorizationsSchema; + private NiFiRegistryProperties properties; + private File authorizationsFile; + private String initialAdminIdentity; + private Set<String> nifiIdentities; + private List<IdentityMapping> identityMappings; + + private UserGroupProvider userGroupProvider; + private UserGroupProviderLookup userGroupProviderLookup; + private final AtomicReference<AuthorizationsHolder> authorizationsHolder = new AtomicReference<>(); + + @Override + public void initialize(AccessPolicyProviderInitializationContext initializationContext) throws SecurityProviderCreationException { + userGroupProviderLookup = initializationContext.getUserGroupProviderLookup(); + + try { + final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + authorizationsSchema = schemaFactory.newSchema(FileAuthorizer.class.getResource(AUTHORIZATIONS_XSD)); + } catch (Exception e) { + throw new SecurityProviderCreationException(e); + } + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + try { + final PropertyValue userGroupProviderIdentifier = configurationContext.getProperty(PROP_USER_GROUP_PROVIDER); + if (!userGroupProviderIdentifier.isSet()) { + throw new SecurityProviderCreationException("The user group provider must be specified."); + } + + userGroupProvider = userGroupProviderLookup.getUserGroupProvider(userGroupProviderIdentifier.getValue()); + if (userGroupProvider == null) { + throw new SecurityProviderCreationException("Unable to locate user group provider with identifier " + userGroupProviderIdentifier.getValue()); + } + + final PropertyValue authorizationsPath = configurationContext.getProperty(PROP_AUTHORIZATIONS_FILE); + if (StringUtils.isBlank(authorizationsPath.getValue())) { + throw new SecurityProviderCreationException("The authorizations file must be specified."); + } + + // get the authorizations file and ensure it exists + authorizationsFile = new File(authorizationsPath.getValue()); + if (!authorizationsFile.exists()) { + logger.info("Creating new authorizations file at {}", new Object[] {authorizationsFile.getAbsolutePath()}); + saveAuthorizations(new Authorizations()); + } + + // extract the identity mappings from nifi-registry.properties if any are provided + identityMappings = Collections.unmodifiableList(IdentityMappingUtil.getIdentityMappings(properties)); + + // get the value of the initial admin identity + final PropertyValue initialAdminIdentityProp = configurationContext.getProperty(PROP_INITIAL_ADMIN_IDENTITY); + initialAdminIdentity = initialAdminIdentityProp.isSet() ? IdentityMappingUtil.mapIdentity(initialAdminIdentityProp.getValue(), identityMappings) : null; + + // extract any nifi identities + nifiIdentities = new HashSet<>(); + for (Map.Entry<String,String> entry : configurationContext.getProperties().entrySet()) { + Matcher matcher = NIFI_IDENTITY_PATTERN.matcher(entry.getKey()); + if (matcher.matches() && !StringUtils.isBlank(entry.getValue())) { + nifiIdentities.add(IdentityMappingUtil.mapIdentity(entry.getValue(), identityMappings)); + } + } + + // load the authorizations + load(); + + logger.info(String.format("Authorizations file loaded at %s", new Date().toString())); + } catch (SecurityProviderCreationException | JAXBException | IllegalStateException | SAXException e) { + throw new SecurityProviderCreationException(e); + } + } + + @Override + public UserGroupProvider getUserGroupProvider() { + return userGroupProvider; + } + + @Override + public Set<AccessPolicy> getAccessPolicies() throws AuthorizationAccessException { + return authorizationsHolder.get().getAllPolicies(); + } + + @Override + public synchronized AccessPolicy addAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + if (accessPolicy == null) { + throw new IllegalArgumentException("AccessPolicy cannot be null"); + } + + // create the new JAXB Policy + final Policy policy = createJAXBPolicy(accessPolicy); + + // add the new Policy to the top-level list of policies + final AuthorizationsHolder holder = authorizationsHolder.get(); + final Authorizations authorizations = holder.getAuthorizations(); + authorizations.getPolicies().getPolicy().add(policy); + + saveAndRefreshHolder(authorizations); + + return authorizationsHolder.get().getPoliciesById().get(accessPolicy.getIdentifier()); + } + + @Override + public AccessPolicy getAccessPolicy(String identifier) throws AuthorizationAccessException { + if (identifier == null) { + return null; + } + + final AuthorizationsHolder holder = authorizationsHolder.get(); + return holder.getPoliciesById().get(identifier); + } + + @Override + public AccessPolicy getAccessPolicy(String resourceIdentifier, RequestAction action) throws AuthorizationAccessException { + return authorizationsHolder.get().getAccessPolicy(resourceIdentifier, action); + } + + @Override + public synchronized AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + if (accessPolicy == null) { + throw new IllegalArgumentException("AccessPolicy cannot be null"); + } + + final AuthorizationsHolder holder = this.authorizationsHolder.get(); + final Authorizations authorizations = holder.getAuthorizations(); + + // try to find an existing Authorization that matches the policy id + Policy updatePolicy = null; + for (Policy policy : authorizations.getPolicies().getPolicy()) { + if (policy.getIdentifier().equals(accessPolicy.getIdentifier())) { + updatePolicy = policy; + break; + } + } + + // no matching Policy so return null + if (updatePolicy == null) { + return null; + } + + // update the Policy, save, reload, and return + transferUsersAndGroups(accessPolicy, updatePolicy); + saveAndRefreshHolder(authorizations); + + return this.authorizationsHolder.get().getPoliciesById().get(accessPolicy.getIdentifier()); + } + + @Override + public synchronized AccessPolicy deleteAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + if (accessPolicy == null) { + throw new IllegalArgumentException("AccessPolicy cannot be null"); + } + + return deleteAccessPolicy(accessPolicy.getIdentifier()); + } + + @Override + public synchronized AccessPolicy deleteAccessPolicy(String accessPolicyIdentifer) throws AuthorizationAccessException { + if (accessPolicyIdentifer == null) { + throw new IllegalArgumentException("Access policy identifier cannot be null"); + } + + final AuthorizationsHolder holder = this.authorizationsHolder.get(); + AccessPolicy deletedPolicy = holder.getPoliciesById().get(accessPolicyIdentifer); + if (deletedPolicy == null) { + return null; + } + + // find the matching Policy and remove it + final Authorizations authorizations = holder.getAuthorizations(); + Iterator<Policy> policyIter = authorizations.getPolicies().getPolicy().iterator(); + while (policyIter.hasNext()) { + final Policy policy = policyIter.next(); + if (policy.getIdentifier().equals(accessPolicyIdentifer)) { + policyIter.remove(); + break; + } + } + + saveAndRefreshHolder(authorizations); + return deletedPolicy; + } + + AuthorizationsHolder getAuthorizationsHolder() { + return authorizationsHolder.get(); + } + + @AuthorizerContext + public void setNiFiProperties(NiFiRegistryProperties properties) { + this.properties = properties; + } + + @Override + public synchronized void inheritFingerprint(String fingerprint) throws AuthorizationAccessException { + parsePolicies(fingerprint).forEach(policy -> addAccessPolicy(policy)); + } + + @Override + public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { + try { + // ensure we can understand the proposed fingerprint + parsePolicies(proposedFingerprint); + } catch (final AuthorizationAccessException e) { + throw new UninheritableAuthorizationsException("Unable to parse the proposed fingerprint: " + e); + } + + // ensure we are in a proper state to inherit the fingerprint + if (!getAccessPolicies().isEmpty()) { + throw new UninheritableAuthorizationsException("Proposed fingerprint is not inheritable because the current access policies is not empty."); + } + } + + @Override + public String getFingerprint() throws AuthorizationAccessException { + final List<AccessPolicy> policies = new ArrayList<>(getAccessPolicies()); + Collections.sort(policies, Comparator.comparing(AccessPolicy::getIdentifier)); + + XMLStreamWriter writer = null; + final StringWriter out = new StringWriter(); + try { + writer = XML_OUTPUT_FACTORY.createXMLStreamWriter(out); + writer.writeStartDocument(); + writer.writeStartElement("accessPolicies"); + + 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 List<AccessPolicy> parsePolicies(final String fingerprint) { + final List<AccessPolicy> policies = 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 policies and add them to the current access policy provider + NodeList policyNodes = rootElement.getElementsByTagName(POLICY_ELEMENT); + for (int i = 0; i < policyNodes.getLength(); i++) { + Node policyNode = policyNodes.item(i); + policies.add(parsePolicy((Element) policyNode)); + } + } catch (SAXException | ParserConfigurationException | IOException e) { + throw new AuthorizationAccessException("Unable to parse fingerprint", e); + } + + return policies; + } + + 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 if (actions.equals(RequestAction.DELETE.name())) { + builder.action(RequestAction.DELETE); + } 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(); + } + + 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(); + } + + /** + * Loads the authorizations file and populates the AuthorizationsHolder, only called during start-up. + * + * @throws JAXBException Unable to reload the authorized users file + */ + private synchronized void load() throws JAXBException, SAXException { + // attempt to unmarshal + final Authorizations authorizations = unmarshallAuthorizations(); + if (authorizations.getPolicies() == null) { + authorizations.setPolicies(new Policies()); + } + + final AuthorizationsHolder authorizationsHolder = new AuthorizationsHolder(authorizations); + final boolean emptyAuthorizations = authorizationsHolder.getAllPolicies().isEmpty(); + final boolean hasInitialAdminIdentity = (initialAdminIdentity != null && !StringUtils.isBlank(initialAdminIdentity)); + final boolean hasNiFiIdentities = (nifiIdentities != null && !nifiIdentities.isEmpty()); + + // if we are starting fresh then we might need to populate an initial admin + if (emptyAuthorizations) { + if (hasInitialAdminIdentity) { + logger.info("Populating authorizations for Initial Admin: " + initialAdminIdentity); + populateInitialAdmin(authorizations); + } + + if (hasNiFiIdentities) { + logger.info("Populating proxy authorizations for NiFi clients: [{}]", StringUtils.join(nifiIdentities, ";")); + populateNiFiIdentities(authorizations); + } + + saveAndRefreshHolder(authorizations); + } else { + this.authorizationsHolder.set(authorizationsHolder); + } + } + + private void saveAuthorizations(final Authorizations authorizations) throws JAXBException { + final Marshaller marshaller = JAXB_AUTHORIZATIONS_CONTEXT.createMarshaller(); + marshaller.setSchema(authorizationsSchema); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + marshaller.marshal(authorizations, authorizationsFile); + } + + private Authorizations unmarshallAuthorizations() throws JAXBException { + final Unmarshaller unmarshaller = JAXB_AUTHORIZATIONS_CONTEXT.createUnmarshaller(); + unmarshaller.setSchema(authorizationsSchema); + + final JAXBElement<Authorizations> element = unmarshaller.unmarshal(new StreamSource(authorizationsFile), Authorizations.class); + return element.getValue(); + } + + /** + * Creates the initial admin user and sets policies managing buckets, users, and policies. + * + * TODO - move this somewhere into nifi-registry-security-framework so it can be applied to any ConfigurableAccessPolicyProvider + */ + private void populateInitialAdmin(final Authorizations authorizations) { + final User initialAdmin = userGroupProvider.getUserByIdentity(initialAdminIdentity); + if (initialAdmin == null) { + throw new SecurityProviderCreationException("Unable to locate initial admin " + initialAdminIdentity + " to seed policies"); + } + + for (ResourceActionPair resourceAction : INITIAL_ADMIN_ACCESS_POLICIES) { + addUserToAccessPolicy(authorizations, resourceAction.resource, initialAdmin.getIdentifier(), resourceAction.actionCode); + } + } + + /** + * Creates a user for each NiFi client and gives each one write permission to /proxy. + * + * @param authorizations the overall authorizations + */ + private void populateNiFiIdentities(Authorizations authorizations) { + for (String nifiIdentity : nifiIdentities) { + final User nifiUser = userGroupProvider.getUserByIdentity(nifiIdentity); + if (nifiUser == null) { + throw new SecurityProviderCreationException("Unable to locate node " + nifiIdentity + " to seed policies."); + } + + // grant access to the resources needed for initial nifi-proxy identities + for (ResourceActionPair resourceAction : NIFI_ACCESS_POLICIES) { + addUserToAccessPolicy(authorizations, resourceAction.resource, nifiUser.getIdentifier(), resourceAction.actionCode); + } + } + } + + + /** + * Creates and adds an access policy for the given resource, identity, and actions to the specified authorizations. + * + * @param authorizations the Authorizations instance to add the policy to + * @param resource the resource for the policy + * @param userIdentifier the identifier for the user to add to the policy + * @param action the action for the policy + */ + private void addUserToAccessPolicy(final Authorizations authorizations, final String resource, final String userIdentifier, final String action) { + // first try to find an existing policy for the given resource and action + Policy foundPolicy = null; + for (Policy policy : authorizations.getPolicies().getPolicy()) { + if (policy.getResource().equals(resource) && policy.getAction().equals(action)) { + foundPolicy = policy; + break; + } + } + + if (foundPolicy == null) { + // if we didn't find an existing policy create a new one + final String uuidSeed = resource + action; + + final AccessPolicy.Builder builder = new AccessPolicy.Builder() + .identifierGenerateFromSeed(uuidSeed) + .resource(resource) + .addUser(userIdentifier); + + if (action.equals(READ_CODE)) { + builder.action(RequestAction.READ); + } else if (action.equals(WRITE_CODE)) { + builder.action(RequestAction.WRITE); + } else if (action.equals(DELETE_CODE)) { + builder.action(RequestAction.DELETE); + } else { + throw new IllegalStateException("Unknown Policy Action: " + action); + } + + final AccessPolicy accessPolicy = builder.build(); + final Policy jaxbPolicy = createJAXBPolicy(accessPolicy); + authorizations.getPolicies().getPolicy().add(jaxbPolicy); + } else { + // otherwise add the user to the existing policy + Policy.User policyUser = new Policy.User(); + policyUser.setIdentifier(userIdentifier); + foundPolicy.getUser().add(policyUser); + } + } + + private Policy createJAXBPolicy(final AccessPolicy accessPolicy) { + final Policy policy = new Policy(); + policy.setIdentifier(accessPolicy.getIdentifier()); + policy.setResource(accessPolicy.getResource()); + + switch (accessPolicy.getAction()) { + case READ: + policy.setAction(READ_CODE); + break; + case WRITE: + policy.setAction(WRITE_CODE); + break; + case DELETE: + policy.setAction(DELETE_CODE); + break; + default: + break; + } + + transferUsersAndGroups(accessPolicy, policy); + return policy; + } + + /** + * Sets the given Policy to the state of the provided AccessPolicy. Users and Groups will be cleared and + * set to match the AccessPolicy, the resource and action will be set to match the AccessPolicy. + * + * Does not set the identifier. + * + * @param accessPolicy the AccessPolicy to transfer state from + * @param policy the Policy to transfer state to + */ + private void transferUsersAndGroups(AccessPolicy accessPolicy, Policy policy) { + // add users to the policy + policy.getUser().clear(); + for (String userIdentifier : accessPolicy.getUsers()) { + Policy.User policyUser = new Policy.User(); + policyUser.setIdentifier(userIdentifier); + policy.getUser().add(policyUser); + } + + // add groups to the policy + policy.getGroup().clear(); + for (String groupIdentifier : accessPolicy.getGroups()) { + Policy.Group policyGroup = new Policy.Group(); + policyGroup.setIdentifier(groupIdentifier); + policy.getGroup().add(policyGroup); + } + } + + /** + * Adds the given user identifier to the policy if it doesn't already exist. + * + * @param userIdentifier a user identifier + * @param policy a policy to add the user to + */ + private void addUserToPolicy(final String userIdentifier, final Policy policy) { + // determine if the user already exists in the policy + boolean userExists = false; + for (Policy.User policyUser : policy.getUser()) { + if (policyUser.getIdentifier().equals(userIdentifier)) { + userExists = true; + break; + } + } + + // add the user to the policy if doesn't already exist + if (!userExists) { + Policy.User policyUser = new Policy.User(); + policyUser.setIdentifier(userIdentifier); + policy.getUser().add(policyUser); + } + } + + /** + * Adds the given group identifier to the policy if it doesn't already exist. + * + * @param groupIdentifier a group identifier + * @param policy a policy to add the user to + */ + private void addGroupToPolicy(final String groupIdentifier, final Policy policy) { + // determine if the group already exists in the policy + boolean groupExists = false; + for (Policy.Group policyGroup : policy.getGroup()) { + if (policyGroup.getIdentifier().equals(groupIdentifier)) { + groupExists = true; + break; + } + } + + // add the group to the policy if doesn't already exist + if (!groupExists) { + Policy.Group policyGroup = new Policy.Group(); + policyGroup.setIdentifier(groupIdentifier); + policy.getGroup().add(policyGroup); + } + } + + /** + * Finds the Policy matching the resource and action, or creates a new one and adds it to the list of policies. + * + * @param policies the policies to search through + * @param seedIdentity the seedIdentity to use when creating identifiers for new policies + * @param resource the resource for the policy + * @param action the action string for the police (R or RW) + * @return the matching policy or a new policy + */ + private Policy getOrCreatePolicy(final List<Policy> policies, final String seedIdentity, final String resource, final String action) { + Policy foundPolicy = null; + + // try to find a policy with the same resource and actions + for (Policy policy : policies) { + if (policy.getResource().equals(resource) && policy.getAction().equals(action)) { + foundPolicy = policy; + break; + } + } + + // if a matching policy wasn't found then create one + if (foundPolicy == null) { + final String uuidSeed = resource + action + seedIdentity; + final String policyIdentifier = IdentifierUtil.getIdentifier(uuidSeed); + + foundPolicy = new Policy(); + foundPolicy.setIdentifier(policyIdentifier); + foundPolicy.setResource(resource); + foundPolicy.setAction(action); + + policies.add(foundPolicy); + } + + return foundPolicy; + } + + /** + * Saves the Authorizations instance by marshalling to a file, then re-populates the + * in-memory data structures and sets the new holder. + * + * Synchronized to ensure only one thread writes the file at a time. + * + * @param authorizations the authorizations to save and populate from + * @throws AuthorizationAccessException if an error occurs saving the authorizations + */ + private synchronized void saveAndRefreshHolder(final Authorizations authorizations) throws AuthorizationAccessException { + try { + saveAuthorizations(authorizations); + + this.authorizationsHolder.set(new AuthorizationsHolder(authorizations)); + } catch (JAXBException e) { + throw new AuthorizationAccessException("Unable to save Authorizations", e); + } + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + } + + private static class ResourceActionPair { + public String resource; + public String actionCode; + public ResourceActionPair(String resource, String actionCode) { + this.resource = resource; + this.actionCode = actionCode; + } + } +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAuthorizer.java ---------------------------------------------------------------------- diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAuthorizer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAuthorizer.java new file mode 100644 index 0000000..ad59eb6 --- /dev/null +++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAuthorizer.java @@ -0,0 +1,288 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.security.authorization.file; + +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.security.authorization.AbstractPolicyBasedAuthorizer; +import org.apache.nifi.registry.security.authorization.AccessPolicy; +import org.apache.nifi.registry.security.authorization.AccessPolicyProviderInitializationContext; +import org.apache.nifi.registry.security.authorization.AccessPolicyProviderLookup; +import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.AuthorizerInitializationContext; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.StandardAuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.User; +import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext; +import org.apache.nifi.registry.security.authorization.UserGroupProviderLookup; +import org.apache.nifi.registry.security.authorization.UsersAndAccessPolicies; +import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext; +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; + +/** + * Provides authorizes requests to resources using policies persisted in a file. + */ +public class FileAuthorizer extends AbstractPolicyBasedAuthorizer { + + private static final Logger logger = LoggerFactory.getLogger(FileAuthorizer.class); + + private static final String FILE_USER_GROUP_PROVIDER_ID = "file-user-group-provider"; + private static final String FILE_ACCESS_POLICY_PROVIDER_ID = "file-access-policy-provider"; + + static final String PROP_LEGACY_AUTHORIZED_USERS_FILE = "Legacy Authorized Users File"; + + private FileUserGroupProvider userGroupProvider = new FileUserGroupProvider(); + private FileAccessPolicyProvider accessPolicyProvider = new FileAccessPolicyProvider(); + + @Override + public void initialize(final AuthorizerInitializationContext initializationContext) throws SecurityProviderCreationException { + // initialize the user group provider + userGroupProvider.initialize(new UserGroupProviderInitializationContext() { + @Override + public String getIdentifier() { + return FILE_USER_GROUP_PROVIDER_ID; + } + + @Override + public UserGroupProviderLookup getUserGroupProviderLookup() { + return (identifier) -> null; + } + }); + + // initialize the access policy provider + accessPolicyProvider.initialize(new AccessPolicyProviderInitializationContext() { + @Override + public String getIdentifier() { + return FILE_ACCESS_POLICY_PROVIDER_ID; + } + + @Override + public UserGroupProviderLookup getUserGroupProviderLookup() { + return (identifier) -> { + if (FILE_USER_GROUP_PROVIDER_ID.equals(identifier)) { + return userGroupProvider; + } + + return null; + }; + } + + @Override + public AccessPolicyProviderLookup getAccessPolicyProviderLookup() { + return (identifier) -> null; + } + }); + } + + @Override + public void doOnConfigured(final AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + final Map<String, String> configurationProperties = configurationContext.getProperties(); + + // relay the relevant config + final Map<String, String> userGroupProperties = new HashMap<>(); + if (configurationProperties.containsKey(FileUserGroupProvider.PROP_TENANTS_FILE)) { + userGroupProperties.put(FileUserGroupProvider.PROP_TENANTS_FILE, configurationProperties.get(FileUserGroupProvider.PROP_TENANTS_FILE)); + } + if (configurationProperties.containsKey(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE)) { + userGroupProperties.put(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE, configurationProperties.get(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE)); + } + + // relay the relevant config + final Map<String, String> accessPolicyProperties = new HashMap<>(); + accessPolicyProperties.put(FileAccessPolicyProvider.PROP_USER_GROUP_PROVIDER, FILE_USER_GROUP_PROVIDER_ID); + if (configurationProperties.containsKey(FileAccessPolicyProvider.PROP_AUTHORIZATIONS_FILE)) { + accessPolicyProperties.put(FileAccessPolicyProvider.PROP_AUTHORIZATIONS_FILE, configurationProperties.get(FileAccessPolicyProvider.PROP_AUTHORIZATIONS_FILE)); + } + if (configurationProperties.containsKey(FileAccessPolicyProvider.PROP_INITIAL_ADMIN_IDENTITY)) { + accessPolicyProperties.put(FileAccessPolicyProvider.PROP_INITIAL_ADMIN_IDENTITY, configurationProperties.get(FileAccessPolicyProvider.PROP_INITIAL_ADMIN_IDENTITY)); + } + if (configurationProperties.containsKey(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE)) { + accessPolicyProperties.put(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE, configurationProperties.get(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE)); + } + + // ensure all nifi identities are seeded into the user provider + configurationProperties.forEach((property, value) -> { + final Matcher matcher = FileAccessPolicyProvider.NIFI_IDENTITY_PATTERN.matcher(property); + if (matcher.matches()) { + accessPolicyProperties.put(property, value); + userGroupProperties.put(property.replace(FileAccessPolicyProvider.PROP_NIFI_IDENTITY_PREFIX, FileUserGroupProvider.PROP_INITIAL_USER_IDENTITY_PREFIX), value); + } + }); + + // ensure the initial admin is seeded into the user provider if appropriate + if (configurationProperties.containsKey(FileAccessPolicyProvider.PROP_INITIAL_ADMIN_IDENTITY)) { + int i = 0; + while (true) { + final String key = FileUserGroupProvider.PROP_INITIAL_USER_IDENTITY_PREFIX + i++; + if (!userGroupProperties.containsKey(key)) { + userGroupProperties.put(key, configurationProperties.get(FileAccessPolicyProvider.PROP_INITIAL_ADMIN_IDENTITY)); + break; + } + } + } + + // configure the user group provider + userGroupProvider.onConfigured(new StandardAuthorizerConfigurationContext(FILE_USER_GROUP_PROVIDER_ID, userGroupProperties)); + + // configure the access policy provider + accessPolicyProvider.onConfigured(new StandardAuthorizerConfigurationContext(FILE_USER_GROUP_PROVIDER_ID, accessPolicyProperties)); + } + + @Override + public void preDestruction() { + + } + + // ------------------ Groups ------------------ + + @Override + public synchronized Group doAddGroup(Group group) throws AuthorizationAccessException { + return userGroupProvider.addGroup(group); + } + + @Override + public Group getGroup(String identifier) throws AuthorizationAccessException { + return userGroupProvider.getGroup(identifier); + } + + @Override + public synchronized Group doUpdateGroup(Group group) throws AuthorizationAccessException { + return userGroupProvider.updateGroup(group); + } + + @Override + public synchronized Group deleteGroup(Group group) throws AuthorizationAccessException { + return userGroupProvider.deleteGroup(group); + } + + @Override + public synchronized Group deleteGroup(String groupId) throws AuthorizationAccessException { + return userGroupProvider.deleteGroup(groupId); + } + + @Override + public Set<Group> getGroups() throws AuthorizationAccessException { + return userGroupProvider.getGroups(); + } + + // ------------------ Users ------------------ + + @Override + public synchronized User doAddUser(final User user) throws AuthorizationAccessException { + return userGroupProvider.addUser(user); + } + + @Override + public User getUser(final String identifier) throws AuthorizationAccessException { + return userGroupProvider.getUser(identifier); + } + + @Override + public User getUserByIdentity(final String identity) throws AuthorizationAccessException { + return userGroupProvider.getUserByIdentity(identity); + } + + @Override + public synchronized User doUpdateUser(final User user) throws AuthorizationAccessException { + return userGroupProvider.updateUser(user); + } + + @Override + public synchronized User deleteUser(final User user) throws AuthorizationAccessException { + return userGroupProvider.deleteUser(user); + } + + @Override + public synchronized User deleteUser(final String userId) throws AuthorizationAccessException { + return userGroupProvider.deleteUser(userId); + } + + @Override + public Set<User> getUsers() throws AuthorizationAccessException { + return userGroupProvider.getUsers(); + } + + // ------------------ AccessPolicies ------------------ + + @Override + public synchronized AccessPolicy doAddAccessPolicy(final AccessPolicy accessPolicy) throws AuthorizationAccessException { + return accessPolicyProvider.addAccessPolicy(accessPolicy); + } + + @Override + public AccessPolicy getAccessPolicy(final String identifier) throws AuthorizationAccessException { + return accessPolicyProvider.getAccessPolicy(identifier); + } + + @Override + public synchronized AccessPolicy updateAccessPolicy(final AccessPolicy accessPolicy) throws AuthorizationAccessException { + return accessPolicyProvider.updateAccessPolicy(accessPolicy); + } + + @Override + public synchronized AccessPolicy deleteAccessPolicy(final AccessPolicy accessPolicy) throws AuthorizationAccessException { + return accessPolicyProvider.deleteAccessPolicy(accessPolicy); + } + + @Override + public synchronized AccessPolicy deleteAccessPolicy(final String accessPolicyIdentifier) throws AuthorizationAccessException { + return accessPolicyProvider.deleteAccessPolicy(accessPolicyIdentifier); + } + + @Override + public Set<AccessPolicy> getAccessPolicies() throws AuthorizationAccessException { + return accessPolicyProvider.getAccessPolicies(); + } + + @AuthorizerContext + public void setNiFiProperties(NiFiRegistryProperties properties) { + userGroupProvider.setNiFiProperties(properties); + accessPolicyProvider.setNiFiProperties(properties); + } + + @Override + public synchronized UsersAndAccessPolicies getUsersAndAccessPolicies() throws AuthorizationAccessException { + final AuthorizationsHolder authorizationsHolder = accessPolicyProvider.getAuthorizationsHolder(); + final UserGroupHolder userGroupHolder = userGroupProvider.getUserGroupHolder(); + + return new UsersAndAccessPolicies() { + @Override + public AccessPolicy getAccessPolicy(String resourceIdentifier, RequestAction action) { + return authorizationsHolder.getAccessPolicy(resourceIdentifier, action); + } + + @Override + public User getUser(String identity) { + return userGroupHolder.getUser(identity); + } + + @Override + public Set<Group> getGroups(String userIdentity) { + return userGroupHolder.getGroups(userIdentity); + } + }; + } + +}