/* * 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.web.security.spring; import java.io.File; import java.io.IOException; 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.List; import java.util.Map; 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.transform.stream.StreamSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.authentication.AuthenticationResponse; import org.apache.nifi.authentication.LoginCredentials; import org.apache.nifi.authentication.LoginIdentityProvider; import org.apache.nifi.authentication.LoginIdentityProviderConfigurationContext; import org.apache.nifi.authentication.LoginIdentityProviderInitializationContext; import org.apache.nifi.authentication.LoginIdentityProviderLookup; import org.apache.nifi.authentication.annotation.LoginIdentityProviderContext; import org.apache.nifi.authentication.exception.ProviderCreationException; import org.apache.nifi.authentication.exception.ProviderDestructionException; import org.apache.nifi.authentication.generated.LoginIdentityProviders; import org.apache.nifi.authentication.generated.Property; import org.apache.nifi.authentication.generated.Provider; import org.apache.nifi.bundle.Bundle; import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.nar.NarCloseable; import org.apache.nifi.properties.AESSensitivePropertyProviderFactory; import org.apache.nifi.properties.NiFiPropertiesLoader; import org.apache.nifi.properties.SensitivePropertyProtectionException; import org.apache.nifi.properties.SensitivePropertyProvider; import org.apache.nifi.properties.SensitivePropertyProviderFactory; import org.apache.nifi.util.NiFiProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.xml.sax.SAXException; /** * */ public class LoginIdentityProviderFactoryBean implements FactoryBean, DisposableBean, LoginIdentityProviderLookup { private static final Logger logger = LoggerFactory.getLogger(LoginIdentityProviderFactoryBean.class); private static final String LOGIN_IDENTITY_PROVIDERS_XSD = "/login-identity-providers.xsd"; private static final String JAXB_GENERATED_PATH = "org.apache.nifi.authentication.generated"; private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext(); private static SensitivePropertyProviderFactory SENSITIVE_PROPERTY_PROVIDER_FACTORY; private static SensitivePropertyProvider SENSITIVE_PROPERTY_PROVIDER; /** * Load the JAXBContext. */ private static JAXBContext initializeJaxbContext() { try { return JAXBContext.newInstance(JAXB_GENERATED_PATH, LoginIdentityProviderFactoryBean.class.getClassLoader()); } catch (JAXBException e) { throw new RuntimeException("Unable to create JAXBContext."); } } private NiFiProperties properties; private LoginIdentityProvider loginIdentityProvider; private final Map<String, LoginIdentityProvider> loginIdentityProviders = new HashMap<>(); @Override public LoginIdentityProvider getLoginIdentityProvider(String identifier) { return loginIdentityProviders.get(identifier); } @Override public Object getObject() throws Exception { if (loginIdentityProvider == null) { // look up the login identity provider to use final String loginIdentityProviderIdentifier = properties.getProperty(NiFiProperties.SECURITY_USER_LOGIN_IDENTITY_PROVIDER); // ensure the login identity provider class name was specified if (StringUtils.isNotBlank(loginIdentityProviderIdentifier)) { final LoginIdentityProviders loginIdentityProviderConfiguration = loadLoginIdentityProvidersConfiguration(); // create each login identity provider for (final Provider provider : loginIdentityProviderConfiguration.getProvider()) { loginIdentityProviders.put(provider.getIdentifier(), createLoginIdentityProvider(provider.getIdentifier(), provider.getClazz())); } // configure each login identity provider for (final Provider provider : loginIdentityProviderConfiguration.getProvider()) { final LoginIdentityProvider instance = loginIdentityProviders.get(provider.getIdentifier()); instance.onConfigured(loadLoginIdentityProviderConfiguration(provider)); } // get the login identity provider instance loginIdentityProvider = getLoginIdentityProvider(loginIdentityProviderIdentifier); // ensure it was found if (loginIdentityProvider == null) { throw new Exception(String.format("The specified login identity provider '%s' could not be found.", loginIdentityProviderIdentifier)); } } } return loginIdentityProvider; } private LoginIdentityProviders loadLoginIdentityProvidersConfiguration() throws Exception { final File loginIdentityProvidersConfigurationFile = properties.getLoginIdentityProviderConfigurationFile(); // 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(LoginIdentityProviders.class.getResource(LOGIN_IDENTITY_PROVIDERS_XSD)); // attempt to unmarshal final Unmarshaller unmarshaller = JAXB_CONTEXT.createUnmarshaller(); unmarshaller.setSchema(schema); final JAXBElement<LoginIdentityProviders> element = unmarshaller.unmarshal(new StreamSource(loginIdentityProvidersConfigurationFile), LoginIdentityProviders.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 LoginIdentityProvider createLoginIdentityProvider(final String identifier, final String loginIdentityProviderClassName) throws Exception { // get the classloader for the specified login identity provider final List<Bundle> loginIdentityProviderBundles = ExtensionManager.getBundles(loginIdentityProviderClassName); if (loginIdentityProviderBundles.size() == 0) { throw new Exception(String.format("The specified login identity provider class '%s' is not known to this nifi.", loginIdentityProviderClassName)); } if (loginIdentityProviderBundles.size() > 1) { throw new Exception(String.format("Multiple bundles found for the specified login identity provider class '%s', only one is allowed.", loginIdentityProviderClassName)); } final Bundle loginIdentityProviderBundle = loginIdentityProviderBundles.get(0); final ClassLoader loginIdentityProviderClassLoader = loginIdentityProviderBundle.getClassLoader(); // get the current context classloader final ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); final LoginIdentityProvider instance; try { // set the appropriate class loader Thread.currentThread().setContextClassLoader(loginIdentityProviderClassLoader); // attempt to load the class Class<?> rawLoginIdentityProviderClass = Class.forName(loginIdentityProviderClassName, true, loginIdentityProviderClassLoader); Class<? extends LoginIdentityProvider> loginIdentityProviderClass = rawLoginIdentityProviderClass.asSubclass(LoginIdentityProvider.class); // otherwise create a new instance Constructor constructor = loginIdentityProviderClass.getConstructor(); instance = (LoginIdentityProvider) constructor.newInstance(); // method injection performMethodInjection(instance, loginIdentityProviderClass); // field injection performFieldInjection(instance, loginIdentityProviderClass); // call post construction lifecycle event instance.initialize(new StandardLoginIdentityProviderInitializationContext(identifier, this)); } finally { if (currentClassLoader != null) { Thread.currentThread().setContextClassLoader(currentClassLoader); } } return withNarLoader(instance); } private LoginIdentityProviderConfigurationContext 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 StandardLoginIdentityProviderConfigurationContext(provider.getIdentifier(), providerProperties); } private String decryptValue(String cipherText, String encryptionScheme) throws SensitivePropertyProtectionException { initializeSensitivePropertyProvider(encryptionScheme); return SENSITIVE_PROPERTY_PROVIDER.unprotect(cipherText); } private static void initializeSensitivePropertyProvider(String encryptionScheme) throws SensitivePropertyProtectionException { if (SENSITIVE_PROPERTY_PROVIDER == null || !SENSITIVE_PROPERTY_PROVIDER.getIdentifierKey().equalsIgnoreCase(encryptionScheme)) { try { String keyHex = getMasterKey(); SENSITIVE_PROPERTY_PROVIDER_FACTORY = new AESSensitivePropertyProviderFactory(keyHex); SENSITIVE_PROPERTY_PROVIDER = SENSITIVE_PROPERTY_PROVIDER_FACTORY.getProvider(); } catch (IOException e) { logger.error("Error extracting master key from bootstrap.conf for login identity provider decryption", e); throw new SensitivePropertyProtectionException("Could not read master key from bootstrap.conf"); } } } private static String getMasterKey() throws IOException { return NiFiPropertiesLoader.extractKeyFromBootstrapFile(); } private void performMethodInjection(final LoginIdentityProvider instance, final Class loginIdentityProviderClass) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { for (final Method method : loginIdentityProviderClass.getMethods()) { if (method.isAnnotationPresent(LoginIdentityProviderContext.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 (NiFiProperties.class.isAssignableFrom(argumentType)) { // nifi properties injection method.invoke(instance, properties); } } } finally { method.setAccessible(isAccessible); } } } final Class parentClass = loginIdentityProviderClass.getSuperclass(); if (parentClass != null && LoginIdentityProvider.class.isAssignableFrom(parentClass)) { performMethodInjection(instance, parentClass); } } private void performFieldInjection(final LoginIdentityProvider instance, final Class loginIdentityProviderClass) throws IllegalArgumentException, IllegalAccessException { for (final Field field : loginIdentityProviderClass.getDeclaredFields()) { if (field.isAnnotationPresent(LoginIdentityProviderContext.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 (NiFiProperties.class.isAssignableFrom(fieldType)) { // nifi properties injection field.set(instance, properties); } } } finally { field.setAccessible(isAccessible); } } } final Class parentClass = loginIdentityProviderClass.getSuperclass(); if (parentClass != null && LoginIdentityProvider.class.isAssignableFrom(parentClass)) { performFieldInjection(instance, parentClass); } } private LoginIdentityProvider withNarLoader(final LoginIdentityProvider baseProvider) { return new LoginIdentityProvider() { @Override public AuthenticationResponse authenticate(LoginCredentials credentials) { try (final NarCloseable narCloseable = NarCloseable.withNarLoader()) { return baseProvider.authenticate(credentials); } } @Override public void initialize(LoginIdentityProviderInitializationContext initializationContext) throws ProviderCreationException { try (final NarCloseable narCloseable = NarCloseable.withNarLoader()) { baseProvider.initialize(initializationContext); } } @Override public void onConfigured(LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException { try (final NarCloseable narCloseable = NarCloseable.withNarLoader()) { baseProvider.onConfigured(configurationContext); } } @Override public void preDestruction() throws ProviderDestructionException { try (final NarCloseable narCloseable = NarCloseable.withNarLoader()) { baseProvider.preDestruction(); } } }; } @Override public Class getObjectType() { return LoginIdentityProvider.class; } @Override public boolean isSingleton() { return true; } @Override public void destroy() throws Exception { if (loginIdentityProvider != null) { loginIdentityProvider.preDestruction(); } } public void setProperties(NiFiProperties properties) { this.properties = properties; } }