Skip to content

Commit

Permalink
NIFI-7888 Added support for authenticating via SAML
Browse files Browse the repository at this point in the history
- Add dependency on spring-security-saml2-core
- Updated AccessResource with new SAML end-points
- Updated Login/Logout filters to handle SAML scenario
- Updated logout process to track a logout request using a cookie
- Added database storage for cached SAML credential and user groups
- Updated proxied requests when clustered to send IDP groups in a header
- Updated X509 filter to process the IDP groups from the header if present
- Updated admin guide
- Fixed logout action on error page

- Updated UserGroupProvider with a default method for getGroupByName
- Updated StandardManagedAuthorizer to combine groups from request with groups from lookup
- Updated UserGroupProvider implementations with more efficient impl of getGroupByName
- Added/updated unit tests

- Ensure signing algorithm is applied to all signatures and not just metadata signatures
- Added property to specify signature digest algorithm

- Added option to specify whether JDK truststore or NiFi's truststore should be used when connecting to IDP over https
- Added properties to configure connect and read timeouts for http client

- Added URL encoding of issuer when generating JWT to prevent potential issue with the frontend performing base64 decoding

- Made atomic replace methods for storing groups and saml credential in database

- Added properties to control AuthnRequestsSigned and WantAssertionsSigned in the generated service provider metadata

- Dynamically determine the private key alias from the keystore and remove the property for specifying the signing key alias

- Fixed unit test

- Added property to specify an optional identity attribute which would be used instead of NameID

- Cleaned up logging

- Fallback to keystore password when key password is blank

- Make signature and digest default to SHA-256 when no value provided in nifi.properties

This closes apache#4614
  • Loading branch information
