http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBean.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBean.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBean.java new file mode 100644 index 0000000..a7da2eb --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBean.java @@ -0,0 +1,334 @@ +/* + * 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.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; +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.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.generated.LoginIdentityProviders; +import org.apache.nifi.authentication.generated.Property; +import org.apache.nifi.authentication.generated.Provider; +import org.apache.nifi.authorization.exception.ProviderCreationException; +import org.apache.nifi.authorization.exception.ProviderDestructionException; +import org.apache.nifi.nar.ExtensionManager; +import org.apache.nifi.nar.NarCloseable; +import org.apache.nifi.util.NiFiProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.xml.sax.SAXException; + +/** + * + */ +public class LoginIdentityProviderFactoryBean implements FactoryBean, ApplicationContextAware, 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(); + + /** + * 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 ApplicationContext context; + 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.getLoginIdentityProviderConfiguraitonFile(); + + // 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 ClassLoader loginIdentityProviderClassLoader = ExtensionManager.getClassLoader(loginIdentityProviderClassName); + if (loginIdentityProviderClassLoader == null) { + throw new Exception(String.format("The specified login identity provider class '%s' is not known to this nifi.", loginIdentityProviderClassName)); + } + + // 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()) { + providerProperties.put(property.getName(), property.getValue()); + } + + return new StandardLoginIdentityProviderConfigurationContext(provider.getIdentifier(), providerProperties); + } + + 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 boolean supportsRegistration() { + try (final NarCloseable narCloseable = NarCloseable.withNarLoader()) { + return baseProvider.supportsRegistration(); + } + } + + @Override + public void register(LoginCredentials credentials) { + try (final NarCloseable narCloseable = NarCloseable.withNarLoader()) { + baseProvider.register(credentials); + } + } + + @Override + public boolean 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; + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + this.context = context; + } +}
http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/StandardLoginIdentityProviderConfigurationContext.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/StandardLoginIdentityProviderConfigurationContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/StandardLoginIdentityProviderConfigurationContext.java new file mode 100644 index 0000000..5c662c7 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/StandardLoginIdentityProviderConfigurationContext.java @@ -0,0 +1,51 @@ +/* + * 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.util.Collections; +import java.util.Map; +import org.apache.nifi.authentication.LoginIdentityProviderConfigurationContext; + +/** + * + */ +public class StandardLoginIdentityProviderConfigurationContext implements LoginIdentityProviderConfigurationContext { + + private final String identifier; + private final Map<String, String> properties; + + public StandardLoginIdentityProviderConfigurationContext(String identifier, Map<String, String> properties) { + this.identifier = identifier; + this.properties = properties; + } + + @Override + public String getIdentifier() { + return identifier; + } + + @Override + public Map<String, String> getProperties() { + return Collections.unmodifiableMap(properties); + } + + @Override + public String getProperty(String property) { + return properties.get(property); + } + +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/StandardLoginIdentityProviderInitializationContext.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/StandardLoginIdentityProviderInitializationContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/StandardLoginIdentityProviderInitializationContext.java new file mode 100644 index 0000000..af54df9 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/StandardLoginIdentityProviderInitializationContext.java @@ -0,0 +1,45 @@ +/* + * 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 org.apache.nifi.authentication.LoginIdentityProviderInitializationContext; +import org.apache.nifi.authentication.LoginIdentityProviderLookup; + +/** + * + */ +public class StandardLoginIdentityProviderInitializationContext implements LoginIdentityProviderInitializationContext { + + private final String identifier; + private final LoginIdentityProviderLookup lookup; + + public StandardLoginIdentityProviderInitializationContext(String identifier, final LoginIdentityProviderLookup lookup) { + this.identifier = identifier; + this.lookup = lookup; + } + + @Override + public String getIdentifier() { + return identifier; + } + + @Override + public LoginIdentityProviderLookup getAuthorityProviderLookup() { + return lookup; + } + +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationRequestToken.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationRequestToken.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationRequestToken.java new file mode 100644 index 0000000..d1ca4d2 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationRequestToken.java @@ -0,0 +1,37 @@ +/* + * 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.token; + +import java.util.List; +import org.apache.nifi.authentication.LoginCredentials; + +/** + * This is an Authentication Token for requesting an ID token for accessing the NIFI REST API. + */ +public class LoginAuthenticationRequestToken extends NiFiAuthenticationRequestToken { + + final LoginCredentials credentials; + + public LoginAuthenticationRequestToken(final List<String> proxyChain, final LoginCredentials credentials) { + super(proxyChain); + this.credentials = credentials; + } + + public LoginCredentials getLoginCredentials() { + return credentials; + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java new file mode 100644 index 0000000..528b60b --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.token; + +import org.apache.nifi.authentication.LoginCredentials; +import org.springframework.security.authentication.AbstractAuthenticationToken; + +/** + * This is an Authentication Token for logging in. Once a user is authenticated, they can be issues an ID token. + */ +public class LoginAuthenticationToken extends AbstractAuthenticationToken { + + final LoginCredentials credentials; + + public LoginAuthenticationToken(final LoginCredentials credentials) { + super(null); + setAuthenticated(true); + this.credentials = credentials; + } + + public LoginCredentials getLoginCredentials() { + return credentials; + } + + @Override + public Object getCredentials() { + return credentials.getPassword(); + } + + @Override + public Object getPrincipal() { + return credentials.getUsername(); + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NewAccountAuthenticationRequestToken.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NewAccountAuthenticationRequestToken.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NewAccountAuthenticationRequestToken.java new file mode 100644 index 0000000..6fee4ec --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NewAccountAuthenticationRequestToken.java @@ -0,0 +1,40 @@ +/* + * 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.token; + +import org.apache.nifi.web.security.user.NewAccountRequest; + +/** + * This is an Authentication Token for a user that is requesting authentication in order to submit a new account request. + */ +public class NewAccountAuthenticationRequestToken extends NiFiAuthenticationRequestToken { + + final NewAccountRequest newAccountRequest; + + public NewAccountAuthenticationRequestToken(final NewAccountRequest newAccountRequest) { + super(newAccountRequest.getChain()); + this.newAccountRequest = newAccountRequest; + } + + public String getJustification() { + return newAccountRequest.getJustification(); + } + + public NewAccountRequest getNewAccountRequest() { + return newAccountRequest; + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NewAccountAuthenticationToken.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NewAccountAuthenticationToken.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NewAccountAuthenticationToken.java new file mode 100644 index 0000000..5fe3a1d --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NewAccountAuthenticationToken.java @@ -0,0 +1,46 @@ +/* + * 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.token; + +import org.apache.nifi.web.security.user.NewAccountRequest; +import org.springframework.security.authentication.AbstractAuthenticationToken; + +/** + * This is an Authentication Token for a user that has been authenticated but is not authorized to access the NiFi APIs. Typically, this authentication token is used successfully when requesting a + * NiFi account. Requesting any other endpoint would be rejected due to lack of roles. + */ +public class NewAccountAuthenticationToken extends AbstractAuthenticationToken { + + final NewAccountRequest newAccountRequest; + + public NewAccountAuthenticationToken(final NewAccountRequest newAccountRequest) { + super(null); + super.setAuthenticated(true); + this.newAccountRequest = newAccountRequest; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return newAccountRequest; + } + +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NiFiAuthenticationRequestToken.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NiFiAuthenticationRequestToken.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NiFiAuthenticationRequestToken.java new file mode 100644 index 0000000..3ae6491 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NiFiAuthenticationRequestToken.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.web.security.token; + +import java.util.Collections; +import java.util.List; +import org.springframework.security.authentication.AbstractAuthenticationToken; + +/** + * An authentication token that is used as an authentication request. The request chain is specified during creation and is used authenticate the user(s). If the user is authenticated, the token is + * used to authorized the user(s). + */ +public class NiFiAuthenticationRequestToken extends AbstractAuthenticationToken { + + private final List<String> chain; + + public NiFiAuthenticationRequestToken(final List<String> chain) { + super(null); + this.chain = chain; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return chain; + } + + public List<String> getChain() { + return Collections.unmodifiableList(chain); + } + + @Override + public final void setAuthenticated(boolean authenticated) { + throw new IllegalArgumentException("Cannot change the authenticated state."); + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NiFiAuthorizationToken.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NiFiAuthorizationToken.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NiFiAuthorizationToken.java new file mode 100644 index 0000000..0cb0353 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NiFiAuthorizationToken.java @@ -0,0 +1,50 @@ +/* + * 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.token; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.userdetails.UserDetails; + +/** + * An authentication token that represents an Authenticated and Authorized user of the NiFi Apis. The authorities are based off the specified UserDetails. + */ +public class NiFiAuthorizationToken extends AbstractAuthenticationToken { + + final UserDetails nifiUserDetails; + + public NiFiAuthorizationToken(final UserDetails nifiUserDetails) { + super(nifiUserDetails.getAuthorities()); + super.setAuthenticated(true); + setDetails(nifiUserDetails); + this.nifiUserDetails = nifiUserDetails; + } + + @Override + public Object getCredentials() { + return nifiUserDetails.getPassword(); + } + + @Override + public Object getPrincipal() { + return nifiUserDetails; + } + + @Override + public final void setAuthenticated(boolean authenticated) { + throw new IllegalArgumentException("Cannot change the authenticated state."); + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/user/NewAccountRequest.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/user/NewAccountRequest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/user/NewAccountRequest.java new file mode 100644 index 0000000..3ec147a --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/user/NewAccountRequest.java @@ -0,0 +1,47 @@ +/* + * 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.user; + +import java.util.List; + +/** + * + */ +public class NewAccountRequest { + + private final List<String> chain; + private final String justification; + + public NewAccountRequest(final List<String> chain, final String justification) { + this.chain = chain; + this.justification = justification; + } + + public List<String> getChain() { + return chain; + } + + public String getJustification() { + return justification; + } + + public String getUsername() { + // the end user is the first item in the chain + return chain.get(0); + } + +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/user/NiFiUserDetails.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/user/NiFiUserDetails.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/user/NiFiUserDetails.java index c69b1e6..5645f78 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/user/NiFiUserDetails.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/user/NiFiUserDetails.java @@ -76,7 +76,6 @@ public class NiFiUserDetails implements UserDetails { return user.getDn(); } - // TODO: not sure how to handle these yet @Override public boolean isAccountNonExpired() { return true; http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/user/NiFiUserUtils.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/user/NiFiUserUtils.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/user/NiFiUserUtils.java index bf1fe43..341663e 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/user/NiFiUserUtils.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/user/NiFiUserUtils.java @@ -25,8 +25,7 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; /** - * Utility methods for retrieving information about the current application - * user. + * Utility methods for retrieving information about the current application user. * */ public final class NiFiUserUtils { @@ -58,8 +57,7 @@ public final class NiFiUserUtils { } /** - * Returns the current NiFiUser or null if the current user is not a - * NiFiUser. + * Returns the current NiFiUser or null if the current user is not a NiFiUser. * * @return user */ @@ -79,6 +77,27 @@ public final class NiFiUserUtils { return user; } + /** + * Returns the NewAccountRequest or null if this is not a new account request. + * + * @return new account request + */ + public static NewAccountRequest getNewAccountRequest() { + NewAccountRequest newAccountRequest = null; + + // obtain the principal in the current authentication + final SecurityContext context = SecurityContextHolder.getContext(); + final Authentication authentication = context.getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof NewAccountRequest) { + newAccountRequest = (NewAccountRequest) principal; + } + } + + return newAccountRequest; + } + public static String getNiFiUserName() { // get the nifi user to extract the username NiFiUser user = NiFiUserUtils.getNiFiUser(); http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilter.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilter.java index 72baecb..e57a5ac 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilter.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilter.java @@ -17,114 +17,40 @@ package org.apache.nifi.web.security.x509; import org.apache.nifi.web.security.x509.ocsp.OcspCertificateValidator; -import java.io.IOException; -import java.io.PrintWriter; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; +import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.nifi.admin.service.AdministrationException; -import org.apache.nifi.admin.service.UserService; -import org.apache.nifi.web.security.DnUtils; -import org.apache.nifi.web.security.UntrustedProxyException; -import org.apache.nifi.util.NiFiProperties; -import org.apache.commons.lang3.StringUtils; -import org.springframework.security.authentication.AccountStatusException; -import org.springframework.security.authentication.AuthenticationServiceException; +import org.apache.nifi.web.security.NiFiAuthenticationFilter; +import org.apache.nifi.web.security.ProxiedEntitiesUtils; +import org.apache.nifi.web.security.token.NewAccountAuthenticationRequestToken; +import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken; +import org.apache.nifi.web.security.user.NewAccountRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; /** - * Custom X509 filter that will inspect the HTTP headers for a proxied user - * before extracting the user details from the client certificate. + * Custom X509 filter that will inspect the HTTP headers for a proxied user before extracting the user details from the client certificate. */ -public class X509AuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter { +public class X509AuthenticationFilter extends NiFiAuthenticationFilter { - public static final String PROXY_ENTITIES_CHAIN = "X-ProxiedEntitiesChain"; - public static final String PROXY_ENTITIES_ACCEPTED = "X-ProxiedEntitiesAccepted"; - public static final String PROXY_ENTITIES_DETAILS = "X-ProxiedEntitiesDetails"; + private static final Logger logger = LoggerFactory.getLogger(X509AuthenticationFilter.class); - private final X509CertificateExtractor certificateExtractor = new X509CertificateExtractor(); - private final X509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor(); + private X509PrincipalExtractor principalExtractor; + private X509CertificateExtractor certificateExtractor; private OcspCertificateValidator certificateValidator; - private NiFiProperties properties; - private UserService userService; @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - final HttpServletResponse httpResponse = (HttpServletResponse) response; - - // determine if this request is attempting to create a new account - if (isNewAccountRequest((HttpServletRequest) request)) { - // determine if this nifi supports new account requests - if (properties.getSupportNewAccountRequests()) { - // ensure there is a certificate in the request - X509Certificate certificate = certificateExtractor.extractClientCertificate((HttpServletRequest) request); - if (certificate != null) { - // extract the principal from the certificate - Object certificatePrincipal = principalExtractor.extractPrincipal(certificate); - String principal = certificatePrincipal.toString(); - - // log the new user account request - logger.info("Requesting new user account for " + principal); - - try { - // get the justification - String justification = request.getParameter("justification"); - if (justification == null) { - justification = StringUtils.EMPTY; - } - - // create the pending user account - userService.createPendingUserAccount(principal, justification); - - // generate a response - httpResponse.setStatus(HttpServletResponse.SC_CREATED); - httpResponse.setContentType("text/plain"); - - // write the response message - PrintWriter out = response.getWriter(); - out.println("Not authorized. User account created. Authorization pending."); - } catch (IllegalArgumentException iae) { - handleUserServiceError((HttpServletRequest) request, httpResponse, HttpServletResponse.SC_BAD_REQUEST, iae.getMessage()); - } catch (AdministrationException ae) { - handleUserServiceError((HttpServletRequest) request, httpResponse, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ae.getMessage()); - } - } else { - // can this really happen? - handleMissingCertificate((HttpServletRequest) request, httpResponse); - } - } else { - handleUserServiceError((HttpServletRequest) request, httpResponse, HttpServletResponse.SC_NOT_FOUND, "This NiFi does not support new account requests."); - } - } else { - try { - // this not a request to create a user account - try to authorize - super.doFilter(request, response, chain); - } catch (AuthenticationException ae) { - // continue the filter chain since anonymous access should be supported - if (!properties.getNeedClientAuth()) { - chain.doFilter(request, response); - } else { - // create an appropriate response for the given exception - handleUnsuccessfulAuthentication((HttpServletRequest) request, httpResponse, ae); - } - } + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { + // only suppport x509 login when running securely + if (!request.isSecure()) { + return null; } - } - - @Override - protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) { - String principal; - + // extract the cert X509Certificate certificate = certificateExtractor.extractClientCertificate(request); @@ -135,7 +61,7 @@ public class X509AuthenticationFilter extends AbstractPreAuthenticatedProcessing // extract the principal Object certificatePrincipal = principalExtractor.extractPrincipal(certificate); - principal = DnUtils.formatProxyDn(certificatePrincipal.toString()); + final String principal = ProxiedEntitiesUtils.formatProxyDn(certificatePrincipal.toString()); try { // ensure the cert is valid @@ -167,151 +93,25 @@ public class X509AuthenticationFilter extends AbstractPreAuthenticatedProcessing return null; } - // look for a proxied user - if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) { - principal = request.getHeader(PROXY_ENTITIES_CHAIN) + principal; - } - - // log the request attempt - response details will be logged later - logger.info(String.format("Attempting request for (%s) %s %s (source ip: %s)", principal, request.getMethod(), - request.getRequestURL().toString(), request.getRemoteAddr())); - - return principal; - } - - @Override - protected Object getPreAuthenticatedCredentials(HttpServletRequest request) { - return certificateExtractor.extractClientCertificate(request); - } - - @Override - protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) { - if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) { - response.setHeader(PROXY_ENTITIES_ACCEPTED, Boolean.TRUE.toString()); - } - super.successfulAuthentication(request, response, authResult); - } - - @Override - protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) { - if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) { - response.setHeader(PROXY_ENTITIES_DETAILS, failed.getMessage()); - } - super.unsuccessfulAuthentication(request, response, failed); - } - - /** - * Determines if the specified request is attempting to register a new user - * account. - * - * @param request http request - * @return true if new user - */ - private boolean isNewAccountRequest(HttpServletRequest request) { - if ("POST".equalsIgnoreCase(request.getMethod())) { - String path = request.getPathInfo(); - if (StringUtils.isNotBlank(path)) { - if ("/controller/users".equals(path)) { - return true; - } - } - } - return false; - } - - /** - * Handles requests that were unable to be authorized. - * - * @param request request - * @param response response - * @param ae ex - * @throws IOException ex - */ - private void handleUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException ae) throws IOException { - // set the response status - response.setContentType("text/plain"); - - // write the response message - PrintWriter out = response.getWriter(); - - // use the type of authentication exception to determine the response code - if (ae instanceof UsernameNotFoundException) { - if (properties.getSupportNewAccountRequests()) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - out.println("Not authorized."); - } else { - response.setStatus(HttpServletResponse.SC_FORBIDDEN); - out.println("Access is denied."); - } - } else if (ae instanceof AccountStatusException) { - response.setStatus(HttpServletResponse.SC_FORBIDDEN); - out.println(ae.getMessage()); - } else if (ae instanceof UntrustedProxyException) { - response.setStatus(HttpServletResponse.SC_FORBIDDEN); - out.println(ae.getMessage()); - } else if (ae instanceof AuthenticationServiceException) { - logger.error(String.format("Unable to authorize: %s", ae.getMessage()), ae); - response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - out.println(String.format("Unable to authorize: %s", ae.getMessage())); + final List<String> proxyChain = ProxiedEntitiesUtils.buildProxyChain(request, principal); + if (isNewAccountRequest(request)) { + return new NewAccountAuthenticationRequestToken(new NewAccountRequest(proxyChain, getJustification(request))); } else { - logger.error(String.format("Unable to authorize: %s", ae.getMessage()), ae); - response.setStatus(HttpServletResponse.SC_FORBIDDEN); - out.println("Access is denied."); - } - - // log the failure - logger.info(String.format("Rejecting access to web api: %s", ae.getMessage())); - - // optionally log the stack trace - if (logger.isDebugEnabled()) { - logger.debug(StringUtils.EMPTY, ae); + return new NiFiAuthenticationRequestToken(proxyChain); } } - private void handleUserServiceError(HttpServletRequest request, HttpServletResponse response, int responseCode, String message) throws IOException { - // set the response status - response.setContentType("text/plain"); - response.setStatus(responseCode); - - // write the response message - PrintWriter out = response.getWriter(); - out.println(message); - - // log the failure - logger.info(String.format("Unable to process request because %s", message)); - } - - /** - * Handles requests that failed because they were bad input. - * - * @param request request - * @param response response - * @throws IOException ioe - */ - private void handleMissingCertificate(HttpServletRequest request, HttpServletResponse response) throws IOException { - // set the response status - response.setContentType("text/plain"); - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - - // write the response message - PrintWriter out = response.getWriter(); - out.println("Unable to process request because the user certificate was not specified."); - - // log the failure - logger.info("Unable to process request because the user certificate was not specified."); - } - /* setters */ - public void setProperties(NiFiProperties properties) { - this.properties = properties; + public void setCertificateValidator(OcspCertificateValidator certificateValidator) { + this.certificateValidator = certificateValidator; } - public void setUserService(UserService userService) { - this.userService = userService; + public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) { + this.principalExtractor = principalExtractor; } - public void setCertificateValidator(OcspCertificateValidator certificateValidator) { - this.certificateValidator = certificateValidator; + public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) { + this.certificateExtractor = certificateExtractor; } } http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilterOld.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilterOld.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilterOld.java new file mode 100644 index 0000000..711639b --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilterOld.java @@ -0,0 +1,317 @@ +/* + * 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.x509; + +import org.apache.nifi.web.security.x509.ocsp.OcspCertificateValidator; +import java.io.IOException; +import java.io.PrintWriter; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.nifi.admin.service.AdministrationException; +import org.apache.nifi.admin.service.UserService; +import org.apache.nifi.web.security.ProxiedEntitiesUtils; +import org.apache.nifi.web.security.UntrustedProxyException; +import org.apache.nifi.util.NiFiProperties; +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.authentication.AccountStatusException; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; +import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; + +/** + * Custom X509 filter that will inspect the HTTP headers for a proxied user + * before extracting the user details from the client certificate. + */ +public class X509AuthenticationFilterOld extends AbstractPreAuthenticatedProcessingFilter { + + public static final String PROXY_ENTITIES_CHAIN = "X-ProxiedEntitiesChain"; + public static final String PROXY_ENTITIES_ACCEPTED = "X-ProxiedEntitiesAccepted"; + public static final String PROXY_ENTITIES_DETAILS = "X-ProxiedEntitiesDetails"; + + private final X509CertificateExtractor certificateExtractor = new X509CertificateExtractor(); + private final X509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor(); + private OcspCertificateValidator certificateValidator; + private NiFiProperties properties; + private UserService userService; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + final HttpServletResponse httpResponse = (HttpServletResponse) response; + + // determine if this request is attempting to create a new account + if (isNewAccountRequest((HttpServletRequest) request)) { + // determine if this nifi supports new account requests + if (properties.getSupportNewAccountRequests()) { + // ensure there is a certificate in the request + X509Certificate certificate = certificateExtractor.extractClientCertificate((HttpServletRequest) request); + if (certificate != null) { + // extract the principal from the certificate + Object certificatePrincipal = principalExtractor.extractPrincipal(certificate); + String principal = certificatePrincipal.toString(); + + // log the new user account request + logger.info("Requesting new user account for " + principal); + + try { + // get the justification + String justification = request.getParameter("justification"); + if (justification == null) { + justification = StringUtils.EMPTY; + } + + // create the pending user account + userService.createPendingUserAccount(principal, justification); + + // generate a response + httpResponse.setStatus(HttpServletResponse.SC_CREATED); + httpResponse.setContentType("text/plain"); + + // write the response message + PrintWriter out = response.getWriter(); + out.println("Not authorized. User account created. Authorization pending."); + } catch (IllegalArgumentException iae) { + handleUserServiceError((HttpServletRequest) request, httpResponse, HttpServletResponse.SC_BAD_REQUEST, iae.getMessage()); + } catch (AdministrationException ae) { + handleUserServiceError((HttpServletRequest) request, httpResponse, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ae.getMessage()); + } + } else { + // can this really happen? + handleMissingCertificate((HttpServletRequest) request, httpResponse); + } + } else { + handleUserServiceError((HttpServletRequest) request, httpResponse, HttpServletResponse.SC_NOT_FOUND, "This NiFi does not support new account requests."); + } + } else { + try { + // this not a request to create a user account - try to authorize + super.doFilter(request, response, chain); + } catch (AuthenticationException ae) { + // continue the filter chain since anonymous access should be supported + if (!properties.getNeedClientAuth()) { + chain.doFilter(request, response); + } else { + // create an appropriate response for the given exception + handleUnsuccessfulAuthentication((HttpServletRequest) request, httpResponse, ae); + } + } + } + } + + @Override + protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) { + String principal; + + // extract the cert + X509Certificate certificate = certificateExtractor.extractClientCertificate(request); + + // ensure the cert was found + if (certificate == null) { + return null; + } + + // extract the principal + Object certificatePrincipal = principalExtractor.extractPrincipal(certificate); + principal = ProxiedEntitiesUtils.formatProxyDn(certificatePrincipal.toString()); + + try { + // ensure the cert is valid + certificate.checkValidity(); + } catch (CertificateExpiredException cee) { + final String message = String.format("Client certificate for (%s) is expired.", principal); + logger.info(message, cee); + if (logger.isDebugEnabled()) { + logger.debug("", cee); + } + return null; + } catch (CertificateNotYetValidException cnyve) { + final String message = String.format("Client certificate for (%s) is not yet valid.", principal); + logger.info(message, cnyve); + if (logger.isDebugEnabled()) { + logger.debug("", cnyve); + } + return null; + } + + // validate the certificate in question + try { + certificateValidator.validate(request); + } catch (final Exception e) { + logger.info(e.getMessage()); + if (logger.isDebugEnabled()) { + logger.debug("", e); + } + return null; + } + + // look for a proxied user + if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) { + principal = request.getHeader(PROXY_ENTITIES_CHAIN) + principal; + } + + // log the request attempt - response details will be logged later + logger.info(String.format("Attempting request for (%s) %s %s (source ip: %s)", principal, request.getMethod(), + request.getRequestURL().toString(), request.getRemoteAddr())); + + return principal; + } + + @Override + protected Object getPreAuthenticatedCredentials(HttpServletRequest request) { + return certificateExtractor.extractClientCertificate(request); + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) { + if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) { + response.setHeader(PROXY_ENTITIES_ACCEPTED, Boolean.TRUE.toString()); + } + super.successfulAuthentication(request, response, authResult); + } + + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) { + if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) { + response.setHeader(PROXY_ENTITIES_DETAILS, failed.getMessage()); + } + super.unsuccessfulAuthentication(request, response, failed); + } + + /** + * Determines if the specified request is attempting to register a new user + * account. + * + * @param request http request + * @return true if new user + */ + private boolean isNewAccountRequest(HttpServletRequest request) { + if ("POST".equalsIgnoreCase(request.getMethod())) { + String path = request.getPathInfo(); + if (StringUtils.isNotBlank(path)) { + if ("/controller/users".equals(path)) { + return true; + } + } + } + return false; + } + + /** + * Handles requests that were unable to be authorized. + * + * @param request request + * @param response response + * @param ae ex + * @throws IOException ex + */ + private void handleUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException ae) throws IOException { + // set the response status + response.setContentType("text/plain"); + + // write the response message + PrintWriter out = response.getWriter(); + + // use the type of authentication exception to determine the response code + if (ae instanceof UsernameNotFoundException) { + if (properties.getSupportNewAccountRequests()) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + out.println("Not authorized."); + } else { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + out.println("Access is denied."); + } + } else if (ae instanceof AccountStatusException) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + out.println(ae.getMessage()); + } else if (ae instanceof UntrustedProxyException) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + out.println(ae.getMessage()); + } else if (ae instanceof AuthenticationServiceException) { + logger.error(String.format("Unable to authorize: %s", ae.getMessage()), ae); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + out.println(String.format("Unable to authorize: %s", ae.getMessage())); + } else { + logger.error(String.format("Unable to authorize: %s", ae.getMessage()), ae); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + out.println("Access is denied."); + } + + // log the failure + logger.info(String.format("Rejecting access to web api: %s", ae.getMessage())); + + // optionally log the stack trace + if (logger.isDebugEnabled()) { + logger.debug(StringUtils.EMPTY, ae); + } + } + + private void handleUserServiceError(HttpServletRequest request, HttpServletResponse response, int responseCode, String message) throws IOException { + // set the response status + response.setContentType("text/plain"); + response.setStatus(responseCode); + + // write the response message + PrintWriter out = response.getWriter(); + out.println(message); + + // log the failure + logger.info(String.format("Unable to process request because %s", message)); + } + + /** + * Handles requests that failed because they were bad input. + * + * @param request request + * @param response response + * @throws IOException ioe + */ + private void handleMissingCertificate(HttpServletRequest request, HttpServletResponse response) throws IOException { + // set the response status + response.setContentType("text/plain"); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + + // write the response message + PrintWriter out = response.getWriter(); + out.println("Unable to process request because the user certificate was not specified."); + + // log the failure + logger.info("Unable to process request because the user certificate was not specified."); + } + + /* setters */ + public void setProperties(NiFiProperties properties) { + this.properties = properties; + } + + public void setUserService(UserService userService) { + this.userService = userService; + } + + public void setCertificateValidator(OcspCertificateValidator certificateValidator) { + this.certificateValidator = certificateValidator; + } + +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationProvider.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationProvider.java new file mode 100644 index 0000000..df23856 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationProvider.java @@ -0,0 +1,47 @@ +/* + * 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.x509; + +import org.apache.nifi.web.security.token.NewAccountAuthenticationRequestToken; +import org.apache.nifi.web.security.token.NewAccountAuthenticationToken; +import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +/** + * + */ +public class X509AuthenticationProvider implements AuthenticationProvider { + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if (NewAccountAuthenticationRequestToken.class.isAssignableFrom(authentication.getClass())) { + return new NewAccountAuthenticationToken(((NewAccountAuthenticationRequestToken) authentication).getNewAccountRequest()); + } else if (NiFiAuthenticationRequestToken.class.isAssignableFrom(authentication.getClass())) { + return authentication; + } else { + return null; + } + } + + @Override + public boolean supports(Class<?> authentication) { + return NiFiAuthenticationRequestToken.class.isAssignableFrom(authentication); + } + +} http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml index fa5b9e2..5f4e1b2 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml @@ -13,71 +13,80 @@ See the License for the specific language governing permissions and limitations under the License. --> -<!-- marked as lazy so that security beans are not created when applications runs in non-secure mode --> -<beans default-lazy-init="true" - xmlns="http://www.springframework.org/schema/beans" +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:sec="http://www.springframework.org/schema/security" - xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd - http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd"> + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> - <!-- http security configuration --> - <sec:http create-session="stateless" auto-config="false" entry-point-ref="entryPoint"> +<!-- <sec:http create-session="stateless" auto-config="false" entry-point-ref="entryPoint"> <sec:anonymous enabled="false"/> <sec:custom-filter ref="nodeAuthorizedUserFilter" before="PRE_AUTH_FILTER"/> <sec:custom-filter ref="x509Filter" position="PRE_AUTH_FILTER"/> <sec:custom-filter ref="anonymousFilter" position="ANONYMOUS_FILTER"/> - </sec:http> + </sec:http>--> <!-- enable method level security --> - <sec:global-method-security pre-post-annotations="enabled"/> + <!--<sec:global-method-security pre-post-annotations="enabled"/>--> + + <!--<bean class="org.apache.nifi.web.security.NiFiSecurityWebApplicationInitializer"></bean>--> + + <!-- security config --> +<!-- <bean id="securityConfig" class="org.apache.nifi.web.security.NiFiSecurityConfig"> + <property name="properties" ref="nifiProperties"/> + <property name="userDetailsService" ref="userAuthorizationService"/> + <property name="authorizedUserFilter" ref="nodeAuthorizedUserFilter"/> + <property name="entryPoint" ref="entryPoint"/> + </bean>--> <!-- entry point reference --> - <bean id="entryPoint" class="org.apache.nifi.web.security.authentication.NiFiAuthenticationEntryPoint"/> + <!--<bean id="entryPoint" class="org.apache.nifi.web.security.authentication.NiFiAuthenticationEntryPoint"/>--> <!-- authentication manager --> - <sec:authentication-manager alias="authenticationManager"> +<!-- <sec:authentication-manager alias="authenticationManager"> <sec:authentication-provider ref="preauthAuthProvider"/> - </sec:authentication-manager> + </sec:authentication-manager>--> <!-- pre-authentication provider --> - <bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider"> +<!-- <bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider"> <property name="preAuthenticatedUserDetailsService"> <bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper"> <property name="userDetailsService" ref="userAuthorizationService"/> </bean> </property> - </bean> + </bean>--> <!-- user details service --> - <bean id="userAuthorizationService" class="org.apache.nifi.web.security.authorization.NiFiAuthorizationService"> + <bean id="userDetailsService" class="org.apache.nifi.web.security.authorization.NiFiAuthorizationService"> <property name="userService" ref="userService"/> <property name="properties" ref="nifiProperties"/> </bean> - + + <!-- jwt service --> + <bean id="jwtService" class="org.apache.nifi.web.security.jwt.JwtService"></bean> + + <!-- login identity provider --> + <bean id="loginIdentityProvider" class="org.apache.nifi.web.security.spring.LoginIdentityProviderFactoryBean"> + <property name="properties" ref="nifiProperties"/> + </bean> + <!-- performs ocsp certificate validation --> - <bean id="ocspCertificateValidator" class="org.apache.nifi.web.security.x509.ocsp.OcspCertificateValidator"> +<!-- <bean id="ocspCertificateValidator" class="org.apache.nifi.web.security.x509.ocsp.OcspCertificateValidator"> <constructor-arg ref="nifiProperties"/> - </bean> + </bean>--> <!-- custom x509 filter for checking for proxied users --> - <bean id="x509Filter" class="org.apache.nifi.web.security.x509.X509AuthenticationFilter"> - <property name="authenticationManager" ref="authenticationManager"/> - <property name="properties" ref="nifiProperties"/> - <property name="userService" ref="userService"/> +<!-- <bean id="x509Filter" class="org.apache.nifi.web.security.x509.X509AuthenticationFilter"> <property name="certificateValidator" ref="ocspCertificateValidator"/> - <property name="continueFilterChainOnUnsuccessfulAuthentication" value="false"/> - </bean> + </bean>--> <!-- custom filter for checking for proxied users that are already authenticated --> - <bean id="nodeAuthorizedUserFilter" class="org.apache.nifi.web.security.authorization.NodeAuthorizedUserFilter"> +<!-- <bean id="nodeAuthorizedUserFilter" class="org.apache.nifi.web.security.authorization.NodeAuthorizedUserFilter"> <property name="properties" ref="nifiProperties"/> - </bean> + </bean>--> <!-- custom anonymous filter to assign default roles based on current operating mode --> - <bean id="anonymousFilter" class="org.apache.nifi.web.security.anonymous.NiFiAnonymousUserFilter"> +<!-- <bean id="anonymousFilter" class="org.apache.nifi.web.security.anonymous.NiFiAnonymousUserFilter"> <property name="userService" ref="userService"/> <property name="properties" ref="nifiProperties"/> - </bean> + </bean>--> </beans> http://git-wip-us.apache.org/repos/asf/nifi/blob/a40e5a07/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/xsd/login-identity-providers.xsd ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/xsd/login-identity-providers.xsd b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/xsd/login-identity-providers.xsd new file mode 100644 index 0000000..628f390 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/xsd/login-identity-providers.xsd @@ -0,0 +1,49 @@ +<?xml version="1.0"?> +<!-- + 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. +--> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <!-- role --> + <xs:complexType name="Provider"> + <xs:sequence> + <xs:element name="identifier" type="NonEmptyStringType"/> + <xs:element name="class" type="NonEmptyStringType"/> + <xs:element name="property" type="Property" minOccurs="0" maxOccurs="unbounded" /> + </xs:sequence> + </xs:complexType> + + <!-- Name/Value properties--> + <xs:complexType name="Property"> + <xs:simpleContent> + <xs:extension base="xs:string"> + <xs:attribute name="name" type="NonEmptyStringType"></xs:attribute> + </xs:extension> + </xs:simpleContent> + </xs:complexType> + + <xs:simpleType name="NonEmptyStringType"> + <xs:restriction base="xs:string"> + <xs:minLength value="1"/> + </xs:restriction> + </xs:simpleType> + + <!-- login identity provider --> + <xs:element name="loginIdentityProviders"> + <xs:complexType> + <xs:sequence> + <xs:element name="provider" type="Provider" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + </xs:element> +</xs:schema> \ No newline at end of file
