blob: 3c2a3f4b02ca19d9351f860375617c9ab62a3bd1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.registry.security.authentication;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.registry.extension.ExtensionManager;
import org.apache.nifi.registry.properties.NiFiRegistryProperties;
import org.apache.nifi.registry.properties.SensitivePropertyProtectionException;
import org.apache.nifi.registry.properties.SensitivePropertyProvider;
import org.apache.nifi.registry.security.authentication.annotation.IdentityProviderContext;
import org.apache.nifi.registry.security.authentication.generated.IdentityProviders;
import org.apache.nifi.registry.security.authentication.generated.Property;
import org.apache.nifi.registry.security.authentication.generated.Provider;
import org.apache.nifi.registry.security.util.XmlUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.lang.Nullable;
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.Unmarshaller;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class IdentityProviderFactory implements IdentityProviderLookup, DisposableBean {
private static final Logger logger = LoggerFactory.getLogger(IdentityProviderFactory.class);
private static final String LOGIN_IDENTITY_PROVIDERS_XSD = "/identity-providers.xsd";
private static final String JAXB_GENERATED_PATH = "org.apache.nifi.registry.security.authentication.generated";
private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext();
private static JAXBContext initializeJaxbContext() {
try {
return JAXBContext.newInstance(JAXB_GENERATED_PATH, IdentityProviderFactory.class.getClassLoader());
} catch (JAXBException e) {
throw new RuntimeException("Unable to create JAXBContext.");
}
}
private NiFiRegistryProperties properties;
private ExtensionManager extensionManager;
private SensitivePropertyProvider sensitivePropertyProvider;
private IdentityProvider identityProvider;
private final Map<String, IdentityProvider> identityProviders = new HashMap<>();
@Autowired
public IdentityProviderFactory(
final NiFiRegistryProperties properties,
final ExtensionManager extensionManager,
@Nullable final SensitivePropertyProvider sensitivePropertyProvider) {
this.properties = properties;
this.extensionManager = extensionManager;
this.sensitivePropertyProvider = sensitivePropertyProvider;
if (this.properties == null) {
throw new IllegalStateException("NiFiRegistryProperties cannot be null");
}
if (this.extensionManager == null) {
throw new IllegalStateException("ExtensionManager cannot be null");
}
}
@Override
public IdentityProvider getIdentityProvider(String identifier) {
return identityProviders.get(identifier);
}
@Bean
@Primary
public IdentityProvider getIdentityProvider() throws Exception {
if (identityProvider == null) {
// look up the login identity provider to use
final String loginIdentityProviderIdentifier = properties.getProperty(NiFiRegistryProperties.SECURITY_IDENTITY_PROVIDER);
// ensure the login identity provider class name was specified
if (StringUtils.isNotBlank(loginIdentityProviderIdentifier)) {
final IdentityProviders loginIdentityProviderConfiguration = loadLoginIdentityProvidersConfiguration();
// create each login identity provider
for (final Provider provider : loginIdentityProviderConfiguration.getProvider()) {
identityProviders.put(provider.getIdentifier(), createLoginIdentityProvider(provider.getIdentifier(), provider.getClazz()));
}
// configure each login identity provider
for (final Provider provider : loginIdentityProviderConfiguration.getProvider()) {
final IdentityProvider instance = identityProviders.get(provider.getIdentifier());
instance.onConfigured(loadLoginIdentityProviderConfiguration(provider));
}
// get the login identity provider instance
identityProvider = getIdentityProvider(loginIdentityProviderIdentifier);
// ensure it was found
if (identityProvider == null) {
throw new Exception(String.format("The specified login identity provider '%s' could not be found.", loginIdentityProviderIdentifier));
}
}
}
return identityProvider;
}
@Override
public void destroy() throws Exception {
if (identityProviders != null) {
identityProviders.entrySet().stream().forEach(e -> e.getValue().preDestruction());
}
}
private IdentityProviders loadLoginIdentityProvidersConfiguration() throws Exception {
final File loginIdentityProvidersConfigurationFile = properties.getIdentityProviderConfigurationFile();
// load the users from the specified file
if (loginIdentityProvidersConfigurationFile.exists()) {
try {
// find the schema
final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
final Schema schema = schemaFactory.newSchema(IdentityProviders.class.getResource(LOGIN_IDENTITY_PROVIDERS_XSD));
// attempt to unmarshal
XMLStreamReader xsr = XmlUtils.createSafeReader(new StreamSource(loginIdentityProvidersConfigurationFile));
final Unmarshaller unmarshaller = JAXB_CONTEXT.createUnmarshaller();
unmarshaller.setSchema(schema);
final JAXBElement<IdentityProviders> element = unmarshaller.unmarshal(xsr, IdentityProviders.class);
return element.getValue();
} catch (SAXException | JAXBException e) {
throw new Exception("Unable to load the login identity provider configuration file at: " + loginIdentityProvidersConfigurationFile.getAbsolutePath());
}
} else {
throw new Exception("Unable to find the login identity provider configuration file at " + loginIdentityProvidersConfigurationFile.getAbsolutePath());
}
}
private IdentityProvider createLoginIdentityProvider(final String identifier, final String loginIdentityProviderClassName) throws Exception {
final IdentityProvider instance;
final ClassLoader classLoader = extensionManager.getExtensionClassLoader(loginIdentityProviderClassName);
if (classLoader == null) {
throw new IllegalStateException("Extension not found in any of the configured class loaders: " + loginIdentityProviderClassName);
}
// attempt to load the class
Class<?> rawLoginIdentityProviderClass = Class.forName(loginIdentityProviderClassName, true, classLoader);
Class<? extends IdentityProvider> loginIdentityProviderClass = rawLoginIdentityProviderClass.asSubclass(IdentityProvider.class);
// otherwise create a new instance
Constructor constructor = loginIdentityProviderClass.getConstructor();
instance = (IdentityProvider) constructor.newInstance();
// method injection
performMethodInjection(instance, loginIdentityProviderClass);
// field injection
performFieldInjection(instance, loginIdentityProviderClass);
return instance;
}
private IdentityProviderConfigurationContext loadLoginIdentityProviderConfiguration(final Provider provider) {
final Map<String, String> providerProperties = new HashMap<>();
for (final Property property : provider.getProperty()) {
if (!StringUtils.isBlank(property.getEncryption())) {
String decryptedValue = decryptValue(property.getValue(), property.getEncryption());
providerProperties.put(property.getName(), decryptedValue);
} else {
providerProperties.put(property.getName(), property.getValue());
}
}
return new StandardIdentityProviderConfigurationContext(provider.getIdentifier(), this, providerProperties);
}
private void performMethodInjection(final IdentityProvider instance, final Class loginIdentityProviderClass)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
for (final Method method : loginIdentityProviderClass.getMethods()) {
if (method.isAnnotationPresent(IdentityProviderContext.class)) {
// make the method accessible
final boolean isAccessible = method.isAccessible();
method.setAccessible(true);
try {
final Class<?>[] argumentTypes = method.getParameterTypes();
// look for setters (single argument)
if (argumentTypes.length == 1) {
final Class<?> argumentType = argumentTypes[0];
// look for well known types
if (NiFiRegistryProperties.class.isAssignableFrom(argumentType)) {
// nifi properties injection
method.invoke(instance, properties);
}
}
} finally {
method.setAccessible(isAccessible);
}
}
}
final Class parentClass = loginIdentityProviderClass.getSuperclass();
if (parentClass != null && IdentityProvider.class.isAssignableFrom(parentClass)) {
performMethodInjection(instance, parentClass);
}
}
private void performFieldInjection(final IdentityProvider instance, final Class loginIdentityProviderClass) throws IllegalArgumentException, IllegalAccessException {
for (final Field field : loginIdentityProviderClass.getDeclaredFields()) {
if (field.isAnnotationPresent(IdentityProviderContext.class)) {
// make the method accessible
final boolean isAccessible = field.isAccessible();
field.setAccessible(true);
try {
// get the type
final Class<?> fieldType = field.getType();
// only consider this field if it isn't set yet
if (field.get(instance) == null) {
// look for well known types
if (NiFiRegistryProperties.class.isAssignableFrom(fieldType)) {
// nifi properties injection
field.set(instance, properties);
}
}
} finally {
field.setAccessible(isAccessible);
}
}
}
final Class parentClass = loginIdentityProviderClass.getSuperclass();
if (parentClass != null && IdentityProvider.class.isAssignableFrom(parentClass)) {
performFieldInjection(instance, parentClass);
}
}
private String decryptValue(String cipherText, String encryptionScheme) throws SensitivePropertyProtectionException {
if (sensitivePropertyProvider == null) {
throw new SensitivePropertyProtectionException("Sensitive Property Provider dependency was never wired, so protected " +
"properties cannot be decrypted. This usually indicates that a master key for this NiFi Registry was not " +
"detected and configured during the bootstrap startup sequence. Contact the system administrator.");
}
if (!sensitivePropertyProvider.getIdentifierKey().equalsIgnoreCase(encryptionScheme)) {
throw new SensitivePropertyProtectionException("Identity Provider configuration XML was protected using " +
encryptionScheme +
", but the configured Sensitive Property Provider supports " +
sensitivePropertyProvider.getIdentifierKey() +
". Cannot configure this Identity Provider due to failing to decrypt protected configuration properties.");
}
return sensitivePropertyProvider.unprotect(cipherText);
}
}