bbende authored and mcgilman committed Nov 18, 2020
1 parent 7848ba5 commit dcc4fb0
Show file tree
Hide file tree
Showing 109 changed files with 7,031 additions and 340 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,8 +37,6 @@
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The NiFiProperties class holds all properties which are needed for various
Expand Down Expand Up @@ -175,6 +176,23 @@ public abstract class NiFiProperties {
public static final String SECURITY_USER_KNOX_COOKIE_NAME = "nifi.security.user.knox.cookieName";
public static final String SECURITY_USER_KNOX_AUDIENCES = "nifi.security.user.knox.audiences";

// saml
public static final String SECURITY_USER_SAML_IDP_METADATA_URL = "nifi.security.user.saml.idp.metadata.url";
public static final String SECURITY_USER_SAML_SP_ENTITY_ID = "nifi.security.user.saml.sp.entity.id";
public static final String SECURITY_USER_SAML_IDENTITY_ATTRIBUTE_NAME = "nifi.security.user.saml.identity.attribute.name";
public static final String SECURITY_USER_SAML_GROUP_ATTRIBUTE_NAME = "nifi.security.user.saml.group.attribute.name";
public static final String SECURITY_USER_SAML_METADATA_SIGNING_ENABLED = "nifi.security.user.saml.metadata.signing.enabled";
public static final String SECURITY_USER_SAML_REQUEST_SIGNING_ENABLED = "nifi.security.user.saml.request.signing.enabled";
public static final String SECURITY_USER_SAML_WANT_ASSERTIONS_SIGNED = "nifi.security.user.saml.want.assertions.signed";
public static final String SECURITY_USER_SAML_SIGNATURE_ALGORITHM = "nifi.security.user.saml.signature.algorithm";
public static final String SECURITY_USER_SAML_SIGNATURE_DIGEST_ALGORITHM = "nifi.security.user.saml.signature.digest.algorithm";
public static final String SECURITY_USER_SAML_MESSAGE_LOGGING_ENABLED = "nifi.security.user.saml.message.logging.enabled";
public static final String SECURITY_USER_SAML_AUTHENTICATION_EXPIRATION = "nifi.security.user.saml.authentication.expiration";
public static final String SECURITY_USER_SAML_SINGLE_LOGOUT_ENABLED = "nifi.security.user.saml.single.logout.enabled";
public static final String SECURITY_USER_SAML_HTTP_CLIENT_TRUSTSTORE_STRATEGY = "nifi.security.user.saml.http.client.truststore.strategy";
public static final String SECURITY_USER_SAML_HTTP_CLIENT_CONNECT_TIMEOUT = "nifi.security.user.saml.http.client.connect.timeout";
public static final String SECURITY_USER_SAML_HTTP_CLIENT_READ_TIMEOUT = "nifi.security.user.saml.http.client.read.timeout";

// web properties
public static final String WEB_HTTP_PORT = "nifi.web.http.port";
public static final String WEB_HTTP_PORT_FORWARDING = "nifi.web.http.port.forwarding";
Expand Down Expand Up @@ -301,6 +319,17 @@ public abstract class NiFiProperties {
public static final String DEFAULT_FLOW_CONFIGURATION_ARCHIVE_MAX_STORAGE = "500 MB";
public static final String DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT = "5 secs";
public static final String DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT = "5 secs";
public static final String DEFAULT_SECURITY_USER_SAML_METADATA_SIGNING_ENABLED = "false";
public static final String DEFAULT_SECURITY_USER_SAML_REQUEST_SIGNING_ENABLED = "false";
public static final String DEFAULT_SECURITY_USER_SAML_WANT_ASSERTIONS_SIGNED = "true";
public static final String DEFAULT_SECURITY_USER_SAML_SIGNATURE_ALGORITHM = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
public static final String DEFAULT_SECURITY_USER_SAML_DIGEST_ALGORITHM = "http://www.w3.org/2001/04/xmlenc#sha256";
public static final String DEFAULT_SECURITY_USER_SAML_MESSAGE_LOGGING_ENABLED = "false";
public static final String DEFAULT_SECURITY_USER_SAML_AUTHENTICATION_EXPIRATION = "12 hours";
public static final String DEFAULT_SECURITY_USER_SAML_SINGLE_LOGOUT_ENABLED = "false";
public static final String DEFAULT_SECURITY_USER_SAML_HTTP_CLIENT_TRUSTSTORE_STRATEGY = "JDK";
public static final String DEFAULT_SECURITY_USER_SAML_HTTP_CLIENT_CONNECT_TIMEOUT = "30 secs";
public static final String DEFAULT_SECURITY_USER_SAML_HTTP_CLIENT_READ_TIMEOUT = "30 secs";
public static final String DEFAULT_WEB_SHOULD_SEND_SERVER_VERSION = "true";

// cluster common defaults
Expand Down Expand Up @@ -1037,6 +1066,153 @@ public String getKnoxCookieName() {
return getProperty(SECURITY_USER_KNOX_COOKIE_NAME);
}

/**
* Returns whether SAML is enabled.
*
* @return whether saml is enabled
*/
public boolean isSamlEnabled() {
return !StringUtils.isBlank(getSamlIdentityProviderMetadataUrl());
}

/**
* The URL to obtain the identity provider metadata.
* Must be a value starting with 'file://' or 'http://'.
*
* @return the url to obtain the identity provider metadata
*/
public String getSamlIdentityProviderMetadataUrl() {
return getProperty(SECURITY_USER_SAML_IDP_METADATA_URL);
}

/**
* The entity id for the service provider.
*
* @return the service provider entity id
*/
public String getSamlServiceProviderEntityId() {
return getProperty(SECURITY_USER_SAML_SP_ENTITY_ID);
}

/**
* The name of an attribute in the SAML assertions that contains the user identity.
*
* If not specified, or missing, the NameID of the Subject will be used.
*
* @return the attribute name containing the user identity
*/
public String getSamlIdentityAttributeName() {
return getProperty(SECURITY_USER_SAML_IDENTITY_ATTRIBUTE_NAME);
}

/**
* The name of the attribute in the SAML assertions that contains the groups the user belongs to.
*
* @return the attribute name containing user groups
*/
public String getSamlGroupAttributeName() {
return getProperty(SECURITY_USER_SAML_GROUP_ATTRIBUTE_NAME);
}

/**
* The signing algorithm to use for signing SAML requests.
*
* @return the signing algorithm to use
*/
public String getSamlSignatureAlgorithm() {
return getProperty(SECURITY_USER_SAML_SIGNATURE_ALGORITHM, DEFAULT_SECURITY_USER_SAML_SIGNATURE_ALGORITHM);
}

/**
* The digest algorithm to use for signing SAML requests.
*
* @return the digest algorithm
*/
public String getSamlSignatureDigestAlgorithm() {
return getProperty(SECURITY_USER_SAML_SIGNATURE_DIGEST_ALGORITHM, DEFAULT_SECURITY_USER_SAML_DIGEST_ALGORITHM);
}

/**
* Whether or not to sign the service provider metadata.
*
* @return whether or not to sign the service provider metadata
*/
public boolean isSamlMetadataSigningEnabled() {
return Boolean.parseBoolean(getProperty(SECURITY_USER_SAML_METADATA_SIGNING_ENABLED, DEFAULT_SECURITY_USER_SAML_METADATA_SIGNING_ENABLED));
}

/**
* Whether or not to sign requests sent to the identity provider.
*
* @return whether or not to sign requests sent to the identity provider
*/
public boolean isSamlRequestSigningEnabled() {
return Boolean.parseBoolean(getProperty(SECURITY_USER_SAML_REQUEST_SIGNING_ENABLED, DEFAULT_SECURITY_USER_SAML_REQUEST_SIGNING_ENABLED));
}

/**
* Whether or not the identity provider should sign assertions when sending response back.
*
* @return whether or not the identity provider should sign assertions when sending response back
*/
public boolean isSamlWantAssertionsSigned() {
return Boolean.parseBoolean(getProperty(SECURITY_USER_SAML_WANT_ASSERTIONS_SIGNED, DEFAULT_SECURITY_USER_SAML_WANT_ASSERTIONS_SIGNED));
}

/**
* Whether or not to log messages for debug purposes.
*
* @return whether or not to log messages
*/
public boolean isSamlMessageLoggingEnabled() {
return Boolean.parseBoolean(getProperty(SECURITY_USER_SAML_MESSAGE_LOGGING_ENABLED, DEFAULT_SECURITY_USER_SAML_MESSAGE_LOGGING_ENABLED));
}

/**
* The expiration value for a JWT created from a SAML authentication.
*
* @return the expiration value for a SAML authentication
*/
public String getSamlAuthenticationExpiration() {
return getProperty(SECURITY_USER_SAML_AUTHENTICATION_EXPIRATION, DEFAULT_SECURITY_USER_SAML_AUTHENTICATION_EXPIRATION);
}

/**
* Whether or not logging out of NiFi should logout of the SAML IDP using the SAML SingleLogoutService.
*
* @return whether or not SAML single logout is enabled
*/
public boolean isSamlSingleLogoutEnabled() {
return Boolean.parseBoolean(getProperty(SECURITY_USER_SAML_SINGLE_LOGOUT_ENABLED, DEFAULT_SECURITY_USER_SAML_SINGLE_LOGOUT_ENABLED));
}

/**
* The truststore to use when interacting with a SAML IDP over https. Valid values are "JDK" and "NIFI".
*
* @return the type of truststore to use
*/
public String getSamlHttpClientTruststoreStrategy() {
return getProperty(SECURITY_USER_SAML_HTTP_CLIENT_TRUSTSTORE_STRATEGY, DEFAULT_SECURITY_USER_SAML_HTTP_CLIENT_TRUSTSTORE_STRATEGY);
}

/**
* The connect timeout for the http client created for SAML operations.
*
* @return the connect timeout
*/
public String getSamlHttpClientConnectTimeout() {
return getProperty(SECURITY_USER_SAML_HTTP_CLIENT_CONNECT_TIMEOUT, DEFAULT_SECURITY_USER_SAML_HTTP_CLIENT_CONNECT_TIMEOUT);
}

/**
* The read timeout for the http client created for SAML operations.
*
* @return the read timeout
*/
public String getSamlHttpClientReadTimeout() {
return getProperty(SECURITY_USER_SAML_HTTP_CLIENT_READ_TIMEOUT, DEFAULT_SECURITY_USER_SAML_HTTP_CLIENT_READ_TIMEOUT);
}

/**
* Returns true if client certificates are required for REST API. Determined
* if the following conditions are all true:
Expand All @@ -1051,7 +1227,12 @@ public String getKnoxCookieName() {
* @return true if client certificates are required for access to the REST API
*/
public boolean isClientAuthRequiredForRestApi() {
return !isLoginIdentityProviderEnabled() && !isKerberosSpnegoSupportEnabled() && !isOidcEnabled() && !isKnoxSsoEnabled() && !isAnonymousAuthenticationAllowed();
return !isLoginIdentityProviderEnabled()
&& !isKerberosSpnegoSupportEnabled()
&& !isOidcEnabled()
&& !isKnoxSsoEnabled()
&& !isSamlEnabled()
&& !isAnonymousAuthenticationAllowed();
}

public InetSocketAddress getNodeApiAddress() {
Expand Down
33 changes: 29 additions & 4 deletions nifi-docs/src/main/asciidoc/administration-guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Apache NiFi can run on something as simple as a laptop, but it can also be clust
** Linux
** Unix
** Windows
** macOS
** macOS
* Supported Web Browsers:
** Microsoft Edge: Current & (Current - 1)
** Mozilla FireFox: Current & (Current - 1)
Expand Down Expand Up @@ -343,7 +343,7 @@ Modify _login-identity-providers.xml_ to enable the `kerberos-provider`. Here is

The `kerberos-provider` has the following properties:

[options="header,footer"]
[options="header"]
|==================================================================================================================================================
| Property Name | Description
|`Default Realm` | Default realm to provide when user enters incomplete user principal (i.e. `NIFI.APACHE.ORG`).
Expand All @@ -359,7 +359,7 @@ NOTE: For changes to _nifi.properties_ and _login-identity-providers.xml_ to tak

To enable authentication via OpenId Connect the following properties must be configured in _nifi.properties_.

[options="header,footer"]
[options="header"]
|==================================================================================================================================================
| Property Name | Description
|`nifi.security.user.oidc.discovery.url` | The discovery URL for the desired OpenId Connect Provider (link:http://openid.net/specs/openid-connect-discovery-1_0.html[http://openid.net/specs/openid-connect-discovery-1_0.html^]).
Expand All @@ -375,12 +375,37 @@ JSON Web Key (JWK) provided through the jwks_uri in the metadata found at the di
|`nifi.security.user.oidc.claim.identifying.user` | Claim that identifies the user to be logged in; default is `email`. May need to be requested via the `nifi.security.user.oidc.additional.scopes` before usage.
|==================================================================================================================================================

[[saml]]
=== SAML

To enable authentication via SAML the following properties must be configured in _nifi.properties_.

[options="header"]
|==================================================================================================================================================
| Property Name | Description
|`nifi.security.user.saml.idp.metadata.url` | The URL for obtaining the identity provider's metadata. The metadata can be retrieved from the identity provider via `http://` or `https://`, or a local file can be referenced using `file://` .
|`nifi.security.user.saml.sp.entity.id`| The entity id of the service provider (i.e. NiFi). This value will be used as the `Issuer` for SAML authentication requests and should be a valid URI. In some cases the service provider entity id must be registered ahead of time with the identity provider.
|`nifi.security.user.saml.identity.attribute.name`| The name of a SAML assertion attribute containing the user'sidentity. This property is optional and if not specified, or if the attribute is not found, then the NameID of the Subject will be used.
|`nifi.security.user.saml.group.attribute.name`| The name of a SAML assertion attribute containing group names the user belongs to. This property is optional, but if populated the groups will be passed along to the authorization process.
|`nifi.security.user.saml.metadata.signing.enabled`| Enables signing of the generated service provider metadata.
|`nifi.security.user.saml.request.signing.enabled`| Controls the value of `AuthnRequestsSigned` in the generated service provider metadata from `nifi-api/access/saml/metadata`. This indicates that the service provider (i.e. NiFi) should not sign authentication requests sent to the identity provider, but the requests may still need to be signed if the identity provider indicates `WantAuthnRequestSigned=true`.
|`nifi.security.user.saml.want.assertions.signed`| Controls the value of `WantAssertionsSigned` in the generated service provider metadata from `nifi-api/access/saml/metadata`. This indictaes that the identity provider should sign assertions, but some identity providers may provide their own configuration for controlling whether assertions are signed.
|`nifi.security.user.saml.signature.algorithm`| The algorithm to use when signing SAML messages. Reference the link:https://git.shibboleth.net/view/?p=java-xmltooling.git;a=blob;f=src/main/java/org/opensaml/xml/signature/SignatureConstants.java[Open SAML Signature Constants] for a list of valid values. If not specified, a default of SHA-256 will be used.
|`nifi.security.user.saml.signature.digest.algorithm`| The digest algorithm to use when signing SAML messages. Reference the link:https://git.shibboleth.net/view/?p=java-xmltooling.git;a=blob;f=src/main/java/org/opensaml/xml/signature/SignatureConstants.java[Open SAML Signature Constants] for a list of valid values. If not specified, a default of SHA-256 will be used.
|`nifi.security.user.saml.message.logging.enabled`| Enables logging of SAML messages for debugging purposes.
|`nifi.security.user.saml.authentication.expiration`| The expiration of the NiFi JWT that will be produced from a successful SAML authentication response.
|`nifi.security.user.saml.single.logout.enabled`| Enables SAML SingleLogout which causes a logout from NiFi to logout of the identity provider. By default, a logout of NiFi will only remove the NiFi JWT.
|`nifi.security.user.saml.http.client.truststore.strategy`| The truststore strategy when the IDP metadata URL begins with https. A value of `JDK` indicates to use the JDK's default truststore. A value of`NIFI`indicates to use the truststore specified by `nifi.security.truststore`.
|`nifi.security.user.saml.http.client.connect.timeout`| The connection timeout when communicating with the SAML IDP.
|`nifi.security.user.saml.http.client.read.timeout`| The read timeout when communicating with the SAML IDP.
|==================================================================================================================================================

[[apache_knox]]
=== Apache Knox

To enable authentication via Apache Knox the following properties must be configured in _nifi.properties_.

[options="header,footer"]
[options="header"]
|==================================================================================================================================================
| Property Name | Description
|`nifi.security.user.knox.url` | The URL for the Apache Knox login page.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,6 @@
*/
package org.apache.nifi.authorization;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import org.apache.nifi.authorization.exception.AuthorizationAccessException;
import org.apache.nifi.authorization.exception.AuthorizerCreationException;
import org.apache.nifi.authorization.exception.AuthorizerDestructionException;
Expand All @@ -43,6 +29,21 @@
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;

/**
* An Authorizer that provides management of users, groups, and policies.
*/
Expand Down Expand Up @@ -150,6 +151,15 @@ public final synchronized Group addGroup(Group group) throws AuthorizationAccess
*/
public abstract Group getGroup(String identifier) throws AuthorizationAccessException;

/**
* Retrieves a group by name.
*
* @param name the name of the group to retrieve
* @return the group with the given name, or null if no matching group was found
* @throws AuthorizationAccessException if there was an unexpected error performing the operation
*/
public abstract Group getGroupByName(String name) throws AuthorizationAccessException;

protected abstract void purgePoliciesUsersAndGroups();

protected abstract void backupPoliciesUsersAndGroups();
Expand Down Expand Up @@ -610,6 +620,11 @@ public Group getGroup(String identifier) throws AuthorizationAccessException {
return AbstractPolicyBasedAuthorizer.this.getGroup(identifier);
}

@Override
public Group getGroupByName(String name) throws AuthorizationAccessException {
return AbstractPolicyBasedAuthorizer.this.getGroupByName(name);
}

@Override
public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException {
final UsersAndAccessPolicies usersAndAccessPolicies = AbstractPolicyBasedAuthorizer.this.getUsersAndAccessPolicies();
Expand Down
Loading

0 comments on commit dcc4fb0

Please sign in to comment.