Repository: nifi-registry Updated Branches: refs/heads/master a87d42ee9 -> 785cb81ff
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/ProxiedEntitiesUtils.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/ProxiedEntitiesUtils.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/ProxiedEntitiesUtils.java new file mode 100644 index 0000000..33015fc --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/ProxiedEntitiesUtils.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.authorization.user.NiFiUser; +import org.apache.nifi.registry.authorization.user.NiFiUserUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * + */ +public class ProxiedEntitiesUtils { + private static final Logger logger = LoggerFactory.getLogger(ProxiedEntitiesUtils.class); + + public static final String PROXY_ENTITIES_CHAIN = "X-ProxiedEntitiesChain"; + public static final String PROXY_ENTITIES_ACCEPTED = "X-ProxiedEntitiesAccepted"; + public static final String PROXY_ENTITIES_DETAILS = "X-ProxiedEntitiesDetails"; + + private static final String GT = ">"; + private static final String ESCAPED_GT = "\\\\>"; + private static final String LT = "<"; + private static final String ESCAPED_LT = "\\\\<"; + + private static final String ANONYMOUS_CHAIN = "<>"; + + /** + * Formats the specified DN to be set as a HTTP header using well known conventions. + * + * @param dn raw dn + * @return the dn formatted as an HTTP header + */ + public static String formatProxyDn(String dn) { + return LT + sanitizeDn(dn) + GT; + } + + /** + * If a user provides a DN with the sequence '><', they could escape the tokenization process and impersonate another user. + * <p> + * Example: + * <p> + * Provided DN: {@code jdoe><alopresto} -> {@code <jdoe><alopresto><proxy...>} would allow the user to impersonate jdoe + * + * @param rawDn the unsanitized DN + * @return the sanitized DN + */ + private static String sanitizeDn(String rawDn) { + if (StringUtils.isEmpty(rawDn)) { + return rawDn; + } else { + String sanitizedDn = rawDn.replaceAll(GT, ESCAPED_GT).replaceAll(LT, ESCAPED_LT); + if (!sanitizedDn.equals(rawDn)) { + logger.warn("The provided DN [" + rawDn + "] contained dangerous characters that were escaped to [" + sanitizedDn + "]"); + } + return sanitizedDn; + } + } + + /** + * Reconstitutes the original DN from the sanitized version passed in the proxy chain. + * <p> + * Example: + * <p> + * {@code alopresto\>\<proxy1} -> {@code alopresto><proxy1} + * + * @param sanitizedDn the sanitized DN + * @return the original DN + */ + private static String unsanitizeDn(String sanitizedDn) { + if (StringUtils.isEmpty(sanitizedDn)) { + return sanitizedDn; + } else { + String unsanitizedDn = sanitizedDn.replaceAll(ESCAPED_GT, GT).replaceAll(ESCAPED_LT, LT); + if (!unsanitizedDn.equals(sanitizedDn)) { + logger.warn("The provided DN [" + sanitizedDn + "] had been escaped, and was reconstituted to the dangerous DN [" + unsanitizedDn + "]"); + } + return unsanitizedDn; + } + } + + /** + * Tokenizes the specified proxy chain. + * + * @param rawProxyChain raw chain + * @return tokenized proxy chain + */ + public static List<String> tokenizeProxiedEntitiesChain(String rawProxyChain) { + final List<String> proxyChain = new ArrayList<>(); + if (!StringUtils.isEmpty(rawProxyChain)) { + // Split the String on the >< token + List<String> elements = Arrays.asList(StringUtils.splitByWholeSeparatorPreserveAllTokens(rawProxyChain, "><")); + + // Unsanitize each DN and collect back + elements = elements.stream().map(ProxiedEntitiesUtils::unsanitizeDn).collect(Collectors.toList()); + + // Remove the leading < from the first element + elements.set(0, elements.get(0).replaceFirst(LT, "")); + + // Remove the trailing > from the last element + int last = elements.size() - 1; + String lastElement = elements.get(last); + if (lastElement.endsWith(GT)) { + elements.set(last, lastElement.substring(0, lastElement.length() - 1)); + } + + proxyChain.addAll(elements); + } + + return proxyChain; + } + + /** + * Builds the proxy chain for the specified user. + * + * @param user The current user + * @return The proxy chain for that user in String form + */ + public static String buildProxiedEntitiesChainString(final NiFiUser user) { + // calculate the dn chain + List<String> proxyChain = NiFiUserUtils.buildProxiedEntitiesChain(user); + if (proxyChain.isEmpty()) { + return ANONYMOUS_CHAIN; + } + proxyChain = proxyChain.stream().map(ProxiedEntitiesUtils::formatProxyDn).collect(Collectors.toList()); + return StringUtils.join(proxyChain, ""); + } + + public static void successfulAuthorization(HttpServletRequest request, HttpServletResponse response, Authentication authResult) { + if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) { + response.setHeader(PROXY_ENTITIES_ACCEPTED, Boolean.TRUE.toString()); + } + } + + public static void unsuccessfulAuthorization(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) { + if (StringUtils.isNotBlank(request.getHeader(PROXY_ENTITIES_CHAIN))) { + response.setHeader(PROXY_ENTITIES_DETAILS, failed.getMessage()); + } + } +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/UntrustedProxyException.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/UntrustedProxyException.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/UntrustedProxyException.java new file mode 100644 index 0000000..fad8cc1 --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/UntrustedProxyException.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security; + +import org.springframework.security.core.AuthenticationException; + +/** + * + */ +public class UntrustedProxyException extends AuthenticationException { + + public UntrustedProxyException(String msg) { + super(msg); + } + + public UntrustedProxyException(String msg, Throwable t) { + super(msg, t); + } + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/LoginAuthenticationToken.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/LoginAuthenticationToken.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/LoginAuthenticationToken.java new file mode 100644 index 0000000..cbb3b1f --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/LoginAuthenticationToken.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.token; + +import org.apache.nifi.registry.web.security.util.CertificateUtils; +import org.springframework.security.authentication.AbstractAuthenticationToken; + +import java.text.SimpleDateFormat; +import java.util.Calendar; + +/** + * This is an Authentication Token for logging in. Once a user is authenticated, they can be issued an ID token. + */ +public class LoginAuthenticationToken extends AbstractAuthenticationToken { + + private final String identity; + private final String username; + private final long expiration; + private final String issuer; + + /** + * Creates a representation of the authentication token for a user. + * + * @param identity The unique identifier for this user + * @param expiration The relative time to expiration in milliseconds + * @param issuer The IdentityProvider implementation that generated this token + */ + public LoginAuthenticationToken(final String identity, final long expiration, final String issuer) { + this(identity, null, expiration, issuer); + } + + /** + * Creates a representation of the authentication token for a user. + * + * @param identity The unique identifier for this user (cannot be null or empty) + * @param username The preferred username for this user + * @param expiration The relative time to expiration in milliseconds + * @param issuer The IdentityProvider implementation that generated this token + */ + public LoginAuthenticationToken(final String identity, final String username, final long expiration, final String issuer) { + super(null); + setAuthenticated(true); + this.identity = identity; + this.username = username; + this.issuer = issuer; + Calendar now = Calendar.getInstance(); + this.expiration = now.getTimeInMillis() + expiration; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return identity; + } + + /** + * Returns the expiration instant in milliseconds. This value is an absolute point in time (i.e. Nov + * 16, 2015 11:30:00.000 GMT), not a relative time (i.e. 60 minutes). It is calculated by adding the + * relative expiration from the constructor to the timestamp at object creation. + * + * @return the expiration in millis + */ + public long getExpiration() { + return expiration; + } + + public String getIssuer() { + return issuer; + } + + @Override + public String getName() { + if (username == null) { + // if the username is a DN this will extract the username or CN... if not will return what was passed + return CertificateUtils.extractUsername(identity); + } else { + return username; + } + } + + @Override + public String toString() { + Calendar expirationTime = Calendar.getInstance(); + expirationTime.setTimeInMillis(getExpiration()); + long remainingTime = expirationTime.getTimeInMillis() - Calendar.getInstance().getTimeInMillis(); + + SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss.SSS"); + dateFormat.setTimeZone(expirationTime.getTimeZone()); + String expirationTimeString = dateFormat.format(expirationTime.getTime()); + + return new StringBuilder("LoginAuthenticationToken for ") + .append(getName()) + .append(" issued by ") + .append(getIssuer()) + .append(" expiring at ") + .append(expirationTimeString) + .append(" [") + .append(getExpiration()) + .append(" ms, ") + .append(remainingTime) + .append(" ms remaining]") + .toString(); + } + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/NiFiAuthenticationToken.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/NiFiAuthenticationToken.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/NiFiAuthenticationToken.java new file mode 100644 index 0000000..5ea9e5b --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/NiFiAuthenticationToken.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.token; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.userdetails.UserDetails; + +/** + * An authentication token that represents an Authenticated and Authorized user of the NiFi Apis. The authorities are based off the specified UserDetails. + */ +public class NiFiAuthenticationToken extends AbstractAuthenticationToken { + + final UserDetails nifiUserDetails; + + public NiFiAuthenticationToken(final UserDetails nifiUserDetails) { + super(nifiUserDetails.getAuthorities()); + super.setAuthenticated(true); + setDetails(nifiUserDetails); + this.nifiUserDetails = nifiUserDetails; + } + + @Override + public Object getCredentials() { + return nifiUserDetails.getPassword(); + } + + @Override + public Object getPrincipal() { + return nifiUserDetails; + } + + @Override + public final void setAuthenticated(boolean authenticated) { + throw new IllegalArgumentException("Cannot change the authenticated state."); + } + + @Override + public String toString() { + return nifiUserDetails.getUsername(); + } +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/OtpAuthenticationToken.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/OtpAuthenticationToken.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/OtpAuthenticationToken.java new file mode 100644 index 0000000..f49e97d --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/token/OtpAuthenticationToken.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.token; + +import org.springframework.security.authentication.AbstractAuthenticationToken; + +/** + * This is an Authentication Token for logging in. Once a user is authenticated, they can be issued an ID token. + */ +public class OtpAuthenticationToken extends AbstractAuthenticationToken { + + private final String identity; + + /** + * Creates a representation of the otp authentication token for a user. + * + * @param identity The unique identifier for this user + */ + public OtpAuthenticationToken(final String identity) { + super(null); + setAuthenticated(true); + this.identity = identity; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return identity; + } + + @Override + public String toString() { + return new StringBuilder("OtpAuthenticationToken for ") + .append(getName()) + .toString(); + } + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/CertificateUtils.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/CertificateUtils.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/CertificateUtils.java new file mode 100644 index 0000000..3282c42 --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/CertificateUtils.java @@ -0,0 +1,668 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.util; + +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Set; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.pkcs.Attribute; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x500.AttributeTypeAndValue; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSocket; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.net.Socket; +import java.net.URL; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public final class CertificateUtils { + private static final Logger logger = LoggerFactory.getLogger(CertificateUtils.class); + private static final String PEER_NOT_AUTHENTICATED_MSG = "peer not authenticated"; + private static final Map<ASN1ObjectIdentifier, Integer> dnOrderMap = createDnOrderMap(); + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + /** + * The time in milliseconds that the last unique serial number was generated + */ + private static long lastSerialNumberMillis = 0L; + + /** + * An incrementor to add uniqueness to serial numbers generated in the same millisecond + */ + private static int serialNumberIncrementor = 0; + + /** + * BigInteger value to use for the base of the unique serial number + */ + private static BigInteger millisecondBigInteger; + + private static Map<ASN1ObjectIdentifier, Integer> createDnOrderMap() { + Map<ASN1ObjectIdentifier, Integer> orderMap = new HashMap<>(); + int count = 0; + orderMap.put(BCStyle.CN, count++); + orderMap.put(BCStyle.L, count++); + orderMap.put(BCStyle.ST, count++); + orderMap.put(BCStyle.O, count++); + orderMap.put(BCStyle.OU, count++); + orderMap.put(BCStyle.C, count++); + orderMap.put(BCStyle.STREET, count++); + orderMap.put(BCStyle.DC, count++); + orderMap.put(BCStyle.UID, count++); + return Collections.unmodifiableMap(orderMap); + } + + public enum ClientAuth { + NONE(0, "none"), + WANT(1, "want"), + NEED(2, "need"); + + private int value; + private String description; + + ClientAuth(int value, String description) { + this.value = value; + this.description = description; + } + + @Override + public String toString() { + return "Client Auth: " + this.description + " (" + this.value + ")"; + } + } + + /** + * Returns true if the given keystore can be loaded using the given keystore type and password. Returns false otherwise. + * + * @param keystore the keystore to validate + * @param keystoreType the type of the keystore + * @param password the password to access the keystore + * @return true if valid; false otherwise + */ + public static boolean isStoreValid(final URL keystore, final KeystoreType keystoreType, final char[] password) { + + if (keystore == null) { + throw new IllegalArgumentException("keystore may not be null"); + } else if (keystoreType == null) { + throw new IllegalArgumentException("keystore type may not be null"); + } else if (password == null) { + throw new IllegalArgumentException("password may not be null"); + } + + BufferedInputStream bis = null; + final KeyStore ks; + try { + + // load the keystore + bis = new BufferedInputStream(keystore.openStream()); + ks = KeyStoreUtils.getKeyStore(keystoreType.name()); + ks.load(bis, password); + + return true; + + } catch (Exception e) { + return false; + } finally { + if (bis != null) { + try { + bis.close(); + } catch (final IOException ioe) { + logger.warn("Failed to close input stream", ioe); + } + } + } + } + + /** + * Extracts the username from the specified DN. If the username cannot be extracted because the CN is in an unrecognized format, the entire CN is returned. If the CN cannot be extracted because + * the DN is in an unrecognized format, the entire DN is returned. + * + * @param dn the dn to extract the username from + * @return the exatracted username + */ + public static String extractUsername(String dn) { + String username = dn; + + // ensure the dn is specified + if (StringUtils.isNotBlank(dn)) { + // determine the separate + final String separator = StringUtils.indexOfIgnoreCase(dn, "/cn=") > 0 ? "/" : ","; + + // attempt to locate the cd + final String cnPattern = "cn="; + final int cnIndex = StringUtils.indexOfIgnoreCase(dn, cnPattern); + if (cnIndex >= 0) { + int separatorIndex = StringUtils.indexOf(dn, separator, cnIndex); + if (separatorIndex > 0) { + username = StringUtils.substring(dn, cnIndex + cnPattern.length(), separatorIndex); + } else { + username = StringUtils.substring(dn, cnIndex + cnPattern.length()); + } + } + } + + return username; + } + + /** + * Returns a list of subject alternative names. Any name that is represented as a String by X509Certificate.getSubjectAlternativeNames() is converted to lowercase and returned. + * + * @param certificate a certificate + * @return a list of subject alternative names; list is never null + * @throws CertificateParsingException if parsing the certificate failed + */ + public static List<String> getSubjectAlternativeNames(final X509Certificate certificate) throws CertificateParsingException { + + final Collection<List<?>> altNames = certificate.getSubjectAlternativeNames(); + if (altNames == null) { + return new ArrayList<>(); + } + + final List<String> result = new ArrayList<>(); + for (final List<?> generalName : altNames) { + /** + * generalName has the name type as the first element a String or byte array for the second element. We return any general names that are String types. + * + * We don't inspect the numeric name type because some certificates incorrectly put IPs and DNS names under the wrong name types. + */ + final Object value = generalName.get(1); + if (value instanceof String) { + result.add(((String) value).toLowerCase()); + } + + } + + return result; + } + + /** + * Returns the DN extracted from the peer certificate (the server DN if run on the client; the client DN (if available) if run on the server). + * + * If the client auth setting is WANT or NONE and a client certificate is not present, this method will return {@code null}. + * If the client auth is NEED, it will throw a {@link CertificateException}. + * + * @param socket the SSL Socket + * @return the extracted DN + * @throws CertificateException if there is a problem parsing the certificate + */ + public static String extractPeerDNFromSSLSocket(Socket socket) throws CertificateException { + String dn = null; + if (socket instanceof SSLSocket) { + final SSLSocket sslSocket = (SSLSocket) socket; + + boolean clientMode = sslSocket.getUseClientMode(); + logger.debug("SSL Socket in {} mode", clientMode ? "client" : "server"); + ClientAuth clientAuth = getClientAuthStatus(sslSocket); + logger.debug("SSL Socket client auth status: {}", clientAuth); + + if (clientMode) { + logger.debug("This socket is in client mode, so attempting to extract certificate from remote 'server' socket"); + dn = extractPeerDNFromServerSSLSocket(sslSocket); + } else { + logger.debug("This socket is in server mode, so attempting to extract certificate from remote 'client' socket"); + dn = extractPeerDNFromClientSSLSocket(sslSocket); + } + } + + return dn; + } + + /** + * Returns the DN extracted from the client certificate. + * + * If the client auth setting is WANT or NONE and a certificate is not present (and {@code respectClientAuth} is {@code true}), this method will return {@code null}. + * If the client auth is NEED, it will throw a {@link CertificateException}. + * + * @param sslSocket the SSL Socket + * @return the extracted DN + * @throws CertificateException if there is a problem parsing the certificate + */ + private static String extractPeerDNFromClientSSLSocket(SSLSocket sslSocket) throws CertificateException { + String dn = null; + + /** The clientAuth value can be "need", "want", or "none" + * A client must send client certificates for need, should for want, and will not for none. + * This method should throw an exception if none are provided for need, return null if none are provided for want, and return null (without checking) for none. + */ + + ClientAuth clientAuth = getClientAuthStatus(sslSocket); + logger.debug("SSL Socket client auth status: {}", clientAuth); + + if (clientAuth != ClientAuth.NONE) { + try { + final Certificate[] certChains = sslSocket.getSession().getPeerCertificates(); + if (certChains != null && certChains.length > 0) { + X509Certificate x509Certificate = convertAbstractX509Certificate(certChains[0]); + dn = x509Certificate.getSubjectDN().getName().trim(); + logger.debug("Extracted DN={} from client certificate", dn); + } + } catch (SSLPeerUnverifiedException e) { + if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) { + logger.error("The incoming request did not contain client certificates and thus the DN cannot" + + " be extracted. Check that the other endpoint is providing a complete client certificate chain"); + } + if (clientAuth == ClientAuth.WANT) { + logger.warn("Suppressing missing client certificate exception because client auth is set to 'want'"); + return dn; + } + throw new CertificateException(e); + } + } + return dn; + } + + /** + * Returns the DN extracted from the server certificate. + * + * @param socket the SSL Socket + * @return the extracted DN + * @throws CertificateException if there is a problem parsing the certificate + */ + private static String extractPeerDNFromServerSSLSocket(Socket socket) throws CertificateException { + String dn = null; + if (socket instanceof SSLSocket) { + final SSLSocket sslSocket = (SSLSocket) socket; + try { + final Certificate[] certChains = sslSocket.getSession().getPeerCertificates(); + if (certChains != null && certChains.length > 0) { + X509Certificate x509Certificate = convertAbstractX509Certificate(certChains[0]); + dn = x509Certificate.getSubjectDN().getName().trim(); + logger.debug("Extracted DN={} from server certificate", dn); + } + } catch (SSLPeerUnverifiedException e) { + if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) { + logger.error("The server did not present a certificate and thus the DN cannot" + + " be extracted. Check that the other endpoint is providing a complete certificate chain"); + } + throw new CertificateException(e); + } + } + return dn; + } + + private static ClientAuth getClientAuthStatus(SSLSocket sslSocket) { + return sslSocket.getNeedClientAuth() ? ClientAuth.NEED : sslSocket.getWantClientAuth() ? ClientAuth.WANT : ClientAuth.NONE; + } + + /** + * Accepts a legacy {@link javax.security.cert.X509Certificate} and returns an {@link X509Certificate}. The {@code javax.*} package certificate classes are for legacy compatibility and should + * not be used for new development. + * + * @param legacyCertificate the {@code javax.security.cert.X509Certificate} + * @return a new {@code java.security.cert.X509Certificate} + * @throws CertificateException if there is an error generating the new certificate + */ + public static X509Certificate convertLegacyX509Certificate(javax.security.cert.X509Certificate legacyCertificate) throws CertificateException { + if (legacyCertificate == null) { + throw new IllegalArgumentException("The X.509 certificate cannot be null"); + } + + try { + return formX509Certificate(legacyCertificate.getEncoded()); + } catch (javax.security.cert.CertificateEncodingException e) { + throw new CertificateException(e); + } + } + + /** + * Accepts an abstract {@link Certificate} and returns an {@link X509Certificate}. Because {@code sslSocket.getSession().getPeerCertificates()} returns an array of the + * abstract certificates, they must be translated to X.509 to replace the functionality of {@code sslSocket.getSession().getPeerCertificateChain()}. + * + * @param abstractCertificate the {@code java.security.cert.Certificate} + * @return a new {@code java.security.cert.X509Certificate} + * @throws CertificateException if there is an error generating the new certificate + */ + public static X509Certificate convertAbstractX509Certificate(Certificate abstractCertificate) throws CertificateException { + if (abstractCertificate == null || !(abstractCertificate instanceof X509Certificate)) { + throw new IllegalArgumentException("The certificate cannot be null and must be an X.509 certificate"); + } + + try { + return formX509Certificate(abstractCertificate.getEncoded()); + } catch (java.security.cert.CertificateEncodingException e) { + throw new CertificateException(e); + } + } + + private static X509Certificate formX509Certificate(byte[] encodedCertificate) throws CertificateException { + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + ByteArrayInputStream bais = new ByteArrayInputStream(encodedCertificate); + return (X509Certificate) cf.generateCertificate(bais); + } catch (CertificateException e) { + logger.error("Error converting the certificate", e); + throw e; + } + } + + /** + * Reorders DN to the order the elements appear in the RFC 2253 table + * + * https://www.ietf.org/rfc/rfc2253.txt + * + * String X.500 AttributeType + * ------------------------------ + * CN commonName + * L localityName + * ST stateOrProvinceName + * O organizationName + * OU organizationalUnitName + * C countryName + * STREET streetAddress + * DC domainComponent + * UID userid + * + * @param dn a possibly unordered DN + * @return the ordered dn + */ + public static String reorderDn(String dn) { + RDN[] rdNs = new X500Name(dn).getRDNs(); + Arrays.sort(rdNs, new Comparator<RDN>() { + @Override + public int compare(RDN o1, RDN o2) { + AttributeTypeAndValue o1First = o1.getFirst(); + AttributeTypeAndValue o2First = o2.getFirst(); + + ASN1ObjectIdentifier o1Type = o1First.getType(); + ASN1ObjectIdentifier o2Type = o2First.getType(); + + Integer o1Rank = dnOrderMap.get(o1Type); + Integer o2Rank = dnOrderMap.get(o2Type); + if (o1Rank == null) { + if (o2Rank == null) { + int idComparison = o1Type.getId().compareTo(o2Type.getId()); + if (idComparison != 0) { + return idComparison; + } + return String.valueOf(o1Type).compareTo(String.valueOf(o2Type)); + } + return 1; + } else if (o2Rank == null) { + return -1; + } + return o1Rank - o2Rank; + } + }); + return new X500Name(rdNs).toString(); + } + + /** + * Reverses the X500Name in order make the certificate be in the right order + * [see http://stackoverflow.com/questions/7567837/attributes-reversed-in-certificate-subject-and-issuer/12645265] + * + * @param x500Name the X500Name created with the intended order + * @return the X500Name reversed + */ + private static X500Name reverseX500Name(X500Name x500Name) { + List<RDN> rdns = Arrays.asList(x500Name.getRDNs()); + Collections.reverse(rdns); + return new X500Name(rdns.toArray(new RDN[rdns.size()])); + } + + /** + * Generates a unique serial number by using the current time in milliseconds left shifted 32 bits (to make room for incrementor) with an incrementor added + * + * @return a unique serial number (technically unique to this classloader) + */ + protected static synchronized BigInteger getUniqueSerialNumber() { + final long currentTimeMillis = System.currentTimeMillis(); + final int incrementorValue; + + if (lastSerialNumberMillis != currentTimeMillis) { + // We can only get into this block once per millisecond + millisecondBigInteger = BigInteger.valueOf(currentTimeMillis).shiftLeft(32); + lastSerialNumberMillis = currentTimeMillis; + incrementorValue = 0; + serialNumberIncrementor = 1; + } else { + // Already created at least one serial number this millisecond + incrementorValue = serialNumberIncrementor++; + } + + return millisecondBigInteger.add(BigInteger.valueOf(incrementorValue)); + } + + /** + * Generates a self-signed {@link X509Certificate} suitable for use as a Certificate Authority. + * + * @param keyPair the {@link KeyPair} to generate the {@link X509Certificate} for + * @param dn the distinguished name to user for the {@link X509Certificate} + * @param signingAlgorithm the signing algorithm to use for the {@link X509Certificate} + * @param certificateDurationDays the duration in days for which the {@link X509Certificate} should be valid + * @return a self-signed {@link X509Certificate} suitable for use as a Certificate Authority + * @throws CertificateException if there is an generating the new certificate + */ + public static X509Certificate generateSelfSignedX509Certificate(KeyPair keyPair, String dn, String signingAlgorithm, int certificateDurationDays) + throws CertificateException { + try { + ContentSigner sigGen = new JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate()); + SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()); + Date startDate = new Date(); + Date endDate = new Date(startDate.getTime() + TimeUnit.DAYS.toMillis(certificateDurationDays)); + + X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder( + reverseX500Name(new X500Name(dn)), + getUniqueSerialNumber(), + startDate, endDate, + reverseX500Name(new X500Name(dn)), + subPubKeyInfo); + + // Set certificate extensions + // (1) digitalSignature extension + certBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.dataEncipherment + | KeyUsage.keyAgreement | KeyUsage.nonRepudiation | KeyUsage.cRLSign | KeyUsage.keyCertSign)); + + certBuilder.addExtension(Extension.basicConstraints, false, new BasicConstraints(true)); + + certBuilder.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic())); + + certBuilder.addExtension(Extension.authorityKeyIdentifier, false, new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(keyPair.getPublic())); + + // (2) extendedKeyUsage extension + certBuilder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth})); + + // Sign the certificate + X509CertificateHolder certificateHolder = certBuilder.build(sigGen); + return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certificateHolder); + } catch (CertIOException | NoSuchAlgorithmException | OperatorCreationException e) { + throw new CertificateException(e); + } + } + + /** + * Generates an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair} + * + * @param dn the distinguished name to use + * @param publicKey the public key to issue the certificate to + * @param issuer the issuer's certificate + * @param issuerKeyPair the issuer's keypair + * @param signingAlgorithm the signing algorithm to use + * @param days the number of days it should be valid for + * @return an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair} + * @throws CertificateException if there is an error issuing the certificate + */ + public static X509Certificate generateIssuedCertificate(String dn, PublicKey publicKey, X509Certificate issuer, KeyPair issuerKeyPair, String signingAlgorithm, int days) + throws CertificateException { + return generateIssuedCertificate(dn, publicKey, null, issuer, issuerKeyPair, signingAlgorithm, days); + } + + /** + * Generates an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair} + * + * @param dn the distinguished name to use + * @param publicKey the public key to issue the certificate to + * @param extensions extensions extracted from the CSR + * @param issuer the issuer's certificate + * @param issuerKeyPair the issuer's keypair + * @param signingAlgorithm the signing algorithm to use + * @param days the number of days it should be valid for + * @return an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair} + * @throws CertificateException if there is an error issuing the certificate + */ + public static X509Certificate generateIssuedCertificate(String dn, PublicKey publicKey, Extensions extensions, X509Certificate issuer, KeyPair issuerKeyPair, String signingAlgorithm, int days) + throws CertificateException { + try { + ContentSigner sigGen = new JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(issuerKeyPair.getPrivate()); + SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded()); + Date startDate = new Date(); + Date endDate = new Date(startDate.getTime() + TimeUnit.DAYS.toMillis(days)); + + X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder( + reverseX500Name(new X500Name(issuer.getSubjectX500Principal().getName())), + getUniqueSerialNumber(), + startDate, endDate, + reverseX500Name(new X500Name(dn)), + subPubKeyInfo); + + certBuilder.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(publicKey)); + + certBuilder.addExtension(Extension.authorityKeyIdentifier, false, new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(issuerKeyPair.getPublic())); + // Set certificate extensions + // (1) digitalSignature extension + certBuilder.addExtension(Extension.keyUsage, true, + new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.dataEncipherment | KeyUsage.keyAgreement | KeyUsage.nonRepudiation)); + + certBuilder.addExtension(Extension.basicConstraints, false, new BasicConstraints(false)); + + // (2) extendedKeyUsage extension + certBuilder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth})); + + // (3) subjectAlternativeName + if(extensions != null && extensions.getExtension(Extension.subjectAlternativeName) != null) { + certBuilder.addExtension(Extension.subjectAlternativeName, false, extensions.getExtensionParsedValue(Extension.subjectAlternativeName)); + } + + X509CertificateHolder certificateHolder = certBuilder.build(sigGen); + return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certificateHolder); + } catch (CertIOException | NoSuchAlgorithmException | OperatorCreationException e) { + throw new CertificateException(e); + } + } + + /** + * Returns true if the two provided DNs are equivalent, regardless of the order of the elements. Returns false if one or both are invalid DNs. + * + * Example: + * + * CN=test1, O=testOrg, C=US compared to CN=test1, O=testOrg, C=US -> true + * CN=test1, O=testOrg, C=US compared to O=testOrg, CN=test1, C=US -> true + * CN=test1, O=testOrg, C=US compared to CN=test2, O=testOrg, C=US -> false + * CN=test1, O=testOrg, C=US compared to O=testOrg, CN=test2, C=US -> false + * CN=test1, O=testOrg, C=US compared to -> false + * compared to -> true + * + * @param dn1 the first DN to compare + * @param dn2 the second DN to compare + * @return true if the DNs are equivalent, false otherwise + */ + public static boolean compareDNs(String dn1, String dn2) { + if (dn1 == null) { + dn1 = ""; + } + + if (dn2 == null) { + dn2 = ""; + } + + if (StringUtils.isEmpty(dn1) || StringUtils.isEmpty(dn2)) { + return dn1.equals(dn2); + } + try { + List<Rdn> rdn1 = new LdapName(dn1).getRdns(); + List<Rdn> rdn2 = new LdapName(dn2).getRdns(); + + return rdn1.size() == rdn2.size() && rdn1.containsAll(rdn2); + } catch (InvalidNameException e) { + logger.warn("Cannot compare DNs: {} and {} because one or both is not a valid DN", dn1, dn2); + return false; + } + } + + /** + * Extract extensions from CSR object + */ + public static Extensions getExtensionsFromCSR(JcaPKCS10CertificationRequest csr) { + Attribute[] attributess = csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest); + for (Attribute attribute : attributess) { + ASN1Set attValue = attribute.getAttrValues(); + if (attValue != null) { + ASN1Encodable extension = attValue.getObjectAt(0); + if (extension instanceof Extensions) { + return (Extensions) extension; + } else if (extension instanceof DERSequence) { + return Extensions.getInstance(extension); + } + } + } + return null; + } + + private CertificateUtils() { + } +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/KeyStoreUtils.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/KeyStoreUtils.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/KeyStoreUtils.java new file mode 100644 index 0000000..877c5b7 --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/KeyStoreUtils.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.nifi.registry.web.security.util; + +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.Security; + +public class KeyStoreUtils { + private static final Logger logger = LoggerFactory.getLogger(KeyStoreUtils.class); + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + /** + * Returns the provider that will be used for the given keyStoreType + * + * @param keyStoreType the keyStoreType + * @return the provider that will be used + */ + public static String getKeyStoreProvider(String keyStoreType) { + if (KeystoreType.PKCS12.toString().equalsIgnoreCase(keyStoreType)) { + return BouncyCastleProvider.PROVIDER_NAME; + } + return null; + } + + /** + * Returns an empty KeyStore backed by the appropriate provider + * + * @param keyStoreType the keyStoreType + * @return an empty KeyStore + * @throws KeyStoreException if a KeyStore of the given type cannot be instantiated + */ + public static KeyStore getKeyStore(String keyStoreType) throws KeyStoreException { + String keyStoreProvider = getKeyStoreProvider(keyStoreType); + if (StringUtils.isNotEmpty(keyStoreProvider)) { + try { + return KeyStore.getInstance(keyStoreType, keyStoreProvider); + } catch (Exception e) { + logger.error("Unable to load " + keyStoreProvider + " " + keyStoreType + + " keystore. This may cause issues getting trusted CA certificates as well as Certificate Chains for use in TLS.", e); + } + } + return KeyStore.getInstance(keyStoreType); + } + + /** + * Returns an empty KeyStore intended for use as a TrustStore backed by the appropriate provider + * + * @param trustStoreType the trustStoreType + * @return an empty KeyStore + * @throws KeyStoreException if a KeyStore of the given type cannot be instantiated + */ + public static KeyStore getTrustStore(String trustStoreType) throws KeyStoreException { + if (KeystoreType.PKCS12.toString().equalsIgnoreCase(trustStoreType)) { + logger.warn(trustStoreType + " truststores are deprecated. " + KeystoreType.JKS.toString() + " is preferred."); + } + return getKeyStore(trustStoreType); + } +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/KeystoreType.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/KeystoreType.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/KeystoreType.java new file mode 100644 index 0000000..f519b58 --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/util/KeystoreType.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.util; + +/** + * Keystore types. + */ +public enum KeystoreType { + PKCS12, + JKS; +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/SubjectDnX509PrincipalExtractor.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/SubjectDnX509PrincipalExtractor.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/SubjectDnX509PrincipalExtractor.java new file mode 100644 index 0000000..f8ab1fe --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/SubjectDnX509PrincipalExtractor.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.x509; + +import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; + +import java.security.cert.X509Certificate; + +/** + * Principal extractor for extracting a DN. + */ +public class SubjectDnX509PrincipalExtractor implements X509PrincipalExtractor { + + @Override + public Object extractPrincipal(X509Certificate cert) { + return cert.getSubjectDN().getName().trim(); + } + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationFilter.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationFilter.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationFilter.java new file mode 100644 index 0000000..df8bbbf --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationFilter.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.x509; + +import org.apache.nifi.registry.web.security.NiFiAuthenticationFilter; +import org.apache.nifi.registry.web.security.ProxiedEntitiesUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; + +import javax.servlet.http.HttpServletRequest; +import java.security.cert.X509Certificate; + +/** + * Custom X509 filter that will inspect the HTTP headers for a proxied user before extracting the user details from the client certificate. + */ +public class X509AuthenticationFilter extends NiFiAuthenticationFilter { + + private static final Logger logger = LoggerFactory.getLogger(X509AuthenticationFilter.class); + + private X509CertificateExtractor certificateExtractor; + private X509PrincipalExtractor principalExtractor; + + @Override + public Authentication attemptAuthentication(final HttpServletRequest request) { + // only suppport x509 login when running securely + if (!request.isSecure()) { + return null; + } + + // look for a client certificate + final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(request); + if (certificates == null) { + return null; + } + + return new X509AuthenticationRequestToken(request.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN), principalExtractor, certificates, request.getRemoteAddr()); + } + + /* setters */ + public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) { + this.certificateExtractor = certificateExtractor; + } + + public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) { + this.principalExtractor = principalExtractor; + } + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationProvider.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationProvider.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationProvider.java new file mode 100644 index 0000000..5ed7859 --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationProvider.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.x509; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.authorization.exception.AccessDeniedException; +import org.apache.nifi.registry.authorization.Authorizer; +import org.apache.nifi.registry.authorization.RequestAction; +import org.apache.nifi.registry.authorization.Resource; +import org.apache.nifi.registry.authorization.UserContextKeys; +import org.apache.nifi.registry.authorization.resource.Authorizable; +import org.apache.nifi.registry.authorization.resource.ResourceFactory; +import org.apache.nifi.registry.authorization.user.NiFiUser; +import org.apache.nifi.registry.authorization.user.NiFiUserDetails; +import org.apache.nifi.registry.authorization.user.StandardNiFiUser; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.web.response.AuthenticationResponse; +import org.apache.nifi.registry.web.security.InvalidAuthenticationException; +import org.apache.nifi.registry.web.security.NiFiAuthenticationProvider; +import org.apache.nifi.registry.web.security.ProxiedEntitiesUtils; +import org.apache.nifi.registry.web.security.UntrustedProxyException; +import org.apache.nifi.registry.web.security.token.NiFiAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; + +/** + * + */ +public class X509AuthenticationProvider extends NiFiAuthenticationProvider { + + private static final Authorizable PROXY_AUTHORIZABLE = new Authorizable() { + @Override + public Authorizable getParentAuthorizable() { + return null; + } + + @Override + public Resource getResource() { + return ResourceFactory.getProxyResource(); + } + }; + + private X509IdentityProvider certificateIdentityProvider; + private Authorizer authorizer; + + public X509AuthenticationProvider( + final X509IdentityProvider certificateIdentityProvider, + final Authorizer authorizer, + final NiFiRegistryProperties properties) { + super(properties, authorizer); + this.certificateIdentityProvider = certificateIdentityProvider; + this.authorizer = authorizer; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + final X509AuthenticationRequestToken request = (X509AuthenticationRequestToken) authentication; + + // attempt to authenticate if certificates were found + final AuthenticationResponse authenticationResponse; + try { + authenticationResponse = certificateIdentityProvider.authenticate(request.getCertificates()); + } catch (final IllegalArgumentException iae) { + throw new InvalidAuthenticationException(iae.getMessage(), iae); + } + + if (StringUtils.isBlank(request.getProxiedEntitiesChain())) { + final String mappedIdentity = mapIdentity(authenticationResponse.getIdentity()); + return new NiFiAuthenticationToken(new NiFiUserDetails( + new StandardNiFiUser.Builder() + .identity(mappedIdentity) + .groups(getUserGroups(mappedIdentity)) + .clientAddress(request.getClientAddress()) + .build())); + } else { + // build the entire proxy chain if applicable - <end-user><proxy1><proxy2> + final List<String> proxyChain = new ArrayList<>(ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(request.getProxiedEntitiesChain())); + proxyChain.add(authenticationResponse.getIdentity()); + + // add the chain as appropriate to each proxy + NiFiUser proxy = null; + for (final ListIterator<String> chainIter = proxyChain.listIterator(proxyChain.size()); chainIter.hasPrevious(); ) { + String identity = chainIter.previous(); + + // determine if the user is anonymous + final boolean isAnonymous = StringUtils.isBlank(identity); + if (isAnonymous) { + identity = StandardNiFiUser.ANONYMOUS_IDENTITY; + } else { + identity = mapIdentity(identity); + } + + final Set<String> groups = getUserGroups(identity); + + // Only set the client address for client making the request because we don't know the clientAddress of the proxied entities + String clientAddress = (proxy == null) ? request.getClientAddress() : null; + proxy = createUser(identity, groups, proxy, clientAddress, isAnonymous); + + if (chainIter.hasPrevious()) { + try { + PROXY_AUTHORIZABLE.authorize(authorizer, RequestAction.WRITE, proxy); + } catch (final AccessDeniedException e) { + throw new UntrustedProxyException(String.format("Untrusted proxy %s", identity)); + } + } + } + + return new NiFiAuthenticationToken(new NiFiUserDetails(proxy)); + } + } + + /** + * Returns a regular user populated with the provided values, or if the user should be anonymous, a well-formed instance of the anonymous user with the provided values. + * + * @param identity the user's identity + * @param chain the proxied entities + * @param clientAddress the requesting IP address + * @param isAnonymous if true, an anonymous user will be returned (identity will be ignored) + * @return the populated user + */ + protected static NiFiUser createUser(String identity, Set<String> groups, NiFiUser chain, String clientAddress, boolean isAnonymous) { + if (isAnonymous) { + return StandardNiFiUser.populateAnonymousUser(chain, clientAddress); + } else { + return new StandardNiFiUser.Builder().identity(identity).groups(groups).chain(chain).clientAddress(clientAddress).build(); + } + } + + private Map<String, String> getUserContext(final X509AuthenticationRequestToken request) { + final Map<String, String> userContext; + if (!StringUtils.isBlank(request.getClientAddress())) { + userContext = new HashMap<>(); + userContext.put(UserContextKeys.CLIENT_ADDRESS.name(), request.getClientAddress()); + } else { + userContext = null; + } + return userContext; + } + + @Override + public boolean supports(Class<?> authentication) { + return X509AuthenticationRequestToken.class.isAssignableFrom(authentication); + } +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationRequestToken.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationRequestToken.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationRequestToken.java new file mode 100644 index 0000000..22bc8bd --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509AuthenticationRequestToken.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.registry.web.security.x509; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.web.security.NiFiAuthenticationRequestToken; +import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; + +import java.security.cert.X509Certificate; + +/** + * This is an authentication request with a given JWT token. + */ +public class X509AuthenticationRequestToken extends NiFiAuthenticationRequestToken { + + private final String proxiedEntitiesChain; + private final X509PrincipalExtractor principalExtractor; + private final X509Certificate[] certificates; + + /** + * Creates a representation of the jwt authentication request for a user. + * + * @param proxiedEntitiesChain The http servlet request + * @param certificates The certificate chain + */ + public X509AuthenticationRequestToken(final String proxiedEntitiesChain, final X509PrincipalExtractor principalExtractor, final X509Certificate[] certificates, final String clientAddress) { + super(clientAddress); + setAuthenticated(false); + this.proxiedEntitiesChain = proxiedEntitiesChain; + this.principalExtractor = principalExtractor; + this.certificates = certificates; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + if (StringUtils.isBlank(proxiedEntitiesChain)) { + return principalExtractor.extractPrincipal(certificates[0]); + } else { + return String.format("%s<%s>", proxiedEntitiesChain, principalExtractor.extractPrincipal(certificates[0])); + } + } + + public String getProxiedEntitiesChain() { + return proxiedEntitiesChain; + } + + public X509Certificate[] getCertificates() { + return certificates; + } + + @Override + public String toString() { + return getName(); + } + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509CertificateExtractor.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509CertificateExtractor.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509CertificateExtractor.java new file mode 100644 index 0000000..ad62b37 --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509CertificateExtractor.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.x509; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import java.security.cert.X509Certificate; + +/** + * Extracts client certificates from Http requests. + */ +public class X509CertificateExtractor { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * Extract the client certificate from the specified HttpServletRequest or + * null if none is specified. + * + * @param request http request + * @return cert + */ + public X509Certificate[] extractClientCertificate(HttpServletRequest request) { + X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate"); + + if (certs != null && certs.length > 0) { + return certs; + } + + if (logger.isDebugEnabled()) { + logger.debug("No client certificate found in request."); + } + + return null; + } + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509CertificateValidator.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509CertificateValidator.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509CertificateValidator.java new file mode 100644 index 0000000..fe2c3e5 --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509CertificateValidator.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.x509; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; + +/** + * Extracts client certificates from Http requests. + */ +public class X509CertificateValidator { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * Extract the client certificate from the specified HttpServletRequest or null if none is specified. + * + * @param certificates the client certificates + * @throws CertificateExpiredException cert is expired + * @throws CertificateNotYetValidException cert is not yet valid + */ + public void validateClientCertificate(final X509Certificate[] certificates) + throws CertificateExpiredException, CertificateNotYetValidException { + + // ensure the cert is valid + certificates[0].checkValidity(); + } + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509IdentityProvider.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509IdentityProvider.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509IdentityProvider.java new file mode 100644 index 0000000..38aa0b4 --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/x509/X509IdentityProvider.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.registry.web.security.x509; + +import org.apache.nifi.registry.web.response.AuthenticationResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; + +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.concurrent.TimeUnit; + +/** + * Identity provider for extract the authenticating a ServletRequest with a X509Certificate. + */ +public class X509IdentityProvider { + + private static final Logger logger = LoggerFactory.getLogger(X509IdentityProvider.class); + + private final String issuer = getClass().getSimpleName(); + + private X509CertificateValidator certificateValidator; + private X509PrincipalExtractor principalExtractor; + + /** + * Authenticates the specified request by checking certificate validity. + * + * @param certificates the client certificates + * @return an authentication response + * @throws IllegalArgumentException the request did not contain a valid certificate (or no certificate) + */ + public AuthenticationResponse authenticate(final X509Certificate[] certificates) throws IllegalArgumentException { + // ensure the cert was found + if (certificates == null || certificates.length == 0) { + throw new IllegalArgumentException("The specified request does not contain a client certificate."); + } + + // extract the principal + final Object certificatePrincipal = principalExtractor.extractPrincipal(certificates[0]); + final String principal = certificatePrincipal.toString(); + + try { + certificateValidator.validateClientCertificate(certificates); + } catch (CertificateExpiredException cee) { + final String message = String.format("Client certificate for (%s) is expired.", principal); + logger.info(message, cee); + if (logger.isDebugEnabled()) { + logger.debug("", cee); + } + throw new IllegalArgumentException(message, cee); + } catch (CertificateNotYetValidException cnyve) { + final String message = String.format("Client certificate for (%s) is not yet valid.", principal); + logger.info(message, cnyve); + if (logger.isDebugEnabled()) { + logger.debug("", cnyve); + } + throw new IllegalArgumentException(message, cnyve); + } catch (final Exception e) { + logger.info(e.getMessage()); + if (logger.isDebugEnabled()) { + logger.debug("", e); + } + throw new IllegalArgumentException(e.getMessage(), e); + } + + // build the authentication response + return new AuthenticationResponse(principal, principal, TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS), issuer); + } + + /* setters */ + public void setCertificateValidator(X509CertificateValidator certificateValidator) { + this.certificateValidator = certificateValidator; + } + + public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) { + this.principalExtractor = principalExtractor; + } + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java b/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java index d204b80..bfc9a46 100644 --- a/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java +++ b/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java @@ -72,10 +72,12 @@ public class TestLinkService { final VersionedFlowSnapshotMetadata snapshotMetadata1 = new VersionedFlowSnapshotMetadata(); snapshotMetadata1.setFlowIdentifier(flow1.getIdentifier()); snapshotMetadata1.setVersion(1); + snapshotMetadata1.setBucketIdentifier(bucket1.getIdentifier()); final VersionedFlowSnapshotMetadata snapshotMetadata2 = new VersionedFlowSnapshotMetadata(); snapshotMetadata2.setFlowIdentifier(flow1.getIdentifier()); snapshotMetadata2.setVersion(2); + snapshotMetadata2.setBucketIdentifier(bucket1.getIdentifier()); snapshots = new ArrayList<>(); snapshots.add(snapshotMetadata1); @@ -91,14 +93,16 @@ public class TestLinkService { public void testPopulateBucketLinks() { buckets.stream().forEach(b -> Assert.assertNull(b.getLink())); linkService.populateBucketLinks(buckets); - buckets.stream().forEach(b -> Assert.assertEquals("buckets/" + b.getIdentifier(), b.getLink().getUri().toString())); + buckets.stream().forEach(b -> Assert.assertEquals( + "buckets/" + b.getIdentifier(), b.getLink().getUri().toString())); } @Test public void testPopulateFlowLinks() { flows.stream().forEach(f -> Assert.assertNull(f.getLink())); linkService.populateFlowLinks(flows); - flows.stream().forEach(f -> Assert.assertEquals("flows/" + f.getIdentifier(), f.getLink().getUri().toString())); + flows.stream().forEach(f -> Assert.assertEquals( + "buckets/" + f.getBucketIdentifier() + "/flows/" + f.getIdentifier(), f.getLink().getUri().toString())); } @Test @@ -106,14 +110,15 @@ public class TestLinkService { snapshots.stream().forEach(s -> Assert.assertNull(s.getLink())); linkService.populateSnapshotLinks(snapshots); snapshots.stream().forEach(s -> Assert.assertEquals( - "flows/" + s.getFlowIdentifier() + "/versions/" + s.getVersion(), s.getLink().getUri().toString())); + "buckets/" + s.getBucketIdentifier() + "/flows/" + s.getFlowIdentifier() + "/versions/" + s.getVersion(), s.getLink().getUri().toString())); } @Test public void testPopulateItemLinks() { items.stream().forEach(i -> Assert.assertNull(i.getLink())); linkService.populateItemLinks(items); - items.stream().forEach(i -> Assert.assertEquals("flows/" + i.getIdentifier(), i.getLink().getUri().toString())); + items.stream().forEach(i -> Assert.assertEquals( + "buckets/" + i.getBucketIdentifier() + "/flows/" + i.getIdentifier(), i.getLink().getUri().toString())); } } http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/pom.xml ---------------------------------------------------------------------- diff --git a/pom.xml b/pom.xml index 1c61e41..66c8197 100644 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,8 @@ <module>nifi-registry-jetty</module> <module>nifi-registry-resources</module> <module>nifi-registry-runtime</module> - <module>nifi-registry-security</module> + <module>nifi-registry-security-api</module> + <module>nifi-registry-security-api-impl</module> <module>nifi-registry-framework</module> <module>nifi-registry-provider-api</module> <module>nifi-registry-web-api</module> @@ -265,7 +266,7 @@ <version>2.0.0.Final</version> </dependency> <dependency> - <groupId>org.hibernate</groupId> + <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.2.Final</version> </dependency> @@ -396,7 +397,7 @@ <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>jaxb2-maven-plugin</artifactId> - <version>1.6</version> + <version>2.3.1</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId>
