From 05ef1821455331279bc5761c45834a5474c72e63 Mon Sep 17 00:00:00 2001 From: Kevin Doran Date: Mon, 30 Oct 2017 14:39:56 -0400 Subject: [PATCH 1/2] NIFIREG-45 Refactor LoginIdentityProvider interface as a more generic and flexible IdentityProvider interface --- .../client/impl/JerseyNiFiRegistryClient.java | 40 +--- nifi-registry-framework/pom.xml | 19 ++ .../registry/extension/ExtensionManager.java | 4 +- .../IdentityProviderFactory.java | 71 +++--- ...dIdentityProviderConfigurationContext.java | 18 +- .../security/ldap/LdapIdentityProvider.java | 164 +++++++------- .../ldap/tenants/LdapUserGroupProvider.java | 6 +- ....security.authentication.IdentityProvider} | 0 .../src/main/xsd/identity-providers.xsd | 0 nifi-registry-security-api/pom.xml | 6 + .../authentication/AuthenticationRequest.java | 82 +++++++ .../AuthenticationResponse.java | 35 ++- .../BasicAuthIdentityProvider.java | 95 ++++++++ .../BearerAuthIdentityProvider.java | 72 +++++++ .../authentication/IdentityProvider.java | 157 ++++++++++++++ ...IdentityProviderConfigurationContext.java} | 10 +- ...ookup.java => IdentityProviderLookup.java} | 4 +- ...ntials.java => IdentityProviderUsage.java} | 29 +-- .../authentication/LoginIdentityProvider.java | 61 ------ ...sernamePasswordAuthenticationRequest.java} | 10 +- ...text.java => IdentityProviderContext.java} | 6 +- ....java => InvalidCredentialsException.java} | 8 +- .../AuthorizerCreationException.java | 3 +- .../AuthorizerDestructionException.java | 3 +- .../SecurityProviderCreationException.java} | 15 +- ...SecurityProviderDestructionException.java} | 15 +- .../security/util}/ProxiedEntitiesUtils.java | 38 +--- nifi-registry-web-api/pom.xml | 25 --- .../nifi/registry/web/api/AccessResource.java | 201 +++++++++++++---- .../web/request/IntegerParameter.java | 39 ---- .../web/response/AuthenticationResponse.java | 65 ------ .../security/NiFiRegistrySecurityConfig.java | 111 ++++------ ...lter.java => AnonymousIdentityFilter.java} | 7 +- .../AuthenticationRequestToken.java | 107 +++++++++ ...n.java => AuthenticationSuccessToken.java} | 8 +- .../IdentityAuthenticationFilter.java | 203 ++++++++++++++++++ .../IdentityAuthenticationProvider.java | 140 ++++++++++++ .../authentication/IdentityFilter.java | 97 +++++++++ .../NiFiAuthenticationFilter.java | 156 -------------- .../NiFiAuthenticationProvider.java | 84 -------- .../NiFiAuthenticationRequestToken.java | 41 ---- ...IdentityProviderInitializationContext.java | 45 ---- .../exception/UntrustedProxyException.java | 3 - .../jwt/JwtAuthenticationFilter.java | 58 ----- .../jwt/JwtAuthenticationProvider.java | 69 ------ .../jwt/JwtAuthenticationRequestToken.java | 60 ------ .../jwt/JwtIdentityProvider.java | 76 +++++++ .../authentication/jwt/JwtService.java | 76 +++++-- .../token/LoginAuthenticationToken.java | 123 ----------- .../x509/X509AuthenticationFilter.java | 64 ------ .../x509/X509AuthenticationProvider.java | 166 -------------- .../x509/X509AuthenticationRequestToken.java | 75 ------- .../x509/X509CertificateValidator.java | 49 ----- .../X509IdentityAuthenticationProvider.java | 131 +++++++++++ .../x509/X509IdentityProvider.java | 134 ++++++++---- .../nifi/registry/web/api/SecureLdapIT.java | 91 ++++++-- 56 files changed, 1842 insertions(+), 1633 deletions(-) rename nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/LoginIdentityProviderFactory.java => nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderFactory.java (75%) rename nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/StandardLoginIdentityProviderConfigurationContext.java => nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/StandardIdentityProviderConfigurationContext.java (73%) rename nifi-registry-framework/src/main/resources/META-INF/services/{org.apache.nifi.registry.security.authentication.LoginIdentityProvider => org.apache.nifi.registry.security.authentication.IdentityProvider} (100%) rename {nifi-registry-web-api => nifi-registry-framework}/src/main/xsd/identity-providers.xsd (100%) create mode 100644 nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationRequest.java create mode 100644 nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BasicAuthIdentityProvider.java create mode 100644 nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BearerAuthIdentityProvider.java create mode 100644 nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProvider.java rename nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/{LoginIdentityProviderConfigurationContext.java => IdentityProviderConfigurationContext.java} (89%) rename nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/{LoginIdentityProviderLookup.java => IdentityProviderLookup.java} (87%) rename nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/{LoginCredentials.java => IdentityProviderUsage.java} (67%) delete mode 100644 nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/LoginIdentityProvider.java rename nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/{LoginIdentityProviderInitializationContext.java => UsernamePasswordAuthenticationRequest.java} (80%) rename nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/annotation/{LoginIdentityProviderContext.java => IdentityProviderContext.java} (95%) rename nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/{InvalidLoginCredentialsException.java => InvalidCredentialsException.java} (80%) rename nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/{authentication/exception/ProviderCreationException.java => exception/SecurityProviderCreationException.java} (66%) rename nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/{authentication/exception/ProviderDestructionException.java => exception/SecurityProviderDestructionException.java} (65%) rename {nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication => nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util}/ProxiedEntitiesUtils.java (74%) delete mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/request/IntegerParameter.java delete mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/response/AuthenticationResponse.java rename nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/{NiFiAnonymousUserFilter.java => AnonymousIdentityFilter.java} (83%) create mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationRequestToken.java rename nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/{token/NiFiAuthenticationToken.java => AuthenticationSuccessToken.java} (87%) create mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationFilter.java create mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationProvider.java create mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityFilter.java delete mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationFilter.java delete mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationProvider.java delete mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationRequestToken.java delete mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/StandardLoginIdentityProviderInitializationContext.java delete mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationFilter.java delete mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationProvider.java delete mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationRequestToken.java create mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtIdentityProvider.java delete mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/token/LoginAuthenticationToken.java delete mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationFilter.java delete mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationProvider.java delete mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestToken.java delete mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509CertificateValidator.java create mode 100644 nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java index 454d872e3..c2eaee5f7 100644 --- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java +++ b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java @@ -30,6 +30,7 @@ import org.apache.nifi.registry.client.NiFiRegistryClient; import org.apache.nifi.registry.client.NiFiRegistryClientConfig; import org.apache.nifi.registry.client.UserClient; +import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider; @@ -52,13 +53,6 @@ */ public class JerseyNiFiRegistryClient implements NiFiRegistryClient { - static final String PROXY_ENTITIES_CHAIN = "X-ProxiedEntitiesChain"; - - static final String GT = ">"; - static final String ESCAPED_GT = "\\\\>"; - static final String LT = "<"; - static final String ESCAPED_LT = "\\\\<"; - static final String NIFI_REGISTRY_CONTEXT = "nifi-registry-api"; static final int DEFAULT_CONNECT_TIMEOUT = 10000; static final int DEFAULT_READ_TIMEOUT = 10000; @@ -184,7 +178,7 @@ private Map getHeaders(String[] proxiedEntities) { final Map headers = new HashMap<>(); if (proxiedEntitiesValue != null) { - headers.put(PROXY_ENTITIES_CHAIN, proxiedEntitiesValue); + headers.put(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxiedEntitiesValue); } return headers; } @@ -194,38 +188,10 @@ private String getProxiedEntitesValue(String[] proxiedEntities) { return null; } - final List proxiedEntityChain = Arrays.asList(proxiedEntities).stream().map(dn -> formatProxyDn(dn)).collect(Collectors.toList()); + final List proxiedEntityChain = Arrays.stream(proxiedEntities).map(ProxiedEntitiesUtils::formatProxyDn).collect(Collectors.toList()); return StringUtils.join(proxiedEntityChain, ""); } - /** - * Formats the specified DN to be set as a HTTP header using well known conventions. - * - * @param dn raw dn - * @return the dn formatted as an HTTP header - */ - private static String formatProxyDn(String dn) { - return LT + sanitizeDn(dn) + GT; - } - - /** - * If a user provides a DN with the sequence '><', they could escape the tokenization process and impersonate another user. - *

- * Example: - *

- * Provided DN: {@code jdoe> {@code } would allow the user to impersonate jdoe - * - * @param rawDn the unsanitized DN - * @return the sanitized DN - */ - private static String sanitizeDn(String rawDn) { - if (StringUtils.isEmpty(rawDn)) { - return rawDn; - } else { - return rawDn.replaceAll(GT, ESCAPED_GT).replaceAll(LT, ESCAPED_LT); - } - } - @Override public void close() throws IOException { if (this.client != null) { diff --git a/nifi-registry-framework/pom.xml b/nifi-registry-framework/pom.xml index 95381f56a..e4bc46224 100644 --- a/nifi-registry-framework/pom.xml +++ b/nifi-registry-framework/pom.xml @@ -90,6 +90,19 @@ false + + identity-providers + + xjc + + + + src/main/xsd/identity-providers.xsd + + org.apache.nifi.registry.security.authentication.generated + false + + @@ -156,6 +169,12 @@ nifi-registry-security-utils 0.0.1-SNAPSHOT + + javax.servlet + javax.servlet-api + 3.1.0 + provided + org.springframework.boot spring-boot-starter-security diff --git a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java index 27e8b9143..4c8b5ac94 100644 --- a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java +++ b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java @@ -17,7 +17,7 @@ package org.apache.nifi.registry.extension; import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.registry.security.authentication.LoginIdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProvider; import org.apache.nifi.registry.security.authorization.AccessPolicyProvider; import org.apache.nifi.registry.security.authorization.Authorizer; import org.apache.nifi.registry.security.authorization.UserGroupProvider; @@ -55,7 +55,7 @@ public class ExtensionManager { classes.add(UserGroupProvider.class); classes.add(AccessPolicyProvider.class); classes.add(Authorizer.class); - classes.add(LoginIdentityProvider.class); + classes.add(IdentityProvider.class); EXTENSION_CLASSES = Collections.unmodifiableList(classes); } diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/LoginIdentityProviderFactory.java b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderFactory.java similarity index 75% rename from nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/LoginIdentityProviderFactory.java rename to nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderFactory.java index 434d88131..720bd9cac 100644 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/LoginIdentityProviderFactory.java +++ b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderFactory.java @@ -14,15 +14,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.nifi.registry.web.security.authentication; +package org.apache.nifi.registry.security.authentication; import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.registry.security.authentication.LoginIdentityProvider; -import org.apache.nifi.registry.security.authentication.LoginIdentityProviderConfigurationContext; -import org.apache.nifi.registry.security.authentication.LoginIdentityProviderLookup; -import org.apache.nifi.registry.security.authentication.annotation.LoginIdentityProviderContext; import org.apache.nifi.registry.extension.ExtensionManager; import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.security.authentication.annotation.IdentityProviderContext; import org.apache.nifi.registry.security.authentication.generated.IdentityProviders; import org.apache.nifi.registry.security.authentication.generated.Property; import org.apache.nifi.registry.security.authentication.generated.Provider; @@ -33,6 +30,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.xml.sax.SAXException; import javax.xml.XMLConstants; @@ -53,16 +51,16 @@ import java.util.Map; @Configuration -public class LoginIdentityProviderFactory implements LoginIdentityProviderLookup, DisposableBean { +public class IdentityProviderFactory implements IdentityProviderLookup, DisposableBean { - private static final Logger logger = LoggerFactory.getLogger(LoginIdentityProviderFactory.class); + private static final Logger logger = LoggerFactory.getLogger(IdentityProviderFactory.class); private static final String LOGIN_IDENTITY_PROVIDERS_XSD = "/identity-providers.xsd"; private static final String JAXB_GENERATED_PATH = "org.apache.nifi.registry.security.authentication.generated"; private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext(); private static JAXBContext initializeJaxbContext() { try { - return JAXBContext.newInstance(JAXB_GENERATED_PATH, LoginIdentityProviderFactory.class.getClassLoader()); + return JAXBContext.newInstance(JAXB_GENERATED_PATH, IdentityProviderFactory.class.getClassLoader()); } catch (JAXBException e) { throw new RuntimeException("Unable to create JAXBContext."); } @@ -70,11 +68,11 @@ private static JAXBContext initializeJaxbContext() { private NiFiRegistryProperties properties; private ExtensionManager extensionManager; - private LoginIdentityProvider loginIdentityProvider; - private final Map loginIdentityProviders = new HashMap<>(); + private IdentityProvider identityProvider; + private final Map identityProviders = new HashMap<>(); @Autowired - public LoginIdentityProviderFactory(final NiFiRegistryProperties properties, final ExtensionManager extensionManager) { + public IdentityProviderFactory(final NiFiRegistryProperties properties, final ExtensionManager extensionManager) { this.properties = properties; this.extensionManager = extensionManager; @@ -88,13 +86,15 @@ public LoginIdentityProviderFactory(final NiFiRegistryProperties properties, fin } @Override - public LoginIdentityProvider getLoginIdentityProvider(String identifier) { - return loginIdentityProviders.get(identifier); + public IdentityProvider getIdentityProvider(String identifier) { + return identityProviders.get(identifier); } + @Primary @Bean - public LoginIdentityProvider getLoginIdentityProvider() throws Exception { - if (loginIdentityProvider == null) { +// @Bean("LoginIdentityProvider") + public IdentityProvider getIdentityProvider() throws Exception { + if (identityProvider == null) { // look up the login identity provider to use final String loginIdentityProviderIdentifier = properties.getProperty(NiFiRegistryProperties.SECURITY_IDENTITY_PROVIDER); @@ -104,32 +104,32 @@ public LoginIdentityProvider getLoginIdentityProvider() throws Exception { // create each login identity provider for (final Provider provider : loginIdentityProviderConfiguration.getProvider()) { - loginIdentityProviders.put(provider.getIdentifier(), createLoginIdentityProvider(provider.getIdentifier(), provider.getClazz())); + identityProviders.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()); + final IdentityProvider instance = identityProviders.get(provider.getIdentifier()); instance.onConfigured(loadLoginIdentityProviderConfiguration(provider)); } // get the login identity provider instance - loginIdentityProvider = getLoginIdentityProvider(loginIdentityProviderIdentifier); + identityProvider = getIdentityProvider(loginIdentityProviderIdentifier); // ensure it was found - if (loginIdentityProvider == null) { + if (identityProvider == null) { throw new Exception(String.format("The specified login identity provider '%s' could not be found.", loginIdentityProviderIdentifier)); } } } - return loginIdentityProvider; + return identityProvider; } @Override public void destroy() throws Exception { - if (loginIdentityProviders != null) { - loginIdentityProviders.entrySet().stream().forEach(e -> e.getValue().preDestruction()); + if (identityProviders != null) { + identityProviders.entrySet().stream().forEach(e -> e.getValue().preDestruction()); } } @@ -157,8 +157,8 @@ private IdentityProviders loadLoginIdentityProvidersConfiguration() throws Excep } } - private LoginIdentityProvider createLoginIdentityProvider(final String identifier, final String loginIdentityProviderClassName) throws Exception { - final LoginIdentityProvider instance; + private IdentityProvider createLoginIdentityProvider(final String identifier, final String loginIdentityProviderClassName) throws Exception { + final IdentityProvider instance; final ClassLoader classLoader = extensionManager.getExtensionClassLoader(loginIdentityProviderClassName); if (classLoader == null) { @@ -167,11 +167,11 @@ private LoginIdentityProvider createLoginIdentityProvider(final String identifie // attempt to load the class Class rawLoginIdentityProviderClass = Class.forName(loginIdentityProviderClassName, true, classLoader); - Class loginIdentityProviderClass = rawLoginIdentityProviderClass.asSubclass(LoginIdentityProvider.class); + Class loginIdentityProviderClass = rawLoginIdentityProviderClass.asSubclass(IdentityProvider.class); // otherwise create a new instance Constructor constructor = loginIdentityProviderClass.getConstructor(); - instance = (LoginIdentityProvider) constructor.newInstance(); + instance = (IdentityProvider) constructor.newInstance(); // method injection performMethodInjection(instance, loginIdentityProviderClass); @@ -179,27 +179,24 @@ private LoginIdentityProvider createLoginIdentityProvider(final String identifie // field injection performFieldInjection(instance, loginIdentityProviderClass); - // call post construction lifecycle event - instance.initialize(new StandardLoginIdentityProviderInitializationContext(identifier, this)); - return instance; } - private LoginIdentityProviderConfigurationContext loadLoginIdentityProviderConfiguration(final Provider provider) { + private IdentityProviderConfigurationContext loadLoginIdentityProviderConfiguration(final Provider provider) { final Map providerProperties = new HashMap<>(); for (final Property property : provider.getProperty()) { providerProperties.put(property.getName(), property.getValue()); } - return new StandardLoginIdentityProviderConfigurationContext(provider.getIdentifier(), providerProperties); + return new StandardIdentityProviderConfigurationContext(provider.getIdentifier(), this, providerProperties); } - private void performMethodInjection(final LoginIdentityProvider instance, final Class loginIdentityProviderClass) + private void performMethodInjection(final IdentityProvider instance, final Class loginIdentityProviderClass) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { for (final Method method : loginIdentityProviderClass.getMethods()) { - if (method.isAnnotationPresent(LoginIdentityProviderContext.class)) { + if (method.isAnnotationPresent(IdentityProviderContext.class)) { // make the method accessible final boolean isAccessible = method.isAccessible(); method.setAccessible(true); @@ -224,14 +221,14 @@ private void performMethodInjection(final LoginIdentityProvider instance, final } final Class parentClass = loginIdentityProviderClass.getSuperclass(); - if (parentClass != null && LoginIdentityProvider.class.isAssignableFrom(parentClass)) { + if (parentClass != null && IdentityProvider.class.isAssignableFrom(parentClass)) { performMethodInjection(instance, parentClass); } } - private void performFieldInjection(final LoginIdentityProvider instance, final Class loginIdentityProviderClass) throws IllegalArgumentException, IllegalAccessException { + private void performFieldInjection(final IdentityProvider instance, final Class loginIdentityProviderClass) throws IllegalArgumentException, IllegalAccessException { for (final Field field : loginIdentityProviderClass.getDeclaredFields()) { - if (field.isAnnotationPresent(LoginIdentityProviderContext.class)) { + if (field.isAnnotationPresent(IdentityProviderContext.class)) { // make the method accessible final boolean isAccessible = field.isAccessible(); field.setAccessible(true); @@ -256,7 +253,7 @@ private void performFieldInjection(final LoginIdentityProvider instance, final C } final Class parentClass = loginIdentityProviderClass.getSuperclass(); - if (parentClass != null && LoginIdentityProvider.class.isAssignableFrom(parentClass)) { + if (parentClass != null && IdentityProvider.class.isAssignableFrom(parentClass)) { performFieldInjection(instance, parentClass); } } diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/StandardLoginIdentityProviderConfigurationContext.java b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/StandardIdentityProviderConfigurationContext.java similarity index 73% rename from nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/StandardLoginIdentityProviderConfigurationContext.java rename to nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/StandardIdentityProviderConfigurationContext.java index 3a9cdd6d4..3e89dcce4 100644 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/StandardLoginIdentityProviderConfigurationContext.java +++ b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/StandardIdentityProviderConfigurationContext.java @@ -14,23 +14,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.nifi.registry.web.security.authentication; - -import org.apache.nifi.registry.security.authentication.LoginIdentityProviderConfigurationContext; +package org.apache.nifi.registry.security.authentication; import java.util.Collections; import java.util.Map; -/** - * - */ -public class StandardLoginIdentityProviderConfigurationContext implements LoginIdentityProviderConfigurationContext { +public class StandardIdentityProviderConfigurationContext implements IdentityProviderConfigurationContext { private final String identifier; + private final IdentityProviderLookup lookup; private final Map properties; - public StandardLoginIdentityProviderConfigurationContext(String identifier, Map properties) { + public StandardIdentityProviderConfigurationContext(String identifier, final IdentityProviderLookup lookup, Map properties) { this.identifier = identifier; + this.lookup = lookup; this.properties = properties; } @@ -39,6 +36,11 @@ public String getIdentifier() { return identifier; } + @Override + public IdentityProviderLookup getIdentityProviderLookup() { + return lookup; + } + @Override public Map getProperties() { return Collections.unmodifiableMap(properties); diff --git a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapIdentityProvider.java b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapIdentityProvider.java index 6beebc57f..5d1643ae9 100644 --- a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapIdentityProvider.java +++ b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapIdentityProvider.java @@ -17,15 +17,15 @@ package org.apache.nifi.registry.security.ldap; import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; import org.apache.nifi.registry.security.authentication.AuthenticationResponse; -import org.apache.nifi.registry.security.authentication.LoginCredentials; -import org.apache.nifi.registry.security.authentication.LoginIdentityProvider; -import org.apache.nifi.registry.security.authentication.LoginIdentityProviderConfigurationContext; -import org.apache.nifi.registry.security.authentication.LoginIdentityProviderInitializationContext; +import org.apache.nifi.registry.security.authentication.BasicAuthIdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProviderConfigurationContext; import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException; -import org.apache.nifi.registry.security.authentication.exception.InvalidLoginCredentialsException; -import org.apache.nifi.registry.security.authentication.exception.ProviderCreationException; -import org.apache.nifi.registry.security.authentication.exception.ProviderDestructionException; +import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; import org.apache.nifi.registry.security.util.SslContextFactory; import org.apache.nifi.registry.security.util.SslContextFactory.ClientAuth; import org.apache.nifi.registry.util.FormatUtils; @@ -60,33 +60,29 @@ import java.util.concurrent.TimeUnit; /** - * Abstract LDAP based implementation of a login identity provider. + * LDAP based implementation of a login identity provider. */ -public class LdapIdentityProvider implements LoginIdentityProvider { +public class LdapIdentityProvider extends BasicAuthIdentityProvider implements IdentityProvider { private static final Logger logger = LoggerFactory.getLogger(LdapIdentityProvider.class); - private AbstractLdapAuthenticationProvider provider; - private String issuer; + private static final String issuer = LdapIdentityProvider.class.getSimpleName(); + + private AbstractLdapAuthenticationProvider ldapAuthenticationProvider; private long expiration; private IdentityStrategy identityStrategy; @Override - public final void initialize(final LoginIdentityProviderInitializationContext initializationContext) throws ProviderCreationException { - this.issuer = getClass().getSimpleName(); - } - - @Override - public final void onConfigured(final LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException { + public final void onConfigured(final IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException { final String rawExpiration = configurationContext.getProperty("Authentication Expiration"); if (StringUtils.isBlank(rawExpiration)) { - throw new ProviderCreationException("The Authentication Expiration must be specified."); + throw new SecurityProviderCreationException("The Authentication Expiration must be specified."); } try { expiration = FormatUtils.getTimeDuration(rawExpiration, TimeUnit.MILLISECONDS); } catch (final IllegalArgumentException iae) { - throw new ProviderCreationException(String.format("The Expiration Duration '%s' is not a valid time duration", rawExpiration)); + throw new SecurityProviderCreationException(String.format("The Expiration Duration '%s' is not a valid time duration", rawExpiration)); } final LdapContextSource context = new LdapContextSource(); @@ -103,7 +99,7 @@ public final void onConfigured(final LoginIdentityProviderConfigurationContext c try { authenticationStrategy = LdapAuthenticationStrategy.valueOf(rawAuthenticationStrategy); } catch (final IllegalArgumentException iae) { - throw new ProviderCreationException(String.format("Unrecognized authentication strategy '%s'. Possible values are [%s]", + throw new SecurityProviderCreationException(String.format("Unrecognized authentication strategy '%s'. Possible values are [%s]", rawAuthenticationStrategy, StringUtils.join(LdapAuthenticationStrategy.values(), ", "))); } @@ -166,7 +162,7 @@ public final void onConfigured(final LoginIdentityProviderConfigurationContext c try { referralStrategy = ReferralStrategy.valueOf(rawReferralStrategy); } catch (final IllegalArgumentException iae) { - throw new ProviderCreationException(String.format("Unrecognized referral strategy '%s'. Possible values are [%s]", + throw new SecurityProviderCreationException(String.format("Unrecognized referral strategy '%s'. Possible values are [%s]", rawReferralStrategy, StringUtils.join(ReferralStrategy.values(), ", "))); } @@ -177,7 +173,7 @@ public final void onConfigured(final LoginIdentityProviderConfigurationContext c final String urls = configurationContext.getProperty("Url"); if (StringUtils.isBlank(urls)) { - throw new ProviderCreationException("LDAP identity provider 'Url' must be specified."); + throw new SecurityProviderCreationException("LDAP identity provider 'Url' must be specified."); } // connection @@ -188,7 +184,7 @@ public final void onConfigured(final LoginIdentityProviderConfigurationContext c final String userSearchFilter = configurationContext.getProperty("User Search Filter"); if (StringUtils.isBlank(userSearchBase) || StringUtils.isBlank(userSearchFilter)) { - throw new ProviderCreationException("LDAP identity provider 'User Search Base' and 'User Search Filter' must be specified."); + throw new SecurityProviderCreationException("LDAP identity provider 'User Search Base' and 'User Search Filter' must be specified."); } final LdapUserSearch userSearch = new FilterBasedLdapUserSearch(userSearchBase, userSearchFilter, context); @@ -210,7 +206,7 @@ public final void onConfigured(final LoginIdentityProviderConfigurationContext c // attempt to get the configured identity strategy identityStrategy = IdentityStrategy.valueOf(rawIdentityStrategy); } catch (final IllegalArgumentException iae) { - throw new ProviderCreationException(String.format("Unrecognized identity strategy '%s'. Possible values are [%s]", + throw new SecurityProviderCreationException(String.format("Unrecognized identity strategy '%s'. Possible values are [%s]", rawIdentityStrategy, StringUtils.join(IdentityStrategy.values(), ", "))); } } @@ -225,17 +221,68 @@ public final void onConfigured(final LoginIdentityProviderConfigurationContext c context.afterPropertiesSet(); authenticator.afterPropertiesSet(); } catch (final Exception e) { - throw new ProviderCreationException(e.getMessage(), e); + throw new SecurityProviderCreationException(e.getMessage(), e); } // create the underlying provider - provider = new LdapAuthenticationProvider(authenticator); + ldapAuthenticationProvider = new LdapAuthenticationProvider(authenticator); } - private void setTimeout(final LoginIdentityProviderConfigurationContext configurationContext, - final Map baseEnvironment, - final String configurationProperty, - final String environmentKey) { + @Override + public AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) throws InvalidCredentialsException, IdentityAccessException { + if (ldapAuthenticationProvider == null) { + throw new IdentityAccessException("The LDAP authentication provider is not initialized."); + } + + try { + final String username = authenticationRequest.getUsername(); + final Object credentials = authenticationRequest.getCredentials(); + final String password = credentials != null && credentials instanceof String ? (String) credentials : null; + + // perform the authentication + final UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, credentials); + final Authentication authentication = ldapAuthenticationProvider.authenticate(token); + + // use dn if configured + if (IdentityStrategy.USE_DN.equals(identityStrategy)) { + // attempt to get the ldap user details to get the DN + if (authentication.getPrincipal() instanceof LdapUserDetails) { + final LdapUserDetails userDetails = (LdapUserDetails) authentication.getPrincipal(); + return new AuthenticationResponse(userDetails.getDn(), username, expiration, issuer); + } else { + logger.warn(String.format("Unable to determine user DN for %s, using username.", authentication.getName())); + return new AuthenticationResponse(authentication.getName(), username, expiration, issuer); + } + } else { + return new AuthenticationResponse(authentication.getName(), username, expiration, issuer); + } + } catch (final BadCredentialsException | UsernameNotFoundException | AuthenticationException e) { + throw new InvalidCredentialsException(e.getMessage(), e); + } catch (final Exception e) { + // there appears to be a bug that generates a InternalAuthenticationServiceException wrapped around an AuthenticationException. this + // shouldn't be the case as they the service exception suggestions that something was wrong with the service. while the authentication + // exception suggests that username and/or credentials were incorrect. checking the cause seems to address this scenario. + final Throwable cause = e.getCause(); + if (cause instanceof AuthenticationException) { + throw new InvalidCredentialsException(e.getMessage(), e); + } + + logger.error(e.getMessage()); + if (logger.isDebugEnabled()) { + logger.debug(StringUtils.EMPTY, e); + } + throw new IdentityAccessException("Unable to validate the supplied credentials. Please contact the system administrator.", e); + } + } + + @Override + public final void preDestruction() throws SecurityProviderDestructionException { + } + + private void setTimeout(final IdentityProviderConfigurationContext configurationContext, + final Map baseEnvironment, + final String configurationProperty, + final String environmentKey) { final String rawTimeout = configurationContext.getProperty(configurationProperty); if (StringUtils.isNotBlank(rawTimeout)) { @@ -243,12 +290,12 @@ private void setTimeout(final LoginIdentityProviderConfigurationContext configur final Long timeout = FormatUtils.getTimeDuration(rawTimeout, TimeUnit.MILLISECONDS); baseEnvironment.put(environmentKey, timeout.toString()); } catch (final IllegalArgumentException iae) { - throw new ProviderCreationException(String.format("The %s '%s' is not a valid time duration", configurationProperty, rawTimeout)); + throw new SecurityProviderCreationException(String.format("The %s '%s' is not a valid time duration", configurationProperty, rawTimeout)); } } } - private SSLContext getConfiguredSslContext(final LoginIdentityProviderConfigurationContext configurationContext) { + private SSLContext getConfiguredSslContext(final IdentityProviderConfigurationContext configurationContext) { final String rawKeystore = configurationContext.getProperty("TLS - Keystore"); final String rawKeystorePassword = configurationContext.getProperty("TLS - Keystore Password"); final String rawKeystoreType = configurationContext.getProperty("TLS - Keystore Type"); @@ -266,7 +313,7 @@ private SSLContext getConfiguredSslContext(final LoginIdentityProviderConfigurat } else { // ensure the protocol is specified if (StringUtils.isBlank(rawProtocol)) { - throw new ProviderCreationException("TLS - Protocol must be specified."); + throw new SecurityProviderCreationException("TLS - Protocol must be specified."); } if (StringUtils.isBlank(rawKeystore)) { @@ -282,7 +329,7 @@ private SSLContext getConfiguredSslContext(final LoginIdentityProviderConfigurat try { clientAuth = ClientAuth.valueOf(rawClientAuth); } catch (final IllegalArgumentException iae) { - throw new ProviderCreationException(String.format("Unrecognized client auth '%s'. Possible values are [%s]", + throw new SecurityProviderCreationException(String.format("Unrecognized client auth '%s'. Possible values are [%s]", rawClientAuth, StringUtils.join(ClientAuth.values(), ", "))); } } @@ -292,57 +339,10 @@ private SSLContext getConfiguredSslContext(final LoginIdentityProviderConfigurat } } } catch (final KeyStoreException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | KeyManagementException | IOException e) { - throw new ProviderCreationException(e.getMessage(), e); + throw new SecurityProviderCreationException(e.getMessage(), e); } return sslContext; } - @Override - public final AuthenticationResponse authenticate(final LoginCredentials credentials) throws InvalidLoginCredentialsException, IdentityAccessException { - if (provider == null) { - throw new IdentityAccessException("The LDAP authentication provider is not initialized."); - } - - try { - // perform the authentication - final UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(credentials.getUsername(), credentials.getPassword()); - final Authentication authentication = provider.authenticate(token); - - // use dn if configured - if (IdentityStrategy.USE_DN.equals(identityStrategy)) { - // attempt to get the ldap user details to get the DN - if (authentication.getPrincipal() instanceof LdapUserDetails) { - final LdapUserDetails userDetails = (LdapUserDetails) authentication.getPrincipal(); - return new AuthenticationResponse(userDetails.getDn(), credentials.getUsername(), expiration, issuer); - } else { - logger.warn(String.format("Unable to determine user DN for %s, using username.", authentication.getName())); - return new AuthenticationResponse(authentication.getName(), credentials.getUsername(), expiration, issuer); - } - } else { - return new AuthenticationResponse(authentication.getName(), credentials.getUsername(), expiration, issuer); - } - } catch (final BadCredentialsException | UsernameNotFoundException | AuthenticationException e) { - throw new InvalidLoginCredentialsException(e.getMessage(), e); - } catch (final Exception e) { - // there appears to be a bug that generates a InternalAuthenticationServiceException wrapped around an AuthenticationException. this - // shouldn't be the case as they the service exception suggestions that something was wrong with the service. while the authentication - // exception suggests that username and/or credentials were incorrect. checking the cause seems to address this scenario. - final Throwable cause = e.getCause(); - if (cause instanceof AuthenticationException) { - throw new InvalidLoginCredentialsException(e.getMessage(), e); - } - - logger.error(e.getMessage()); - if (logger.isDebugEnabled()) { - logger.debug(StringUtils.EMPTY, e); - } - throw new IdentityAccessException("Unable to validate the supplied credentials. Please contact the system administrator.", e); - } - } - - @Override - public final void preDestruction() throws ProviderDestructionException { - } - } diff --git a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProvider.java b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProvider.java index af10ece41..71bee9a90 100644 --- a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProvider.java +++ b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProvider.java @@ -20,7 +20,6 @@ import org.apache.nifi.registry.properties.NiFiRegistryProperties; import org.apache.nifi.registry.properties.util.IdentityMapping; import org.apache.nifi.registry.properties.util.IdentityMappingUtil; -import org.apache.nifi.registry.security.authentication.exception.ProviderDestructionException; import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; import org.apache.nifi.registry.security.authorization.Group; import org.apache.nifi.registry.security.authorization.User; @@ -30,6 +29,7 @@ import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext; import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; import org.apache.nifi.registry.security.authorization.exception.AuthorizerCreationException; +import org.apache.nifi.registry.security.authorization.exception.AuthorizerDestructionException; import org.apache.nifi.registry.security.ldap.LdapAuthenticationStrategy; import org.apache.nifi.registry.security.ldap.LdapsSocketFactory; import org.apache.nifi.registry.security.ldap.ReferralStrategy; @@ -113,8 +113,6 @@ public class LdapUserGroupProvider implements UserGroupProvider { public static final String PROP_SYNC_INTERVAL = "Sync Interval"; - //private AuthorizerConfigurationContext configurationContext; - private List identityMappings; private NiFiRegistryProperties properties; @@ -669,7 +667,7 @@ public void setNiFiProperties(NiFiRegistryProperties properties) { } @Override - public final void preDestruction() throws ProviderDestructionException { + public final void preDestruction() throws AuthorizerDestructionException { ldapSync.shutdown(); try { if (!ldapSync.awaitTermination(10000, TimeUnit.MILLISECONDS)) { diff --git a/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.LoginIdentityProvider b/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.IdentityProvider similarity index 100% rename from nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.LoginIdentityProvider rename to nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.IdentityProvider diff --git a/nifi-registry-web-api/src/main/xsd/identity-providers.xsd b/nifi-registry-framework/src/main/xsd/identity-providers.xsd similarity index 100% rename from nifi-registry-web-api/src/main/xsd/identity-providers.xsd rename to nifi-registry-framework/src/main/xsd/identity-providers.xsd diff --git a/nifi-registry-security-api/pom.xml b/nifi-registry-security-api/pom.xml index 564a435c1..b1bcb470c 100644 --- a/nifi-registry-security-api/pom.xml +++ b/nifi-registry-security-api/pom.xml @@ -32,6 +32,12 @@ org.apache.nifi.registry nifi-registry-utils + + javax.servlet + javax.servlet-api + 3.1.0 + provided + \ No newline at end of file diff --git a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationRequest.java b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationRequest.java new file mode 100644 index 000000000..72ae50e06 --- /dev/null +++ b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationRequest.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.security.authentication; + +import java.io.Serializable; + +public class AuthenticationRequest implements Serializable { + + private String username; + private Object credentials; + private Object details; + + public AuthenticationRequest(String username, Object credentials, Object details) { + this.username = username; + this.credentials = credentials; + this.details = details; + } + + public AuthenticationRequest() {} + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public Object getCredentials() { + return credentials; + } + + public void setCredentials(Object credentials) { + this.credentials = credentials; + } + + public Object getDetails() { + return details; + } + + public void setDetails(Object details) { + this.details = details; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AuthenticationRequest that = (AuthenticationRequest) o; + + return username != null ? username.equals(that.username) : that.username == null; + } + + @Override + public int hashCode() { + return username != null ? username.hashCode() : 0; + } + + @Override + public String toString() { + return "AuthenticationRequest{" + + "username='" + username + '\'' + + ", credentials=[PROTECTED]" + + ", details=" + details + + '}'; + } +} diff --git a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationResponse.java b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationResponse.java index e6bfeb244..b8eb72183 100644 --- a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationResponse.java +++ b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationResponse.java @@ -16,10 +16,12 @@ */ package org.apache.nifi.registry.security.authentication; +import java.io.Serializable; + /** * Authentication response for a user login attempt. */ -public class AuthenticationResponse { +public class AuthenticationResponse implements Serializable { private final String identity; private final String username; @@ -62,4 +64,35 @@ public long getExpiration() { return expiration; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AuthenticationResponse that = (AuthenticationResponse) o; + + if (expiration != that.expiration) return false; + if (identity != null ? !identity.equals(that.identity) : that.identity != null) return false; + if (username != null ? !username.equals(that.username) : that.username != null) return false; + return issuer != null ? issuer.equals(that.issuer) : that.issuer == null; + } + + @Override + public int hashCode() { + int result = identity != null ? identity.hashCode() : 0; + result = 31 * result + (username != null ? username.hashCode() : 0); + result = 31 * result + (int) (expiration ^ (expiration >>> 32)); + result = 31 * result + (issuer != null ? issuer.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "AuthenticationResponse{" + + "identity='" + identity + '\'' + + ", username='" + username + '\'' + + ", expiration=" + expiration + + ", issuer='" + issuer + '\'' + + '}'; + } } diff --git a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BasicAuthIdentityProvider.java b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BasicAuthIdentityProvider.java new file mode 100644 index 000000000..10a106504 --- /dev/null +++ b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BasicAuthIdentityProvider.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.security.authentication; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import java.nio.charset.Charset; +import java.util.Base64; + +public abstract class BasicAuthIdentityProvider implements IdentityProvider { + + public static final String AUTHORIZATION = "Authorization"; + public static final String BASIC = "Basic "; + + private static final Logger logger = LoggerFactory.getLogger(BasicAuthIdentityProvider.class); + + private static final IdentityProviderUsage usage = new IdentityProviderUsage() { + @Override + public String getText() { + return "The user credentials must be passed in standard HTTP Basic Auth format. " + + "That is: 'Authorization: Basic ', " + + "where is the base64 encoded value of ':'."; + } + }; + + @Override + public IdentityProviderUsage getUsageInstructions() { + return usage; + } + + @Override + public AuthenticationRequest extractCredentials(HttpServletRequest servletRequest) { + + if (servletRequest == null) { + logger.debug("Cannot extract user credentials from null servletRequest"); + return null; + } + + // only support this type of login when running securely + if (!servletRequest.isSecure()) { + return null; + } + + AuthenticationRequest authenticationRequest; + + try { + + final String authorization = servletRequest.getHeader(AUTHORIZATION); + if (authorization == null || !authorization.startsWith(BASIC)) { + logger.debug("HTTP Basic Auth credentials not present. Not attempting to extract credentials for authentication."); + return null; + } + + // Authorization: Basic {base64credentials} + String base64Credentials = authorization.substring(BASIC.length()).trim(); + String credentials = new String(Base64.getDecoder().decode(base64Credentials), Charset.forName("UTF-8")); + // credentials = username:password + final String[] credentialParts = credentials.split(":", 2); + String username = credentialParts[0]; + String password = credentialParts[1]; + + authenticationRequest = new UsernamePasswordAuthenticationRequest(username, password); + + } catch (IllegalArgumentException | IndexOutOfBoundsException e) { + logger.info("Failed to extract user identity credentials."); + logger.debug("", e); + return null; + } + + return authenticationRequest; + + } + + @Override + public boolean supports(Class authenticationRequestClazz) { + return UsernamePasswordAuthenticationRequest.class.isAssignableFrom(authenticationRequestClazz); + } + +} diff --git a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BearerAuthIdentityProvider.java b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BearerAuthIdentityProvider.java new file mode 100644 index 000000000..5d08e9a8f --- /dev/null +++ b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BearerAuthIdentityProvider.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.security.authentication; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; + +public abstract class BearerAuthIdentityProvider implements IdentityProvider { + + public static final String AUTHORIZATION = "Authorization"; + public static final String BEARER = "Bearer "; + + private static final Logger logger = LoggerFactory.getLogger(BearerAuthIdentityProvider.class); + + private static final IdentityProviderUsage usage = new IdentityProviderUsage() { + @Override + public String getText() { + return "The user credentials must be passed in standard HTTP Bearer Authorization format. " + + "That is: 'Authorization: Bearer ', " + + "where is a value that will be validated by this identity provider."; + } + }; + + @Override + public IdentityProviderUsage getUsageInstructions() { + return usage; + } + + @Override + public AuthenticationRequest extractCredentials(HttpServletRequest request) { + + if (request == null) { + logger.debug("Cannot extract user credentials from null servletRequest"); + return null; + } + + // only support this type of login when running securely + if (!request.isSecure()) { + return null; + } + + // get the principal out of the user token + final String authorization = request.getHeader(AUTHORIZATION); + if (authorization == null || !authorization.startsWith(BEARER)) { + logger.debug("HTTP Bearer Auth credentials not present. Not attempting to extract credentials for authentication."); + return null; + } + + // Extract the encoded token from the Authorization header + final String token = authorization.substring(BEARER.length()).trim(); + + return new AuthenticationRequest(null, token, null); + + } + +} diff --git a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProvider.java b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProvider.java new file mode 100644 index 000000000..88488fbe6 --- /dev/null +++ b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProvider.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.security.authentication; + +import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException; +import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; + +import javax.servlet.http.HttpServletRequest; + +/** + * IdentityProvider is an interface for a class that is able to establish a client identity. + * + * Specifically, this provider can: + * - extract credentials from an HttpServletRequest (eg, parse a header, form parameter, or client certificates) + * - authenticate those credentials and map them to an authenticated identity value + * (eg, determine a username given a valid auth token) + */ +public interface IdentityProvider { + + /** + * @return an IdentityProviderUsage that describes the expectations of the inputs + * to {@link #authenticate(AuthenticationRequest)} + */ + IdentityProviderUsage getUsageInstructions(); + + /** + * Extracts credentials from an {@link HttpServletRequest}. + * + * First, a check to the HttpServletRequest should be made to determine if this IdentityProvider is + * well suited to authenticate the request. For example, if the IdentityProvider is designed to read + * a particular header field to look for a token or identity claim, the check might be that the proper + * header field exists and (if a shared header field, such as "Authorization") that the format of the + * value in the header matches the expected format for this identity provider (e.g., must start with + * a prefix such as "Bearer"). Note, the expectations of the HttpServletRequest can be described by + * the {@link #getUsageInstructions()} method. + * + * If this check fails, this method should return null. This will indicate to the framework that the + * IdentityProvider does not recognize an identity claim present in the HttpServletRequest and that + * the framework should try another IdentityProvider. + * + * If the identity claim format is recognized, it should be extracted and returned in an + * {@link AuthenticationRequest}. The types and values set in the {@link AuthenticationRequest} are + * left to the discretion of the IdentityProvider, as the intended audience of the request is the + * {@link #authenticate(AuthenticationRequest)} method, where the corresponding logic to interpret + * an {@link AuthenticationRequest} can be implemented. As a rule of thumb, any values that could be considered + * sensitive, such as a password or persistent token susceptible to replay attacks, should be stored + * in the credentials field of the {@link AuthenticationRequest} as the framework will make the most effort + * to protect that value, including obscuring it in toString() output. + * + * If the {@link AuthenticationRequest} is insufficient or too generic for this IdentityProvider implementation, + * this IdentityProvider may subclass {@link AuthenticationRequest} to create a credentials-bearing request + * object that is better suited for this IdentityProvider implementation. In that case, the implementation + * might wish to also override the {@link #supports(Class)} method to indicate what types of request + * objects it supports in the call to {@link #authenticate(AuthenticationRequest)}. + * + * If credential location is recognized in the {@link HttpServletRequest} but extraction fails, + * in most cases that exceptional case should be caught, logged, and null should be returned, as it + * is possible another IdentityProvider will be able to parse the credentials or find a separate + * set of credentials in the {@link HttpServletRequest} (e.g., a request containing an Authorization + * header and a client certificate.) + * + * @param servletRequest the {@link HttpServletRequest} request that may contain credentials + * understood by this IdentityProvider + * @return an AuthenticationRequest containing the extracted credentials in a format this + * IdentityProvider understands, or null if no credentials could be found in or extracted + * successfully from the servletRequest + */ + AuthenticationRequest extractCredentials(HttpServletRequest servletRequest); + + /** + * Authenticates the credentials passed in the {@link AuthenticationRequest}. + * + * In typical usage, the AuthenticationRequest argument is expected to originate from this + * IdentityProvider's {@link #extractCredentials} method, so the logic for interpreting the + * values in the {@link AuthenticationRequest} should correspond to how the {@link AuthenticationRequest} + * is formed there. + * + * The first step of authentication should be to check if the credentials are understandable + * by this IdentityProvider. If this check fails, this method should return null. This will + * indicate to the framework that the IdentityProvider is not able to make a judgement call + * on if the request can be authenticated, and the framework can check with another IdentityProvider + * if one is available. + * + * If this IdentityProvider is able to interpret the AuthenticationRequest, it should perform + * and authentication check. If the authentication check fails, an exception should be thrown. + * Use an {@link InvalidCredentialsException} if the authentication check completed and the + * credentials failed authentication. Use an {@link IdentityAccessException} if a dependency + * service or provider fails, such as an failure to read a persistent store of identity or + * credential data. Either exception type will indicate to the framework that this IdentityProvider's + * opinion is that the client making the request should be blocked from accessing a resource + * that requires authentication. (Versus a null return value, which is an indication that this + * IdentityProvider is not well suited to make a judgement call one way or the other.) + * + * @param authenticationRequest the request, containing identity claim credentials for the + * IdentityProvider to authenticate and determine an identity + * @return The authentication response containing a fully populated identity value, + * or null if identity cannot be determined + * @throws InvalidCredentialsException The login credentials were interpretable by this + * IdentityProvider and failed authentication + * @throws IdentityAccessException Unable to assign an identity due to an issue accessing + * underlying storage or service + */ + AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) + throws InvalidCredentialsException, IdentityAccessException; + + /** + * Allows this IdentityProvider to declare support for specific subclasses of {@link AuthenticationRequest}. + * + * In normal usage, only an AuthenticationRequest originating from this IdentityProvider's + * {@link #extractCredentials(HttpServletRequest)} method will be passed to {@link #authenticate(AuthenticationRequest)}. + * However, when IdentityProviders are used with another framework, + * another component may formulate the AuthenticationRequest to pass to the + * {@link #authenticate(AuthenticationRequest)} method. This allows a caller to + * check if the IdentityProvider can support the AuthenticationRequest class. + * If the caller knows the IdentityProvider can support the AuthenticationRequest + * (e.g., it was generated by calling {@link #extractCredentials(HttpServletRequest)}, + * this check is optional and does not need to be performed. + * + * @param authenticationRequestClazz the class the caller wants to check + * @return a boolean value indicating if this IdentityProvider supports authenticationRequestClazz + */ + default boolean supports(Class authenticationRequestClazz) { + return AuthenticationRequest.class.equals(authenticationRequestClazz); + } + + /** + * Called to configure the AuthorityProvider after instance creation. + * + * @param configurationContext at the time of configuration + * @throws SecurityProviderCreationException for any issues configuring the provider + */ + void onConfigured(IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException; + + /** + * Called immediately before instance destruction for implementers to release resources. + * + * @throws SecurityProviderDestructionException If pre-destruction fails. + */ + void preDestruction() throws SecurityProviderDestructionException; + +} diff --git a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/LoginIdentityProviderConfigurationContext.java b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderConfigurationContext.java similarity index 89% rename from nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/LoginIdentityProviderConfigurationContext.java rename to nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderConfigurationContext.java index a7f21be6e..6be02077d 100644 --- a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/LoginIdentityProviderConfigurationContext.java +++ b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderConfigurationContext.java @@ -18,16 +18,18 @@ import java.util.Map; -/** - * - */ -public interface LoginIdentityProviderConfigurationContext { +public interface IdentityProviderConfigurationContext { /** * @return identifier for the authority provider */ String getIdentifier(); + /** + * @return the IdentityProviderLookup from the factory context + */ + public IdentityProviderLookup getIdentityProviderLookup(); + /** * Retrieves all properties the component currently understands regardless * of whether a value has been set for them or not. If no value is present diff --git a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/LoginIdentityProviderLookup.java b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderLookup.java similarity index 87% rename from nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/LoginIdentityProviderLookup.java rename to nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderLookup.java index 8720bbaa5..dbf6d58cd 100644 --- a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/LoginIdentityProviderLookup.java +++ b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderLookup.java @@ -16,8 +16,8 @@ */ package org.apache.nifi.registry.security.authentication; -public interface LoginIdentityProviderLookup { +public interface IdentityProviderLookup { - LoginIdentityProvider getLoginIdentityProvider(String identifier); + IdentityProvider getIdentityProvider(String identifier); } diff --git a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/LoginCredentials.java b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderUsage.java similarity index 67% rename from nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/LoginCredentials.java rename to nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderUsage.java index 925d36dab..98eea95df 100644 --- a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/LoginCredentials.java +++ b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderUsage.java @@ -16,24 +16,17 @@ */ package org.apache.nifi.registry.security.authentication; -/** - * Login credentials for a user. - */ -public class LoginCredentials { - - private final String username; - private final String password; - - public LoginCredentials(String username, String password) { - this.username = username; - this.password = password; - } +public interface IdentityProviderUsage { - public String getUsername() { - return username; - } + /** + * Provides the usage instructions for an identity provider. + * + * The instructions should target a human consumer of the + * NiFi Registry REST API that needs to know how to handle + * Authentication when using / programming an API client. + * + * @return the usage instructions for an identity provider + */ + String getText(); - public String getPassword() { - return password; - } } diff --git a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/LoginIdentityProvider.java b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/LoginIdentityProvider.java deleted file mode 100644 index b74069ac7..000000000 --- a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/LoginIdentityProvider.java +++ /dev/null @@ -1,61 +0,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. - */ -package org.apache.nifi.registry.security.authentication; - -import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException; -import org.apache.nifi.registry.security.authentication.exception.InvalidLoginCredentialsException; -import org.apache.nifi.registry.security.authentication.exception.ProviderCreationException; -import org.apache.nifi.registry.security.authentication.exception.ProviderDestructionException; - -/** - * Identity provider that is able to authentication a user with username/password credentials. - */ -public interface LoginIdentityProvider { - - /** - * Authenticates the specified login credentials. - * - * @param credentials the credentials - * @return The authentication response - * @throws InvalidLoginCredentialsException The login credentials were invalid - * @throws IdentityAccessException Unable to register the user due to an issue accessing the underlying storage - */ - AuthenticationResponse authenticate(LoginCredentials credentials) throws InvalidLoginCredentialsException, IdentityAccessException; - - /** - * Called immediately after instance creation for implementers to perform additional setup - * - * @param initializationContext in which to initialize - * @throws ProviderCreationException Unable to initialize - */ - void initialize(LoginIdentityProviderInitializationContext initializationContext) throws ProviderCreationException; - - /** - * Called to configure the AuthorityProvider. - * - * @param configurationContext at the time of configuration - * @throws ProviderCreationException for any issues configuring the provider - */ - void onConfigured(LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException; - - /** - * Called immediately before instance destruction for implementers to release resources. - * - * @throws ProviderDestructionException If pre-destruction fails. - */ - void preDestruction() throws ProviderDestructionException; -} diff --git a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/LoginIdentityProviderInitializationContext.java b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/UsernamePasswordAuthenticationRequest.java similarity index 80% rename from nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/LoginIdentityProviderInitializationContext.java rename to nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/UsernamePasswordAuthenticationRequest.java index 755c2e86f..3abcf9446 100644 --- a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/LoginIdentityProviderInitializationContext.java +++ b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/UsernamePasswordAuthenticationRequest.java @@ -16,12 +16,10 @@ */ package org.apache.nifi.registry.security.authentication; -/** - * - */ -public interface LoginIdentityProviderInitializationContext { +public class UsernamePasswordAuthenticationRequest extends AuthenticationRequest { - public String getIdentifier(); + public UsernamePasswordAuthenticationRequest(String username, String password) { + super(username, password, null); + } - public LoginIdentityProviderLookup getAuthorityProviderLookup(); } diff --git a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/annotation/LoginIdentityProviderContext.java b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/annotation/IdentityProviderContext.java similarity index 95% rename from nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/annotation/LoginIdentityProviderContext.java rename to nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/annotation/IdentityProviderContext.java index 1c4d17fa0..8d0ddf0a1 100644 --- a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/annotation/LoginIdentityProviderContext.java +++ b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/annotation/IdentityProviderContext.java @@ -23,13 +23,9 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * - * - */ @Documented @Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited -public @interface LoginIdentityProviderContext { +public @interface IdentityProviderContext { } diff --git a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/InvalidLoginCredentialsException.java b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/InvalidCredentialsException.java similarity index 80% rename from nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/InvalidLoginCredentialsException.java rename to nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/InvalidCredentialsException.java index c43285772..e7c7339fc 100644 --- a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/InvalidLoginCredentialsException.java +++ b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/InvalidCredentialsException.java @@ -18,15 +18,15 @@ /** * Represents the case when the identity could not be confirmed because the - * login credentials were invalid. + * identity claim credentials were invalid. */ -public class InvalidLoginCredentialsException extends RuntimeException { +public class InvalidCredentialsException extends RuntimeException { - public InvalidLoginCredentialsException(String message, Throwable cause) { + public InvalidCredentialsException(String message, Throwable cause) { super(message, cause); } - public InvalidLoginCredentialsException(String message) { + public InvalidCredentialsException(String message) { super(message); } diff --git a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AuthorizerCreationException.java b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AuthorizerCreationException.java index 4d34cf5cb..dbe90e7d6 100644 --- a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AuthorizerCreationException.java +++ b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AuthorizerCreationException.java @@ -18,8 +18,9 @@ /** * Represents the exceptional case when an Authorizer fails instantiation. - * + * @deprecated Migrate to use SecurityProviderCreationException */ +@Deprecated // TODO migrate all usages to SecurityProviderCreationException public class AuthorizerCreationException extends RuntimeException { public AuthorizerCreationException() { diff --git a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AuthorizerDestructionException.java b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AuthorizerDestructionException.java index fbec6d367..6892445d8 100644 --- a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AuthorizerDestructionException.java +++ b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AuthorizerDestructionException.java @@ -18,8 +18,9 @@ /** * Represents the exceptional case when an Authorizer fails destruction. - * + * @deprecated Migrate to use SecurityProviderDestructionException */ +@Deprecated // TODO migrate all usages to SecurityProviderDestructionException public class AuthorizerDestructionException extends RuntimeException { public AuthorizerDestructionException() { diff --git a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/ProviderCreationException.java b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderCreationException.java similarity index 66% rename from nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/ProviderCreationException.java rename to nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderCreationException.java index 12844cec0..01531d6c5 100644 --- a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/ProviderCreationException.java +++ b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderCreationException.java @@ -14,26 +14,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.nifi.registry.security.authentication.exception; +package org.apache.nifi.registry.security.exception; /** - * Represents the exceptional case when an AuthorityProvider fails instantiated. - * + * Represents the exceptional case when a security api provider fails instantiation. */ -public class ProviderCreationException extends RuntimeException { +public class SecurityProviderCreationException extends RuntimeException { - public ProviderCreationException() { + public SecurityProviderCreationException() { } - public ProviderCreationException(String msg) { + public SecurityProviderCreationException(String msg) { super(msg); } - public ProviderCreationException(Throwable cause) { + public SecurityProviderCreationException(Throwable cause) { super(cause); } - public ProviderCreationException(String msg, Throwable cause) { + public SecurityProviderCreationException(String msg, Throwable cause) { super(msg, cause); } } diff --git a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/ProviderDestructionException.java b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderDestructionException.java similarity index 65% rename from nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/ProviderDestructionException.java rename to nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderDestructionException.java index 8a0157b6c..337062363 100644 --- a/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/ProviderDestructionException.java +++ b/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderDestructionException.java @@ -14,26 +14,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.nifi.registry.security.authentication.exception; +package org.apache.nifi.registry.security.exception; /** - * Represents the exceptional case when an AuthorityProvider fails destruction. - * + * Represents the exceptional case when a security api provider fails destruction. */ -public class ProviderDestructionException extends RuntimeException { +public class SecurityProviderDestructionException extends RuntimeException { - public ProviderDestructionException() { + public SecurityProviderDestructionException() { } - public ProviderDestructionException(String msg) { + public SecurityProviderDestructionException(String msg) { super(msg); } - public ProviderDestructionException(Throwable cause) { + public SecurityProviderDestructionException(Throwable cause) { super(cause); } - public ProviderDestructionException(String msg, Throwable cause) { + public SecurityProviderDestructionException(String msg, Throwable cause) { super(msg, cause); } } diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/ProxiedEntitiesUtils.java b/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java similarity index 74% rename from nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/ProxiedEntitiesUtils.java rename to nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java index 05687f8dd..f85034163 100644 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/ProxiedEntitiesUtils.java +++ b/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java @@ -14,26 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.nifi.registry.web.security.authentication; +package org.apache.nifi.registry.security.util; import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.registry.security.authorization.user.NiFiUser; -import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; -/** - * - */ public class ProxiedEntitiesUtils { private static final Logger logger = LoggerFactory.getLogger(ProxiedEntitiesUtils.class); @@ -133,31 +124,4 @@ public static List tokenizeProxiedEntitiesChain(String rawProxyChain) { return proxyChain; } - /** - * Builds the proxy chain for the specified user. - * - * @param user The current user - * @return The proxy chain for that user in String form - */ - public static String buildProxiedEntitiesChainString(final NiFiUser user) { - // calculate the dn chain - List proxyChain = NiFiUserUtils.buildProxiedEntitiesChain(user); - if (proxyChain.isEmpty()) { - return ANONYMOUS_CHAIN; - } - proxyChain = proxyChain.stream().map(ProxiedEntitiesUtils::formatProxyDn).collect(Collectors.toList()); - return StringUtils.join(proxyChain, ""); - } - - public static void successfulAuthorization(HttpServletRequest request, HttpServletResponse response, Authentication authResult) { - if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) { - response.setHeader(PROXY_ENTITIES_ACCEPTED, Boolean.TRUE.toString()); - } - } - - public static void unsuccessfulAuthorization(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) { - if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) { - response.setHeader(PROXY_ENTITIES_DETAILS, failed.getMessage()); - } - } } diff --git a/nifi-registry-web-api/pom.xml b/nifi-registry-web-api/pom.xml index ab7dd8474..203b58de5 100644 --- a/nifi-registry-web-api/pom.xml +++ b/nifi-registry-web-api/pom.xml @@ -31,9 +31,6 @@ src/main/resources - - src/main/xsd - @@ -47,28 +44,6 @@ false - - org.codehaus.mojo - jaxb2-maven-plugin - - - identity-providers - - xjc - - - org.apache.nifi.registry.security.authentication.generated - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - **/authentication/generated/*.java, - - com.github.kongchen swagger-maven-plugin diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java index 2732d5a62..84693b10f 100644 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java @@ -23,16 +23,16 @@ import org.apache.commons.lang3.StringUtils; import org.apache.nifi.registry.exception.AdministrationException; import org.apache.nifi.registry.model.authorization.AccessStatus; +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; import org.apache.nifi.registry.security.authentication.AuthenticationResponse; -import org.apache.nifi.registry.security.authentication.LoginCredentials; -import org.apache.nifi.registry.security.authentication.LoginIdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.security.authentication.UsernamePasswordAuthenticationRequest; import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException; -import org.apache.nifi.registry.security.authentication.exception.InvalidLoginCredentialsException; +import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException; import org.apache.nifi.registry.security.authorization.user.NiFiUser; import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils; import org.apache.nifi.registry.service.AuthorizationService; import org.apache.nifi.registry.web.security.authentication.jwt.JwtService; -import org.apache.nifi.registry.web.security.authentication.token.LoginAuthenticationToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -49,7 +49,6 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.net.URI; -import java.util.concurrent.TimeUnit; @Component @Path("/access") @@ -61,16 +60,16 @@ public class AccessResource extends ApplicationResource { private static final Logger logger = LoggerFactory.getLogger(AccessResource.class); - private LoginIdentityProvider loginIdentityProvider; + private IdentityProvider identityProvider; private JwtService jwtService; @Autowired public AccessResource( AuthorizationService authorizationService, JwtService jwtService, - LoginIdentityProvider loginIdentityProvider) { + IdentityProvider identityProvider) { this.jwtService = jwtService; - this.loginIdentityProvider = loginIdentityProvider; + this.identityProvider = identityProvider; } /** @@ -114,17 +113,15 @@ public Response getAccessStatus(@Context HttpServletRequest httpServletRequest) } /** - * Creates a token for accessing the REST API via username/password. + * Creates a token for accessing the REST API. * * @param httpServletRequest the servlet request - * @param username the username - * @param password the password * @return A JWT (string) */ @POST - @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Consumes(MediaType.WILDCARD) @Produces(MediaType.TEXT_PLAIN) - @Path("/token") + @Path("/token/login") @ApiOperation( value = "Creates a token for accessing the REST API via username/password", notes = "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " + @@ -136,9 +133,9 @@ public Response getAccessStatus(@Context HttpServletRequest httpServletRequest) @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), - @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support username/password login."), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with username/password."), @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) }) - public Response createAccessToken( + public Response createAccessTokenUsingFormLogin( @Context HttpServletRequest httpServletRequest, @FormParam("username") String username, @FormParam("password") String password) { @@ -148,56 +145,182 @@ public Response createAccessToken( throw new IllegalStateException("Access tokens are only issued over HTTPS"); } - // if not configuration for login, don't consider credentials - if (loginIdentityProvider == null) { + // if not configured with custom identity provider, or if provider doesn't support username/password authentication, don't consider credentials + if (identityProvider == null || !identityProvider.supports(UsernamePasswordAuthenticationRequest.class)) { throw new IllegalStateException("Username/Password login not supported by this NiFi"); } - final LoginAuthenticationToken loginAuthenticationToken; - // ensure we have login credentials if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { throw new IllegalArgumentException("The username and password must be specified"); } + final AuthenticationResponse authenticationResponse; + try { // attempt to authenticate - final AuthenticationResponse authenticationResponse = loginIdentityProvider.authenticate(new LoginCredentials(username, password)); - long expiration = validateTokenExpiration(authenticationResponse.getExpiration(), authenticationResponse.getIdentity()); + AuthenticationRequest authenticationRequest = new UsernamePasswordAuthenticationRequest(username, password); + authenticationResponse = identityProvider.authenticate(authenticationRequest); + } catch (final InvalidCredentialsException ice) { + throw new IllegalArgumentException("The supplied client credentials are not valid.", ice); + } catch (final IdentityAccessException iae) { + throw new AdministrationException(iae.getMessage(), iae); + } + + // generate JWT for response + final String token = jwtService.generateSignedToken(authenticationResponse); - // create the authentication token - loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getIdentity(), expiration, authenticationResponse.getIssuer()); - } catch (final InvalidLoginCredentialsException ilce) { - throw new IllegalArgumentException("The supplied username and password are not valid.", ilce); + // build the response + final URI uri = URI.create(generateResourceUri("access", "token")); + return generateCreatedResponse(uri, token).build(); + } + + /** + * Creates a token for accessing the REST API using a custom identity provider configured using NiFi Registry extensions. + * + * @param httpServletRequest the servlet request + * @return A JWT (string) + */ + @POST + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.TEXT_PLAIN) + @Path("/token/identity-provider") + @ApiOperation( + value = "Creates a token for accessing the REST API via a custom identity provider.", + notes = "The user credentials must be passed in a format understood by the custom identity provider, e.g., a third-party auth token in an HTTP header. " + + "The exact format of the user credentials expected by the custom identity provider can be discovered by 'GET /token/identity-provider/usage'. " + + "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " + + "the body, and the signature. The expiration of the token is a contained within the body. The token can be used in the Authorization header " + + "in the format 'Authorization: Bearer '.", + response = String.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with customized credentials."), + @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) }) + public Response createAccessTokenUsingIdentityProviderCredentials(@Context HttpServletRequest httpServletRequest) { + + // only support access tokens when communicating over HTTPS + if (!httpServletRequest.isSecure()) { + throw new IllegalStateException("Access tokens are only issued over HTTPS"); + } + + // if not configured with custom identity provider, don't consider credentials + if (identityProvider == null) { + throw new IllegalStateException("Custom login not supported by this NiFi Registry"); + } + + final AuthenticationResponse authenticationResponse; + + try { + // attempt to authenticate + AuthenticationRequest authenticationRequest = identityProvider.extractCredentials(httpServletRequest); + authenticationResponse = identityProvider.authenticate(authenticationRequest); + } catch (final InvalidCredentialsException ice) { + throw new IllegalArgumentException("The supplied client credentials are not valid.", ice); } catch (final IdentityAccessException iae) { throw new AdministrationException(iae.getMessage(), iae); } // generate JWT for response - final String token = jwtService.generateSignedToken(loginAuthenticationToken); + final String token = jwtService.generateSignedToken(authenticationResponse); // build the response final URI uri = URI.create(generateResourceUri("access", "token")); return generateCreatedResponse(uri, token).build(); } - private long validateTokenExpiration(long proposedTokenExpiration, String identity) { - final long maxExpiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS); - final long minExpiration = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES); - - if (proposedTokenExpiration > maxExpiration) { - logger.warn(String.format("Max token expiration exceeded. Setting expiration to %s from %s for %s", maxExpiration, - proposedTokenExpiration, identity)); - proposedTokenExpiration = maxExpiration; - } else if (proposedTokenExpiration < minExpiration) { - logger.warn(String.format("Min token expiration not met. Setting expiration to %s from %s for %s", minExpiration, - proposedTokenExpiration, identity)); - proposedTokenExpiration = minExpiration; + /** + * Creates a token for accessing the REST API using a custom identity provider configured using NiFi Registry extensions. + * + * @param httpServletRequest the servlet request + * @return A JWT (string) + */ + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.TEXT_PLAIN) + @Path("/token/identity-provider/usage") + @ApiOperation( + value = "Provides a description of how the currently configured identity provider expects credentials to be passed to POST /token/identity-provider", + response = String.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with customized credentials."), + @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) }) + public Response getIdentityProviderUsageInstructions(@Context HttpServletRequest httpServletRequest) { + + // if not configuration for login, don't consider credentials + if (identityProvider == null) { + throw new IllegalStateException("Custom login not supported by this NiFi Registry"); + } + + Class ipClazz = identityProvider.getClass(); + String identityProviderName = StringUtils.isNotEmpty(ipClazz.getSimpleName()) ? ipClazz.getSimpleName() : ipClazz.getName(); + + try { + String usageInstructions = "Usage Instructions for '" + identityProviderName + "': "; + usageInstructions += identityProvider.getUsageInstructions().getText(); + return generateOkResponse(usageInstructions).build(); + + } catch (Exception e) { + // If, for any reason, this identity provider does not support getUsageInstructions(), e.g., throws NotImplemented Exception. + return Response.status(Response.Status.NOT_IMPLEMENTED) + .entity("The currently configured identity provider, '" + identityProvider.getClass().getName() + "' does not provide usage instructions.") + .build(); } - return proposedTokenExpiration; } + /** + * Creates a token for accessing the REST API using a custom identity provider configured using NiFi Registry extensions. + * + * @param httpServletRequest the servlet request + * @return A JWT (string) + */ + @POST + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.TEXT_PLAIN) + @Path("/token/identity-provider/test") + @ApiOperation( + value = "Tests the format of the credentials against this identity provider without preforming authentication on the credentials to validate them.", + notes = "The user credentials should be passed in a format understood by the custom identity provider as defined by 'GET /token/identity-provider/usage'.", + response = String.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = "The format of the credentials were not recognized by the currently configured identity provider."), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with customized credentials."), + @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) }) + public Response testIdentityProviderRecognizesCredentialsFormat(@Context HttpServletRequest httpServletRequest) { + + // only support access tokens when communicating over HTTPS + if (!httpServletRequest.isSecure()) { + throw new IllegalStateException("Access tokens are only issued over HTTPS"); + } + + // if not configured with custom identity provider, don't consider credentials + if (identityProvider == null) { + throw new IllegalStateException("Custom login not supported by this NiFi Registry"); + } + final Class ipClazz = identityProvider.getClass(); + final String identityProviderName = StringUtils.isNotEmpty(ipClazz.getSimpleName()) ? ipClazz.getSimpleName() : ipClazz.getName(); + + // attempt to extract client credentials without authenticating them + AuthenticationRequest authenticationRequest = identityProvider.extractCredentials(httpServletRequest); + if (authenticationRequest != null) { + final String successMessage = identityProviderName + " recognized the format of the credentials in the HTTP request."; + return generateOkResponse(successMessage).build(); + } + + return Response.status(Response.Status.UNAUTHORIZED) + .entity("The format of the credentials were not recognized by the currently configured identity provider " + + "'" + identityProviderName + "'. See GET /token/identity-provider/usage for more information.") + .build(); + + } } diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/request/IntegerParameter.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/request/IntegerParameter.java deleted file mode 100644 index f5048b36c..000000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/request/IntegerParameter.java +++ /dev/null @@ -1,39 +0,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. - */ -package org.apache.nifi.registry.web.request; - -/** - * Class for parsing integer parameters and providing a user friendly error message. - */ -public class IntegerParameter { - - private static final String INVALID_INTEGER_MESSAGE = "Unable to parse '%s' as an integer value."; - - private Integer integerValue; - - public IntegerParameter(String rawIntegerValue) { - try { - integerValue = Integer.parseInt(rawIntegerValue); - } catch (NumberFormatException nfe) { - throw new IllegalArgumentException(String.format(INVALID_INTEGER_MESSAGE, rawIntegerValue)); - } - } - - public Integer getInteger() { - return integerValue; - } -} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/response/AuthenticationResponse.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/response/AuthenticationResponse.java deleted file mode 100644 index a0b87b5b8..000000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/response/AuthenticationResponse.java +++ /dev/null @@ -1,65 +0,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. - */ -package org.apache.nifi.registry.web.response; - -/** - * Authentication response for a user login attempt. - */ -public class AuthenticationResponse { - - private final String identity; - private final String username; - private final long expiration; - private final String issuer; - - /** - * Creates an authentication response. The username and how long the authentication is valid in milliseconds - * - * @param identity The user identity - * @param username The username - * @param expiration The expiration in milliseconds - * @param issuer The issuer of the token - */ - public AuthenticationResponse(final String identity, final String username, final long expiration, final String issuer) { - this.identity = identity; - this.username = username; - this.expiration = expiration; - this.issuer = issuer; - } - - public String getIdentity() { - return identity; - } - - public String getUsername() { - return username; - } - - public String getIssuer() { - return issuer; - } - - /** - * Returns the expiration of a given authentication in milliseconds. - * - * @return The expiration in milliseconds - */ - public long getExpiration() { - return expiration; - } - -} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java index ce81ee259..16133caac 100644 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java @@ -17,18 +17,17 @@ package org.apache.nifi.registry.web.security; import org.apache.nifi.registry.properties.NiFiRegistryProperties; -import org.apache.nifi.registry.web.security.authentication.NiFiAnonymousUserFilter; -import org.apache.nifi.registry.web.security.authentication.jwt.JwtAuthenticationFilter; -import org.apache.nifi.registry.web.security.authentication.jwt.JwtAuthenticationProvider; -import org.apache.nifi.registry.web.security.authentication.x509.X509AuthenticationFilter; -import org.apache.nifi.registry.web.security.authentication.x509.X509AuthenticationProvider; -import org.apache.nifi.registry.web.security.authentication.x509.X509CertificateExtractor; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.web.security.authentication.AnonymousIdentityFilter; +import org.apache.nifi.registry.web.security.authentication.IdentityAuthenticationProvider; +import org.apache.nifi.registry.web.security.authentication.IdentityFilter; +import org.apache.nifi.registry.web.security.authentication.jwt.JwtIdentityProvider; +import org.apache.nifi.registry.web.security.authentication.x509.X509IdentityAuthenticationProvider; +import org.apache.nifi.registry.web.security.authentication.x509.X509IdentityProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -37,10 +36,9 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; -import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; /** - * NiFi Web Api Spring security + * NiFi Registry Web Api Spring security */ @Configuration @EnableWebSecurity @@ -50,18 +48,17 @@ public class NiFiRegistrySecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private NiFiRegistryProperties properties; - @Autowired private X509CertificateExtractor certificateExtractor; - @Autowired private X509PrincipalExtractor principalExtractor; - @Autowired private X509AuthenticationProvider x509AuthenticationProvider; - private X509AuthenticationFilter x509AuthenticationFilter; + @Autowired private Authorizer authorizer; - @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider; - private JwtAuthenticationFilter jwtAuthenticationFilter; + private AnonymousIdentityFilter anonymousAuthenticationFilter = new AnonymousIdentityFilter(); -// @Autowired private OtpAuthenticationProvider otpAuthenticationProvider; -// private OtpAuthenticationFilter otpAuthenticationFilter; + @Autowired private X509IdentityProvider x509IdentityProvider; + private IdentityFilter x509AuthenticationFilter; + private IdentityAuthenticationProvider x509AuthenticationProvider; - private NiFiAnonymousUserFilter anonymousAuthenticationFilter; + @Autowired private JwtIdentityProvider jwtIdentityProvider; + private IdentityFilter jwtAuthenticationFilter; + private IdentityAuthenticationProvider jwtAuthenticationProvider; public NiFiRegistrySecurityConfig() { super(true); // disable defaults @@ -69,12 +66,8 @@ public NiFiRegistrySecurityConfig() { @Override public void configure(WebSecurity webSecurity) throws Exception { - // ignore the access endpoints for obtaining the access config, access token - // granting, and access status for a given user (note: we are not ignoring the - // the /access/download-token endpoints) - webSecurity - .ignoring() - .antMatchers( "/access/token"); + // allow any client to access the endpoint for logging in to generate an access token + webSecurity.ignoring().antMatchers( "/access/token/*"); } @Override @@ -91,68 +84,48 @@ protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(x509AuthenticationFilter(), AnonymousAuthenticationFilter.class); // jwt - http.addFilterBefore(jwtFilterBean(), AnonymousAuthenticationFilter.class); + http.addFilterBefore(jwtAuthenticationFilter(), AnonymousAuthenticationFilter.class); // otp - // http.addFilterBefore(otpFilterBean(), AnonymousAuthenticationFilter.class); + // todo, if needed one-time password auth filter goes here // anonymous - http.anonymous().authenticationFilter(anonymousFilter()); - } - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - // override xxxBean method so the authentication manager is available in app context (necessary for the method level security) - return super.authenticationManagerBean(); + http.anonymous().authenticationFilter(anonymousAuthenticationFilter); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth - .authenticationProvider(x509AuthenticationProvider) - .authenticationProvider(jwtAuthenticationProvider); -// .authenticationProvider(otpAuthenticationProvider); // TODO OTP support + .authenticationProvider(x509AuthenticationProvider()) + .authenticationProvider(jwtAuthenticationProvider()); } - @Bean - public JwtAuthenticationFilter jwtFilterBean() throws Exception { - if (jwtAuthenticationFilter == null) { - jwtAuthenticationFilter = new JwtAuthenticationFilter(); - jwtAuthenticationFilter.setProperties(properties); - jwtAuthenticationFilter.setAuthenticationManager(authenticationManager()); + private IdentityFilter x509AuthenticationFilter() throws Exception { + if (x509AuthenticationFilter == null) { + x509AuthenticationFilter = new IdentityFilter(x509IdentityProvider); } - return jwtAuthenticationFilter; + return x509AuthenticationFilter; } -// @Bean // TODO OtpAuthenticationFilter -// public OtpAuthenticationFilter otpFilterBean() throws Exception { -// if (otpAuthenticationFilter == null) { -// otpAuthenticationFilter = new OtpAuthenticationFilter(); -// otpAuthenticationFilter.setProperties(properties); -// otpAuthenticationFilter.setAuthenticationManager(authenticationManager()); -// } -// return otpAuthenticationFilter; -// } - - @Bean - public X509AuthenticationFilter x509AuthenticationFilter() throws Exception { - if (x509AuthenticationFilter == null) { - x509AuthenticationFilter = new X509AuthenticationFilter(); - x509AuthenticationFilter.setProperties(properties); - x509AuthenticationFilter.setCertificateExtractor(certificateExtractor); - x509AuthenticationFilter.setPrincipalExtractor(principalExtractor); - x509AuthenticationFilter.setAuthenticationManager(authenticationManager()); + private IdentityAuthenticationProvider x509AuthenticationProvider() { + if (x509AuthenticationProvider == null) { + x509AuthenticationProvider = new X509IdentityAuthenticationProvider(properties, authorizer, x509IdentityProvider); } - return x509AuthenticationFilter; + return x509AuthenticationProvider; + } + + private IdentityFilter jwtAuthenticationFilter() throws Exception { + if (jwtAuthenticationFilter == null) { + jwtAuthenticationFilter = new IdentityFilter(jwtIdentityProvider); + } + return jwtAuthenticationFilter; } - @Bean - public NiFiAnonymousUserFilter anonymousFilter() throws Exception { - if (anonymousAuthenticationFilter == null) { - anonymousAuthenticationFilter = new NiFiAnonymousUserFilter(); + private IdentityAuthenticationProvider jwtAuthenticationProvider() { + if (jwtAuthenticationProvider == null) { + jwtAuthenticationProvider = new X509IdentityAuthenticationProvider(properties, authorizer, jwtIdentityProvider); } - return anonymousAuthenticationFilter; + return jwtAuthenticationProvider; } } diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAnonymousUserFilter.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AnonymousIdentityFilter.java similarity index 83% rename from nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAnonymousUserFilter.java rename to nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AnonymousIdentityFilter.java index a27fd1836..f879f0dee 100644 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAnonymousUserFilter.java +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AnonymousIdentityFilter.java @@ -18,23 +18,22 @@ import org.apache.nifi.registry.security.authorization.user.NiFiUserDetails; import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser; -import org.apache.nifi.registry.web.security.authentication.token.NiFiAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; import javax.servlet.http.HttpServletRequest; -public class NiFiAnonymousUserFilter extends AnonymousAuthenticationFilter { +public class AnonymousIdentityFilter extends AnonymousAuthenticationFilter { private static final String ANONYMOUS_KEY = "anonymousNifiKey"; - public NiFiAnonymousUserFilter() { + public AnonymousIdentityFilter() { super(ANONYMOUS_KEY); } @Override protected Authentication createAuthentication(HttpServletRequest request) { - return new NiFiAuthenticationToken(new NiFiUserDetails(StandardNiFiUser.ANONYMOUS)); + return new AuthenticationSuccessToken(new NiFiUserDetails(StandardNiFiUser.ANONYMOUS)); } } diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationRequestToken.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationRequestToken.java new file mode 100644 index 000000000..a5a5ec304 --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationRequestToken.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.authentication; + +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +import java.security.Principal; +import java.util.Collection; + +/** + * Wraps an AuthenticationRequest in a Token that implements the Spring Security Authentication interface. + */ +public class AuthenticationRequestToken implements Authentication { + + private final AuthenticationRequest authenticationRequest; + private final Class authenticationRequestOrigin; + private final String clientAddress; + + public AuthenticationRequestToken(AuthenticationRequest authenticationRequest, Class authenticationRequestOrigin, String clientAddress) { + this.authenticationRequest = authenticationRequest; + this.authenticationRequestOrigin = authenticationRequestOrigin; + this.clientAddress = clientAddress; + } + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public Object getCredentials() { + return authenticationRequest.getCredentials(); + } + + @Override + public Object getDetails() { + return authenticationRequest.getDetails(); + } + + @Override + public Object getPrincipal() { + return new Principal() { + @Override + public String getName() { + return authenticationRequest.getUsername(); + } + }; + } + + @Override + public boolean isAuthenticated() { + return false; + } + + @Override + public void setAuthenticated(boolean b) throws IllegalArgumentException { + throw new IllegalArgumentException("AuthenticationRequestWrapper cannot be trusted. It is only to be used for storing an identity claim."); + } + + @Override + public String getName() { + return authenticationRequest.getUsername(); + } + + @Override + public int hashCode() { + return authenticationRequest.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return authenticationRequest.equals(obj); + } + + @Override + public String toString() { + return authenticationRequest.toString(); + } + + public AuthenticationRequest getAuthenticationRequest() { + return authenticationRequest; + } + + public Class getAuthenticationRequestOrigin() { + return authenticationRequestOrigin; + } + + public String getClientAddress() { + return clientAddress; + } +} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/token/NiFiAuthenticationToken.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationSuccessToken.java similarity index 87% rename from nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/token/NiFiAuthenticationToken.java rename to nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationSuccessToken.java index 19e56c58f..ea6f1e982 100644 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/token/NiFiAuthenticationToken.java +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationSuccessToken.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.nifi.registry.web.security.authentication.token; +package org.apache.nifi.registry.web.security.authentication; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.userdetails.UserDetails; @@ -22,11 +22,11 @@ /** * An authentication token that represents an Authenticated and Authorized user of the NiFi Apis. The authorities are based off the specified UserDetails. */ -public class NiFiAuthenticationToken extends AbstractAuthenticationToken { +public class AuthenticationSuccessToken extends AbstractAuthenticationToken { - final UserDetails nifiUserDetails; + private final UserDetails nifiUserDetails; - public NiFiAuthenticationToken(final UserDetails nifiUserDetails) { + public AuthenticationSuccessToken(final UserDetails nifiUserDetails) { super(nifiUserDetails.getAuthorities()); super.setAuthenticated(true); setDetails(nifiUserDetails); diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationFilter.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationFilter.java new file mode 100644 index 000000000..7c2b27f1c --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationFilter.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.authentication; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils; +import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils; +import org.apache.nifi.registry.web.security.authentication.exception.InvalidAuthenticationException; +import org.apache.nifi.registry.web.security.authentication.exception.UntrustedProxyException; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collection; + +/** + * Note: This class is deprecated and is being considered for complete removal in favor of using {@link IdentityFilter}. + * It is remaining in place for the time being until the pattern of authentication implemented by {@link IdentityFilter} + * has been more thoroughly vetted in real use. + */ +@Deprecated +public class IdentityAuthenticationFilter extends AbstractAuthenticationProcessingFilter { + + private static final RequestMatcher requiresAuthenticationRequestMatcher = new RequestMatcher() { + @Override + public boolean matches(HttpServletRequest httpServletRequest) { + return NiFiUserUtils.getNiFiUser() == null; + } + }; + + private final IdentityProvider identityProvider; + + public IdentityAuthenticationFilter(IdentityProvider identityProvider, AuthenticationManager authenticationManager, String defaultFilterProcessesUrl) { + super(defaultFilterProcessesUrl); + super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(defaultFilterProcessesUrl)); // Authentication will only be initiated for the request url matching this pattern + setAuthenticationManager(authenticationManager); + this.identityProvider = identityProvider; + } + + public IdentityAuthenticationFilter(IdentityProvider identityProvider, AuthenticationManager authenticationManager) { + super(requiresAuthenticationRequestMatcher); + setAuthenticationManager(authenticationManager); + this.identityProvider = identityProvider; + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException { + + // Only require authentication from an identity provider if the NiFi registry is running securely. + if (!httpServletRequest.isSecure()) { + // Otherwise, requests will be "authenticated" by the AnonymousIdentityFilter + //return null; + return new ContinueFilterChainAuthentication(); // see successfulAuthentication for why we do this + } + + AuthenticationRequest authenticationRequest = identityProvider.extractCredentials(httpServletRequest); + if (authenticationRequest == null) { + //return null; + return new ContinueFilterChainAuthentication(); // see successfulAuthentication for why we do this + } + Authentication authentication = new AuthenticationRequestToken(authenticationRequest, identityProvider.getClass(), httpServletRequest.getRemoteAddr()); + Authentication authenticationResult = getAuthenticationManager().authenticate(authentication); // See IdentityProviderAuthenticationProvider for authentication impl. + if (authenticationResult == null) { + return new ContinueFilterChainAuthentication(); // see successfulAuthentication for why we do this + } else { + return authenticationResult; + } + // Super class will invoke successfulAuthentication() or unsuccessfulAuthentication() depending on the outcome of the authentication attempt + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { + + if (authResult.getClass().equals(ContinueFilterChainAuthentication.class)) { + logger.info("Authentication unknown, continue chain"); + // Because this NiFi Registry might be configured with multiple AbstractAuthenticationProcessingFilter's, + // the request should continue through the filter chain. If none of the IdentityProviderAuthenticationFilters + // can authenticate the request and register a user identity, then the AnonymousIdentityFilter will assign the + // Anonymous identity which will not be authorized for access. + // A refinement of this would be to extend something other than AbstractAuthenticationProcessingFilter, such as + // GenericFilterBean, or to register different filter chains based on context, such as only include + // AbstractAuthenticationProcessingFilter(s) when running securely, otherwise don't register any and only register + // the AnonymousIdentityFilter. + chain.doFilter(request, response); + } + + logger.info("Authentication success for " + authResult); + + SecurityContextHolder.getContext().setAuthentication(authResult); + if (StringUtils.isNotBlank(request.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN))) { + response.setHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_ACCEPTED, Boolean.TRUE.toString()); + } + + // continue the filter chain, which now holds a NiFiUser in the SecurityContext's authentication + chain.doFilter(request, response); + } + + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { + this.logger.debug("Authentication request failed: " + failed.toString(), failed); + + SecurityContextHolder.clearContext(); + this.logger.debug("Updated SecurityContextHolder to contain null Authentication"); + + // populate the response + if (StringUtils.isNotBlank(request.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN))) { + response.setHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_DETAILS, failed.getMessage()); + } + + // 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 (failed instanceof InvalidAuthenticationException) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + out.println(failed.getMessage()); + } else if (failed instanceof UntrustedProxyException) { // thrown in X509IdentityProviderAuthenticationProvider + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + out.println(failed.getMessage()); + } else if (failed instanceof AuthenticationServiceException) { + logger.error(String.format("Unable to authorize: %s", failed.getMessage()), failed); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + out.println(String.format("Unable to authorize: %s", failed.getMessage())); + } else { + logger.error(String.format("Unable to authorize: %s", failed.getMessage()), failed); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + out.println("Access is denied."); + } + + // log the failure + logger.warn(String.format("Rejecting access to web api: %s", failed.getMessage())); + logger.debug(StringUtils.EMPTY, failed); + } + + protected class ContinueFilterChainAuthentication implements Authentication { + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getDetails() { + return null; + } + + @Override + public Object getPrincipal() { + return null; + } + + @Override + public boolean isAuthenticated() { + return false; + } + + @Override + public void setAuthenticated(boolean b) throws IllegalArgumentException { + throw new IllegalArgumentException("Cannot set authenticated on ContinueFilterChainAuthentication"); + } + + @Override + public String getName() { + return null; + } + } + +} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationProvider.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationProvider.java new file mode 100644 index 000000000..67c45d884 --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationProvider.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.authentication; + +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.properties.util.IdentityMapping; +import org.apache.nifi.registry.properties.util.IdentityMappingUtil; +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.security.authentication.AuthenticationResponse; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.ManagedAuthorizer; +import org.apache.nifi.registry.security.authorization.UserAndGroups; +import org.apache.nifi.registry.security.authorization.UserGroupProvider; +import org.apache.nifi.registry.security.authorization.user.NiFiUserDetails; +import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class IdentityAuthenticationProvider implements AuthenticationProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(IdentityAuthenticationProvider.class); + + protected NiFiRegistryProperties properties; + protected Authorizer authorizer; + protected final IdentityProvider identityProvider; + private List mappings; + + public IdentityAuthenticationProvider( + NiFiRegistryProperties properties, + Authorizer authorizer, + IdentityProvider identityProvider) { + this.properties = properties; + this.authorizer = authorizer; + this.identityProvider = identityProvider; + this.mappings = Collections.unmodifiableList(IdentityMappingUtil.getIdentityMappings(properties)); + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + + // Determine if this AuthenticationProvider's identityProvider should be able to support this AuthenticationRequest + boolean tokenOriginatedFromThisIdentityProvider = checkTokenOriginatedFromThisIdentityProvider(authentication); + + if (!tokenOriginatedFromThisIdentityProvider) { + // Returning null indicates to The Spring Security AuthenticationManager that this AuthenticationProvider + // cannot authenticate this token and another provider should be tried. + return null; + } + + AuthenticationRequestToken authenticationRequestToken = ((AuthenticationRequestToken)authentication); + AuthenticationRequest authenticationRequest = authenticationRequestToken.getAuthenticationRequest(); + + try { + AuthenticationResponse authenticationResponse = identityProvider.authenticate(authenticationRequest); + if (authenticationResponse == null) { + return null; + } + return buildAuthenticatedToken(authenticationRequestToken, authenticationResponse); + } catch (InvalidCredentialsException e) { + throw new BadCredentialsException("Identity Provider authentication failed.", e); + } + + } + + @Override + public boolean supports(Class authenticationClazz) { + // is authenticationClazz a subclass of AuthenticationRequestWrapper? + return AuthenticationRequestToken.class.isAssignableFrom(authenticationClazz); + } + + protected AuthenticationSuccessToken buildAuthenticatedToken( + AuthenticationRequestToken requestToken, + AuthenticationResponse response) { + + final String mappedIdentity = mapIdentity(response.getIdentity()); + + return new AuthenticationSuccessToken(new NiFiUserDetails( + new StandardNiFiUser.Builder() + .identity(mappedIdentity) + .groups(getUserGroups(mappedIdentity)) + .clientAddress(requestToken.getClientAddress()) + .build())); + } + + protected boolean checkTokenOriginatedFromThisIdentityProvider(Authentication authentication) { + return (authentication instanceof AuthenticationRequestToken + && identityProvider.getClass().equals(((AuthenticationRequestToken) authentication).getAuthenticationRequestOrigin())); + } + + protected String mapIdentity(final String identity) { + return IdentityMappingUtil.mapIdentity(identity, mappings); + } + + protected Set getUserGroups(final String identity) { + return getUserGroups(authorizer, identity); + } + + private static Set getUserGroups(final Authorizer authorizer, final String userIdentity) { + if (authorizer instanceof ManagedAuthorizer) { + final ManagedAuthorizer managedAuthorizer = (ManagedAuthorizer) authorizer; + final UserGroupProvider userGroupProvider = managedAuthorizer.getAccessPolicyProvider().getUserGroupProvider(); + final UserAndGroups userAndGroups = userGroupProvider.getUserAndGroups(userIdentity); + final Set userGroups = userAndGroups.getGroups(); + + if (userGroups == null || userGroups.isEmpty()) { + return Collections.EMPTY_SET; + } else { + return userAndGroups.getGroups().stream().map(Group::getName).collect(Collectors.toSet()); + } + } else { + return null; + } + } +} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityFilter.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityFilter.java new file mode 100644 index 000000000..40c266249 --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityFilter.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.authentication; + +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * A class that will extract an identity / credentials claim from an HttpServlet Request using an injected IdentityProvider. + * + * This class is designed to be used in collaboration with an {@link IdentityAuthenticationProvider}. The identity/credentials will be + * extracted by this filter and later validated by the {@link IdentityAuthenticationProvider} in the default SecurityInterceptorFilter. + */ +public class IdentityFilter extends GenericFilterBean { + + private static final Logger logger = LoggerFactory.getLogger(IdentityFilter.class); + + private final IdentityProvider identityProvider; + + public IdentityFilter(IdentityProvider identityProvider) { + this.identityProvider = identityProvider; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + + // Only require authentication from an identity provider if the NiFi registry is running securely. + if (!servletRequest.isSecure()) { + // Otherwise, requests will be "authenticated" by the AnonymousIdentityFilter + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + if (identityProvider == null) { + logger.warn("Identity Filter configured with NULL identity provider. Credentials will not be extracted."); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + if (credentialsAlreadyPresent()) { + logger.debug("Credentials already extracted for {}, skipping credentials extraction filter for {}", + SecurityContextHolder.getContext().getAuthentication().getPrincipal(), + identityProvider.getClass().getSimpleName()); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + logger.debug("Attempting to extract user credentials using {}", identityProvider.getClass().getSimpleName()); + + try { + AuthenticationRequest authenticationRequest = identityProvider.extractCredentials((HttpServletRequest)servletRequest); + if (authenticationRequest != null) { + Authentication authentication = new AuthenticationRequestToken(authenticationRequest, identityProvider.getClass(), servletRequest.getRemoteAddr()); + logger.debug("Adding credentials claim to SecurityContext to be authenticated. Credentials extracted by {}: {}", + identityProvider.getClass().getSimpleName(), + authenticationRequest); + SecurityContextHolder.getContext().setAuthentication(authentication); + // This filter's job, which is merely to search for and extract an identity claim, is done. + // The actual authentication of the identity claim will be handled by a corresponding IdentityAuthenticationProvider + } + } catch (Exception e) { + logger.debug("Exception occurred while extracting credentials:", e); + } + + filterChain.doFilter(servletRequest, servletResponse); + } + + private boolean credentialsAlreadyPresent() { + return SecurityContextHolder.getContext().getAuthentication() != null; + } +} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationFilter.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationFilter.java deleted file mode 100644 index 24af504c2..000000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationFilter.java +++ /dev/null @@ -1,156 +0,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. - */ -package org.apache.nifi.registry.web.security.authentication; - -import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils; -import org.apache.nifi.registry.properties.NiFiRegistryProperties; -import org.apache.nifi.registry.web.security.authentication.exception.InvalidAuthenticationException; -import org.apache.nifi.registry.web.security.authentication.exception.UntrustedProxyException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.AuthenticationServiceException; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.filter.GenericFilterBean; - -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 java.io.IOException; -import java.io.PrintWriter; - -/** - * - */ -public abstract class NiFiAuthenticationFilter extends GenericFilterBean { - - private static final Logger log = LoggerFactory.getLogger(NiFiAuthenticationFilter.class); - - private AuthenticationManager authenticationManager; - private NiFiRegistryProperties properties; - - @Override - public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { - final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (log.isDebugEnabled()) { - log.debug("Checking secure context token: " + authentication); - } - - if (requiresAuthentication((HttpServletRequest) request)) { - authenticate((HttpServletRequest) request, (HttpServletResponse) response, chain); - } else { - chain.doFilter(request, response); - } - - } - - private boolean requiresAuthentication(final HttpServletRequest request) { - return NiFiUserUtils.getNiFiUser() == null; - } - - private void authenticate(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException { - String dnChain = null; - try { - final Authentication authenticationRequest = attemptAuthentication(request); - if (authenticationRequest != null) { - // log the request attempt - response details will be logged later - log.info(String.format("Attempting request for (%s) %s %s (source ip: %s)", authenticationRequest.toString(), request.getMethod(), - request.getRequestURL().toString(), request.getRemoteAddr())); - - // attempt to authorize the user - final Authentication authenticated = authenticationManager.authenticate(authenticationRequest); - successfulAuthorization(request, response, authenticated); - } - - // continue - chain.doFilter(request, response); - } catch (final AuthenticationException ae) { - // invalid authentication - always error out - unsuccessfulAuthorization(request, response, ae); - } - } - - /** - * Attempt to extract an authentication attempt from the specified request. - * - * @param request The request - * @return The authentication attempt or null if none is found int he request - */ - public abstract Authentication attemptAuthentication(HttpServletRequest request); - - protected void successfulAuthorization(HttpServletRequest request, HttpServletResponse response, Authentication authResult) { - log.info("Authentication success for " + authResult); - - SecurityContextHolder.getContext().setAuthentication(authResult); - ProxiedEntitiesUtils.successfulAuthorization(request, response, authResult); - } - - protected void unsuccessfulAuthorization(HttpServletRequest request, HttpServletResponse response, AuthenticationException ae) throws IOException { - // populate the response - ProxiedEntitiesUtils.unsuccessfulAuthorization(request, response, ae); - - // 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 InvalidAuthenticationException) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - out.println(ae.getMessage()); - } else if (ae instanceof UntrustedProxyException) { - response.setStatus(HttpServletResponse.SC_FORBIDDEN); - out.println(ae.getMessage()); - } else if (ae instanceof AuthenticationServiceException) { - log.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 { - log.error(String.format("Unable to authorize: %s", ae.getMessage()), ae); - response.setStatus(HttpServletResponse.SC_FORBIDDEN); - out.println("Access is denied."); - } - - // log the failure - log.warn(String.format("Rejecting access to web api: %s", ae.getMessage())); - - // optionally log the stack trace - if (log.isDebugEnabled()) { - log.debug(StringUtils.EMPTY, ae); - } - } - - @Override - public void destroy() { - } - - public void setAuthenticationManager(AuthenticationManager authenticationManager) { - this.authenticationManager = authenticationManager; - } - - public void setProperties(NiFiRegistryProperties properties) { - this.properties = properties; - } - -} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationProvider.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationProvider.java deleted file mode 100644 index 5617e0ef9..000000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationProvider.java +++ /dev/null @@ -1,84 +0,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. - */ -package org.apache.nifi.registry.web.security.authentication; - -import org.apache.nifi.registry.security.authorization.Authorizer; -import org.apache.nifi.registry.security.authorization.Group; -import org.apache.nifi.registry.security.authorization.ManagedAuthorizer; -import org.apache.nifi.registry.security.authorization.UserAndGroups; -import org.apache.nifi.registry.security.authorization.UserGroupProvider; -import org.apache.nifi.registry.properties.NiFiRegistryProperties; -import org.apache.nifi.registry.properties.util.IdentityMapping; -import org.apache.nifi.registry.properties.util.IdentityMappingUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.authentication.AuthenticationProvider; - -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Base AuthenticationProvider that provides common functionality to mapping identities. - */ -public abstract class NiFiAuthenticationProvider implements AuthenticationProvider { - - private static final Logger LOGGER = LoggerFactory.getLogger(NiFiAuthenticationProvider.class); - - private NiFiRegistryProperties properties; - private Authorizer authorizer; - private List mappings; - - /** - * @param properties the NiFiProperties instance - */ - public NiFiAuthenticationProvider(final NiFiRegistryProperties properties, final Authorizer authorizer) { - this.properties = properties; - this.mappings = Collections.unmodifiableList(IdentityMappingUtil.getIdentityMappings(properties)); - this.authorizer = authorizer; - } - - public List getMappings() { - return mappings; - } - - protected String mapIdentity(final String identity) { - return IdentityMappingUtil.mapIdentity(identity, mappings); - } - - protected Set getUserGroups(final String identity) { - return getUserGroups(authorizer, identity); - } - - protected static Set getUserGroups(final Authorizer authorizer, final String userIdentity) { - if (authorizer instanceof ManagedAuthorizer) { - final ManagedAuthorizer managedAuthorizer = (ManagedAuthorizer) authorizer; - final UserGroupProvider userGroupProvider = managedAuthorizer.getAccessPolicyProvider().getUserGroupProvider(); - final UserAndGroups userAndGroups = userGroupProvider.getUserAndGroups(userIdentity); - final Set userGroups = userAndGroups.getGroups(); - - if (userGroups == null || userGroups.isEmpty()) { - return Collections.EMPTY_SET; - } else { - return userAndGroups.getGroups().stream().map(group -> group.getName()).collect(Collectors.toSet()); - } - } else { - return null; - } - } -} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationRequestToken.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationRequestToken.java deleted file mode 100644 index 3da95c5ff..000000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/NiFiAuthenticationRequestToken.java +++ /dev/null @@ -1,41 +0,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. - */ -package org.apache.nifi.registry.web.security.authentication; - -import org.springframework.security.authentication.AbstractAuthenticationToken; - -/** - * Base class for authentication request tokens in NiFI. - */ -public abstract class NiFiAuthenticationRequestToken extends AbstractAuthenticationToken { - - private final String clientAddress; - - /** - * @param clientAddress The address of the client making the request - */ - public NiFiAuthenticationRequestToken(final String clientAddress) { - super(null); - setAuthenticated(false); - this.clientAddress = clientAddress; - } - - public String getClientAddress() { - return clientAddress; - } - -} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/StandardLoginIdentityProviderInitializationContext.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/StandardLoginIdentityProviderInitializationContext.java deleted file mode 100644 index e8fba2e19..000000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/StandardLoginIdentityProviderInitializationContext.java +++ /dev/null @@ -1,45 +0,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. - */ -package org.apache.nifi.registry.web.security.authentication; - -import org.apache.nifi.registry.security.authentication.LoginIdentityProviderInitializationContext; -import org.apache.nifi.registry.security.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; - } - -} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/UntrustedProxyException.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/UntrustedProxyException.java index 6245ac2ea..82570a3da 100644 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/UntrustedProxyException.java +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/UntrustedProxyException.java @@ -18,9 +18,6 @@ import org.springframework.security.core.AuthenticationException; -/** - * - */ public class UntrustedProxyException extends AuthenticationException { public UntrustedProxyException(String msg) { diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationFilter.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationFilter.java deleted file mode 100644 index 1e5c194f7..000000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationFilter.java +++ /dev/null @@ -1,58 +0,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. - */ -package org.apache.nifi.registry.web.security.authentication.jwt; - -import org.apache.commons.lang3.StringUtils; - -import org.apache.nifi.registry.web.security.authentication.NiFiAuthenticationFilter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.core.Authentication; - -import javax.servlet.http.HttpServletRequest; - -/** - */ -public class JwtAuthenticationFilter extends NiFiAuthenticationFilter { - - private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class); - - public static final String AUTHORIZATION = "Authorization"; - public static final String BEARER = "Bearer "; - - @Override - public Authentication attemptAuthentication(final HttpServletRequest request) { - // only support jwt login when running securely - if (!request.isSecure()) { - return null; - } - - // TODO: Refactor request header extraction logic to shared utility as it is duplicated in AccessResource - - // get the principal out of the user token - final String authorization = request.getHeader(AUTHORIZATION); - - // if there is no authorization header, we don't know the user - if (authorization == null || !StringUtils.startsWith(authorization, BEARER)) { - return null; - } else { - // Extract the Base64 encoded token from the Authorization header - final String token = StringUtils.substringAfterLast(authorization, " "); - return new JwtAuthenticationRequestToken(token, request.getRemoteAddr()); - } - } -} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationProvider.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationProvider.java deleted file mode 100644 index 224792359..000000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationProvider.java +++ /dev/null @@ -1,69 +0,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. - */ -package org.apache.nifi.registry.web.security.authentication.jwt; - -import io.jsonwebtoken.JwtException; -import org.apache.nifi.registry.security.authorization.Authorizer; -import org.apache.nifi.registry.security.authorization.user.NiFiUser; -import org.apache.nifi.registry.security.authorization.user.NiFiUserDetails; -import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser; -import org.apache.nifi.registry.properties.NiFiRegistryProperties; -import org.apache.nifi.registry.web.security.authentication.exception.InvalidAuthenticationException; -import org.apache.nifi.registry.web.security.authentication.NiFiAuthenticationProvider; -import org.apache.nifi.registry.web.security.authentication.token.NiFiAuthenticationToken; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.stereotype.Component; - -/** - * - */ -@Component -public class JwtAuthenticationProvider extends NiFiAuthenticationProvider { - - private final JwtService jwtService; - - @Autowired - public JwtAuthenticationProvider(JwtService jwtService, NiFiRegistryProperties nifiProperties, Authorizer authorizer) { - super(nifiProperties, authorizer); - this.jwtService = jwtService; - } - - @Override - public Authentication authenticate(Authentication authentication) throws AuthenticationException { - final JwtAuthenticationRequestToken request = (JwtAuthenticationRequestToken) authentication; - - try { - final String jwtPrincipal = jwtService.getAuthenticationFromToken(request.getToken()); - final String mappedIdentity = mapIdentity(jwtPrincipal); - final NiFiUser user = new StandardNiFiUser.Builder() - .identity(mappedIdentity) - .groups(getUserGroups(mappedIdentity)) - .clientAddress(request.getClientAddress()) - .build(); - return new NiFiAuthenticationToken(new NiFiUserDetails(user)); - } catch (JwtException e) { - throw new InvalidAuthenticationException(e.getMessage(), e); - } - } - - @Override - public boolean supports(Class authentication) { - return JwtAuthenticationRequestToken.class.isAssignableFrom(authentication); - } -} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationRequestToken.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationRequestToken.java deleted file mode 100644 index e8af9ff4c..000000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtAuthenticationRequestToken.java +++ /dev/null @@ -1,60 +0,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. - */ -package org.apache.nifi.registry.web.security.authentication.jwt; - - -import org.apache.nifi.registry.web.security.authentication.NiFiAuthenticationRequestToken; - -/** - * This is an authentication request with a given JWT token. - */ -public class JwtAuthenticationRequestToken extends NiFiAuthenticationRequestToken { - - private final String token; - - /** - * Creates a representation of the jwt authentication request for a user. - * - * @param token The unique token for this user - * @param clientAddress the address of the client making the request - */ - public JwtAuthenticationRequestToken(final String token, final String clientAddress) { - super(clientAddress); - setAuthenticated(false); - this.token = token; - } - - @Override - public Object getCredentials() { - return null; - } - - @Override - public Object getPrincipal() { - return token; - } - - public String getToken() { - return token; - } - - @Override - public String toString() { - return ""; - } - -} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtIdentityProvider.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtIdentityProvider.java new file mode 100644 index 000000000..9592c2b56 --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtIdentityProvider.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.authentication.jwt; + +import io.jsonwebtoken.JwtException; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.security.authentication.AuthenticationResponse; +import org.apache.nifi.registry.security.authentication.BearerAuthIdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProviderConfigurationContext; +import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException; +import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.web.security.authentication.exception.InvalidAuthenticationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class JwtIdentityProvider extends BearerAuthIdentityProvider implements IdentityProvider { + + private static final Logger logger = LoggerFactory.getLogger(JwtIdentityProvider.class); + + private static final String issuer = JwtIdentityProvider.class.getSimpleName(); + + private static final long expiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS); + + private final JwtService jwtService; + + @Autowired + public JwtIdentityProvider(JwtService jwtService, NiFiRegistryProperties nifiProperties, Authorizer authorizer) { + this.jwtService = jwtService; + } + + @Override + public AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) throws InvalidCredentialsException, IdentityAccessException { + try { + String jwtAuthToken = (String) authenticationRequest.getCredentials(); + final String jwtPrincipal = jwtService.getAuthenticationFromToken(jwtAuthToken); + + return new AuthenticationResponse(jwtPrincipal, jwtPrincipal, expiration, issuer); + } catch (ClassCastException e) { + // token String in credentials Object + return null; + } catch (JwtException e) { + throw new InvalidAuthenticationException(e.getMessage(), e); + } + } + + @Override + public void onConfigured(IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException {} + + @Override + public void preDestruction() throws SecurityProviderDestructionException {} + +} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java index 49c17ea35..4401a15fd 100644 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java @@ -28,17 +28,20 @@ import io.jsonwebtoken.SigningKeyResolverAdapter; import io.jsonwebtoken.UnsupportedJwtException; import org.apache.commons.lang3.StringUtils; - import org.apache.nifi.registry.exception.AdministrationException; +import org.apache.nifi.registry.security.authentication.AuthenticationResponse; import org.apache.nifi.registry.security.key.Key; import org.apache.nifi.registry.security.key.KeyService; -import org.apache.nifi.registry.web.security.authentication.token.LoginAuthenticationToken; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; import java.util.Calendar; +import java.util.concurrent.TimeUnit; +// TODO, look into replacing this JwtService service with Apache Licensed JJWT library @Service public class JwtService { @@ -50,6 +53,7 @@ public class JwtService { private final KeyService keyService; + @Autowired public JwtService(final KeyService keyService) { this.keyService = keyService; } @@ -68,7 +72,7 @@ public String getAuthenticationFromToken(final String base64EncodedToken) throws throw new JwtException("No subject available in token"); } - // TODO: Validate issuer against active registry? + // TODO: Validate issuer against active IdentityProvider? if (StringUtils.isEmpty(jws.getBody().getIssuer())) { throw new JwtException("No issuer available in token"); } @@ -110,46 +114,48 @@ public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { /** * 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 + * @param authenticationResponse 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) { + public String generateSignedToken(final AuthenticationResponse authenticationResponse) throws JwtException { + if (authenticationResponse == 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 Calendar now = Calendar.getInstance(); + long expirationMillisRelativeToNow = validateTokenExpiration(authenticationResponse.getExpiration(), authenticationResponse.getIdentity()); + long expirationMillis = now.getTimeInMillis() + expirationMillisRelativeToNow; + final Calendar expiration = new Calendar.Builder().setInstant(expirationMillis).build(); - final Object principal = authenticationToken.getPrincipal(); + final Object principal = authenticationResponse.getIdentity(); 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(); + final String errorMessage = "Cannot generate a JWT for a token with an empty identity issued by " + authenticationResponse.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(); + final String username = authenticationResponse.getUsername(); 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); + logger.trace("Generating JWT for " + describe(authenticationResponse)); // 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()) + .setIssuer(authenticationResponse.getIssuer()) + .setAudience(authenticationResponse.getIssuer()) .claim(USERNAME_CLAIM, username) .claim(KEY_ID_CLAIM, key.getId()) + .setIssuedAt(now.getTime()) .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; @@ -157,4 +163,44 @@ public String generateSignedToken(final LoginAuthenticationToken authenticationT throw new JwtException(errorMessage, e); } } + + private long validateTokenExpiration(long proposedTokenExpiration, String identity) { + final long maxExpiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS); + final long minExpiration = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES); + + if (proposedTokenExpiration > maxExpiration) { + logger.warn(String.format("Max token expiration exceeded. Setting expiration to %s from %s for %s", maxExpiration, + proposedTokenExpiration, identity)); + proposedTokenExpiration = maxExpiration; + } else if (proposedTokenExpiration < minExpiration) { + logger.warn(String.format("Min token expiration not met. Setting expiration to %s from %s for %s", minExpiration, + proposedTokenExpiration, identity)); + proposedTokenExpiration = minExpiration; + } + + return proposedTokenExpiration; + } + + private static String describe(AuthenticationResponse authenticationResponse) { + Calendar expirationTime = Calendar.getInstance(); + expirationTime.setTimeInMillis(authenticationResponse.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(authenticationResponse.getUsername()) + .append(" issued by ") + .append(authenticationResponse.getIssuer()) + .append(" expiring at ") + .append(expirationTimeString) + .append(" [") + .append(authenticationResponse.getExpiration()) + .append(" ms, ") + .append(remainingTime) + .append(" ms remaining]") + .toString(); + } } diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/token/LoginAuthenticationToken.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/token/LoginAuthenticationToken.java deleted file mode 100644 index 08f063764..000000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/token/LoginAuthenticationToken.java +++ /dev/null @@ -1,123 +0,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. - */ -package org.apache.nifi.registry.web.security.authentication.token; - -import org.apache.nifi.registry.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(); - } - -} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationFilter.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationFilter.java deleted file mode 100644 index fa0fce255..000000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationFilter.java +++ /dev/null @@ -1,64 +0,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. - */ -package org.apache.nifi.registry.web.security.authentication.x509; - -import org.apache.nifi.registry.web.security.authentication.NiFiAuthenticationFilter; -import org.apache.nifi.registry.web.security.authentication.ProxiedEntitiesUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; - -import javax.servlet.http.HttpServletRequest; -import java.security.cert.X509Certificate; - -/** - * 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 NiFiAuthenticationFilter { - - private static final Logger logger = LoggerFactory.getLogger(X509AuthenticationFilter.class); - - private X509CertificateExtractor certificateExtractor; - private X509PrincipalExtractor principalExtractor; - - @Override - public Authentication attemptAuthentication(final HttpServletRequest request) { - // only suppport x509 login when running securely - if (!request.isSecure()) { - return null; - } - - // look for a client certificate - final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(request); - if (certificates == null) { - return null; - } - - return new X509AuthenticationRequestToken(request.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN), principalExtractor, certificates, request.getRemoteAddr()); - } - - /* setters */ - public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) { - this.certificateExtractor = certificateExtractor; - } - - public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) { - this.principalExtractor = principalExtractor; - } - -} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationProvider.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationProvider.java deleted file mode 100644 index 3e935a235..000000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationProvider.java +++ /dev/null @@ -1,166 +0,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. - */ -package org.apache.nifi.registry.web.security.authentication.x509; - -import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; -import org.apache.nifi.registry.security.authorization.Authorizer; -import org.apache.nifi.registry.security.authorization.RequestAction; -import org.apache.nifi.registry.security.authorization.Resource; -import org.apache.nifi.registry.security.authorization.UserContextKeys; -import org.apache.nifi.registry.security.authorization.resource.Authorizable; -import org.apache.nifi.registry.security.authorization.resource.ResourceFactory; -import org.apache.nifi.registry.security.authorization.user.NiFiUser; -import org.apache.nifi.registry.security.authorization.user.NiFiUserDetails; -import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser; -import org.apache.nifi.registry.properties.NiFiRegistryProperties; -import org.apache.nifi.registry.web.response.AuthenticationResponse; -import org.apache.nifi.registry.web.security.authentication.exception.InvalidAuthenticationException; -import org.apache.nifi.registry.web.security.authentication.NiFiAuthenticationProvider; -import org.apache.nifi.registry.web.security.authentication.ProxiedEntitiesUtils; -import org.apache.nifi.registry.web.security.authentication.exception.UntrustedProxyException; -import org.apache.nifi.registry.web.security.authentication.token.NiFiAuthenticationToken; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.Set; - -@Component -public class X509AuthenticationProvider extends NiFiAuthenticationProvider { - - private static final Authorizable PROXY_AUTHORIZABLE = new Authorizable() { - @Override - public Authorizable getParentAuthorizable() { - return null; - } - - @Override - public Resource getResource() { - return ResourceFactory.getProxyResource(); - } - }; - - private X509IdentityProvider certificateIdentityProvider; - private Authorizer authorizer; - - @Autowired - public X509AuthenticationProvider( - final X509IdentityProvider certificateIdentityProvider, - final Authorizer authorizer, - final NiFiRegistryProperties properties) { - super(properties, authorizer); - this.certificateIdentityProvider = certificateIdentityProvider; - this.authorizer = authorizer; - } - - @Override - public Authentication authenticate(Authentication authentication) throws AuthenticationException { - final X509AuthenticationRequestToken request = (X509AuthenticationRequestToken) authentication; - - // attempt to authenticate if certificates were found - final AuthenticationResponse authenticationResponse; - try { - authenticationResponse = certificateIdentityProvider.authenticate(request.getCertificates()); - } catch (final IllegalArgumentException iae) { - throw new InvalidAuthenticationException(iae.getMessage(), iae); - } - - if (StringUtils.isBlank(request.getProxiedEntitiesChain())) { - final String mappedIdentity = mapIdentity(authenticationResponse.getIdentity()); - return new NiFiAuthenticationToken(new NiFiUserDetails( - new StandardNiFiUser.Builder() - .identity(mappedIdentity) - .groups(getUserGroups(mappedIdentity)) - .clientAddress(request.getClientAddress()) - .build())); - } else { - // build the entire proxy chain if applicable - - final List proxyChain = new ArrayList<>(ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(request.getProxiedEntitiesChain())); - proxyChain.add(authenticationResponse.getIdentity()); - - // add the chain as appropriate to each proxy - NiFiUser proxy = null; - for (final ListIterator chainIter = proxyChain.listIterator(proxyChain.size()); chainIter.hasPrevious(); ) { - String identity = chainIter.previous(); - - // determine if the user is anonymous - final boolean isAnonymous = StringUtils.isBlank(identity); - if (isAnonymous) { - identity = StandardNiFiUser.ANONYMOUS_IDENTITY; - } else { - identity = mapIdentity(identity); - } - - final Set groups = getUserGroups(identity); - - // Only set the client address for client making the request because we don't know the clientAddress of the proxied entities - String clientAddress = (proxy == null) ? request.getClientAddress() : null; - proxy = createUser(identity, groups, proxy, clientAddress, isAnonymous); - - if (chainIter.hasPrevious()) { - try { - PROXY_AUTHORIZABLE.authorize(authorizer, RequestAction.WRITE, proxy); - } catch (final AccessDeniedException e) { - throw new UntrustedProxyException(String.format("Untrusted proxy %s", identity)); - } - } - } - - return new NiFiAuthenticationToken(new NiFiUserDetails(proxy)); - } - } - - /** - * Returns a regular user populated with the provided values, or if the user should be anonymous, a well-formed instance of the anonymous user with the provided values. - * - * @param identity the user's identity - * @param chain the proxied entities - * @param clientAddress the requesting IP address - * @param isAnonymous if true, an anonymous user will be returned (identity will be ignored) - * @return the populated user - */ - protected static NiFiUser createUser(String identity, Set groups, NiFiUser chain, String clientAddress, boolean isAnonymous) { - if (isAnonymous) { - return StandardNiFiUser.populateAnonymousUser(chain, clientAddress); - } else { - return new StandardNiFiUser.Builder().identity(identity).groups(groups).chain(chain).clientAddress(clientAddress).build(); - } - } - - private Map getUserContext(final X509AuthenticationRequestToken request) { - final Map userContext; - if (!StringUtils.isBlank(request.getClientAddress())) { - userContext = new HashMap<>(); - userContext.put(UserContextKeys.CLIENT_ADDRESS.name(), request.getClientAddress()); - } else { - userContext = null; - } - return userContext; - } - - @Override - public boolean supports(Class authentication) { - return X509AuthenticationRequestToken.class.isAssignableFrom(authentication); - } -} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestToken.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestToken.java deleted file mode 100644 index d5aca23a2..000000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestToken.java +++ /dev/null @@ -1,75 +0,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. - */ -package org.apache.nifi.registry.web.security.authentication.x509; - -import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.registry.web.security.authentication.NiFiAuthenticationRequestToken; -import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; - -import java.security.cert.X509Certificate; - -/** - * This is an authentication request with a given JWT token. - */ -public class X509AuthenticationRequestToken extends NiFiAuthenticationRequestToken { - - private final String proxiedEntitiesChain; - private final X509PrincipalExtractor principalExtractor; - private final X509Certificate[] certificates; - - /** - * Creates a representation of the jwt authentication request for a user. - * - * @param proxiedEntitiesChain The http servlet request - * @param certificates The certificate chain - */ - public X509AuthenticationRequestToken(final String proxiedEntitiesChain, final X509PrincipalExtractor principalExtractor, final X509Certificate[] certificates, final String clientAddress) { - super(clientAddress); - setAuthenticated(false); - this.proxiedEntitiesChain = proxiedEntitiesChain; - this.principalExtractor = principalExtractor; - this.certificates = certificates; - } - - @Override - public Object getCredentials() { - return null; - } - - @Override - public Object getPrincipal() { - if (StringUtils.isBlank(proxiedEntitiesChain)) { - return principalExtractor.extractPrincipal(certificates[0]); - } else { - return String.format("%s<%s>", proxiedEntitiesChain, principalExtractor.extractPrincipal(certificates[0])); - } - } - - public String getProxiedEntitiesChain() { - return proxiedEntitiesChain; - } - - public X509Certificate[] getCertificates() { - return certificates; - } - - @Override - public String toString() { - return getName(); - } - -} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509CertificateValidator.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509CertificateValidator.java deleted file mode 100644 index d748b9390..000000000 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509CertificateValidator.java +++ /dev/null @@ -1,49 +0,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. - */ -package org.apache.nifi.registry.web.security.authentication.x509; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; -import java.security.cert.X509Certificate; - -/** - * Extracts client certificates from Http requests. - */ -@Component -public class X509CertificateValidator { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - /** - * Extract the client certificate from the specified HttpServletRequest or null if none is specified. - * - * @param certificates the client certificates - * @throws CertificateExpiredException cert is expired - * @throws CertificateNotYetValidException cert is not yet valid - */ - public void validateClientCertificate(final X509Certificate[] certificates) - throws CertificateExpiredException, CertificateNotYetValidException { - - // ensure the cert is valid - certificates[0].checkValidity(); - } - -} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java new file mode 100644 index 000000000..d4be5e9a1 --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.authentication.x509; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.web.security.authentication.AuthenticationRequestToken; +import org.apache.nifi.registry.security.authentication.AuthenticationResponse; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.web.security.authentication.IdentityAuthenticationProvider; +import org.apache.nifi.registry.web.security.authentication.AuthenticationSuccessToken; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.Resource; +import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; +import org.apache.nifi.registry.security.authorization.resource.Authorizable; +import org.apache.nifi.registry.security.authorization.resource.ResourceFactory; +import org.apache.nifi.registry.security.authorization.user.NiFiUser; +import org.apache.nifi.registry.security.authorization.user.NiFiUserDetails; +import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser; +import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils; +import org.apache.nifi.registry.web.security.authentication.exception.UntrustedProxyException; + +import java.util.List; +import java.util.ListIterator; +import java.util.Set; + +public class X509IdentityAuthenticationProvider extends IdentityAuthenticationProvider { + + private static final Authorizable PROXY_AUTHORIZABLE = new Authorizable() { + @Override + public Authorizable getParentAuthorizable() { + return null; + } + + @Override + public Resource getResource() { + return ResourceFactory.getProxyResource(); + } + }; + + public X509IdentityAuthenticationProvider(NiFiRegistryProperties properties, Authorizer authorizer, IdentityProvider identityProvider) { + super(properties, authorizer, identityProvider); + } + + @Override + protected AuthenticationSuccessToken buildAuthenticatedToken( + AuthenticationRequestToken requestToken, + AuthenticationResponse response) { + + AuthenticationRequest authenticationRequest = requestToken.getAuthenticationRequest(); + + String proxiedEntitiesChain = authenticationRequest.getDetails() != null + ? (String)authenticationRequest.getDetails() + : null; + + if (StringUtils.isBlank(proxiedEntitiesChain)) { + return super.buildAuthenticatedToken(requestToken, response); + } + + // build the entire proxy chain if applicable - + final List proxyChain = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(proxiedEntitiesChain); + proxyChain.add(response.getIdentity()); + + // add the chain as appropriate to each proxy + NiFiUser proxy = null; + for (final ListIterator chainIter = proxyChain.listIterator(proxyChain.size()); chainIter.hasPrevious(); ) { + String identity = chainIter.previous(); + + // determine if the user is anonymous + final boolean isAnonymous = StringUtils.isBlank(identity); + if (isAnonymous) { + identity = StandardNiFiUser.ANONYMOUS_IDENTITY; + } else { + identity = mapIdentity(identity); + } + + final Set groups = getUserGroups(identity); + + // Only set the client address for client making the request because we don't know the clientAddress of the proxied entities + String clientAddress = (proxy == null) ? requestToken.getClientAddress() : null; + proxy = createUser(identity, groups, proxy, clientAddress, isAnonymous); + + if (chainIter.hasPrevious()) { + try { + PROXY_AUTHORIZABLE.authorize(authorizer, RequestAction.WRITE, proxy); + } catch (final AccessDeniedException e) { + throw new UntrustedProxyException(String.format("Untrusted proxy %s", identity)); + } + } + } + + return new AuthenticationSuccessToken(new NiFiUserDetails(proxy)); + + } + + /** + * Returns a regular user populated with the provided values, or if the user should be anonymous, a well-formed instance of the anonymous user with the provided values. + * + * @param identity the user's identity + * @param chain the proxied entities + * @param clientAddress the requesting IP address + * @param isAnonymous if true, an anonymous user will be returned (identity will be ignored) + * @return the populated user + */ + private static NiFiUser createUser(String identity, Set groups, NiFiUser chain, String clientAddress, boolean isAnonymous) { + if (isAnonymous) { + return StandardNiFiUser.populateAnonymousUser(chain, clientAddress); + } else { + return new StandardNiFiUser.Builder().identity(identity).groups(groups).chain(chain).clientAddress(clientAddress).build(); + } + } + + + +} diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java index 692b31871..9631efc60 100644 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java @@ -16,13 +16,22 @@ */ package org.apache.nifi.registry.web.security.authentication.x509; -import org.apache.nifi.registry.web.response.AuthenticationResponse; +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.security.authentication.AuthenticationResponse; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProviderConfigurationContext; +import org.apache.nifi.registry.security.authentication.IdentityProviderUsage; +import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; import org.springframework.stereotype.Component; +import javax.servlet.http.HttpServletRequest; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; @@ -32,73 +41,126 @@ * Identity provider for extract the authenticating a ServletRequest with a X509Certificate. */ @Component -public class X509IdentityProvider { +public class X509IdentityProvider implements IdentityProvider { private static final Logger logger = LoggerFactory.getLogger(X509IdentityProvider.class); - private final String issuer = getClass().getSimpleName(); + private static final String issuer = X509IdentityProvider.class.getSimpleName(); + + private static final long expiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS); + + private static final IdentityProviderUsage usage = new IdentityProviderUsage() { + @Override + public String getText() { + return "The client must connect over HTTPS and must provide a client certificate during the TLS handshake. " + + "Additionally, the client may declare itself a proxy for another user identity by populating the " + + ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN + " HTTP header field with a value of the format " + + "'...'" + + "for all identities in the chain prior to this client. If the " + ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN + + " header is present in the request, this client's identity will be extracted from the client certificate " + + "used for TLS and added to the end of the chain, and then the entire chain will be authorized. Each proxy " + + "will be authorized to have 'write' access to '/proxy', and the originating user identity will be " + + "authorized for access to the resource being accessed in the request."; + } + }; - private X509CertificateValidator certificateValidator; private X509PrincipalExtractor principalExtractor; + private X509CertificateExtractor certificateExtractor; @Autowired - public X509IdentityProvider(X509CertificateValidator certificateValidator, X509PrincipalExtractor principalExtractor) { - this.certificateValidator = certificateValidator; + public X509IdentityProvider(X509PrincipalExtractor principalExtractor, X509CertificateExtractor certificateExtractor) { this.principalExtractor = principalExtractor; + this.certificateExtractor = certificateExtractor; + } + + @Override + public IdentityProviderUsage getUsageInstructions() { + return usage; } /** - * Authenticates the specified request by checking certificate validity. + * Extracts certificate-based credentials from an {@link HttpServletRequest}. + * + * The resulting {@link AuthenticationRequest} will be populated as: + * - username: principal DN from first client cert + * - credentials: first client certificate (X509Certificate) + * - details: proxied-entities chain (String) * - * @param certificates the client certificates - * @return an authentication response - * @throws IllegalArgumentException the request did not contain a valid certificate (or no certificate) + * @param servletRequest the {@link HttpServletRequest} request that may contain credentials understood by this IdentityProvider + * @return a populated AuthenticationRequest or null if the credentials could not be found. */ - public AuthenticationResponse authenticate(final X509Certificate[] certificates) throws IllegalArgumentException { - // ensure the cert was found + @Override + public AuthenticationRequest extractCredentials(HttpServletRequest servletRequest) { + + // only support x509 login when running securely + if (!servletRequest.isSecure()) { + return null; + } + + // look for a client certificate + final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(servletRequest); if (certificates == null || certificates.length == 0) { - throw new IllegalArgumentException("The specified request does not contain a client certificate."); + return null; } // extract the principal final Object certificatePrincipal = principalExtractor.extractPrincipal(certificates[0]); final String principal = certificatePrincipal.toString(); + // extract the proxiedEntitiesChain header value from the servletRequest + String proxiedEntitiesChainHeader = servletRequest.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN); + + return new AuthenticationRequest(principal, certificates[0], proxiedEntitiesChainHeader); + + } + + /** + * For a given {@link AuthenticationRequest}, this validates the client certificate and creates a populated {@link AuthenticationResponse}. + * + * The {@link AuthenticationRequest} authenticationRequest paramenter is expected to be populated as: + * - username: principal DN from first client cert + * - credentials: first client certificate (X509Certificate) + * - details: proxied-entities chain (String) + * + * @param authenticationRequest the request, containing identity claim credentials for the IdentityProvider to authenticate and determine an identity + */ + @Override + public AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) throws InvalidCredentialsException { + + if (authenticationRequest == null || authenticationRequest.getUsername() == null) { + return null; + } + + String principal = authenticationRequest.getUsername(); + try { - certificateValidator.validateClientCertificate(certificates); + X509Certificate clientCertificate = (X509Certificate)authenticationRequest.getCredentials(); + validateClientCertificate(clientCertificate); } 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); + logger.warn(message, cee); + throw new InvalidCredentialsException(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); + logger.warn(message, cnyve); + throw new InvalidCredentialsException(message, cnyve); } catch (final Exception e) { - logger.info(e.getMessage()); - if (logger.isDebugEnabled()) { - logger.debug("", e); - } - throw new IllegalArgumentException(e.getMessage(), e); + logger.warn(e.getMessage(), e); } // build the authentication response - return new AuthenticationResponse(principal, principal, TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS), issuer); + return new AuthenticationResponse(principal, principal, expiration, issuer); } - /* setters */ - public void setCertificateValidator(X509CertificateValidator certificateValidator) { - this.certificateValidator = certificateValidator; - } + @Override + public void onConfigured(IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException {} - public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) { - this.principalExtractor = principalExtractor; + @Override + public void preDestruction() throws SecurityProviderDestructionException {} + + + private void validateClientCertificate(X509Certificate certificate) throws CertificateExpiredException, CertificateNotYetValidException { + certificate.checkValidity(); } } diff --git a/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java b/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java index d6b94c299..bdd8e11b0 100644 --- a/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java +++ b/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java @@ -45,6 +45,7 @@ import javax.ws.rs.core.Form; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.nio.charset.Charset; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -71,20 +72,23 @@ @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/clearDB.sql") public class SecureLdapIT extends IntegrationTestBase { + private static final String tokenLoginPath = "access/token/login"; + private static final String tokenIdentityProviderPath = "access/token/identity-provider"; + @TestConfiguration @Profile("ITSecureLdap") public static class LdapTestConfiguration { - static AuthorizerFactory af; + static AuthorizerFactory authorizerFactory; @Primary @Bean @DependsOn({"directoryServer"}) // Can't load LdapUserGroupProvider until the embedded LDAP server, which creates the "directoryServer" bean, is running public static Authorizer getAuthorizer(@Autowired NiFiRegistryProperties properties, ExtensionManager extensionManager) { - if (af == null) { - af = new AuthorizerFactory(properties, extensionManager); + if (authorizerFactory == null) { + authorizerFactory = new AuthorizerFactory(properties, extensionManager); } - return af.getAuthorizer(); + return authorizerFactory.getAuthorizer(); } } @@ -93,11 +97,9 @@ public static Authorizer getAuthorizer(@Autowired NiFiRegistryProperties propert @Before public void generateAuthToken() { - final Form form = new Form() - .param("username", "nifiadmin") - .param("password", "password"); + final Form form = encodeCredentialsForURLFormParams("nifiadmin", "password"); final String token = client - .target(createURL("access/token")) + .target(createURL(tokenLoginPath)) .request() .post(Entity.form(form), String.class); adminAuthToken = token; @@ -121,12 +123,10 @@ public void testTokenGenerationAndAccessStatus() throws Exception { "\"status\":\"ACTIVE\"" + "}"; - // When: the /access/token endpoint is queried - final Form form = new Form() - .param("username", "nobel") - .param("password", "password"); + // When: the /access/token/login endpoint is queried + final Form form = encodeCredentialsForURLFormParams("nobel", "password"); final Response tokenResponse = client - .target(createURL("access/token")) + .target(createURL(tokenLoginPath)) .request() .post(Entity.form(form), Response.class); @@ -153,6 +153,52 @@ public void testTokenGenerationAndAccessStatus() throws Exception { } + @Test + public void testTokenGenerationWithIdentityProvider() throws Exception { + + // Given: the client and server have been configured correctly for LDAP authentication + String expectedJwtPayloadJson = "{" + + "\"sub\":\"nobel\"," + + "\"preferred_username\":\"nobel\"," + + "\"iss\":\"LdapIdentityProvider\"," + + "\"aud\":\"LdapIdentityProvider\"" + + "}"; + String expectedAccessStatusJson = "{" + + "\"identity\":\"nobel\"," + + "\"status\":\"ACTIVE\"" + + "}"; + + // When: the /access/token/identity-provider endpoint is queried + final String basicAuthCredentials = encodeCredentialsForBasicAuth("nobel", "password"); + final Response tokenResponse = client + .target(createURL(tokenIdentityProviderPath)) + .request() + .header("Authorization", "Basic " + basicAuthCredentials) + .post(null, Response.class); + + // Then: the server returns 200 OK with an access token + assertEquals(201, tokenResponse.getStatus()); + String token = tokenResponse.readEntity(String.class); + assertTrue(StringUtils.isNotEmpty(token)); + String[] jwtParts = token.split("\\."); + assertEquals(3, jwtParts.length); + String jwtPayload = new String(Base64.decodeBase64(jwtParts[1]), "UTF-8"); + JSONAssert.assertEquals(expectedJwtPayloadJson, jwtPayload, false); + + // When: the token is returned in the Authorization header + final Response accessResponse = client + .target(createURL("access")) + .request() + .header("Authorization", "Bearer " + token) + .get(Response.class); + + // Then: the server acknowledges the client has access + assertEquals(200, accessResponse.getStatus()); + String accessStatus = accessResponse.readEntity(String.class); + JSONAssert.assertEquals(expectedAccessStatusJson, accessStatus, false); + + } + @Test public void testUsers() throws Exception { @@ -240,15 +286,13 @@ public void testAccessPolicyCreation() throws Exception { // Given: the server has been configured with an initial admin "nifiadmin" and a user with no accessPolicies "nobel" String nobelId = getTenantIdentifierByIdentity("nobel"); String chemistsId = getTenantIdentifierByIdentity("chemists"); // a group containing user "nobel" - final Form form = new Form() - .param("username", "nobel") - .param("password", "password"); + + final Form form = encodeCredentialsForURLFormParams("nobel", "password"); final String nobelAuthToken = client - .target(createURL("access/token")) + .target(createURL(tokenLoginPath)) .request() .post(Entity.form(form), String.class); - // When: nifiadmin creates a bucket final Bucket bucket = new Bucket(); bucket.setName("Integration Test Bucket"); @@ -382,4 +426,15 @@ private String getTenantIdentifierByIdentity(String tenantIdentity) { return matchedTenant != null ? matchedTenant.getIdentifier() : null; } + private static Form encodeCredentialsForURLFormParams(String username, String password) { + return new Form() + .param("username", username) + .param("password", password); + } + + private static String encodeCredentialsForBasicAuth(String username, String password) { + final String credentials = username + ":" + password; + final String base64credentials = new String(java.util.Base64.getEncoder().encode(credentials.getBytes(Charset.forName("UTF-8")))); + return base64credentials; + } } From 3c0c820f944546eef4d47957efda75b1a00292a5 Mon Sep 17 00:00:00 2001 From: Kevin Doran Date: Mon, 20 Nov 2017 19:16:15 -0500 Subject: [PATCH 2/2] NIFIREG-45 Changes based on peer review feedback --- .../nifi/registry/security/ldap/LdapIdentityProvider.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapIdentityProvider.java b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapIdentityProvider.java index 5d1643ae9..bebe2d58e 100644 --- a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapIdentityProvider.java +++ b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapIdentityProvider.java @@ -230,6 +230,12 @@ public final void onConfigured(final IdentityProviderConfigurationContext config @Override public AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) throws InvalidCredentialsException, IdentityAccessException { + + if (authenticationRequest == null || StringUtils.isEmpty(authenticationRequest.getUsername())) { + logger.debug("Call to authenticate method with null or empty authenticationRequest, returning null without attempting to authenticate"); + return null; + } + if (ldapAuthenticationProvider == null) { throw new IdentityAccessException("The LDAP authentication provider is not initialized."); }