http://git-wip-us.apache.org/repos/asf/nifi/blob/aaf14c45/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.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/jwt/JwtService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java new file mode 100644 index 0000000..9635354 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java @@ -0,0 +1,162 @@ +/* + * 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.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.SigningKeyResolverAdapter; +import io.jsonwebtoken.UnsupportedJwtException; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.admin.service.AdministrationException; +import org.apache.nifi.admin.service.KeyService; +import org.apache.nifi.web.security.token.LoginAuthenticationToken; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.util.Calendar; +import org.apache.nifi.key.Key; + +/** + * + */ +public class JwtService { + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(JwtService.class); + + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256; + private static final String KEY_ID_CLAIM = "kid"; + private static final String USERNAME_CLAIM = "preferred_username"; + + private final KeyService keyService; + + public JwtService(final KeyService keyService) { + this.keyService = keyService; + } + + public String getAuthenticationFromToken(final String base64EncodedToken) throws JwtException { + // The library representations of the JWT should be kept internal to this service. + try { + final Jws<Claims> jws = parseTokenFromBase64EncodedString(base64EncodedToken); + + if (jws == null) { + throw new JwtException("Unable to parse token"); + } + + // Additional validation that subject is present + if (StringUtils.isEmpty(jws.getBody().getSubject())) { + throw new JwtException("No subject available in token"); + } + + // TODO: Validate issuer against active registry? + if (StringUtils.isEmpty(jws.getBody().getIssuer())) { + // TODO: Remove after testing +// logger.info("Decoded JWT payload: " + jws.toString()); + throw new JwtException("No issuer available in token"); + } + return jws.getBody().getSubject(); + } catch (JwtException e) { + logger.debug("The Base64 encoded JWT: " + base64EncodedToken); + final String errorMessage = "There was an error validating the JWT"; + logger.error(errorMessage, e); + throw e; + } + } + + private Jws<Claims> parseTokenFromBase64EncodedString(final String base64EncodedToken) throws JwtException { + try { + return Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() { + @Override + public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + final String identity = claims.getSubject(); + + // Get the key based on the key id in the claims + final Integer keyId = claims.get(KEY_ID_CLAIM, Integer.class); + final Key key = keyService.getKey(keyId); + + // Ensure we were able to find a key that was previously issued by this key service for this user + if (key == null || key.getKey() == null) { + throw new UnsupportedJwtException("Unable to determine signing key for " + identity + " [kid: " + keyId + "]"); + } + + return key.getKey().getBytes(StandardCharsets.UTF_8); + } + }).parseClaimsJws(base64EncodedToken); + } catch (final MalformedJwtException | UnsupportedJwtException | SignatureException | ExpiredJwtException | IllegalArgumentException | AdministrationException e) { + // TODO: Exercise all exceptions to ensure none leak key material to logs + final String errorMessage = "There was an error validating the JWT"; + throw new JwtException(errorMessage, e); + } + } + + /** + * Generates a signed JWT token from the provided (Spring Security) login authentication token. + * + * @param authenticationToken an instance of the Spring Security token after login credentials have been verified against the respective information source + * @return a signed JWT containing the user identity and the identity provider, Base64-encoded + * @throws JwtException if there is a problem generating the signed token + */ + public String generateSignedToken(final LoginAuthenticationToken authenticationToken) throws JwtException { + if (authenticationToken == null) { + throw new IllegalArgumentException("Cannot generate a JWT for a null authentication token"); + } + + // Set expiration from the token + final Calendar expiration = Calendar.getInstance(); + expiration.setTimeInMillis(authenticationToken.getExpiration()); + + final Object principal = authenticationToken.getPrincipal(); + if (principal == null || StringUtils.isEmpty(principal.toString())) { + final String errorMessage = "Cannot generate a JWT for a token with an empty identity issued by " + authenticationToken.getIssuer(); + logger.error(errorMessage); + throw new JwtException(errorMessage); + } + + // Create a JWT with the specified authentication + final String identity = principal.toString(); + final String username = authenticationToken.getName(); + + try { + // Get/create the key for this user + final Key key = keyService.getOrCreateKey(identity); + final byte[] keyBytes = key.getKey().getBytes(StandardCharsets.UTF_8); + + logger.trace("Generating JWT for " + authenticationToken); + + // TODO: Implement "jti" claim with nonce to prevent replay attacks and allow blacklisting of revoked tokens + + // Build the token + return Jwts.builder().setSubject(identity) + .setIssuer(authenticationToken.getIssuer()) + .setAudience(authenticationToken.getIssuer()) + .claim(USERNAME_CLAIM, username) + .claim(KEY_ID_CLAIM, key.getId()) + .setExpiration(expiration.getTime()) + .setIssuedAt(Calendar.getInstance().getTime()) + .signWith(SIGNATURE_ALGORITHM, keyBytes).compact(); + } catch (NullPointerException | AdministrationException e) { + final String errorMessage = "Could not retrieve the signing key for JWT for " + identity; + logger.error(errorMessage, e); + throw new JwtException(errorMessage, e); + } + } +}
http://git-wip-us.apache.org/repos/asf/nifi/blob/aaf14c45/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/node/NodeAuthorizedUserFilter.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/node/NodeAuthorizedUserFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/node/NodeAuthorizedUserFilter.java new file mode 100644 index 0000000..a3e6c3c --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/node/NodeAuthorizedUserFilter.java @@ -0,0 +1,127 @@ +/* + * 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.node; + +import java.io.IOException; +import java.io.Serializable; +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 org.apache.nifi.controller.FlowController; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.authentication.AuthenticationResponse; +import org.apache.nifi.web.security.user.NiFiUserDetails; +import org.apache.nifi.user.NiFiUser; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.security.token.NiFiAuthorizationToken; +import org.apache.nifi.web.security.x509.X509CertificateExtractor; +import org.apache.nifi.web.security.x509.X509IdentityProvider; +import org.apache.nifi.web.util.WebUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.support.WebApplicationContextUtils; +import org.springframework.web.filter.GenericFilterBean; + +/** + * Custom filter to extract a user's authorities from the request where the user was authenticated by the cluster manager and populate the threadlocal with the authorized user. If the request contains + * the appropriate header with authorities and the application instance is a node connected to the cluster, then the authentication/authorization steps remaining in the filter chain are skipped. + * + * Checking if the application instance is a connected node is important because it prevents external clients from faking the request headers and bypassing the authentication processing chain. + */ +public class NodeAuthorizedUserFilter extends GenericFilterBean { + + private static final Logger LOGGER = LoggerFactory.getLogger(NodeAuthorizedUserFilter.class); + + public static final String PROXY_USER_DETAILS = "X-ProxiedEntityUserDetails"; + + private NiFiProperties properties; + private X509CertificateExtractor certificateExtractor; + private X509IdentityProvider certificateIdentityProvider; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + final HttpServletRequest httpServletRequest = (HttpServletRequest) request; + + // get the proxied user's authorities + final String hexEncodedUserDetails = httpServletRequest.getHeader(PROXY_USER_DETAILS); + + // check if the request has the necessary header information and this instance is configured as a node + if (StringUtils.isNotBlank(hexEncodedUserDetails) && properties.isNode()) { + + // get the flow controller from the Spring context + final ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); + final FlowController flowController = ctx.getBean("flowController", FlowController.class); + + // check that we are connected to the cluster + if (flowController.getNodeId() != null) { + try { + // attempt to extract the client certificate + final X509Certificate[] certificate = certificateExtractor.extractClientCertificate(httpServletRequest); + if (certificate != null) { + // authenticate the certificate + final AuthenticationResponse authenticationResponse = certificateIdentityProvider.authenticate(certificate); + + // only consider the pre-authorized user when the request came directly from the NCM according to the DN in the certificate + final String clusterManagerIdentity = flowController.getClusterManagerDN(); + if (clusterManagerIdentity != null && clusterManagerIdentity.equals(authenticationResponse.getIdentity())) { + // deserialize hex encoded object + final Serializable userDetailsObj = WebUtils.deserializeHexToObject(hexEncodedUserDetails); + + // if we have a valid object, set the authentication token and bypass the remaining authentication processing chain + if (userDetailsObj instanceof NiFiUserDetails) { + final NiFiUserDetails userDetails = (NiFiUserDetails) userDetailsObj; + final NiFiUser user = userDetails.getNiFiUser(); + + // log the request attempt - response details will be logged later + logger.info(String.format("Attempting request for (%s) %s %s (source ip: %s)", user.getIdentity(), httpServletRequest.getMethod(), + httpServletRequest.getRequestURL().toString(), request.getRemoteAddr())); + + // create the authorized nifi token + final NiFiAuthorizationToken token = new NiFiAuthorizationToken(userDetails); + SecurityContextHolder.getContext().setAuthentication(token); + } + } + } + } catch (final ClassNotFoundException cnfe) { + LOGGER.warn("Classpath issue detected because failed to deserialize authorized user in request header due to: " + cnfe, cnfe); + } catch (final IllegalArgumentException iae) { + // unable to authenticate a serialized user from the incoming request + } + } + } + + chain.doFilter(request, response); + } + + public void setProperties(NiFiProperties properties) { + this.properties = properties; + } + + public void setCertificateIdentityProvider(X509IdentityProvider certificateIdentityProvider) { + this.certificateIdentityProvider = certificateIdentityProvider; + } + + public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) { + this.certificateExtractor = certificateExtractor; + } + +} http://git-wip-us.apache.org/repos/asf/nifi/blob/aaf14c45/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..92a27ae --- /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,312 @@ +/* + * 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.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.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.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(); + + /** + * 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.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 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; + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/aaf14c45/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/aaf14c45/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/aaf14c45/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..3591239 --- /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,123 @@ +/* + * 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.security.util.CertificateUtils; +import org.springframework.security.authentication.AbstractAuthenticationToken; + +import java.text.SimpleDateFormat; +import java.util.Calendar; + +/** + * This is an Authentication Token for logging in. Once a user is authenticated, they can be issued an ID token. + */ +public class LoginAuthenticationToken extends AbstractAuthenticationToken { + + private final String identity; + private final String username; + private final long expiration; + private final String issuer; + + /** + * Creates a representation of the authentication token for a user. + * + * @param identity The unique identifier for this user + * @param expiration The relative time to expiration in milliseconds + * @param issuer The IdentityProvider implementation that generated this token + */ + public LoginAuthenticationToken(final String identity, final long expiration, final String issuer) { + this(identity, null, expiration, issuer); + } + + /** + * Creates a representation of the authentication token for a user. + * + * @param identity The unique identifier for this user (cannot be null or empty) + * @param username The preferred username for this user + * @param expiration The relative time to expiration in milliseconds + * @param issuer The IdentityProvider implementation that generated this token + */ + public LoginAuthenticationToken(final String identity, final String username, final long expiration, final String issuer) { + super(null); + setAuthenticated(true); + this.identity = identity; + this.username = username; + this.issuer = issuer; + Calendar now = Calendar.getInstance(); + this.expiration = now.getTimeInMillis() + expiration; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return identity; + } + + /** + * Returns the expiration instant in milliseconds. This value is an absolute point in time (i.e. Nov + * 16, 2015 11:30:00.000 GMT), not a relative time (i.e. 60 minutes). It is calculated by adding the + * relative expiration from the constructor to the timestamp at object creation. + * + * @return the expiration in millis + */ + public long getExpiration() { + return expiration; + } + + public String getIssuer() { + return issuer; + } + + @Override + public String getName() { + if (username == null) { + // if the username is a DN this will extract the username or CN... if not will return what was passed + return CertificateUtils.extractUsername(identity); + } else { + return username; + } + } + + @Override + public String toString() { + Calendar expirationTime = Calendar.getInstance(); + expirationTime.setTimeInMillis(getExpiration()); + long remainingTime = expirationTime.getTimeInMillis() - Calendar.getInstance().getTimeInMillis(); + + SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss.SSS"); + dateFormat.setTimeZone(expirationTime.getTimeZone()); + String expirationTimeString = dateFormat.format(expirationTime.getTime()); + + return new StringBuilder("LoginAuthenticationToken for ") + .append(getName()) + .append(" issued by ") + .append(getIssuer()) + .append(" expiring at ") + .append(expirationTimeString) + .append(" [") + .append(getExpiration()) + .append(" ms, ") + .append(remainingTime) + .append(" ms remaining]") + .toString(); + } + +} http://git-wip-us.apache.org/repos/asf/nifi/blob/aaf14c45/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/aaf14c45/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/aaf14c45/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/aaf14c45/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/aaf14c45/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/aaf14c45/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..b559269 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 @@ -73,10 +73,9 @@ public class NiFiUserDetails implements UserDetails { @Override public String getUsername() { - return user.getDn(); + return user.getIdentity(); } - // TODO: not sure how to handle these yet @Override public boolean isAccountNonExpired() { return true; http://git-wip-us.apache.org/repos/asf/nifi/blob/aaf14c45/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/aaf14c45/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..dd7d47e 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 @@ -16,302 +16,66 @@ */ 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.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; +import org.apache.nifi.authentication.AuthenticationResponse; +import org.apache.nifi.web.security.InvalidAuthenticationException; +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; /** - * 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 OcspCertificateValidator certificateValidator; - private NiFiProperties properties; - private UserService userService; + private X509CertificateExtractor certificateExtractor; + private X509IdentityProvider certificateIdentityProvider; @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) { + public NiFiAuthenticationRequestToken attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { + // only suppport x509 login when running securely + if (!request.isSecure()) { return null; } - // extract the principal - Object certificatePrincipal = principalExtractor.extractPrincipal(certificate); - principal = DnUtils.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); - } + // look for a client certificate + final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(request); + if (certificates == null) { return null; } - // validate the certificate in question + // attempt to authenticate if certificates were found + final AuthenticationResponse authenticationResponse; try { - certificateValidator.validate(request); - } catch (final Exception e) { - logger.info(e.getMessage()); - if (logger.isDebugEnabled()) { - logger.debug("", e); - } - return null; + authenticationResponse = certificateIdentityProvider.authenticate(certificates); + } catch (final IllegalArgumentException iae) { + throw new InvalidAuthenticationException(iae.getMessage(), iae); } - // 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.buildProxiedEntitiesChain(request, authenticationResponse.getIdentity()); + 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."); + return new NiFiAuthenticationRequestToken(proxyChain); } - - // 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 setCertificateExtractor(X509CertificateExtractor certificateExtractor) { + this.certificateExtractor = certificateExtractor; } - public void setCertificateValidator(OcspCertificateValidator certificateValidator) { - this.certificateValidator = certificateValidator; + public void setCertificateIdentityProvider(X509IdentityProvider certificateIdentityProvider) { + this.certificateIdentityProvider = certificateIdentityProvider; } } http://git-wip-us.apache.org/repos/asf/nifi/blob/aaf14c45/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509CertificateExtractor.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/X509CertificateExtractor.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509CertificateExtractor.java index b40d5a5..98d0154 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509CertificateExtractor.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509CertificateExtractor.java @@ -35,11 +35,11 @@ public class X509CertificateExtractor { * @param request http request * @return cert */ - public X509Certificate extractClientCertificate(HttpServletRequest request) { + public X509Certificate[] extractClientCertificate(HttpServletRequest request) { X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate"); if (certs != null && certs.length > 0) { - return certs[0]; + return certs; } if (logger.isDebugEnabled()) { http://git-wip-us.apache.org/repos/asf/nifi/blob/aaf14c45/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509CertificateValidator.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/X509CertificateValidator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509CertificateValidator.java new file mode 100644 index 0000000..cb56a11 --- /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/X509CertificateValidator.java @@ -0,0 +1,58 @@ +/* + * 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 java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import org.apache.nifi.web.security.x509.ocsp.CertificateStatusException; +import org.apache.nifi.web.security.x509.ocsp.OcspCertificateValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Extracts client certificates from Http requests. + */ +public class X509CertificateValidator { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private OcspCertificateValidator ocspValidator; + + /** + * Extract the client certificate from the specified HttpServletRequest or null if none is specified. + * + * @param certificates the client certificates + * @throws java.security.cert.CertificateExpiredException cert is expired + * @throws java.security.cert.CertificateNotYetValidException cert is not yet valid + * @throws org.apache.nifi.web.security.x509.ocsp.CertificateStatusException ocsp validation issue + */ + public void validateClientCertificate(final X509Certificate[] certificates) + throws CertificateExpiredException, CertificateNotYetValidException, CertificateStatusException { + + // ensure the cert is valid + certificates[0].checkValidity(); + + // perform ocsp validator if necessary + ocspValidator.validate(certificates); + } + + public void setOcspValidator(OcspCertificateValidator ocspValidator) { + this.ocspValidator = ocspValidator; + } + +} http://git-wip-us.apache.org/repos/asf/nifi/blob/aaf14c45/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509IdentityProvider.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/X509IdentityProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509IdentityProvider.java new file mode 100644 index 0000000..db0b529 --- /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/X509IdentityProvider.java @@ -0,0 +1,94 @@ +/* + * 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 java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.concurrent.TimeUnit; +import org.apache.nifi.authentication.AuthenticationResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; + +/** + * Identity provider for extract the authenticating a ServletRequest with a X509Certificate. + */ +public class X509IdentityProvider { + + private static final Logger logger = LoggerFactory.getLogger(X509IdentityProvider.class); + + private final String issuer = getClass().getSimpleName(); + + private X509CertificateValidator certificateValidator; + private X509PrincipalExtractor principalExtractor; + + /** + * Authenticates the specified request by checking certificate validity. + * + * @param certificates the client certificates + * @return an authentication response + * @throws IllegalArgumentException the request did not contain a valid certificate (or no certificate) + */ + public AuthenticationResponse authenticate(final X509Certificate[] certificates) throws IllegalArgumentException { + // ensure the cert was found + if (certificates == null || certificates.length == 0) { + throw new IllegalArgumentException("The specified request does not contain a client certificate."); + } + + // extract the principal + final Object certificatePrincipal = principalExtractor.extractPrincipal(certificates[0]); + final String principal = certificatePrincipal.toString(); + + try { + certificateValidator.validateClientCertificate(certificates); + } 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); + } + throw new IllegalArgumentException(message, cee); + } 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); + } + throw new IllegalArgumentException(message, cnyve); + } catch (final Exception e) { + logger.info(e.getMessage()); + if (logger.isDebugEnabled()) { + logger.debug("", e); + } + throw new IllegalArgumentException(e.getMessage(), e); + } + + // build the authentication response + return new AuthenticationResponse(principal, principal, TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS), issuer); + } + + /* setters */ + public void setCertificateValidator(X509CertificateValidator certificateValidator) { + this.certificateValidator = certificateValidator; + } + + public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) { + this.principalExtractor = principalExtractor; + } + +}
