diff --git a/nifi-commons/nifi-security-utils/pom.xml b/nifi-commons/nifi-security-utils/pom.xml index 1284ea3d34..3bf5b0bf98 100644 --- a/nifi-commons/nifi-security-utils/pom.xml +++ b/nifi-commons/nifi-security-utils/pom.xml @@ -79,6 +79,11 @@ 3.1.0 test + + org.spockframework + spock-core + test + diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/krb/KerberosPrincipalParser.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/krb/KerberosPrincipalParser.java new file mode 100644 index 0000000000..8726c9c30e --- /dev/null +++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/krb/KerberosPrincipalParser.java @@ -0,0 +1,60 @@ +/* + * 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.security.util.krb; + +import org.apache.nifi.util.StringUtils; + +public class KerberosPrincipalParser { + + /** + *

Determines the realm specified in the given kerberos principal. + * + *

The content of the given {@code principal} after the occurrence + * of the last non-escaped realm delimiter ("@") will be considered + * the realm of the principal. + * + *

The validity of the given {@code principal} and the determined realm + * is not be verified by this method. + * + * @param principal the principal for which the realm will be determined + * @return the realm of the given principal + */ + public static String getRealm(String principal) { + if (StringUtils.isBlank(principal)) { + throw new IllegalArgumentException("principal can not be null or empty"); + } + + char previousChar = 0; + int realmDelimiterIndex = -1; + char currentChar; + boolean realmDelimiterFound = false; + int principalLength = principal.length(); + + // find the last non-escaped occurrence of the realm delimiter + for (int i = 0; i < principalLength; ++i) { + currentChar = principal.charAt(i); + if (currentChar == '@' && previousChar != '\\' ) { + realmDelimiterIndex = i; + realmDelimiterFound = true; + } + previousChar = currentChar; + } + + String principalAfterLastRealmDelimiter = principal.substring(realmDelimiterIndex + 1); + return realmDelimiterFound && realmDelimiterIndex + 1 < principalLength ? principalAfterLastRealmDelimiter : null; + } +} diff --git a/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/krb/KerberosPrincipalParserSpec.groovy b/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/krb/KerberosPrincipalParserSpec.groovy new file mode 100644 index 0000000000..883dc31631 --- /dev/null +++ b/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/krb/KerberosPrincipalParserSpec.groovy @@ -0,0 +1,44 @@ +/* + * 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.security.util.krb + +import spock.lang.Specification +import spock.lang.Unroll + +class KerberosPrincipalParserSpec extends Specification { + + @Unroll + def "Verify parsed realm from '#testPrincipal' == '#expectedRealm'"() { + expect: + KerberosPrincipalParser.getRealm(testPrincipal) == expectedRealm + + where: + testPrincipal || expectedRealm + "user" || null + "user@" || null + "user@EXAMPLE.COM" || "EXAMPLE.COM" + "user@name@EXAMPLE.COM" || "EXAMPLE.COM" + "user\\@" || null + "user\\@name" || null + "user\\@name@EXAMPLE.COM" || "EXAMPLE.COM" + "user@EXAMPLE.COM\\@" || "EXAMPLE.COM\\@" + "user@@name@\\@@\\@" || "\\@" + "user@@name@\\@@\\@@EXAMPLE.COM" || "EXAMPLE.COM" + "user@@name@\\@@\\@@EXAMPLE.COM@" || null + "user\\@\\@name@EXAMPLE.COM" || "EXAMPLE.COM" + } +} 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 index f9856020af..2e46f6a4e5 100644 --- 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 @@ -26,6 +26,7 @@ import org.apache.nifi.authentication.exception.IdentityAccessException; import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException; import org.apache.nifi.authentication.exception.ProviderCreationException; import org.apache.nifi.authentication.exception.ProviderDestructionException; +import org.apache.nifi.security.util.krb.KerberosPrincipalParser; import org.apache.nifi.util.FormatUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,6 +47,7 @@ public class KerberosProvider implements LoginIdentityProvider { private KerberosAuthenticationProvider provider; private String issuer; + private String defaultRealm; private long expiration; @Override @@ -61,11 +63,16 @@ public class KerberosProvider implements LoginIdentityProvider { } try { - expiration = FormatUtils.getTimeDuration(rawExpiration, TimeUnit.MILLISECONDS); + expiration = Double.valueOf(FormatUtils.getPreciseTimeDuration(rawExpiration, TimeUnit.MILLISECONDS)).longValue(); } catch (final IllegalArgumentException iae) { throw new ProviderCreationException(String.format("The Expiration Duration '%s' is not a valid time duration", rawExpiration)); } + defaultRealm = configurationContext.getProperty("Default Realm"); + if (StringUtils.isNotBlank(defaultRealm) && defaultRealm.contains("@")) { + throw new ProviderCreationException(String.format("The Default Realm '%s' must not contain \"@\"", defaultRealm)); + } + provider = new KerberosAuthenticationProvider(); SunJaasKerberosClient client = new SunJaasKerberosClient(); client.setDebug(true); @@ -80,15 +87,40 @@ public class KerberosProvider implements LoginIdentityProvider { } try { + final String rawPrincipal = credentials.getUsername(); + final String parsedRealm = KerberosPrincipalParser.getRealm(rawPrincipal); + + // Apply default realm from KerberosIdentityProvider's configuration specified in login-identity-providers.xml if a principal without a realm was given + // Otherwise, the default realm configured from the krb5 configuration specified in the nifi.kerberos.krb5.file property will end up being used + boolean realmInRawPrincipal = StringUtils.isNotBlank(parsedRealm); + final String identity; + if (realmInRawPrincipal) { + // there's a realm already in the given principal, use it + identity = rawPrincipal; + logger.debug("Realm was specified in principal {}, default realm was not added to the identity being authenticated", rawPrincipal); + } else if (StringUtils.isNotBlank(defaultRealm)) { + // the value for the default realm is not blank, append the realm to the given principal + identity = StringUtils.joinWith("@", rawPrincipal, defaultRealm); + logger.debug("Realm was not specified in principal {}, default realm {} was added to the identity being authenticated", rawPrincipal, defaultRealm); + } else { + // otherwise, use the given principal, which will use the default realm as specified in the krb5 configuration + identity = rawPrincipal; + logger.debug("Realm was not specified in principal {}, default realm is blank and was not added to the identity being authenticated", rawPrincipal); + } + // 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 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(identity, credentials.getPassword()); + if (logger.isDebugEnabled()) { + 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()); + if (logger.isDebugEnabled()) { + 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); + return new AuthenticationResponse(authentication.getName(), identity, expiration, issuer); } catch (final AuthenticationException e) { throw new InvalidLoginCredentialsException(e.getMessage(), e); }