From 76f2d5702fe3465471c78a1d23d557f33d0b7664 Mon Sep 17 00:00:00 2001 From: Andy LoPresto Date: Fri, 11 Mar 2016 15:11:19 -0800 Subject: [PATCH] NIFI-1274 Added Kerberos authentication mechanism. NIFI-1274 Cleaned up TODO statements. (+3 squashed commits) Squashed commits: [fd101cd] Removed logic to check for presence of services to determine if token support is enabled when username/password authentication is enabled (Kerberos also requires tokens). [c2ce29f] Reverted import changes to RulesResource.java. [c269d72] Added Kerberos authentication mechanism. Moved Kerberos service wiring from XML to Java to handle scenario where admin has not configured Kerberos (previously threw NullPointerException in FileSystemResource constructor). (+15 squashed commits) Squashed commits: [09fc694] Added Kerberos documentation to Admin Guide. [ecfb864] Cleaned up unused logic. [157efb3] Added logic to determine if client certificates are required for REST API (login, anonymous, and Kerberos service authentication all disabled). Cleaned up KerberosService by moving logic to NiFiProperties. [5438619] Added documentation for Kerberos login-identity-providers.xml. [3332d9f] Added NiFi properties for Kerberos SSO. [b14a557] Fixed canvas call to only attempt Kerberos login if JWT not present in local storage. Added logic to handle ticket validation failure in AccessResource. Changed wiring of Kerberos service beans to XML in nifi-web-security-context.xml for consistency. [c31ae3d] Kerberos SPNEGO works without additional filter (new entry endpoint accepts Kerberos ticket in Authorization header and returns JWT so the rest of the application functions the same as LDAP). [98460e7] Added check to only instantiate beans when Kerberos enabled to allow access control integration tests to pass. [6ed0724] Renamed Kerberos discovery method to be explicit about service vs. credential login. [ed67d2e] Removed temporary solution for Rules Resource access via Kerberos ticket. [c8b2b01] Added temporary solution for Rules Resource access via Kerberos ticket. [81ca80f] NIFI-1274 Added KerberosAuthenticationFilter to conduct SPNEGO authentication with local (client) Kerberos ticket. Added properties and accessors for service principal and keytab location for NiFi app server. Added KAF to NiFiWebApiSecurityConfiguration. Added AlternateKerberosUserDetailsService to provide user lookup without dependency on extension bundle (nifi-kerberos-iaa-provider). Added dependencies on spring-security-kerberos-core and -web modules to pom.xml. [0605ba8] Added working configuration files to test/resources in kerberos module to document necessary config. This version requires the user to enter their Kerberos username (without realm) and password into the NiFi login screen and will authenticate them against the running KDC. Also includes a sample keystore and root CA public key for configuring a secure instance. [49236c8] Added kerberos module dependencies to nifi/pom.xml and nifi-assembly/pom.xml. Added default properties to login-identity-providers.xml. [928c52b] Added nifi-kerberos-iaa-providers-bundle module to nifi/pom.xml. Added skeleton of Kerberos authenticator using Spring Security Kerberos plugin. This closes #284 Signed-off-by: Matt Gilman --- nifi-assembly/pom.xml | 8 + .../org/apache/nifi/util/NiFiProperties.java | 61 +- .../main/asciidoc/administration-guide.adoc | 108 ++- .../conf/login-identity-providers.xml | 17 + .../src/main/resources/conf/nifi.properties | 3 + .../apache/nifi/web/server/JettyServer.java | 67 +- .../nifi-web/nifi-web-api/pom.xml | 12 + .../web/NiFiWebApiSecurityConfiguration.java | 32 +- .../apache/nifi/web/api/AccessResource.java | 257 ++++--- .../nifi/web/api/ApplicationResource.java | 68 +- .../main/resources/nifi-web-api-context.xml | 1 + .../nifi-web/nifi-web-security/pom.xml | 5 + .../security/jwt/JwtAuthenticationFilter.java | 8 +- .../nifi/web/security/jwt/JwtService.java | 6 +- .../AlternateKerberosUserDetailsService.java | 32 + .../security/kerberos/KerberosService.java | 75 ++ .../kerberos/KerberosServiceFactoryBean.java | 74 ++ .../security/otp/OtpAuthenticationFilter.java | 4 - .../resources/nifi-web-security-context.xml | 19 +- .../otp/OtpAuthenticationFilterTest.java | 12 - .../src/main/webapp/js/nf/canvas/nf-canvas.js | 707 +++++++++--------- .../nifi-kerberos-iaa-providers-nar/pom.xml | 36 + .../src/main/resources/META-INF/NOTICE | 37 + .../nifi-kerberos-iaa-providers/pom.xml | 60 ++ .../nifi/kerberos/KerberosProvider.java | 101 +++ .../kerberos/KerberosUserDetailsService.java | 31 + ....nifi.authentication.LoginIdentityProvider | 15 + .../pom.xml | 38 + nifi-nar-bundles/pom.xml | 1 + pom.xml | 11 + 30 files changed, 1350 insertions(+), 556 deletions(-) create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/kerberos/AlternateKerberosUserDetailsService.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/kerberos/KerberosService.java create mode 100644 nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/kerberos/KerberosServiceFactoryBean.java create mode 100644 nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers-nar/pom.xml create mode 100644 nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers-nar/src/main/resources/META-INF/NOTICE create mode 100644 nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers/pom.xml create mode 100644 nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers/src/main/java/org/apache/nifi/kerberos/KerberosProvider.java create mode 100644 nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers/src/main/java/org/apache/nifi/kerberos/KerberosUserDetailsService.java create mode 100644 nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authentication.LoginIdentityProvider create mode 100644 nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/pom.xml diff --git a/nifi-assembly/pom.xml b/nifi-assembly/pom.xml index 3ad558b893..fff3b22cab 100644 --- a/nifi-assembly/pom.xml +++ b/nifi-assembly/pom.xml @@ -182,6 +182,11 @@ language governing permissions and limitations under the License. --> nifi-ldap-iaa-providers-nar nar + + org.apache.nifi + nifi-kerberos-iaa-providers-nar + nar + org.apache.nifi nifi-dbcp-service-nar @@ -448,6 +453,9 @@ language governing permissions and limitations under the License. --> + + + 12 hours diff --git a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java index 3d05a4775b..8c98c0bc7b 100644 --- a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java +++ b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java @@ -16,6 +16,9 @@ */ package org.apache.nifi.util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; @@ -34,9 +37,6 @@ import java.util.Map; import java.util.Properties; import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - public class NiFiProperties extends Properties { private static final long serialVersionUID = 2119177359005492702L; @@ -189,6 +189,9 @@ public class NiFiProperties extends Properties { // kerberos properties public static final String KERBEROS_KRB5_FILE = "nifi.kerberos.krb5.file"; + public static final String KERBEROS_SERVICE_PRINCIPAL = "nifi.kerberos.service.principal"; + public static final String KERBEROS_KEYTAB_LOCATION = "nifi.kerberos.keytab.location"; + public static final String KERBEROS_AUTHENTICATION_EXPIRATION = "nifi.kerberos.authentication.expiration"; // state management public static final String STATE_MANAGEMENT_CONFIG_FILE = "nifi.state.management.configuration.file"; @@ -247,6 +250,9 @@ public class NiFiProperties extends Properties { // state management defaults public static final String DEFAULT_STATE_MANAGEMENT_CONFIG_FILE = "conf/state-management.xml"; + // Kerberos defaults + public static final String DEFAULT_KERBEROS_AUTHENTICATION_EXPIRATION = "12 hours"; + private NiFiProperties() { super(); } @@ -861,6 +867,55 @@ public class NiFiProperties extends Properties { } } + public String getKerberosServicePrincipal() { + final String servicePrincipal = getProperty(KERBEROS_SERVICE_PRINCIPAL); + if (!StringUtils.isBlank(servicePrincipal)) { + return servicePrincipal.trim(); + } else { + return null; + } + } + + public String getKerberosKeytabLocation() { + final String keytabLocation = getProperty(KERBEROS_KEYTAB_LOCATION); + if (!StringUtils.isBlank(keytabLocation)) { + return keytabLocation.trim(); + } else { + return null; + } + } + + public String getKerberosAuthenticationExpiration() { + final String authenticationExpirationString = getProperty(KERBEROS_AUTHENTICATION_EXPIRATION, DEFAULT_KERBEROS_AUTHENTICATION_EXPIRATION); + if (!StringUtils.isBlank(authenticationExpirationString)) { + return authenticationExpirationString.trim(); + } else { + return null; + } + } + + /** + * Returns true if the Kerberos service principal and keytab location properties are populated. + * + * @return true if Kerberos service support is enabled + */ + public boolean isKerberosServiceSupportEnabled() { + return !StringUtils.isBlank(getKerberosServicePrincipal()) && !StringUtils.isBlank(getKerberosKeytabLocation()); + } + + /** + * Returns true if client certificates are required for REST API. Determined if the following conditions are all true: + * + * - login identity provider is not populated + * - anonymous authorities is empty + * - Kerberos service support is not enabled + * + * @return true if client certificates are required for access to the REST API + */ + public boolean isClientAuthRequiredForRestApi() { + return StringUtils.isBlank(getProperty(NiFiProperties.SECURITY_USER_LOGIN_IDENTITY_PROVIDER)) && getAnonymousAuthorities().isEmpty() && !isKerberosServiceSupportEnabled(); + } + public InetSocketAddress getNodeApiAddress() { final String rawScheme = getClusterProtocolManagerToNodeApiScheme(); diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc index 74e2e8abde..6b1f682b1e 100644 --- a/nifi-docs/src/main/asciidoc/administration-guide.adoc +++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc @@ -191,6 +191,9 @@ explicity enabled. NiFi does not perform user authentication over HTTP. Using HTTP all users will be granted all roles. +Lightweight Directory Access Protocol (LDAP) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Below is an example and description of configuring a Login Identity Provider that integrates with a Directory Server to authenticate users. ---- @@ -254,6 +257,37 @@ nifi.security.user.login.identity.provider=ldap-provider |`Authentication Expiration` | The duration of how long the user authentication is valid for. If the user never logs out, they will be required to log back in following this duration. |================================================================================================================================================== +[[kerberos_login_identity_provider]] +Kerberos +~~~~~~~~ + +Below is an example and description of configuring a Login Identity Provider that integrates with a Kerberos Key Distribution Center (KDC) to authenticate users. + +---- + + kerberos-provider + org.apache.nifi.kerberos.KerberosProvider + NIFI.APACHE.ORG + /etc/krb5.conf + 12 hours + +---- + +With this configuration, username/password authentication can be enabled by referencing this provider in _nifi.properties_. + +---- +nifi.security.user.login.identity.provider=kerberos-provider +---- + +[options="header,footer"] +|================================================================================================================================================== +| Property Name | Description +|`Default Realm` | Default realm to provide when user enters incomplete user principal (i.e. NIFI.APACHE.ORG). +|`Kerberos Config File` | Absolute path to Kerberos client configuration file. +|`Authentication Expiration`| The duration of how long the user authentication is valid for. If the user never logs out, they will be required to log back in following this duration. +|================================================================================================================================================== + +See also <> to allow single sign-on access via client Kerberos tickets. Controlling Levels of Access ---------------------------- @@ -298,7 +332,7 @@ in place of "user dn - read only and admin": ---- -Here is an example entry using the name John Smith: +Here is an LDAP example entry using the name John Smith: ---- @@ -308,6 +342,16 @@ Here is an example entry using the name John Smith: ---- +Here is a Kerberos example entry using the name John Smith and realm `NIFI.APACHE.ORG`: + +---- + + + + + +---- + After the _authorized-users.xml_ file has been edited and saved, restart NiFi. Once the application starts, the ADMIN user is able to access the UI at the HTTPS URL that is configured in the _nifi.properties_ file. @@ -1108,6 +1152,58 @@ A complete example of configuring the Email service would look like the followin .... +[[kerberos_service]] +Kerberos Service +---------------- +NiFi can be configured to use Kerberos SPNEGO (or "Kerberos Service") for authentication. In this scenario, users will hit the REST endpoint `/access/kerberos` and the server will respond with a `401` status code and the challenge response header `WWW-Authenticate: Negotiate`. This communicates to the browser to use the GSS-API and load the user's Kerberos ticket and provide it as a Base64-encoded header value in the subsequent request. It will be of the form `Authorization: Negotiate YII...`. NiFi will attempt to validate this ticket with the KDC. If it is successful, the user's _principal_ will be returned as the identity, and the flow will follow login/credential authentication, in that a JWT will be issued in the response to prevent the unnecessary overhead of Kerberos authentication on every subsequent request. If the ticket cannot be validated, it will return with the appropriate error response code. The user will then be able to provide their Kerberos credentials to the login form if the `KerberosLoginIdentityProvider` has been configured. See <> login identity provider for more details. + +NiFi will only respond to Kerberos SPNEGO negotiation over an HTTPS connection, as unsecured requests are never authenticated. + +The following properties must be set in _nifi.properties_ to enable Kerberos service authentication. + +|==== +|*Property*|*Required*|*Description* +|Service Principal|true|The service principal used by NiFi to communicate with the KDC +|Keytab Location|true|The file path to the keytab containing the service principal +|==== + +See <> for complete documentation. + +[[kerberos_service_notes]] +Notes +~~~~~ + +* Kerberos is case-sensitive in many places and the error messages (or lack thereof) may not be sufficiently explanatory. Check the case sensitivity of the service principal in your configuration files. Convention is `HTTP/fully.qualified.domain@REALM`. +* Browsers have varying levels of restriction when dealing with SPNEGO negotiations. Some will provide the local Kerberos ticket to any domain that requests it, while others whitelist the trusted domains. See link:http://docs.spring.io/autorepo/docs/spring-security-kerberos/1.0.2.BUILD-SNAPSHOT/reference/htmlsingle/#browserspnegoconfig[Spring Security Kerberos - Reference Documentation: Appendix E. Configure browsers for SPNEGO Negotiation] for common browsers. +* Some browsers (legacy IE) do not support recent encryption algorithms such as AES, and are restricted to legacy algorithms (DES). This should be noted when generating keytabs. +* The KDC must be configured and a service principal defined for NiFi and a keytab exported. Comprehensive instructions for Kerberos server configuration and administration are beyond the scope of this document (see link:http://web.mit.edu/kerberos/krb5-current/doc/admin/index.html[MIT Kerberos Admin Guide]), but an example is below: + + +Adding a service principal for a server at `nifi.nifi.apache.org` and exporting the keytab from the KDC: + +.... +root@kdc:/etc/krb5kdc# kadmin.local +Authenticating as principal admin/admin@NIFI.APACHE.ORG with password. +kadmin.local: listprincs +K/M@NIFI.APACHE.ORG +admin/admin@NIFI.APACHE.ORG +... +kadmin.local: addprinc -randkey HTTP/nifi.nifi.apache.org +WARNING: no policy specified for HTTP/nifi.nifi.apache.org@NIFI.APACHE.ORG; defaulting to no policy +Principal "HTTP/nifi.nifi.apache.org@NIFI.APACHE.ORG" created. +kadmin.local: ktadd -k /http-nifi.keytab HTTP/nifi.nifi.apache.org +Entry for principal HTTP/nifi.nifi.apache.org with kvno 2, encryption type des3-cbc-sha1 added to keytab WRFILE:/http-nifi.keytab. +Entry for principal HTTP/nifi.nifi.apache.org with kvno 2, encryption type des-cbc-crc added to keytab WRFILE:/http-nifi.keytab. +kadmin.local: listprincs +HTTP/nifi.nifi.apache.org@NIFI.APACHE.ORG +K/M@NIFI.APACHE.ORG +admin/admin@NIFI.APACHE.ORG +... +kadmin.local: q +root@kdc:~# ll /http* +-rw------- 1 root root 162 Mar 14 21:43 /http-nifi.keytab +root@kdc:~# +.... [[system_properties]] System Properties @@ -1429,13 +1525,21 @@ Only configure these properties for the cluster manager. |nifi.cluster.manager.safemode.duration|Upon restart of an already existing cluster, this is the amount of time that the cluster manager waits for the primary node to connect before giving up and selecting another node to be the primary node. The default value is 0 sec, which means to wait forever. If the administrator does not care which node is the primary node, this value can be changed to some amount of time other than 0 sec. |==== -*Kerberos* + +[[kerberos_properties]] +*Kerberos Properties* + |==== |*Property*|*Description* |nifi.kerberos.krb5.file*|The location of the krb5 file, if used. It is blank by default. Note that this property is not used to authenticate NiFi users. Rather, it is made available for extension points, such as Hadoop-based Processors, to use. At this time, only a single krb5 file is allowed to be specified per NiFi instance, so this property is configured here rather than in individual Processors. + Example: `/etc/krb5.conf` +|nifi.kerberos.service.principal*|The name of the NiFi Kerberos service principal, if used. It is blank by default. Note that this property is used to authenticate NiFi users. + Example: `HTTP/nifi.example.com` or `HTTP/nifi.example.com@EXAMPLE.COM` +|nifi.kerberos.keytab.location*|The file path of the NiFi Kerberos keytab, if used. It is blank by default. Note that this property is used to authenticate NiFi users. + Example: `/etc/http-nifi.keytab` +|nifi.kerberos.authentication.expiration*|The expiration duration of a successful Kerberos user authentication, if used. It is 12 hours by default. + Example: `12 hours` |==== diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/login-identity-providers.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/login-identity-providers.xml index d6a0c2e098..3a57e35640 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/login-identity-providers.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/login-identity-providers.xml @@ -89,4 +89,21 @@ 12 hours To enable the ldap-provider remove 2 lines. This is 2 of 2. --> + + + \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties index 4a665d97a2..24d2295082 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties @@ -183,3 +183,6 @@ nifi.cluster.manager.safemode.duration=${nifi.cluster.manager.safemode.duration} # kerberos # nifi.kerberos.krb5.file=${nifi.kerberos.krb5.file} +nifi.kerberos.service.principal=${nifi.kerberos.service.principal} +nifi.kerberos.keytab.location=${nifi.kerberos.keytab.location} +nifi.kerberos.authentication.expiration=${nifi.kerberos.authentication.expiration} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java index d1bd5c879c..3e4c237dd4 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java @@ -16,32 +16,9 @@ */ package org.apache.nifi.web.server; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileFilter; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; - -import javax.servlet.DispatcherType; -import javax.servlet.ServletContext; - +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.nifi.NiFiServer; import org.apache.nifi.controller.FlowSerializationException; import org.apache.nifi.controller.FlowSynchronizationException; @@ -50,15 +27,12 @@ import org.apache.nifi.lifecycle.LifeCycleStartException; import org.apache.nifi.nar.ExtensionMapping; import org.apache.nifi.nar.NarClassLoaders; import org.apache.nifi.services.FlowService; -import org.apache.nifi.util.NiFiProperties; -import org.apache.nifi.web.NiFiWebContext; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.nifi.web.ContentAccess; import org.apache.nifi.ui.extension.UiExtension; import org.apache.nifi.ui.extension.UiExtensionMapping; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.ContentAccess; import org.apache.nifi.web.NiFiWebConfigurationContext; +import org.apache.nifi.web.NiFiWebContext; import org.apache.nifi.web.UiExtensionType; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; @@ -86,6 +60,31 @@ import org.springframework.context.ApplicationContext; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; +import javax.servlet.DispatcherType; +import javax.servlet.ServletContext; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + /** * Encapsulates the Jetty instance. */ @@ -615,8 +614,8 @@ public class JettyServer implements NiFiServer { private SslContextFactory createSslContextFactory() { final SslContextFactory contextFactory = new SslContextFactory(); - // require client auth when not supporting login or anonymous access - if (StringUtils.isBlank(props.getProperty(NiFiProperties.SECURITY_USER_LOGIN_IDENTITY_PROVIDER)) && props.getAnonymousAuthorities().isEmpty()) { + // require client auth when not supporting login, Kerberos service, or anonymous access + if (props.isClientAuthRequiredForRestApi()) { contextFactory.setNeedClientAuth(true); } else { contextFactory.setWantClientAuth(true); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml index ee36193f75..4460439010 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml @@ -325,5 +325,17 @@ nifi-volatile-provenance-repository test + + org.springframework.security.kerberos + spring-security-kerberos-core + 1.0.1.RELEASE + provided + + + org.springframework.security.kerberos + spring-security-kerberos-web + 1.0.1.RELEASE + provided + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java index 64ecc5ff40..fd44636caf 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java @@ -17,7 +17,6 @@ package org.apache.nifi.web; import org.apache.nifi.admin.service.UserService; -import org.apache.nifi.authentication.LoginIdentityProvider; import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.web.security.NiFiAuthenticationProvider; import org.apache.nifi.web.security.anonymous.NiFiAnonymousUserFilter; @@ -30,6 +29,8 @@ import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken; import org.apache.nifi.web.security.x509.X509AuthenticationFilter; import org.apache.nifi.web.security.x509.X509CertificateExtractor; import org.apache.nifi.web.security.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; @@ -51,15 +52,15 @@ import org.springframework.security.web.authentication.AnonymousAuthenticationFi @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapter { + private static final Logger logger = LoggerFactory.getLogger(NiFiWebApiSecurityConfiguration.class); private NiFiProperties properties; private UserService userService; - private AuthenticationUserDetailsService userDetailsService; + private AuthenticationUserDetailsService authenticationUserDetailsService; private JwtService jwtService; private OtpService otpService; private X509CertificateExtractor certificateExtractor; private X509IdentityProvider certificateIdentityProvider; - private LoginIdentityProvider loginIdentityProvider; private NodeAuthorizedUserFilter nodeAuthorizedUserFilter; private JwtAuthenticationFilter jwtAuthenticationFilter; @@ -78,7 +79,7 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte // the /access/download-token and /access/ui-extension-token endpoints webSecurity .ignoring() - .antMatchers("/access", "/access/config", "/access/token"); + .antMatchers("/access", "/access/config", "/access/token", "/access/kerberos"); } @Override @@ -116,7 +117,7 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.authenticationProvider(new NiFiAuthenticationProvider(userDetailsService)); + auth.authenticationProvider(new NiFiAuthenticationProvider(authenticationUserDetailsService)); } @Bean @@ -136,25 +137,18 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte jwtAuthenticationFilter = new JwtAuthenticationFilter(); jwtAuthenticationFilter.setProperties(properties); jwtAuthenticationFilter.setAuthenticationManager(authenticationManager()); - - // only consider the tokens when configured for login - if (loginIdentityProvider != null) { - jwtAuthenticationFilter.setJwtService(jwtService); - } + jwtAuthenticationFilter.setJwtService(jwtService); } return jwtAuthenticationFilter; } + @Bean public OtpAuthenticationFilter otpFilterBean() throws Exception { if (otpAuthenticationFilter == null) { otpAuthenticationFilter = new OtpAuthenticationFilter(); otpAuthenticationFilter.setProperties(properties); otpAuthenticationFilter.setAuthenticationManager(authenticationManager()); - - // only consider the tokens when configured for login - if (loginIdentityProvider != null) { - otpAuthenticationFilter.setOtpService(otpService); - } + otpAuthenticationFilter.setOtpService(otpService); } return otpAuthenticationFilter; } @@ -182,7 +176,7 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte @Autowired public void setUserDetailsService(AuthenticationUserDetailsService userDetailsService) { - this.userDetailsService = userDetailsService; + this.authenticationUserDetailsService = userDetailsService; } @Autowired @@ -205,11 +199,6 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte this.otpService = otpService; } - @Autowired - public void setLoginIdentityProvider(LoginIdentityProvider loginIdentityProvider) { - this.loginIdentityProvider = loginIdentityProvider; - } - @Autowired public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) { this.certificateExtractor = certificateExtractor; @@ -219,5 +208,4 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte public void setCertificateIdentityProvider(X509IdentityProvider certificateIdentityProvider) { this.certificateIdentityProvider = certificateIdentityProvider; } - } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java index 24536fcb72..5ec8d014cc 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java @@ -16,32 +16,12 @@ */ package org.apache.nifi.web.api; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; - -import io.jsonwebtoken.JwtException; -import org.apache.nifi.user.NiFiUser; -import org.apache.nifi.util.NiFiProperties; import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiOperation; import com.wordnik.swagger.annotations.ApiParam; import com.wordnik.swagger.annotations.ApiResponse; import com.wordnik.swagger.annotations.ApiResponses; -import java.net.URI; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.DefaultValue; -import javax.ws.rs.FormParam; -import javax.ws.rs.POST; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; +import io.jsonwebtoken.JwtException; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.admin.service.AdministrationException; import org.apache.nifi.authentication.AuthenticationResponse; @@ -50,17 +30,21 @@ import org.apache.nifi.authentication.LoginIdentityProvider; import org.apache.nifi.authentication.exception.IdentityAccessException; import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException; import org.apache.nifi.security.util.CertificateUtils; -import org.apache.nifi.web.api.dto.AccessStatusDTO; +import org.apache.nifi.user.NiFiUser; +import org.apache.nifi.util.FormatUtils; +import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.web.api.dto.AccessConfigurationDTO; +import org.apache.nifi.web.api.dto.AccessStatusDTO; import org.apache.nifi.web.api.dto.RevisionDTO; -import org.apache.nifi.web.api.entity.AccessStatusEntity; import org.apache.nifi.web.api.entity.AccessConfigurationEntity; +import org.apache.nifi.web.api.entity.AccessStatusEntity; import org.apache.nifi.web.api.request.ClientIdParameter; import org.apache.nifi.web.security.InvalidAuthenticationException; import org.apache.nifi.web.security.ProxiedEntitiesUtils; import org.apache.nifi.web.security.UntrustedProxyException; import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter; import org.apache.nifi.web.security.jwt.JwtService; +import org.apache.nifi.web.security.kerberos.KerberosService; import org.apache.nifi.web.security.otp.OtpService; import org.apache.nifi.web.security.token.LoginAuthenticationToken; import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken; @@ -73,11 +57,30 @@ import org.slf4j.LoggerFactory; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AccountStatusException; import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + /** * RESTful endpoint for managing a cluster. */ @@ -98,13 +101,15 @@ public class AccessResource extends ApplicationResource { private JwtService jwtService; private OtpService otpService; + private KerberosService kerberosService; + private AuthenticationUserDetailsService userDetailsService; /** * Retrieves the access configuration for this NiFi. * * @param httpServletRequest the servlet request - * @param clientId Optional client id. If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response. + * @param clientId Optional client id. If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response. * @return A accessConfigurationEntity */ @GET @@ -146,7 +151,7 @@ public class AccessResource extends ApplicationResource { * Gets the status the client's access. * * @param httpServletRequest the servlet request - * @param clientId Optional client id. If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response. + * @param clientId Optional client id. If the client id is not specified, a new one will be generated. This value (whether specified or generated) is included in the response. * @return A accessStatusEntity */ @GET @@ -159,11 +164,11 @@ public class AccessResource extends ApplicationResource { ) @ApiResponses( value = { - @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."), - @ApiResponse(code = 401, message = "Unable to determine access status because the client could not be authenticated."), - @ApiResponse(code = 403, message = "Unable to determine access status because the client is not authorized to make this request."), - @ApiResponse(code = 409, message = "Unable to determine access status because NiFi is not in the appropriate state."), - @ApiResponse(code = 500, message = "Unable to determine access status because an unexpected error occurred.") + @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."), + @ApiResponse(code = 401, message = "Unable to determine access status because the client could not be authenticated."), + @ApiResponse(code = 403, message = "Unable to determine access status because the client is not authorized to make this request."), + @ApiResponse(code = 409, message = "Unable to determine access status because NiFi is not in the appropriate state."), + @ApiResponse(code = 500, message = "Unable to determine access status because an unexpected error occurred.") } ) public Response getAccessStatus( @@ -194,11 +199,6 @@ public class AccessResource extends ApplicationResource { accessStatus.setStatus(AccessStatusDTO.Status.UNKNOWN.name()); accessStatus.setMessage("No credentials supplied, unknown user."); } else { - // not currently configured for username/password login, don't accept existing tokens - if (loginIdentityProvider == null) { - throw new IllegalStateException("This NiFi is not configured to support username/password logins."); - } - try { // Extract the Base64 encoded token from the Authorization header final String token = StringUtils.substringAfterLast(authorization, " "); @@ -296,26 +296,26 @@ public class AccessResource extends ApplicationResource { /** * Creates a single use access token for downloading FlowFile content. * - * @param httpServletRequest the servlet request - * @return A token (string) + * @param httpServletRequest the servlet request + * @return A token (string) */ @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.TEXT_PLAIN) @Path("/download-token") @ApiOperation( - value = "Creates a single use access token for downloading FlowFile content.", - notes = "The token returned is a base64 encoded string. It is valid for a single request up to five minutes from being issued. " + - "It is used as a query parameter name 'access_token'.", - response = String.class + value = "Creates a single use access token for downloading FlowFile content.", + notes = "The token returned is a base64 encoded string. It is valid for a single request up to five minutes from being issued. " + + "It is used as a query parameter name 'access_token'.", + response = String.class ) @ApiResponses( - value = { - @ApiResponse(code = 403, message = "Client is not authorized to make this request."), - @ApiResponse(code = 409, message = "Unable to create the download token because NiFi is not in the appropriate state. " + - "(i.e. may not have any tokens to grant or be configured to support username/password login)"), - @ApiResponse(code = 500, message = "Unable to create download token because an unexpected error occurred.") - } + value = { + @ApiResponse(code = 403, message = "Client is not authorized to make this request."), + @ApiResponse(code = 409, message = "Unable to create the download token because NiFi is not in the appropriate state. " + + "(i.e. may not have any tokens to grant or be configured to support username/password login)"), + @ApiResponse(code = 500, message = "Unable to create download token because an unexpected error occurred.") + } ) public Response createDownloadToken(@Context HttpServletRequest httpServletRequest) { // only support access tokens when communicating over HTTPS @@ -323,11 +323,6 @@ public class AccessResource extends ApplicationResource { throw new IllegalStateException("Download tokens are only issued over HTTPS."); } - // if not configuration for login, don't consider credentials - if (loginIdentityProvider == null) { - throw new IllegalStateException("Download tokens not supported by this NiFi."); - } - final NiFiUser user = NiFiUserUtils.getNiFiUser(); if (user == null) { throw new AccessDeniedException("Unable to determine user details."); @@ -346,26 +341,26 @@ public class AccessResource extends ApplicationResource { /** * Creates a single use access token for accessing a NiFi UI extension. * - * @param httpServletRequest the servlet request - * @return A token (string) + * @param httpServletRequest the servlet request + * @return A token (string) */ @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.TEXT_PLAIN) @Path("/ui-extension-token") @ApiOperation( - value = "Creates a single use access token for accessing a NiFi UI extension.", - notes = "The token returned is a base64 encoded string. It is valid for a single request up to five minutes from being issued. " + - "It is used as a query parameter name 'access_token'.", - response = String.class + value = "Creates a single use access token for accessing a NiFi UI extension.", + notes = "The token returned is a base64 encoded string. It is valid for a single request up to five minutes from being issued. " + + "It is used as a query parameter name 'access_token'.", + response = String.class ) @ApiResponses( - value = { - @ApiResponse(code = 403, message = "Client is not authorized to make this request."), - @ApiResponse(code = 409, message = "Unable to create the download token because NiFi is not in the appropriate state. " + - "(i.e. may not have any tokens to grant or be configured to support username/password login)"), - @ApiResponse(code = 500, message = "Unable to create download token because an unexpected error occurred.") - } + value = { + @ApiResponse(code = 403, message = "Client is not authorized to make this request."), + @ApiResponse(code = 409, message = "Unable to create the download token because NiFi is not in the appropriate state. " + + "(i.e. may not have any tokens to grant or be configured to support username/password login)"), + @ApiResponse(code = 500, message = "Unable to create download token because an unexpected error occurred.") + } ) public Response createUiExtensionToken(@Context HttpServletRequest httpServletRequest) { // only support access tokens when communicating over HTTPS @@ -373,11 +368,6 @@ public class AccessResource extends ApplicationResource { throw new IllegalStateException("UI extension access tokens are only issued over HTTPS."); } - // if not configuration for login, don't consider credentials - if (loginIdentityProvider == null) { - throw new IllegalStateException("UI extension access tokens not supported by this NiFi."); - } - final NiFiUser user = NiFiUserUtils.getNiFiUser(); if (user == null) { throw new AccessDeniedException("Unable to determine user details."); @@ -393,12 +383,88 @@ public class AccessResource extends ApplicationResource { return generateCreatedResponse(uri, token).build(); } + /** + * Creates a token for accessing the REST API via Kerberos ticket exchange / SPNEGO negotiation. + * + * @param httpServletRequest the servlet request + * @return A JWT (string) + */ + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Path("/kerberos") + @ApiOperation( + value = "Creates a token for accessing the REST API via Kerberos ticket exchange / SPNEGO negotiation", + notes = "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( + value = { + @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."), + @ApiResponse(code = 401, message = "NiFi was unable to complete the request because it did not contain a valid Kerberos " + + "ticket in the Authorization header. Retry this request after initializing a ticket with kinit and " + + "ensuring your browser is configured to support SPNEGO."), + @ApiResponse(code = 403, message = "Client is not authorized to make this request."), + @ApiResponse(code = 409, message = "Unable to create access token because NiFi is not in the appropriate state. (i.e. may not be configured to support Kerberos login."), + @ApiResponse(code = 500, message = "Unable to create access token because an unexpected error occurred.") + } + ) + public Response createAccessTokenFromTicket( + @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 Kerberos Service Principal and keytab location not configured, throws exception + if (!properties.isKerberosServiceSupportEnabled() || kerberosService == null) { + throw new IllegalStateException("Kerberos ticket login not supported by this NiFi."); + } + + String authorizationHeaderValue = httpServletRequest.getHeader(KerberosService.AUTHORIZATION_HEADER_NAME); + + if (!kerberosService.isValidKerberosHeader(authorizationHeaderValue)) { + final Response response = generateNotAuthorizedResponse().header(KerberosService.AUTHENTICATION_CHALLENGE_HEADER_NAME, KerberosService.AUTHORIZATION_NEGOTIATE).build(); + return response; + } else { + try { + // attempt to authenticate + Authentication authentication = kerberosService.validateKerberosTicket(httpServletRequest); + + if (authentication == null) { + throw new IllegalArgumentException("Request is not HTTPS or Kerberos ticket missing or malformed"); + } + + final String expirationFromProperties = properties.getKerberosAuthenticationExpiration(); + long expiration = FormatUtils.getTimeDuration(expirationFromProperties, TimeUnit.MILLISECONDS); + final String identity = authentication.getName(); + expiration = validateTokenExpiration(expiration, identity); + + // create the authentication token + final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(identity, expiration, "KerberosService"); + + + // generate JWT for response + final String token = jwtService.generateSignedToken(loginAuthenticationToken); + + // build the response + final URI uri = URI.create(generateResourceUri("access", "kerberos")); + return generateCreatedResponse(uri, token).build(); + } catch (final AuthenticationException e) { + throw new AccessDeniedException(e.getMessage(), e); + } + } + } + /** * Creates a token for accessing the REST API via username/password. * * @param httpServletRequest the servlet request - * @param username the username - * @param password the password + * @param username the username + * @param password the password * @return A JWT (string) */ @POST @@ -408,16 +474,16 @@ public class AccessResource extends ApplicationResource { @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, " + - "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 '.", + "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( value = { - @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."), - @ApiResponse(code = 403, message = "Client is not authorized to make this request."), - @ApiResponse(code = 409, message = "Unable to create access token because NiFi is not in the appropriate state. (i.e. may not be configured to support username/password login."), - @ApiResponse(code = 500, message = "Unable to create access token because an unexpected error occurred.") + @ApiResponse(code = 400, message = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."), + @ApiResponse(code = 403, message = "Client is not authorized to make this request."), + @ApiResponse(code = 409, message = "Unable to create access token because NiFi is not in the appropriate state. (i.e. may not be configured to support username/password login."), + @ApiResponse(code = 500, message = "Unable to create access token because an unexpected error occurred.") } ) public Response createAccessToken( @@ -449,21 +515,7 @@ public class AccessResource extends ApplicationResource { try { // attempt to authenticate final AuthenticationResponse authenticationResponse = loginIdentityProvider.authenticate(new LoginCredentials(username, password)); - final long maxExpiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS); - final long minExpiration = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES); - - long expiration = authenticationResponse.getExpiration(); - if (expiration > maxExpiration) { - expiration = maxExpiration; - - logger.warn(String.format("Max token expiration exceeded. Setting expiration to %s from %s for %s", expiration, - authenticationResponse.getExpiration(), authenticationResponse.getIdentity())); - } else if (expiration < minExpiration) { - expiration = minExpiration; - - logger.warn(String.format("Min token expiration not met. Setting expiration to %s from %s for %s", expiration, - authenticationResponse.getExpiration(), authenticationResponse.getIdentity())); - } + long expiration = validateTokenExpiration(authenticationResponse.getExpiration(), authenticationResponse.getIdentity()); // create the authentication token loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getIdentity(), expiration, authenticationResponse.getIssuer()); @@ -522,6 +574,23 @@ public class AccessResource extends ApplicationResource { } } + 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; + } + // setters public void setProperties(NiFiProperties properties) { this.properties = properties; @@ -535,6 +604,10 @@ public class AccessResource extends ApplicationResource { this.jwtService = jwtService; } + public void setKerberosService(KerberosService kerberosService) { + this.kerberosService = kerberosService; + } + public void setOtpService(OtpService otpService) { this.otpService = otpService; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java index 256d41f159..6f895b8e8c 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java @@ -20,6 +20,36 @@ import com.sun.jersey.api.core.HttpContext; import com.sun.jersey.api.representation.Form; import com.sun.jersey.core.util.MultivaluedMapImpl; import com.sun.jersey.server.impl.model.method.dispatch.FormDispatchProvider; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.apache.nifi.action.Action; +import org.apache.nifi.action.FlowChangeAction; +import org.apache.nifi.action.Operation; +import org.apache.nifi.cluster.context.ClusterContext; +import org.apache.nifi.cluster.context.ClusterContextThreadLocal; +import org.apache.nifi.cluster.manager.impl.WebClusterManager; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.api.entity.Entity; +import org.apache.nifi.web.api.request.ClientIdParameter; +import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter; +import org.apache.nifi.web.security.user.NiFiUserDetails; +import org.apache.nifi.web.util.WebUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.CacheControl; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriBuilderException; +import javax.ws.rs.core.UriInfo; import java.io.Serializable; import java.net.URI; import java.net.URISyntaxException; @@ -29,34 +59,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.TreeMap; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.core.CacheControl; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.ResponseBuilder; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriBuilderException; -import javax.ws.rs.core.UriInfo; -import org.apache.nifi.action.Action; -import org.apache.nifi.action.FlowChangeAction; -import org.apache.nifi.action.Operation; -import org.apache.nifi.cluster.context.ClusterContext; -import org.apache.nifi.cluster.context.ClusterContextThreadLocal; -import org.apache.nifi.cluster.manager.impl.WebClusterManager; -import org.apache.nifi.web.security.user.NiFiUserDetails; -import org.apache.nifi.util.NiFiProperties; -import org.apache.nifi.web.api.entity.Entity; -import org.apache.nifi.web.api.request.ClientIdParameter; -import org.apache.nifi.web.util.WebUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.builder.ReflectionToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; -import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; /** * Base class for controllers. @@ -278,6 +280,16 @@ public abstract class ApplicationResource { return Response.created(uri).entity(entity); } + /** + * Generates a 401 Not Authorized response with no content. + + * @return The response to be built + */ + protected ResponseBuilder generateNotAuthorizedResponse() { + // generate the response builder + return Response.status(HttpServletResponse.SC_UNAUTHORIZED); + } + /** * Generates a 150 Node Continue response to be used within the cluster request handshake. * diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml index c9326b23bd..60e8bcfece 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml @@ -260,6 +260,7 @@ + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml index 8f98b0609b..fc56936a3c 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml @@ -123,5 +123,10 @@ javax.servlet-api provided + + org.springframework.security.kerberos + spring-security-kerberos-core + 1.0.1.RELEASE + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java index e13fcead7b..bd468e4705 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java @@ -18,6 +18,7 @@ package org.apache.nifi.web.security.jwt; import io.jsonwebtoken.JwtException; import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.web.security.InvalidAuthenticationException; import org.apache.nifi.web.security.NiFiAuthenticationFilter; import org.apache.nifi.web.security.token.NewAccountAuthorizationRequestToken; import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken; @@ -27,7 +28,6 @@ import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletRequest; import java.util.Arrays; -import org.apache.nifi.web.security.InvalidAuthenticationException; /** */ @@ -52,13 +52,9 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter { final String authorization = request.getHeader(AUTHORIZATION); // if there is no authorization header, we don't know the user - if (authorization == null) { + if (authorization == null || !StringUtils.startsWith(authorization, "Bearer ")) { return null; } else { - if (jwtService == null) { - throw new InvalidAuthenticationException("NiFi is not configured to support username/password logins."); - } - // Extract the Base64 encoded token from the Authorization header final String token = StringUtils.substringAfterLast(authorization, " "); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java index 4bbec215c5..dd6a17ade3 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java @@ -29,13 +29,13 @@ import io.jsonwebtoken.SigningKeyResolverAdapter; import io.jsonwebtoken.UnsupportedJwtException; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.admin.service.AdministrationException; +import org.apache.nifi.admin.service.UserService; +import org.apache.nifi.key.Key; import org.apache.nifi.web.security.token.LoginAuthenticationToken; import org.slf4j.LoggerFactory; import java.nio.charset.StandardCharsets; import java.util.Calendar; -import org.apache.nifi.admin.service.UserService; -import org.apache.nifi.key.Key; /** * @@ -70,8 +70,6 @@ public class JwtService { // TODO: Validate issuer against active registry? if (StringUtils.isEmpty(jws.getBody().getIssuer())) { - // TODO: Remove after testing -// logger.info("Decoded JWT payload: " + jws.toString()); throw new JwtException("No issuer available in token"); } return jws.getBody().getSubject(); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/kerberos/AlternateKerberosUserDetailsService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/kerberos/AlternateKerberosUserDetailsService.java new file mode 100644 index 0000000000..4d072e67f3 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/kerberos/AlternateKerberosUserDetailsService.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.kerberos; + +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +/* Potential refactoring is documented in NIFI-1637 */ +public class AlternateKerberosUserDetailsService implements UserDetailsService { + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return new User(username, "notUsed", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_USER")); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/kerberos/KerberosService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/kerberos/KerberosService.java new file mode 100644 index 0000000000..f3d57bbf3a --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/kerberos/KerberosService.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.kerberos; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.codec.Base64; +import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider; +import org.springframework.security.kerberos.authentication.KerberosServiceRequestToken; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; + +import javax.servlet.http.HttpServletRequest; +import java.nio.charset.StandardCharsets; + +/** + * + */ +public class KerberosService { + + private static final Logger logger = LoggerFactory.getLogger(KerberosService.class); + + public static final String AUTHORIZATION_HEADER_NAME = "Authorization"; + public static final String AUTHENTICATION_CHALLENGE_HEADER_NAME = "WWW-Authenticate"; + public static final String AUTHORIZATION_NEGOTIATE = "Negotiate"; + + private KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider; + private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); + + public void setKerberosServiceAuthenticationProvider(KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider) { + this.kerberosServiceAuthenticationProvider = kerberosServiceAuthenticationProvider; + } + + public Authentication validateKerberosTicket(HttpServletRequest request) { + // Only support Kerberos login when running securely + if (!request.isSecure()) { + return null; + } + + String header = request.getHeader(AUTHORIZATION_HEADER_NAME); + + if (isValidKerberosHeader(header)) { + if (logger.isDebugEnabled()) { + logger.debug("Received Negotiate Header for request " + request.getRequestURL() + ": " + header); + } + byte[] base64Token = header.substring(header.indexOf(" ") + 1).getBytes(StandardCharsets.UTF_8); + byte[] kerberosTicket = Base64.decode(base64Token); + KerberosServiceRequestToken authenticationRequest = new KerberosServiceRequestToken(kerberosTicket); + authenticationRequest.setDetails(authenticationDetailsSource.buildDetails(request)); + + return kerberosServiceAuthenticationProvider.authenticate(authenticationRequest); + } else { + return null; + } + } + + public boolean isValidKerberosHeader(String header) { + return header != null && (header.startsWith("Negotiate ") || header.startsWith("Kerberos ")); + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/kerberos/KerberosServiceFactoryBean.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/kerberos/KerberosServiceFactoryBean.java new file mode 100644 index 0000000000..8b834a1209 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/kerberos/KerberosServiceFactoryBean.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.kerberos; + +import org.apache.nifi.util.NiFiProperties; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.core.io.FileSystemResource; +import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider; +import org.springframework.security.kerberos.authentication.KerberosTicketValidator; +import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator; + +public class KerberosServiceFactoryBean implements FactoryBean { + + private KerberosService kerberosService = null; + private NiFiProperties properties = null; + + @Override + public KerberosService getObject() throws Exception { + if (kerberosService == null && properties.isKerberosServiceSupportEnabled()) { + kerberosService = new KerberosService(); + kerberosService.setKerberosServiceAuthenticationProvider(createKerberosServiceAuthenticationProvider()); + } + + return kerberosService; + } + + @Override + public Class getObjectType() { + return KerberosService.class; + } + + @Override + public boolean isSingleton() { + return true; + } + + public void setProperties(NiFiProperties properties) { + this.properties = properties; + } + + private KerberosServiceAuthenticationProvider createKerberosServiceAuthenticationProvider() throws Exception { + KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider = new KerberosServiceAuthenticationProvider(); + kerberosServiceAuthenticationProvider.setTicketValidator(createTicketValidator()); + kerberosServiceAuthenticationProvider.setUserDetailsService(createAlternateKerberosUserDetailsService()); + kerberosServiceAuthenticationProvider.afterPropertiesSet(); + return kerberosServiceAuthenticationProvider; + } + + private AlternateKerberosUserDetailsService createAlternateKerberosUserDetailsService() { + return new AlternateKerberosUserDetailsService(); + } + + private KerberosTicketValidator createTicketValidator() throws Exception { + SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator(); + ticketValidator.setServicePrincipal(properties.getKerberosServicePrincipal()); + ticketValidator.setKeyTabLocation(new FileSystemResource(properties.getKerberosKeytabLocation())); + ticketValidator.afterPropertiesSet(); + return ticketValidator; + } +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpAuthenticationFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpAuthenticationFilter.java index 9c4429831e..7cf3eeb977 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpAuthenticationFilter.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpAuthenticationFilter.java @@ -57,10 +57,6 @@ public class OtpAuthenticationFilter extends NiFiAuthenticationFilter { if (accessToken == null) { return null; } else { - if (otpService == null) { - throw new InvalidAuthenticationException("NiFi is not configured to support username/password logins."); - } - try { String identity = null; if (request.getContextPath().equals("/nifi-api")) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml index 44a93af19a..4e24badc61 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml @@ -22,29 +22,29 @@ - + - + - + - + - + - + @@ -53,9 +53,14 @@ + + + + + - + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/otp/OtpAuthenticationFilterTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/otp/OtpAuthenticationFilterTest.java index d309191ea2..ad6f7221d6 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/otp/OtpAuthenticationFilterTest.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/otp/OtpAuthenticationFilterTest.java @@ -16,7 +16,6 @@ */ package org.apache.nifi.web.security.otp; -import org.apache.nifi.web.security.InvalidAuthenticationException; import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken; import org.junit.Before; import org.junit.Test; @@ -24,7 +23,6 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import javax.servlet.http.HttpServletRequest; - import java.util.List; import java.util.UUID; @@ -98,16 +96,6 @@ public class OtpAuthenticationFilterTest { assertNull(otpAuthenticationFilter.attemptAuthentication(request)); } - @Test(expected = InvalidAuthenticationException.class) - public void testTokenSupportDisabled() throws Exception { - final HttpServletRequest request = mock(HttpServletRequest.class); - when(request.isSecure()).thenReturn(true); - when(request.getParameter(OtpAuthenticationFilter.ACCESS_TOKEN)).thenReturn("my-access-token"); - - final OtpAuthenticationFilter noTokenAuthenticationFilter = new OtpAuthenticationFilter(); - noTokenAuthenticationFilter.attemptAuthentication(request); - } - @Test public void testUnsupportedDownloadPath() throws Exception { final HttpServletRequest request = mock(HttpServletRequest.class); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js index e11aa5669a..cfeb4a5ac1 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js @@ -59,6 +59,7 @@ nf.Canvas = (function () { urls: { identity: '../nifi-api/controller/identity', authorities: '../nifi-api/controller/authorities', + kerberos: '../nifi-api/access/kerberos', revision: '../nifi-api/controller/revision', status: '../nifi-api/controller/status', bulletinBoard: '../nifi-api/controller/bulletin-board', @@ -73,7 +74,7 @@ nf.Canvas = (function () { /** * Generates the breadcrumbs. - * + * * @argument {object} processGroup The process group */ var generateBreadcrumbs = function (processGroup) { @@ -110,7 +111,7 @@ nf.Canvas = (function () { /** * Starts polling for the revision. - * + * * @argument {int} autoRefreshInterval The auto refresh interval */ var startRevisionPolling = function (autoRefreshInterval) { @@ -121,7 +122,7 @@ nf.Canvas = (function () { /** * Polls for the revision. - * + * * @argument {int} autoRefreshInterval The auto refresh interval */ var pollForRevision = function (autoRefreshInterval) { @@ -139,7 +140,7 @@ nf.Canvas = (function () { /** * Start polling for the status. - * + * * @argument {int} autoRefreshInterval The auto refresh interval */ var startStatusPolling = function (autoRefreshInterval) { @@ -150,7 +151,7 @@ nf.Canvas = (function () { /** * Register the status poller. - * + * * @argument {int} autoRefreshInterval The auto refresh interval */ var pollForStatus = function (autoRefreshInterval) { @@ -222,165 +223,165 @@ nf.Canvas = (function () { // create the canvas svg = d3.select('#canvas-container').append('svg') - .on('contextmenu', function () { - // reset the canvas click flag - canvasClicked = false; + .on('contextmenu', function () { + // reset the canvas click flag + canvasClicked = false; - // since the context menu event propagated back to the canvas, clear the selection - nf.CanvasUtils.getSelection().classed('selected', false); + // since the context menu event propagated back to the canvas, clear the selection + nf.CanvasUtils.getSelection().classed('selected', false); - // show the context menu on the canvas - nf.ContextMenu.show(); + // show the context menu on the canvas + nf.ContextMenu.show(); - // prevent default browser behavior - d3.event.preventDefault(); - }); + // prevent default browser behavior + d3.event.preventDefault(); + }); // create the definitions element var defs = svg.append('defs'); // create arrow definitions for the various line types defs.selectAll('marker') - .data(['normal', 'ghost']) - .enter().append('marker') - .attr({ - 'id': function (d) { - return d; - }, - 'viewBox': '0 0 6 6', - 'refX': 5, - 'refY': 3, - 'markerWidth': 6, - 'markerHeight': 6, - 'orient': 'auto', - 'fill': function (d) { - if (d === 'ghost') { - return '#aaaaaa'; - } else { - return '#000000'; - } + .data(['normal', 'ghost']) + .enter().append('marker') + .attr({ + 'id': function (d) { + return d; + }, + 'viewBox': '0 0 6 6', + 'refX': 5, + 'refY': 3, + 'markerWidth': 6, + 'markerHeight': 6, + 'orient': 'auto', + 'fill': function (d) { + if (d === 'ghost') { + return '#aaaaaa'; + } else { + return '#000000'; } - }) - .append('path') - .attr('d', 'M2,3 L0,6 L6,3 L0,0 z'); + } + }) + .append('path') + .attr('d', 'M2,3 L0,6 L6,3 L0,0 z'); // define the gradient for the processor stats background var processGroupStatsBackground = defs.append('linearGradient') - .attr({ - 'id': 'process-group-stats-background', - 'x1': '0%', - 'y1': '100%', - 'x2': '0%', - 'y2': '0%' - }); + .attr({ + 'id': 'process-group-stats-background', + 'x1': '0%', + 'y1': '100%', + 'x2': '0%', + 'y2': '0%' + }); processGroupStatsBackground.append('stop') - .attr({ - 'offset': '0%', - 'stop-color': '#dedede' - }); + .attr({ + 'offset': '0%', + 'stop-color': '#dedede' + }); processGroupStatsBackground.append('stop') - .attr({ - 'offset': '50%', - 'stop-color': '#ffffff' - }); + .attr({ + 'offset': '50%', + 'stop-color': '#ffffff' + }); processGroupStatsBackground.append('stop') - .attr({ - 'offset': '100%', - 'stop-color': '#dedede' - }); + .attr({ + 'offset': '100%', + 'stop-color': '#dedede' + }); // define the gradient for the processor stats background var processorStatsBackground = defs.append('linearGradient') - .attr({ - 'id': 'processor-stats-background', - 'x1': '0%', - 'y1': '100%', - 'x2': '0%', - 'y2': '0%' - }); + .attr({ + 'id': 'processor-stats-background', + 'x1': '0%', + 'y1': '100%', + 'x2': '0%', + 'y2': '0%' + }); processorStatsBackground.append('stop') - .attr({ - 'offset': '0%', - 'stop-color': '#6f97ac' - }); + .attr({ + 'offset': '0%', + 'stop-color': '#6f97ac' + }); processorStatsBackground.append('stop') - .attr({ - 'offset': '100%', - 'stop-color': '#30505c' - }); + .attr({ + 'offset': '100%', + 'stop-color': '#30505c' + }); // define the gradient for the port background var portBackground = defs.append('linearGradient') - .attr({ - 'id': 'port-background', - 'x1': '0%', - 'y1': '100%', - 'x2': '0%', - 'y2': '0%' - }); + .attr({ + 'id': 'port-background', + 'x1': '0%', + 'y1': '100%', + 'x2': '0%', + 'y2': '0%' + }); portBackground.append('stop') - .attr({ - 'offset': '0%', - 'stop-color': '#aaaaaa' - }); + .attr({ + 'offset': '0%', + 'stop-color': '#aaaaaa' + }); portBackground.append('stop') - .attr({ - 'offset': '100%', - 'stop-color': '#ffffff' - }); + .attr({ + 'offset': '100%', + 'stop-color': '#ffffff' + }); // define the gradient for the expiration icon var expirationBackground = defs.append('linearGradient') - .attr({ - 'id': 'expiration', - 'x1': '0%', - 'y1': '0%', - 'x2': '0%', - 'y2': '100%' - }); + .attr({ + 'id': 'expiration', + 'x1': '0%', + 'y1': '0%', + 'x2': '0%', + 'y2': '100%' + }); expirationBackground.append('stop') - .attr({ - 'offset': '0%', - 'stop-color': '#aeafb1' - }); + .attr({ + 'offset': '0%', + 'stop-color': '#aeafb1' + }); expirationBackground.append('stop') - .attr({ - 'offset': '100%', - 'stop-color': '#87888a' - }); + .attr({ + 'offset': '100%', + 'stop-color': '#87888a' + }); // create the canvas element canvas = svg.append('g') - .attr({ - 'transform': 'translate(' + TRANSLATE + ') scale(' + SCALE + ')', - 'pointer-events': 'all', - 'id': 'canvas' - }); + .attr({ + 'transform': 'translate(' + TRANSLATE + ') scale(' + SCALE + ')', + 'pointer-events': 'all', + 'id': 'canvas' + }); // handle canvas events svg.on('mousedown.selection', function () { - canvasClicked = true; + canvasClicked = true; - if (d3.event.button !== 0) { - // prevent further propagation (to parents and others handlers - // on the same element to prevent zoom behavior) - d3.event.stopImmediatePropagation(); - return; - } + if (d3.event.button !== 0) { + // prevent further propagation (to parents and others handlers + // on the same element to prevent zoom behavior) + d3.event.stopImmediatePropagation(); + return; + } - // show selection box if shift is held down - if (d3.event.shiftKey) { - var position = d3.mouse(canvas.node()); - canvas.append('rect') + // show selection box if shift is held down + if (d3.event.shiftKey) { + var position = d3.mouse(canvas.node()); + canvas.append('rect') .attr('rx', 6) .attr('ry', 6) .attr('x', position[0]) @@ -396,108 +397,108 @@ nf.Canvas = (function () { }) .datum(position); - // prevent further propagation (to parents and others handlers - // on the same element to prevent zoom behavior) - d3.event.stopImmediatePropagation(); + // prevent further propagation (to parents and others handlers + // on the same element to prevent zoom behavior) + d3.event.stopImmediatePropagation(); - // prevents the browser from changing to a text selection cursor - d3.event.preventDefault(); - } - }) - .on('mousemove.selection', function () { - // update selection box if shift is held down - if (d3.event.shiftKey) { - // get the selection box - var selectionBox = d3.select('rect.selection'); - if (!selectionBox.empty()) { - // get the original position - var originalPosition = selectionBox.datum(); - var position = d3.mouse(canvas.node()); - - var d = {}; - if (originalPosition[0] < position[0]) { - d.x = originalPosition[0]; - d.width = position[0] - originalPosition[0]; - } else { - d.x = position[0]; - d.width = originalPosition[0] - position[0]; - } - - if (originalPosition[1] < position[1]) { - d.y = originalPosition[1]; - d.height = position[1] - originalPosition[1]; - } else { - d.y = position[1]; - d.height = originalPosition[1] - position[1]; - } - - // update the selection box - selectionBox.attr(d); - - // prevent further propagation (to parents) - d3.event.stopPropagation(); - } - } - }) - .on('mouseup.selection', function () { - // ensure this originated from clicking the canvas, not a component. - // when clicking on a component, the event propagation is stopped so - // it never reaches the canvas. we cannot do this however on up events - // since the drag events break down - if (canvasClicked === false) { - return; - } - - // reset the canvas click flag - canvasClicked = false; - - // get the selection box + // prevents the browser from changing to a text selection cursor + d3.event.preventDefault(); + } + }) + .on('mousemove.selection', function () { + // update selection box if shift is held down + if (d3.event.shiftKey) { + // get the selection box var selectionBox = d3.select('rect.selection'); if (!selectionBox.empty()) { - var selectionBoundingBox = { - x: parseInt(selectionBox.attr('x'), 10), - y: parseInt(selectionBox.attr('y'), 10), - width: parseInt(selectionBox.attr('width'), 10), - height: parseInt(selectionBox.attr('height'), 10) - }; + // get the original position + var originalPosition = selectionBox.datum(); + var position = d3.mouse(canvas.node()); - // see if a component should be selected or not - d3.selectAll('g.component').classed('selected', function (d) { - // consider it selected if its already selected or enclosed in the bounding box - return d3.select(this).classed('selected') || - d.component.position.x >= selectionBoundingBox.x && (d.component.position.x + d.dimensions.width) <= (selectionBoundingBox.x + selectionBoundingBox.width) && - d.component.position.y >= selectionBoundingBox.y && (d.component.position.y + d.dimensions.height) <= (selectionBoundingBox.y + selectionBoundingBox.height); - }); + var d = {}; + if (originalPosition[0] < position[0]) { + d.x = originalPosition[0]; + d.width = position[0] - originalPosition[0]; + } else { + d.x = position[0]; + d.width = originalPosition[0] - position[0]; + } - // see if a connection should be selected or not - d3.selectAll('g.connection').classed('selected', function (d) { - // consider all points - var points = [d.start].concat(d.bends, [d.end]); + if (originalPosition[1] < position[1]) { + d.y = originalPosition[1]; + d.height = position[1] - originalPosition[1]; + } else { + d.y = position[1]; + d.height = originalPosition[1] - position[1]; + } - // determine the bounding box - var x = d3.extent(points, function (pt) { - return pt.x; - }); - var y = d3.extent(points, function (pt) { - return pt.y; - }); + // update the selection box + selectionBox.attr(d); - // consider it selected if its already selected or enclosed in the bounding box - return d3.select(this).classed('selected') || - x[0] >= selectionBoundingBox.x && x[1] <= (selectionBoundingBox.x + selectionBoundingBox.width) && - y[0] >= selectionBoundingBox.y && y[1] <= (selectionBoundingBox.y + selectionBoundingBox.height); - }); - - // remove the selection box - selectionBox.remove(); - } else if (panning === false) { - // deselect as necessary if we are not panning - nf.CanvasUtils.getSelection().classed('selected', false); + // prevent further propagation (to parents) + d3.event.stopPropagation(); } + } + }) + .on('mouseup.selection', function () { + // ensure this originated from clicking the canvas, not a component. + // when clicking on a component, the event propagation is stopped so + // it never reaches the canvas. we cannot do this however on up events + // since the drag events break down + if (canvasClicked === false) { + return; + } - // update the toolbar - nf.CanvasToolbar.refresh(); - }); + // reset the canvas click flag + canvasClicked = false; + + // get the selection box + var selectionBox = d3.select('rect.selection'); + if (!selectionBox.empty()) { + var selectionBoundingBox = { + x: parseInt(selectionBox.attr('x'), 10), + y: parseInt(selectionBox.attr('y'), 10), + width: parseInt(selectionBox.attr('width'), 10), + height: parseInt(selectionBox.attr('height'), 10) + }; + + // see if a component should be selected or not + d3.selectAll('g.component').classed('selected', function (d) { + // consider it selected if its already selected or enclosed in the bounding box + return d3.select(this).classed('selected') || + d.component.position.x >= selectionBoundingBox.x && (d.component.position.x + d.dimensions.width) <= (selectionBoundingBox.x + selectionBoundingBox.width) && + d.component.position.y >= selectionBoundingBox.y && (d.component.position.y + d.dimensions.height) <= (selectionBoundingBox.y + selectionBoundingBox.height); + }); + + // see if a connection should be selected or not + d3.selectAll('g.connection').classed('selected', function (d) { + // consider all points + var points = [d.start].concat(d.bends, [d.end]); + + // determine the bounding box + var x = d3.extent(points, function (pt) { + return pt.x; + }); + var y = d3.extent(points, function (pt) { + return pt.y; + }); + + // consider it selected if its already selected or enclosed in the bounding box + return d3.select(this).classed('selected') || + x[0] >= selectionBoundingBox.x && x[1] <= (selectionBoundingBox.x + selectionBoundingBox.width) && + y[0] >= selectionBoundingBox.y && y[1] <= (selectionBoundingBox.y + selectionBoundingBox.height); + }); + + // remove the selection box + selectionBox.remove(); + } else if (panning === false) { + // deselect as necessary if we are not panning + nf.CanvasUtils.getSelection().classed('selected', false); + } + + // update the toolbar + nf.CanvasToolbar.refresh(); + }); // define a function for update the graph dimensions var updateGraphSize = function () { @@ -623,7 +624,7 @@ nf.Canvas = (function () { /** * Sets the colors for the specified type. - * + * * @param {array} colors The possible colors * @param {string} type The component type for these colors */ @@ -637,30 +638,30 @@ nf.Canvas = (function () { // define the gradient for the processor background var gradient = processorSelection.enter().append('linearGradient') - .attr({ - 'id': function (d) { - return type + '-background-' + d; - }, - 'class': type + '-background', - 'x1': '0%', - 'y1': '100%', - 'x2': '0%', - 'y2': '0%' - }); + .attr({ + 'id': function (d) { + return type + '-background-' + d; + }, + 'class': type + '-background', + 'x1': '0%', + 'y1': '100%', + 'x2': '0%', + 'y2': '0%' + }); gradient.append('stop') - .attr({ - 'offset': '0%', - 'stop-color': function (d) { - return '#' + d; - } - }); + .attr({ + 'offset': '0%', + 'stop-color': function (d) { + return '#' + d; + } + }); gradient.append('stop') - .attr({ - 'offset': '100%', - 'stop-color': '#ffffff' - }); + .attr({ + 'offset': '100%', + 'stop-color': '#ffffff' + }); // remove old processor colors processorSelection.exit().remove(); @@ -780,7 +781,7 @@ nf.Canvas = (function () { /** * Refreshes the graph. - * + * * @argument {string} processGroupId The process group id */ var reloadProcessGroup = function (processGroupId) { @@ -827,12 +828,12 @@ nf.Canvas = (function () { /** * Refreshes the status for the resources that exist in the specified process group. - * + * * @argument {string} processGroupId The id of the process group */ var reloadStatus = function (processGroupId) { // get the stats - return $.Deferred(function (deferred) { + return $.Deferred(function (deferred) { $.ajax({ type: 'GET', url: config.urls.controller + '/process-groups/' + encodeURIComponent(processGroupId) + '/status', @@ -956,55 +957,77 @@ nf.Canvas = (function () { * Initialize NiFi. */ init: function () { - // get the current user's identity - var identityXhr = $.ajax({ - type: 'GET', - url: config.urls.identity, - dataType: 'json' - }); - - // get the current user's authorities - var authoritiesXhr = $.ajax({ - type: 'GET', - url: config.urls.authorities, - dataType: 'json' - }); + // attempt kerberos authentication + var ticketExchange = $.Deferred(function (deferred) { + if (nf.Storage.hasItem('jwt')) { + deferred.resolve(); + } else { + $.ajax({ + type: 'POST', + url: config.urls.kerberos, + dataType: 'text' + }).done(function (jwt) { + // get the payload and store the token with the appropriate expiration + var token = nf.Common.getJwtPayload(jwt); + var expiration = parseInt(token['exp'], 10) * nf.Common.MILLIS_PER_SECOND; + nf.Storage.setItem('jwt', jwt, expiration); + deferred.resolve(); + }).fail(function () { + deferred.reject(); + }); + } + }).promise(); // load the identity and authorities for the current user var userXhr = $.Deferred(function (deferred) { - $.when(authoritiesXhr, identityXhr).done(function (authoritiesResult, identityResult) { - var authoritiesResponse = authoritiesResult[0]; - var identityResponse = identityResult[0]; + ticketExchange.always(function () { + // get the current user's identity + var identityXhr = $.ajax({ + type: 'GET', + url: config.urls.identity, + dataType: 'json' + }); - // set the user's authorities - nf.Common.setAuthorities(authoritiesResponse.authorities); + // get the current user's authorities + var authoritiesXhr = $.ajax({ + type: 'GET', + url: config.urls.authorities, + dataType: 'json' + }); - // at this point the user may be themselves or anonymous + $.when(authoritiesXhr, identityXhr).done(function (authoritiesResult, identityResult) { + var authoritiesResponse = authoritiesResult[0]; + var identityResponse = identityResult[0]; - // if the user is logged, we want to determine if they were logged in using a certificate - if (identityResponse.identity !== 'anonymous') { - // rendner the users name - $('#current-user').text(identityResponse.identity).show(); + // set the user's authorities + nf.Common.setAuthorities(authoritiesResponse.authorities); - // render the logout button if there is a token locally - if (nf.Storage.getItem('jwt') !== null) { - $('#logout-link-container').show(); + // at this point the user may be themselves or anonymous + + // if the user is logged, we want to determine if they were logged in using a certificate + if (identityResponse.identity !== 'anonymous') { + // rendner the users name + $('#current-user').text(identityResponse.identity).show(); + + // render the logout button if there is a token locally + if (nf.Storage.getItem('jwt') !== null) { + $('#logout-link-container').show(); + } + } else { + // set the anonymous user label + nf.Common.setAnonymousUserLabel(); } - } else { - // set the anonymous user label - nf.Common.setAnonymousUserLabel(); - } - deferred.resolve(); - }).fail(function (xhr, status, error) { - // there is no anonymous access and we don't know this user - open the login page which handles login/registration/etc - if (xhr.status === 401 || xhr.status === 403) { - window.location = '/nifi/login'; - } else { - deferred.reject(xhr, status, error); - } + deferred.resolve(); + }).fail(function (xhr, status, error) { + // there is no anonymous access and we don't know this user - open the login page which handles login/registration/etc + if (xhr.status === 401 || xhr.status === 403) { + window.location = '/nifi/login'; + } else { + deferred.reject(xhr, status, error); + } + }); }); }).promise(); - userXhr.done(function () { // get the controller config to register the status poller var configXhr = $.ajax({ @@ -1132,7 +1155,7 @@ nf.Canvas = (function () { /** * Defines the gradient colors used to render processors. - * + * * @param {array} colors The colors */ defineProcessorColors: function (colors) { @@ -1141,7 +1164,7 @@ nf.Canvas = (function () { /** * Defines the gradient colors used to render label. - * + * * @param {array} colors The colors */ defineLabelColors: function (colors) { @@ -1150,7 +1173,7 @@ nf.Canvas = (function () { /** * Return whether this instance of NiFi is clustered. - * + * * @returns {Boolean} */ isClustered: function () { @@ -1166,7 +1189,7 @@ nf.Canvas = (function () { /** * Set the group id. - * + * * @argument {string} gi The group id */ setGroupId: function (gi) { @@ -1182,7 +1205,7 @@ nf.Canvas = (function () { /** * Set the group name. - * + * * @argument {string} gn The group name */ setGroupName: function (gn) { @@ -1198,7 +1221,7 @@ nf.Canvas = (function () { /** * Set the parent group id. - * + * * @argument {string} pgi The id of the parent group */ setParentGroupId: function (pgi) { @@ -1277,9 +1300,9 @@ nf.Canvas = (function () { // mark the selection as appropriate selection.classed('visible', visible) - .classed('entering', function () { - return visible && !wasVisible; - }).classed('leaving', function () { + .classed('entering', function () { + return visible && !wasVisible; + }).classed('leaving', function () { return !visible && wasVisible; }); }; @@ -1315,55 +1338,55 @@ nf.Canvas = (function () { // define the behavior behavior = d3.behavior.zoom() - .scaleExtent([MIN_SCALE, MAX_SCALE]) - .translate(TRANSLATE) - .scale(SCALE) - .on('zoomstart', function () { - // hide the context menu - nf.ContextMenu.hide(); - }) - .on('zoom', function () { - // if we have zoomed, indicate that we are panning - // to prevent deselection elsewhere - if (zoomed) { - panning = true; - } else { - zoomed = true; - } + .scaleExtent([MIN_SCALE, MAX_SCALE]) + .translate(TRANSLATE) + .scale(SCALE) + .on('zoomstart', function () { + // hide the context menu + nf.ContextMenu.hide(); + }) + .on('zoom', function () { + // if we have zoomed, indicate that we are panning + // to prevent deselection elsewhere + if (zoomed) { + panning = true; + } else { + zoomed = true; + } - // see if the scale has changed during this zoom event, - // we want to only transition when zooming in/out as running - // the transitions during pan events is - var transition = d3.event.sourceEvent.type === 'wheel' || d3.event.sourceEvent.type === 'mousewheel'; + // see if the scale has changed during this zoom event, + // we want to only transition when zooming in/out as running + // the transitions during pan events is + var transition = d3.event.sourceEvent.type === 'wheel' || d3.event.sourceEvent.type === 'mousewheel'; - // refresh the canvas - refreshed = nf.Canvas.View.refresh({ - persist: false, - transition: transition, - refreshComponents: false, - refreshBirdseye: false - }); - }) - .on('zoomend', function () { - // ensure the canvas was actually refreshed - if (nf.Common.isDefinedAndNotNull(refreshed)) { - nf.Canvas.View.updateVisibility(); - - // refresh the birdseye - refreshed.done(function () { - nf.Birdseye.refresh(); - }); - - // persist the users view - nf.CanvasUtils.persistUserView(); - - // reset the refreshed deferred - refreshed = null; - } - - panning = false; - zoomed = false; + // refresh the canvas + refreshed = nf.Canvas.View.refresh({ + persist: false, + transition: transition, + refreshComponents: false, + refreshBirdseye: false }); + }) + .on('zoomend', function () { + // ensure the canvas was actually refreshed + if (nf.Common.isDefinedAndNotNull(refreshed)) { + nf.Canvas.View.updateVisibility(); + + // refresh the birdseye + refreshed.done(function () { + nf.Birdseye.refresh(); + }); + + // persist the users view + nf.CanvasUtils.persistUserView(); + + // reset the refreshed deferred + refreshed = null; + } + + panning = false; + zoomed = false; + }); // add the behavior to the canvas and disable dbl click zoom svg.call(behavior).on('dblclick.zoom', null); @@ -1371,7 +1394,7 @@ nf.Canvas = (function () { /** * Whether or not a component should be rendered based solely on the current scale. - * + * * @returns {Boolean} */ shouldRenderPerScale: function () { @@ -1388,7 +1411,7 @@ nf.Canvas = (function () { /** * Sets/gets the current translation. - * + * * @param {array} translate [x, y] */ translate: function (translate) { @@ -1401,7 +1424,7 @@ nf.Canvas = (function () { /** * Sets/gets the current scale. - * + * * @param {number} scale The new scale */ scale: function (scale) { @@ -1560,7 +1583,7 @@ nf.Canvas = (function () { /** * Refreshes the view based on the configured translation and scale. - * + * * @param {object} options Options for the refresh operation */ refresh: function (options) { @@ -1592,18 +1615,18 @@ nf.Canvas = (function () { // update the canvas if (transition === true) { canvas.transition() - .duration(500) - .attr('transform', function () { - return 'translate(' + behavior.translate() + ') scale(' + behavior.scale() + ')'; - }) - .each('end', function () { - // refresh birdseye if appropriate - if (refreshBirdseye === true) { - nf.Birdseye.refresh(); - } + .duration(500) + .attr('transform', function () { + return 'translate(' + behavior.translate() + ') scale(' + behavior.scale() + ')'; + }) + .each('end', function () { + // refresh birdseye if appropriate + if (refreshBirdseye === true) { + nf.Birdseye.refresh(); + } - deferred.resolve(); - }); + deferred.resolve(); + }); } else { canvas.attr('transform', function () { return 'translate(' + behavior.translate() + ') scale(' + behavior.scale() + ')'; diff --git a/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers-nar/pom.xml b/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers-nar/pom.xml new file mode 100644 index 0000000000..a30cfb5027 --- /dev/null +++ b/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers-nar/pom.xml @@ -0,0 +1,36 @@ + + + + 4.0.0 + + org.apache.nifi + nifi-kerberos-iaa-providers-bundle + 0.6.0-SNAPSHOT + + nifi-kerberos-iaa-providers-nar + nar + + true + true + + + + org.apache.nifi + nifi-kerberos-iaa-providers + + + nifi-kerberos-iaa-providers-nar + \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers-nar/src/main/resources/META-INF/NOTICE b/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers-nar/src/main/resources/META-INF/NOTICE new file mode 100644 index 0000000000..5c9bfc6e39 --- /dev/null +++ b/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers-nar/src/main/resources/META-INF/NOTICE @@ -0,0 +1,37 @@ +nifi-kerberos-iaa-providers-nar +Copyright 2014-2016 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +****************** +Apache Software License v2 +****************** + +The following binary components are provided under the Apache Software License v2 + + (ASLv2) Spring Framework + The following NOTICE information applies: + Spring Framework 4.1.6.RELEASE + Copyright (c) 2002-2015 Pivotal, Inc. + + (ASLv2) Spring Security + The following NOTICE information applies: + Spring Framework 4.0.3.RELEASE + Copyright (c) 2002-2015 Pivotal, Inc. + + (ASLv2) Apache Commons Lang + The following NOTICE information applies: + Apache Commons Lang + Copyright 2001-2015 The Apache Software Foundation + + This product includes software from the Spring Framework, + under the Apache License 2.0 (see: StringUtils.containsWhitespace()) + +***************** +Public Domain +***************** + +The following binary components are provided to the 'Public Domain'. See project link for details. + + (Public Domain) AOP Alliance 1.0 (http://aopalliance.sourceforge.net/) \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers/pom.xml b/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers/pom.xml new file mode 100644 index 0000000000..86afc6abbc --- /dev/null +++ b/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers/pom.xml @@ -0,0 +1,60 @@ + + + + 4.0.0 + + org.apache.nifi + nifi-kerberos-iaa-providers-bundle + 0.6.0-SNAPSHOT + + nifi-kerberos-iaa-providers + jar + + + org.apache.nifi + nifi-api + + + org.apache.nifi + nifi-utils + + + org.apache.nifi + nifi-security-utils + + + org.springframework.security.kerberos + spring-security-kerberos-core + + + org.springframework + spring-beans + + + org.springframework + spring-context + + + org.springframework + spring-tx + + + org.apache.commons + commons-lang3 + + + nifi-kerberos-iaa-providers + \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers/src/main/java/org/apache/nifi/kerberos/KerberosProvider.java b/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers/src/main/java/org/apache/nifi/kerberos/KerberosProvider.java new file mode 100644 index 0000000000..d0636c5be6 --- /dev/null +++ b/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers/src/main/java/org/apache/nifi/kerberos/KerberosProvider.java @@ -0,0 +1,101 @@ +/* + * 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.kerberos; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.authentication.AuthenticationResponse; +import org.apache.nifi.authentication.LoginCredentials; +import org.apache.nifi.authentication.LoginIdentityProvider; +import org.apache.nifi.authentication.LoginIdentityProviderConfigurationContext; +import org.apache.nifi.authentication.LoginIdentityProviderInitializationContext; +import org.apache.nifi.authentication.exception.IdentityAccessException; +import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException; +import org.apache.nifi.authorization.exception.ProviderCreationException; +import org.apache.nifi.authorization.exception.ProviderDestructionException; +import org.apache.nifi.util.FormatUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider; +import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient; + +import java.util.concurrent.TimeUnit; + +/** + * Kerberos-based implementation of a login identity provider. + */ +public class KerberosProvider implements LoginIdentityProvider { + + private static final Logger logger = LoggerFactory.getLogger(KerberosProvider.class); + + private KerberosAuthenticationProvider provider; + private String issuer; + private long expiration; + + @Override + public final void initialize(final LoginIdentityProviderInitializationContext initializationContext) throws ProviderCreationException { + this.issuer = getClass().getSimpleName(); + } + + @Override + public final void onConfigured(final LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException { + final String rawExpiration = configurationContext.getProperty("Authentication Expiration"); + if (StringUtils.isBlank(rawExpiration)) { + throw new ProviderCreationException("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)); + } + + provider = new KerberosAuthenticationProvider(); + SunJaasKerberosClient client = new SunJaasKerberosClient(); + client.setDebug(true); + provider.setKerberosClient(client); + provider.setUserDetailsService(new KerberosUserDetailsService()); + } + + @Override + public final AuthenticationResponse authenticate(final LoginCredentials credentials) throws InvalidLoginCredentialsException, IdentityAccessException { + if (provider == null) { + throw new IdentityAccessException("The Kerberos authentication provider is not initialized."); + } + + try { + // Perform the authentication + final UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(credentials.getUsername(), credentials.getPassword()); + logger.debug("Created authentication token for principal {} with name {} and is authenticated {}", token.getPrincipal(), token.getName(), token.isAuthenticated()); + + final Authentication authentication = provider.authenticate(token); + logger.debug("Ran provider.authenticate() and returned authentication for " + + "principal {} with name {} and is authenticated {}", authentication.getPrincipal(), authentication.getName(), authentication.isAuthenticated()); + + return new AuthenticationResponse(authentication.getName(), credentials.getUsername(), expiration, issuer); + } catch (final AuthenticationException e) { + throw new InvalidLoginCredentialsException(e.getMessage(), e); + } + } + + @Override + public final void preDestruction() throws ProviderDestructionException { + } + +} diff --git a/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers/src/main/java/org/apache/nifi/kerberos/KerberosUserDetailsService.java b/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers/src/main/java/org/apache/nifi/kerberos/KerberosUserDetailsService.java new file mode 100644 index 0000000000..6a5719ae25 --- /dev/null +++ b/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers/src/main/java/org/apache/nifi/kerberos/KerberosUserDetailsService.java @@ -0,0 +1,31 @@ +/* + * 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.kerberos; + +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +public class KerberosUserDetailsService implements UserDetailsService { + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return new User(username, "notUsed", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_USER")); + } +} diff --git a/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authentication.LoginIdentityProvider b/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authentication.LoginIdentityProvider new file mode 100644 index 0000000000..28bd2ab870 --- /dev/null +++ b/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/nifi-kerberos-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authentication.LoginIdentityProvider @@ -0,0 +1,15 @@ +# 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. +org.apache.nifi.kerberos.KerberosProvider \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/pom.xml b/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/pom.xml new file mode 100644 index 0000000000..e834e1b395 --- /dev/null +++ b/nifi-nar-bundles/nifi-kerberos-iaa-providers-bundle/pom.xml @@ -0,0 +1,38 @@ + + + + 4.0.0 + + org.apache.nifi + nifi-nar-bundles + 0.6.0-SNAPSHOT + + nifi-kerberos-iaa-providers-bundle + pom + + nifi-kerberos-iaa-providers + nifi-kerberos-iaa-providers-nar + + + + + org.apache.nifi + nifi-kerberos-iaa-providers + 0.6.0-SNAPSHOT + + + + \ No newline at end of file diff --git a/nifi-nar-bundles/pom.xml b/nifi-nar-bundles/pom.xml index c20201cf30..7e82c97a12 100644 --- a/nifi-nar-bundles/pom.xml +++ b/nifi-nar-bundles/pom.xml @@ -49,6 +49,7 @@ nifi-couchbase-bundle nifi-azure-bundle nifi-ldap-iaa-providers-bundle + nifi-kerberos-iaa-providers-bundle nifi-riemann-bundle nifi-html-bundle nifi-scripting-bundle diff --git a/pom.xml b/pom.xml index 8a08af48d3..ec2db7409f 100644 --- a/pom.xml +++ b/pom.xml @@ -533,6 +533,11 @@ language governing permissions and limitations under the License. --> + + org.springframework.security.kerberos + spring-security-kerberos-core + 1.0.1.RELEASE + org.aspectj aspectjweaver @@ -1066,6 +1071,12 @@ language governing permissions and limitations under the License. --> 0.6.0-SNAPSHOT nar + + org.apache.nifi + nifi-kerberos-iaa-providers-nar + 0.6.0-SNAPSHOT + nar + org.apache.nifi nifi-scripting-nar