diff --git a/commons/pom.xml b/commons/pom.xml new file mode 100644 index 00000000000..6d083bc6efe --- /dev/null +++ b/commons/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + gov.nsa.datawave + datawave-parent + 7.40.0-SNAPSHOT + + + gov.nsa.datawave.commons + datawave-commons-parent + pom + ${project.artifactId} + + + security + + diff --git a/commons/security/README.md b/commons/security/README.md new file mode 100644 index 00000000000..cb2dcf01d62 --- /dev/null +++ b/commons/security/README.md @@ -0,0 +1,6 @@ +# Overview + +This project contains security-related classes that are commonly used between the main Datawave project and the microservices. It is expected that this project will be configured and deployed as a JBOSS module via the [Wildfly assembly](../../web-services/deploy/application) project to make it available to the Datawave EAR deployment. + +## Note: +Any compile dependencies here are expected to be imported into the Datawave webservices projects with scope `provided`, and provided via JBOSS modules. This is required to avoid classloader conflicts between the JBOSS modules and the Datawave EAR deployment. See the [Wildfly assembly README](../../web-services/deploy/application/README.md) for more details. diff --git a/commons/security/pom.xml b/commons/security/pom.xml new file mode 100644 index 00000000000..b4d8a134291 --- /dev/null +++ b/commons/security/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + gov.nsa.datawave.commons + datawave-commons-parent + 7.40.0-SNAPSHOT + + + datawave-commons-security + ${project.artifactId} + + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-core + ${version.jackson} + + + com.fasterxml.jackson.core + jackson-databind + ${version.jackson} + + + com.google.guava + guava + + + io.jsonwebtoken + jjwt-api + ${version.jjwt} + + + io.jsonwebtoken + jjwt-impl + ${version.jjwt} + + + jakarta.xml.bind + jakarta.xml.bind-api + ${version.jakarta} + + + org.junit.jupiter + junit-jupiter-api + + + org.slf4j + slf4j-api + + + org.junit-pioneer + junit-pioneer + test + + + diff --git a/microservices/services/authorization/api/src/main/java/datawave/security/authorization/AuthorizationException.java b/commons/security/src/main/java/datawave/security/authorization/AuthorizationException.java similarity index 100% rename from microservices/services/authorization/api/src/main/java/datawave/security/authorization/AuthorizationException.java rename to commons/security/src/main/java/datawave/security/authorization/AuthorizationException.java diff --git a/microservices/services/authorization/api/src/main/java/datawave/security/authorization/CachedDatawaveUserService.java b/commons/security/src/main/java/datawave/security/authorization/CachedDatawaveUserService.java similarity index 100% rename from microservices/services/authorization/api/src/main/java/datawave/security/authorization/CachedDatawaveUserService.java rename to commons/security/src/main/java/datawave/security/authorization/CachedDatawaveUserService.java diff --git a/core/common-util/src/main/java/datawave/security/authorization/DatawavePrincipal.java b/commons/security/src/main/java/datawave/security/authorization/DatawavePrincipal.java similarity index 97% rename from core/common-util/src/main/java/datawave/security/authorization/DatawavePrincipal.java rename to commons/security/src/main/java/datawave/security/authorization/DatawavePrincipal.java index 0a151843b62..a94deee0bd1 100644 --- a/core/common-util/src/main/java/datawave/security/authorization/DatawavePrincipal.java +++ b/commons/security/src/main/java/datawave/security/authorization/DatawavePrincipal.java @@ -17,7 +17,7 @@ import javax.xml.bind.annotation.XmlType; import datawave.security.authorization.DatawaveUser.UserType; -import datawave.security.util.ProxiedEntityUtils; +import datawave.security.util.DnUtils; /** * A {@link Principal} that represents a set of proxied {@link DatawaveUser}s. For example, this proxied user could represent a GUI server acting on behalf of a @@ -103,8 +103,8 @@ static protected List orderProxiedUsers(List datawav if (position >= 0) { users.add(datawaveUsers.get(position)); if (datawaveUsers.size() > 1) { - datawaveUsers.stream().limit(position).forEach(u -> users.add(u)); - datawaveUsers.stream().skip(position + 1).forEach(u -> users.add(u)); + datawaveUsers.stream().limit(position).forEach(users::add); + datawaveUsers.stream().skip(position + 1).forEach(users::add); } } return users; @@ -152,7 +152,7 @@ public String getName() { @Override public String getShortName() { - return ProxiedEntityUtils.getShortName(getPrimaryUser().getName()); + return DnUtils.getShortName(getPrimaryUser().getName()); } public SubjectIssuerDNPair getUserDN() { diff --git a/microservices/services/authorization/api/src/main/java/datawave/security/authorization/DatawaveUser.java b/commons/security/src/main/java/datawave/security/authorization/DatawaveUser.java similarity index 96% rename from microservices/services/authorization/api/src/main/java/datawave/security/authorization/DatawaveUser.java rename to commons/security/src/main/java/datawave/security/authorization/DatawaveUser.java index 9c03597efba..75aec6ced6f 100644 --- a/microservices/services/authorization/api/src/main/java/datawave/security/authorization/DatawaveUser.java +++ b/commons/security/src/main/java/datawave/security/authorization/DatawaveUser.java @@ -12,13 +12,14 @@ import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; -import datawave.security.util.ProxiedEntityUtils; +import datawave.security.util.DnUtils; /** * A user of a DATAWAVE service. Typically, one or more of these users (a chain where a user called an intermediate service which in turn called us) is * represented with a DatawavePrincipal. */ public class DatawaveUser implements Serializable { + private static final long serialVersionUID = -6676807246749142999L; public enum UserType { @@ -26,6 +27,7 @@ public enum UserType { } public static final DatawaveUser ANONYMOUS_USER = new DatawaveUser(SubjectIssuerDNPair.of("ANONYMOUS"), UserType.USER, null, null, null, null, -1L); + private final String name; private final String commonName; private final String email; @@ -58,8 +60,8 @@ public DatawaveUser(@JsonProperty(value = "dn", required = true) SubjectIssuerDN @JsonProperty(value = "creationTime", defaultValue = "-1L") long creationTime, @JsonProperty(value = "expirationTime", defaultValue = "-1L") long expirationTime) { this.name = dn.toString(); - this.commonName = ProxiedEntityUtils.getCommonName(dn.subjectDN()); - this.login = ProxiedEntityUtils.getShortName(dn.subjectDN()); + this.commonName = DnUtils.getCommonName(dn.subjectDN()); + this.login = DnUtils.getShortName(dn.subjectDN()); this.email = email; this.dn = dn; this.userType = userType; diff --git a/microservices/services/authorization/api/src/main/java/datawave/security/authorization/DatawaveUserInfo.java b/commons/security/src/main/java/datawave/security/authorization/DatawaveUserInfo.java similarity index 100% rename from microservices/services/authorization/api/src/main/java/datawave/security/authorization/DatawaveUserInfo.java rename to commons/security/src/main/java/datawave/security/authorization/DatawaveUserInfo.java diff --git a/microservices/services/authorization/api/src/main/java/datawave/security/authorization/DatawaveUserService.java b/commons/security/src/main/java/datawave/security/authorization/DatawaveUserService.java similarity index 100% rename from microservices/services/authorization/api/src/main/java/datawave/security/authorization/DatawaveUserService.java rename to commons/security/src/main/java/datawave/security/authorization/DatawaveUserService.java diff --git a/microservices/services/authorization/api/src/main/java/datawave/security/authorization/DatawaveUserV1.java b/commons/security/src/main/java/datawave/security/authorization/DatawaveUserV1.java similarity index 100% rename from microservices/services/authorization/api/src/main/java/datawave/security/authorization/DatawaveUserV1.java rename to commons/security/src/main/java/datawave/security/authorization/DatawaveUserV1.java diff --git a/microservices/services/authorization/api/src/main/java/datawave/security/authorization/JWTTokenHandler.java b/commons/security/src/main/java/datawave/security/authorization/JWTTokenHandler.java similarity index 99% rename from microservices/services/authorization/api/src/main/java/datawave/security/authorization/JWTTokenHandler.java rename to commons/security/src/main/java/datawave/security/authorization/JWTTokenHandler.java index 15fcd6c997a..431893cddd4 100644 --- a/microservices/services/authorization/api/src/main/java/datawave/security/authorization/JWTTokenHandler.java +++ b/commons/security/src/main/java/datawave/security/authorization/JWTTokenHandler.java @@ -137,7 +137,7 @@ public Collection createUsersFromToken(String token, String claimN return principalsClaim.stream().map(obj -> objectMapper.convertValue(obj, DatawaveUser.class)).collect(Collectors.toList()); } - private class CustomJWTBuilder extends DefaultJwtBuilder { + private static class CustomJWTBuilder extends DefaultJwtBuilder { private final ObjectMapper objectMapper; private CustomJWTBuilder(ObjectMapper objectMapper) { diff --git a/microservices/services/authorization/api/src/main/java/datawave/security/authorization/ProxiedUserDetails.java b/commons/security/src/main/java/datawave/security/authorization/ProxiedUserDetails.java similarity index 100% rename from microservices/services/authorization/api/src/main/java/datawave/security/authorization/ProxiedUserDetails.java rename to commons/security/src/main/java/datawave/security/authorization/ProxiedUserDetails.java diff --git a/microservices/services/authorization/api/src/main/java/datawave/security/authorization/SubjectIssuerDNPair.java b/commons/security/src/main/java/datawave/security/authorization/SubjectIssuerDNPair.java similarity index 88% rename from microservices/services/authorization/api/src/main/java/datawave/security/authorization/SubjectIssuerDNPair.java rename to commons/security/src/main/java/datawave/security/authorization/SubjectIssuerDNPair.java index ea90cf789f6..cccbb120e88 100644 --- a/microservices/services/authorization/api/src/main/java/datawave/security/authorization/SubjectIssuerDNPair.java +++ b/commons/security/src/main/java/datawave/security/authorization/SubjectIssuerDNPair.java @@ -8,12 +8,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.Preconditions; -import datawave.security.util.ProxiedEntityUtils; +import datawave.security.util.DnUtils; /** * A simple pair containing a subject and (optional) issuer DN. The supplied DN values are normalized into a lower-case form with the CN portion first. * - * @see ProxiedEntityUtils#normalizeDN(String) + * @see DnUtils#normalizeDN(String) */ public class SubjectIssuerDNPair implements Serializable { @@ -61,7 +61,7 @@ public static SubjectIssuerDNPair of(@JsonProperty("subjectDN") String subjectDN * if DNs cannot be parsed from the argument, or if exactly two DNs are not parsed */ public static SubjectIssuerDNPair parse(String value) { - String[] dns = ProxiedEntityUtils.splitProxiedSubjectIssuerDNs(value); + String[] dns = DnUtils.splitProxiedSubjectIssuerDNs(value); if (dns.length != 2) { throw new IllegalArgumentException("'" + value + "' must contain a single subject and issuer DN"); } @@ -70,9 +70,9 @@ public static SubjectIssuerDNPair parse(String value) { protected SubjectIssuerDNPair(String subjectDN, String issuerDN) { Preconditions.checkNotNull(subjectDN, "Parameter subjectDN must not be null"); - this.subjectDN = ProxiedEntityUtils.normalizeDN(subjectDN); + this.subjectDN = DnUtils.normalizeDN(subjectDN); if (issuerDN != null) { - this.issuerDN = ProxiedEntityUtils.normalizeDN(issuerDN); + this.issuerDN = DnUtils.normalizeDN(issuerDN); } else { this.issuerDN = null; } @@ -90,7 +90,7 @@ public String issuerDN() { @Override public String toString() { - return issuerDN == null ? subjectDN + "<>" : ProxiedEntityUtils.buildProxiedDN(subjectDN, issuerDN); + return issuerDN == null ? subjectDN + "<>" : DnUtils.buildProxiedDN(subjectDN, issuerDN); } @Override diff --git a/commons/security/src/main/java/datawave/security/cert/DatawaveCertVerifier.java b/commons/security/src/main/java/datawave/security/cert/DatawaveCertVerifier.java new file mode 100644 index 00000000000..b39eabafbca --- /dev/null +++ b/commons/security/src/main/java/datawave/security/cert/DatawaveCertVerifier.java @@ -0,0 +1,122 @@ +package datawave.security.cert; + +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Objects; + +import org.slf4j.Logger; + +/** + * A Datawave-specific {@link X509CertificateVerifier} implementation. + */ +public class DatawaveCertVerifier implements X509CertificateVerifier { + + public enum OcspLevel { + OFF, OPTIONAL, REQUIRED + } + + protected Logger log; + protected boolean trace; + protected OcspLevel ocspLevel = OcspLevel.OFF; + + /** + * Verify the given certificate + * + * @param cert + * the X509Certificate to verify + * @param alias + * the certificate alias + * @param keystore + * the keystore for the cert + * @param truststore + * the truststore for the cert + * @return whether the certificate is considered valid + */ + @Override + public boolean verify(X509Certificate cert, String alias, KeyStore keystore, KeyStore truststore) { + boolean validity = false; + try { + cert.checkValidity(); + validity = checkOCSP(cert, alias, truststore); + } catch (Exception e) { + if (trace) + log.trace("Validity exception", e); + } + return validity; + + } + + /** + * Handle OSCP initialization. + */ + protected void initOcsp() {} + + /** + * Return the OSCP level set for this verifier is supported for the given certificate. + * + * @param cert + * the certificate + * @param alias + * the certificate alias + * @param truststore + * the truststore + * @return true if the OSCP level is supported, or false otherwise + */ + protected boolean checkOCSP(X509Certificate cert, String alias, KeyStore truststore) { + if (Objects.requireNonNull(ocspLevel) == OcspLevel.OFF) { + return true; + } else { + log.error("OCSP level {} is not supported!", ocspLevel); + throw new IllegalArgumentException("OCSP level " + ocspLevel + " is not supported!"); + } + } + + /** + * Return whether the given issuer is supported. + * + * @param issuerSubjectDn + * the issuer DN + * @param trustStore + * the truststore + * @return true if the issuer is supported, or false otherwise + */ + public boolean isIssuerSupported(String issuerSubjectDn, KeyStore trustStore) { + return true; + } + + /** + * Set the delegate logger for this {@link DatawaveCertVerifier}. + * + * @param log + * the logger + */ + public void setLogger(Logger log) { + this.log = log; + this.trace = log.isTraceEnabled(); + } + + /** + * Return the OSCP level. + * + * @return the OSCP level + */ + public OcspLevel getOcspLevel() { + return ocspLevel; + } + + /** + * Set the OSCP level. + * + * @param level + * the OSCP level + */ + public void setOcspLevel(String level) { + ocspLevel = OcspLevel.valueOf(level.toUpperCase()); + switch (ocspLevel) { + case REQUIRED: + case OPTIONAL: + initOcsp(); + break; + } + } +} diff --git a/commons/security/src/main/java/datawave/security/cert/SSLStores.java b/commons/security/src/main/java/datawave/security/cert/SSLStores.java new file mode 100644 index 00000000000..e14221ac5df --- /dev/null +++ b/commons/security/src/main/java/datawave/security/cert/SSLStores.java @@ -0,0 +1,49 @@ +package datawave.security.cert; + +import java.security.KeyStore; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.TrustManager; + +/** + * Represents a key store/trust store pair. + */ +public interface SSLStores { + + /** + * Return the key store + * + * @return the keystore + */ + default KeyStore getKeyStore() { + return null; + } + + /** + * Return the key managers + * + * @return the key managers + */ + default KeyManager[] getKeyManagers() { + return new KeyManager[0]; + } + + /** + * Return the trust store + * + * @return the truststore + */ + default KeyStore getTrustStore() { + return null; + } + + /** + * Return the trust managers + * + * @return the trust managers + */ + default TrustManager[] getTrustManagers() { + return new TrustManager[0]; + } + +} diff --git a/commons/security/src/main/java/datawave/security/cert/SSLStoresImpl.java b/commons/security/src/main/java/datawave/security/cert/SSLStoresImpl.java new file mode 100644 index 00000000000..0a6d0c54abf --- /dev/null +++ b/commons/security/src/main/java/datawave/security/cert/SSLStoresImpl.java @@ -0,0 +1,235 @@ +package datawave.security.cert; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import com.google.common.base.Preconditions; + +/** + * Represents a key store/trust store pair. + */ +public class SSLStoresImpl implements SSLStores { + + private final KeyStore keyStore; + private final KeyManager[] keyManagers; + + private final KeyStore trustStore; + private final TrustManager[] trustManagers; + + /** + * Return a new builder. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + public SSLStoresImpl(KeyStore keyStore, KeyManager[] keyManagers, KeyStore trustStore, TrustManager[] trustManagers) { + this.keyStore = keyStore; + this.keyManagers = keyManagers; + this.trustStore = trustStore; + this.trustManagers = trustManagers; + } + + @Override + public KeyStore getKeyStore() { + return this.keyStore; + } + + @Override + public KeyManager[] getKeyManagers() { + return this.keyManagers; + } + + @Override + public KeyStore getTrustStore() { + return this.trustStore; + } + + @Override + public TrustManager[] getTrustManagers() { + return this.trustManagers; + } + + public static class Builder { + private String keyStoreUrl; + private String keyStoreType; + private String keyStorePassword; + private String trustStoreUrl; + private String trustStoreType; + private String trustStorePassword; + + /** + * Set the information required to load the keystore. + * + * @param url + * the keystore URL, this may be a URL, file path, or classloader resource + * @param password + * the keystore password + * @param type + * the keystore type + * @return this builder + */ + public Builder withKeystore(String url, String password, String type) { + Preconditions.checkNotNull(url, "keystore URL cannot be null"); + Preconditions.checkNotNull(password, "keystore password cannot be null"); + Preconditions.checkNotNull(type, "keystore type cannot be null"); + this.keyStoreUrl = url; + this.keyStoreType = type; + this.keyStorePassword = password; + return this; + } + + /** + * Set the information required to load the trust store. + * + * @param url + * the trust store URL, this may be a URL, file path, or classpath resource + * @param password + * the trust store password + * @param type + * the trust store type + * @return this builder + */ + public Builder withTruststore(String url, String password, String type) { + Preconditions.checkNotNull(url, "truststore URL cannot be null"); + Preconditions.checkNotNull(password, "truststore password cannot be null"); + Preconditions.checkNotNull(type, "truststore type cannot be null"); + this.trustStoreUrl = url; + this.trustStoreType = type; + this.trustStorePassword = password; + return this; + } + + /** + * Build and return an {@link SSLStoresImpl} instance. + * + * @return the new instance + * @throws Exception + * if an error occurs while loading the keystore and truststore + */ + public SSLStoresImpl build() throws Exception { + // Load the keystore. + char[] keyStorePassword = this.keyStorePassword.toCharArray(); + KeyStore keyStore = getKeyStore(keyStoreUrl, keyStorePassword, keyStoreType); + KeyManager[] keyManagers = getKeyManagers(keyStore, keyStorePassword); + + KeyStore trustStore; + // If no trust store URL was set, use the keystore as the trust store. + if (trustStoreUrl != null) { + trustStore = getKeyStore(trustStoreUrl, trustStorePassword.toCharArray(), trustStoreType); + } else { + trustStore = keyStore; + } + TrustManager[] trustManagers = getTrustManagers(trustStore); + + return new SSLStoresImpl(keyStore, keyManagers, trustStore, trustManagers); + } + + private static final String PKCS11 = "PKCS11"; + private static final String PKCS11IMPLKS = "PKCS11IMPLKS"; + + /** + * Load and return a {@link KeyStore}. + * + * @param url + * the keystore URL + * @param password + * the keystore password + * @param type + * the keystore type + * @return the keystore + */ + private KeyStore getKeyStore(final String url, final char[] password, String type) + throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException { + // If no type was specified, use JKS by default. + KeyStore keyStore = KeyStore.getInstance(type); + // If the type is not PKCS11, load the keystore from the URL stream. + if (!PKCS11.equalsIgnoreCase(type) && !PKCS11IMPLKS.equalsIgnoreCase(type)) { + URL keyStoreUrl = validateKeystoreUrl(url); + try (InputStream is = keyStoreUrl.openStream()) { + keyStore.load(is, password); + } + } + + return keyStore; + } + + /** + * Return the key managers from the given keystore. + * + * @param keyStore + * the keystore + * @param password + * the keystore password + * @return the key managers + */ + private KeyManager[] getKeyManagers(KeyStore keyStore, char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException { + String algorithm = KeyManagerFactory.getDefaultAlgorithm(); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(algorithm); + keyManagerFactory.init(keyStore, password); + return keyManagerFactory.getKeyManagers(); + } + + /** + * Return the trust managers from the given truststore. + * + * @param trustStore + * the truststore + * @return the trust managers + */ + private TrustManager[] getTrustManagers(KeyStore trustStore) throws NoSuchAlgorithmException, KeyStoreException { + String algorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm); + trustManagerFactory.init(trustStore); + return trustManagerFactory.getTrustManagers(); + } + + /** + * Validate the given URL string and verify that it is a valid URL, file path, or classpath resource. + * + * @param urlStr + * the URL string + * @return the URL + */ + private URL validateKeystoreUrl(String urlStr) throws MalformedURLException { + // First, try to parse it as a URL. + try { + return new URL(urlStr); + } catch (MalformedURLException e) { + // Either not a URL or protocol without a handler. + } + + // Next, try to locate this as a file path. + File file = new File(urlStr); + if (file.exists()) { + return file.toURI().toURL(); + } + + // Next, try to locate this as a classpath resource. + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader != null) { + URL url = classLoader.getResource(urlStr); + if (url != null) { + return url; + } + } + + throw new MalformedURLException("Failed to validate " + urlStr + " as a URL, file, or classpath resource."); + } + } +} diff --git a/commons/security/src/main/java/datawave/security/cert/X509CertificateVerifier.java b/commons/security/src/main/java/datawave/security/cert/X509CertificateVerifier.java new file mode 100644 index 00000000000..bf6f495778b --- /dev/null +++ b/commons/security/src/main/java/datawave/security/cert/X509CertificateVerifier.java @@ -0,0 +1,25 @@ +package datawave.security.cert; + +import java.security.KeyStore; +import java.security.cert.X509Certificate; + +/** + * A base X509 certificate verifier. + */ +public interface X509CertificateVerifier { + + /** + * Validate a cert. + * + * @param cert + * the X509Certificate to verify + * @param alias + * the expected keystore alias + * @param keyStore + * the keystore for the cert + * @param trustStore + * the truststore for the cert + * @return true if the cert is valid, false otherwise + */ + boolean verify(X509Certificate cert, String alias, KeyStore keyStore, KeyStore trustStore); +} diff --git a/commons/security/src/main/java/datawave/security/util/DnProperties.java b/commons/security/src/main/java/datawave/security/util/DnProperties.java new file mode 100644 index 00000000000..5ed179d3b30 --- /dev/null +++ b/commons/security/src/main/java/datawave/security/util/DnProperties.java @@ -0,0 +1,164 @@ +package datawave.security.util; + +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.StringJoiner; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class contains properties that are often required when parsing/manipulating DNs. + */ +public class DnProperties { + + private static final Logger log = LoggerFactory.getLogger(DnProperties.class); + + /** Config for specifying default subject DN patterns and NPE OUs */ + public static final String DEFAULT_PROPERTIES_FILE = "dnutils.properties"; + + /** System property that contains a regex pattern that matches against subject DNs. */ + public static final String SUBJECT_DN_PATTERN_PROPERTY = "subject.dn.pattern"; + + /** System property containing a comma-delimited list of NPE OUs. */ + public static final String NPE_OU_PROPERTY = "npe.ou.entries"; + + /** + * The default instance loaded from system properties and/or a properties file. + */ + private static DnProperties defaultInstance; + + private final Pattern subjectDnPattern; + private final Set npeOUs; + + /** + * Return the default instance of {@link DnProperties}. If it does not exist, it will be set to the result of {@link #createInstanceFromProperties(String)} + * with the properties file {@value DEFAULT_PROPERTIES_FILE}. + * + * @return the default {@link DnProperties} + * @see #createInstanceFromProperties(String) + */ + public static DnProperties getDefaultInstance() { + if (defaultInstance == null) { + defaultInstance = createInstanceFromProperties(DEFAULT_PROPERTIES_FILE); + } + return defaultInstance; + } + + /** + * Create an instance of {@link DnProperties} where the subject DN pattern and NPE OU list are loaded (in priority order) from the system properties + * {@value SUBJECT_DN_PATTERN_PROPERTY} and {@value NPE_OU_PROPERTY}, or those same properties in the given properties file from the classloader if they + * were not specified in the system properties. + * + * @return a new {@link DnProperties} + */ + public static DnProperties createInstanceFromProperties(String propertiesFile) { + // Attempt to fetch a subject DN pattern and NPE OU list from system properties. + String subjectDnPatternValue = System.getProperty(SUBJECT_DN_PATTERN_PROPERTY, ""); + String npeOUsValue = System.getProperty(NPE_OU_PROPERTY, ""); + + // If either a subject DN pattern or NPE OU list were not specified, attempt to load them from the properties file. + if ((subjectDnPatternValue.isBlank()) || (npeOUsValue.isBlank())) { + // Attempt to load the default subject DN pattern and NPE OU list from the properties file available via the classloader. + Properties props = new Properties(); + try (InputStream in = DnProperties.class.getClassLoader().getResourceAsStream(propertiesFile)) { + props.load(in); + + // Update the subject DN if needed. + if (subjectDnPatternValue.isBlank()) { + subjectDnPatternValue = props.getProperty(SUBJECT_DN_PATTERN_PROPERTY, ""); + if (subjectDnPatternValue.isBlank()) { + log.warn("Subject DN pattern property {} not set in {}", SUBJECT_DN_PATTERN_PROPERTY, propertiesFile); + } + } + + // Update the NPE OU list if needed. + if (npeOUsValue.isBlank()) { + npeOUsValue = props.getProperty(NPE_OU_PROPERTY, ""); + if (npeOUsValue.isBlank()) { + log.warn("NPE OUs list property {} not set in {}", NPE_OU_PROPERTY, propertiesFile); + } + } + + } catch (Exception e) { + // If we failed to load the properties file, throw an error. + log.error("Failed to load properties file {}", propertiesFile, e); + throw new RuntimeException("Failed to load properties file " + propertiesFile, e); + } + + // Throw an error if the subject DN is still blank. + if (subjectDnPatternValue.isBlank()) { + throw new IllegalArgumentException("Failed to load valid subject DN pattern from property " + SUBJECT_DN_PATTERN_PROPERTY + + " from system or properties file " + propertiesFile); + } + + // Throw an error if the NPE OU list is still blank. + if (npeOUsValue.isBlank()) { + throw new IllegalArgumentException( + "Failed to load valid NPE OU list from property " + NPE_OU_PROPERTY + " from system or properties file " + propertiesFile); + } + } + + // Compile the subject DN pattern. + Pattern subjectDnPattern; + try { + subjectDnPattern = Pattern.compile(subjectDnPatternValue, Pattern.CASE_INSENSITIVE); + } catch (Throwable t) { + log.error("{} = '{}' could not be compiled", SUBJECT_DN_PATTERN_PROPERTY, subjectDnPatternValue, t); + throw new RuntimeException("Unable to compile subject DN pattern '" + subjectDnPatternValue + "'", t); + } + + // Parse the NPE OU list. + // @formatter:off + List npeOUs = Arrays.stream(npeOUsValue.split(",")) + .map(String::trim) + .map(String::toUpperCase) + .collect(Collectors.toList()); + // @formatter:on + + return new DnProperties(subjectDnPattern, npeOUs); + } + + /** + * Create a new {@link DnProperties} from the given subject DN pattern and NPE OUs. + * + * @param subjectDnPattern + * the subject DN patterns + * @param npeOUs + * the NPE OUs + */ + public DnProperties(Pattern subjectDnPattern, Collection npeOUs) { + this.subjectDnPattern = subjectDnPattern; + this.npeOUs = npeOUs.stream().map(String::trim).map(String::toUpperCase).collect(Collectors.toUnmodifiableSet()); + } + + /** + * Return the subject DN pattern + * + * @return the subject DN pattern + */ + public Pattern getSubjectDnPattern() { + return subjectDnPattern; + } + + /** + * Return the set of NPE OUs, in all uppercase. + * + * @return the NPE OUs + */ + public Set getNpeOUs() { + return npeOUs; + } + + @Override + public String toString() { + return new StringJoiner(", ", DnProperties.class.getSimpleName() + "[", "]").add("subjectDnPattern=" + subjectDnPattern).add("npeOUs=" + npeOUs) + .toString(); + } +} diff --git a/microservices/services/authorization/api/src/main/java/datawave/security/util/ProxiedEntityUtils.java b/commons/security/src/main/java/datawave/security/util/DnUtils.java similarity index 56% rename from microservices/services/authorization/api/src/main/java/datawave/security/util/ProxiedEntityUtils.java rename to commons/security/src/main/java/datawave/security/util/DnUtils.java index 630bb865c38..65a26863676 100644 --- a/microservices/services/authorization/api/src/main/java/datawave/security/util/ProxiedEntityUtils.java +++ b/commons/security/src/main/java/datawave/security/util/DnUtils.java @@ -2,8 +2,10 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -16,16 +18,23 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import datawave.security.authorization.SubjectIssuerDNPair; + /** - * Provides utility functions for splitting and extracting components from DNs. + * Utilities for parsing and manipulating user/server DNs. */ -public class ProxiedEntityUtils { +public final class DnUtils { - private static final Logger log = LoggerFactory.getLogger(ProxiedEntityUtils.class); + private static final Logger log = LoggerFactory.getLogger(DnUtils.class); + + /** + * Regex patterns to match against a comma followed by a space. + */ + private static final Pattern COMMA_SPACE_PATTERN = Pattern.compile(",[^ ]"); /** * Split the given string into proxied DNs and return them. It is expected that the DN string matches the format returned by - * {@link ProxiedEntityUtils#buildProxiedDN(String...)}, i,e, that the DNs should be in the format {@code "dn1..."}. + * {@link DnUtils#buildProxiedDN(String...)}, i,e, that the DNs should be in the format {@code "dn1..."}. * * @param proxiedDNs * the proxied DNs @@ -56,8 +65,8 @@ public static String[] splitProxiedDNs(String proxiedDNs, boolean allowDuplicate /** * Split the given string into proxied subject and issuer pairs and return them. It is expected that the DN string matches the format returned by - * {@link ProxiedEntityUtils#buildProxiedDN(String...)}, i,e, that the DNs should be in the format {@code "dn1..."}. If an uneven number of - * DNs greater than 1 is found, an {@link IllegalArgumentException} will be thrown. Only the first pairing for any distinct subject DNs will be retained. + * {@link DnUtils#buildProxiedDN(String...)}, i,e, that the DNs should be in the format {@code "dn1..."}. If an uneven number of DNs greater + * than 1 is found, an {@link IllegalArgumentException} will be thrown. Only the first pairing for any distinct subject DNs will be retained. * * @param proxiedDNs * the proxied DNs @@ -198,11 +207,6 @@ public static String[] getComponents(String dn, String rdnName) { return components.toArray(new String[0]); } - /** - * Regex patterns to match against a comma followed by a space. - */ - private static final Pattern COMMA_SPACE_PATTERN = Pattern.compile(",[^ ]"); - /** * Attempts to normalize a DN by taking it and reversing the components if it doesn't start with CN. Some systems require the DN components be in a specific * order, or that order reversed. We cannot arbitrarily reorder the components, however, e.g., such as by sorting them. The username returned will fall into @@ -238,10 +242,171 @@ public static String normalizeDN(String userName) { return normalizedUserName; } + /** + * Build and return a collection of normalized DNs built from the given DNs. The subject DN pattern will be used to verify that no proxied subject DNs were + * provided as issuer DNs. The subject DN and issuer DN will always be the last DNs in the list. + * + * @param subjectDN + * the subject DN + * @param issuerDN + * the issuer DN + * @param proxiedSubjectDNs + * the proxied subject DNs + * @param proxiedIssuerDNs + * the proxied issuer DNs + * @param subjectDnPattern + * the subject DN pattern + * @return a collection of normalized DNs + */ + public static List buildNormalizedDNList(String subjectDN, String issuerDN, String proxiedSubjectDNs, String proxiedIssuerDNs, + Pattern subjectDnPattern) { + List dnList = new ArrayList<>(); + if (proxiedSubjectDNs != null) { + if (proxiedIssuerDNs == null) { + throw new IllegalArgumentException("If proxied subject DNs are supplied, then issuer DNs must be supplied as well."); + } + + // Parse the subject and issuer DNs, verifying we have valid pairs. + String[] subjectDns = splitProxiedDNs(proxiedSubjectDNs, true); + String[] issuerDns = splitProxiedDNs(proxiedIssuerDNs, true); + if (subjectDns.length != issuerDns.length) + throw new IllegalArgumentException("Subject and issuer DN lists do not have the same number of entries: " + Arrays.toString(subjectDns) + " vs " + + Arrays.toString(issuerDns)); + + // Normalize each subject/issuer DN, and add them to the final DN list. + for (int i = 0; i < subjectDns.length; ++i) { + subjectDns[i] = normalizeDN(subjectDns[i]); + issuerDns[i] = normalizeDN(issuerDns[i]); + // Verify the subject and issuer DN are not the same. + if (issuerDns[i].equalsIgnoreCase(subjectDns[i])) { + throw new IllegalArgumentException("Subject DN " + issuerDns[i] + " was passed as an issuer DN."); + } + // Verify the issuer DN is not a subject DN. + if (subjectDnPattern.matcher(issuerDns[i]).find()) { + throw new IllegalArgumentException("It appears that a subject DN (" + issuerDns[i] + ") was passed as an issuer DN."); + } + + dnList.add(subjectDns[i]); + dnList.add(issuerDns[i]); + } + } + + // Normalize the non-proxied subject and issuer DN, and add them to the list. + subjectDN = normalizeDN(subjectDN); + issuerDN = normalizeDN(issuerDN); + dnList.add(subjectDN.replaceAll("(?])", "\\\\$1")); + dnList.add(issuerDN.replaceAll("(?])", "\\\\$1")); + return dnList; + } + + /** + * Build and return the result of {@link #buildNormalizedDNList(String, String, String, String, Pattern)}, where the string consist of each DN, and every DN + * after the first one is wrapped with {@code <} and {@code >}. + * + * @param subjectDN + * the subject DN + * @param issuerDN + * the issuer DN + * @param proxiedSubjectDNs + * the proxied subject DNs + * @param proxiedIssuerDNs + * the proxied issuer DNs + * @param subjectDnPattern + * the subject DN pattern + * @return the constructed string + * @see #buildNormalizedDNList(String, String, String, String, Pattern) + */ + public static String buildNormalizedProxyDN(String subjectDN, String issuerDN, String proxiedSubjectDNs, String proxiedIssuerDNs, + Pattern subjectDnPattern) { + StringBuilder sb = new StringBuilder(); + for (String escapedDN : buildNormalizedDNList(subjectDN, issuerDN, proxiedSubjectDNs, proxiedIssuerDNs, subjectDnPattern)) { + if (sb.length() == 0) + sb.append(escapedDN); + else + sb.append('<').append(escapedDN).append('>'); + } + return sb.toString(); + } + + /** + * Build and return a string consisting of normalized subject and issuer DNs from the given list, where the string consist of each DN, and every DN after + * the first one is wrapped with {@code <} and {@code >}. + * + * @param dns + * the DNs to normalize + * @return the constructed string + * @see #normalizeDN(String) + */ + public static String buildNormalizedProxyDN(List dns) { + StringBuilder sb = new StringBuilder(); + dns.forEach(dn -> { + if (sb.length() == 0) { + sb.append(normalizeDN(dn.subjectDN())); + } else { + sb.append('<').append(normalizeDN(dn.subjectDN())).append('>'); + } + sb.append('<').append(normalizeDN(dn.issuerDN())).append('>'); + }); + return sb.toString(); + } + + /** + * Return whether the given DN is considered a server DN. The DN will be considered a server DN if the OU of the DN is found in the given list of NPE OUs. + * + * @param dn + * the DN + * @param npeOUs + * the list of NPE OUs, expected to be all uppercase + * @return true if the DN is a server DN, or false otherwise + */ + public static boolean isServerDN(String dn, Collection npeOUs) { + String[] ouList = DnUtils.getOrganizationalUnits(dn); + return Arrays.stream(ouList).anyMatch((ou) -> npeOUs.contains(ou.toUpperCase())); + } + + /** + * Return the first DN that represents a user. A DN will be considered a user DN if it does not have an OU that is found in the given list of NPE OUs. + * + * @param dns + * the DNs, expected to be all subject DNs + * @param npeOUs + * the list of NPE OUs, expected to be all uppercase + * @return the first user DN, or null if no user DN was found + */ + public static String getUserDN(String[] dns, Collection npeOUs) { + return getUserDN(dns, false, npeOUs); + } + + /** + * Return the first subject DN that represents a user. A DN will be considered a user DN if it does not have an OU that is found in the given list of NPE + * OUs. + * + * @param dns + * the DNs + * @param containsIssuerDns + * whether the argument {@code dns} consists of subject/issuer pairs, with issuer DNs present in odd indexes + * @param npeOUs + * the list of NPE OUs, expected to be all uppercase + * @return the first user subject DN, or null if no user DN was found + */ + public static String getUserDN(String[] dns, boolean containsIssuerDns, Collection npeOUs) { + // If the array list contains subject/issuer pairs, verify it has an even length. + if (containsIssuerDns && (dns.length % 2) != 0) + throw new IllegalArgumentException("DNs array is not a subject/issuer DN list: " + Arrays.toString(dns)); + + // Find the first subject DN that is not a server DN. + for (int i = 0; i < dns.length; i += (containsIssuerDns) ? 2 : 1) { + String dn = dns[i]; + if (!isServerDN(dn, npeOUs)) + return dn; + } + return null; + } + /** * Do not allow this class to be instantiated. */ - private ProxiedEntityUtils() { + private DnUtils() { throw new UnsupportedOperationException(); } } diff --git a/commons/security/src/main/java/datawave/security/util/SecurityConstants.java b/commons/security/src/main/java/datawave/security/util/SecurityConstants.java new file mode 100644 index 00000000000..f10245ec7a4 --- /dev/null +++ b/commons/security/src/main/java/datawave/security/util/SecurityConstants.java @@ -0,0 +1,66 @@ +package datawave.security.util; + +/** + * Contains commonly used security-related constants. + */ +public final class SecurityConstants { + + /** + * An internal header used to store the start time of the HTTP request, as retrieved from the web container (e.g., Undertow). + */ + public static final String REQUEST_START_TIME_HEADER = "X-Internal-RequestStartTimeNanos"; + + /** + * An internal header used to store the time required to authenticate the user for the current request. + */ + public static final String REQUEST_LOGIN_TIME_HEADER = "X-Internal-RequestLoginTimeMillis"; + + /** + * The header used to store proxied subjects. + */ + public static final String PROXIED_ENTITIES_HEADER = "X-ProxiedEntitiesChain"; + + /** + * The header used to store proxied issuers. + */ + public static final String PROXIED_ISSUERS_HEADER = "X-ProxiedIssuersChain"; + + /** + * The default header used to store the subject DN when using trusted header authentication. + */ + public static final String DEFAULT_TRUSTED_SUBJECT_DN_HEADER = "X-SSL-ClientCert-Subject"; + + /** + * The default header used to store the issuer DN when using trusted header authentication. + */ + public static final String DEFAULT_TRUSTED_ISSUER_DN_HEADER = "X-SSL-ClientCert-Issuer"; + + /** + * The system property used to store whether JWT authentication is enabled. + */ + public static final String JWT_HEADER_AUTHENTICATION_SYSTEM_PROPERTY = "dw.jwt.header.authentication"; + + /** + * The system property used to store whether trusted header authentication is enabled. + */ + public static final String TRUSTED_HEADER_AUTHENTICATION_SYSTEM_PROPERTY = "dw.trusted.header.authentication"; + + /** + * The system property used to store the header that will be used to store the subject DN when using trusted header authentication. + */ + public static final String TRUSTED_SUBJECT_DN_HEADER_SYSTEM_PROPERTY = "dw.trusted.header.subjectDn"; + + /** + * The system property used to store the header that will be used to store the issuer DN when using trusted header authentication. + */ + public static final String TRUSTED_ISSUER_DN_HEADER_SYSTEM_PROPERTY = "dw.trusted.header.issuerDn"; + + /** + * The system property used to store trusted proxied entities. + */ + public static final String TRUSTED_PROXIED_ENTITIES_SYSTEM_PROPERTY = "dw.trusted.proxied.entities"; + + private SecurityConstants() { + throw new UnsupportedOperationException(); + } +} diff --git a/microservices/services/authorization/api/src/test/java/datawave/security/authorization/SubjectIssuerDNPairTest.java b/commons/security/src/test/java/datawave/security/authorization/SubjectIssuerDnPairTest.java similarity index 100% rename from microservices/services/authorization/api/src/test/java/datawave/security/authorization/SubjectIssuerDNPairTest.java rename to commons/security/src/test/java/datawave/security/authorization/SubjectIssuerDnPairTest.java diff --git a/commons/security/src/test/java/datawave/security/cert/SSLStoresImplTest.java b/commons/security/src/test/java/datawave/security/cert/SSLStoresImplTest.java new file mode 100644 index 00000000000..746df7d56ab --- /dev/null +++ b/commons/security/src/test/java/datawave/security/cert/SSLStoresImplTest.java @@ -0,0 +1,74 @@ +package datawave.security.cert; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.net.URL; + +import org.junit.jupiter.api.Test; + +class SSLStoresImplTest { + + private static final String JKS = "JKS"; + private static final String PKCS12 = "PKCS12"; + + private static final String jksKeyStorePath = "/datawave-server-keystore.jks"; + private static final String jksTrustStorePath = "/datawave-server-truststore.jks"; + private static final String pkcs12KeyStorePath = "/datawave-server-keystore.p12"; + private static final String pkcs12TrustStorePath = "/datawave-server-truststore.p12"; + + private static final String password = "ChangeIt"; + + @Test + void testJKSKeyStore() throws Exception { + SSLStoresImpl sslContext = SSLStoresImpl.builder().withKeystore(getResource(jksKeyStorePath), password, JKS).build(); + + assertNotNull(sslContext.getKeyStore(), "Keystore is null"); + assertNotNull(sslContext.getTrustStore(), "Truststore is null"); + assertSame(sslContext.getKeyStore(), sslContext.getTrustStore(), "Keystore and truststore are not the same instance"); + } + + @Test + void testJKSKeyStoreWithPKCS12TrustStore() throws Exception { + SSLStoresImpl sslContext = SSLStoresImpl.builder().withKeystore(getResource(jksKeyStorePath), password, JKS) + .withTruststore(getResource(pkcs12TrustStorePath), password, PKCS12).build(); + + assertNotNull(sslContext.getKeyStore(), "Keystore is null"); + assertEquals(JKS, sslContext.getKeyStore().getType(), "Keystore is incorrect type"); + assertNotNull(sslContext.getTrustStore(), "Truststore is null"); + assertEquals(PKCS12, sslContext.getTrustStore().getType(), "Truststore is incorrect type"); + assertNotSame(sslContext.getKeyStore(), sslContext.getTrustStore(), "Keystore and truststore are the same instance"); + } + + @Test + void testPKCS12KeyStore() throws Exception { + SSLStoresImpl sslContext = SSLStoresImpl.builder().withKeystore(getResource(pkcs12KeyStorePath), password, PKCS12).build(); + + assertNotNull(sslContext.getKeyStore(), "Keystore is null"); + assertNotNull(sslContext.getTrustStore(), "Truststore is null"); + assertSame(sslContext.getKeyStore(), sslContext.getTrustStore(), "Keystore and truststore are not the same instance"); + } + + @Test + void testPKCS12KeyStoreWithJKSTrustStore() throws Exception { + SSLStoresImpl sslContext = SSLStoresImpl.builder().withKeystore(getResource(pkcs12KeyStorePath), password, PKCS12) + .withTruststore(getResource(jksTrustStorePath), password, JKS).build(); + + assertNotNull(sslContext.getKeyStore(), "Keystore is null"); + assertEquals(PKCS12, sslContext.getKeyStore().getType(), "Keystore is incorrect type"); + assertNotNull(sslContext.getTrustStore(), "Truststore is null"); + assertEquals(JKS, sslContext.getTrustStore().getType(), "Truststore is incorrect type"); + assertNotSame(sslContext.getKeyStore(), sslContext.getTrustStore(), "Keystore and truststore are the same instance"); + } + + private String getResource(String resource) { + URL url = getClass().getResource(resource); + if (url != null) { + return url.toExternalForm(); + } else { + throw new NullPointerException("Could not load resource " + resource); + } + } +} diff --git a/commons/security/src/test/java/datawave/security/util/DnPropertiesTest.java b/commons/security/src/test/java/datawave/security/util/DnPropertiesTest.java new file mode 100644 index 00000000000..960ccb84d8e --- /dev/null +++ b/commons/security/src/test/java/datawave/security/util/DnPropertiesTest.java @@ -0,0 +1,162 @@ +package datawave.security.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Set; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.ClearSystemProperty; +import org.junitpioneer.jupiter.SetSystemProperty; + +import com.google.common.collect.Sets; + +/** + * Tests for {@link DnProperties}. + */ +class DnPropertiesTest { + + /** + * Verify that {@link DnProperties#getDefaultInstance()} will load properties from the properties file {@value DnProperties#DEFAULT_PROPERTIES_FILE}. + */ + @ClearSystemProperty(key = DnProperties.SUBJECT_DN_PATTERN_PROPERTY) + @ClearSystemProperty(key = DnProperties.NPE_OU_PROPERTY) + @Test + void testGetDefaultInstance() { + Pattern expectedSubjectDnPattern = Pattern.compile("(?:^|,)\\s*OU\\s*=\\s*My Department\\s*(?:,|$)", Pattern.CASE_INSENSITIVE); + Set expectedNpeOUs = Sets.newHashSet("IAMNOTAPERSON", "NPE", "STILLNOTAPERSON"); + + DnProperties actual = DnProperties.getDefaultInstance(); + assertTrue(arePatternsEqual(expectedSubjectDnPattern, actual.getSubjectDnPattern())); + assertEquals(expectedNpeOUs, actual.getNpeOUs()); + } + + /** + * Verify that {@link DnProperties#createInstanceFromProperties(String)} is able to load properties from a properties file when no system properties are + * configured. + */ + @ClearSystemProperty(key = DnProperties.SUBJECT_DN_PATTERN_PROPERTY) + @ClearSystemProperty(key = DnProperties.NPE_OU_PROPERTY) + @Test + void testCreatingInstanceFromPropertiesFile() { + Pattern expectedSubjectDnPattern = Pattern.compile("(?:^|,)\\s*OU\\s*=\\s*My Department\\s*(?:,|$)", Pattern.CASE_INSENSITIVE); + Set expectedNpeOUs = Sets.newHashSet("IAMNOTAPERSON", "NPE", "STILLNOTAPERSON"); + + DnProperties actual = DnProperties.createInstanceFromProperties("dnutils.properties"); + assertTrue(arePatternsEqual(expectedSubjectDnPattern, actual.getSubjectDnPattern())); + assertEquals(expectedNpeOUs, actual.getNpeOUs()); + } + + /** + * Verify that {@link DnProperties#createInstanceFromProperties(String)} is able to load properties from system properties when no properties file exists. + */ + @SetSystemProperty(key = DnProperties.SUBJECT_DN_PATTERN_PROPERTY, value = "(?:^|,)\\s*OU\\s*=\\s*My Department\\s*(?:,|$)") + @SetSystemProperty(key = DnProperties.NPE_OU_PROPERTY, value = "iamnotaperson,npe,stillnotaperson") + @Test + void testCreatingInstanceFromSystemProperties() { + Pattern expectedSubjectDnPattern = Pattern.compile("(?:^|,)\\s*OU\\s*=\\s*My Department\\s*(?:,|$)", Pattern.CASE_INSENSITIVE); + Set expectedNpeOUs = Sets.newHashSet("IAMNOTAPERSON", "NPE", "STILLNOTAPERSON"); + + DnProperties actual = DnProperties.createInstanceFromProperties("nonexistent.properties"); + assertTrue(arePatternsEqual(expectedSubjectDnPattern, actual.getSubjectDnPattern())); + assertEquals(expectedNpeOUs, actual.getNpeOUs()); + } + + /** + * Verify that {@link DnProperties#createInstanceFromProperties(String)} will prioritize system properties over the properties file. + */ + @SetSystemProperty(key = DnProperties.SUBJECT_DN_PATTERN_PROPERTY, value = "(?:^|,)\\s*OU\\s*=\\s*My Other Department\\s*(?:,|$)") + @SetSystemProperty(key = DnProperties.NPE_OU_PROPERTY, value = "iamnotaperson") + @Test + void testSystemPropertiesOverridePropertiesFile() { + Pattern expectedSubjectDnPattern = Pattern.compile("(?:^|,)\\s*OU\\s*=\\s*My Other Department\\s*(?:,|$)", Pattern.CASE_INSENSITIVE); + Set expectedNpeOUs = Sets.newHashSet("IAMNOTAPERSON"); + + DnProperties actual = DnProperties.createInstanceFromProperties("dnutils.properties"); + assertTrue(arePatternsEqual(expectedSubjectDnPattern, actual.getSubjectDnPattern())); + assertEquals(expectedNpeOUs, actual.getNpeOUs()); + } + + /** + * Verify an exception is thrown if the properties file cannot be loaded and either a subject DN pattern or NPE OU list were not specified via system + * properties. + */ + @SetSystemProperty(key = DnProperties.SUBJECT_DN_PATTERN_PROPERTY, value = " ") + @SetSystemProperty(key = DnProperties.NPE_OU_PROPERTY, value = " ") + @Test + void testNonexistentPropertiesFileThrowsException() { + Throwable throwable = assertThrows(RuntimeException.class, () -> DnProperties.createInstanceFromProperties("nonexistent.properties")); + assertEquals("Failed to load properties file nonexistent.properties", throwable.getMessage()); + } + + /** + * Verify an exception is not thrown for a non-existent properties file if a subject DN pattern and NPE OU list were provided via system properties. + */ + @SetSystemProperty(key = DnProperties.SUBJECT_DN_PATTERN_PROPERTY, value = "(?:^|,)\\s*OU\\s*=\\s*My Other Department\\s*(?:,|$)") + @SetSystemProperty(key = DnProperties.NPE_OU_PROPERTY, value = "iamnotaperson") + @Test + void testExceptionNotThrownForNonExistentPropertiesFileWhenSystemPropertiesSet() { + Pattern expectedSubjectDnPattern = Pattern.compile("(?:^|,)\\s*OU\\s*=\\s*My Other Department\\s*(?:,|$)", Pattern.CASE_INSENSITIVE); + Set expectedNpeOUs = Sets.newHashSet("IAMNOTAPERSON"); + + DnProperties actual = DnProperties.createInstanceFromProperties("dnutils.properties"); + assertTrue(arePatternsEqual(expectedSubjectDnPattern, actual.getSubjectDnPattern())); + assertEquals(expectedNpeOUs, actual.getNpeOUs()); + } + + /** + * Verify an exception is thrown by {@link DnProperties#createInstanceFromProperties(String)} when no valid subject DN pattern can be loaded. + */ + @SetSystemProperty(key = DnProperties.SUBJECT_DN_PATTERN_PROPERTY, value = " ") + @SetSystemProperty(key = DnProperties.NPE_OU_PROPERTY, value = "iamnotaperson,npe,stillnotaperson") + @Test + void testNoValidSubjectDnPattern() { + Throwable thrown = assertThrows(IllegalArgumentException.class, () -> DnProperties.createInstanceFromProperties("dnutils_blank.properties")); + assertEquals("Failed to load valid subject DN pattern from property subject.dn.pattern from system or properties file dnutils_blank.properties", + thrown.getMessage()); + } + + /** + * Verify an exception is thrown by {@link DnProperties#createInstanceFromProperties(String)} when no valid NPE OUs can be loaded. + */ + @SetSystemProperty(key = DnProperties.SUBJECT_DN_PATTERN_PROPERTY, value = "(?:^|,)\\s*OU\\s*=\\s*My Department\\s*(?:,|$)") + @SetSystemProperty(key = DnProperties.NPE_OU_PROPERTY, value = " ") + @Test + void testNoValidNpeOUs() { + Throwable thrown = assertThrows(IllegalArgumentException.class, () -> DnProperties.createInstanceFromProperties("dnutils_blank.properties")); + assertEquals("Failed to load valid NPE OU list from property npe.ou.entries from system or properties file dnutils_blank.properties", + thrown.getMessage()); + } + + /** + * Verify an exception is thrown by {@link DnProperties#createInstanceFromProperties(String)} when the subject DN pattern cannot be compiled. + */ + @SetSystemProperty(key = DnProperties.SUBJECT_DN_PATTERN_PROPERTY, value = "([sda") + @SetSystemProperty(key = DnProperties.NPE_OU_PROPERTY, value = "iamnotaperson,npe,stillnotaperson") + @Test + void givenUncompilableSubjectDnPattern() { + Throwable thrown = assertThrows(RuntimeException.class, () -> DnProperties.createInstanceFromProperties("nonexistent.properties")); + assertEquals("Unable to compile subject DN pattern '([sda'", thrown.getMessage()); + assertInstanceOf(PatternSyntaxException.class, thrown.getCause()); + } + + /** + * Return whether the given patterns have the same pattern and flags. + * + * @param pattern1 + * the first pattern + * @param pattern2 + * the second pattern + * @return true if the patterns are equal, or false otherwise + */ + private boolean arePatternsEqual(Pattern pattern1, Pattern pattern2) { + if (pattern1 == pattern2) { + return true; + } + return pattern1.pattern().equals(pattern2.pattern()) && pattern1.flags() == pattern2.flags(); + } +} diff --git a/commons/security/src/test/java/datawave/security/util/DnUtilsTest.java b/commons/security/src/test/java/datawave/security/util/DnUtilsTest.java new file mode 100644 index 00000000000..71490c794e9 --- /dev/null +++ b/commons/security/src/test/java/datawave/security/util/DnUtilsTest.java @@ -0,0 +1,370 @@ +package datawave.security.util; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; + +import com.google.common.collect.Lists; + +import datawave.security.authorization.SubjectIssuerDNPair; + +class DnUtilsTest { + + private static final Pattern subjectDnPattern = Pattern.compile("(?:^|,)\\s*OU\\s*=\\s*My Department\\s*(?:,|$)", Pattern.CASE_INSENSITIVE); + private static final List npeOUs = List.of("IAMNOTAPERSON", "NPE", "STILLNOTAPERSON"); + + /** + * Tests for {@link DnUtils#splitProxiedDNs(String, boolean)}. + */ + @Test + void testSplitProxiedDNs() { + // Verify that a single DN results in an array with the DN. + assertArrayEquals(new String[] {"cn=john q. doe, c=us, o=my org, ou=my dept"}, + DnUtils.splitProxiedDNs("cn=john q. doe, c=us, o=my org, ou=my dept", true)); + + // Verify that a single DN with escaped < and > characters results in an array with the DN. + assertArrayEquals(new String[] {"cn=john q. doe, c=\\, o=my org, ou=\\"}, + DnUtils.splitProxiedDNs("cn=john q. doe, c=\\, o=my org, ou=\\", true)); + + // Verify that multiple DNs result in an array with the DNs. + assertArrayEquals(new String[] {"cn=john q. doe, c=\\, o=my org, ou=\\", "cn=server1, c=us, o=my org, ou=my dept"}, + DnUtils.splitProxiedDNs("cn=john q. doe, c=\\, o=my org, ou=\\", true)); + + // Verify that duplicate DNs are retained when specified to allow duplicates. + // @formatter:off + assertArrayEquals(new String[] {"cn=john q. doe, c=\\, o=my org, ou=\\", + "cn=server1, c=us, o=my org, ou=my dept", + "cn=server1, c=us, o=my org, ou=my dept"}, + DnUtils.splitProxiedDNs("cn=john q. doe, c=\\, o=my org, ou=\\", true)); + // @formatter:on + + // Verify that only the first instance of a duplicate DN is retained when duplicates are not allowed. + // Verify that duplicate DNs are retained when specified to allow duplicates. + // @formatter:off + assertArrayEquals(new String[] {"cn=john q. doe, c=\\, o=my org, ou=\\", + "cn=server1, c=us, o=my org, ou=my dept", + "cn=server2, c=us, o=my org, ou=my dept"}, + DnUtils.splitProxiedDNs("cn=john q. doe, c=\\, o=my org, ou=\\", false)); + // @formatter:on + + // Verify that any blank DNs are pruned. + assertArrayEquals(new String[] {"cn=john q. doe, c=\\, o=my org, ou=\\", "cn=server1, c=us, o=my org, ou=my dept"}, + DnUtils.splitProxiedDNs("cn=john q. doe, c=\\, o=my org, ou=\\< >", true)); + + } + + /** + * Tests for {@link DnUtils#splitProxiedSubjectIssuerDNs(String)}. + */ + @Test + void testSplitProxiedSubjectIssuerDNs() { + // Verify that a single DN results in an array with the DN. + assertArrayEquals(new String[] {"cn=john q. doe, c=us, o=my org, ou=my dept"}, + DnUtils.splitProxiedSubjectIssuerDNs("cn=john q. doe, c=us, o=my org, ou=my dept")); + + // Verify that a single DN with escaped < and > characters results in an array with the DN. + assertArrayEquals(new String[] {"cn=john q. doe, c=us, o=my org, ou=\\"}, + DnUtils.splitProxiedSubjectIssuerDNs("cn=john q. doe, c=us, o=my org, ou=\\")); + + // Verify that an uneven number of DNs greater than one results in an exception. + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> DnUtils.splitProxiedSubjectIssuerDNs("cn=subject1")); + assertEquals("Invalid proxied DNs list does not have entries in pairs.", exception.getMessage()); + + // Verify that only the first subject-issuer pair for any unique subject DN is retained. + assertArrayEquals(new String[] {"cn=subject1", "cn=issuer1"}, DnUtils.splitProxiedSubjectIssuerDNs("cn=subject1")); + + // Verify that multiple subject-issuer pairs are parsed correctly. + assertArrayEquals(new String[] {"cn=subject1", "cn=issuer1", "cn=subject2", "cn=issuer2"}, + DnUtils.splitProxiedSubjectIssuerDNs("cn=subject1")); + + // Verify that any blank DNs are pruned. + assertArrayEquals(new String[] {"cn=subject1", "cn=issuer1", "cn=subject2", "cn=issuer2"}, + DnUtils.splitProxiedSubjectIssuerDNs("cn=subject1< >< >")); + + } + + /** + * Tests for {@link DnUtils#buildProxiedDN(String...)}. + */ + @Test + void testBuildProxiedDN() { + // Verify that a blank string results in the original string. + assertEquals(" ", DnUtils.buildProxiedDN(" ")); + + // Verify that a single DN with no arrows results in the original DN. + assertEquals("cn=john q. doe, c=us, o=my org, ou=my dept", DnUtils.buildProxiedDN("cn=john q. doe, c=us, o=my org, ou=my dept")); + + // Verify that a single DN with arrows results in the original DN with the arrows escaped. + assertEquals("cn=john q. doe, c=\\, o=my org, ou=\\", DnUtils.buildProxiedDN("cn=john q. doe, c=, o=my org, ou=")); + + // Verify that multiple DNs, some with arrows, result in the DNs concatenated, wrapped by arrows, with original arrows escaped. + // @formatter:off + assertEquals("cn=john q. doe, c=us, o=my org, ou=my dept, o=my org, ou=\\>", + DnUtils.buildProxiedDN("cn=john q. doe, c=us, o=my org, ou=my dept", + "cn=server2, c=\\, o=my org, ou=\\", + "cn=server1, c=us, o=my org, ou=my dept")); + // @formatter:on + } + + /** + * Tests for {@link DnUtils#getCommonName(String)}. + */ + @Test + void testGetCommonName() { + // Verify that a blank string results in a null CN. + assertNull(DnUtils.getCommonName(" ")); + + // Verify that a non-DN results in a null CN. + assertNull(DnUtils.getCommonName("S-1-1-0")); + + // Verify that a DN with no CN results in a null CN. + assertNull(DnUtils.getCommonName("c=us, o=my org, ou=my dept")); + + // Verify that a DN with a single CN returns the value of the CN. + assertEquals("john q. doe", DnUtils.getCommonName("cn=john q. doe, c=us, o=my org, ou=my dept")); + + // Verify that a DN with multiple CNs returns the value of the last CN. + assertEquals("john q. doe", DnUtils.getCommonName("cn=johnny q. doe, cn=johnathan q. doe, cn=john q. doe, c=us, o=my org, ou=my dept")); + } + + /** + * Tests for {@link DnUtils#getOrganizationalUnits(String)}. + */ + @Test + void testGetOrganizationalUnits() { + // Verify that a blank DN results in an empty array. + assertEquals(0, DnUtils.getOrganizationalUnits(" ").length); + + // Verify that a DN with no matching OU results in an empty array. + assertEquals(0, DnUtils.getOrganizationalUnits("cn=john q. doe, c=us, o=my org").length); + + // Verify that a DN with a single OU results in an array with a single element. + assertArrayEquals(new String[] {"my dept"}, DnUtils.getOrganizationalUnits("cn=john q. doe, c=us, o=my org, ou=my dept")); + + // Verify that a DN with a multiple OUs results in an array with multiple elements. + assertArrayEquals(new String[] {"my subsidiary", "my dept"}, + DnUtils.getOrganizationalUnits("cn=john q. doe, c=us, o=my org, ou=my dept, ou=my subsidiary")); + + // Verify that DN that cannot be parsed results in an empty array. + assertEquals(0, DnUtils.getOrganizationalUnits("S-1-1-0").length); + } + + /** + * Tests for {@link DnUtils#getShortName(String)}. + */ + @Test + public void testGetShortName() { + // Verify that a blank string results in empty string. + assertEquals("", DnUtils.getShortName(" ")); + + // Verify that a non-DN results in the last text portion returned. + assertEquals("apples", DnUtils.getShortName("pears to apples")); + + // Verify that a DN with no CN results in the last text portion returned. + assertEquals("dept", DnUtils.getShortName("c=us, o=my org, ou=my dept")); + + // Verify that a DN with a single CN results in the last text portion of the CN. + assertEquals("doe", DnUtils.getShortName("cn=john q. doe, c=us, o=my org, ou=my dept")); + + // Verify that a DN with multiple CNs results in the last text portion of the last CN. + assertEquals("hart", DnUtils.getShortName("cn=johnny q. doe, cn=jonathan q. buck, cn=john q. hart, c=us, o=my org, ou=my dept")); + } + + /** + * Tests for {@link DnUtils#getComponents(String, String)}. + */ + @Test + public void testGetComponents() { + // Verify that a blank DN results in an empty array. + assertEquals(0, DnUtils.getComponents(" ", "cn").length); + + // Verify that a blank component name results in an empty array. + assertEquals(0, DnUtils.getComponents("cn=john q. doe, c=us, o=my org, ou=my dept", " ").length); + + // Verify that a DN with no matching component results in an empty array. + assertEquals(0, DnUtils.getComponents("cn=john q. doe, c=us, o=my org, ou=my dept", "dc").length); + + // Verify that a DN with a single-value matching component results in an array with a single element. + assertArrayEquals(new String[] {"my org"}, DnUtils.getComponents("cn=john q. doe, c=us, o=my org, ou=my dept", "o")); + + // Verify that a DN with a multi-value matching component results in an array with multiple elements. + assertArrayEquals(new String[] {"com", "example"}, DnUtils.getComponents("cn=john q. doe, c=us, o=my org, ou=my dept, dc=example, dc=com", "dc")); + + // Verify that component name matching is case-insensitive. + assertArrayEquals(new String[] {"my org"}, DnUtils.getComponents("cn=john q. doe, c=us, o=my org, ou=my dept", "O")); + + // Verify that DN that cannot be parsed results in an empty array. + assertEquals(0, DnUtils.getComponents("S-1-1-0", "cn").length); + } + + /** + * Tests for {@link DnUtils#normalizeDN(String)}. + */ + @Test + public void testNormalizedDN() { + // Verify the DN is trimmed of whitespace and cast to lowercase. + assertEquals("c=us, o=my org, cn=john q. doe, ou=my dept", DnUtils.normalizeDN(" C=US, O=My Org, CN=John Q. Doe, OU=My Dept ")); + + // Verify that if the last RDN is the CN, that the RDNs are reversed. + assertEquals("cn=john q. doe, ou=my dept, o=my org, c=us", DnUtils.normalizeDN("C=US, O=My Org, OU=My Dept, CN=John Q. Doe")); + + // Verify that the components are not reordered if the CN is already in the first position. + assertEquals("cn=john q. doe, c=us, o=my org, ou=my dept", DnUtils.normalizeDN("CN=John Q. Doe, C=US, O=My Org, OU=My Dept")); + + // Verify a string that cannot be parsed as a DN, e.g., a sid, is returned in its original form, trimmed and in lowercase. + assertEquals("s-1-1-0", DnUtils.normalizeDN(" S-1-1-0 ")); + } + + /** + * Tests for {@link DnUtils#buildNormalizedDNList(String, String, String, String, Pattern)}. + */ + @Test + public void testBuildNormalizedDNList() { + // Verify that given no proxied subject or issuer DNs, the list consists of the normalized subject and issuer Dn. + List expected = Lists.newArrayList("sdn", "idn"); + List actual = DnUtils.buildNormalizedDNList("SDN", "IDN", null, null, subjectDnPattern); + assertEquals(expected, actual); + + // Verify that given a single proxied subject and issuer dn, the list contains them in the correct order. + expected = Lists.newArrayList("sdn2", "idn2", "sdn1", "idn1"); + actual = DnUtils.buildNormalizedDNList("SDN1", "IDN1", "SDN2", "IDN2", subjectDnPattern); + assertEquals(expected, actual); + + // Verify that given multiple proxied subject and issuer DNs, the list contains them in the correct order. + expected = Lists.newArrayList("sdn2", "idn2", "sdn3", "idn3", "sdn1", "idn1"); + actual = DnUtils.buildNormalizedDNList("SDN1", "IDN1", "SDN2", "IDN2", subjectDnPattern); + assertEquals(expected, actual); + + // Verify that an exception is thrown if proxied subject DNs are given, but proxied issuer DNs are not. + Throwable throwable = assertThrows(IllegalArgumentException.class, () -> DnUtils.buildNormalizedDNList("SDN1", "IDN1", "SDN2", null, subjectDnPattern)); + assertEquals("If proxied subject DNs are supplied, then issuer DNs must be supplied as well.", throwable.getMessage()); + + // Verify that an exception is thrown if an unequal number of subject DNs and issuer DNs were supplied. + throwable = assertThrows(IllegalArgumentException.class, () -> DnUtils.buildNormalizedDNList("SDN1", "IDN2", "SDN2", "IDN2", subjectDnPattern)); + assertEquals("Subject and issuer DN lists do not have the same number of entries: [SDN2, SDN3] vs [IDN2]", throwable.getMessage()); + + // Verify that an exception is thrown if a proxied subject DN is equal to its issuer DN. + throwable = assertThrows(IllegalArgumentException.class, () -> DnUtils.buildNormalizedDNList("SDN1", "IDN2", "SDN2", "SDN2", subjectDnPattern)); + assertEquals("Subject DN sdn2 was passed as an issuer DN.", throwable.getMessage()); + + // Verify that an exception is thrown if a proxied issuer DN matches the subject DN pattern. + throwable = assertThrows(IllegalArgumentException.class, + () -> DnUtils.buildNormalizedDNList("SDN1", "IDN2", "SDN2", "CN=foo,OU=My Department", subjectDnPattern)); + assertEquals("It appears that a subject DN (cn=foo, ou=my department) was passed as an issuer DN.", throwable.getMessage()); + } + + /** + * Tests for {@link DnUtils#buildNormalizedProxyDN(String, String, String, String, Pattern)}. + */ + @Test + public void testBuildNormalizedProxyDNGivenStringArgs() { + // Verify that given no proxied subject or issuer DN, string consists of the normalized subject and issuer DN. + String expected = "sdn"; + String actual = DnUtils.buildNormalizedProxyDN("SDN", "IDN", null, null, subjectDnPattern); + assertEquals(expected, actual); + + // Verify that given a single proxied subject and issuer dn, the string contains them in the correct order. + expected = "sdn2"; + actual = DnUtils.buildNormalizedProxyDN("SDN1", "IDN1", "SDN2", "IDN2", subjectDnPattern); + assertEquals(expected, actual); + + // Verify that given multiple proxied subject and issuer DNs, the string contains them in the correct order. + expected = "sdn2"; + actual = DnUtils.buildNormalizedProxyDN("SDN1", "IDN1", "SDN2", "IDN2", subjectDnPattern); + assertEquals(expected, actual); + + // Verify that an exception is thrown if proxied subject DNs are given, but proxied issuer DNs are not. + Throwable throwable = assertThrows(IllegalArgumentException.class, + () -> DnUtils.buildNormalizedProxyDN("SDN1", "IDN1", "SDN2", null, subjectDnPattern)); + assertEquals("If proxied subject DNs are supplied, then issuer DNs must be supplied as well.", throwable.getMessage()); + + // Verify that an exception is thrown if an unequal number of subject DNs and issuer DNs were supplied. + throwable = assertThrows(IllegalArgumentException.class, () -> DnUtils.buildNormalizedProxyDN("SDN1", "IDN2", "SDN2", "IDN2", subjectDnPattern)); + assertEquals("Subject and issuer DN lists do not have the same number of entries: [SDN2, SDN3] vs [IDN2]", throwable.getMessage()); + + // Verify that an exception is thrown if a proxied subject DN is equal to its issuer DN. + throwable = assertThrows(IllegalArgumentException.class, () -> DnUtils.buildNormalizedProxyDN("SDN1", "IDN2", "SDN2", "SDN2", subjectDnPattern)); + assertEquals("Subject DN sdn2 was passed as an issuer DN.", throwable.getMessage()); + + // Verify that an exception is thrown if a proxied issuer DN matches the subject DN pattern. + throwable = assertThrows(IllegalArgumentException.class, + () -> DnUtils.buildNormalizedProxyDN("SDN1", "IDN2", "SDN2", "CN=foo,OU=My Department", subjectDnPattern)); + assertEquals("It appears that a subject DN (cn=foo, ou=my department) was passed as an issuer DN.", throwable.getMessage()); + } + + /** + * Tests for {@link DnUtils#buildNormalizedProxyDN(List)}. + */ + @Test + public void testBuildNormalizedProxyDNGivenSubjectIssuerDNPairArgs() { + // Verify that given an empty list, a blank string is returned. + assertEquals("", DnUtils.buildNormalizedProxyDN(List.of())); + + // Verify that given a single SubjectIssuerDnPair, the string consists of the normalized subject and issuer DN. + assertEquals("sdn", DnUtils.buildNormalizedProxyDN(List.of(SubjectIssuerDNPair.of("SDN", "IDN")))); + + // Verify that given multiple SubjectIssuerDnPairs, the string consists of them in normalized form. + assertEquals("sdn2", DnUtils.buildNormalizedProxyDN( + List.of(SubjectIssuerDNPair.of("SDN2", "IDN2"), SubjectIssuerDNPair.of("SDN3", "IDN3"), SubjectIssuerDNPair.of("SDN1", "IDN1")))); + } + + /** + * Tests for {@link DnUtils#isServerDN(String, Collection)}. + */ + @Test + void testIsServerDN() { + // Verify that given a DN with an OU that is in the NPE OUs, true is returned. + assertTrue(DnUtils.isServerDN("cn=serverA, OU=npe, OU=Other OU", npeOUs)); + + // Verify that given a DN without a NPE OU, false is returned. + + // Verify that given a DN with an OU that is in the NPE OUs, true is returned. + assertFalse(DnUtils.isServerDN("cn=serverA, OU=Example, OU=Other OU", npeOUs)); + } + + /** + * Tests for {@link DnUtils#getUserDN(String[], Collection)} + */ + @Test + void testGetUserDNGivenSubjectDNsOnly() { + // Verify that given an array that does not contain a user DN, null is returned. + assertNull(DnUtils.getUserDN(new String[] {"CN=Server1,OU=Npe", "CN=Server2,OU=IAmNotAPerson"}, npeOUs)); + + // Verify that given an array with user DNs, the first user DN is returned. + assertEquals("CN=User1,OU=Example", DnUtils + .getUserDN(new String[] {"CN=Server1,OU=Npe", "CN=User1,OU=Example", "CN=Server2,OU=IAmNotAPerson", "CN=User2,OU=Example"}, npeOUs)); + } + + /** + * Tests for {@link DnUtils#getUserDN(String[], boolean, Collection)}. + */ + @Test + void testGetUserDNGivenSubjectAndIssuerDNs() { + // Verify that given an array that does not contain a user DN, null is returned. + assertNull(DnUtils.getUserDN(new String[] {"CN=Server1,OU=Npe", "CN=Server2,OU=IAmNotAPerson"}, false, npeOUs)); + + // Verify that given an array with user DNs, the first user DN is returned. + assertEquals("CN=User1,OU=Example", DnUtils.getUserDN(new String[] {"CN=Server1,OU=Npe", "CN=User1,OU=Example", "CN=User2,OU=Example"}, false, npeOUs)); + + // Verify that given an array with subject and issuer DNs, that only the subject DNs are examined. + assertNull(DnUtils.getUserDN(new String[] {"CN=Server1,OU=Npe", "CN=Issuer1,OU=Example", "CN=Server2,OU=IAmNotAPerson", "CN=Issuer2,OU=Example"}, true, + npeOUs)); + assertEquals("CN=User,OU=Example", DnUtils + .getUserDN(new String[] {"CN=Server1,OU=Npe", "CN=Issuer1,OU=Example", "CN=User,OU=Example", "CN=Issuer2,OU=Example"}, true, npeOUs)); + + // Verify that an exception is thrown if containsIssuerDns is true, but the DN array has an uneven length. + Throwable throwable = assertThrows(IllegalArgumentException.class, + () -> DnUtils.getUserDN(new String[] {"CN=Server1,OU=Npe", "CN=Issuer1,OU=Example", "CN=User,OU=Example"}, true, npeOUs)); + assertEquals("DNs array is not a subject/issuer DN list: [CN=Server1,OU=Npe, CN=Issuer1,OU=Example, CN=User,OU=Example]", throwable.getMessage()); + } +} diff --git a/commons/security/src/test/resources/datawave-server-keystore.jks b/commons/security/src/test/resources/datawave-server-keystore.jks new file mode 100644 index 00000000000..ca02e0bbb77 Binary files /dev/null and b/commons/security/src/test/resources/datawave-server-keystore.jks differ diff --git a/commons/security/src/test/resources/datawave-server-keystore.p12 b/commons/security/src/test/resources/datawave-server-keystore.p12 new file mode 100644 index 00000000000..b4ab0f3cb7f Binary files /dev/null and b/commons/security/src/test/resources/datawave-server-keystore.p12 differ diff --git a/commons/security/src/test/resources/datawave-server-truststore.jks b/commons/security/src/test/resources/datawave-server-truststore.jks new file mode 100644 index 00000000000..95feb046172 Binary files /dev/null and b/commons/security/src/test/resources/datawave-server-truststore.jks differ diff --git a/commons/security/src/test/resources/datawave-server-truststore.p12 b/commons/security/src/test/resources/datawave-server-truststore.p12 new file mode 100644 index 00000000000..ebd02da5fd5 Binary files /dev/null and b/commons/security/src/test/resources/datawave-server-truststore.p12 differ diff --git a/commons/security/src/test/resources/dnutils.properties b/commons/security/src/test/resources/dnutils.properties new file mode 100644 index 00000000000..ffb4017dff2 --- /dev/null +++ b/commons/security/src/test/resources/dnutils.properties @@ -0,0 +1,2 @@ +npe.ou.entries=iamnotaperson,npe,stillnotaperson +subject.dn.pattern=(?:^|,)\\s*OU\\s*=\\s*My Department\\s*(?:,|$) \ No newline at end of file diff --git a/commons/security/src/test/resources/dnutils_blank.properties b/commons/security/src/test/resources/dnutils_blank.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/contrib/datawave-quickstart/bin/common.sh b/contrib/datawave-quickstart/bin/common.sh index f83fd6bc0ac..9992ada95d4 100644 --- a/contrib/datawave-quickstart/bin/common.sh +++ b/contrib/datawave-quickstart/bin/common.sh @@ -341,7 +341,7 @@ function getDataWaveVersion() { function allUninstall() { # ${DW_CLOUD_DATA} will be removed by default. To keep it, use '--keep-data' flag - # Uninstalls all registered services. + # Uninstalls all registered services. if servicesAreRunning ; then echo "Stop running services before uninstalling!" allStatus diff --git a/contrib/datawave-quickstart/bin/services/datawave/bootstrap-web.sh b/contrib/datawave-quickstart/bin/services/datawave/bootstrap-web.sh index 3cb37f90ad4..c70adf44a05 100644 --- a/contrib/datawave-quickstart/bin/services/datawave/bootstrap-web.sh +++ b/contrib/datawave-quickstart/bin/services/datawave/bootstrap-web.sh @@ -3,7 +3,7 @@ DW_WILDFLY_VERSION="${DW_WILDFLY_VERSION:-$(mvn -q -f ${DW_CLOUD_HOME}/docker/pom.xml help:evaluate -DforceStdout -Dexpression=version.quickstart.wildfly | tail -1)}" DW_WILDFLY_DIST_SHA512_CHECKSUM="${DW_WILDFLY_DIST_SHA512_CHECKSUM:-$(mvn -q -f ${DW_CLOUD_HOME}/docker/pom.xml help:evaluate -DforceStdout -Dexpression=sha512.checksum.wildfly | tail -1)}" -DW_WILDFLY_DIST_URI="${DW_WILDFLY_DIST_URI:-https://download.jboss.org/wildfly/${DW_WILDFLY_VERSION}.Final/wildfly-${DW_WILDFLY_VERSION}.Final.tar.gz}" +DW_WILDFLY_DIST_URI="${DW_WILDFLY_DIST_URI:-https://github.com/wildfly/wildfly/releases/download/${DW_WILDFLY_VERSION}/wildfly-${DW_WILDFLY_VERSION}.tar.gz}" DW_WILDFLY_DIST="$( basename "${DW_WILDFLY_DIST_URI}" )" DW_WILDFLY_BASEDIR="wildfly-install" DW_WILDFLY_SYMLINK="wildfly" diff --git a/contrib/datawave-quickstart/docker/pom.xml b/contrib/datawave-quickstart/docker/pom.xml index 79b21a37c15..628daf498a4 100644 --- a/contrib/datawave-quickstart/docker/pom.xml +++ b/contrib/datawave-quickstart/docker/pom.xml @@ -20,7 +20,7 @@ 3a5e9ade2c84d4f8e0cb0551a9f6ea74a5cc2611afa141f4685f26431132c4cc60daeeedf22ab27c961ed7cd2df8b687e9fcaf00280093743e5c576fcdb53a52 09cda6943625bc8e4307deca7a4df76d676a51aca1b9a0171938b793521dfe1ab5970fdb9a490bab34c12a2230ffdaed2992bad16458169ac51b281be1ab6741 - fcbdff4bc275f478c3bf5f665a83e62468a920e58fcddeaa2710272dd0f1ce3154cdc371d5011763a6be24ae1a5e0bca0218cceea63543edb4b5cf22de60b485 + e1e75dfaa1d6483acce02f76c1fdcb0f1318acf3ace749e66ad2c47b2d817c036f44894a53e60e75c1d9f9c700613661789fad1517962060caf4a206c6506c18 4d85d6f7644d5f36d9c4d65e78bd662ab35ebe1380d762c24c12b98af029027eee453437c9245dbdf2b9beb77cd6b690b69e26f91cf9d11b0a183a979c73fa43 false diff --git a/core/annotation/pom.xml b/core/annotation/pom.xml index 86438ec92ff..454e97df3f7 100644 --- a/core/annotation/pom.xml +++ b/core/annotation/pom.xml @@ -39,6 +39,17 @@ gov.nsa.datawave datawave-core ${project.version} + + + org.apache.commons + commons-lang3 + + + + + org.apache.commons + commons-lang3 + ${version.commons-lang3} org.apache.accumulo diff --git a/core/base-rest-responses/pom.xml b/core/base-rest-responses/pom.xml index 6eb8da755fc..b658a35824e 100644 --- a/core/base-rest-responses/pom.xml +++ b/core/base-rest-responses/pom.xml @@ -59,6 +59,10 @@ com.sun.xml.bind jaxb-impl + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.core accumulo-utils diff --git a/core/base-rest-responses/src/main/java/datawave/security/DnList.java b/core/base-rest-responses/src/main/java/datawave/security/DnList.java index 99ba0061f54..245635a2f60 100644 --- a/core/base-rest-responses/src/main/java/datawave/security/DnList.java +++ b/core/base-rest-responses/src/main/java/datawave/security/DnList.java @@ -16,8 +16,8 @@ import org.apache.commons.text.StringEscapeUtils; -import datawave.microservice.security.util.DnUtils; import datawave.security.authorization.DatawaveUserInfo; +import datawave.security.util.DnUtils; import datawave.webservice.HtmlProvider; @XmlRootElement diff --git a/core/common-util/pom.xml b/core/common-util/pom.xml index 4024a08b2f7..28b9f9c1b43 100644 --- a/core/common-util/pom.xml +++ b/core/common-util/pom.xml @@ -8,7 +8,9 @@ datawave-core-common-util ${project.artifactId} - + + 3.17.0 + @@ -26,6 +28,11 @@ gov.nsa.datawave.microservice authorization-api + + org.apache.commons + commons-lang3 + ${version.commons-lang3} + org.apache.accumulo accumulo-core @@ -51,6 +58,10 @@ test-classes true src/test/resources + + *.jks + *.p12 + diff --git a/core/map-reduce/pom.xml b/core/map-reduce/pom.xml index 912c8152dd7..7d952658bf9 100644 --- a/core/map-reduce/pom.xml +++ b/core/map-reduce/pom.xml @@ -30,7 +30,12 @@ org.jboss.resteasy - resteasy-jaxrs + resteasy-core + provided + + + org.jboss.resteasy + resteasy-core-spi provided diff --git a/core/query/pom.xml b/core/query/pom.xml index 43aca8b82dd..98fcd67b1c4 100644 --- a/core/query/pom.xml +++ b/core/query/pom.xml @@ -10,6 +10,10 @@ ${project.artifactId} + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.core base-rest-responses diff --git a/core/utils/accumulo-utils/pom.xml b/core/utils/accumulo-utils/pom.xml index 40c0b4e3beb..412f493a6da 100644 --- a/core/utils/accumulo-utils/pom.xml +++ b/core/utils/accumulo-utils/pom.xml @@ -54,8 +54,9 @@ commons-collections - gov.nsa.datawave.microservice - authorization-api + gov.nsa.datawave.commons + datawave-commons-security + ${project.version} io.dropwizard.metrics diff --git a/core/utils/common-utils/src/main/java/datawave/microservice/security/util/DnUtils.java b/core/utils/common-utils/src/main/java/datawave/microservice/security/util/DnUtils.java deleted file mode 100644 index 45d54c83e5d..00000000000 --- a/core/utils/common-utils/src/main/java/datawave/microservice/security/util/DnUtils.java +++ /dev/null @@ -1,137 +0,0 @@ -package datawave.microservice.security.util; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.regex.Pattern; - -import datawave.security.authorization.SubjectIssuerDNPair; -import datawave.security.util.ProxiedEntityUtils; - -public class DnUtils { - - private final Pattern subjectDnPattern; - - /** Parsed NPE OU identifiers */ - private final List npeOuList; - - public DnUtils(Pattern subjectDnPattern, List npeOuList) { - this.subjectDnPattern = subjectDnPattern; - this.npeOuList = npeOuList; - } - - public static String[] splitProxiedDNs(String proxiedDNs, boolean allowDups) { - return ProxiedEntityUtils.splitProxiedDNs(proxiedDNs, allowDups); - } - - public static String[] splitProxiedSubjectIssuerDNs(String proxiedDNs) { - return ProxiedEntityUtils.splitProxiedSubjectIssuerDNs(proxiedDNs); - } - - public static String buildProxiedDN(String... dns) { - return ProxiedEntityUtils.buildProxiedDN(dns); - } - - public Collection buildNormalizedDNList(String subjectDN, String issuerDN, String proxiedSubjectDNs, String proxiedIssuerDNs) { - List dnList = new ArrayList<>(); - if (proxiedSubjectDNs != null) { - if (proxiedIssuerDNs == null) - throw new IllegalArgumentException("If proxied subject DNs are supplied, then issuer DNs must be supplied as well."); - String[] subjectDNarray = splitProxiedDNs(proxiedSubjectDNs, true); - String[] issuerDNarray = splitProxiedDNs(proxiedIssuerDNs, true); - if (subjectDNarray.length != issuerDNarray.length) - throw new IllegalArgumentException("Subject and issuer DN lists do not have the same number of entries: " + Arrays.toString(subjectDNarray) - + " vs " + Arrays.toString(issuerDNarray)); - for (int i = 0; i < subjectDNarray.length; ++i) { - subjectDNarray[i] = normalizeDN(subjectDNarray[i]); - issuerDNarray[i] = normalizeDN(issuerDNarray[i]); - dnList.add(subjectDNarray[i]); - dnList.add(issuerDNarray[i]); - if (issuerDNarray[i].equalsIgnoreCase(subjectDNarray[i])) - throw new IllegalArgumentException("Subject DN " + issuerDNarray[i] + " was passed as an issuer DN."); - if (subjectDnPattern.matcher(issuerDNarray[i]).find()) - throw new IllegalArgumentException("It appears that a subject DN (" + issuerDNarray[i] + ") was passed as an issuer DN."); - } - } - subjectDN = normalizeDN(subjectDN); - issuerDN = normalizeDN(issuerDN); - dnList.add(subjectDN.replaceAll("(?])", "\\\\$1")); - dnList.add(issuerDN.replaceAll("(?])", "\\\\$1")); - return dnList; - } - - public String buildNormalizedProxyDN(String subjectDN, String issuerDN, String proxiedSubjectDNs, String proxiedIssuerDNs) { - StringBuilder sb = new StringBuilder(); - for (String escapedDN : buildNormalizedDNList(subjectDN, issuerDN, proxiedSubjectDNs, proxiedIssuerDNs)) { - if (sb.length() == 0) - sb.append(escapedDN); - else - sb.append('<').append(escapedDN).append('>'); - } - return sb.toString(); - } - - public static String buildNormalizedProxyDN(List dns) { - StringBuilder sb = new StringBuilder(); - dns.stream().forEach(dn -> { - if (sb.length() == 0) { - sb.append(normalizeDN(dn.subjectDN())); - } else { - sb.append('<').append(normalizeDN(dn.subjectDN())).append('>'); - } - sb.append('<').append(normalizeDN(dn.issuerDN())).append('>'); - }); - return sb.toString(); - } - - public static String getCommonName(String dn) { - return ProxiedEntityUtils.getCommonName(dn); - } - - public static String[] getOrganizationalUnits(String dn) { - return ProxiedEntityUtils.getOrganizationalUnits(dn); - } - - public static String getShortName(String dn) { - return ProxiedEntityUtils.getShortName(dn); - } - - public boolean isServerDN(String dn) { - return isNPE(dn); - } - - public String getUserDN(String[] dns) { - return getUserDN(dns, false); - } - - public String getUserDN(String[] dns, boolean issuerDNs) { - if (issuerDNs && (dns.length % 2) != 0) - throw new IllegalArgumentException("DNs array is not a subject/issuer DN list: " + Arrays.toString(dns)); - - for (int i = 0; i < dns.length; i += (issuerDNs) ? 2 : 1) { - String dn = dns[i]; - if (!isServerDN(dn)) - return dn; - } - return null; - } - - public String[] getComponents(String dn, String componentName) { - return ProxiedEntityUtils.getComponents(dn, componentName); - } - - public static String normalizeDN(String userName) { - return ProxiedEntityUtils.normalizeDN(userName); - } - - private boolean isNPE(String dn) { - String[] ouList = ProxiedEntityUtils.getOrganizationalUnits(dn); - for (String ou : ouList) { - if (npeOuList.contains(ou.toUpperCase())) { - return true; - } - } - return false; - } -} diff --git a/core/utils/common-utils/src/test/java/datawave/microservice/security/util/DnUtilsTest.java b/core/utils/common-utils/src/test/java/datawave/microservice/security/util/DnUtilsTest.java deleted file mode 100644 index ef07365b1f6..00000000000 --- a/core/utils/common-utils/src/test/java/datawave/microservice/security/util/DnUtilsTest.java +++ /dev/null @@ -1,143 +0,0 @@ -package datawave.microservice.security.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.Arrays; -import java.util.Collection; -import java.util.regex.Pattern; - -import org.junit.jupiter.api.Test; - -import com.google.common.collect.Lists; - -public class DnUtilsTest { - - private DnUtils dnUtils = new DnUtils(Pattern.compile("(?:^|,)\\s*OU\\s*=\\s*My Department\\s*(?:,|$)", Pattern.CASE_INSENSITIVE), - Arrays.asList("iamnotaperson", "npe", "stillnotaperson")); - - @Test - public void testBuildNormalizedProxyDN() { - String expected = "sdn"; - String actual = dnUtils.buildNormalizedProxyDN("SDN", "IDN", null, null); - assertEquals(expected, actual); - - expected = "sdn2"; - actual = dnUtils.buildNormalizedProxyDN("SDN1", "IDN1", "SDN2", "IDN2"); - assertEquals(expected, actual); - - expected = "sdn2"; - actual = dnUtils.buildNormalizedProxyDN("SDN1", "IDN1", "SDN2", "IDN2"); - assertEquals(expected, actual); - - expected = "sdn2"; - actual = dnUtils.buildNormalizedProxyDN("SDN1", "IDN1", "", ""); - assertEquals(expected, actual); - } - - @Test - public void testBuildNormalizedDN() { - Collection expected = Lists.newArrayList("sdn", "idn"); - Collection actual = dnUtils.buildNormalizedDNList("SDN", "IDN", null, null); - assertEquals(expected, actual); - - expected = Lists.newArrayList("sdn2", "idn2", "sdn1", "idn1"); - actual = dnUtils.buildNormalizedDNList("SDN1", "IDN1", "SDN2", "IDN2"); - assertEquals(expected, actual); - - expected = Lists.newArrayList("sdn2", "idn2", "sdn3", "idn3", "sdn1", "idn1"); - actual = dnUtils.buildNormalizedDNList("SDN1", "IDN1", "SDN2", "IDN2"); - assertEquals(expected, actual); - - expected = Lists.newArrayList("sdn2", "idn2", "sdn3", "idn3", "sdn1", "idn1"); - actual = dnUtils.buildNormalizedDNList("SDN1", "IDN1", "", ""); - assertEquals(expected, actual); - } - - @Test - public void testGetUserDnFromArray() { - String userDnForTest = "snd1"; - String[] array = new String[] {userDnForTest, "idn"}; - String userDN = dnUtils.getUserDN(array); - assertEquals(userDnForTest, userDN); - } - - @Test - public void testTest() { - assertThrows(IllegalArgumentException.class, () -> { - String[] dns = new String[] {"sdn"}; - dnUtils.getUserDN(dns, true); - }); - } - - @Test - public void testBuildNormalizedProxyDNTooMissingIssuers() { - assertThrows(IllegalArgumentException.class, () -> { - dnUtils.buildNormalizedProxyDN("SDN", "IDN", "SDN2", null); - }); - } - - @Test - public void testBuildNormalizedProxyDNTooFewIssuers() { - assertThrows(IllegalArgumentException.class, () -> { - dnUtils.buildNormalizedProxyDN("SDN", "IDN", "SDN2", "IDN2"); - }); - } - - @Test - public void testBuildNormalizedProxyDNTooFewSubjects() { - assertThrows(IllegalArgumentException.class, () -> { - dnUtils.buildNormalizedProxyDN("SDN", "IDN", "SDN2", "IDN2"); - }); - } - - @Test - public void testBuildNormalizedProxyDNSubjectEqualsIssuer() { - assertThrows(IllegalArgumentException.class, () -> { - dnUtils.buildNormalizedProxyDN("SDN", "IDN", "SDN2", "SDN2"); - }); - } - - @Test - public void testBuildNormalizedProxyDNSubjectDNInIssuer() { - assertThrows(IllegalArgumentException.class, () -> { - dnUtils.buildNormalizedProxyDN("SDN", "IDN", "SDN2", "CN=foo,OU=My Department"); - }); - } - - @Test - public void testBuildNormalizedDNListTooMissingIssuers() { - assertThrows(IllegalArgumentException.class, () -> { - dnUtils.buildNormalizedDNList("SDN", "IDN", "SDN2", null); - }); - } - - @Test - public void testBuildNormalizedDNListTooFewIssuers() { - assertThrows(IllegalArgumentException.class, () -> { - dnUtils.buildNormalizedDNList("SDN", "IDN", "SDN2", "IDN2"); - }); - } - - @Test - public void testBuildNormalizedDNListTooFewSubjects() { - assertThrows(IllegalArgumentException.class, () -> { - dnUtils.buildNormalizedDNList("SDN", "IDN", "SDN2", "IDN2"); - }); - } - - @Test - public void testBuildNormalizedDNListSubjectEqualsIssuer() { - assertThrows(IllegalArgumentException.class, () -> { - dnUtils.buildNormalizedDNList("SDN", "IDN", "SDN2", "SDN2"); - }); - } - - @Test - public void testBuildNormalizedDNListSubjectDNInIssuer() { - assertThrows(IllegalArgumentException.class, () -> { - dnUtils.buildNormalizedDNList("SDN", "IDN", "SDN2", "CN=foo,OU=My Department"); - }); - } - -} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 7a014326b67..de419567db0 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -37,6 +37,8 @@ services: - "9443:8443" # web server debug port - "5011:8787" + # wildfly admin ui + - "9990:9990" extra_hosts: - "${DW_HOSTNAME}:${DW_HOST_IP}" - "${DW_HOST_FQDN}:${DW_HOST_IP}" diff --git a/docs/pom.xml b/docs/pom.xml index 1159b8746b6..8ee1f03b515 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -82,6 +82,11 @@ ${project.version} true + + gov.nsa.datawave.commons + datawave-commons-security + true + gov.nsa.datawave.webservices datawave-ws-accumulo @@ -149,6 +154,12 @@ ${project.version} true + + gov.nsa.datawave.webservices + datawave-ws-security-elytron + ${project.version} + true + org.codehaus.jackson jackson-jaxrs @@ -206,7 +217,12 @@ org.jboss.resteasy - resteasy-jaxrs + resteasy-core + provided + + + org.jboss.resteasy + resteasy-core-spi provided @@ -221,7 +237,7 @@ org.jboss.spec.javax.transaction - jboss-transaction-api_1.2_spec + jboss-transaction-api_1.3_spec provided @@ -235,16 +251,6 @@ 2.28.2 provided - - org.picketbox - picketbox - provided - - - org.picketbox - picketbox-infinispan - provided - org.projectlombok lombok diff --git a/microservices/microservice-parent/pom.xml b/microservices/microservice-parent/pom.xml index 55a8f5ee33c..247de00389b 100644 --- a/microservices/microservice-parent/pom.xml +++ b/microservices/microservice-parent/pom.xml @@ -53,6 +53,7 @@ UTF-8 1C + 7.40.0-3508-RC1 3.3.4 2.14.3 @@ -65,6 +66,11 @@ + + gov.nsa.datawave.commons + datawave-commons-security + ${version.datawave-commons-security} + diff --git a/microservices/microservice-service-parent/pom.xml b/microservices/microservice-service-parent/pom.xml index c3321d86f10..0b00a0acf23 100644 --- a/microservices/microservice-service-parent/pom.xml +++ b/microservices/microservice-service-parent/pom.xml @@ -4,7 +4,7 @@ gov.nsa.datawave.microservice datawave-microservice-parent - 4.0.10 + 4.0.11-3508-RC1 ../microservice-parent datawave-microservice-service-parent diff --git a/microservices/services/authorization/api/pom.xml b/microservices/services/authorization/api/pom.xml index a1769178b2a..4ed12d2c845 100644 --- a/microservices/services/authorization/api/pom.xml +++ b/microservices/services/authorization/api/pom.xml @@ -4,7 +4,7 @@ gov.nsa.datawave.microservice datawave-microservice-parent - 4.0.10 + 4.0.11-3508-RC1 ../../../microservice-parent/pom.xml authorization-api @@ -59,16 +59,6 @@ jjwt-api ${version.jjwt} - - io.jsonwebtoken - jjwt-impl - ${version.jjwt} - - - io.jsonwebtoken - jjwt-jackson - ${version.jjwt} - jakarta.xml.bind jakarta.xml.bind-api @@ -85,6 +75,18 @@ + + io.jsonwebtoken + jjwt-impl + ${version.jjwt} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${version.jjwt} + runtime + @@ -92,14 +94,6 @@ com.fasterxml.jackson.core jackson-annotations - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - com.google.guava guava @@ -108,14 +102,21 @@ com.sun.xml.bind jaxb-impl + + gov.nsa.datawave.commons + datawave-commons-security + ${version.datawave-commons-security} + io.jsonwebtoken jjwt-api + io.jsonwebtoken jjwt-impl + io.jsonwebtoken jjwt-jackson diff --git a/microservices/services/authorization/api/src/test/java/datawave/security/util/ProxiedEntityUtilsTest.java b/microservices/services/authorization/api/src/test/java/datawave/security/util/ProxiedEntityUtilsTest.java deleted file mode 100644 index 303fb78cff5..00000000000 --- a/microservices/services/authorization/api/src/test/java/datawave/security/util/ProxiedEntityUtilsTest.java +++ /dev/null @@ -1,220 +0,0 @@ -package datawave.security.util; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; - -/** - * Tests for {@link ProxiedEntityUtils}. - */ -public class ProxiedEntityUtilsTest { - - /** - * Tests for {@link ProxiedEntityUtils#splitProxiedDNs(String, boolean)}. - */ - @Test - void testSplitProxiedDNs() { - // Verify that a single DN results in an array with the DN. - assertArrayEquals(new String[] {"cn=john q. doe, c=us, o=my org, ou=my dept"}, - ProxiedEntityUtils.splitProxiedDNs("cn=john q. doe, c=us, o=my org, ou=my dept", true)); - - // Verify that a single DN with escaped < and > characters results in an array with the DN. - assertArrayEquals(new String[] {"cn=john q. doe, c=\\, o=my org, ou=\\"}, - ProxiedEntityUtils.splitProxiedDNs("cn=john q. doe, c=\\, o=my org, ou=\\", true)); - - // Verify that multiple DNs result in an array with the DNs. - assertArrayEquals(new String[] {"cn=john q. doe, c=\\, o=my org, ou=\\", "cn=server1, c=us, o=my org, ou=my dept"}, ProxiedEntityUtils - .splitProxiedDNs("cn=john q. doe, c=\\, o=my org, ou=\\", true)); - - // Verify that duplicate DNs are retained when specified to allow duplicates. - // @formatter:off - assertArrayEquals(new String[] {"cn=john q. doe, c=\\, o=my org, ou=\\", - "cn=server1, c=us, o=my org, ou=my dept", - "cn=server1, c=us, o=my org, ou=my dept"}, - ProxiedEntityUtils.splitProxiedDNs("cn=john q. doe, c=\\, o=my org, ou=\\", true)); - // @formatter:on - - // Verify that only the first instance of a duplicate DN is retained when duplicates are not allowed. - // Verify that duplicate DNs are retained when specified to allow duplicates. - // @formatter:off - assertArrayEquals(new String[] {"cn=john q. doe, c=\\, o=my org, ou=\\", - "cn=server1, c=us, o=my org, ou=my dept", - "cn=server2, c=us, o=my org, ou=my dept"}, - ProxiedEntityUtils.splitProxiedDNs("cn=john q. doe, c=\\, o=my org, ou=\\", false)); - // @formatter:on - - // Verify that any blank DNs are pruned. - assertArrayEquals(new String[] {"cn=john q. doe, c=\\, o=my org, ou=\\", "cn=server1, c=us, o=my org, ou=my dept"}, ProxiedEntityUtils - .splitProxiedDNs("cn=john q. doe, c=\\, o=my org, ou=\\< >", true)); - - } - - /** - * Tests for {@link ProxiedEntityUtils#splitProxiedSubjectIssuerDNs(String)}. - */ - @Test - void testSplitProxiedSubjectIssuerDNs() { - // Verify that a single DN results in an array with the DN. - assertArrayEquals(new String[] {"cn=john q. doe, c=us, o=my org, ou=my dept"}, - ProxiedEntityUtils.splitProxiedSubjectIssuerDNs("cn=john q. doe, c=us, o=my org, ou=my dept")); - - // Verify that a single DN with escaped < and > characters results in an array with the DN. - assertArrayEquals(new String[] {"cn=john q. doe, c=us, o=my org, ou=\\"}, - ProxiedEntityUtils.splitProxiedSubjectIssuerDNs("cn=john q. doe, c=us, o=my org, ou=\\")); - - // Verify that an uneven number of DNs greater than one results in an exception. - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> ProxiedEntityUtils.splitProxiedSubjectIssuerDNs("cn=subject1")); - assertEquals("Invalid proxied DNs list does not have entries in pairs.", exception.getMessage()); - - // Verify that only the first subject-issuer pair for any unique subject DN is retained. - assertArrayEquals(new String[] {"cn=subject1", "cn=issuer1"}, - ProxiedEntityUtils.splitProxiedSubjectIssuerDNs("cn=subject1")); - - // Verify that multiple subject-issuer pairs are parsed correctly. - assertArrayEquals(new String[] {"cn=subject1", "cn=issuer1", "cn=subject2", "cn=issuer2"}, - ProxiedEntityUtils.splitProxiedSubjectIssuerDNs("cn=subject1")); - - // Verify that any blank DNs are pruned. - assertArrayEquals(new String[] {"cn=subject1", "cn=issuer1", "cn=subject2", "cn=issuer2"}, - ProxiedEntityUtils.splitProxiedSubjectIssuerDNs("cn=subject1< >< >")); - - } - - /** - * Tests for {@link ProxiedEntityUtils#buildProxiedDN(String...)}. - */ - @Test - void testBuildProxiedDN() { - // Verify that a blank string results in the original string. - assertEquals(" ", ProxiedEntityUtils.buildProxiedDN(" ")); - - // Verify that a single DN with no arrows results in the original DN. - assertEquals("cn=john q. doe, c=us, o=my org, ou=my dept", ProxiedEntityUtils.buildProxiedDN("cn=john q. doe, c=us, o=my org, ou=my dept")); - - // Verify that a single DN with arrows results in the original DN with the arrows escaped. - assertEquals("cn=john q. doe, c=\\, o=my org, ou=\\", - ProxiedEntityUtils.buildProxiedDN("cn=john q. doe, c=, o=my org, ou=")); - - // Verify that multiple DNs, some with arrows, result in the DNs concatenated, wrapped by arrows, with original arrows escaped. - // @formatter:off - assertEquals("cn=john q. doe, c=us, o=my org, ou=my dept, o=my org, ou=\\>", - ProxiedEntityUtils.buildProxiedDN("cn=john q. doe, c=us, o=my org, ou=my dept", - "cn=server2, c=\\, o=my org, ou=\\", - "cn=server1, c=us, o=my org, ou=my dept")); - // @formatter:on - } - - /** - * Tests for {@link ProxiedEntityUtils#getCommonName(String)}. - */ - @Test - void testGetCommonName() { - // Verify that a blank string results in a null CN. - assertNull(ProxiedEntityUtils.getCommonName(" ")); - - // Verify that a non-DN results in a null CN. - assertNull(ProxiedEntityUtils.getCommonName("S-1-1-0")); - - // Verify that a DN with no CN results in a null CN. - assertNull(ProxiedEntityUtils.getCommonName("c=us, o=my org, ou=my dept")); - - // Verify that a DN with a single CN returns the value of the CN. - assertEquals("john q. doe", ProxiedEntityUtils.getCommonName("cn=john q. doe, c=us, o=my org, ou=my dept")); - - // Verify that a DN with multiple CNs returns the value of the last CN. - assertEquals("john q. doe", ProxiedEntityUtils.getCommonName("cn=johnny q. doe, cn=johnathan q. doe, cn=john q. doe, c=us, o=my org, ou=my dept")); - } - - /** - * Tests for {@link ProxiedEntityUtils#getOrganizationalUnits(String)}. - */ - @Test - void testGetOrganizationalUnits() { - // Verify that a blank DN results in an empty array. - assertEquals(0, ProxiedEntityUtils.getOrganizationalUnits(" ").length); - - // Verify that a DN with no matching OU results in an empty array. - assertEquals(0, ProxiedEntityUtils.getOrganizationalUnits("cn=john q. doe, c=us, o=my org").length); - - // Verify that a DN with a single OU results in an array with a single element. - assertArrayEquals(new String[] {"my dept"}, ProxiedEntityUtils.getOrganizationalUnits("cn=john q. doe, c=us, o=my org, ou=my dept")); - - // Verify that a DN with a multiple OUs results in an array with multiple elements. - assertArrayEquals(new String[] {"my subsidiary", "my dept"}, - ProxiedEntityUtils.getOrganizationalUnits("cn=john q. doe, c=us, o=my org, ou=my dept, ou=my subsidiary")); - - // Verify that DN that cannot be parsed results in an empty array. - assertEquals(0, ProxiedEntityUtils.getOrganizationalUnits("S-1-1-0").length); - } - - /** - * Tests for {@link ProxiedEntityUtils#getShortName(String)}. - */ - @Test - public void testGetShortName() { - // Verify that a blank string results in empty string. - assertEquals("", ProxiedEntityUtils.getShortName(" ")); - - // Verify that a non-DN results in the last text portion returned. - assertEquals("apples", ProxiedEntityUtils.getShortName("pears to apples")); - - // Verify that a DN with no CN results in the last text portion returned. - assertEquals("dept", ProxiedEntityUtils.getShortName("c=us, o=my org, ou=my dept")); - - // Verify that a DN with a single CN results in the last text portion of the CN. - assertEquals("doe", ProxiedEntityUtils.getShortName("cn=john q. doe, c=us, o=my org, ou=my dept")); - - // Verify that a DN with multiple CNs results in the last text portion of the last CN. - assertEquals("hart", ProxiedEntityUtils.getShortName("cn=johnny q. doe, cn=jonathan q. buck, cn=john q. hart, c=us, o=my org, ou=my dept")); - } - - /** - * Tests for {@link ProxiedEntityUtils#getComponents(String, String)}. - */ - @Test - public void testGetComponents() { - // Verify that a blank DN results in an empty array. - assertEquals(0, ProxiedEntityUtils.getComponents(" ", "cn").length); - - // Verify that a blank component name results in an empty array. - assertEquals(0, ProxiedEntityUtils.getComponents("cn=john q. doe, c=us, o=my org, ou=my dept", " ").length); - - // Verify that a DN with no matching component results in an empty array. - assertEquals(0, ProxiedEntityUtils.getComponents("cn=john q. doe, c=us, o=my org, ou=my dept", "dc").length); - - // Verify that a DN with a single-value matching component results in an array with a single element. - assertArrayEquals(new String[] {"my org"}, ProxiedEntityUtils.getComponents("cn=john q. doe, c=us, o=my org, ou=my dept", "o")); - - // Verify that a DN with a multi-value matching component results in an array with multiple elements. - assertArrayEquals(new String[] {"com", "example"}, - ProxiedEntityUtils.getComponents("cn=john q. doe, c=us, o=my org, ou=my dept, dc=example, dc=com", "dc")); - - // Verify that component name matching is case-insensitive. - assertArrayEquals(new String[] {"my org"}, ProxiedEntityUtils.getComponents("cn=john q. doe, c=us, o=my org, ou=my dept", "O")); - - // Verify that DN that cannot be parsed results in an empty array. - assertEquals(0, ProxiedEntityUtils.getComponents("S-1-1-0", "cn").length); - } - - /** - * Tests for {@link ProxiedEntityUtils#normalizeDN(String)}. - */ - @Test - public void testNormalizedDN() { - // Verify the DN is trimmed of whitespace and cast to lowercase. - assertEquals("c=us, o=my org, cn=john q. doe, ou=my dept", ProxiedEntityUtils.normalizeDN(" C=US, O=My Org, CN=John Q. Doe, OU=My Dept ")); - - // Verify that if the last RDN is the CN, that the RDNs are reversed. - assertEquals("cn=john q. doe, ou=my dept, o=my org, c=us", ProxiedEntityUtils.normalizeDN("C=US, O=My Org, OU=My Dept, CN=John Q. Doe")); - - // Verify that the components are not reordered if the CN is already in the first position. - assertEquals("cn=john q. doe, c=us, o=my org, ou=my dept", ProxiedEntityUtils.normalizeDN("CN=John Q. Doe, C=US, O=My Org, OU=My Dept")); - - // Verify a string that cannot be parsed as a DN, e.g., a sid, is returned in its original form, trimmed and in lowercase. - assertEquals("s-1-1-0", ProxiedEntityUtils.normalizeDN(" S-1-1-0 ")); - } -} diff --git a/microservices/services/authorization/pom.xml b/microservices/services/authorization/pom.xml index 2f5f20b5897..294eb1ac4c2 100644 --- a/microservices/services/authorization/pom.xml +++ b/microservices/services/authorization/pom.xml @@ -4,7 +4,7 @@ gov.nsa.datawave.microservice datawave-microservice-parent - 4.0.10 + 4.0.11-3508-RC1 ../../microservice-parent/pom.xml authorization-service-parent diff --git a/microservices/services/authorization/service/pom.xml b/microservices/services/authorization/service/pom.xml index 361a9c6624b..511ea3114e5 100644 --- a/microservices/services/authorization/service/pom.xml +++ b/microservices/services/authorization/service/pom.xml @@ -4,7 +4,7 @@ gov.nsa.datawave.microservice datawave-microservice-service-parent - 5.0.11 + 5.0.12-3508-RC1 ../../../microservice-service-parent/pom.xml authorization-service @@ -21,10 +21,10 @@ datawave.microservice.authorization.AuthorizationService 3.20.0 7.33.1 - 4.0.2 + 4.0.3-3508-RC1 4.0.4 4.0.4 - 4.0.7 + 4.0.8-3508-RC1 31.1-jre @@ -51,6 +51,11 @@ jaxb-impl ${version.jaxb-impl} + + gov.nsa.datawave.commons + datawave-commons-security + ${version.datawave-commons-security} + gov.nsa.datawave.core base-rest-responses @@ -125,6 +130,10 @@ com.hazelcast hazelcast + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.core base-rest-responses @@ -153,6 +162,7 @@ io.jsonwebtoken jjwt-api + io.swagger.core.v3 swagger-annotations diff --git a/microservices/services/authorization/service/src/main/java/datawave/microservice/authorization/AuthorizationOperationsV1.java b/microservices/services/authorization/service/src/main/java/datawave/microservice/authorization/AuthorizationOperationsV1.java index f6a9a424d5b..9a43aac5929 100644 --- a/microservices/services/authorization/service/src/main/java/datawave/microservice/authorization/AuthorizationOperationsV1.java +++ b/microservices/services/authorization/service/src/main/java/datawave/microservice/authorization/AuthorizationOperationsV1.java @@ -24,7 +24,6 @@ import datawave.microservice.authorization.user.DatawaveUserDetails; import datawave.microservice.authorization.user.DatawaveUserDetailsFactory; import datawave.microservice.authorization.util.AuthorizationsUtil; -import datawave.microservice.security.util.DnUtils; import datawave.security.DnList; import datawave.security.authorization.CachedDatawaveUserService; import datawave.security.authorization.DatawaveUser; @@ -33,6 +32,8 @@ import datawave.security.authorization.JWTTokenHandler; import datawave.security.authorization.ProxiedUserDetails; import datawave.security.authorization.UserOperations; +import datawave.security.util.DnProperties; +import datawave.security.util.DnUtils; import datawave.user.AuthorizationsListBase; import datawave.webservice.result.GenericResponse; @@ -51,21 +52,21 @@ public class AuthorizationOperationsV1 { protected final AuthorizationsListSupplier authorizationsListSupplier; - protected final DnUtils dnUtils; + protected final DnProperties dnProperties; protected final Set registeredFederatedUserOperations; protected final DatawaveUserDetailsFactory datawaveUserDetailsFactory; public AuthorizationOperationsV1(JWTTokenHandler tokenHandler, CachedDatawaveUserService cachedDatawaveUserService, ApplicationContext appCtx, - BusProperties busProperties, AuthorizationsListSupplier authorizationsListSupplier, DnUtils dnUtils, + BusProperties busProperties, AuthorizationsListSupplier authorizationsListSupplier, DnProperties dnProperties, Set registeredFederatedUserOperations, DatawaveUserDetailsFactory datawaveUserDetailsFactory) { this.tokenHandler = tokenHandler; this.cachedDatawaveUserService = cachedDatawaveUserService; this.appCtx = appCtx; this.busProperties = busProperties; this.authorizationsListSupplier = authorizationsListSupplier; - this.dnUtils = dnUtils; + this.dnProperties = dnProperties; this.registeredFederatedUserOperations = registeredFederatedUserOperations; this.datawaveUserDetailsFactory = datawaveUserDetailsFactory; } @@ -89,7 +90,7 @@ public AuthorizationsListBase listEffectiveAuthorizations(DatawaveUserDetails final AuthorizationsListBase list = authorizationsListSupplier.get(); // Find out who/what called this method - String name = dnUtils.getShortName(currentUser.getPrimaryUser().getName()); + String name = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (federate) { for (UserOperations federatedUserOperations : registeredFederatedUserOperations) { diff --git a/microservices/services/authorization/service/src/main/java/datawave/microservice/authorization/AuthorizationOperationsV2.java b/microservices/services/authorization/service/src/main/java/datawave/microservice/authorization/AuthorizationOperationsV2.java index eca982ad8c3..8e346f699b3 100644 --- a/microservices/services/authorization/service/src/main/java/datawave/microservice/authorization/AuthorizationOperationsV2.java +++ b/microservices/services/authorization/service/src/main/java/datawave/microservice/authorization/AuthorizationOperationsV2.java @@ -10,11 +10,11 @@ import datawave.microservice.authorization.config.AuthorizationsListSupplier; import datawave.microservice.authorization.user.DatawaveUserDetails; import datawave.microservice.authorization.user.DatawaveUserDetailsFactory; -import datawave.microservice.security.util.DnUtils; import datawave.security.authorization.CachedDatawaveUserService; import datawave.security.authorization.DatawaveUser; import datawave.security.authorization.JWTTokenHandler; import datawave.security.authorization.UserOperations; +import datawave.security.util.DnProperties; /** * Presents the REST operations for the authorization service. This version returns the updated (V2) DatawaveUser @@ -23,9 +23,9 @@ public class AuthorizationOperationsV2 extends AuthorizationOperationsV1 { public AuthorizationOperationsV2(JWTTokenHandler tokenHandler, CachedDatawaveUserService cachedDatawaveUserService, ApplicationContext appCtx, - BusProperties busProperties, AuthorizationsListSupplier authorizationsListSupplier, DnUtils dnUtils, + BusProperties busProperties, AuthorizationsListSupplier authorizationsListSupplier, DnProperties dnProperties, Set registeredFederatedUserOperations, DatawaveUserDetailsFactory datawaveUserDetailsFactory) { - super(tokenHandler, cachedDatawaveUserService, appCtx, busProperties, authorizationsListSupplier, dnUtils, registeredFederatedUserOperations, + super(tokenHandler, cachedDatawaveUserService, appCtx, busProperties, authorizationsListSupplier, dnProperties, registeredFederatedUserOperations, datawaveUserDetailsFactory); } diff --git a/microservices/services/authorization/service/src/test/java/datawave/microservice/authorization/OAuthOperationsV2TestCommon.java b/microservices/services/authorization/service/src/test/java/datawave/microservice/authorization/OAuthOperationsV2TestCommon.java index 899ea386569..f06c9339752 100644 --- a/microservices/services/authorization/service/src/test/java/datawave/microservice/authorization/OAuthOperationsV2TestCommon.java +++ b/microservices/services/authorization/service/src/test/java/datawave/microservice/authorization/OAuthOperationsV2TestCommon.java @@ -58,7 +58,7 @@ import datawave.security.authorization.SubjectIssuerDNPair; import datawave.security.authorization.oauth.OAuthTokenResponse; import datawave.security.authorization.oauth.OAuthUserInfo; -import datawave.security.util.ProxiedEntityUtils; +import datawave.security.util.DnUtils; public class OAuthOperationsV2TestCommon { @@ -144,7 +144,7 @@ public void TestCodeFlowValid(DatawaveUser dwUser, AUTH_TYPE authType) throws Ex // Call the user endpoint with the access_token to get the primary user ResponseEntity userResponse = user(access_token, JWTTokenHandler.PRINCIPALS_CLAIM); OAuthUserInfo oAuthUserInfo = userResponse.getBody(); - assertEquals(ProxiedEntityUtils.getCommonName(dwUser.getDn().subjectDN()), oAuthUserInfo.getName()); + assertEquals(DnUtils.getCommonName(dwUser.getDn().subjectDN()), oAuthUserInfo.getName()); assertEquals(dwUser.getLogin(), oAuthUserInfo.getLogin()); assertEquals(dwUser.getEmail(), oAuthUserInfo.getEmail()); assertEquals(dwUser.getDn(), oAuthUserInfo.getDn()); diff --git a/microservices/services/mapreduce-query/service/pom.xml b/microservices/services/mapreduce-query/service/pom.xml index 672dbcb0ca4..b15e44dc929 100644 --- a/microservices/services/mapreduce-query/service/pom.xml +++ b/microservices/services/mapreduce-query/service/pom.xml @@ -4,7 +4,7 @@ gov.nsa.datawave.microservice datawave-microservice-service-parent - 5.0.11 + 5.0.12-3508-RC1 ../../../microservice-service-parent/pom.xml mapreduce-query-service @@ -37,6 +37,11 @@ + + gov.nsa.datawave.commons + datawave-commons-security + ${version.datawave-commons-security} + gov.nsa.datawave.core base-rest-responses @@ -186,6 +191,10 @@ + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.core base-rest-responses diff --git a/microservices/services/mapreduce-query/service/src/main/java/datawave/microservice/query/mapreduce/MapReduceQueryJobRunner.java b/microservices/services/mapreduce-query/service/src/main/java/datawave/microservice/query/mapreduce/MapReduceQueryJobRunner.java index e84b27b980d..646250853b3 100644 --- a/microservices/services/mapreduce-query/service/src/main/java/datawave/microservice/query/mapreduce/MapReduceQueryJobRunner.java +++ b/microservices/services/mapreduce-query/service/src/main/java/datawave/microservice/query/mapreduce/MapReduceQueryJobRunner.java @@ -36,7 +36,7 @@ import datawave.microservice.query.mapreduce.remote.MapReduceQueryRequestHandler; import datawave.microservice.query.mapreduce.status.MapReduceQueryCache; import datawave.microservice.query.mapreduce.status.MapReduceQueryStatus; -import datawave.security.util.ProxiedEntityUtils; +import datawave.security.util.DnUtils; import datawave.webservice.query.exception.DatawaveErrorCode; import datawave.webservice.query.exception.QueryException; @@ -113,7 +113,7 @@ private void submit(MapReduceQueryStatus mapReduceQueryStatus) { StringBuilder name = new StringBuilder() .append(mapReduceQueryStatus.getJobName()) .append("_sid_") - .append(ProxiedEntityUtils.getShortName(mapReduceQueryStatus.getCurrentUser().getPrimaryUser().getName())) + .append(DnUtils.getShortName(mapReduceQueryStatus.getCurrentUser().getPrimaryUser().getName())) .append("_id_") .append(mapReduceQueryStatus.getId()); // @formatter:on diff --git a/microservices/services/query-metric/service/pom.xml b/microservices/services/query-metric/service/pom.xml index 4623e1a5b89..ad491b85361 100644 --- a/microservices/services/query-metric/service/pom.xml +++ b/microservices/services/query-metric/service/pom.xml @@ -4,7 +4,7 @@ gov.nsa.datawave.microservice datawave-microservice-service-parent - 5.0.11 + 5.0.12-3508-RC1 ../../../microservice-service-parent/pom.xml query-metric-service @@ -31,7 +31,7 @@ 4.0.4 1.0.1 4.1.3 - 4.0.7 + 4.0.8-3508-RC1 3.0.4 3.0.5 31.1-jre @@ -426,6 +426,11 @@ gov.nsa.datawave datawave-query-core + + gov.nsa.datawave.commons + datawave-commons-security + ${version.datawave-commons-security} + gov.nsa.datawave.core accumulo-utils diff --git a/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/QueryMetricOperations.java b/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/QueryMetricOperations.java index 158e3d2e7f7..4bfebdffadd 100644 --- a/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/QueryMetricOperations.java +++ b/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/QueryMetricOperations.java @@ -60,9 +60,10 @@ import datawave.microservice.querymetric.handler.QueryGeometryHandler; import datawave.microservice.querymetric.handler.ShardTableQueryMetricHandler; import datawave.microservice.querymetric.handler.SimpleQueryGeometryHandler; -import datawave.microservice.security.util.DnUtils; import datawave.query.jexl.visitors.JexlFormattedStringBuildingVisitor; import datawave.security.authorization.DatawaveUser; +import datawave.security.util.DnProperties; +import datawave.security.util.DnUtils; import datawave.webservice.query.exception.DatawaveErrorCode; import datawave.webservice.query.exception.QueryException; import datawave.webservice.result.VoidResponse; @@ -97,7 +98,7 @@ public class QueryMetricOperations { private static Set inProcess = Collections.synchronizedSet(new HashSet<>()); private final QueryMetricClient queryMetricClient; - private final DnUtils dnUtils; + private final DnProperties dnProperties; /** * The enum Default datetime. @@ -136,14 +137,12 @@ enum DEFAULT_DATETIME { * the stats * @param queryMetricClient * the QueryMetricClient - * @param dnUtils - * the dnUtils */ @Autowired public QueryMetricOperations(@Qualifier("queryMetricCacheManager") CacheManager cacheManager, ShardTableQueryMetricHandler handler, QueryGeometryHandler geometryHandler, MarkingFunctions markingFunctions, QueryMetricResponseFactory queryMetricResponseFactory, MergeLockLifecycleListener mergeLock, Correlator correlator, MetricUpdateEntryProcessorFactory entryProcessorFactory, - QueryMetricOperationsStats stats, QueryMetricClient queryMetricClient, DnUtils dnUtils) { + QueryMetricOperationsStats stats, QueryMetricClient queryMetricClient, DnProperties dnProperties) { this.handler = handler; this.geometryHandler = geometryHandler; this.cacheManager = cacheManager; @@ -155,7 +154,7 @@ public QueryMetricOperations(@Qualifier("queryMetricCacheManager") CacheManager this.entryProcessorFactory = entryProcessorFactory; this.stats = stats; this.queryMetricClient = queryMetricClient; - this.dnUtils = dnUtils; + this.dnProperties = dnProperties; } @PreDestroy @@ -462,7 +461,7 @@ private boolean isSameUser(DatawaveUserDetails currentUser, BaseQueryMetric metr boolean sameUser = false; if (currentUser != null) { String metricUser = metric.getUser(); - String requestingUser = dnUtils.getShortName(currentUser.getPrimaryUser().getName()); + String requestingUser = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); sameUser = metricUser != null && metricUser.equals(requestingUser); } return sameUser; diff --git a/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/config/QueryMetricHandlerConfiguration.java b/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/config/QueryMetricHandlerConfiguration.java index a11c1ea1619..febb282df9a 100644 --- a/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/config/QueryMetricHandlerConfiguration.java +++ b/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/config/QueryMetricHandlerConfiguration.java @@ -48,7 +48,6 @@ import datawave.microservice.querymetric.handler.RemoteShardTableQueryMetricHandler; import datawave.microservice.querymetric.handler.ShardTableQueryMetricHandler; import datawave.microservice.querymetric.handler.SimpleQueryGeometryHandler; -import datawave.microservice.security.util.DnUtils; import datawave.query.language.builder.jexl.JexlTreeBuilder; import datawave.query.language.functions.jexl.EvaluationOnly; import datawave.query.language.functions.jexl.JexlQueryFunction; @@ -56,6 +55,7 @@ import datawave.query.util.DateIndexHelper; import datawave.query.util.DateIndexHelperFactory; import datawave.security.authorization.JWTTokenHandler; +import datawave.security.util.DnProperties; import datawave.webservice.query.result.event.ResponseObjectFactory; @Configuration @@ -101,14 +101,15 @@ public ShardTableQueryMetricHandler shardTableQueryMetricHandler(QueryMetricHand AccumuloConnectionFactory connectionFactory, QueryMetricQueryLogicFactory logicFactory, QueryMetricFactory metricFactory, MarkingFunctions markingFunctions, QueryMetricCombiner queryMetricCombiner, LuceneToJexlQueryParser luceneToJexlQueryParser, ResponseObjectFactory responseObjectFactory, WebClient.Builder webClientBuilder, - @Autowired(required = false) JWTTokenHandler jwtTokenHandler, DnUtils dnUtils, QueryMetricResponseFactory queryMetricResponseFactory) { + @Autowired(required = false) JWTTokenHandler jwtTokenHandler, DnProperties dnProperties, + QueryMetricResponseFactory queryMetricResponseFactory) { ShardTableQueryMetricHandler handler; if (queryMetricHandlerProperties.isUseRemoteQuery()) { handler = new RemoteShardTableQueryMetricHandler(queryMetricHandlerProperties, connectionFactory, logicFactory, metricFactory, markingFunctions, - queryMetricCombiner, luceneToJexlQueryParser, responseObjectFactory, webClientBuilder, jwtTokenHandler, dnUtils); + queryMetricCombiner, luceneToJexlQueryParser, responseObjectFactory, webClientBuilder, jwtTokenHandler, dnProperties); } else { handler = new LocalShardTableQueryMetricHandler(queryMetricHandlerProperties, connectionFactory, logicFactory, metricFactory, markingFunctions, - queryMetricCombiner, luceneToJexlQueryParser, dnUtils); + queryMetricCombiner, luceneToJexlQueryParser, dnProperties); } handler.setQueryMetricResponseFactory(queryMetricResponseFactory); return handler; diff --git a/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/handler/LocalShardTableQueryMetricHandler.java b/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/handler/LocalShardTableQueryMetricHandler.java index 90b776d72bd..3d702025968 100644 --- a/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/handler/LocalShardTableQueryMetricHandler.java +++ b/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/handler/LocalShardTableQueryMetricHandler.java @@ -29,11 +29,11 @@ import datawave.microservice.querymetric.QueryMetricFactory; import datawave.microservice.querymetric.config.QueryMetricHandlerProperties; import datawave.microservice.querymetric.factory.QueryMetricQueryLogicFactory; -import datawave.microservice.security.util.DnUtils; import datawave.query.language.parser.jexl.LuceneToJexlQueryParser; import datawave.security.authorization.DatawavePrincipal; import datawave.security.authorization.DatawaveUser; import datawave.security.authorization.SubjectIssuerDNPair; +import datawave.security.util.DnProperties; import datawave.webservice.query.runner.RunningQuery; import datawave.webservice.result.BaseQueryResponse; @@ -49,9 +49,9 @@ public class LocalShardTableQueryMetricHandler extend public LocalShardTableQueryMetricHandler(QueryMetricHandlerProperties queryMetricHandlerProperties, AccumuloConnectionFactory connectionFactory, QueryMetricQueryLogicFactory logicFactory, QueryMetricFactory metricFactory, MarkingFunctions markingFunctions, - QueryMetricCombiner queryMetricCombiner, LuceneToJexlQueryParser luceneToJexlQueryParser, DnUtils dnUtils) { + QueryMetricCombiner queryMetricCombiner, LuceneToJexlQueryParser luceneToJexlQueryParser, DnProperties dnProperties) { super(queryMetricHandlerProperties, connectionFactory, logicFactory, metricFactory, markingFunctions, queryMetricCombiner, luceneToJexlQueryParser, - dnUtils); + dnProperties); this.datawaveQueryMetricFactory = metricFactory; diff --git a/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/handler/RemoteShardTableQueryMetricHandler.java b/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/handler/RemoteShardTableQueryMetricHandler.java index 81eae0285ce..57266b9975e 100644 --- a/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/handler/RemoteShardTableQueryMetricHandler.java +++ b/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/handler/RemoteShardTableQueryMetricHandler.java @@ -22,10 +22,10 @@ import datawave.microservice.querymetric.QueryMetricFactory; import datawave.microservice.querymetric.config.QueryMetricHandlerProperties; import datawave.microservice.querymetric.factory.QueryMetricQueryLogicFactory; -import datawave.microservice.security.util.DnUtils; import datawave.query.language.parser.jexl.LuceneToJexlQueryParser; import datawave.security.authorization.DatawaveUser; import datawave.security.authorization.JWTTokenHandler; +import datawave.security.util.DnProperties; import datawave.webservice.query.result.event.ResponseObjectFactory; import datawave.webservice.result.BaseQueryResponse; @@ -41,9 +41,9 @@ public class RemoteShardTableQueryMetricHandler exten public RemoteShardTableQueryMetricHandler(QueryMetricHandlerProperties queryMetricHandlerProperties, AccumuloConnectionFactory connectionFactory, QueryMetricQueryLogicFactory logicFactory, QueryMetricFactory metricFactory, MarkingFunctions markingFunctions, QueryMetricCombiner queryMetricCombiner, LuceneToJexlQueryParser luceneToJexlQueryParser, ResponseObjectFactory responseObjectFactory, - WebClient.Builder webClientBuilder, JWTTokenHandler jwtTokenHandler, DnUtils dnUtils) { + WebClient.Builder webClientBuilder, JWTTokenHandler jwtTokenHandler, DnProperties dnProperties) { super(queryMetricHandlerProperties, connectionFactory, logicFactory, metricFactory, markingFunctions, queryMetricCombiner, luceneToJexlQueryParser, - dnUtils); + dnProperties); this.responseObjectFactory = responseObjectFactory; diff --git a/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/handler/ShardTableQueryMetricHandler.java b/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/handler/ShardTableQueryMetricHandler.java index 351ba82ad4b..9c48d931955 100644 --- a/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/handler/ShardTableQueryMetricHandler.java +++ b/microservices/services/query-metric/service/src/main/java/datawave/microservice/querymetric/handler/ShardTableQueryMetricHandler.java @@ -75,10 +75,11 @@ import datawave.microservice.querymetric.QueryMetricsSummaryResponse; import datawave.microservice.querymetric.config.QueryMetricHandlerProperties; import datawave.microservice.querymetric.factory.QueryMetricQueryLogicFactory; -import datawave.microservice.security.util.DnUtils; import datawave.query.QueryParameters; import datawave.query.language.parser.jexl.LuceneToJexlQueryParser; import datawave.security.authorization.DatawaveUser; +import datawave.security.util.DnProperties; +import datawave.security.util.DnUtils; import datawave.security.util.WSAuthorizationsUtil; import datawave.webservice.query.exception.QueryExceptionType; import datawave.webservice.query.result.event.EventBase; @@ -107,19 +108,19 @@ public abstract class ShardTableQueryMetricHandler ex protected UIDBuilder uidBuilder = UID.builder(); protected QueryMetricCombiner queryMetricCombiner; protected MarkingFunctions markingFunctions; - protected DnUtils dnUtils; + protected DnProperties dnProperties; // this lock is necessary for when there is an error condition and the accumuloRecordWriter needs to be replaced protected ReentrantReadWriteLock accumuloRecordWriterLock = new ReentrantReadWriteLock(); public ShardTableQueryMetricHandler(QueryMetricHandlerProperties queryMetricHandlerProperties, AccumuloConnectionFactory connectionFactory, QueryMetricQueryLogicFactory logicFactory, QueryMetricFactory metricFactory, MarkingFunctions markingFunctions, - QueryMetricCombiner queryMetricCombiner, LuceneToJexlQueryParser luceneToJexlQueryParser, DnUtils dnUtils) { + QueryMetricCombiner queryMetricCombiner, LuceneToJexlQueryParser luceneToJexlQueryParser, DnProperties dnProperties) { super(luceneToJexlQueryParser); this.queryMetricHandlerProperties = queryMetricHandlerProperties; this.logicFactory = logicFactory; this.metricFactory = metricFactory; this.markingFunctions = markingFunctions; - this.dnUtils = dnUtils; + this.dnProperties = dnProperties; this.connectionFactory = connectionFactory; this.queryMetricCombiner = queryMetricCombiner; @@ -890,7 +891,7 @@ public QueryMetricsSummaryResponse getQueryMetricsSummary(Date begin, Date end, try { // this method is open to any user DatawaveUser datawaveUser = currentUser.getPrimaryUser(); - String datawaveUserShortName = dnUtils.getShortName(datawaveUser.getName()); + String datawaveUserShortName = DnUtils.getShortName(datawaveUser.getName()); Collection userAuths = new ArrayList<>(datawaveUser.getAuths()); if (clientAuthorizations != null) { Collection connectorAuths = new ArrayList<>(); diff --git a/microservices/services/query-metric/service/src/test/java/datawave/microservice/querymetric/QueryMetricTestBase.java b/microservices/services/query-metric/service/src/test/java/datawave/microservice/querymetric/QueryMetricTestBase.java index a61f8dd3cf8..d3fe995f24a 100644 --- a/microservices/services/query-metric/service/src/test/java/datawave/microservice/querymetric/QueryMetricTestBase.java +++ b/microservices/services/query-metric/service/src/test/java/datawave/microservice/querymetric/QueryMetricTestBase.java @@ -73,10 +73,11 @@ import datawave.microservice.querymetric.function.QueryMetricSupplier; import datawave.microservice.querymetric.handler.QueryMetricCombiner; import datawave.microservice.querymetric.handler.ShardTableQueryMetricHandler; -import datawave.microservice.security.util.DnUtils; import datawave.security.authorization.DatawaveUser; import datawave.security.authorization.JWTTokenHandler; import datawave.security.authorization.SubjectIssuerDNPair; +import datawave.security.util.DnProperties; +import datawave.security.util.DnUtils; import datawave.webservice.query.result.event.DefaultEvent; import datawave.webservice.query.result.event.DefaultField; import datawave.webservice.query.result.event.EventBase; @@ -128,7 +129,7 @@ public class QueryMetricTestBase { private QueryMetricClientProperties queryMetricClientProperties; @Autowired - private DnUtils dnUtils; + private DnProperties dnProperties; @Autowired @Qualifier("lastWrittenQueryMetrics") diff --git a/microservices/services/query-metric/service/src/test/java/datawave/microservice/querymetric/config/AlternateQueryMetricConfiguration.java b/microservices/services/query-metric/service/src/test/java/datawave/microservice/querymetric/config/AlternateQueryMetricConfiguration.java index 3695bf7602e..06e67a10f41 100644 --- a/microservices/services/query-metric/service/src/test/java/datawave/microservice/querymetric/config/AlternateQueryMetricConfiguration.java +++ b/microservices/services/query-metric/service/src/test/java/datawave/microservice/querymetric/config/AlternateQueryMetricConfiguration.java @@ -16,8 +16,8 @@ import datawave.microservice.querymetric.factory.QueryMetricQueryLogicFactory; import datawave.microservice.querymetric.handler.QueryMetricCombiner; import datawave.microservice.querymetric.handler.ShardTableQueryMetricHandler; -import datawave.microservice.security.util.DnUtils; import datawave.query.language.parser.jexl.LuceneToJexlQueryParser; +import datawave.security.util.DnProperties; @ImportAutoConfiguration({RefreshAutoConfiguration.class}) @AutoConfigureCache(cacheProvider = CacheType.HAZELCAST) @@ -49,8 +49,8 @@ public BaseQueryMetric createMetric(boolean populateVersionMap) { public ShardTableQueryMetricHandler shardTableQueryMetricHandler(QueryMetricHandlerProperties queryMetricHandlerProperties, AccumuloConnectionFactory connectionFactory, QueryMetricQueryLogicFactory logicFactory, QueryMetricFactory metricFactory, MarkingFunctions markingFunctions, QueryMetricCombiner queryMetricCombiner, LuceneToJexlQueryParser luceneToJexlQueryParser, - DnUtils dnUtils) { + DnProperties dnProperties) { return new AlternateShardTableQueryMetricHandler(queryMetricHandlerProperties, connectionFactory, logicFactory, metricFactory, markingFunctions, - queryMetricCombiner, luceneToJexlQueryParser, dnUtils); + queryMetricCombiner, luceneToJexlQueryParser, dnProperties); } } diff --git a/microservices/services/query-metric/service/src/test/java/datawave/microservice/querymetric/config/AlternateShardTableQueryMetricHandler.java b/microservices/services/query-metric/service/src/test/java/datawave/microservice/querymetric/config/AlternateShardTableQueryMetricHandler.java index ff985dc0bd8..28549bfaaad 100644 --- a/microservices/services/query-metric/service/src/test/java/datawave/microservice/querymetric/config/AlternateShardTableQueryMetricHandler.java +++ b/microservices/services/query-metric/service/src/test/java/datawave/microservice/querymetric/config/AlternateShardTableQueryMetricHandler.java @@ -10,8 +10,8 @@ import datawave.microservice.querymetric.handler.ContentQueryMetricsIngestHelper; import datawave.microservice.querymetric.handler.LocalShardTableQueryMetricHandler; import datawave.microservice.querymetric.handler.QueryMetricCombiner; -import datawave.microservice.security.util.DnUtils; import datawave.query.language.parser.jexl.LuceneToJexlQueryParser; +import datawave.security.util.DnProperties; import datawave.webservice.query.result.event.EventBase; import datawave.webservice.query.result.event.FieldBase; @@ -19,9 +19,9 @@ public class AlternateShardTableQueryMetricHandler extends LocalShardTableQueryM public AlternateShardTableQueryMetricHandler(QueryMetricHandlerProperties queryMetricHandlerProperties, AccumuloConnectionFactory connectionFactory, QueryMetricQueryLogicFactory logicFactory, QueryMetricFactory metricFactory, MarkingFunctions markingFunctions, - QueryMetricCombiner queryMetricCombiner, LuceneToJexlQueryParser luceneToJexlQueryParser, DnUtils dnUtils) { + QueryMetricCombiner queryMetricCombiner, LuceneToJexlQueryParser luceneToJexlQueryParser, DnProperties dnProperties) { super(queryMetricHandlerProperties, connectionFactory, logicFactory, metricFactory, markingFunctions, queryMetricCombiner, luceneToJexlQueryParser, - dnUtils); + dnProperties); } @Override diff --git a/microservices/services/query/pom.xml b/microservices/services/query/pom.xml index 7445827eff0..65a0c090d75 100644 --- a/microservices/services/query/pom.xml +++ b/microservices/services/query/pom.xml @@ -4,7 +4,7 @@ gov.nsa.datawave.microservice datawave-microservice-parent - 4.0.10 + 4.0.11-3508-RC1 ../../microservice-parent/pom.xml query-service-parent diff --git a/microservices/services/query/service/pom.xml b/microservices/services/query/service/pom.xml index 4ee665c5a92..8cc5b198ff1 100644 --- a/microservices/services/query/service/pom.xml +++ b/microservices/services/query/service/pom.xml @@ -4,7 +4,7 @@ gov.nsa.datawave.microservice datawave-microservice-service-parent - 5.0.11 + 5.0.12-3508-RC1 ../../../microservice-service-parent/pom.xml query-service @@ -367,6 +367,10 @@ + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.core base-rest-responses diff --git a/microservices/services/query/service/src/main/java/datawave/microservice/query/QueryManagementService.java b/microservices/services/query/service/src/main/java/datawave/microservice/query/QueryManagementService.java index a16e1816bdd..a7808195216 100644 --- a/microservices/services/query/service/src/main/java/datawave/microservice/query/QueryManagementService.java +++ b/microservices/services/query/service/src/main/java/datawave/microservice/query/QueryManagementService.java @@ -75,7 +75,7 @@ import datawave.microservice.querymetric.BaseQueryMetric; import datawave.microservice.querymetric.QueryMetricClient; import datawave.microservice.querymetric.QueryMetricType; -import datawave.security.util.ProxiedEntityUtils; +import datawave.security.util.DnUtils; import datawave.webservice.common.audit.AuditParameters; import datawave.webservice.common.audit.Auditor; import datawave.webservice.query.exception.BadRequestQueryException; @@ -166,7 +166,7 @@ public QueryManagementService(QueryProperties queryProperties, ApplicationEventP * @return the query logic descriptions */ public QueryLogicResponse listQueryLogic(DatawaveUserDetails currentUser) { - log.info("Request: listQueryLogic from {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName())); + log.info("Request: listQueryLogic from {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName())); QueryLogicResponse response = new QueryLogicResponse(); List> queryLogicList = queryLogicFactory.getQueryLogicList(); @@ -273,7 +273,7 @@ public QueryLogicResponse listQueryLogic(DatawaveUserDetails currentUser) { */ public GenericResponse define(String queryLogicName, MultiValueMap parameters, String pool, DatawaveUserDetails currentUser) throws QueryException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: {}/define from {} with params: {}", queryLogicName, user, parameters); } else { @@ -331,7 +331,7 @@ public GenericResponse define(String queryLogicName, MultiValueMap create(String queryLogicName, MultiValueMap parameters, String pool, DatawaveUserDetails currentUser) throws QueryException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: {}/create from {} with params: {}", queryLogicName, user, parameters); } else { @@ -386,7 +386,7 @@ public GenericResponse create(String queryLogicName, MultiValueMap plan(String queryLogicName, MultiValueMap parameters, String pool, DatawaveUserDetails currentUser) throws QueryException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: {}/plan from {} with params: {}", queryLogicName, user, parameters); } else { @@ -443,7 +443,7 @@ public GenericResponse plan(String queryLogicName, MultiValueMap predict(String queryLogicName, MultiValueMap parameters, String pool, DatawaveUserDetails currentUser) throws QueryException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: {}/predict from {} with params: {}", queryLogicName, user, parameters); } else { @@ -520,7 +520,7 @@ private TaskKey storeQuery(String queryLogicName, MultiValueMap p // validate query and get a query logic QueryLogic queryLogic = validateQuery(queryLogicName, parameters, currentUser); - String userId = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String userId = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); log.trace("{} has authorizations {}", userId, currentUser.getPrimaryUser().getAuths()); Query query = createQuery(queryLogicName, parameters, currentUser, queryId); @@ -744,7 +744,7 @@ private void sendRequestAwaitResponse(QueryRequest request, String computedPool, */ public BaseQueryResponse createAndNext(String queryLogicName, MultiValueMap parameters, String pool, DatawaveUserDetails currentUser) throws QueryException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: {}/createAndNext from {} with params: {}", queryLogicName, user, parameters); } else { @@ -808,7 +808,7 @@ public BaseQueryResponse createAndNext(String queryLogicName, MultiValueMap queryStatuses = queryStorageCache.getQueryStatus(); @@ -1205,7 +1205,7 @@ public void cancel(String queryId, boolean publishEvent) throws InterruptedExcep * if there is an unknown error */ public VoidResponse close(String queryId, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: close from {} for {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId); + log.info("Request: close from {} for {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId); return close(queryId, currentUser, false); } @@ -1234,7 +1234,7 @@ public VoidResponse close(String queryId, DatawaveUserDetails currentUser) throw * if there is an unknown error */ public VoidResponse adminClose(String queryId, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: adminClose from {} for {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId); + log.info("Request: adminClose from {} for {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId); return close(queryId, currentUser, true); } @@ -1260,7 +1260,7 @@ public VoidResponse adminClose(String queryId, DatawaveUserDetails currentUser) * if there is an unknown error */ public VoidResponse adminCloseAll(DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: adminCloseAll from {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName())); + log.info("Request: adminCloseAll from {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName())); try { List queryStatuses = queryStorageCache.getQueryStatus(); @@ -1423,7 +1423,7 @@ public void close(String queryId) throws InterruptedException, QueryException { * if there is an unknown error */ public GenericResponse reset(String queryId, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: reset from {} for {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId); + log.info("Request: reset from {} for {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId); try { // make sure the query is valid, and the user can act on it @@ -1471,7 +1471,7 @@ public GenericResponse reset(String queryId, DatawaveUserDetails current * if there is an unknown error */ public VoidResponse remove(String queryId, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: remove from {} for {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId); + log.info("Request: remove from {} for {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId); return remove(queryId, currentUser, false); } @@ -1495,7 +1495,7 @@ public VoidResponse remove(String queryId, DatawaveUserDetails currentUser) thro * if there is an unknown error */ public VoidResponse adminRemove(String queryId, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: adminRemove from {} for {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId); + log.info("Request: adminRemove from {} for {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId); return remove(queryId, currentUser, true); } @@ -1516,7 +1516,7 @@ public VoidResponse adminRemove(String queryId, DatawaveUserDetails currentUser) * if there is an unknown error */ public VoidResponse adminRemoveAll(DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: adminRemoveAll from {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName())); + log.info("Request: adminRemoveAll from {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName())); try { List queryStatuses = queryStorageCache.getQueryStatus(); @@ -1633,7 +1633,7 @@ private boolean remove(QueryStatus queryStatus) throws IOException { * if there is an unknown error */ public GenericResponse update(String queryId, MultiValueMap parameters, DatawaveUserDetails currentUser) throws QueryException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: {}/update from {} with params: {}", queryId, user, parameters); } else { @@ -1751,7 +1751,7 @@ public GenericResponse update(String queryId, MultiValueMap duplicate(String queryId, MultiValueMap parameters, DatawaveUserDetails currentUser) throws QueryException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: {}/duplicate from {} with params: {}", queryId, user, parameters); } else { @@ -1879,10 +1879,9 @@ private boolean updateParameters(Collection parameterNames, MultiValueMa * if there is an unknown error */ public QueryImplListResponse list(String queryId, String queryName, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: list from {} for queryId: {}, queryName: {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId, - queryName); + log.info("Request: list from {} for queryId: {}, queryName: {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId, queryName); - return list(queryId, queryName, ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getDn().subjectDN())); + return list(queryId, queryName, DnUtils.getShortName(currentUser.getPrimaryUser().getDn().subjectDN())); } /** @@ -1904,8 +1903,8 @@ public QueryImplListResponse list(String queryId, String queryName, DatawaveUser * if there is an unknown error */ public QueryImplListResponse adminList(String queryId, String queryName, String userId, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: adminList from {} for queryId: {}, queryName: {}, userId: {}", - ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId, queryName, userId); + log.info("Request: adminList from {} for queryId: {}, queryName: {}, userId: {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId, + queryName, userId); return list(queryId, queryName, userId); } @@ -1971,7 +1970,7 @@ private QueryImplListResponse list(String queryId, String queryName, String user * if there is an unknown error */ public GenericResponse plan(String queryId, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: plan from {} for queryId: {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId); + log.info("Request: plan from {} for queryId: {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId); try { // make sure the query is valid, and the user can act on it @@ -2011,7 +2010,7 @@ public GenericResponse plan(String queryId, DatawaveUserDetails currentU * if there is an unknown error */ public GenericResponse predictions(String queryId, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: predictions from {} for queryId: {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId); + log.info("Request: predictions from {} for queryId: {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), queryId); try { // make sure the query is valid, and the user can act on it @@ -2061,7 +2060,7 @@ public QueryStatus validateRequest(String queryId, DatawaveUserDetails currentUs // admin requests can operate on any query, regardless of ownership if (!adminOverride) { // does the current user own this query? - String userId = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getDn().subjectDN()); + String userId = DnUtils.getShortName(currentUser.getPrimaryUser().getDn().subjectDN()); Query query = queryStatus.getQuery(); if (!query.getOwner().equals(userId)) { throw new UnauthorizedQueryException(DatawaveErrorCode.QUERY_OWNER_MISMATCH, MessageFormat.format("{0} != {1}", userId, query.getOwner())); diff --git a/microservices/services/query/service/src/main/java/datawave/microservice/query/lookup/LookupService.java b/microservices/services/query/service/src/main/java/datawave/microservice/query/lookup/LookupService.java index 8fdb8c3eb38..a15e0579b81 100644 --- a/microservices/services/query/service/src/main/java/datawave/microservice/query/lookup/LookupService.java +++ b/microservices/services/query/service/src/main/java/datawave/microservice/query/lookup/LookupService.java @@ -48,7 +48,7 @@ import datawave.query.data.UUIDType; import datawave.security.authorization.AuthorizationException; import datawave.security.authorization.ProxiedUserDetails; -import datawave.security.util.ProxiedEntityUtils; +import datawave.security.util.DnUtils; import datawave.webservice.query.exception.BadRequestQueryException; import datawave.webservice.query.exception.DatawaveErrorCode; import datawave.webservice.query.exception.NoResultsQueryException; @@ -134,7 +134,7 @@ public LookupService(LookupProperties lookupProperties, QueryLogicFactory queryL */ public void lookupUUID(MultiValueMap parameters, String pool, DatawaveUserDetails currentUser, StreamingResponseListener listener) throws QueryException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: lookupUUID from {} with params: {}", user, parameters); } else { @@ -188,7 +188,7 @@ public void lookupUUID(MultiValueMap parameters, String pool, Dat * if there is an unknown error */ public BaseQueryResponse lookupUUID(MultiValueMap parameters, String pool, DatawaveUserDetails currentUser) throws QueryException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: lookupUUID from {} with params: {}", user, parameters); } else { @@ -245,7 +245,7 @@ public BaseQueryResponse lookupUUID(MultiValueMap parameters, Str */ public T lookupContentUUID(MultiValueMap parameters, String pool, DatawaveUserDetails currentUser, StreamingResponseListener listener) throws QueryException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: lookupContentUUID from {} with params: {}", user, parameters); } else { @@ -300,7 +300,7 @@ public T lookupContentUUID(MultiValueMap parameters, String p * if there is an unknown error */ public BaseQueryResponse lookupContentUUID(MultiValueMap parameters, String pool, DatawaveUserDetails currentUser) throws QueryException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: lookupContentUUID from {} with params: {}", user, parameters); } else { @@ -533,7 +533,7 @@ public String getAuths(MultiValueMap queryParameters, QueryLogic< protected void setupEventQueryParameters(MultiValueMap parameters, LookupQueryLogic queryLogic, DatawaveUserDetails currentUser) throws AuthorizationException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); final String queryName = user + "-" + UUID.randomUUID().toString(); final String endDate; @@ -664,7 +664,7 @@ private List createContentQueries(Set contentLookupTerms) { } protected void setContentQueryParameters(MultiValueMap parameters, DatawaveUserDetails currentUser) { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); setOptionalQueryParameters(parameters); diff --git a/microservices/services/query/service/src/main/java/datawave/microservice/query/mapreduce/MapReduceQueryManagementService.java b/microservices/services/query/service/src/main/java/datawave/microservice/query/mapreduce/MapReduceQueryManagementService.java index 92e709476b4..6484314a55f 100644 --- a/microservices/services/query/service/src/main/java/datawave/microservice/query/mapreduce/MapReduceQueryManagementService.java +++ b/microservices/services/query/service/src/main/java/datawave/microservice/query/mapreduce/MapReduceQueryManagementService.java @@ -65,7 +65,7 @@ import datawave.microservice.query.mapreduce.status.MapReduceQueryCache; import datawave.microservice.query.mapreduce.status.MapReduceQueryStatus; import datawave.microservice.query.storage.QueryStatus; -import datawave.security.util.ProxiedEntityUtils; +import datawave.security.util.DnUtils; import datawave.webservice.common.audit.AuditParameters; import datawave.webservice.query.exception.BadRequestQueryException; import datawave.webservice.query.exception.DatawaveErrorCode; @@ -125,7 +125,7 @@ public MapReduceQueryManagementService(QueryProperties queryProperties, MapReduc } public MapReduceJobDescriptionList listConfigurations(String jobType, DatawaveUserDetails currentUser) { - log.info("Request: listConfigurations from {} for {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), jobType); + log.info("Request: listConfigurations from {} for {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), jobType); MapReduceJobDescriptionList response = new MapReduceJobDescriptionList(); List jobs = new ArrayList<>(); @@ -156,7 +156,7 @@ protected MapReduceJobDescription createMapReduceJobDescription(String name, Map public GenericResponse oozieSubmit(MultiValueMap parameters, DatawaveUserDetails currentUser) throws QueryException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: submit from {} with params: {}", user, parameters); } else { @@ -181,7 +181,7 @@ public GenericResponse oozieSubmit(MultiValueMap paramete } public GenericResponse submit(MultiValueMap parameters, DatawaveUserDetails currentUser) throws QueryException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: submit from {} with params: {}", user, parameters); } else { @@ -410,13 +410,13 @@ public void handleRemoteRequest(MapReduceQueryRequest queryRequest, String origi } public GenericResponse cancel(String id, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: cancel from {} for {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), id); + log.info("Request: cancel from {} for {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), id); return cancel(id, currentUser, false); } public GenericResponse adminCancel(String id, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: adminCancel from {} for {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), id); + log.info("Request: adminCancel from {} for {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), id); return cancel(id, currentUser, true); } @@ -477,7 +477,7 @@ private void removeDirectory(String directory) throws IOException, QueryExceptio } public GenericResponse restart(String id, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: restart from {} for {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), id); + log.info("Request: restart from {} for {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), id); try { // make sure the map reduce query is valid, and the user can act on it @@ -506,7 +506,7 @@ public GenericResponse restart(String id, DatawaveUserDetails currentUse } public MapReduceInfoResponseList list(String id, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: list from {} for {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), id); + log.info("Request: list from {} for {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), id); try { // make sure the query is valid, and the user can act on it @@ -540,7 +540,7 @@ protected MapReduceInfoResponse createMapReduceInfoResponse(MapReduceQueryStatus } public MapReduceInfoResponseList list(DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: list for {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName())); + log.info("Request: list for {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName())); try { Set ids = mapReduceQueryCache.lookupQueryIdsByUsername(currentUser.getUsername()); @@ -578,7 +578,7 @@ public MapReduceQueryStatus validateRequest(String id, DatawaveUserDetails curre // admin requests can operate on any job, regardless of ownership if (!adminOverride) { // does the current user own this job? - String userId = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getDn().subjectDN()); + String userId = DnUtils.getShortName(currentUser.getPrimaryUser().getDn().subjectDN()); Query query = mapReduceQueryStatus.getQuery(); if (!query.getOwner().equals(userId)) { throw new UnauthorizedQueryException(DatawaveErrorCode.QUERY_OWNER_MISMATCH, MessageFormat.format("{0} != {1}", userId, query.getOwner())); @@ -589,7 +589,7 @@ public MapReduceQueryStatus validateRequest(String id, DatawaveUserDetails curre } public Map.Entry getFile(String id, String fileName, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: getFile from {} for {}, {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), id, fileName); + log.info("Request: getFile from {} for {}, {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), id, fileName); try { // make sure the query is valid, and the user can act on it @@ -632,7 +632,7 @@ private FSDataInputStream getFileInputStream(Path filePath) throws QueryExceptio } public Map getAllFiles(String id, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: getAllFiles from {} for {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), id); + log.info("Request: getAllFiles from {} for {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), id); Map resultFiles = new HashMap<>(); try { @@ -686,13 +686,13 @@ private Path getRelativeFilePath(FileStatus basePath, FileStatus filePath) { } public VoidResponse remove(String id, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: remove from {} for {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), id); + log.info("Request: remove from {} for {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), id); return remove(id, currentUser, false); } public VoidResponse adminRemove(String id, DatawaveUserDetails currentUser) throws QueryException { - log.info("Request: adminRemove from {} for {}", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), id); + log.info("Request: adminRemove from {} for {}", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), id); return remove(id, currentUser, true); } diff --git a/microservices/services/query/service/src/main/java/datawave/microservice/query/stream/StreamingService.java b/microservices/services/query/service/src/main/java/datawave/microservice/query/stream/StreamingService.java index fb8e6d80c2f..00c611817cc 100644 --- a/microservices/services/query/service/src/main/java/datawave/microservice/query/stream/StreamingService.java +++ b/microservices/services/query/service/src/main/java/datawave/microservice/query/stream/StreamingService.java @@ -11,7 +11,7 @@ import datawave.microservice.query.stream.listener.StreamingResponseListener; import datawave.microservice.query.stream.runner.StreamingCall; import datawave.microservice.querymetric.QueryMetricClient; -import datawave.security.util.ProxiedEntityUtils; +import datawave.security.util.DnUtils; import datawave.webservice.query.exception.BadRequestQueryException; import datawave.webservice.query.exception.NoResultsQueryException; import datawave.webservice.query.exception.QueryException; @@ -72,7 +72,7 @@ public StreamingService(QueryManagementService queryManagementService, QueryMetr */ public String createAndExecute(String queryLogicName, MultiValueMap parameters, String pool, DatawaveUserDetails currentUser, DatawaveUserDetails serverUser, StreamingResponseListener listener) throws QueryException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: {}/createAndExecute from {} with params: {}", queryLogicName, user, parameters); } else { @@ -101,7 +101,7 @@ public String createAndExecute(String queryLogicName, MultiValueMap setupQueryParameters(MultiValueMap gov.nsa.datawave.microservice datawave-microservice-service-parent - 5.0.11 + 5.0.12-3508-RC1 ../../microservice-service-parent/pom.xml spring-boot-starter-datawave-cached-results @@ -43,6 +43,11 @@ mysql-connector-j ${version.mysql-connector} + + gov.nsa.datawave.commons + datawave-commons-security + ${version.datawave-commons-security} + gov.nsa.datawave.core accumulo-utils @@ -169,6 +174,10 @@ com.mysql mysql-connector-j + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.core accumulo-utils diff --git a/microservices/starters/cached-results/src/main/java/datawave/microservice/query/cachedresults/CachedResultsQueryService.java b/microservices/starters/cached-results/src/main/java/datawave/microservice/query/cachedresults/CachedResultsQueryService.java index cb294c76221..a2338d59123 100644 --- a/microservices/starters/cached-results/src/main/java/datawave/microservice/query/cachedresults/CachedResultsQueryService.java +++ b/microservices/starters/cached-results/src/main/java/datawave/microservice/query/cachedresults/CachedResultsQueryService.java @@ -61,7 +61,7 @@ import datawave.microservice.query.storage.QueryStatus; import datawave.microservice.query.storage.QueryStorageCache; import datawave.security.authorization.ProxiedUserDetails; -import datawave.security.util.ProxiedEntityUtils; +import datawave.security.util.DnUtils; import datawave.webservice.common.audit.AuditParameters; import datawave.webservice.common.audit.Auditor; import datawave.webservice.query.cachedresults.CacheableQueryRow; @@ -171,7 +171,7 @@ private void initializeTableTemplate() { * if the operation fails */ public GenericResponse load(String definedQueryId, String alias, ProxiedUserDetails currentUser) throws QueryException { - log.info("Request: {}/load from {} with alias: {}", definedQueryId, ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), alias); + log.info("Request: {}/load from {} with alias: {}", definedQueryId, DnUtils.getShortName(currentUser.getPrimaryUser().getName()), alias); CachedResultsQueryStatus cachedResultsQueryStatus = null; try { @@ -558,7 +558,7 @@ private String getViewName(String newQueryId) { } public CachedResultsResponse create(String key, MultiValueMap parameters, ProxiedUserDetails currentUser) throws QueryException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: {}/create from {} with params: {}", key, user, parameters); } else { @@ -679,7 +679,7 @@ private CachedResultsResponse create(String definedQueryId, MultiValueMap parameters, ProxiedUserDetails currentUser) throws QueryException { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: {}/loadAndCreate from {} with params: {}", definedQueryId, user, parameters); } else { @@ -1076,7 +1076,7 @@ private boolean isFunction(String field) { public BaseQueryResponse getRows(String key, Integer rowBegin, Integer rowEnd, ProxiedUserDetails currentUser) throws QueryException { try { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: {}/getRows from {} with rowBegin: {} rowEnd: {}", key, user, rowBegin, rowEnd); } else { @@ -1187,7 +1187,7 @@ private String getSqlQuery(String sqlQuery, int beginRow, int endRow) { } public GenericResponse status(String key, ProxiedUserDetails currentUser) throws QueryException { - log.info("Request: {}/status from {}", key, ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName())); + log.info("Request: {}/status from {}", key, DnUtils.getShortName(currentUser.getPrimaryUser().getName())); CachedResultsQueryStatus cachedResultsQueryStatus = validateRequest(key, currentUser); @@ -1197,7 +1197,7 @@ public GenericResponse status(String key, ProxiedUserDetails currentUser } public CachedResultsDescribeResponse describe(String key, ProxiedUserDetails currentUser) throws QueryException { - log.info("Request: {}/describe from {}", key, ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName())); + log.info("Request: {}/describe from {}", key, DnUtils.getShortName(currentUser.getPrimaryUser().getName())); CachedResultsQueryStatus cachedResultsQueryStatus = validateRequest(key, currentUser); @@ -1209,12 +1209,12 @@ public CachedResultsDescribeResponse describe(String key, ProxiedUserDetails cur } public VoidResponse cancel(String key, ProxiedUserDetails currentUser) throws QueryException { - log.info("Request: {}/cancel from {}", key, ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName())); + log.info("Request: {}/cancel from {}", key, DnUtils.getShortName(currentUser.getPrimaryUser().getName())); return cancel(key, currentUser, false); } public VoidResponse adminCancel(String key, ProxiedUserDetails currentUser) throws QueryException { - log.info("Request: {}/adminCancel from {}", key, ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName())); + log.info("Request: {}/adminCancel from {}", key, DnUtils.getShortName(currentUser.getPrimaryUser().getName())); return cancel(key, currentUser, true); } @@ -1241,12 +1241,12 @@ private VoidResponse cancel(String key, ProxiedUserDetails currentUser, boolean } public VoidResponse close(String key, ProxiedUserDetails currentUser) throws QueryException { - log.info("Request: {}/close from {}", key, ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName())); + log.info("Request: {}/close from {}", key, DnUtils.getShortName(currentUser.getPrimaryUser().getName())); return close(key, currentUser, false); } public VoidResponse adminClose(String key, ProxiedUserDetails currentUser) throws QueryException { - log.info("Request: {}/adminClose from {}", key, ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName())); + log.info("Request: {}/adminClose from {}", key, DnUtils.getShortName(currentUser.getPrimaryUser().getName())); return close(key, currentUser, true); } @@ -1286,7 +1286,7 @@ private VoidResponse close(String key, ProxiedUserDetails currentUser, boolean a public CachedResultsResponse setAlias(String key, String alias, ProxiedUserDetails currentUser) throws QueryException { try { - log.info("Request: {}/setAlias from {} with alias {}", key, ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), alias); + log.info("Request: {}/setAlias from {} with alias {}", key, DnUtils.getShortName(currentUser.getPrimaryUser().getName()), alias); CachedResultsQueryStatus cachedResultsQueryStatus = validateRequest(key, currentUser); @@ -1317,7 +1317,7 @@ public CachedResultsResponse setAlias(String key, String alias, ProxiedUserDetai public CachedResultsResponse update(String key, String fields, String conditions, String grouping, String order, Integer pagesize, ProxiedUserDetails currentUser) throws QueryException { try { - String user = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()); + String user = DnUtils.getShortName(currentUser.getPrimaryUser().getName()); if (log.isDebugEnabled()) { log.info("Request: {}/udpate from {} with fields: {}, contitions: {}, groupind: {}, order: {}, pagesize: {}", key, user, fields, conditions, grouping, order, pagesize); @@ -1421,8 +1421,8 @@ private CachedResultsQueryStatus validateRequest(String key, ProxiedUserDetails // admin requests can operate on any query, regardless of ownership if (!adminOverride) { // does the current user own this query? - String currentUserId = ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getDn().subjectDN()); - String ownerUserId = ProxiedEntityUtils.getShortName(cachedResultsQueryStatus.getCurrentUser().getPrimaryUser().getDn().subjectDN()); + String currentUserId = DnUtils.getShortName(currentUser.getPrimaryUser().getDn().subjectDN()); + String ownerUserId = DnUtils.getShortName(cachedResultsQueryStatus.getCurrentUser().getPrimaryUser().getDn().subjectDN()); if (!ownerUserId.equals(currentUserId)) { throw new UnauthorizedQueryException(DatawaveErrorCode.QUERY_OWNER_MISMATCH, MessageFormat.format("{0} != {1}", currentUserId, ownerUserId)); } diff --git a/microservices/starters/datawave/pom.xml b/microservices/starters/datawave/pom.xml index 2dc04cc69e8..4f1c5546ff9 100644 --- a/microservices/starters/datawave/pom.xml +++ b/microservices/starters/datawave/pom.xml @@ -4,7 +4,7 @@ gov.nsa.datawave.microservice datawave-microservice-service-parent - 5.0.11 + 5.0.12-3508-RC1 ../../microservice-service-parent/pom.xml spring-boot-starter-datawave @@ -27,7 +27,7 @@ 1.9.25.1 1.9.4 7.33.1 - 4.0.2 + 4.0.3-3508-RC1 3.0.0 4.0.2 31.1-jre diff --git a/microservices/starters/datawave/src/main/java/datawave/microservice/authorization/preauth/ProxiedEntityX509Filter.java b/microservices/starters/datawave/src/main/java/datawave/microservice/authorization/preauth/ProxiedEntityX509Filter.java index ed83d5f2ac4..c1d2bfe91c7 100644 --- a/microservices/starters/datawave/src/main/java/datawave/microservice/authorization/preauth/ProxiedEntityX509Filter.java +++ b/microservices/starters/datawave/src/main/java/datawave/microservice/authorization/preauth/ProxiedEntityX509Filter.java @@ -28,7 +28,7 @@ import org.springframework.util.StringUtils; import datawave.security.authorization.SubjectIssuerDNPair; -import datawave.security.util.ProxiedEntityUtils; +import datawave.security.util.DnUtils; /** * Allows authorization based on a supplied X.509 client certificate (or information from trusted headers) and proxied entities/issuers named in headers. @@ -131,11 +131,11 @@ protected List getSubjectIssuerDNPairs(String proxiedSubjec return null; } else { List proxiedEntities; - Collection entities = Arrays.asList(ProxiedEntityUtils.splitProxiedDNs(proxiedSubjects, true)); + Collection entities = Arrays.asList(DnUtils.splitProxiedDNs(proxiedSubjects, true)); if (!requireIssuers) { proxiedEntities = entities.stream().map(SubjectIssuerDNPair::of).collect(Collectors.toCollection(ArrayList::new)); } else { - Collection issuers = Arrays.asList(ProxiedEntityUtils.splitProxiedDNs(proxiedIssuers, true)); + Collection issuers = Arrays.asList(DnUtils.splitProxiedDNs(proxiedIssuers, true)); if (issuers.size() != entities.size()) { logger.warn("Failing authorization since issuers list (" + proxiedIssuers + ") and entities list (" + proxiedSubjects + ") don't match up."); diff --git a/microservices/starters/datawave/src/main/java/datawave/microservice/authorization/service/RemoteAuthorizationServiceUserDetailsService.java b/microservices/starters/datawave/src/main/java/datawave/microservice/authorization/service/RemoteAuthorizationServiceUserDetailsService.java index e5e434207dc..f2d8f0bb924 100644 --- a/microservices/starters/datawave/src/main/java/datawave/microservice/authorization/service/RemoteAuthorizationServiceUserDetailsService.java +++ b/microservices/starters/datawave/src/main/java/datawave/microservice/authorization/service/RemoteAuthorizationServiceUserDetailsService.java @@ -26,7 +26,7 @@ import datawave.security.authorization.DatawaveUser; import datawave.security.authorization.JWTTokenHandler; import datawave.security.authorization.SubjectIssuerDNPair; -import datawave.security.util.ProxiedEntityUtils; +import datawave.security.util.DnUtils; /** * An {@link AuthenticationUserDetailsService} that retrieves user information from a remote authorization service for a set of proxied entity names, and @@ -94,7 +94,7 @@ private String buildDNChain(ProxiedEntityPreauthPrincipal principal, Function<")) + ">"; // @formatter:on diff --git a/microservices/starters/datawave/src/main/java/datawave/microservice/authorization/user/DatawaveUserDetails.java b/microservices/starters/datawave/src/main/java/datawave/microservice/authorization/user/DatawaveUserDetails.java index ae85234fd13..f0a2b0214be 100644 --- a/microservices/starters/datawave/src/main/java/datawave/microservice/authorization/user/DatawaveUserDetails.java +++ b/microservices/starters/datawave/src/main/java/datawave/microservice/authorization/user/DatawaveUserDetails.java @@ -20,7 +20,7 @@ import datawave.security.authorization.DatawaveUser.UserType; import datawave.security.authorization.ProxiedUserDetails; import datawave.security.authorization.SubjectIssuerDNPair; -import datawave.security.util.ProxiedEntityUtils; +import datawave.security.util.DnUtils; /** * A {@link UserDetails} that represents a set of proxied users. For example, this proxied user could represent a GUI server acting on behalf of a user. The GUI @@ -77,7 +77,7 @@ public String getName() { @Override @JsonIgnore public String getShortName() { - return ProxiedEntityUtils.getShortName(getPrimaryUser().getName()); + return DnUtils.getShortName(getPrimaryUser().getName()); } /** diff --git a/microservices/starters/datawave/src/main/java/datawave/microservice/config/security/util/DnUtilsProperties.java b/microservices/starters/datawave/src/main/java/datawave/microservice/security/util/DnPropertiesConfig.java similarity index 91% rename from microservices/starters/datawave/src/main/java/datawave/microservice/config/security/util/DnUtilsProperties.java rename to microservices/starters/datawave/src/main/java/datawave/microservice/security/util/DnPropertiesConfig.java index dcd6a0c2bfc..5d4c22fd733 100644 --- a/microservices/starters/datawave/src/main/java/datawave/microservice/config/security/util/DnUtilsProperties.java +++ b/microservices/starters/datawave/src/main/java/datawave/microservice/security/util/DnPropertiesConfig.java @@ -1,4 +1,4 @@ -package datawave.microservice.config.security.util; +package datawave.microservice.security.util; import java.util.List; import java.util.regex.Pattern; @@ -10,7 +10,7 @@ @Validated @ConfigurationProperties(prefix = "datawave.security.util") -public class DnUtilsProperties { +public class DnPropertiesConfig { @NotEmpty private String subjectDnPattern; @NotEmpty diff --git a/microservices/starters/datawave/src/main/java/datawave/microservice/config/security/util/DnUtilsConfig.java b/microservices/starters/datawave/src/main/java/datawave/microservice/security/util/DnPropertiesProvider.java similarity index 53% rename from microservices/starters/datawave/src/main/java/datawave/microservice/config/security/util/DnUtilsConfig.java rename to microservices/starters/datawave/src/main/java/datawave/microservice/security/util/DnPropertiesProvider.java index ddb94593a17..aa1d41bf340 100644 --- a/microservices/starters/datawave/src/main/java/datawave/microservice/config/security/util/DnUtilsConfig.java +++ b/microservices/starters/datawave/src/main/java/datawave/microservice/security/util/DnPropertiesProvider.java @@ -1,18 +1,19 @@ -package datawave.microservice.config.security.util; +package datawave.microservice.security.util; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import datawave.microservice.security.util.DnUtils; +import datawave.security.util.DnProperties; @Configuration @ConditionalOnProperty(name = {"datawave.security.util.subjectDnPattern", "datawave.security.util.npeOuList"}) -@EnableConfigurationProperties(DnUtilsProperties.class) -public class DnUtilsConfig { +@EnableConfigurationProperties(DnPropertiesConfig.class) +public class DnPropertiesProvider { + @Bean - public DnUtils dnUtils(DnUtilsProperties dnUtilsProperties) { - return new DnUtils(dnUtilsProperties.getCompiledSubjectDnPattern(), dnUtilsProperties.getNpeOuList()); + public DnProperties dnProperties(DnPropertiesConfig config) { + return new DnProperties(config.getCompiledSubjectDnPattern(), config.getNpeOuList()); } } diff --git a/microservices/starters/query/pom.xml b/microservices/starters/query/pom.xml index 628f46ffeee..260009f7a6d 100644 --- a/microservices/starters/query/pom.xml +++ b/microservices/starters/query/pom.xml @@ -4,7 +4,7 @@ gov.nsa.datawave.microservice datawave-microservice-service-parent - 5.0.11 + 5.0.12-3508-RC1 ../../microservice-service-parent/pom.xml spring-boot-starter-datawave-query @@ -102,6 +102,11 @@ + + gov.nsa.datawave.commons + datawave-commons-security + ${version.datawave-commons-security} + gov.nsa.datawave.core accumulo-utils @@ -296,6 +301,10 @@ gov.nsa.datawave datawave-query-core + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.core accumulo-utils diff --git a/microservices/starters/query/src/main/java/datawave/microservice/query/mapreduce/jobs/OozieJob.java b/microservices/starters/query/src/main/java/datawave/microservice/query/mapreduce/jobs/OozieJob.java index 056856d208a..e6207bf88b4 100644 --- a/microservices/starters/query/src/main/java/datawave/microservice/query/mapreduce/jobs/OozieJob.java +++ b/microservices/starters/query/src/main/java/datawave/microservice/query/mapreduce/jobs/OozieJob.java @@ -17,7 +17,7 @@ import datawave.microservice.authorization.user.DatawaveUserDetails; import datawave.microservice.query.mapreduce.config.MapReduceQueryProperties; import datawave.microservice.query.mapreduce.status.MapReduceQueryStatus; -import datawave.security.util.ProxiedEntityUtils; +import datawave.security.util.DnUtils; import datawave.webservice.common.audit.Auditor; public class OozieJob extends MapReduceJob { @@ -38,7 +38,7 @@ public OozieJob(MapReduceQueryProperties mapReduceQueryProperties) { @Override public String createId(DatawaveUserDetails currentUser) { - return String.join("_", ProxiedEntityUtils.getShortName(currentUser.getPrimaryUser().getName()), UUID.randomUUID().toString()); + return String.join("_", DnUtils.getShortName(currentUser.getPrimaryUser().getName()), UUID.randomUUID().toString()); } @Override diff --git a/pom.xml b/pom.xml index aa849568694..ae425a5c449 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,7 @@ core web-services warehouse + commons scm:git:https://github.com/NationalSecurityAgency/datawave.git @@ -53,7 +54,7 @@ 1C 2.1.4 1.4.1.Final - 1.0.0.Final + 2.1.0.Final 3.20.2 1.11.4 1.14.11 @@ -68,6 +69,7 @@ 2.18.0 3.3 2.6 + 3.20.0 1.2 3.9.0 1.6 @@ -101,7 +103,7 @@ 4.4.8 4.0.4 9.4.21.Final - 2.13.5 + 2.12.7 1.9.13 2.3.3 3.24.0-GA @@ -117,6 +119,7 @@ 0.11.2 20231013 1.19.0 + 2.3.0 4.13.2 5.12.0 1.12.0 @@ -131,7 +134,6 @@ 9.3.0 4.1.42.Final 3.3 - 5.0.3.Final 2.0.9 2.61.0 3.16.3 @@ -144,11 +146,11 @@ ${version.spring} 2.9.6 0.17.0 - 3.1.1.Final - 3.1.1.Final + 3.1.9.Final + 3.1.9.Final 2.3.5.Final - 17.0.1.Final + 26.1.3.Final 5.4.0 3.1.4 2.12.2 @@ -175,7 +177,7 @@ org.wildfly.bom - wildfly-javaee8-with-tools + wildfly-jakartaee8-with-tools ${version.wildfly} pom import @@ -398,6 +400,11 @@ + + gov.nsa.datawave.commons + datawave-commons-security + ${project.version} + gov.nsa.datawave.core accumulo-utils @@ -674,6 +681,11 @@ metrics-cdi ${version.metrics-cdi} + + io.jsonwebtoken + jjwt-api + ${version.jjwt} + io.jsonwebtoken jjwt-impl @@ -1357,24 +1369,6 @@ ${version.weld} provided - - org.picketbox - picketbox - ${version.picketbox} - provided - - - org.picketbox - picketbox-infinispan - ${version.picketbox} - provided - - - dom4j - dom4j - - - org.wildfly wildfly-security @@ -1459,14 +1453,14 @@ org.jboss.arquillian.container - arquillian-weld-ee-embedded-1.1 - ${version.arquillian-weld-ee-embedded} + arquillian-weld-embedded + ${version.arquillian-weld-embedded} test - org.jboss.weld - weld-core - ${version.weld-test} + org.junit-pioneer + junit-pioneer + ${version.junit-pioneer} test diff --git a/warehouse/assemble/webservice/pom.xml b/warehouse/assemble/webservice/pom.xml index 580f7c48c4d..3417b3f0bf3 100644 --- a/warehouse/assemble/webservice/pom.xml +++ b/warehouse/assemble/webservice/pom.xml @@ -68,7 +68,12 @@ org.jboss.resteasy - resteasy-jaxrs + resteasy-core + provided + + + org.jboss.resteasy + resteasy-core-spi provided diff --git a/warehouse/core/pom.xml b/warehouse/core/pom.xml index 0adc95f7400..e055a3fbba4 100644 --- a/warehouse/core/pom.xml +++ b/warehouse/core/pom.xml @@ -75,12 +75,12 @@ protostuff-api - javax.annotation - javax.annotation-api + jakarta.enterprise + jakarta.enterprise.cdi-api - javax.enterprise - cdi-api + javax.annotation + javax.annotation-api net.sf.opencsv diff --git a/warehouse/data-dictionary-core/pom.xml b/warehouse/data-dictionary-core/pom.xml index e2b4c293bf7..d4a29aa74e3 100644 --- a/warehouse/data-dictionary-core/pom.xml +++ b/warehouse/data-dictionary-core/pom.xml @@ -10,6 +10,10 @@ jar ${project.artifactId} + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.microservice dictionary-api @@ -19,8 +23,8 @@ datawave-ws-common - javax.enterprise - cdi-api + jakarta.enterprise + jakarta.enterprise.cdi-api diff --git a/warehouse/edge-dictionary-core/pom.xml b/warehouse/edge-dictionary-core/pom.xml index fc35e5c146d..d59cd8dbd21 100644 --- a/warehouse/edge-dictionary-core/pom.xml +++ b/warehouse/edge-dictionary-core/pom.xml @@ -10,6 +10,10 @@ jar ${project.artifactId} + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.microservice dictionary-api @@ -19,8 +23,8 @@ datawave-ws-common - javax.enterprise - cdi-api + jakarta.enterprise + jakarta.enterprise.cdi-api diff --git a/warehouse/metrics-core/pom.xml b/warehouse/metrics-core/pom.xml index aa693b62e8d..0b880d56619 100644 --- a/warehouse/metrics-core/pom.xml +++ b/warehouse/metrics-core/pom.xml @@ -31,8 +31,8 @@ datawave-query-core - javax.enterprise - cdi-api + jakarta.enterprise + jakarta.enterprise.cdi-api org.easymock diff --git a/warehouse/query-core/pom.xml b/warehouse/query-core/pom.xml index ee7fbd524df..fce8b562b59 100644 --- a/warehouse/query-core/pom.xml +++ b/warehouse/query-core/pom.xml @@ -26,6 +26,10 @@ com.fasterxml.jackson.core jackson-databind + + com.fasterxml.jackson.datatype + jackson-datatype-guava + com.google.code.findbugs annotations @@ -75,6 +79,10 @@ datawave-ssdeep-common ${project.version} + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.core datawave-core-annotation @@ -139,8 +147,8 @@ - javax.enterprise - cdi-api + jakarta.enterprise + jakarta.enterprise.cdi-api @@ -206,7 +214,12 @@ org.jboss.resteasy - resteasy-jaxrs + resteasy-core + true + + + org.jboss.resteasy + resteasy-core-spi true @@ -352,7 +365,7 @@ org.jboss.arquillian.container - arquillian-weld-ee-embedded-1.1 + arquillian-weld-embedded test @@ -388,12 +401,7 @@ org.jboss.spec.javax.transaction - jboss-transaction-api_1.2_spec - test - - - org.jboss.weld - weld-core + jboss-transaction-api_1.3_spec test @@ -417,11 +425,6 @@ ${version.mockito} test - - org.picketbox - picketbox - test - org.springframework spring-test diff --git a/warehouse/query-core/src/test/java/datawave/query/cardinality/TestCardinalityWithQuery.java b/warehouse/query-core/src/test/java/datawave/query/cardinality/TestCardinalityWithQuery.java index 0b985b5bf65..0244b55be7c 100644 --- a/warehouse/query-core/src/test/java/datawave/query/cardinality/TestCardinalityWithQuery.java +++ b/warehouse/query-core/src/test/java/datawave/query/cardinality/TestCardinalityWithQuery.java @@ -50,7 +50,7 @@ import datawave.security.authorization.DatawaveUser; import datawave.security.authorization.DatawaveUser.UserType; import datawave.security.authorization.SubjectIssuerDNPair; -import datawave.security.util.DnUtils; +import datawave.security.util.DnProperties; import datawave.webservice.query.runner.RunningQuery; import datawave.webservice.result.EventQueryResponseBase; @@ -93,7 +93,7 @@ public static void setUp() throws Exception { @Before public void setup() throws Exception { - System.setProperty(DnUtils.NPE_OU_PROPERTY, "iamnotaperson"); + System.setProperty(DnProperties.NPE_OU_PROPERTY, "iamnotaperson"); temporaryFolder = tempDir.newFolder().toPath(); logic = new ShardQueryLogic(); diff --git a/warehouse/query-core/src/test/java/datawave/query/tables/RemoteEdgeQueryLogicHttpTest.java b/warehouse/query-core/src/test/java/datawave/query/tables/RemoteEdgeQueryLogicHttpTest.java index 8ccce500fdd..2d54c566dea 100644 --- a/warehouse/query-core/src/test/java/datawave/query/tables/RemoteEdgeQueryLogicHttpTest.java +++ b/warehouse/query-core/src/test/java/datawave/query/tables/RemoteEdgeQueryLogicHttpTest.java @@ -53,9 +53,9 @@ import datawave.microservice.query.QueryImpl; import datawave.microservice.query.QueryParameters; import datawave.security.authorization.DatawavePrincipal; -import datawave.security.util.DnUtils; +import datawave.security.util.DnProperties; import datawave.webservice.common.json.DefaultMapperDecorator; -import datawave.webservice.common.remote.TestJSSESecurityDomain; +import datawave.webservice.common.remote.TestSSLStores; import datawave.webservice.query.remote.RemoteQueryServiceImpl; import datawave.webservice.query.result.edge.DefaultEdge; import datawave.webservice.query.result.edge.EdgeBase; @@ -104,7 +104,7 @@ private void setContent(InputStream content) throws IOException { @Before public void setup() throws Exception { final ObjectMapper objectMapper = new DefaultMapperDecorator().decorate(new ObjectMapper()); - System.setProperty(DnUtils.SUBJECT_DN_PATTERN_PROPERTY, ".*ou=server.*"); + System.setProperty(DnProperties.SUBJECT_DN_PATTERN_PROPERTY, ".*ou=server.*"); KeyPairGenerator generater = KeyPairGenerator.getInstance("RSA"); generater.initialize(keysize); KeyPair keypair = generater.generateKeyPair(); @@ -218,7 +218,7 @@ public void handle(HttpExchange exchange) throws IOException { remote.setExecutorService(null); remote.setObjectMapperDecorator(new DefaultMapperDecorator()); remote.setResponseObjectFactory(new DefaultResponseObjectFactory()); - remote.setJsseSecurityDomain(new TestJSSESecurityDomain(alias, privKey, keyPass, chain)); + remote.setSslStores(new TestSSLStores(alias, privKey, keyPass, chain)); remote.setNextQueryResponseClass(remote.getResponseObjectFactory().getEdgeQueryResponse().getClass()); logic.setRemoteQueryService(remote); diff --git a/warehouse/query-core/src/test/java/datawave/query/tables/RemoteQueryServiceTestUtil.java b/warehouse/query-core/src/test/java/datawave/query/tables/RemoteQueryServiceTestUtil.java index 5c7ad67954f..3cf2dcaaf46 100644 --- a/warehouse/query-core/src/test/java/datawave/query/tables/RemoteQueryServiceTestUtil.java +++ b/warehouse/query-core/src/test/java/datawave/query/tables/RemoteQueryServiceTestUtil.java @@ -60,10 +60,10 @@ import datawave.security.authorization.DatawavePrincipal; import datawave.security.authorization.DatawaveUser; import datawave.security.authorization.SubjectIssuerDNPair; -import datawave.security.util.DnUtils; +import datawave.security.util.DnProperties; import datawave.webservice.common.json.DefaultMapperDecorator; import datawave.webservice.common.remote.RemoteServiceUtil; -import datawave.webservice.common.remote.TestJSSESecurityDomain; +import datawave.webservice.common.remote.TestSSLStores; import datawave.webservice.query.remote.RemoteQueryServiceImpl; import datawave.webservice.query.result.event.DefaultEvent; import datawave.webservice.query.result.event.DefaultField; @@ -104,7 +104,7 @@ public void initialize() throws IOException { super.initialize(); final ObjectMapper objectMapper = new DefaultMapperDecorator().decorate(new ObjectMapper()); - System.setProperty(DnUtils.SUBJECT_DN_PATTERN_PROPERTY, ".*ou=server.*"); + System.setProperty(DnProperties.SUBJECT_DN_PATTERN_PROPERTY, ".*ou=server.*"); KeyPairGenerator generater = null; try { generater = KeyPairGenerator.getInstance("RSA"); @@ -200,7 +200,7 @@ public RemoteQueryService getRemoteService() { remote.setExecutorService(null); remote.setObjectMapperDecorator(new DefaultMapperDecorator()); remote.setResponseObjectFactory(new DefaultResponseObjectFactory()); - remote.setJsseSecurityDomain(new TestJSSESecurityDomain(alias, privateKey, keyPass, chain)); + remote.setSslStores(new TestSSLStores(alias, privateKey, keyPass, chain)); return remote; } diff --git a/warehouse/query-core/src/test/java/datawave/query/testframework/AbstractFunctionalQuery.java b/warehouse/query-core/src/test/java/datawave/query/testframework/AbstractFunctionalQuery.java index f0b5d52cd8b..3f7106e29b8 100644 --- a/warehouse/query-core/src/test/java/datawave/query/testframework/AbstractFunctionalQuery.java +++ b/warehouse/query-core/src/test/java/datawave/query/testframework/AbstractFunctionalQuery.java @@ -85,7 +85,7 @@ import datawave.security.authorization.DatawaveUser; import datawave.security.authorization.ProxiedUserDetails; import datawave.security.authorization.SubjectIssuerDNPair; -import datawave.security.util.DnUtils; +import datawave.security.util.DnProperties; import datawave.webservice.query.exception.QueryException; import datawave.webservice.query.result.event.EventBase; import datawave.webservice.query.result.event.FieldBase; @@ -114,7 +114,7 @@ public abstract class AbstractFunctionalQuery implements QueryLogicTestHarness.T static { TimeZone.setDefault(TimeZone.getTimeZone("GMT")); System.setProperty("file.encoding", StandardCharsets.UTF_8.name()); - System.setProperty(DnUtils.NPE_OU_PROPERTY, "iamnotaperson"); + System.setProperty(DnProperties.NPE_OU_PROPERTY, "iamnotaperson"); try { File dir = new File(ClassLoader.getSystemClassLoader().getResource(".").toURI()); File targetDir = dir.getParentFile(); diff --git a/web-services/accumulo/pom.xml b/web-services/accumulo/pom.xml index e335b262bdc..98d97983488 100644 --- a/web-services/accumulo/pom.xml +++ b/web-services/accumulo/pom.xml @@ -26,6 +26,10 @@ dnsjava dnsjava + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.core base-rest-responses @@ -81,14 +85,22 @@ org.apache.hadoop hadoop-client-api + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpcore + com.fasterxml.woodstox woodstox-core provided - javax.enterprise - cdi-api + jakarta.enterprise + jakarta.enterprise.cdi-api provided @@ -98,7 +110,12 @@ org.jboss.resteasy - resteasy-jaxrs + resteasy-core + provided + + + org.jboss.resteasy + resteasy-core-spi provided diff --git a/web-services/annotations/pom.xml b/web-services/annotations/pom.xml index 0331ff7e474..f8970e4d590 100644 --- a/web-services/annotations/pom.xml +++ b/web-services/annotations/pom.xml @@ -41,6 +41,10 @@ datawave-query-core ${project.version} + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.core datawave-core-annotation @@ -69,7 +73,12 @@ org.jboss.resteasy - resteasy-jaxrs + resteasy-core + provided + + + org.jboss.resteasy + resteasy-core-spi provided @@ -131,8 +140,8 @@ test - javax.enterprise - cdi-api + jakarta.enterprise + jakarta.enterprise.cdi-api test @@ -152,7 +161,7 @@ org.jboss.arquillian.container - arquillian-weld-ee-embedded-1.1 + arquillian-weld-embedded test @@ -170,11 +179,6 @@ shrinkwrap-api test - - org.jboss.weld - weld-core - test - org.jboss.weld weld-core-impl @@ -185,11 +189,6 @@ mockito-core test - - org.picketbox - picketbox - test - org.powermock powermock-api-easymock diff --git a/web-services/atom/pom.xml b/web-services/atom/pom.xml index 72aaff3868c..7fccfab8c63 100644 --- a/web-services/atom/pom.xml +++ b/web-services/atom/pom.xml @@ -10,6 +10,10 @@ ejb ${project.artifactId} + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.webservices datawave-ws-common @@ -63,8 +67,8 @@ provided - javax.enterprise - cdi-api + jakarta.enterprise + jakarta.enterprise.cdi-api provided @@ -79,7 +83,12 @@ org.jboss.resteasy - resteasy-jaxrs + resteasy-core + provided + + + org.jboss.resteasy + resteasy-core-spi provided @@ -99,7 +108,7 @@ org.jboss.spec.javax.transaction - jboss-transaction-api_1.2_spec + jboss-transaction-api_1.3_spec provided diff --git a/web-services/atom/src/main/java/datawave/webservice/atom/AtomKeyValueParser.java b/web-services/atom/src/main/java/datawave/webservice/atom/AtomKeyValueParser.java index 00516277344..9a43c3103b6 100644 --- a/web-services/atom/src/main/java/datawave/webservice/atom/AtomKeyValueParser.java +++ b/web-services/atom/src/main/java/datawave/webservice/atom/AtomKeyValueParser.java @@ -14,7 +14,7 @@ import org.apache.accumulo.core.data.Key; import org.apache.accumulo.core.data.Value; import org.apache.accumulo.core.iterators.LongCombiner; -import org.jboss.resteasy.util.Base64; +import org.apache.commons.net.util.Base64; public class AtomKeyValueParser { @@ -123,13 +123,13 @@ public static AtomKeyValueParser parse(Key key, Value value) throws IOException } public static String encodeId(String id) throws UnsupportedEncodingException { - String key64 = Base64.encodeBytes(id.getBytes()); + String key64 = Base64.encodeBase64String(id.getBytes()); return URLEncoder.encode(key64, "UTF-8"); } public static String decodeId(String encodedId) throws IOException { String key64 = URLDecoder.decode(encodedId, "UTF-8"); - byte[] bKey = Base64.decode(key64); + byte[] bKey = Base64.decodeBase64(key64); return new String(bKey); } } diff --git a/web-services/atom/src/main/java/datawave/webservice/atom/AtomServiceBean.java b/web-services/atom/src/main/java/datawave/webservice/atom/AtomServiceBean.java index 9cc2fc468f2..a692dd93219 100644 --- a/web-services/atom/src/main/java/datawave/webservice/atom/AtomServiceBean.java +++ b/web-services/atom/src/main/java/datawave/webservice/atom/AtomServiceBean.java @@ -47,11 +47,11 @@ import org.apache.accumulo.core.data.Range; import org.apache.accumulo.core.data.Value; import org.apache.accumulo.core.security.Authorizations; +import org.apache.commons.net.util.Base64; import org.apache.deltaspike.core.api.config.ConfigProperty; import org.apache.hadoop.io.Text; import org.apache.log4j.Logger; import org.jboss.resteasy.annotations.GZIP; -import org.jboss.resteasy.util.Base64; import datawave.annotation.Required; import datawave.core.common.connection.AccumuloConnectionFactory; @@ -346,7 +346,7 @@ public Entry getEntry(@Required("category") @PathParam("category") String catego private Key deserializeKey(String k) throws Exception { String key64 = URLDecoder.decode(k, "UTF-8"); - byte[] bKey = Base64.decode(key64); + byte[] bKey = Base64.decodeBase64(key64); ByteArrayInputStream bais = new ByteArrayInputStream(bKey); DataInputStream in = new DataInputStream(bais); Key key = new Key(); @@ -359,7 +359,7 @@ private String serializeKey(Key key) throws Exception { DataOutputStream out = new DataOutputStream(baos); key.write(out); out.close(); - String key64 = Base64.encodeBytes(baos.toByteArray()); + String key64 = Base64.encodeBase64String(baos.toByteArray()); return URLEncoder.encode(key64, "UTF-8"); } diff --git a/web-services/cached-results/pom.xml b/web-services/cached-results/pom.xml index 27604329ea1..c431e59ded9 100644 --- a/web-services/cached-results/pom.xml +++ b/web-services/cached-results/pom.xml @@ -26,6 +26,10 @@ gov.nsa.datawave datawave-core + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.core datawave-core-cached-results @@ -46,8 +50,8 @@ query-metric-api - javax.enterprise - cdi-api + jakarta.enterprise + jakarta.enterprise.cdi-api org.apache.commons @@ -101,7 +105,12 @@ org.jboss.resteasy - resteasy-jaxrs + resteasy-core + provided + + + org.jboss.resteasy + resteasy-core-spi provided diff --git a/web-services/client/pom.xml b/web-services/client/pom.xml index 8c10e90c0ae..47d0cc75ee9 100644 --- a/web-services/client/pom.xml +++ b/web-services/client/pom.xml @@ -152,7 +152,12 @@ org.jboss.resteasy - resteasy-jaxrs + resteasy-core + provided + + + org.jboss.resteasy + resteasy-core-spi provided diff --git a/web-services/common-util/pom.xml b/web-services/common-util/pom.xml index 1ec00254b44..7888485b760 100644 --- a/web-services/common-util/pom.xml +++ b/web-services/common-util/pom.xml @@ -31,6 +31,10 @@ datawave-common ${project.version} + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.core accumulo-utils @@ -160,8 +164,8 @@ provided - javax.enterprise - cdi-api + jakarta.enterprise + jakarta.enterprise.cdi-api provided @@ -187,7 +191,12 @@ org.jboss.resteasy - resteasy-jaxrs + resteasy-core + provided + + + org.jboss.resteasy + resteasy-core-spi provided diff --git a/web-services/common-util/src/main/java/datawave/resteasy/interceptor/BaseMethodStatsInterceptor.java b/web-services/common-util/src/main/java/datawave/resteasy/interceptor/BaseMethodStatsInterceptor.java index 56681724e3f..30399242ceb 100644 --- a/web-services/common-util/src/main/java/datawave/resteasy/interceptor/BaseMethodStatsInterceptor.java +++ b/web-services/common-util/src/main/java/datawave/resteasy/interceptor/BaseMethodStatsInterceptor.java @@ -1,7 +1,7 @@ package datawave.resteasy.interceptor; -import static datawave.webservice.metrics.Constants.REQUEST_LOGIN_TIME_HEADER; -import static datawave.webservice.metrics.Constants.REQUEST_START_TIME_HEADER; +import static datawave.security.util.SecurityConstants.REQUEST_LOGIN_TIME_HEADER; +import static datawave.security.util.SecurityConstants.REQUEST_START_TIME_HEADER; import java.io.IOException; import java.io.OutputStream; @@ -22,7 +22,7 @@ import javax.ws.rs.ext.WriterInterceptorContext; import org.apache.log4j.Logger; -import org.jboss.resteasy.core.interception.PreMatchContainerRequestContext; +import org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext; import org.jboss.resteasy.specimpl.MultivaluedTreeMap; import org.jboss.resteasy.spi.Failure; import org.jboss.resteasy.util.CaseInsensitiveMap; diff --git a/web-services/common-util/src/main/java/datawave/resteasy/interceptor/CreateQuerySessionIDFilter.java b/web-services/common-util/src/main/java/datawave/resteasy/interceptor/CreateQuerySessionIDFilter.java index 8e14cd7868b..468303cdd2a 100644 --- a/web-services/common-util/src/main/java/datawave/resteasy/interceptor/CreateQuerySessionIDFilter.java +++ b/web-services/common-util/src/main/java/datawave/resteasy/interceptor/CreateQuerySessionIDFilter.java @@ -12,7 +12,7 @@ import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.jboss.resteasy.core.ResourceMethodInvoker; -import org.jboss.resteasy.util.FindAnnotation; +import org.jboss.resteasy.spi.util.FindAnnotation; import org.jboss.resteasy.util.HttpHeaderNames; import datawave.Constants; diff --git a/web-services/common-util/src/main/java/datawave/resteasy/interceptor/LoggingInterceptor.java b/web-services/common-util/src/main/java/datawave/resteasy/interceptor/LoggingInterceptor.java index 66d83ac69fe..793b8cb5f27 100644 --- a/web-services/common-util/src/main/java/datawave/resteasy/interceptor/LoggingInterceptor.java +++ b/web-services/common-util/src/main/java/datawave/resteasy/interceptor/LoggingInterceptor.java @@ -13,7 +13,7 @@ import javax.ws.rs.ext.WriterInterceptorContext; import org.apache.log4j.Logger; -import org.jboss.resteasy.core.interception.PreMatchContainerRequestContext; +import org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext; @Provider @Priority(Priorities.USER) diff --git a/web-services/common-util/src/main/java/datawave/resteasy/util/DateParamConverterProvider.java b/web-services/common-util/src/main/java/datawave/resteasy/util/DateParamConverterProvider.java index 2c050206c96..87954522c3a 100644 --- a/web-services/common-util/src/main/java/datawave/resteasy/util/DateParamConverterProvider.java +++ b/web-services/common-util/src/main/java/datawave/resteasy/util/DateParamConverterProvider.java @@ -8,7 +8,7 @@ import javax.ws.rs.ext.ParamConverterProvider; import javax.ws.rs.ext.Provider; -import org.jboss.resteasy.util.FindAnnotation; +import org.jboss.resteasy.spi.util.FindAnnotation; import datawave.annotation.DateFormat; diff --git a/web-services/common-util/src/main/java/datawave/resteasy/util/RequiredProcessor.java b/web-services/common-util/src/main/java/datawave/resteasy/util/RequiredProcessor.java index 8c24bbf21e0..dd78215649d 100644 --- a/web-services/common-util/src/main/java/datawave/resteasy/util/RequiredProcessor.java +++ b/web-services/common-util/src/main/java/datawave/resteasy/util/RequiredProcessor.java @@ -5,7 +5,7 @@ import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.jboss.resteasy.spi.StringParameterUnmarshaller; -import org.jboss.resteasy.util.FindAnnotation; +import org.jboss.resteasy.spi.util.FindAnnotation; import datawave.annotation.Required; diff --git a/web-services/common-util/src/main/java/datawave/security/util/DnUtils.java b/web-services/common-util/src/main/java/datawave/security/util/DnUtils.java deleted file mode 100644 index 8faa4ffbc0d..00000000000 --- a/web-services/common-util/src/main/java/datawave/security/util/DnUtils.java +++ /dev/null @@ -1,137 +0,0 @@ -package datawave.security.util; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Properties; -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import datawave.security.authorization.SubjectIssuerDNPair; - -public class DnUtils { - - /** Config for injecting NPE OU identifiers */ - public static final String PROPS_RESOURCE = "dnutils.properties"; - - private static final Properties PROPS = new Properties(); - - public static final String SUBJECT_DN_PATTERN_PROPERTY = "subject.dn.pattern"; - - private static final Pattern SUBJECT_DN_PATTERN; - - /** Property containing a comma-delimited list of OUs */ - public static final String NPE_OU_PROPERTY = "npe.ou.entries"; - - /** Parsed NPE OU identifiers */ - static final List NPE_OU_LIST; - - private static final Logger log = LoggerFactory.getLogger(DnUtils.class); - - private static final datawave.microservice.security.util.DnUtils dnUtils; - - static { - InputStream in = null; - try { - in = DnUtils.class.getClassLoader().getResourceAsStream(PROPS_RESOURCE); - PROPS.load(in); - } catch (Throwable t) { - log.error(PROPS_RESOURCE + " could not be loaded!", t); - throw new RuntimeException(t); - } finally { - if (null != in) { - try { - in.close(); - } catch (IOException e) { - log.warn("Failed to close input stream", e); - } - } - } - - String subjectDnPattern = System.getProperty(SUBJECT_DN_PATTERN_PROPERTY, PROPS.getProperty(SUBJECT_DN_PATTERN_PROPERTY)); - try { - if (null == subjectDnPattern || subjectDnPattern.isEmpty()) { - throw new IllegalStateException(SUBJECT_DN_PATTERN_PROPERTY + " property value cannot be null"); - } - SUBJECT_DN_PATTERN = Pattern.compile(subjectDnPattern, Pattern.CASE_INSENSITIVE); - } catch (Throwable t) { - log.error(SUBJECT_DN_PATTERN_PROPERTY + " = '" + subjectDnPattern + "' could not be compiled", t); - throw new RuntimeException(t); - } - - List npeOUs = new ArrayList<>(); - String ouString = System.getProperty(NPE_OU_PROPERTY, PROPS.getProperty(NPE_OU_PROPERTY)); - if (null == ouString || ouString.isEmpty()) { - throw new IllegalStateException("No '" + NPE_OU_PROPERTY + "' value has been configured"); - } - // Normalize and load... - String[] ouArray = ouString.split(","); - for (String ou : ouArray) { - npeOUs.add(ou.trim().toUpperCase()); - } - NPE_OU_LIST = Collections.unmodifiableList(npeOUs); - - dnUtils = new datawave.microservice.security.util.DnUtils(SUBJECT_DN_PATTERN, NPE_OU_LIST); - } - - public static String[] splitProxiedDNs(String proxiedDNs, boolean allowDups) { - return datawave.microservice.security.util.DnUtils.splitProxiedDNs(proxiedDNs, allowDups); - } - - public static String[] splitProxiedSubjectIssuerDNs(String proxiedDNs) { - return datawave.microservice.security.util.DnUtils.splitProxiedSubjectIssuerDNs(proxiedDNs); - } - - public static String buildProxiedDN(String... dns) { - return datawave.microservice.security.util.DnUtils.buildProxiedDN(dns); - } - - public static Collection buildNormalizedDNList(String subjectDN, String issuerDN, String proxiedSubjectDNs, String proxiedIssuerDNs) { - return dnUtils.buildNormalizedDNList(subjectDN, issuerDN, proxiedSubjectDNs, proxiedIssuerDNs); - } - - public static String buildNormalizedProxyDN(String subjectDN, String issuerDN, String proxiedSubjectDNs, String proxiedIssuerDNs) { - return dnUtils.buildNormalizedProxyDN(subjectDN, issuerDN, proxiedSubjectDNs, proxiedIssuerDNs); - } - - public static String buildNormalizedProxyDN(List dns) { - return datawave.microservice.security.util.DnUtils.buildNormalizedProxyDN(dns); - } - - public static String getCommonName(String dn) { - return datawave.microservice.security.util.DnUtils.getCommonName(dn); - } - - public static String[] getOrganizationalUnits(String dn) { - return datawave.microservice.security.util.DnUtils.getOrganizationalUnits(dn); - } - - public static String getShortName(String dn) { - return datawave.microservice.security.util.DnUtils.getShortName(dn); - } - - public static boolean isServerDN(String dn) { - return dnUtils.isServerDN(dn); - } - - public static String getUserDN(String[] dns) { - return dnUtils.getUserDN(dns); - } - - public static String getUserDN(String[] dns, boolean issuerDNs) { - return dnUtils.getUserDN(dns, issuerDNs); - } - - public static String[] getComponents(String dn, String componentName) { - return dnUtils.getComponents(dn, componentName); - } - - public static String normalizeDN(String userName) { - return datawave.microservice.security.util.DnUtils.normalizeDN(userName); - } -} diff --git a/web-services/common-util/src/main/java/datawave/webservice/metrics/Constants.java b/web-services/common-util/src/main/java/datawave/webservice/metrics/Constants.java deleted file mode 100644 index b22bfa6e5ec..00000000000 --- a/web-services/common-util/src/main/java/datawave/webservice/metrics/Constants.java +++ /dev/null @@ -1,12 +0,0 @@ -package datawave.webservice.metrics; - -public interface Constants { - /** - * An internal header used to store the start time of the HTTP request, as retrieved from the web container (e.g., Undertow). - */ - String REQUEST_START_TIME_HEADER = "X-Internal-RequestStartTimeNanos"; - /** - * An internal header used to store the time required to authenticate the user for the current request. - */ - String REQUEST_LOGIN_TIME_HEADER = "X-Internal-RequestLoginTimeMillis"; -} diff --git a/web-services/common-util/src/test/java/datawave/resteasy/interceptor/CreateQuerySessionIDFilterTest.java b/web-services/common-util/src/test/java/datawave/resteasy/interceptor/CreateQuerySessionIDFilterTest.java index 6ae6e3dd738..1cf3cb15fa1 100644 --- a/web-services/common-util/src/test/java/datawave/resteasy/interceptor/CreateQuerySessionIDFilterTest.java +++ b/web-services/common-util/src/test/java/datawave/resteasy/interceptor/CreateQuerySessionIDFilterTest.java @@ -16,8 +16,8 @@ import org.easymock.Mock; import org.easymock.MockType; import org.jboss.resteasy.core.ResourceMethodInvoker; -import org.jboss.resteasy.core.interception.ContainerResponseContextImpl; -import org.jboss.resteasy.core.interception.ResponseContainerRequestContext; +import org.jboss.resteasy.core.interception.jaxrs.ContainerResponseContextImpl; +import org.jboss.resteasy.core.interception.jaxrs.ResponseContainerRequestContext; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.specimpl.BuiltResponse; diff --git a/web-services/common-util/src/test/java/datawave/security/authorization/DatawavePrincipalTest.java b/web-services/common-util/src/test/java/datawave/security/authorization/DatawavePrincipalTest.java deleted file mode 100644 index 8e894134d94..00000000000 --- a/web-services/common-util/src/test/java/datawave/security/authorization/DatawavePrincipalTest.java +++ /dev/null @@ -1,200 +0,0 @@ -package datawave.security.authorization; - -import java.util.Arrays; -import java.util.Collection; -import java.util.stream.Collectors; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import com.google.common.collect.Lists; - -import datawave.security.authorization.DatawaveUser.UserType; - -public class DatawavePrincipalTest { - - private DatawaveUser finalServer; - private DatawaveUser server1; - private DatawaveUser server2; - private DatawaveUser server3; - private DatawaveUser user; - - final private String finalConnectionServerSubjectDn = "cn=finalserver"; - final private String server1SubjectDn = "cn=server1"; - final private String server2SubjectDn = "cn=server2"; - final private String server3SubjectDn = "cn=server3"; - final private String userSubjectDn = "cn=user"; - final private String issuerDn = "cn=issuer"; - - @Before - public void setUp() throws Exception { - long now = System.currentTimeMillis(); - SubjectIssuerDNPair finalConnectionServerDn = SubjectIssuerDNPair.of(finalConnectionServerSubjectDn, issuerDn); - SubjectIssuerDNPair server1Dn = SubjectIssuerDNPair.of(server1SubjectDn, issuerDn); - SubjectIssuerDNPair server2Dn = SubjectIssuerDNPair.of(server2SubjectDn, issuerDn); - SubjectIssuerDNPair server3Dn = SubjectIssuerDNPair.of(server3SubjectDn, issuerDn); - SubjectIssuerDNPair userDn = SubjectIssuerDNPair.of(userSubjectDn, issuerDn); - finalServer = new DatawaveUser(finalConnectionServerDn, UserType.SERVER, null, null, null, now); - server1 = new DatawaveUser(server1Dn, UserType.SERVER, null, null, null, now); - server2 = new DatawaveUser(server2Dn, UserType.SERVER, null, null, null, now); - server3 = new DatawaveUser(server3Dn, UserType.SERVER, null, null, null, now); - user = new DatawaveUser(userDn, UserType.USER, null, null, null, now); - } - - @Test - public void GetPrimaryUserTest() { - // direct call from a server - DatawavePrincipal dp = new DatawavePrincipal(Lists.newArrayList(finalServer)); - Assert.assertEquals(finalConnectionServerSubjectDn, dp.getPrimaryUser().getDn().subjectDN()); - - // direct call from a user - dp = new DatawavePrincipal(Lists.newArrayList(user)); - Assert.assertEquals(userSubjectDn, dp.getPrimaryUser().getDn().subjectDN()); - - // call from finalConnectionServer proxying initial caller server1 - dp = new DatawavePrincipal(Lists.newArrayList(server1, finalServer)); - Assert.assertEquals(server1SubjectDn, dp.getPrimaryUser().getDn().subjectDN()); - - // call from finalConnectionServer proxying initial caller server1 through server2 - dp = new DatawavePrincipal(Lists.newArrayList(server1, server2, finalServer)); - Assert.assertEquals(server1SubjectDn, dp.getPrimaryUser().getDn().subjectDN()); - - // call from finalConnectionServer proxying initial caller server1 through server2 and server3 - dp = new DatawavePrincipal(Lists.newArrayList(server1, server2, server3, finalServer)); - Assert.assertEquals(server1SubjectDn, dp.getPrimaryUser().getDn().subjectDN()); - - // these tests are for case where a UserType.USER appears anywhere in the proxiedUsers collection - dp = new DatawavePrincipal(Lists.newArrayList(user, server1, server2, server3)); - Assert.assertEquals(userSubjectDn, dp.getPrimaryUser().getDn().subjectDN()); - - dp = new DatawavePrincipal(Lists.newArrayList(server1, user, server2, server3)); - Assert.assertEquals(userSubjectDn, dp.getPrimaryUser().getDn().subjectDN()); - - dp = new DatawavePrincipal(Lists.newArrayList(server1, server2, user, server3)); - Assert.assertEquals(userSubjectDn, dp.getPrimaryUser().getDn().subjectDN()); - - dp = new DatawavePrincipal(Lists.newArrayList(server1, server2, server3, user)); - Assert.assertEquals(userSubjectDn, dp.getPrimaryUser().getDn().subjectDN()); - } - - @Test - public void GetProxyServersTest() { - // direct call from finalServer - DatawavePrincipal dp = new DatawavePrincipal(Lists.newArrayList(finalServer)); - Assert.assertEquals(null, dp.getProxyServers()); - - // direct call from user - dp = new DatawavePrincipal(Lists.newArrayList(user)); - Assert.assertEquals(null, dp.getProxyServers()); - - // call from finalServer proxying initial caller server1 - dp = new DatawavePrincipal(Lists.newArrayList(server1, finalServer)); - Assert.assertEquals(Arrays.asList(finalConnectionServerSubjectDn), dp.getProxyServers()); - - // call from finalServer proxying initial caller server1 through server2 - dp = new DatawavePrincipal(Lists.newArrayList(server1, server2, finalServer)); - Assert.assertEquals(Arrays.asList(server2SubjectDn, finalConnectionServerSubjectDn), dp.getProxyServers()); - - // call from finalServer proxying initial caller server1 through server2 and server3 - dp = new DatawavePrincipal(Lists.newArrayList(server1, server2, server3, finalServer)); - Assert.assertEquals(Arrays.asList(server2SubjectDn, server3SubjectDn, finalConnectionServerSubjectDn), dp.getProxyServers()); - - // these tests are for cases where a UserType.USER appears anywhere in the proxiedUsers collection - - dp = new DatawavePrincipal(Lists.newArrayList(user, server1, server2, server3)); - Assert.assertEquals(Arrays.asList(server1SubjectDn, server2SubjectDn, server3SubjectDn), dp.getProxyServers()); - - dp = new DatawavePrincipal(Lists.newArrayList(server1, user, server2, server3)); - Assert.assertEquals(Arrays.asList(server1SubjectDn, server2SubjectDn, server3SubjectDn), dp.getProxyServers()); - - dp = new DatawavePrincipal(Lists.newArrayList(server1, server2, user, server3)); - Assert.assertEquals(Arrays.asList(server1SubjectDn, server2SubjectDn, server3SubjectDn), dp.getProxyServers()); - - // this case would be very odd -- call from user proxying initial caller server1 through server2 through server3 - dp = new DatawavePrincipal(Lists.newArrayList(server1, server2, server3, user)); - Assert.assertEquals(Arrays.asList(server1SubjectDn, server2SubjectDn, server3SubjectDn), dp.getProxyServers()); - } - - private String joinNames(Collection datawaveUsers) { - return datawaveUsers.stream().map(DatawaveUser::getName).collect(Collectors.joining(" -> ")); - } - - @Test - public void GetNameTest() { - // direct call from finalServer - DatawavePrincipal dp = new DatawavePrincipal(Lists.newArrayList(finalServer)); - Assert.assertEquals(joinNames(Lists.newArrayList(finalServer)), dp.getName()); - - // direct call from user - dp = new DatawavePrincipal(Lists.newArrayList(user)); - Assert.assertEquals(joinNames(Lists.newArrayList(user)), dp.getName()); - - // call from finalServer proxying initial caller server1 - dp = new DatawavePrincipal(Lists.newArrayList(server1, finalServer)); - Assert.assertEquals(joinNames(Lists.newArrayList(server1, finalServer)), dp.getName()); - - // call from finalServer proxying initial caller server1 through server2 - dp = new DatawavePrincipal(Lists.newArrayList(server1, server2, finalServer)); - Assert.assertEquals(joinNames(Lists.newArrayList(server1, server2, finalServer)), dp.getName()); - - // call from finalServer proxying initial caller server1 through server2 and server3 - dp = new DatawavePrincipal(Lists.newArrayList(server1, server2, server3, finalServer)); - Assert.assertEquals(joinNames(Lists.newArrayList(server1, server2, server3, finalServer)), dp.getName()); - - // these tests are for cases where a UserType.USER appears anywhere in the proxiedUsers collection - - // this first case would be very odd -- call from user proxying initial caller server1 through server2 through server3 - dp = new DatawavePrincipal(Lists.newArrayList(user, server1, server2, server3)); - Assert.assertEquals(joinNames(Lists.newArrayList(user, server1, server2, server3)), dp.getName()); - - dp = new DatawavePrincipal(Lists.newArrayList(server1, user, server2, server3)); - Assert.assertEquals(joinNames(Lists.newArrayList(user, server1, server2, server3)), dp.getName()); - - dp = new DatawavePrincipal(Lists.newArrayList(server1, server2, user, server3)); - Assert.assertEquals(joinNames(Lists.newArrayList(user, server1, server2, server3)), dp.getName()); - - dp = new DatawavePrincipal(Lists.newArrayList(server1, server2, server3, user)); - Assert.assertEquals(joinNames(Lists.newArrayList(user, server1, server2, server3)), dp.getName()); - } - - @Test - public void OrderProxiedUsers() { - - // call from finalServer proxying initial caller server1 - Assert.assertEquals(Lists.newArrayList(server1, finalServer), DatawavePrincipal.orderProxiedUsers(Lists.newArrayList(server1, finalServer))); - - // call from finalServer proxying initial caller server1 through server2 - Assert.assertEquals(Lists.newArrayList(server1, server2, finalServer), - DatawavePrincipal.orderProxiedUsers(Lists.newArrayList(server1, server2, finalServer))); - - // call from finalServer proxying initial caller server1 through server2 and server3 - Assert.assertEquals(Lists.newArrayList(server1, server2, server3, finalServer), - DatawavePrincipal.orderProxiedUsers(Lists.newArrayList(server1, server2, server3, finalServer))); - - // these tests are for cases where a UserType.USER appears anywhere in the proxiedUsers collection - - // this first case would be very odd -- call from user proxying initial caller server1 through server2 through server3 - Assert.assertEquals(Lists.newArrayList(user, server1, server2, server3), - DatawavePrincipal.orderProxiedUsers(Lists.newArrayList(user, server1, server2, server3))); - - Assert.assertEquals(Lists.newArrayList(user, server1, server2, server3), - DatawavePrincipal.orderProxiedUsers(Lists.newArrayList(server1, user, server2, server3))); - - Assert.assertEquals(Lists.newArrayList(user, server1, server2, server3), - DatawavePrincipal.orderProxiedUsers(Lists.newArrayList(server1, server2, user, server3))); - - Assert.assertEquals(Lists.newArrayList(user, server1, server2, server3), - DatawavePrincipal.orderProxiedUsers(Lists.newArrayList(server1, server2, server3, user))); - } - - @Test - public void DuplicateUserPreserved() { - // check that duplicate users are preserved - DatawavePrincipal dp = new DatawavePrincipal(Lists.newArrayList(server1, server2, server1)); - Assert.assertEquals(3, dp.getProxiedUsers().size()); - Assert.assertEquals(server1, dp.getProxiedUsers().stream().findFirst().get()); - Assert.assertEquals(server2, dp.getProxiedUsers().stream().skip(1).findFirst().get()); - Assert.assertEquals(server1, dp.getProxiedUsers().stream().skip(2).findFirst().get()); - } -} diff --git a/web-services/common-util/src/test/java/datawave/security/util/DnUtilsTest.java b/web-services/common-util/src/test/java/datawave/security/util/DnUtilsTest.java deleted file mode 100644 index f413e84bc67..00000000000 --- a/web-services/common-util/src/test/java/datawave/security/util/DnUtilsTest.java +++ /dev/null @@ -1,115 +0,0 @@ -package datawave.security.util; - -import static org.junit.Assert.assertEquals; - -import java.util.Collection; - -import org.junit.Test; - -import com.google.common.collect.Lists; - -public class DnUtilsTest { - - @Test - public void testBuildNormalizedProxyDN() { - String expected = "sdn"; - String actual = DnUtils.buildNormalizedProxyDN("SDN", "IDN", null, null); - assertEquals(expected, actual); - - expected = "sdn2"; - actual = DnUtils.buildNormalizedProxyDN("SDN1", "IDN1", "SDN2", "IDN2"); - assertEquals(expected, actual); - - expected = "sdn2"; - actual = DnUtils.buildNormalizedProxyDN("SDN1", "IDN1", "SDN2", "IDN2"); - assertEquals(expected, actual); - - expected = "sdn2"; - actual = DnUtils.buildNormalizedProxyDN("SDN1", "IDN1", "", ""); - assertEquals(expected, actual); - } - - @Test - public void testBuildNormalizedDN() { - Collection expected = Lists.newArrayList("sdn", "idn"); - Collection actual = DnUtils.buildNormalizedDNList("SDN", "IDN", null, null); - assertEquals(expected, actual); - - expected = Lists.newArrayList("sdn2", "idn2", "sdn1", "idn1"); - actual = DnUtils.buildNormalizedDNList("SDN1", "IDN1", "SDN2", "IDN2"); - assertEquals(expected, actual); - - expected = Lists.newArrayList("sdn2", "idn2", "sdn3", "idn3", "sdn1", "idn1"); - actual = DnUtils.buildNormalizedDNList("SDN1", "IDN1", "SDN2", "IDN2"); - assertEquals(expected, actual); - - expected = Lists.newArrayList("sdn2", "idn2", "sdn3", "idn3", "sdn1", "idn1"); - actual = DnUtils.buildNormalizedDNList("SDN1", "IDN1", "", ""); - assertEquals(expected, actual); - } - - @Test - public void testGetUserDnFromArray() { - String userDnForTest = "snd1"; - String[] array = new String[] {userDnForTest, "idn"}; - String userDN = DnUtils.getUserDN(array); - assertEquals(userDnForTest, userDN); - } - - @Test(expected = IllegalArgumentException.class) - public void testTest() { - String[] dns = new String[] {"sdn"}; - DnUtils.getUserDN(dns, true); - } - - @Test(expected = IllegalArgumentException.class) - public void testBuildNormalizedProxyDNTooMissingIssuers() { - DnUtils.buildNormalizedProxyDN("SDN", "IDN", "SDN2", null); - } - - @Test(expected = IllegalArgumentException.class) - public void testBuildNormalizedProxyDNTooFewIssuers() { - DnUtils.buildNormalizedProxyDN("SDN", "IDN", "SDN2", "IDN2"); - } - - @Test(expected = IllegalArgumentException.class) - public void testBuildNormalizedProxyDNTooFewSubjects() { - DnUtils.buildNormalizedProxyDN("SDN", "IDN", "SDN2", "IDN2"); - } - - @Test(expected = IllegalArgumentException.class) - public void testBuildNormalizedProxyDNSubjectEqualsIssuer() { - DnUtils.buildNormalizedProxyDN("SDN", "IDN", "SDN2", "SDN2"); - } - - @Test(expected = IllegalArgumentException.class) - public void testBuildNormalizedProxyDNSubjectDNInIssuer() { - DnUtils.buildNormalizedProxyDN("SDN", "IDN", "SDN2", "CN=foo,OU=My Department"); - } - - @Test(expected = IllegalArgumentException.class) - public void testBuildNormalizedDNListTooMissingIssuers() { - DnUtils.buildNormalizedDNList("SDN", "IDN", "SDN2", null); - } - - @Test(expected = IllegalArgumentException.class) - public void testBuildNormalizedDNListTooFewIssuers() { - DnUtils.buildNormalizedDNList("SDN", "IDN", "SDN2", "IDN2"); - } - - @Test(expected = IllegalArgumentException.class) - public void testBuildNormalizedDNListTooFewSubjects() { - DnUtils.buildNormalizedDNList("SDN", "IDN", "SDN2", "IDN2"); - } - - @Test(expected = IllegalArgumentException.class) - public void testBuildNormalizedDNListSubjectEqualsIssuer() { - DnUtils.buildNormalizedDNList("SDN", "IDN", "SDN2", "SDN2"); - } - - @Test(expected = IllegalArgumentException.class) - public void testBuildNormalizedDNListSubjectDNInIssuer() { - DnUtils.buildNormalizedDNList("SDN", "IDN", "SDN2", "CN=foo,OU=My Department"); - } - -} diff --git a/web-services/common-util/src/test/java/datawave/security/util/WSAuthorizationsUtilTest.java b/web-services/common-util/src/test/java/datawave/security/util/WSAuthorizationsUtilTest.java index f3c789a350c..53f77b84cc4 100644 --- a/web-services/common-util/src/test/java/datawave/security/util/WSAuthorizationsUtilTest.java +++ b/web-services/common-util/src/test/java/datawave/security/util/WSAuthorizationsUtilTest.java @@ -43,7 +43,7 @@ public class WSAuthorizationsUtilTest { @Before public void initialize() { - System.setProperty(DnUtils.NPE_OU_PROPERTY, "iamnotaperson"); + System.setProperty(DnProperties.NPE_OU_PROPERTY, "iamnotaperson"); methodAuths = "A,C"; userAuths = new HashSet<>(); userAuths.add(Sets.newHashSet("A", "C", "D")); diff --git a/web-services/common/pom.xml b/web-services/common/pom.xml index 26efac19a8d..6787435589c 100644 --- a/web-services/common/pom.xml +++ b/web-services/common/pom.xml @@ -43,6 +43,10 @@ gov.nsa.datawave datawave-in-memory-accumulo + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.core datawave-core-annotation @@ -176,8 +180,8 @@ provided - javax.enterprise - cdi-api + jakarta.enterprise + jakarta.enterprise.cdi-api provided @@ -207,7 +211,12 @@ org.jboss.resteasy - resteasy-jaxrs + resteasy-core + provided + + + org.jboss.resteasy + resteasy-core-spi provided @@ -232,7 +241,7 @@ org.jboss.spec.javax.transaction - jboss-transaction-api_1.2_spec + jboss-transaction-api_1.3_spec provided @@ -240,11 +249,6 @@ weld-core-impl provided - - org.picketbox - picketbox - provided - org.apache.accumulo accumulo-tserver diff --git a/web-services/common/src/main/java/datawave/webservice/common/remote/RemoteHttpService.java b/web-services/common/src/main/java/datawave/webservice/common/remote/RemoteHttpService.java index e35d19ad2bc..a6d1ef3d241 100644 --- a/web-services/common/src/main/java/datawave/webservice/common/remote/RemoteHttpService.java +++ b/web-services/common/src/main/java/datawave/webservice/common/remote/RemoteHttpService.java @@ -54,7 +54,6 @@ import org.apache.http.message.BasicHeader; import org.apache.http.protocol.HttpContext; import org.apache.http.util.EntityUtils; -import org.jboss.security.JSSESecurityDomain; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xbill.DNS.DClass; @@ -75,6 +74,7 @@ import datawave.security.authorization.DatawavePrincipal; import datawave.security.authorization.JWTTokenHandler; import datawave.security.authorization.JWTTokenHandler.TtlMode; +import datawave.security.cert.SSLStores; import datawave.security.util.DnUtils; import datawave.webservice.common.exception.DatawaveWebApplicationException; import datawave.webservice.common.json.ObjectMapperDecorator; @@ -96,7 +96,7 @@ public abstract class RemoteHttpService { private AtomicInteger activeExecutions = new AtomicInteger(0); @Inject - private JSSESecurityDomain jsseSecurityDomain; + private SSLStores sslStores; @Resource private ManagedExecutorService executorService; @@ -110,8 +110,8 @@ public abstract class RemoteHttpService { private RemoteHttpServiceConfiguration config = new RemoteHttpServiceConfiguration(); - public void setJsseSecurityDomain(JSSESecurityDomain jsseSecurityDomain) { - this.jsseSecurityDomain = jsseSecurityDomain; + public void setSslStores(SSLStores sslStores) { + this.sslStores = sslStores; } public void setExecutorService(ManagedExecutorService executorService) { @@ -163,10 +163,10 @@ protected void init() { try { SSLContext ctx = SSLContext.getInstance("TLSv1.2"); - ctx.init(jsseSecurityDomain.getKeyManagers(), jsseSecurityDomain.getTrustManagers(), null); + ctx.init(sslStores.getKeyManagers(), sslStores.getTrustManagers(), null); - String alias = jsseSecurityDomain.getKeyStore().aliases().nextElement(); - X509KeyManager keyManager = (X509KeyManager) jsseSecurityDomain.getKeyManagers()[0]; + String alias = sslStores.getKeyStore().aliases().nextElement(); + X509KeyManager keyManager = (X509KeyManager) sslStores.getKeyManagers()[0]; X509Certificate[] certs = keyManager.getCertificateChain(alias); Key signingKey = keyManager.getPrivateKey(alias); diff --git a/web-services/common/src/test/java/datawave/webservice/common/remote/TestJSSESecurityDomain.java b/web-services/common/src/test/java/datawave/webservice/common/remote/TestSSLStores.java similarity index 65% rename from web-services/common/src/test/java/datawave/webservice/common/remote/TestJSSESecurityDomain.java rename to web-services/common/src/test/java/datawave/webservice/common/remote/TestSSLStores.java index be369fcb5e2..83a7982c429 100644 --- a/web-services/common/src/test/java/datawave/webservice/common/remote/TestJSSESecurityDomain.java +++ b/web-services/common/src/test/java/datawave/webservice/common/remote/TestSSLStores.java @@ -3,28 +3,26 @@ import java.io.File; import java.io.FileOutputStream; import java.net.Socket; -import java.security.Key; import java.security.KeyStore; import java.security.Principal; import java.security.PrivateKey; -import java.security.cert.Certificate; import java.security.cert.X509Certificate; -import java.util.Properties; import javax.net.ssl.KeyManager; import javax.net.ssl.TrustManager; import javax.net.ssl.X509KeyManager; -import org.jboss.security.JSSESecurityDomain; +import datawave.security.cert.SSLStores; -public class TestJSSESecurityDomain implements JSSESecurityDomain { - private final PrivateKey privKey; +public class TestSSLStores implements SSLStores { + + private final PrivateKey privateKey; private final X509Certificate[] chain; private final String alias; private final char[] keyPass; - public TestJSSESecurityDomain(String alias, PrivateKey privKey, char[] keyPass, X509Certificate[] chain) { - this.privKey = privKey; + public TestSSLStores(String alias, PrivateKey privateKey, char[] keyPass, X509Certificate[] chain) { + this.privateKey = privateKey; this.chain = chain; this.alias = alias; this.keyPass = keyPass; @@ -35,7 +33,7 @@ public KeyStore getKeyStore() throws SecurityException { try { KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(null, null); - keyStore.setKeyEntry(alias, privKey, keyPass, chain); + keyStore.setKeyEntry(alias, privateKey, keyPass, chain); File file = File.createTempFile("keystore", ".jks"); file.deleteOnExit(); keyStore.store(new FileOutputStream(file), keyPass); @@ -76,7 +74,7 @@ public X509Certificate[] getCertificateChain(String alias) { @Override public PrivateKey getPrivateKey(String alias) { - return privKey; + return privateKey; } }; return managers; @@ -87,7 +85,7 @@ public KeyStore getTrustStore() throws SecurityException { try { KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(null, null); - keyStore.setKeyEntry(alias, privKey, keyPass, chain); + keyStore.setKeyEntry(alias, privateKey, keyPass, chain); keyStore.store(new FileOutputStream(".keystore"), keyPass); return keyStore; } catch (Exception e) { @@ -99,54 +97,4 @@ public KeyStore getTrustStore() throws SecurityException { public TrustManager[] getTrustManagers() throws SecurityException { return new TrustManager[0]; } - - @Override - public void reloadKeyAndTrustStore() throws Exception { - - } - - @Override - public String getServerAlias() { - return null; - } - - @Override - public String getClientAlias() { - return null; - } - - @Override - public boolean isClientAuth() { - return false; - } - - @Override - public Key getKey(String s, String s1) throws Exception { - return null; - } - - @Override - public Certificate getCertificate(String s) throws Exception { - return null; - } - - @Override - public String[] getCipherSuites() { - return new String[0]; - } - - @Override - public String[] getProtocols() { - return new String[0]; - } - - @Override - public Properties getAdditionalProperties() { - return null; - } - - @Override - public String getSecurityDomain() { - return null; - } } diff --git a/web-services/deploy/application/README.md b/web-services/deploy/application/README.md new file mode 100644 index 00000000000..eb028485133 --- /dev/null +++ b/web-services/deploy/application/README.md @@ -0,0 +1,42 @@ +# Overview + +This project helps prepares and assembles the EAR for deployment to Wildfly. It also prepares artifacts that should be copied over onto the Wildfly distribution once unpacked. + +## JBOSS Modules + +Wildfly uses a modular class loading system through the use of JBOSS modules. Each module can define the libraries it provides, as well as dependencies on other modules in order to have the modules from that module added to its class path. In order to avoid classloader conflicts, it is essential to have commonly used libraries isolated in their own modules, and added as dependencies in other modules or deployments that require access to the classes. + +In addition to the Wildfly application modules, EAR deployments are treated as modules, and are defined by the following rules: + +1. The `lib/` directory of the EAR is a single module called the parent module. +2. Each WAR deployment within the EAR is a single module. +3. Each EJB JAR deployment within the EAR is a single module. + +Managing the module dependencies for the Datawave EAR deployment is done through the [jboss-deployment.xml](src/main/application/META-INF/jboss-deployment-structure.xml). **IMPORTANT**: any dependencies that are provided to the EAR deployment through a module should be configured with a `provided` scope in the `webservice-parent` [pom.xml](../../pom.xml) to prevent them from being included in the EAR's `/lib` folder. + +### Custom Modules + +We create several JBOSS modules that will be part of Wildfly upon startup. These modules can be found in the project [modules](src/main/wildfly/overlay/modules) directory. Most of these modules are self-explanatory and are, except for the module `datawave.webservices.datawave-security-elytron-module`, common dependencies between other modules and the Datawave deployment. + +* [datawave.webservice.datawave-security-elytron-module](src/main/wildfly/overlay/modules/datawave/webservice/datawave-security-elytron-module) + * This module contains the custom Wildfly security components that create the Datawave authentication and authorization workflow leveraging Elytron. These classes must be deployed as a separate JBOSS module. This module should not be imported as a dependency to the Datawave deployment. +* [datawave.webservice.datawave-security-elytron](src/main/wildfly/overlay/modules/datawave/webservice/datawave-security-elytron) + * Contains security-related artifacts that are commonly used between the Datawave deployment and the module `datawave.webservice.datawave-security-elytron-module`. +* [datawave.commons.datawave-commons-security](src/main/wildfly/overlay/modules/datawave/commons/datawave-commons-security) + * Contains security-related artifacts that are commonly used between the Datawave deployment, the module `datawave.webservice.datawave-security-elytron-module`, and the microservices. +* [com.fasterxml.jackson.datatype.jackson-datatype-guava](src/main/wildfly/overlay/modules/com/fasterxml/jackson/datatype/jackson-datatype-guava) +* [com.fasterxml.jackson.module.jackson-module-jaxb-annotations](src/main/wildfly/overlay/modules/com/fasterxml/jackson/module/jackson-module-jaxb-annotations) +* [com.github.ben-manes.caffeine](src/main/wildfly/overlay/modules/com/github/ben-manes/caffeine) +* [io.jsonwebtoken.jjwt-api](src/main/wildfly/overlay/modules/io/jsonwebtoken/jjwt-api) +* [io.jsonwebtoken.jjwt-impl](src/main/wildfly/overlay/modules/io/jsonwebtoken/jjwt-impl) +* [io.jsonwebtoken.jjwt-jackson](src/main/wildfly/overlay/modules/io/jsonwebtoken/jjwt-jackson) +* [org.apache.commons.commons-text](src/main/wildfly/overlay/modules/org/apache/commons/commons-text) +* [org.apache.hadoop.common](src/main/wildfly/overlay/modules/org/apache/hadoop/common) + +### Overridden Modules + +We also override the library versions of several modules that are part of the core Wildfly application modules in order to synchronize the versions used by Datawave and Wildfly. These modules may get automatically added to deployments by Wildfly and may be dependencies of other core modules. The versions are overridden by copying updated versions of the libraries and `module.xml` files to the [modules/system/layers/base](src/main/wildfly/overlay/modules/system/layers/base) directory. + +- [org.slf4j](src/main/wildfly/overlay/modules/system/layers/base/org/slf4j) +- [org.slf4j.impl](src/main/wildfly/overlay/modules/system/layers/base/org/slf4j/impl) +- [org.apache.commons.lang3](src/main/wildfly/overlay/modules/system/layers/base/org/apache/commons/lang3) diff --git a/web-services/deploy/application/pom.xml b/web-services/deploy/application/pom.xml index 1ca5fd78d00..e559e336265 100644 --- a/web-services/deploy/application/pom.xml +++ b/web-services/deploy/application/pom.xml @@ -10,6 +10,10 @@ pom ${project.artifactId} + + ${project.build.directory}/wildfly/overlay/modules + + ${base.wildfly.modules.dir}/system/layers/base ${project.build.finalName} 2.0.2.Final @@ -571,6 +575,178 @@ maven-dependency-plugin + + + + copy-org-apache-commons-lang3 + + copy + + process-resources + + ${base.wildfly.system.modules.dir}/org/apache/commons/lang3/main + + + org.apache.commons + commons-lang3 + ${version.commons-lang3} + + + + + + + + copy-org-apache-commons-text + + copy + + process-resources + + ${base.wildfly.modules.dir}/org/apache/commons/commons-text/main + + + org.apache.commons + commons-text + ${version.commons-text} + + + + + + + + copy-io-jsonwebtoken-jjwt-api + + copy + + process-resources + + ${base.wildfly.modules.dir}/io/jsonwebtoken/jjwt-api/main + + + io.jsonwebtoken + jjwt-api + ${version.jjwt} + + + + + + + + copy-io-jsonwebtoken-jjwt-impl + + copy + + process-resources + + ${base.wildfly.modules.dir}/io/jsonwebtoken/jjwt-impl/main + + + io.jsonwebtoken + jjwt-impl + ${version.jjwt} + + + + + + + + copy-com-fasterxml-jackson-datatype-jackson-datatype-guava + + copy + + process-resources + + ${base.wildfly.modules.dir}/com/fasterxml/jackson/datatype/jackson-datatype-guava/main + + + com.fasterxml.jackson.datatype + jackson-datatype-guava + ${version.jackson} + + + + + + + + copy-com-fasterxml-jackson-module-jackson-module-jaxb-annotations + + copy + + process-resources + + ${base.wildfly.modules.dir}/com/fasterxml/jackson/module/jackson-module-jaxb-annotations/main + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + ${version.jackson} + + + + + + + + copy-datawave-commons-security + + copy + + process-resources + + ${base.wildfly.modules.dir}/datawave/commons/datawave-commons-security/main + + + gov.nsa.datawave.commons + datawave-commons-security + ${project.version} + + + + + + + + copy-datawave-ws-security-elytron + + copy + + process-resources + + ${base.wildfly.modules.dir}/datawave/webservice/datawave-security-elytron/main + + + gov.nsa.datawave.webservices + datawave-ws-security-elytron + ${project.version} + + + + + + + + copy-datawave-ws-security-elytron-module + + copy + + process-resources + + ${base.wildfly.modules.dir}/datawave/webservice/datawave-security-elytron-module/main + + + gov.nsa.datawave.webservices + datawave-ws-security-elytron-module + ${project.version} + + + + + @@ -601,7 +777,7 @@ process-resources - ${project.build.directory}/wildfly/overlay/modules/org/apache/hadoop/common/main + ${base.wildfly.modules.dir}/org/apache/hadoop/common/main org.apache.hadoop @@ -649,10 +825,6 @@ org.apache.commons commons-configuration2 - - org.apache.commons - commons-text - javax.servlet servlet-api @@ -682,14 +854,18 @@ javax.inject 1 - - javax.enterprise - cdi-api - - + + jakarta.enterprise + jakarta.enterprise.cdi-api + + org.jboss.resteasy - resteasy-jaxrs - + resteasy-core + + + org.jboss.resteasy + resteasy-core-spi + org.jboss.spec.javax.annotation jboss-annotations-api_1.3_spec @@ -720,7 +896,7 @@ org.jboss.spec.javax.transaction - jboss-transaction-api_1.2_spec + jboss-transaction-api_1.3_spec xerces @@ -729,6 +905,7 @@ + copy-slf4j-api-dependencies @@ -736,7 +913,7 @@ process-resources - ${project.build.directory}/wildfly/overlay/modules/system/layers/base/org/slf4j/main + ${base.wildfly.system.modules.dir}/org/slf4j/main org.slf4j @@ -745,6 +922,7 @@ + copy-slf4j-impl-dependencies @@ -752,7 +930,7 @@ process-resources - ${project.build.directory}/wildfly/overlay/modules/system/layers/base/org/slf4j/impl/main + ${base.wildfly.system.modules.dir}/org/slf4j/impl/main org.jboss.slf4j diff --git a/web-services/deploy/application/src/main/application/META-INF/jboss-deployment-structure.xml b/web-services/deploy/application/src/main/application/META-INF/jboss-deployment-structure.xml index 0cf5fdbd952..325265e8984 100644 --- a/web-services/deploy/application/src/main/application/META-INF/jboss-deployment-structure.xml +++ b/web-services/deploy/application/src/main/application/META-INF/jboss-deployment-structure.xml @@ -1,14 +1,30 @@ - + false + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/web-services/deploy/application/src/main/wildfly/add-datawave-configuration.cli b/web-services/deploy/application/src/main/wildfly/add-datawave-configuration.cli index 21ef1387e7e..7fce0ca230e 100644 --- a/web-services/deploy/application/src/main/wildfly/add-datawave-configuration.cli +++ b/web-services/deploy/application/src/main/wildfly/add-datawave-configuration.cli @@ -9,16 +9,7 @@ batch embed-server --server-config=standalone-full.xml # Add the MySQL Connector as a module -module add --name=com.mysql.driver --dependencies=javax.api,javax.transaction.api --resources=mysql/mysql-connector-j-${version.mysql-connector}.jar - -# Remove the Elytron security subsystem entirely. Our JAAS security setup doesn't work -# with it due to some bugs (the RunAsLoginModule doesn't work -- WFLY-11892) so we'll -# just continue to use the deprecated system for now -/subsystem=elytron:remove - -# Remove the metrics subsystem since it is currently buggy (thread-safety issue) and can cause deployment failures -# See WFLY-11801 for details. -/subsystem=microprofile-metrics-smallrye:remove +module add --name=com.mysql.driver --dependencies=javax.api,javax.transaction.api,com.fasterxml.jackson.core.jackson-core --resources=mysql/mysql-connector-j-${version.mysql-connector}.jar # Change default ports to listen on all interfaces (can still override on command line) /interface=public/:write-attribute(name=inet-address,value="${jboss.bind.address:0.0.0.0}") @@ -94,7 +85,18 @@ module add --name=com.mysql.driver --dependencies=javax.api,javax.transaction.ap /system-property=dw.model.defaultTableName:add(value=${table.name.metadata}) /system-property=dw.basemaps:add(value="${basemaps}") -# proxied entities to be pruned from credentials +# Security SSL Properties +/system-property=dw.ssl.context.info.keyStoreURL:add(value=file://${KEYSTORE}) +/system-property=dw.ssl.context.info.keyStoreType:add(value="${KEYSTORE_TYPE}") +/system-property=dw.ssl.context.info.keyStorePassword:add(value="${KEYSTORE_PASSWORD}") +/system-property=dw.ssl.context.info.trustStoreURL:add(value=file://${TRUSTSTORE}) +/system-property=dw.ssl.context.info.trustStoreType:add(value="${TRUSTSTORE_TYPE}") +/system-property=dw.ssl.context.info.trustStorePassword:add(value="${TRUSTSTORE_PASSWORD}") + +# Required to allow the elytron module to lookup a SecurityEJBProvider instance from JNDI. +/system-property=dw.security.ejb.provider.jndi:add(value=java:global/datawave-ws-deploy-application-${project.version}-compose/gov.nsa.datawave.webservices-datawave-ws-security-${project.version}/SecurityEJBProviderImpl) + +# Proxied entities to be pruned from credentials. /system-property=dw.trusted.proxied.entities:add(value="${trusted.proxied.entities}") # Enable/disable the test authorization service (enable for development use only) @@ -147,41 +149,128 @@ module add --name=com.mysql.driver --dependencies=javax.api,javax.transaction.ap /system-property=jexl.function.namespace.registry:add(value="datawave.query.jexl.functions.JexlFunctionNamespaceRegistry.class") # -# Add cache container for the security realm +# Configure SSL. # -/subsystem=infinispan/cache-container=security:add -/subsystem=infinispan/cache-container=security/local-cache=security:add -/subsystem=infinispan/cache-container=security:write-attribute(name=default-cache,value=security) -/subsystem=infinispan/cache-container=security/local-cache=security/component=expiration:add(lifespan=300000) + +# Create the server key store. +/subsystem=elytron/key-store=serverKeyStore:add( \ + path=${KEYSTORE}, \ + credential-reference={clear-text="${KEYSTORE_PASSWORD}"}, \ + type=${KEYSTORE_TYPE}) + +# Create the server key manager. +/subsystem=elytron/key-manager=serverKeyManager:add( \ + key-store=serverKeyStore, \ + credential-reference={clear-text="${KEYSTORE_PASSWORD}"}) + +# Create the server trust store. +/subsystem=elytron/key-store=serverTrustStore:add( \ + path=${TRUSTSTORE}, \ + credential-reference={clear-text="${TRUSTSTORE_PASSWORD}"}, \ + type=${TRUSTSTORE_TYPE}) + +# Create the server trust manager. +/subsystem=elytron/trust-manager=serverTrustManager:add( \ + key-store=serverTrustStore) + +# Create the server SSL context. The protocols "TLSv1.1" and "TLSv1.2" will be supported, a client certificate will be required on an SSL handshake, and the +# security-domain 'datawaveElytron' will be used for authentication during the SSL session establishment. +/subsystem=elytron/server-ssl-context=serverSSLContext:add( \ + key-manager=serverKeyManager, \ + trust-manager=serverTrustManager, \ + protocols=["TLSv1.1","TLSv1.2"], \ + want-client-auth=true) # -# Add security realm and domain +# Configure the datawave security domain. # -/core-service=management/security-realm=SSLRealm/:add -/core-service=management/security-realm=SSLRealm/server-identity=ssl/:add(enabled-protocols=["TLSv1.1","TLSv1.2"],keystore-path=${KEYSTORE},keystore-provider=${KEYSTORE_TYPE},keystore-password="${KEYSTORE_PASSWORD}") -/core-service=management/security-realm=SSLRealm/authentication=truststore/:add(keystore-path=${TRUSTSTORE},keystore-provider=${TRUSTSTORE_TYPE},keystore-password="${TRUSTSTORE_PASSWORD}") -/core-service=management/security-realm=SSLRealm/authentication=jaas/:add(name=datawave) - -/subsystem=security/security-domain=datawave:add(cache-type=infinispan) -/subsystem=security/security-domain=datawave/authentication=classic:add -/subsystem=security/security-domain=datawave/authentication=classic/login-module=Remoting:add(code=Remoting,flag=optional,module-options={password-stacking=useFirstPass}) -/subsystem=security/security-domain=datawave/authentication=classic/login-module=RunAs:add(code=RunAs,flag=required,module-options={roleName=InternalUser}) -/subsystem=security/security-domain=datawave/authentication=classic/login-module=Datawave:add(code=datawave.security.login.DatawavePrincipalLoginModule,flag=required,module-options={principalClass=datawave.security.authorization.DatawavePrincipal,verifier=datawave.security.login.DatawaveCertVerifier,ocspLevel=off,allowUserProxying=false,trustedHeaderLogin="${dw.trusted.header.authentication:false}"}) -/subsystem=security/security-domain=datawave/jsse=classic:add(keystore={type="${KEYSTORE_TYPE}",password="${KEYSTORE_PASSWORD}",url="file://${KEYSTORE}"},truststore={type="${TRUSTSTORE_TYPE}",password="${TRUSTSTORE_PASSWORD}",url="file://${TRUSTSTORE}"}) -/subsystem=security/security-domain=JmsXARealm:add(cache-type=default) -/subsystem=security/security-domain=JmsXARealm/authentication=classic:add -/subsystem=security/security-domain=JmsXARealm/authentication=classic/login-module=ConfiguredIdentity:add(code=ConfiguredIdentity,flag=required,module-options={principal=${dw.hornetq.system.userName},userName=${dw.hornetq.system.userName},password=${dw.hornetq.system.password},managedConnectionFactoryName="jboss.jca:service=TxCM,name=JmsXA"}) - -/subsystem=security/security-domain=datawave-client:add(cache-type=infinispan) -/subsystem=security/security-domain=datawave-client/authentication=classic:add -/subsystem=security/security-domain=datawave-client/authentication=classic/login-module=ClientCert:add(code=datawave.security.login.ClientCertLoginModule,flag=required,module-options={password-stacking=useFirstPass}) -/subsystem=security/security-domain=datawave-client/authentication=classic/login-module=Client:add(code=Client,flag=required,module-options={password-stacking=useFirstPass,restore-login-identity=true}) +# Create a custom role decoder that will decode the roles a user has from a DatawavePrincipal. The decoder can be +# configured via the configuration attribute. See the class datawave.security.realm.DatawaveRoleDecoder for supported +# configuration options. +/subsystem=elytron/custom-role-decoder=datawaveRoleDecoder:add( \ + class-name=datawave.security.realm.DatawaveRoleDecoder, \ + module="datawave.webservice.datawave-security-elytron-module", \ + configuration={ \ + }) + +# Create a custom evidence decoder that will decode a DatawavePrincipal from evidence. The evidence decoder can be configured via the configuration attribute. +# See the class datawave.security.realm.DatawaveEvidenceDecoder for supported configuration options. +/subsystem=elytron/custom-evidence-decoder=datawaveEvidenceDecoder:add( \ + class-name=datawave.security.realm.DatawaveEvidenceDecoder, \ + module="datawave.webservice.datawave-security-elytron-module", \ + configuration={ \ + jwtEnabled="${dw.jwt.header.authentication:false}", \ + trustedHeadersEnabled="${dw.trusted.header.authentication:false}", \ + maxCacheEntries="-1", \ + maxCacheAge="-1" \ + }) + +# Create a custom realm that handles preparing the DatawavePrincipal for authentication. The realm can be configured via +# the configuration attribute. See the class datawave.security.realm.DatawaveSecurityRealm for supported configuration +# options. +/subsystem=elytron/custom-realm=datawaveRealm:add( \ + class-name=datawave.security.realm.DatawaveSecurityRealm, \ + module="datawave.webservice.datawave-security-elytron-module", \ + configuration={ \ + certVerifier="datawave.security.cert.DatawaveCertVerifier", \ + oscpLevel="off" \ + }) + +# Create and configure the security domain 'datawaveElytron' that will handle the authentication and authorization. +/subsystem=elytron/security-domain=datawaveElytron:add( \ + default-realm=datawaveRealm, \ + realms=[{realm=datawaveRealm}], \ + role-decoder=datawaveRoleDecoder, \ + evidence-decoder=datawaveEvidenceDecoder, \ + permission-mapper=default-permission-mapper) + +# # Configure the HTTP/HTTPS listener for undertow +# +# Switch https-listener from the legacy security-realm to the Elytron server ssl context. +/subsystem=undertow/server=default-server/https-listener=https/:undefine-attribute(name=security-realm) +/subsystem=undertow/server=default-server/https-listener=https/:write-attribute(name=ssl-context,value=serverSSLContext) + +# Ensure that we log the start time. /subsystem=undertow/server=default-server/http-listener=default/:write-attribute(name=record-request-start-time,value=true) /subsystem=undertow/server=default-server/https-listener=https/:write-attribute(name=record-request-start-time,value=true) -/subsystem=undertow/server=default-server/https-listener=https/:write-attribute(name=security-realm,value=SSLRealm) -/subsystem=undertow/server=default-server/https-listener=https/:write-attribute(name=verify-client,value=REQUESTED) + +# +# Configure the authentication mechanism for Elytron. +# +/subsystem=elytron/service-loader-http-server-mechanism-factory=datawaveMechanismFactory:add( \ + module=datawave.webservice.datawave-security-elytron-module) +# Wrap the mechanism factory in a configurable factory so that we can pass in custom configuration options in the +# properties attribute. See the class datawave.security.auth.DatawaveHttpAuthenticationMechanism for supported +# configuration options. +/subsystem=elytron/configurable-http-server-mechanism-factory=configurableDatawaveMechanismFactory:add( \ + http-server-mechanism-factory=datawaveMechanismFactory, \ + properties={ \ + }) + +# Create an authentication factory that references the configurable mechanism factory. +/subsystem=elytron/http-authentication-factory=datawaveAuthenticationFactory:add( \ + http-server-mechanism-factory=configurableDatawaveMechanismFactory, \ + security-domain=datawaveElytron, \ + mechanism-configurations=[{ \ + mechanism-name=DATAWAVE-AUTH, \ + mechanism-realm-configurations=[{realm-name=datawave}] \ + }]) + +# +# Ensure undertow is configured to use the custom authentication mechanism. +# + +# Configure a security domain for the application that uses the datawave mechanism factory. Override the deployment +# config in order to support the use of DATAWAVE-AUTH instead of BASIC only by default. +/subsystem=undertow/application-security-domain=datawave:add( \ + http-authentication-factory=datawaveAuthenticationFactory, \ + override-deployment-config=true) + +# Ensure the default application security domain is 'datawave'. +/subsystem=undertow:write-attribute( \ + name=default-security-domain, value="datawave") # # Set up ActiveMQ, JMS Topics/Queues/DLQs @@ -201,6 +290,8 @@ jms-queue add --queue-address=AccumuloTableCacheDLQ --entries=queue/AccumuloTabl # # EJB subsystem configuration # +# Map the 'datawaveElytron' security domain to the name 'datawave' and make it discoverable by EJBs. +/subsystem=ejb3/application-security-domain=datawave:add(security-domain=datawaveElytron) /subsystem=ejb3/:write-attribute(name=in-vm-remote-interface-invocation-pass-by-value,value=false) /subsystem=ejb3/thread-pool=default/:write-attribute(name=max-threads,value=${jboss.ejb3.async.threads}) /subsystem=ejb3/strict-max-bean-instance-pool=mdb-strict-max-pool/:undefine-attribute(name=derive-size) @@ -216,11 +307,32 @@ jms-queue add --queue-address=AccumuloTableCacheDLQ --entries=queue/AccumuloTabl /subsystem=ee/managed-executor-service=default:write-attribute(name=max-threads,value=${jboss.managed.executor.service.default.max.threads}) # Configure the JDBC DataSource used by MySQL -/subsystem=datasources/jdbc-driver=mysql:add(driver-name=mysql,driver-module-name=com.mysql.driver) -/subsystem=datasources/data-source=CachedResultsDS:add(jndi-name=java:jboss/datasources/CachedResultsDS,connection-url="jdbc:mysql://${mysql.host}:3306/${mysql.dbname}?zeroDateTimeBehavior=convertToNull",min-pool-size=${mysql.pool.min.size},max-pool-size=${mysql.pool.max.size},blocking-timeout-wait-millis=5000,idle-timeout-minutes=15,exception-sorter-class-name=org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLExceptionSorter,valid-connection-checker-class-name=org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLValidConnectionChecker,user-name=${mysql.user.name},password=${mysql.user.password},driver-name=mysql) +# Add a driver for MySQL. +/subsystem=datasources/jdbc-driver=mysql:add( \ + driver-name=mysql, \ + driver-module-name=com.mysql.driver) + +# Add a MySQL datasource for caching results. +/subsystem=datasources/data-source=CachedResultsDS:add( \ + jndi-name=java:jboss/datasources/CachedResultsDS, \ + connection-url="jdbc:mysql://${mysql.host}:3306/${mysql.dbname}?zeroDateTimeBehavior=convertToNull", \ + min-pool-size=${mysql.pool.min.size}, \ + max-pool-size=${mysql.pool.max.size}, \ + blocking-timeout-wait-millis=5000, \ + idle-timeout-minutes=15, \ + exception-sorter-class-name=org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLExceptionSorter, \ + valid-connection-checker-class-name=org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLValidConnectionChecker, \ + user-name=${mysql.user.name}, \ + password=${mysql.user.password}, \ + driver-name=mysql) # Configure the H2 DataSource used by the DatabaseUserService -/subsystem=datasources/data-source=DatabaseUserServiceDS:add(jndi-name=java:jboss/datasources/DatabaseUserServiceDS,connection-url="jdbc:h2:${jboss.server.config.dir}/h2/databaseDatawaveUsers",user-name=sa,password=sa,driver-name=h2) +/subsystem=datasources/data-source=DatabaseUserServiceDS:add( \ + jndi-name=java:jboss/datasources/DatabaseUserServiceDS, \ + connection-url="jdbc:h2:${jboss.server.config.dir}/h2/databaseDatawaveUsers", \ + user-name=sa, \ + password=sa, \ + driver-name=h2) # # Configure custom loggers and categories @@ -235,6 +347,7 @@ jms-queue add --queue-address=AccumuloTableCacheDLQ --entries=queue/AccumuloTabl /subsystem=logging/periodic-rotating-file-handler=MODIFICATION_LOG/:add(level=TRACE,suffix=".yyyy-MM-dd",append=true,autoflush=true,named-formatter=PATTERN,file={relative-to=>"jboss.server.log.dir", path=>"ModificationService.log"}) /subsystem=logging/periodic-rotating-file-handler=DISTRIBUTED_QUERY_LOG/:add(level=TRACE,suffix=".yyyy-MM-dd",append=true,autoflush=true,named-formatter=PATTERN,file={relative-to=>"jboss.server.log.dir", path=>"DistributedQueryService.log"}) /subsystem=logging/periodic-rotating-file-handler=RESTEASY_LOG/:add(level=TRACE,suffix=".yyyy-MM-dd",append=true,autoflush=true,named-formatter=PATTERN,file={relative-to=>"jboss.server.log.dir", path=>"RestEasy.log"}) +/subsystem=logging/periodic-rotating-file-handler=JBOSS_LOG/:add(level=TRACE,suffix=".yyyy-MM-dd",append=true,autoflush=true,named-formatter=PATTERN,file={relative-to=>"jboss.server.log.dir", path=>"JBoss.log"}) /subsystem=logging/logger=datawave.query:add(use-parent-handlers=false,handlers=["QUERY_LOG"],level=DEBUG) /subsystem=logging/logger=datawave.webservice.common.cache.SharedCacheCoordinator:add(use-parent-handlers=false,handlers=["QUERY_LOG"],level=INFO) @@ -257,6 +370,10 @@ jms-queue add --queue-address=AccumuloTableCacheDLQ --entries=queue/AccumuloTabl /subsystem=logging/logger=datawave.security:add(use-parent-handlers=false,handlers=["SECURITY_LOG"]) /subsystem=logging/logger=org.jboss.security.auth:add(use-parent-handlers=false,handlers=["SECURITY_LOG"]) /subsystem=logging/logger=org.jboss.security.authorization.modules.ejb.EJBPolicyModuleDelegate:add(use-parent-handlers=false,handlers=["SECURITY_LOG"]) +# Uncomment the following three lines to also capture logging from the Wildfly Elytron framework if desired. +#/subsystem=logging/logger=org.wildfly.security:add(use-parent-handlers=false,handlers=["SECURITY_LOG"]) +#/subsystem=logging/logger=org.wildfly.security.tls:add(use-parent-handlers=false,handlers=["SECURITY_LOG"]) +#/subsystem=logging/logger=org.wildfly.extension.elytron:add(use-parent-handlers=false,handlers=["SECURITY_LOG"]) /subsystem=logging/logger=datawave.webservice.modification:add(use-parent-handlers=false,handlers=["MODIFICATION_LOG"]) /subsystem=logging/logger=datawave.webservice.query.distributed:add(use-parent-handlers=false,handlers=["DISTRIBUTED_QUERY_LOG"]) /subsystem=logging/logger=org.jboss.resteasy.core:add(use-parent-handlers=false,handlers=["RESTEASY_LOG"]) @@ -265,6 +382,10 @@ jms-queue add --queue-address=AccumuloTableCacheDLQ --entries=queue/AccumuloTabl /subsystem=logging/logger=org.jboss.resteasy.specimpl:add(use-parent-handlers=false,handlers=["RESTEASY_LOG"]) /subsystem=logging/logger=datawave.resteasy.interceptor:add(use-parent-handlers=false,handlers=["RESTEASY_LOG"]) +# Add trace logging for JBoss module class loading. The JBoss.log file will get big fast if activated, so uncomment this +# only when you need to see exactly which libraries/modules Wildfly is loading classes from in the classloaders. +# /subsystem=logging/logger=org.jboss.modules:add(use-parent-handlers=false, handlers=["JBOSS_LOG"]) + # Custom log levels /subsystem=logging/logger=httpclient.wire:add(level=TRACE) /subsystem=logging/logger=datawave.security.util.DnUtils:add(level=DEBUG) diff --git a/web-services/deploy/application/src/main/wildfly/overlay/modules/com/fasterxml/jackson/datatype/jackson-datatype-guava/main/module.xml b/web-services/deploy/application/src/main/wildfly/overlay/modules/com/fasterxml/jackson/datatype/jackson-datatype-guava/main/module.xml new file mode 100644 index 00000000000..1053818b02d --- /dev/null +++ b/web-services/deploy/application/src/main/wildfly/overlay/modules/com/fasterxml/jackson/datatype/jackson-datatype-guava/main/module.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/web-services/deploy/application/src/main/wildfly/overlay/modules/com/fasterxml/jackson/module/jackson-module-jaxb-annotations/main/module.xml b/web-services/deploy/application/src/main/wildfly/overlay/modules/com/fasterxml/jackson/module/jackson-module-jaxb-annotations/main/module.xml new file mode 100644 index 00000000000..f3f6078bf64 --- /dev/null +++ b/web-services/deploy/application/src/main/wildfly/overlay/modules/com/fasterxml/jackson/module/jackson-module-jaxb-annotations/main/module.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/web-services/deploy/application/src/main/wildfly/overlay/modules/datawave/commons/datawave-commons-security/main/module.xml b/web-services/deploy/application/src/main/wildfly/overlay/modules/datawave/commons/datawave-commons-security/main/module.xml new file mode 100644 index 00000000000..9d5489d336f --- /dev/null +++ b/web-services/deploy/application/src/main/wildfly/overlay/modules/datawave/commons/datawave-commons-security/main/module.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/web-services/deploy/application/src/main/wildfly/overlay/modules/datawave/webservice/datawave-security-elytron-module/main/module.xml b/web-services/deploy/application/src/main/wildfly/overlay/modules/datawave/webservice/datawave-security-elytron-module/main/module.xml new file mode 100644 index 00000000000..b5102e9f683 --- /dev/null +++ b/web-services/deploy/application/src/main/wildfly/overlay/modules/datawave/webservice/datawave-security-elytron-module/main/module.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web-services/deploy/application/src/main/wildfly/overlay/modules/datawave/webservice/datawave-security-elytron/main/module.xml b/web-services/deploy/application/src/main/wildfly/overlay/modules/datawave/webservice/datawave-security-elytron/main/module.xml new file mode 100644 index 00000000000..fad0935a1bf --- /dev/null +++ b/web-services/deploy/application/src/main/wildfly/overlay/modules/datawave/webservice/datawave-security-elytron/main/module.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web-services/deploy/application/src/main/wildfly/overlay/modules/io/jsonwebtoken/jjwt-api/main/module.xml b/web-services/deploy/application/src/main/wildfly/overlay/modules/io/jsonwebtoken/jjwt-api/main/module.xml new file mode 100644 index 00000000000..a70a6144979 --- /dev/null +++ b/web-services/deploy/application/src/main/wildfly/overlay/modules/io/jsonwebtoken/jjwt-api/main/module.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/web-services/deploy/application/src/main/wildfly/overlay/modules/io/jsonwebtoken/jjwt-impl/main/module.xml b/web-services/deploy/application/src/main/wildfly/overlay/modules/io/jsonwebtoken/jjwt-impl/main/module.xml new file mode 100644 index 00000000000..66d159afa85 --- /dev/null +++ b/web-services/deploy/application/src/main/wildfly/overlay/modules/io/jsonwebtoken/jjwt-impl/main/module.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/web-services/deploy/application/src/main/wildfly/overlay/modules/org/apache/commons/commons-text/main/module.xml b/web-services/deploy/application/src/main/wildfly/overlay/modules/org/apache/commons/commons-text/main/module.xml new file mode 100644 index 00000000000..7f9edf84ceb --- /dev/null +++ b/web-services/deploy/application/src/main/wildfly/overlay/modules/org/apache/commons/commons-text/main/module.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web-services/deploy/application/src/main/wildfly/overlay/modules/org/apache/hadoop/common/main/module.xml b/web-services/deploy/application/src/main/wildfly/overlay/modules/org/apache/hadoop/common/main/module.xml index 3a25d1c41dd..83a7a5b4e8f 100644 --- a/web-services/deploy/application/src/main/wildfly/overlay/modules/org/apache/hadoop/common/main/module.xml +++ b/web-services/deploy/application/src/main/wildfly/overlay/modules/org/apache/hadoop/common/main/module.xml @@ -9,17 +9,16 @@ - + + - - diff --git a/web-services/deploy/application/src/main/wildfly/overlay/modules/system/layers/base/org/apache/commons/lang3/main/module.xml b/web-services/deploy/application/src/main/wildfly/overlay/modules/system/layers/base/org/apache/commons/lang3/main/module.xml new file mode 100644 index 00000000000..7245ce1b8bb --- /dev/null +++ b/web-services/deploy/application/src/main/wildfly/overlay/modules/system/layers/base/org/apache/commons/lang3/main/module.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/web-services/deploy/application/src/main/wildfly/overlay/modules/system/layers/base/org/slf4j/impl/main/module.xml b/web-services/deploy/application/src/main/wildfly/overlay/modules/system/layers/base/org/slf4j/impl/main/module.xml index c4340eedb35..17aabaecfe4 100644 --- a/web-services/deploy/application/src/main/wildfly/overlay/modules/system/layers/base/org/slf4j/impl/main/module.xml +++ b/web-services/deploy/application/src/main/wildfly/overlay/modules/system/layers/base/org/slf4j/impl/main/module.xml @@ -1,4 +1,4 @@ - + @@ -9,6 +9,7 @@ + diff --git a/web-services/deploy/application/src/main/wildfly/overlay/modules/system/layers/base/org/slf4j/main/module.xml b/web-services/deploy/application/src/main/wildfly/overlay/modules/system/layers/base/org/slf4j/main/module.xml index 20fe7485434..3092e07fe1a 100644 --- a/web-services/deploy/application/src/main/wildfly/overlay/modules/system/layers/base/org/slf4j/main/module.xml +++ b/web-services/deploy/application/src/main/wildfly/overlay/modules/system/layers/base/org/slf4j/main/module.xml @@ -1,4 +1,4 @@ - + diff --git a/web-services/deploy/application/src/main/wildfly/runtime-config.cli b/web-services/deploy/application/src/main/wildfly/runtime-config.cli index f338b068e5e..d2c47ad35d6 100644 --- a/web-services/deploy/application/src/main/wildfly/runtime-config.cli +++ b/web-services/deploy/application/src/main/wildfly/runtime-config.cli @@ -8,9 +8,30 @@ embed-server --server-config=standalone-full.xml /system-property=dw.warehouse.accumulo.password:add(value=${env.ACCUMULO_PASSWORD}) /system-property=dw.metrics.accumulo.password:add(value=${env.ACCUMULO_PASSWORD}) /system-property=dw.uuid.accumulo.password:add(value=${env.ACCUMULO_PASSWORD}) -/core-service=management/security-realm=SSLRealm/server-identity=ssl/:add(enabled-protocols=["TLSv1.1","TLSv1.2"],keystore-path=${env.KEYSTORE},keystore-provider=${env.KEYSTORE_TYPE},keystore-password="${env.KEYSTORE_PASSWORD}") -/core-service=management/security-realm=SSLRealm/authentication=truststore/:add(keystore-path=${env.TRUSTSTORE},keystore-provider=${env.TRUSTSTORE_TYPE},keystore-password="${env.TRUSTSTORE_PASSWORD}") -/subsystem=security/security-domain=datawave/jsse=classic:add(keystore={type="${env.KEYSTORE_TYPE}",password="${env.KEYSTORE_PASSWORD}",url="file://${env.KEYSTORE}"},truststore={type="${env.TRUSTSTORE_TYPE}",password="${env.TRUSTSTORE_PASSWORD}",url="file://${env.TRUSTSTORE}"}) + +/system-property=dw.ssl.context.info.keyStoreURL:add(value=file://${env.KEYSTORE}) +/system-property=dw.ssl.context.info.keyStoreType:add(value="${env.KEYSTORE_TYPE}") +/system-property=dw.ssl.context.info.keyStorePassword:add(value="${env.KEYSTORE_PASSWORD}") +/system-property=dw.ssl.context.info.trustStoreURL:add(value=file://${env.TRUSTSTORE}) +/system-property=dw.ssl.context.info.trustStoreType:add(value="${env.TRUSTSTORE_TYPE}") +/system-property=dw.ssl.context.info.trustStorePassword:add(value="${env.TRUSTSTORE_PASSWORD}") + +# Security configuration via the legacy security framework +#/core-service=management/security-realm=SSLRealm/server-identity=ssl/:add(enabled-protocols=["TLSv1.1","TLSv1.2"],keystore-path=${env.KEYSTORE},keystore-provider=${env.KEYSTORE_TYPE},keystore-password="${env.KEYSTORE_PASSWORD}") +#/core-service=management/security-realm=SSLRealm/authentication=truststore/:add(keystore-path=${env.TRUSTSTORE},keystore-provider=${env.TRUSTSTORE_TYPE},keystore-password="${env.TRUSTSTORE_PASSWORD}") +#/subsystem=security/security-domain=datawave/jsse=classic:add(keystore={type="${env.KEYSTORE_TYPE}",password="${env.KEYSTORE_PASSWORD}",url="file://${env.KEYSTORE}"},truststore={type="${env.TRUSTSTORE_TYPE}",password="${env.TRUSTSTORE_PASSWORD}",url="file://${env.TRUSTSTORE}"}) + + +# +# Security configuration via the Elytron framework +# +/subsystem=elytron/key-store=httpsKeyStore:add(path=${KEYSTORE},credential-reference={clear-text="${env.KEYSTORE_PASSWORD}"},type=${env.KEYSTORE_TYPE}) +/subsystem=elytron/key-manager=httpsKeyManager:add(key-store=httpsKeyStore,credential-reference={clear-text=${env.KEYSTORE_PASSWORD}) +/subsystem=elytron/key-store=httpsTrustStore:add(path=${TRUSTSTORE},credential-reference={clear-text="${env.TRUSTSTORE_PASSWORD}"},type=${env.TRUSTSTORE_TYPE}) +/subsystem=elytron/trust-manager=httpsTrustManager:add(key-store=httpsTrustStore) +/subsystem=elytron/server-ssl-context=httpsSSLContext:add(key-manager=httpsKeyManager,trust-manager=httpsTrustManager,protocols=["TLSv1.1","TLSv1.2"]) +/subsystem=elytron/key-store-realm=ksRealm:add(key-store=httpsKeyStore) +/subsystem=elytron/security-domain=datawave:add(default-realm=ksRealm,realms=[ksRealm]) # Run the batch commands (MUST BE LAST) -run-batch \ No newline at end of file +run-batch diff --git a/web-services/deploy/spring-framework-integration/pom.xml b/web-services/deploy/spring-framework-integration/pom.xml index f8fe7ef61ce..dbe11c53ab4 100644 --- a/web-services/deploy/spring-framework-integration/pom.xml +++ b/web-services/deploy/spring-framework-integration/pom.xml @@ -9,7 +9,12 @@ spring-framework-integration pom ${project.artifactId} + + + gov.nsa.datawave.commons + datawave-commons-security + org.easymock easymock @@ -92,8 +97,8 @@ test - javax.enterprise - cdi-api + jakarta.enterprise + jakarta.enterprise.cdi-api test @@ -103,7 +108,7 @@ org.jboss.arquillian.container - arquillian-weld-ee-embedded-1.1 + arquillian-weld-embedded test @@ -113,7 +118,18 @@ org.jboss.resteasy - resteasy-jaxrs + resteasy-core + test + + + javax.validation + validation-api + + + + + org.jboss.resteasy + resteasy-core-spi test @@ -144,7 +160,7 @@ org.jboss.spec.javax.transaction - jboss-transaction-api_1.2_spec + jboss-transaction-api_1.3_spec test @@ -153,8 +169,8 @@ test - org.picketbox - picketbox + org.jboss.weld.se + weld-se-core test diff --git a/web-services/deploy/spring-framework-integration/src/test/java/datawave/springframework/integration/WiredQueryExecutorBeanTest.java b/web-services/deploy/spring-framework-integration/src/test/java/datawave/springframework/integration/WiredQueryExecutorBeanTest.java index 682c2069ab4..99a54e34db3 100644 --- a/web-services/deploy/spring-framework-integration/src/test/java/datawave/springframework/integration/WiredQueryExecutorBeanTest.java +++ b/web-services/deploy/spring-framework-integration/src/test/java/datawave/springframework/integration/WiredQueryExecutorBeanTest.java @@ -10,7 +10,6 @@ import org.easymock.EasyMock; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.junit.Arquillian; -import org.jboss.security.JSSESecurityDomain; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.EmptyAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; @@ -38,6 +37,7 @@ import datawave.query.transformer.EventQueryDataDecoratorTransformer; import datawave.query.util.DateIndexHelperFactory; import datawave.security.authorization.DatawavePrincipal; +import datawave.security.cert.SSLStores; import datawave.security.system.CallerPrincipal; import datawave.security.system.ServerPrincipal; import datawave.webservice.common.json.DefaultMapperDecorator; @@ -106,15 +106,15 @@ public void testCreatingPrototypeBeans() { } } - private static JSSESecurityDomain mockJsseSecurityDomain = EasyMock.createMock(JSSESecurityDomain.class); + private static SSLStores mockSSLStores = EasyMock.createMock(SSLStores.class); private static DatawavePrincipal mockDatawavePrincipal = EasyMock.createMock(DatawavePrincipal.class); private static RemoteEdgeDictionary mockRemoteEdgeDictionary = EasyMock.createMock(RemoteEdgeDictionary.class); public static class Producer { @Produces - public static JSSESecurityDomain produceSecurityDomain() { - return mockJsseSecurityDomain; + public static SSLStores produceSSLStores() { + return mockSSLStores; } @Produces diff --git a/web-services/dictionary/pom.xml b/web-services/dictionary/pom.xml index 3e79a26ffb5..b208fe60c5e 100644 --- a/web-services/dictionary/pom.xml +++ b/web-services/dictionary/pom.xml @@ -22,7 +22,11 @@ org.jboss.resteasy - resteasy-jaxrs + resteasy-core + + + org.jboss.resteasy + resteasy-core-spi diff --git a/web-services/examples/client-login/pom.xml b/web-services/examples/client-login/pom.xml index 70878e5f321..a1e62a4fa09 100644 --- a/web-services/examples/client-login/pom.xml +++ b/web-services/examples/client-login/pom.xml @@ -25,18 +25,27 @@ datawave-ws-security ${project.version} + + gov.nsa.datawave.webservices + datawave-ws-security-elytron + io.protostuff protostuff-api compile - javax.enterprise - cdi-api + jakarta.enterprise + jakarta.enterprise.cdi-api - org.picketbox - picketbox + org.wildfly + wildfly-undertow + + + gov.nsa.datawave.commons + datawave-commons-security + provided org.jboss.spec.javax.ejb diff --git a/web-services/examples/client-login/src/main/java/datawave/webservice/examples/ClientLoginExampleBean.java b/web-services/examples/client-login/src/main/java/datawave/webservice/examples/ClientLoginExampleBean.java index 01a663be85d..327dac272b1 100644 --- a/web-services/examples/client-login/src/main/java/datawave/webservice/examples/ClientLoginExampleBean.java +++ b/web-services/examples/client-login/src/main/java/datawave/webservice/examples/ClientLoginExampleBean.java @@ -2,6 +2,7 @@ import java.security.KeyStore; import java.security.cert.X509Certificate; +import java.util.concurrent.Callable; import javax.annotation.security.RunAs; import javax.ejb.LocalBean; @@ -9,20 +10,21 @@ import javax.ejb.Singleton; import javax.ejb.Startup; import javax.inject.Inject; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.login.LoginContext; -import org.jboss.security.JSSESecurityDomain; -import org.jboss.security.auth.callback.ObjectCallback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.wildfly.security.auth.server.SecurityDomain; +import org.wildfly.security.auth.server.SecurityIdentity; +import datawave.security.cert.SSLStores; +import datawave.security.evidence.EvidenceFactory; +import datawave.security.evidence.X509CertificateEvidence; import datawave.security.user.UserOperationsBean; -import datawave.security.util.DnUtils; import datawave.user.AuthorizationsListBase; /** - * An example timer bean that shows how one would call a secured EJB from an unsecured context, such as a message-driven bean callback. + * This class demonstrates an example of how to programmatically obtain a {@link SecurityIdentity} from the current {@link SecurityDomain} in order to execute + * operations on secured EJBs with a target user's permissions. */ @Startup @Singleton @@ -30,61 +32,52 @@ @RunAs("InternalUser") public class ClientLoginExampleBean { - // Inject a secured EJB that we need to call. + private static final Logger log = LoggerFactory.getLogger(ClientLoginExampleBean.class); + + // Inject a secured EJB we need to call. @Inject - private UserOperationsBean userOps; + private UserOperationsBean userOperationsBean; - // This only works if our bean is inside the EJB container. - // That is, you can't do it from an arbitrary client, which would instead need - // to get its certificate some other way. + // This only works if our bean is inside the EJB container. That is, you can't do it from an arbitrary client, which would instead need to get its + // certificate some other way. @Inject - private JSSESecurityDomain domain; + private SSLStores sslStores; - @Schedule(hour = "*", minute = "*", second = "0", persistent = false) - public void doScheduledEvent() { + // Execute this call every minute. + @Schedule(hour = "*", minute = "*", persistent = false) + public void executeCall() { + log.info("Executing scheduled call"); try { - // Grab the server certificate from the keystore (we are assuming it is the first one). - // This is the credential we'll set on the object callback. - KeyStore keystore = domain.getKeyStore(); - final X509Certificate cert = (X509Certificate) keystore.getCertificate(keystore.aliases().nextElement()); + // Show that the security identity before authentication is anonymous. + SecurityDomain securityDomain = SecurityDomain.getCurrent(); + SecurityIdentity unauthenticatedIdentity = securityDomain.getCurrentSecurityIdentity(); + log.info("Current security identity within unsecured context (should be anonymous): {}", unauthenticatedIdentity.getPrincipal()); - // Compute the username. This would either be just a user DN if you are using a user's client - // certificate, or a server DN combined with a proxied user DN as we demonstrate here. - String userDN = System.getenv("USER_DN"); // Normally a username would go here. Hack for local testing--query the sid running jboss. - String userIssuerDN = System.getenv("ISSUER_DN"); // We need the issuer of the user's cert. This needs to be set in the environment for this test. - String serverDN = cert.getSubjectX500Principal().getName(); - String serverIssuerDN = cert.getIssuerX500Principal().getName(); - final String dn = DnUtils.buildNormalizedProxyDN(serverDN, serverIssuerDN, userDN, userIssuerDN); + // Grab the server certificate from the keystore (assuming it's the first one). + KeyStore keyStore = sslStores.getKeyStore(); + final X509Certificate certificate = (X509Certificate) keyStore.getCertificate(keyStore.aliases().nextElement()); - // Handle the callback for authentication. We expect two callbacks, a NameCallback and an ObjectCallback. - CallbackHandler cbh = new CallbackHandler() { - @Override - public void handle(Callback[] callbacks) { - NameCallback nc = (NameCallback) callbacks[0]; - ObjectCallback oc = (ObjectCallback) callbacks[1]; - nc.setName(dn); - oc.setCredential(cert); - } - }; + // Create a piece of evidence that will identify the server we want to authenticate as. + X509CertificateEvidence evidence = EvidenceFactory.getDefault().createX509CertificateEvidence(certificate, null, null); + log.info("Authenticating with evidence: {}", evidence); - // Authenticate to the DATAWAVE client domain. This saves the credentials - // we passed in the callback handler above, and passes them along to the server - // when we attempt any calls that require a login on the server. - LoginContext lc = new LoginContext("datawave-client", cbh); - lc.login(); + // Authenticate and fetch a security identity using the evidence we created. + SecurityDomain domain = SecurityDomain.getCurrent(); + SecurityIdentity identity = domain.authenticate(evidence); + log.info("Obtained identity with principal {} and roles {}", identity.getPrincipal(), identity.getRoles()); - // Call secured EJBs + // Using the identity, execute the operations with the permissions of the authenticated server. try { - AuthorizationsListBase auths = userOps.listEffectiveAuthorizations(false); - System.err.println("***** Auths for user " + dn + " are: " + auths); - } finally { - // Logout, which will restore previous credentials, if any. - // Be sure to do this in a finally block. - lc.logout(); + identity.runAs((Callable) () -> { + AuthorizationsListBase auths = userOperationsBean.listEffectiveAuthorizations(false); + log.info("Authorizations for current user: {}", auths); + return null; + }); + } catch (Exception e) { + log.error("Failed to fetch user from cached credentials", e); } } catch (Exception e) { - System.err.println("Error doing login!"); - e.printStackTrace(System.err); + log.error("Failed to execute scheduled call", e); } } } diff --git a/web-services/metrics/pom.xml b/web-services/metrics/pom.xml index 3e2f365493c..93ef46dc8c2 100644 --- a/web-services/metrics/pom.xml +++ b/web-services/metrics/pom.xml @@ -15,6 +15,10 @@ datawave-query-core ${project.version} + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.webservices datawave-ws-common diff --git a/web-services/model/pom.xml b/web-services/model/pom.xml index b17af8a5c18..8dfd3d67c5c 100644 --- a/web-services/model/pom.xml +++ b/web-services/model/pom.xml @@ -27,8 +27,12 @@ commons-configuration - javax.enterprise - cdi-api + gov.nsa.datawave.commons + datawave-commons-security + + + jakarta.enterprise + jakarta.enterprise.cdi-api org.easymock @@ -94,7 +98,12 @@ org.jboss.resteasy - resteasy-jaxrs + resteasy-core + provided + + + org.jboss.resteasy + resteasy-core-spi provided @@ -114,7 +123,7 @@ org.jboss.spec.javax.transaction - jboss-transaction-api_1.2_spec + jboss-transaction-api_1.3_spec provided diff --git a/web-services/model/src/test/java/datawave/webservice/query/model/ModelBeanTest.java b/web-services/model/src/test/java/datawave/webservice/query/model/ModelBeanTest.java index ad5559cc753..9ede22f55fd 100644 --- a/web-services/model/src/test/java/datawave/webservice/query/model/ModelBeanTest.java +++ b/web-services/model/src/test/java/datawave/webservice/query/model/ModelBeanTest.java @@ -42,7 +42,7 @@ import datawave.security.authorization.DatawaveUser; import datawave.security.authorization.DatawaveUser.UserType; import datawave.security.authorization.SubjectIssuerDNPair; -import datawave.security.util.DnUtils; +import datawave.security.util.DnProperties; import datawave.security.util.ScannerHelper; import datawave.webservice.common.exception.DatawaveWebApplicationException; import datawave.webservice.model.ModelList; @@ -67,7 +67,7 @@ public class ModelBeanTest { @BeforeEach public void beforeEach() throws Exception { - System.setProperty(DnUtils.NPE_OU_PROPERTY, "iamnotaperson"); + System.setProperty(DnProperties.NPE_OU_PROPERTY, "iamnotaperson"); System.setProperty("dw.metadatahelper.all.auths", "A,B,C,D"); bean = new ModelBean(); connectionFactory = mock(AccumuloConnectionFactory.class); diff --git a/web-services/modification/pom.xml b/web-services/modification/pom.xml index b6a60831ee8..3ab1e5424e9 100644 --- a/web-services/modification/pom.xml +++ b/web-services/modification/pom.xml @@ -19,14 +19,18 @@ datawave-query-core ${project.version} + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.core datawave-core-modification ${project.version} - javax.enterprise - cdi-api + jakarta.enterprise + jakarta.enterprise.cdi-api gov.nsa.datawave.webservices @@ -59,7 +63,12 @@ org.jboss.resteasy - resteasy-jaxrs + resteasy-core + provided + + + org.jboss.resteasy + resteasy-core-spi provided diff --git a/web-services/pom.xml b/web-services/pom.xml index 2afb2ff76d2..b029c5c8407 100644 --- a/web-services/pom.xml +++ b/web-services/pom.xml @@ -13,7 +13,6 @@ accumulo common-util - security common query client @@ -27,6 +26,7 @@ model dictionary annotations + security-parent default @@ -41,6 +41,7 @@ 1.26.0 1.7 3.2 + 3.20.0 3.9.0 3.6.3 1.1 @@ -116,6 +117,11 @@ dnsjava ${version.dnsjava} + + gov.nsa.datawave + datawave-common-test + ${project.version} + gov.nsa.datawave datawave-core @@ -489,6 +495,67 @@ + + com.fasterxml.jackson.core + jackson-annotations + ${version.jackson} + provided + + + com.fasterxml.jackson.core + jackson-core + ${version.jackson} + provided + + + com.fasterxml.jackson.core + jackson-databind + ${version.jackson} + provided + + + com.fasterxml.jackson.datatype + jackson-datatype-guava + ${version.jackson} + provided + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + ${version.jackson} + provided + + + com.google.guava + guava + ${version.google-guava} + provided + + + + gov.nsa.datawave.commons + datawave-commons-security + ${project.version} + provided + + + gov.nsa.datawave.webservices + datawave-ws-security-elytron + ${project.version} + provided + + + io.jsonwebtoken + jjwt-api + ${version.jjwt} + provided + + + io.jsonwebtoken + jjwt-impl + ${version.jjwt} + provided + javax.jms jms @@ -501,6 +568,26 @@ + + org.apache.commons + commons-lang3 + ${version.commons-lang3} + provided + + + org.apache.commons + commons-text + ${version.commons-text} + + provided + + + org.slf4j + slf4j-api + ${version.slf4j} + provided + org.apache.mrunit mrunit @@ -532,12 +619,6 @@ - - - gov.nsa.datawave - datawave-common-test - - diff --git a/web-services/query-websocket/pom.xml b/web-services/query-websocket/pom.xml index d2bcd10ec76..e07543f9ea8 100644 --- a/web-services/query-websocket/pom.xml +++ b/web-services/query-websocket/pom.xml @@ -18,6 +18,10 @@ com.fasterxml.jackson.module jackson-module-jaxb-annotations + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.webservices datawave-ws-common @@ -34,17 +38,22 @@ ${project.version} - javax.enterprise - cdi-api + jakarta.enterprise + jakarta.enterprise.cdi-api - javax.json - javax.json-api + jakarta.json + jakarta.json-api provided org.jboss.resteasy - resteasy-jaxrs + resteasy-core + provided + + + org.jboss.resteasy + resteasy-core-spi provided @@ -62,6 +71,11 @@ jboss-websocket-api_1.1_spec provided + + org.wildfly + wildfly-undertow + provided + ${project.artifactId} diff --git a/web-services/query-websocket/src/main/java/datawave/webservice/websocket/QueryWebsocket.java b/web-services/query-websocket/src/main/java/datawave/webservice/websocket/QueryWebsocket.java index e25c417745c..8203f4a7482 100644 --- a/web-services/query-websocket/src/main/java/datawave/webservice/websocket/QueryWebsocket.java +++ b/web-services/query-websocket/src/main/java/datawave/webservice/websocket/QueryWebsocket.java @@ -1,12 +1,13 @@ package datawave.webservice.websocket; -import static datawave.webservice.metrics.Constants.REQUEST_LOGIN_TIME_HEADER; +import static datawave.security.util.SecurityConstants.REQUEST_LOGIN_TIME_HEADER; +import static datawave.security.websocket.WebsocketSecurityConfigurator.SESSION_SECURITY_IDENTITY; import java.io.IOException; +import java.util.Map; import java.util.concurrent.Future; import javax.inject.Inject; -import javax.interceptor.Interceptors; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; @@ -16,9 +17,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.wildfly.security.auth.server.SecurityIdentity; import datawave.security.websocket.WebsocketSecurityConfigurator; -import datawave.security.websocket.WebsocketSecurityInterceptor; import datawave.webservice.query.exception.QueryException; import datawave.webservice.query.runner.AsyncQueryStatusObserver; import datawave.webservice.query.runner.QueryExecutorBean; @@ -43,67 +44,135 @@ * Per the JSR-356 specification (section 2.1.1), since we have not configured the endpoint otherwise, there shall be one instance of this class per endpoint, * per peer. *

- * NOTE: This uses vendor-specific security extensions to work around a websocket specification hole. See - * WEBSOCKET_SPEC-238 for more details. + * NOTE: This uses the vendor-specific security extension {@link WebsocketSecurityConfigurator} to work around a websocket specification hole. + * See Jakarta EE #238 for more details. */ @ServerEndpoint(value = "/{logic-name}", encoders = {QueryResponseMessageJsonEncoder.class}, decoders = {JsonQueryMessageDecoder.class}, - configurator = WebsocketSecurityConfigurator.class // required to propagate security along to individual websocket notification calls -) -@Interceptors({WebsocketSecurityInterceptor.class}) -// required to propagate security along to individual websocket notification calls + configurator = WebsocketSecurityConfigurator.class) public class QueryWebsocket { + private static final String LOGIC_NAME = "logicName"; private static final String ACTIVE_QUERY_FUTURE = "activeQueryFuture"; private static final String ACTIVE_QUERY_ID = "activeQueryId"; - private Logger log = LoggerFactory.getLogger(getClass()); + private final Logger log = LoggerFactory.getLogger(getClass()); @Inject private QueryExecutorBean queryExecutorBean; + /** + * Runs when a new websocket connection is opened. The logic name will be extracted from the URL path and stored into the session. + * + * @param logicName + * the logic name + * @param session + * the session + */ @OnOpen - public void openConnection(@PathParam("logic-name") String logicName, Session session) throws IOException { + public void openConnection(@PathParam("logic-name") String logicName, Session session) { session.getUserProperties().put(LOGIC_NAME, logicName); } + /** + * Runs when the websocket connection is closed. If a query is currently active for the session, it will be canceled. + */ @OnClose public void closeConnection(Session session) throws IOException { - cancelActiveQuery(session); + // Ensure the operation is executed using the permissions of the calling user. + runAsSessionUser(session, () -> cancelActiveQuery(session)); } + /** + * Runs when an incoming websocket message is received. The message is expected to be either a {@link CreateQueryMessage} or a {@link CancelMessage}. + * + * @param session + * the session + * @param message + * the message + */ @OnMessage - public void handleMessage(final Session session, QueryMessage message) { + public void handleMessage(final Session session, QueryMessage message) throws IOException { switch (message.getType()) { - case CREATE: { - if (session.getUserProperties().get(ACTIVE_QUERY_FUTURE) != null) { - session.getAsyncRemote().sendObject( - new QueryResponseMessage(ResponseType.CREATION_FAILURE, "Query already active. Only one query per websocket is allowed.")); - } else { - CreateQueryMessage cqm = (CreateQueryMessage) message; - String logicName = (String) session.getUserProperties().get(LOGIC_NAME); - QueryObserver observer = new QueryObserver(log, session); - - Long startTime = System.nanoTime(); - Long loginTime = null; - try { - loginTime = Long.valueOf((String) session.getUserProperties().get(REQUEST_LOGIN_TIME_HEADER)); - } catch (Exception e) { - // Ignore -- login time won't be available - } - - Future activeQuery = queryExecutorBean.executeAsync(logicName, cqm.getParameters(), startTime, loginTime, observer); - session.getUserProperties().put(ACTIVE_QUERY_FUTURE, activeQuery); - } - } + case CREATE: + // Ensure the operation is executed using the permissions of the calling user. + runAsSessionUser(session, () -> createQuery(session, message)); break; - case CANCEL: { - cancelActiveQuery(session); - } + case CANCEL: + // Ensure the operation is executed using the permissions of the calling user. + runAsSessionUser(session, () -> cancelActiveQuery(session)); break; } } - protected void cancelActiveQuery(Session session) { + /** + * Executes the given runnable with the permissions of the calling user for the websocket session. We expected to find a {@link SecurityIdentity} stored in + * the {@value WebsocketSecurityConfigurator#SESSION_SECURITY_IDENTITY} user property. + * + * @param session + * the session + * @param runnable + * the operation to execute + */ + private void runAsSessionUser(Session session, Runnable runnable) throws IOException { + // Fetch the calling user's security identity from the session. + Map userProperties = session.getUserProperties(); + final SecurityIdentity identity = (SecurityIdentity) userProperties.get(SESSION_SECURITY_IDENTITY); + + // If no identity was found, return an error message and close the session. + if (identity == null) { + if (log.isErrorEnabled()) { + log.error("No SecurityIdentity found in session user property {}", SESSION_SECURITY_IDENTITY); + } + session.getAsyncRemote().sendObject(new QueryResponseMessage(ResponseType.ERROR, "Failed to load authenticated user")); + session.close(); + return; + } + + // Execute the runnable with the permissions of the calling user. + identity.runAs(runnable); + } + + /** + * Creates a new active query for the session. + * + * @param session + * the session + * @param message + * the message + */ + private void createQuery(Session session, QueryMessage message) { + // If the session already has an active query, do not allow another one to be created. + if (session.getUserProperties().get(ACTIVE_QUERY_FUTURE) != null) { + session.getAsyncRemote().sendObject( + new QueryResponseMessage(ResponseType.CREATION_FAILURE, "Query already active. Only one query per websocket is allowed.")); + } else { + CreateQueryMessage cqm = (CreateQueryMessage) message; + String logicName = (String) session.getUserProperties().get(LOGIC_NAME); + QueryObserver observer = new QueryObserver(log, session); + + Long startTime = System.nanoTime(); + // Extract the login time from the session. + Long loginTime = null; + try { + loginTime = Long.valueOf((String) session.getUserProperties().get(REQUEST_LOGIN_TIME_HEADER)); + } catch (Exception e) { + // Ignore -- login time won't be available + } + + // Create the query. + Future activeQuery = queryExecutorBean.executeAsync(logicName, cqm.getParameters(), startTime, loginTime, observer); + // Add a property to track that there is now an active query associated with the session. + session.getUserProperties().put(ACTIVE_QUERY_FUTURE, activeQuery); + } + } + + /** + * Cancels any active query running in the session. + * + * @param session + * the session + */ + private void cancelActiveQuery(Session session) { Future activeQuery = (Future) session.getUserProperties().get(ACTIVE_QUERY_FUTURE); if (activeQuery != null && !activeQuery.isDone()) { // Attempt to cancel the async query call. This will cause the async call to return when it is between next calls. @@ -114,15 +183,15 @@ protected void cancelActiveQuery(Session session) { try { queryExecutorBean.cancel(activeQueryId); } catch (Exception e) { - log.warn("Failed to cancel query " + activeQueryId, e); + log.warn("Failed to cancel query {}", activeQueryId, e); } } } } private static class QueryObserver implements AsyncQueryStatusObserver { - private Logger log; - private Session session; + private final Logger log; + private final Session session; public QueryObserver(Logger log, Session session) { this.log = log; @@ -172,7 +241,7 @@ public void queryFinished(String queryId) { try { session.close(); } catch (IOException e) { - log.error("Unable to close peer connection after query " + queryId + " completed.", e); + log.error("Unable to close peer connection after query {} completed.", queryId, e); throw new RuntimeException(e); } } diff --git a/web-services/query-websocket/src/main/webapp/WEB-INF/web.xml b/web-services/query-websocket/src/main/webapp/WEB-INF/web.xml index d827d4d4d63..7d8330cb653 100644 --- a/web-services/query-websocket/src/main/webapp/WEB-INF/web.xml +++ b/web-services/query-websocket/src/main/webapp/WEB-INF/web.xml @@ -12,7 +12,7 @@ --> DATAWAVE-AUTH - DATAWAVE Web Services + datawave diff --git a/web-services/query/pom.xml b/web-services/query/pom.xml index 9abcbc0d2b2..068d7171639 100644 --- a/web-services/query/pom.xml +++ b/web-services/query/pom.xml @@ -52,6 +52,10 @@ datawave-ingest-core ${project.version} + + gov.nsa.datawave.commons + datawave-commons-security + gov.nsa.datawave.core datawave-core-query @@ -162,8 +166,8 @@ provided - javax.enterprise - cdi-api + jakarta.enterprise + jakarta.enterprise.cdi-api provided @@ -183,7 +187,12 @@ org.jboss.resteasy - resteasy-jaxrs + resteasy-core + provided + + + org.jboss.resteasy + resteasy-core-spi provided @@ -218,7 +227,7 @@ org.jboss.spec.javax.transaction - jboss-transaction-api_1.2_spec + jboss-transaction-api_1.3_spec provided diff --git a/web-services/query/src/main/java/datawave/webservice/query/interceptor/QueryMetricsEnrichmentInterceptor.java b/web-services/query/src/main/java/datawave/webservice/query/interceptor/QueryMetricsEnrichmentInterceptor.java index ab10ff17850..18a888c5deb 100644 --- a/web-services/query/src/main/java/datawave/webservice/query/interceptor/QueryMetricsEnrichmentInterceptor.java +++ b/web-services/query/src/main/java/datawave/webservice/query/interceptor/QueryMetricsEnrichmentInterceptor.java @@ -13,9 +13,9 @@ import javax.ws.rs.ext.WriterInterceptorContext; import org.apache.log4j.Logger; -import org.jboss.resteasy.core.interception.ContainerResponseContextImpl; -import org.jboss.resteasy.core.interception.PreMatchContainerRequestContext; -import org.jboss.resteasy.util.FindAnnotation; +import org.jboss.resteasy.core.interception.jaxrs.ContainerResponseContextImpl; +import org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext; +import org.jboss.resteasy.spi.util.FindAnnotation; import datawave.core.query.logic.BaseQueryLogic; import datawave.core.query.logic.QueryLogic; diff --git a/web-services/query/src/main/java/datawave/webservice/query/remote/RemoteQueryServiceImpl.java b/web-services/query/src/main/java/datawave/webservice/query/remote/RemoteQueryServiceImpl.java index 289217761fc..e6a37382a78 100644 --- a/web-services/query/src/main/java/datawave/webservice/query/remote/RemoteQueryServiceImpl.java +++ b/web-services/query/src/main/java/datawave/webservice/query/remote/RemoteQueryServiceImpl.java @@ -27,9 +27,9 @@ import datawave.core.query.remote.RemoteQueryService; import datawave.core.query.remote.RemoteTimeoutQueryException; -import datawave.security.auth.DatawaveAuthenticationMechanism; import datawave.security.authorization.DatawavePrincipal; import datawave.security.authorization.ProxiedUserDetails; +import datawave.security.util.SecurityConstants; import datawave.webservice.common.remote.RemoteHttpService; import datawave.webservice.query.exception.QueryException; import datawave.webservice.result.BaseQueryResponse; @@ -39,8 +39,8 @@ public class RemoteQueryServiceImpl extends RemoteHttpService implements RemoteQueryService { private static final Logger log = LoggerFactory.getLogger(RemoteQueryServiceImpl.class); - public static final String PROXIED_ENTITIES_HEADER = DatawaveAuthenticationMechanism.PROXIED_ENTITIES_HEADER; - public static final String PROXIED_ISSUERS_HEADER = DatawaveAuthenticationMechanism.PROXIED_ISSUERS_HEADER; + public static final String PROXIED_ENTITIES_HEADER = SecurityConstants.PROXIED_ENTITIES_HEADER; + public static final String PROXIED_ISSUERS_HEADER = SecurityConstants.PROXIED_ISSUERS_HEADER; private static final String CREATE = "%s/create"; diff --git a/web-services/query/src/test/java/datawave/webservice/query/interceptor/QueryMetricsEnrichmentInterceptorTest.java b/web-services/query/src/test/java/datawave/webservice/query/interceptor/QueryMetricsEnrichmentInterceptorTest.java index 1f8646b757c..f682b30f179 100644 --- a/web-services/query/src/test/java/datawave/webservice/query/interceptor/QueryMetricsEnrichmentInterceptorTest.java +++ b/web-services/query/src/test/java/datawave/webservice/query/interceptor/QueryMetricsEnrichmentInterceptorTest.java @@ -27,13 +27,13 @@ import org.easymock.Capture; import org.easymock.IAnswer; -import org.jboss.resteasy.core.interception.ContainerResponseContextImpl; -import org.jboss.resteasy.core.interception.PreMatchContainerRequestContext; +import org.jboss.resteasy.core.interception.jaxrs.ContainerResponseContextImpl; +import org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext; import org.jboss.resteasy.specimpl.BuiltResponse; +import org.jboss.resteasy.specimpl.ResteasyUriInfo; import org.jboss.resteasy.spi.HttpRequest; -import org.jboss.resteasy.spi.ResteasyUriInfo; -import org.jboss.resteasy.util.FindAnnotation; -import org.jboss.resteasy.util.HttpResponseCodes; +import org.jboss.resteasy.spi.HttpResponseCodes; +import org.jboss.resteasy.spi.util.FindAnnotation; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -48,7 +48,7 @@ import datawave.core.query.logic.BaseQueryLogic; import datawave.microservice.querymetric.BaseQueryMetric.PageMetric; import datawave.microservice.querymetric.QueryMetric; -import datawave.security.util.DnUtils; +import datawave.security.util.DnProperties; import datawave.webservice.query.annotation.EnrichQueryMetrics; import datawave.webservice.query.cache.QueryCache; import datawave.webservice.query.interceptor.QueryMetricsEnrichmentInterceptor.QueryCall; @@ -117,7 +117,7 @@ public class QueryMetricsEnrichmentInterceptorTest { @Before public void setup() { - System.setProperty(DnUtils.NPE_OU_PROPERTY, "iamnotaperson"); + System.setProperty(DnProperties.NPE_OU_PROPERTY, "iamnotaperson"); System.setProperty("dw.metadatahelper.all.auths", "A,B,C,D"); // noinspection unchecked diff --git a/web-services/query/src/test/java/datawave/webservice/query/logic/composite/CompositeQueryLogicTest.java b/web-services/query/src/test/java/datawave/webservice/query/logic/composite/CompositeQueryLogicTest.java index 15e8a2597df..9a78325c5a0 100644 --- a/web-services/query/src/test/java/datawave/webservice/query/logic/composite/CompositeQueryLogicTest.java +++ b/web-services/query/src/test/java/datawave/webservice/query/logic/composite/CompositeQueryLogicTest.java @@ -50,7 +50,7 @@ import datawave.security.authorization.ProxiedUserDetails; import datawave.security.authorization.SubjectIssuerDNPair; import datawave.security.authorization.UserOperations; -import datawave.security.util.DnUtils; +import datawave.security.util.DnProperties; import datawave.user.AuthorizationsListBase; import datawave.user.DefaultAuthorizationsList; import datawave.webservice.query.exception.QueryException; @@ -505,7 +505,7 @@ public boolean isFiltered() { @Before public void setup() { - System.setProperty(DnUtils.NPE_OU_PROPERTY, "iamnotaperson"); + System.setProperty(DnProperties.NPE_OU_PROPERTY, "iamnotaperson"); System.setProperty("dw.metadatahelper.all.auths", "A,B,C,D"); } diff --git a/web-services/query/src/test/java/datawave/webservice/query/runner/ExtendedRunningQueryTest.java b/web-services/query/src/test/java/datawave/webservice/query/runner/ExtendedRunningQueryTest.java index 6675c693e14..ec47ea94bf8 100644 --- a/web-services/query/src/test/java/datawave/webservice/query/runner/ExtendedRunningQueryTest.java +++ b/web-services/query/src/test/java/datawave/webservice/query/runner/ExtendedRunningQueryTest.java @@ -45,7 +45,7 @@ import datawave.security.authorization.DatawaveUser; import datawave.security.authorization.DatawaveUser.UserType; import datawave.security.authorization.SubjectIssuerDNPair; -import datawave.security.util.DnUtils; +import datawave.security.util.DnProperties; import datawave.webservice.query.cache.RunningQueryTimingImpl; import datawave.webservice.query.metric.QueryMetricsBean; import datawave.webservice.query.util.QueryUncaughtExceptionHandler; @@ -68,7 +68,7 @@ public class ExtendedRunningQueryTest { @BeforeEach public void beforeEach() { - System.setProperty(DnUtils.NPE_OU_PROPERTY, "iamnotaperson"); + System.setProperty(DnProperties.NPE_OU_PROPERTY, "iamnotaperson"); System.setProperty("dw.metadatahelper.all.auths", "A,B,C,D"); executor = Executors.newSingleThreadExecutor(); } diff --git a/web-services/query/src/test/java/datawave/webservice/query/runner/QueryExecutorBeanTest.java b/web-services/query/src/test/java/datawave/webservice/query/runner/QueryExecutorBeanTest.java index 45e44a824bb..fa490ad4f73 100644 --- a/web-services/query/src/test/java/datawave/webservice/query/runner/QueryExecutorBeanTest.java +++ b/web-services/query/src/test/java/datawave/webservice/query/runner/QueryExecutorBeanTest.java @@ -42,11 +42,11 @@ import org.apache.log4j.Logger; import org.easymock.EasyMock; import org.easymock.IAnswer; -import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.specimpl.MultivaluedMapImpl; +import org.jboss.resteasy.spi.Dispatcher; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -92,7 +92,7 @@ import datawave.security.authorization.DatawaveUser; import datawave.security.authorization.DatawaveUser.UserType; import datawave.security.authorization.SubjectIssuerDNPair; -import datawave.security.util.DnUtils; +import datawave.security.util.DnProperties; import datawave.security.util.WSAuthorizationsUtil; import datawave.webservice.common.audit.AuditBean; import datawave.webservice.common.audit.AuditParameterBuilder; @@ -163,7 +163,7 @@ public class QueryExecutorBeanTest { @Before public void setup() throws Exception { - System.setProperty(DnUtils.NPE_OU_PROPERTY, "iamnotaperson"); + System.setProperty(DnProperties.NPE_OU_PROPERTY, "iamnotaperson"); System.setProperty("dw.metadatahelper.all.auths", "A,B,C,D"); QueryTraceCache traceCache = new QueryTraceCache(); Whitebox.invokeMethod(traceCache, "init"); diff --git a/web-services/query/src/test/java/datawave/webservice/query/runner/RunningQueryTest.java b/web-services/query/src/test/java/datawave/webservice/query/runner/RunningQueryTest.java index 30bf16ada90..caf36dbb26d 100644 --- a/web-services/query/src/test/java/datawave/webservice/query/runner/RunningQueryTest.java +++ b/web-services/query/src/test/java/datawave/webservice/query/runner/RunningQueryTest.java @@ -53,7 +53,7 @@ import datawave.security.authorization.DatawaveUser; import datawave.security.authorization.DatawaveUser.UserType; import datawave.security.authorization.SubjectIssuerDNPair; -import datawave.security.util.DnUtils; +import datawave.security.util.DnProperties; import datawave.webservice.query.logic.TestQueryLogic; import datawave.webservice.query.logic.composite.CompositeQueryLogicTest; import datawave.webservice.result.BaseQueryResponse; @@ -78,7 +78,7 @@ class SampleGenericQueryConfiguration extends GenericQueryConfiguration { @Before public void setup() throws MalformedURLException, IllegalArgumentException, IllegalAccessException { - System.setProperty(DnUtils.NPE_OU_PROPERTY, "iamnotaperson"); + System.setProperty(DnProperties.NPE_OU_PROPERTY, "iamnotaperson"); System.setProperty("dw.metadatahelper.all.auths", "A,B,C,D"); settings.setQueryLogicName("testQueryLogic"); diff --git a/web-services/rest-api/src/main/webapp/WEB-INF/web.xml b/web-services/rest-api/src/main/webapp/WEB-INF/web.xml index 98132ab8325..c5b8b30b092 100644 --- a/web-services/rest-api/src/main/webapp/WEB-INF/web.xml +++ b/web-services/rest-api/src/main/webapp/WEB-INF/web.xml @@ -113,8 +113,8 @@ datawave.webservice.result.VoidResponseHtmlMessageBodyWriter, datawave.resteasy.util.DateParamConverterProvider, datawave.security.authorization.DatawaveUserHtmlMessageBodyWriter, - org.jboss.resteasy.plugins.interceptors.encoding.GZIPEncodingInterceptor, - org.jboss.resteasy.plugins.interceptors.encoding.GZIPDecodingInterceptor, + org.jboss.resteasy.plugins.interceptors.GZIPEncodingInterceptor, + org.jboss.resteasy.plugins.interceptors.GZIPDecodingInterceptor, datawave.security.user.TextMessageBodyWriter @@ -144,7 +144,7 @@ --> DATAWAVE-AUTH - DATAWAVE Web Services + datawave diff --git a/web-services/security-parent/README.md b/web-services/security-parent/README.md new file mode 100644 index 00000000000..9cd8349f193 --- /dev/null +++ b/web-services/security-parent/README.md @@ -0,0 +1,17 @@ +# Overview + +This project contains security-related classes that are commonly used between the main Datawave project and the microservices. + +## [datawave-ws-security](security) + +Contains security-related EJBs. Expected to be deployed as part of the Datawave EAR. + +## [datawave-ws-security-elytron](security-elytron) + +Contains security-related artifacts that are commonly used between projects that are deployed as part of the Datawave EAR, and the datawave-ws-security-elytron-module project. It is expected to be configured and deployed as a JBOSS module via the [Wildfly assembly](../../web-services/deploy/application) project. + +## [datawave-ws-security-elytron-module](security-elytron-module) + +Contains security-related artifacts that are required to create the Wildfly security domains that are used for authentication and authorization. It is expected to be configured and deployed as a JBOSS module via the [Wildfly assembly](../../web-services/deploy/application) project. Wildfly requires custom Elytron security components to be deployed in separate JBOSS modules. See the project [README](security-elytron-module/README.md) for more details. +## Note: +All non-test dependencies for datawave-ws-security-elytron and datawave-ws-security-elytron-module are expected have the scope `provided`. These dependencies must be provided via JBOSS modules in order to avoid classloader conflicts between the JBOSS modules and the Datawave EAR deployment. See the [Wildfly assembly README](../deploy/application/README.md) for more details. diff --git a/web-services/security-parent/pom.xml b/web-services/security-parent/pom.xml new file mode 100644 index 00000000000..ac1955753ca --- /dev/null +++ b/web-services/security-parent/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + gov.nsa.datawave.webservices + datawave-ws-parent + 7.40.0-SNAPSHOT + + + datawave-ws-security-parent + pom + ${project.artifactId} + + + security + security-elytron-module + security-elytron + + + diff --git a/web-services/security-parent/security-elytron-module/README.md b/web-services/security-parent/security-elytron-module/README.md new file mode 100644 index 00000000000..2f5933d0eb8 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/README.md @@ -0,0 +1,90 @@ +# Overview + +Datawave leverages Wildfly's Elytron framework for authenticating and authorizing users within the Datawave EAR deployment. The full documentation for Wildfly is available via the [Wildfly docs](https://docs.wildfly.org/26/). There are several key concepts to understand when discussing Elytron, and for the sake of brevity, this README will only touch on concepts that are used by the Datawave security configuration. + +## Key Concepts +* `HttpServerAuthenticationMechanismFactory` + * An HttpServerAuthenticationMechanismFactory is responsible for providing instances of an HttpServerAuthenticationMechanism, and can be associated with specific mechanisms that it supports, such as BASIC, DIGEST, FORM, etc. +* `HttpServerAuthenticationMechanism` + * An HttpServerAuthenticationMechanism is an authentication policy for authentication using HTTP/Websocket mechanisms. A HttpAuthenticationMechanism will be backed by a SecurityDomain. +* `SecurityDomain` + * SecurityDomain can be considered a security policy that is backed by one or more SecurityRealm instances. The SecurityDomain is responsible for providing a SecurityIdentity, which is a representation of the current identity with roles and permissions. The SecurityDomain is the general wrapper around the policy providing a resulting SecurityIdentity, and makes use of the following components to define this policy: + +* `SecurityRealm` + * One more named SecurityRealms are associated with a SecurityDomain, the SecurityRealms are the access to the underlying repository of identities and are used for obtaining credentials to allow authentication mechanisms to perform verification, for validation of Evidence and for obtaining the raw AuthorizationIdentity performing the authentication. +* `RoleDecoder` + * Along with the SecurityRealm association is also a reference to a RoleDecoder, the RoleDecoder takes the raw AuthorizationIdentity returned from the SecurityRealm and converts its attributes into roles. +* `EvidenceDecoder` + * A EvidenceDecoder converts from an Evidence to a Principal. +* `PermissionMapper` + * In addition to having roles a SecurityIdentity can also have a set of permissions, the PermissionMapper assigns those permissions to the identity. Crucially, one such Permissions is the LoginPermission, which allows a request to be successfully authorized. + +## Datawave Elytron Implementations + +* [`DatawaveHttpAuthenticationMechanismFactory`](src/main/java/datawave/security/auth/DatawaveHttpAuthenticationMechanismFactory.java): An implementation of HttpServerAuthenticationMechanismFactory. This class is responsible for providing instances of `DatawaveHttpAuthenticationMechanism`, which will handle incoming HTTP Requests for the mechanism `DATAWAVE-AUTH`. In order to make this factory detected as a service by Wildfly, the fully qualified name of the class must be in a file named org.wildfly.security.http.HttpServerAuthenticationMechanismFactory in the META-INF/services folder. +* [`DatawaveHttpAuthenticationMechanism`](src/main/java/datawave/security/auth/DatawaveHttpAuthenticationMechanism.java): An implementation of HttpServerAuthenticationMechanism. This class is responsible for extracting evidence from incoming HTTP requests (JWT, trusted headers, PKI certs) and submitting them to the backing security domain for authentication and authorization. +* [`DatawaveEvidenceDecoder`](src/main/java/datawave/security/realm/DatawaveEvidenceDecoder.java): An implementation of EvidenceDecoder that will convert an incoming Evidence to a DatawavePrincipal. +* [`DatawaveSecurityRealm`](src/main/java/datawave/security/realm/DatawaveSecurityRealm.java):: An implementation of SecurityRealm that is responsible for providing a RealmIdentity for authentication that is associated with a DatawavePrincipal. This realm is also responsible for extracting any information required for determining the roles of a user, and storing them within the RealmIdentity's Attributes. +* [`DatawaveRoleDecoder`](src/main/java/datawave/security/realm/DatawaveRoleDecoder.java): An implementation of RoleDecoder that will accept an AuthorizationIdentity provided by a RealmIdentity from the DatawaveSecurityRealm, and return a set of roles to be associated with the final SecurityIdentity established within the security domain. + +## Basic Workflow + +This workflow description assumes that a security domain has been configured to: + - Pass HTTP requests to `DatawaveHttpAuthenticationMechanism`. + - Decode evidence using `DatawaveEvidenceDecoder`. + - Obtain a RealmIdentity using `DatawaveSecurityRealm`. + - Decode roles using `DatawaveRoleDecoder`. + - Use the default permissions mapper provided by Wildfly. + +1. An incoming request is received and sent to `DatawaveHttpAuthenticationMechanism.evaluateRequest(HttpServerRequest)`. +2. If configured, the session scope for the request is examined for a previously cached identity. If one is found, we authorize using that identity. (Step 5, but using the cached identity's principal instead of a principal decoded from evidence). +3. If an identity is not cached, we extract a piece of DatawaveEvidence (JWT, trusted headers, PKI cert) identifying the user from the HttpServerRequest. +4. The Evidence is verified. This triggers the following: + 1. The Evidence is passed to `DatawaveEvidenceDecoder.getPrincipal(Evidence)` to obtain a DatawavePrincipal. This DatawavePrincipal is set as the Evidence's decoded principal. + 2. The DatawavePrincipal is passed to `DatawaveSecurityRealm.getRealmIdentity(Principal)` to obtain a RealmIdentity. + 3. Roles are extracted from the DatawavePrincipal and mapped to an Attributes that will be part of the AuthorizationIdentity returned from the RealmIdentity. + 4. The Evidence is passed to the RealmIdentity's `verifyEvidence(Evidence)`. If this returns false, i.e., fails verification, the user cannot be authorized, and login fails. +5. If verification succeeds, we then attempt to authorize the request using the DatawaveEvidence's decoded principal, which will be a DatawavePrincipal. This triggers the following: + 1. The RealmIdentity associated with the DatawavePrincipal is obtained from the security realm. + 2. The AuthorizationIdentity from the RealmIdentity is passed to `DatawaveRoleDecoder.decodeRoles(AuthorizationIdentity)` to obtain a set of Roles for the user. + 3. If a non-empty role set was returned, the user is granted LoginPermission and BatchPermissions by the default permissions mapper, and login succeeds. If an empty role set is returned, login fails. + +This workflow is primarily executed through a series of Callbacks passed to the Wildfly framework. These callbacks are passed to the Wildfly class `org.wildfly.security.auth.server.ServerAuthenticationContext` which manages a state machine and handles the workflow steps described above. The Wildfly Elytron source code can be viewed at [Github](https://github.com/wildfly-security/wildfly-elytron). + +## Configuration Highlights + +### EJB JNDI Lookup +One of the limitations of implementing custom Wildfly Elytron components is that they must be deployed as a separate Wildfly module, and cannot be bundled with the EAR deployment. Wildfly does not support injecting EJBs from a deployment into a separate Wildfly module, so we are forced to use JNDI to look up a [SecurityEJBProvider](../security-elytron/src/main/java/datawave/security/system/SecurityEJBProvider.java) instance that should be created and bound from the Datawave deployment. By default, this will be the singleton instance of [SecurityEJBProviderImpl](../security/src/main/java/datawave/security/system/SecurityEJBProviderImpl.java). The JNDI name must be configured via the system property `dw.security.ejb.provider.jndi`. For example: +```text +/system-property=dw.security.ejb.provider.jndi:add(value=java:global/datawave-ws-deploy-application-${project.version}-compose/gov.nsa.datawave.webservices-datawave-ws-security-${project.version}/SecurityEJBProviderImpl) +``` +The elytron module can then access EJBs created by the Datawave deployment through the EJB provider. + +### Request Session Identity Caching +The DatawaveHttpAuthenticationMechanism can be configured to cache an identity in the request's session, and to change the session ID via the configuration properties `enableRestoreIdentity` and `enableSessionIdChange`. These options are enabled by default. + +### JWT and Trusted Header Authentication +JSON Web Token (JWT) and trusted header authentication is disabled by default, unless enabled in the DatawaveEvidenceDecoder configuration via the properties `jwtEnabled` and `trustedHeaderEnabled`. The header that the mechanism will extract a trusted subject DN from will be loaded in priority from: +1. The DatawaveHttpAuthenticationMechanism configuration properties `trustedSubjectDnHeader`. If not specified, then: +2. The system properties `dw.trusted.header.subjectDn`. If not specified, then: +3. The header `X-SSL-ClientCert-Subject` will be used. + +The header that the mechanism will extract a trusted issuer DN from will be loaded in priority from: +1. The DatawaveHttpAuthenticationMechanism configuration properties `trustedIssuerDnHeader`. If not specified, then: +2. The system properties `dw.trusted.header.issuerDn`. If not specified, then: +3. The header `X-SSL-ClientCert-Issuer` will be used. + + +### PKI Certificate Validation +A PKI certificate validator class can be set via the DatawaveSecurityRealm configuration property `certVerifier`. The class must implement `datawave.security.cert.X509CertificateVerifier`. If the class is an instance of `datawave.security.cert.DatawaveCertVerifier`, the OSCP level for the verifier must be set via the DatawaveSecurityRealm configuration property `oscpLevel`. + +### Local Roles +In addition to the roles returned by the Datawave user service, local roles can be supplied for authenticated users via a properties file where the keys match either a DatawavePrincipal name, or a DatawaveEvidence username (case-insensitive), and the values are comma-delimited roles. To load these roles, the exact path of the role properties file must be specified via the DatawaveSecurityRealm configuration property `roleProperties`. Any local roles for matching users will be part of their final SecurityIdentity. + +### Caching +Both `DatawaveEvidenceDecoder` and `DatawaveSecurityRealm` maintain caches for improving performances when returning DatawavePrincipal and RealmIdentity instances. The maximum size of the caches and the time to live in milliseconds for entries in the cache can be specified via the configuration properties `maxCacheEntries` and `maxCacheAge` for both DatawaveEvidenceDecoder and DatawaveSecurityRealm. The default values for both is `-1`, and negative values imply no limit. + +Additionally, both the DatawaveUser cache in DatawaveEvidenceDecoder and the RealmIdentity cache in DatawaveSecurityRealm will be added to the ElytronCacheManager if available via the configured SecurityEJBProvider from JNDI. These caches can then subsequently be used to fetch DatawaveUsers in the CredentialsCacheBean. + +## Client to Client Authentication +See the class [ClientAuthenticationExample](../security-examples/src/main/java/datawave/security/examples/ClientAuthenticationExample.java) for an example of how to programmatically obtain a SecurityIdentity for executing secured operations from an unsecured context. diff --git a/web-services/security-parent/security-elytron-module/pom.xml b/web-services/security-parent/security-elytron-module/pom.xml new file mode 100644 index 00000000000..6db87a32dc5 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/pom.xml @@ -0,0 +1,142 @@ + + + 4.0.0 + + gov.nsa.datawave.webservices + datawave-ws-security-parent + 7.40.0-SNAPSHOT + + + datawave-ws-security-elytron-module + ${project.artifactId} + + + + 2.9.3 + 2.3.0 + + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + com.fasterxml.jackson.datatype + jackson-datatype-guava + provided + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + provided + + + com.github.ben-manes.caffeine + caffeine + ${version.caffeine} + provided + + + com.google.guava + guava + provided + + + gov.nsa.datawave.commons + datawave-commons-security + provided + + + gov.nsa.datawave.webservices + datawave-ws-security-elytron + ${project.version} + provided + + + org.apache.commons + commons-lang3 + provided + + + org.slf4j + slf4j-api + provided + + + org.wildfly + wildfly-security + provided + + + org.wildfly + wildfly-undertow + provided + + + org.apache.logging.log4j + log4j-1.2-api + test + + + org.assertj + assertj-core + test + + + org.bouncycastle + bcpkix-jdk15on + 1.67 + test + + + org.bouncycastle + bcprov-jdk15on + 1.67 + test + + + org.junit-pioneer + junit-pioneer + ${version.junit-pioneer} + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.platform + junit-platform-suite + test + + + org.mockito + mockito-core + ${version.mockito} + test + + + + + ${project.artifactId} + + + true + src/main/resources + + + test-classes + true + src/test/resources + + *.pkcs12 + *.p12 + *.jks + + + + + diff --git a/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/auth/DatawaveHttpAuthenticationMechanism.java b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/auth/DatawaveHttpAuthenticationMechanism.java new file mode 100644 index 00000000000..dba3632acd3 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/auth/DatawaveHttpAuthenticationMechanism.java @@ -0,0 +1,622 @@ +package datawave.security.auth; + +import static datawave.security.auth.DatawaveHttpAuthenticationMechanismFactory.DATAWAVE_AUTH_NAME; +import static datawave.security.util.SecurityConstants.DEFAULT_TRUSTED_ISSUER_DN_HEADER; +import static datawave.security.util.SecurityConstants.DEFAULT_TRUSTED_SUBJECT_DN_HEADER; +import static datawave.security.util.SecurityConstants.PROXIED_ENTITIES_HEADER; +import static datawave.security.util.SecurityConstants.PROXIED_ISSUERS_HEADER; +import static datawave.security.util.SecurityConstants.TRUSTED_ISSUER_DN_HEADER_SYSTEM_PROPERTY; +import static datawave.security.util.SecurityConstants.TRUSTED_SUBJECT_DN_HEADER_SYSTEM_PROPERTY; + +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; +import java.util.function.Supplier; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; + +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.wildfly.security.auth.callback.AuthenticationCompleteCallback; +import org.wildfly.security.auth.callback.CachedIdentityAuthorizeCallback; +import org.wildfly.security.auth.callback.EvidenceVerifyCallback; +import org.wildfly.security.auth.callback.PrincipalAuthorizeCallback; +import org.wildfly.security.auth.server.SecurityIdentity; +import org.wildfly.security.cache.CachedIdentity; +import org.wildfly.security.cache.IdentityCache; +import org.wildfly.security.evidence.Evidence; +import org.wildfly.security.http.HttpAuthenticationException; +import org.wildfly.security.http.HttpScope; +import org.wildfly.security.http.HttpServerAuthenticationMechanism; +import org.wildfly.security.http.HttpServerRequest; +import org.wildfly.security.http.Scope; +import org.wildfly.security.mechanism.AuthenticationMechanismException; +import org.wildfly.security.x500.X500; + +import datawave.security.evidence.EvidenceFactory; +import datawave.security.evidence.JWTEvidence; +import datawave.security.evidence.TrustedHeaderEvidence; +import datawave.security.evidence.X509CertificateEvidence; +import datawave.security.util.SecurityConstants; +import datawave.security.utils.ConfigUtils; + +/** + * A custom {@link HttpServerAuthenticationMechanism} that handles authentication for the mechanism + * {@value DatawaveHttpAuthenticationMechanismFactory#DATAWAVE_AUTH_NAME}. + */ +public class DatawaveHttpAuthenticationMechanism implements HttpServerAuthenticationMechanism { + + private static final Logger log = LoggerFactory.getLogger(DatawaveHttpAuthenticationMechanism.class); + + /** + * The HTTP header to fetch JWT tokens from. + */ + private static final String AUTHORIZATION_HEADER = "Authorization"; + + /** + * The expected prefix for any tokens found in the header '{@value AUTHORIZATION_HEADER}'. + */ + private static final String JWT_TOKEN_PREFIX = "Bearer "; + + /** + * The length of the string {@link #JWT_TOKEN_PREFIX}. + */ + private static final int JWT_TOKEN_PREFIX_LEN = JWT_TOKEN_PREFIX.length(); + + /** + * The key used when attaching a cached authorization result to an HTTP scope. + */ + protected static final String CACHED_IDENTITY_KEY = DatawaveHttpAuthenticationMechanism.class.getName() + ".elytron-identity"; + + /** + * The configuration for this mechanism. + */ + private final Config config; + + /** + * The callback handler that will handle executing any callbacks we need to. + */ + private final CallbackHandler callbackHandler; + + public DatawaveHttpAuthenticationMechanism(Map properties, CallbackHandler callbackHandler) { + this.callbackHandler = callbackHandler; + // Parse the configuration. + this.config = Config.fromMap(properties); + if (log.isTraceEnabled()) { + log.trace("Created mechanism with config: {} from properties: {}", config, properties); + } + } + + /** + * Return the mechanism name {@value datawave.security.auth.DatawaveHttpAuthenticationMechanismFactory#DATAWAVE_AUTH_NAME}. + * + * @return the mechanism name + */ + @Override + public String getMechanismName() { + return DATAWAVE_AUTH_NAME; + } + + /** + * Evaluate and attempt to authenticate the given request. + * + * @param request + * the request to authenticate + * @throws HttpAuthenticationException + * if an error occurs or authentication fails + */ + @Override + public void evaluateRequest(HttpServerRequest request) throws HttpAuthenticationException { + // If identity restoration is enabled, and we succeed reauthentication, this is a success. + if (config.isIdentityRestorationEnabled() && attemptReauthentication(request)) { + log.trace("Reauthentication succeeded"); + return; + } + + // Otherwise, attempt to perform a new authentication. + if (attemptAuthentication(request)) { + log.trace("New authentication succeeded"); + return; + } + + // If we've reached this point, authentication has failed. + log.trace("Both re-authentication and authentication failed"); + + try { + // Free any resources required for the authentication process. + handleCallback(AuthenticationCompleteCallback.FAILED); + } catch (AuthenticationMechanismException e) { + throw e.toHttpAuthenticationException(); + } + + // Mark in the request that authentication failed. + request.authenticationFailed("Authentication failed"); + } + + /** + * Attempt to reauthenticate the calling user using an identity cached for the session, if any. + * + * @param request + * the request + * @return true if reauthentication succeeded, or false otherwise + * @throws HttpAuthenticationException + * if an error occurs + */ + private boolean attemptReauthentication(HttpServerRequest request) throws HttpAuthenticationException { + IdentityCache identityCache = createIdentityCache(request); + + // Authorize this attempt if a cached identity is present in the session, and it has permissions. + CachedIdentityAuthorizeCallback authorizeCallback = new CachedIdentityAuthorizeCallback(identityCache); + return attemptAuthorization(request, authorizeCallback, authorizeCallback::isAuthorized, identityCache::remove); + } + + /** + * Attempt to authenticate the given request. + * + * @param request + * the request to authenticate + * @return true if authentication succeeded, or false otherwise + * @throws HttpAuthenticationException + * if an error occurs or authentication fails + */ + private boolean attemptAuthentication(HttpServerRequest request) throws HttpAuthenticationException { + // Obtain a piece of evidence that identifies the calling user. + Evidence evidence; + try { + evidence = getEvidence(request); + } catch (Exception e) { + throw new HttpAuthenticationException("Error occurred when obtaining evidence for authentication", e); + } + + // If we failed to obtain any evidence, fail the request. + if (evidence == null) { + log.trace("Failed to obtain any evidence for authentication"); + return false; + } + + if (log.isTraceEnabled()) { + log.trace("Attempting authentication with evidence: {}", evidence); + } + + // Verify the evidence. This will load the decoded principal into the evidence. NOTE: this mechanism expects an evidence decoder such as + // DatawaveEvidenceDecoder to be configured with the backing security domain. + EvidenceVerifyCallback evidenceVerifyCallback = new EvidenceVerifyCallback(evidence); + try { + handleCallback(evidenceVerifyCallback); + } catch (AuthenticationMechanismException e) { + throw e.toHttpAuthenticationException(); + } + + // If the evidence passed verification, attempt to authorize the decoded principal. Authorization will only succeed if the user has valid roles. + if (evidenceVerifyCallback.isVerified()) { + // If we should restore identities, cache the identity for the request session if authorization passes. + if (config.isIdentityRestorationEnabled()) { + IdentityCache identityCache = createIdentityCache(request); + CachedIdentityAuthorizeCallback authorizeCallback = new CachedIdentityAuthorizeCallback(evidence.getDecodedPrincipal(), identityCache); + return attemptAuthorization(request, authorizeCallback, authorizeCallback::isAuthorized, identityCache::remove); + } else { + // Otherwise, attempt to authorize the identity and do not cache it. + PrincipalAuthorizeCallback authorizedCallback = new PrincipalAuthorizeCallback(evidence.getDecodedPrincipal()); + return attemptAuthorization(request, authorizedCallback, authorizedCallback::isAuthorized, null); + } + } else { + if (log.isTraceEnabled()) { + log.trace("Evidence verification failed with decoded principal {}", evidence.getDecodedPrincipal()); + } + return false; + } + } + + /** + * Attempt to authorize the request using the given authorization callback to execute the authentication workflow. This method exists to handle different + * {@link Callback} types that may perform authorization. + * + * @param request + * the request + * @param authorizeCallback + * the callback + * @param authorizeResult + * the supplier that will return whether authentication succeeded via the callback + * @param logoutHandler + * an operation that should execute when the session logs out. This may be null, or an operation to remove an identity from the identity cache + * after the session is finished. + * @return true if authorization succeeded, or false otherwise + * @throws HttpAuthenticationException + * if an error occurs + */ + private boolean attemptAuthorization(HttpServerRequest request, Callback authorizeCallback, Supplier authorizeResult, Runnable logoutHandler) + throws HttpAuthenticationException { + // Attempt to authorize the request. + try { + handleCallback(authorizeCallback); + } catch (AuthenticationMechanismException e) { + throw e.toHttpAuthenticationException(); + } + // If authorization succeeded, mark the request as successfully authorized. + if (authorizeResult.get()) { + succeed(request, logoutHandler); + return true; + } else { + return false; + } + } + + /** + * Mark the request with a successful authentication. + * + * @param request + * the request to mark as succeeded. + * @param logoutHandler + * an operation that should execute when the session logs out. This may be null, or an operation to remove an identity from the identity cache + * after the session is finished. + */ + private void succeed(HttpServerRequest request, Runnable logoutHandler) throws HttpAuthenticationException { + try { + handleCallback(AuthenticationCompleteCallback.SUCCEEDED); + } catch (AuthenticationMechanismException e) { + throw e.toHttpAuthenticationException(); + } + + request.authenticationComplete(null, logoutHandler); + } + + /** + * Execute the given callbacks using the callback handler of this {@link DatawaveHttpAuthenticationMechanism}. + * + * @param callbacks + * the callbacks to handle + * @throws AuthenticationMechanismException + * if an authentication error occurs + */ + private void handleCallback(Callback... callbacks) throws AuthenticationMechanismException { + try { + this.callbackHandler.handle(callbacks); + } catch (AuthenticationMechanismException e) { + throw e; + } catch (Throwable e) { + throw new AuthenticationMechanismException("Callback handler failed for unknown reason", e); + } + } + + /** + * Attempt to extract {@link Evidence} from the request representing the user that can be used for authentication and authorization. + * + * @param request + * the request + * @return the first valid {@link Evidence} found, or null if none was found + * @throws MultipleHeaderValuesException + * if a header had multiple values + * @throws MissingHeaderException + * if a header was missing + */ + private Evidence getEvidence(HttpServerRequest request) throws MultipleHeaderValuesException, MissingHeaderException { + // First check if we have a JSON web token. + Evidence evidence = getJwtEvidence(request); + if (evidence != null) { + return evidence; + } + + // Proxied entities and issuers may be specified in configured headers. Extract them for use when either creating evidence from a certificate or trusted + // headers. + Pair proxiedHeaderValues = getProxiedEntitiesAndIssuers(request); + String proxiedEntities = proxiedHeaderValues.getLeft(); + String proxiedIssuers = proxiedHeaderValues.getRight(); + if (log.isTraceEnabled()) { + log.trace("Authenticating with proxied entities={} amd proxied issuers={}", proxiedEntities, proxiedIssuers); + } + + // Next check if we have a certificate from an SSL session. + evidence = getX509Evidence(request, proxiedEntities, proxiedIssuers); + if (evidence != null) { + return evidence; + } + + // Lastly check if we have entities stored in trusted headers. + return getTrustedHeadersEvidence(request, proxiedEntities, proxiedIssuers); + } + + /** + * Extract proxied entities and proxied issuers (if any) from the request headers {@value SecurityConstants#PROXIED_ENTITIES_HEADER} and + * {@value SecurityConstants#PROXIED_ISSUERS_HEADER}. + * + * @param request + * the request to extract the entities from + * @return the pair of values representing the proxied entities and issuers + * @throws MultipleHeaderValuesException + * if multiple values were found in the header + * @throws MissingHeaderException + * if proxied entities were provided, but proxied issuers were not. + */ + private Pair getProxiedEntitiesAndIssuers(HttpServerRequest request) throws MultipleHeaderValuesException, MissingHeaderException { + String proxiedEntities = getSingularHeaderValue(request, PROXIED_ENTITIES_HEADER); + String proxiedIssuers = getSingularHeaderValue(request, PROXIED_ISSUERS_HEADER); + + // If proxied entities are specified, but proxied issuers are not, then fail authentication immediately. + if (proxiedEntities != null && proxiedIssuers == null) { + throw new MissingHeaderException(PROXIED_ENTITIES_HEADER + " provided, but missing " + PROXIED_ISSUERS_HEADER); + } + + return Pair.of(proxiedEntities, proxiedIssuers); + } + + /** + * Attempt to find and return evidence based on a JWT token from the header {@value AUTHORIZATION_HEADER} in the request. + * + * @param request + * the request to examine + * @return a {@link JWTEvidence} if a JWT token was found, or null otherwise + * @throws MultipleHeaderValuesException + * if multiple token values were found in the header + */ + private JWTEvidence getJwtEvidence(HttpServerRequest request) throws MultipleHeaderValuesException { + String authorizationHeader = getSingularHeaderValue(request, AUTHORIZATION_HEADER); + if (authorizationHeader != null && authorizationHeader.startsWith(JWT_TOKEN_PREFIX)) { + String jwtToken = authorizationHeader.substring(JWT_TOKEN_PREFIX_LEN); + return EvidenceFactory.getDefault().createJwtEvidence(jwtToken); + } else { + return null; + } + } + + /** + * Attempt to find and return evidence based off certificates from the SSL session. + * + * @param request + * the request to examine + * @param proxiedSubjects + * the proxied subjects (if any) + * @param proxiedIssuers + * the proxied issuers (if any) + * @return a {@link X509CertificateEvidence} if a certificate was found in the SSL session, or null otherwise + */ + private X509CertificateEvidence getX509Evidence(HttpServerRequest request, String proxiedSubjects, String proxiedIssuers) { + if (request.getSSLSession() != null) { + Certificate[] peerCertificates = request.getPeerCertificates(); + if (peerCertificates != null) { + // If the request has any peer certificates, grab the first one. + X509Certificate[] x509Certificates = X500.asX509CertificateArray(peerCertificates); + X509Certificate certificate = x509Certificates[0]; + return EvidenceFactory.getDefault().createX509CertificateEvidence(certificate, proxiedSubjects, proxiedIssuers); + } + } + return null; + } + + /** + * Attempt to find and return evidence based of trusted headers. + * + * @param request + * the request to examine + * @param proxiedSubjects + * the proxied subjects (if any) + * @param proxiedIssuers + * the proxied issuers (if any) + * @return a {@link TrustedHeaderEvidence} if trusted subject and issuer DNs were found, or null otherwise + * @throws MultipleHeaderValuesException + * if multiple DNs were found in the headers + * @throws MissingHeaderException + * if either a subject DN or issuer DN was provided, but its counterpart was not + */ + private TrustedHeaderEvidence getTrustedHeadersEvidence(HttpServerRequest request, String proxiedSubjects, String proxiedIssuers) + throws MultipleHeaderValuesException, MissingHeaderException { + String subjectDn = getSingularHeaderValue(request, config.getTrustedSubjectDnHeader()); + String issuerDn = getSingularHeaderValue(request, config.getTrustedIssuerDnHeader()); + + // If no DN headers were supplied, we cannot create trusted header evidence. + if (subjectDn == null && issuerDn == null) { + return null; + } + + // Require both a subject DN and issuer DN to be specified. + if (subjectDn == null || issuerDn == null) { + throw new MissingHeaderException( + "Missing trusted subject DN (" + subjectDn + ") or issuer DN (" + issuerDn + ") for trusted header authentication"); + } + + return EvidenceFactory.getDefault().createTrustedHeadersEvidence(subjectDn, issuerDn, proxiedSubjects, proxiedIssuers); + } + + /** + * Returns the value if one was provided for the given header name in the given http request. If no value was provided, null will be returned. If multiple + * values were provided, an exception will be thrown. + * + * @param httpServerRequest + * the http request + * @param headerName + * the header name + * @return the value, possibly null + * @throws MultipleHeaderValuesException + * if multiple values were provided for the header + */ + private String getSingularHeaderValue(HttpServerRequest httpServerRequest, String headerName) throws MultipleHeaderValuesException { + List values = httpServerRequest.getRequestHeaderValues(headerName); + if (values != null && !values.isEmpty()) { + if (values.size() > 1) { + throw new MultipleHeaderValuesException(headerName + " may not be specified multiple times"); + } + return values.get(0); + } else { + return null; + } + } + + /** + * Create an identity cache that can be associated with the SESSION scope of the request. + * + * @param request + * the request + * @return the identity cache + */ + private IdentityCache createIdentityCache(HttpServerRequest request) { + return new IdentityCache() { + + /** + * Attempt to attach the given identity to the request's SESSION scope. + * + * @param identity + * the identity to cache (not {@code null}) + */ + @Override + public void put(SecurityIdentity identity) { + // Attempt to get an attachable SESSION scope for the request, creating it if need be. If we cannot obtain the scope, return early. + HttpScope scope = getAttachableSessionScope(request, true); + if (scope == null || !scope.exists()) { + return; + } + + // If we are associating an identity with the session for the first time, change the ID of the session unless otherwise disabled. + if (config.isSessionIdChangeEnabled() && scope.getAttachment(CACHED_IDENTITY_KEY) == null) { + scope.changeID(); + } + + // Wrap the identity in a CachedIdentity and attach it to the scope. + CachedIdentity cachedIdentity = new CachedIdentity(getMechanismName(), false, identity); + scope.setAttachment(CACHED_IDENTITY_KEY, cachedIdentity); + } + + /** + * Return the cached identity attached to the SESSION scope of the request. + * + * @return the cached identity, or null if no identity is cached + */ + @Override + public CachedIdentity get() { + // If we cannot obtain a scope that could have an attached identity, return early. + HttpScope scope = getAttachableSessionScope(request, false); + if (scope == null || !scope.exists()) { + return null; + } + + return (CachedIdentity) scope.getAttachment(CACHED_IDENTITY_KEY); + } + + /** + * Delete the cached identity (if any) attached to the SESSION scope of the request. + * + * @return the identity that was removed, possibly null + */ + @Override + public CachedIdentity remove() { + // If we cannot obtain a scope that could have an attached identity, return early. + HttpScope scope = getAttachableSessionScope(request, false); + if (scope == null || !scope.exists()) { + return null; + } + + CachedIdentity identity = (CachedIdentity) scope.getAttachment(CACHED_IDENTITY_KEY); + scope.setAttachment(CACHED_IDENTITY_KEY, null); + return identity; + } + }; + } + + /** + * Return the SESSION scope for the request if it exists and supports attachments. + * + * @param request + * the request + * @param createSession + * whether to create the session if it does not exist + * @return the scope, or null if no attachable SESSION scope could be obtained + */ + private HttpScope getAttachableSessionScope(HttpServerRequest request, boolean createSession) { + HttpScope scope = request.getScope(Scope.SESSION); + + // If no scope could be obtained, or it doesn't support attachments, return null. + if (scope == null || !scope.supportsAttachments()) { + return null; + } + + // Create the scope if indicated. + if (!scope.exists() && createSession) { + scope.create(); + } + + return scope; + } + + /** + * Configuration class that will handle parsing configuration options for a {@link DatawaveHttpAuthenticationMechanism} instance. + */ + public static class Config { + + /** + * The header that will be used to pass in the trusted subject DN when using trusted header authentication. + */ + public static final String OPTION_TRUSTED_SUBJECT_DN_HEADER = "trustedSubjectDnHeader"; + + /** + * The header that will be used to pass in the trusted issuer DN when using trusted header authentication. + */ + public static final String OPTION_TRUSTED_ISSUER_DN_HEADER = "trustedIssuerDnHeader"; + + /** + * Whether to enable the ability to restore identities for the HTTP session. + */ + public static final String OPTION_ENABLE_RESTORE_IDENTITY = "enableRestoreIdentity"; + + /** + * Whether to enable the ability to change the session ID after an identity is first established. + */ + public static final String OPTION_ENABLE_SESSION_ID_CHANGE = "enableSessionIdChange"; + + private final String trustedSubjectDnHeader; + private final String trustedIssuerDnHeader; + private final boolean identityRestorationEnabled; + private final boolean sessionIdChangeEnabled; + + public static Config fromMap(Map properties) { + String trustedSubjectDnHeader = getValue((String) properties.get(OPTION_TRUSTED_SUBJECT_DN_HEADER), TRUSTED_SUBJECT_DN_HEADER_SYSTEM_PROPERTY, + DEFAULT_TRUSTED_SUBJECT_DN_HEADER); + String trustedIssuerDnHeader = getValue((String) properties.get(OPTION_TRUSTED_ISSUER_DN_HEADER), TRUSTED_ISSUER_DN_HEADER_SYSTEM_PROPERTY, + DEFAULT_TRUSTED_ISSUER_DN_HEADER); + + boolean identityRestorationEnabled = ConfigUtils.getBoolean((String) properties.get(OPTION_ENABLE_RESTORE_IDENTITY), true); + boolean sessionIdChangeEnabled = ConfigUtils.getBoolean((String) properties.get(OPTION_ENABLE_SESSION_ID_CHANGE), true); + + return new Config(trustedSubjectDnHeader, trustedIssuerDnHeader, identityRestorationEnabled, sessionIdChangeEnabled); + } + + private static String getValue(String mapValue, String systemProperty, String defaultValue) { + String value = ConfigUtils.getString(mapValue, null); + if (value == null) { + value = ConfigUtils.getString(System.getProperty(systemProperty), defaultValue); + } + return value; + } + + public Config(String trustedSubjectDnHeader, String trustedIssuerDnHeader, boolean identityRestorationEnabled, boolean sessionIdChangeEnabled) { + this.trustedSubjectDnHeader = trustedSubjectDnHeader; + this.trustedIssuerDnHeader = trustedIssuerDnHeader; + this.identityRestorationEnabled = identityRestorationEnabled; + this.sessionIdChangeEnabled = sessionIdChangeEnabled; + } + + public String getTrustedSubjectDnHeader() { + return trustedSubjectDnHeader; + } + + public String getTrustedIssuerDnHeader() { + return trustedIssuerDnHeader; + } + + public boolean isIdentityRestorationEnabled() { + return identityRestorationEnabled; + } + + public boolean isSessionIdChangeEnabled() { + return sessionIdChangeEnabled; + } + + @Override + public String toString() { + return new StringJoiner(", ", Config.class.getSimpleName() + "[", "]").add("trustedSubjectDnHeader='" + trustedSubjectDnHeader + "'") + .add("trustedIssuerDnHeader='" + trustedIssuerDnHeader + "'").add("identityRestorationEnabled=" + identityRestorationEnabled) + .add("sessionIdChangeEnabled=" + sessionIdChangeEnabled).toString(); + } + } +} diff --git a/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/auth/DatawaveHttpAuthenticationMechanismFactory.java b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/auth/DatawaveHttpAuthenticationMechanismFactory.java new file mode 100644 index 00000000000..3f35875ebcf --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/auth/DatawaveHttpAuthenticationMechanismFactory.java @@ -0,0 +1,54 @@ +package datawave.security.auth; + +import java.util.Map; +import java.util.Objects; + +import javax.security.auth.callback.CallbackHandler; + +import org.wildfly.security.http.HttpServerAuthenticationMechanism; +import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; + +/** + * A {@link HttpServerAuthenticationMechanismFactory} implementation to create instances of {@link DatawaveHttpAuthenticationMechanism} for the + * {@value #DATAWAVE_AUTH_NAME} mechanism. + */ +public class DatawaveHttpAuthenticationMechanismFactory implements HttpServerAuthenticationMechanismFactory { + + public static final String DATAWAVE_AUTH_NAME = "DATAWAVE-AUTH"; + + /** + * Returns the name of the HTTP authentication mechanism that can be supplied by this factory, specifically {@value #DATAWAVE_AUTH_NAME}. + * + * @param properties + * the properties to pass configuration to the mechanisms that may be evaluated for mechanism availability. + * @return a single-element array containing the string {@value #DATAWAVE_AUTH_NAME}. + */ + @Override + public String[] getMechanismNames(Map properties) { + return new String[] {DATAWAVE_AUTH_NAME}; + } + + /** + * Returns an instance of {@link DatawaveHttpAuthenticationMechanism} if the mechanism name is {@value #DATAWAVE_AUTH_NAME}, otherwise returns null. + * + * @param mechanismName + * the mechanism name + * @param properties + * the set of properties to select and configure the mechanism that may be evaluated for mechanism availability + * @param callbackHandler + * the {@link CallbackHandler} for use by the mechanism during authentication + * @return the configured {@link DatawaveHttpAuthenticationMechanism} or null if no mechanism could be resolved for the given mechanism name + */ + @Override + public HttpServerAuthenticationMechanism createAuthenticationMechanism(String mechanismName, Map properties, CallbackHandler callbackHandler) { + Objects.requireNonNull(mechanismName, "mechanismName must not be null"); + Objects.requireNonNull(properties, "properties must not be null"); + Objects.requireNonNull(callbackHandler, "callbackHandler must not be null"); + + if (DATAWAVE_AUTH_NAME.equals(mechanismName)) { + return new DatawaveHttpAuthenticationMechanism(properties, callbackHandler); + } else { + return null; + } + } +} diff --git a/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/auth/MissingHeaderException.java b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/auth/MissingHeaderException.java new file mode 100644 index 00000000000..0a5fb67b3dc --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/auth/MissingHeaderException.java @@ -0,0 +1,13 @@ +package datawave.security.auth; + +/** + * An exception that indicates a required header is missing from a request. + */ +public final class MissingHeaderException extends Exception { + + private static final long serialVersionUID = 1L; + + public MissingHeaderException(String message) { + super(message); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/auth/MultipleHeaderValuesException.java b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/auth/MultipleHeaderValuesException.java new file mode 100644 index 00000000000..cacb5ecebcc --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/auth/MultipleHeaderValuesException.java @@ -0,0 +1,13 @@ +package datawave.security.auth; + +/** + * An exception that indicates a header is present in a request with multiple values when the header is expected to have a single value. + */ +public final class MultipleHeaderValuesException extends Exception { + + private static final long serialVersionUID = 1L; + + public MultipleHeaderValuesException(String message) { + super(message); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/cache/DatawaveRealmIdentityCache.java b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/cache/DatawaveRealmIdentityCache.java new file mode 100644 index 00000000000..b7ea8ae6bbf --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/cache/DatawaveRealmIdentityCache.java @@ -0,0 +1,291 @@ +package datawave.security.cache; + +import java.security.Principal; +import java.time.Duration; +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.wildfly.security.auth.server.RealmIdentity; +import org.wildfly.security.cache.RealmIdentityCache; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalCause; +import com.github.benmanes.caffeine.cache.RemovalListener; +import com.google.common.base.Preconditions; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; + +import datawave.security.authorization.DatawavePrincipal; +import datawave.security.authorization.DatawaveUser; + +/** + * A {@link RealmIdentityCache} implementation that can be treated as an {@link ElytronCache}. This cache supports caching {@link RealmIdentity} instances + * associated with security realm and security domain principals, and is intended for use with a {@link org.wildfly.security.auth.server.SecurityRealm} to + * support caching of identities without wrapping the realm in a caching realm. This cache requires the domain principals to be instances of + * {@link DatawavePrincipal}. + */ +public class DatawaveRealmIdentityCache implements RealmIdentityCache, ElytronCache { + + private static final Logger log = LoggerFactory.getLogger(DatawaveRealmIdentityCache.class); + + // Create the lock with fairness to ensure write operations do not get perpetually blocked by read operations. + private final ReadWriteLock lock = new ReentrantReadWriteLock(true); + + /** + * Holds mappings for domain principals to realm identities. + */ + private final Cache domainPrincipalsToRealmIdentities; + + /** + * Holds mappings of realm identity principals to domain principals. + */ + private final Multimap realmPrincipalsToDomainPrincipals; + + /** + * Return a new {@link DatawaveRealmIdentityCache}. + * + * @param maxSize + * the maximum number of entries to keep in the cache, unlimited if given a negative value + * @param ttl + * the time in milliseconds that an entry can stay in the cache, unlimited if given a negative value + */ + public DatawaveRealmIdentityCache(long maxSize, long ttl) { + Caffeine cacheBuilder = Caffeine.newBuilder(); + if (maxSize > -1) { + cacheBuilder.maximumSize(maxSize); + } + if (ttl > -1) { + cacheBuilder.expireAfterWrite(Duration.ofMillis(ttl)); + } + + // Add an eviction listener to the cache that will remove corresponding entries from the realm principal cache if an entry in the domain principal + // cache is removed due to expiration or size. + cacheBuilder.evictionListener(new RemovalListener() { + @Override + public void onRemoval(@Nullable Principal principal, @Nullable RealmIdentity realmIdentity, RemovalCause removalCause) { + if (removalCause == RemovalCause.EXPIRED || removalCause == RemovalCause.SIZE) { + if (realmIdentity != null) { + realmPrincipalsToDomainPrincipals.removeAll(realmIdentity.getRealmIdentityPrincipal()); + } + } + } + }); + + domainPrincipalsToRealmIdentities = cacheBuilder.build(); + realmPrincipalsToDomainPrincipals = HashMultimap.create(); + } + + /** + * Associate the given realm identity with the given principal. The principal must be an instance of {@link DatawavePrincipal}. + * + * @param principal + * the {@link Principal} that references the realm identity being cached + * @param realmIdentity + * the {@link RealmIdentity} instance + */ + @Override + public void put(Principal principal, RealmIdentity realmIdentity) { + Preconditions.checkArgument(principal instanceof DatawavePrincipal, "principal must be of type " + DatawavePrincipal.class.getName()); + + try { + if (obtainedWriteLock()) { + domainPrincipalsToRealmIdentities.put((DatawavePrincipal) principal, realmIdentity); + realmPrincipalsToDomainPrincipals.put(realmIdentity.getRealmIdentityPrincipal(), principal); + } + } finally { + lock.writeLock().unlock(); + } + } + + /** + * Return the realm identity for the given principal. The principal may a realm derived from a realm identity, or a security domain. + * + * @param principal + * the {@link Principal} that references a previously cached realm identity + * @return the identity if present, or null otherwise + */ + @Override + public RealmIdentity get(Principal principal) { + if (obtainedReadLock()) { + try { + // Attempt to fetch an identity directly from the realm identity cache. This will succeed if the principal is a domain principal. + RealmIdentity identity = domainPrincipalsToRealmIdentities.getIfPresent(principal); + if (identity != null) { + return identity; + } + + // If no identity was found, the principal may be a realm identity principal. If any domain principals are stored for the realm identity + // principal, return the realm identity associated with the first available domain principal. + Collection domainPrincipals = realmPrincipalsToDomainPrincipals.get(principal); + if (!domainPrincipals.isEmpty()) { + return domainPrincipalsToRealmIdentities.getIfPresent(domainPrincipals.iterator().next()); + } else { + return null; + } + } finally { + lock.readLock().unlock(); + } + } + return null; + } + + /** + * Remove any realm identities associated with the given principal. + * + * @param principal + * the {@link Principal} that references a previously cached realm identity + */ + @Override + public void remove(Principal principal) { + if (obtainedWriteLock()) { + try { + RealmIdentity identity = domainPrincipalsToRealmIdentities.getIfPresent(principal); + // If the identity is not null, the principal is a domain principal. Remove it from the cache and remove any mappings for the associated realm + // identity principal. + if (identity != null) { + domainPrincipalsToRealmIdentities.invalidate(principal); + Principal realmIdentityPrincipal = identity.getRealmIdentityPrincipal(); + realmPrincipalsToDomainPrincipals.get(realmIdentityPrincipal).forEach(domainPrincipalsToRealmIdentities::invalidate); + realmPrincipalsToDomainPrincipals.removeAll(realmIdentityPrincipal); + } else { + // Otherwise, the principal may be a realm identity principal. Remove all domain principal mappings for it, and remove any mappings for + // those domain principals from the cache. + if (realmPrincipalsToDomainPrincipals.containsKey(principal)) { + realmPrincipalsToDomainPrincipals.get(principal).forEach(domainPrincipalsToRealmIdentities::invalidate); + realmPrincipalsToDomainPrincipals.removeAll(principal); + } + } + } finally { + lock.writeLock().unlock(); + } + } + } + + @Override + public void clear() { + log.trace("Clearing the cache"); + if (obtainedWriteLock()) { + try { + // Clear the cache. + domainPrincipalsToRealmIdentities.invalidateAll(); + domainPrincipalsToRealmIdentities.cleanUp(); + // Clear the domain principals. + realmPrincipalsToDomainPrincipals.clear(); + } finally { + lock.writeLock().unlock(); + } + } + } + + @Override + public Set getUsers() { + if (obtainedReadLock()) { + try { + // @formatter:off + return domainPrincipalsToRealmIdentities.asMap().keySet().stream() + .map(DatawavePrincipal::getProxiedUsers) + .flatMap(Collection::stream).collect(Collectors.toSet()); + // @formatter:on + } finally { + lock.readLock().unlock(); + } + } + return Set.of(); + } + + @Override + public Set getUsersWhereNameContains(String substring) { + if (obtainedReadLock()) { + try { + // @formatter:off + return domainPrincipalsToRealmIdentities.asMap().keySet().stream() + .map(DatawavePrincipal::getProxiedUsers) + .flatMap(Collection::stream) + .filter(user -> user.getName().contains(substring)) + .collect(Collectors.toSet()); + // @formatter:on + } finally { + lock.readLock().unlock(); + } + } + return Set.of(); + } + + @Override + public DatawaveUser getUserWithName(String name) { + if (obtainedReadLock()) { + try { + // @formatter:off + return domainPrincipalsToRealmIdentities.asMap().keySet().stream() + .map(DatawavePrincipal::getProxiedUsers) + .flatMap(Collection::stream) + .filter(user -> user.getName().equals(name)) + .findFirst() + .orElse(null); + // @formatter:on + } finally { + lock.readLock().unlock(); + } + } + return null; + } + + @Override + public void evictUsersWithName(String name) { + if (log.isTraceEnabled()) { + log.trace("Evicting users with name {}", name); + } + + if (obtainedWriteLock()) { + try { + int totalEvictions = 0; + for (DatawavePrincipal principal : domainPrincipalsToRealmIdentities.asMap().keySet()) { + if (principal.getProxiedUsers().stream().anyMatch(user -> name.equals(user.getName()))) { + totalEvictions++; + remove(principal); + } + } + if (log.isTraceEnabled()) { + log.trace("Removed {} entries with user {}", totalEvictions, name); + } + } finally { + lock.writeLock().unlock(); + } + } + } + + /** + * Attempt to obtain a read lock. + * + * @return true if the lock was obtained or false otherwise + */ + private boolean obtainedReadLock() { + try { + lock.readLock().lockInterruptibly(); + return true; + } catch (InterruptedException e) { + return false; + } + } + + /** + * Attempt to obtain a write lock. + * + * @return true if the lock was obtained or false otherwise + */ + private boolean obtainedWriteLock() { + try { + lock.writeLock().lockInterruptibly(); + return true; + } catch (InterruptedException e) { + return false; + } + } +} diff --git a/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/cache/DatawaveUserCache.java b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/cache/DatawaveUserCache.java new file mode 100644 index 00000000000..cfa173bd009 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/cache/DatawaveUserCache.java @@ -0,0 +1,127 @@ +package datawave.security.cache; + +import java.time.Duration; +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import datawave.security.authorization.DatawaveUser; + +/** + * Represents a cache of mappings of keys to collections of {@link DatawaveUser} instances. + */ +public class DatawaveUserCache implements ElytronCache { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + private final Cache> cache; + + /** + * Return a new {@link DatawaveUserCache} with the given maximum size and time to live for entries in the cache. + * + * @param maxSize + * the maximum number of entries to keep in the cache. No maximum will be set if given a negative value + * @param ttl + * the time in milliseconds that an entry can stay in the cache, unlimited if given a negative value + */ + public DatawaveUserCache(long maxSize, long ttl) { + Caffeine cacheBuilder = Caffeine.newBuilder(); + if (maxSize > -1) { + cacheBuilder.maximumSize(maxSize); + } + if (ttl > -1) { + cacheBuilder.expireAfterWrite(Duration.ofMillis(ttl)); + } + this.cache = cacheBuilder.build(); + } + + /** + * Associate the collection of users with the given key in the cache. + * + * @param key + * the key + * @param users + * the users + */ + public void put(String key, Collection users) { + cache.put(key, users); + } + + /** + * Return the collection of users associated with the key, or null if no such mapping exists. + * + * @param key + * the key + * @return the user collection, possibly null + */ + public Collection get(String key) { + return cache.getIfPresent(key); + } + + @Override + public Set getUsers() { + // @formatter:off + return cache.asMap().keySet().stream() + .map(this::get) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + // @formatter:on + } + + @Override + public Set getUsersWhereNameContains(String substring) { + // @formatter:off + return cache.asMap().keySet().stream() + .map(this::get) + .flatMap(Collection::stream) + .filter(user -> user.getName().contains(substring)) + .collect(Collectors.toSet()); + // @formatter:on + } + + @Override + public DatawaveUser getUserWithName(String name) { + // @formatter:off + return cache.asMap().keySet().stream() + .map(this::get) + .flatMap(Collection::stream) + .filter(user -> user.getName().equals(name)) + .findFirst() + .orElse(null); + // @formatter:off + } + + @Override + public void evictUsersWithName(String name) { + if(log.isTraceEnabled()) { + log.trace("Evicting users with name {}", name); + } + + int totalEvictions = 0; + ConcurrentMap> map = cache.asMap(); + for(String key : map.keySet()) { + if(map.get(key).stream().anyMatch(user -> user.getName().equals(name))) { + totalEvictions++; + map.remove(key); + } + } + + if(log.isTraceEnabled()) { + log.trace("Removed {} entries with user {}", totalEvictions, name); + } + } + + @Override + public void clear() { + log.trace("Clearing the cache"); + cache.invalidateAll(); + cache.cleanUp(); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/evidence/X509EvidenceValidator.java b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/evidence/X509EvidenceValidator.java new file mode 100644 index 00000000000..878118e49fb --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/evidence/X509EvidenceValidator.java @@ -0,0 +1,35 @@ +package datawave.security.evidence; + +import java.security.KeyStore; +import java.security.cert.X509Certificate; + +import datawave.security.cert.X509CertificateVerifier; + +/** + * A validator for {@link X509CertificateEvidence} that will validate the certificate of the evidence using a configured certificate verifier. + */ +public class X509EvidenceValidator { + + private final X509CertificateVerifier certVerifier; + private final KeyStore keyStore; + private final KeyStore trustStore; + + public X509EvidenceValidator(X509CertificateVerifier certVerifier, KeyStore keyStore, KeyStore trustStore) { + this.certVerifier = certVerifier; + this.keyStore = keyStore; + this.trustStore = trustStore; + } + + /** + * Return whether the given evidence has a valid certificate. + * + * @param evidence + * the evidence + * @return true if the evidence certificate is valid, or false otherwise + */ + public boolean validate(X509CertificateEvidence evidence) { + X509Certificate certificate = evidence.getCertificate(); + String alias = certificate.getIssuerX500Principal().getName(); + return certVerifier.verify(certificate, alias, keyStore, trustStore); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/AttributeConstants.java b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/AttributeConstants.java new file mode 100644 index 00000000000..c679b560d63 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/AttributeConstants.java @@ -0,0 +1,36 @@ +package datawave.security.realm; + +/** + * Constants that are frequently used when tracking roles in an {@link org.wildfly.security.authz.Attributes} associated with a security identity. + */ +public final class AttributeConstants { + + /** + * The identity's username. + */ + public static final String ATTRIBUTE_USERNAME = "USERNAME"; + + /** + * The primary user's roles. + */ + public static final String ATTRIBUTE_PRIMARY_USER_ROLES = "PRIMARY_USER_ROLES"; + + /** + * The attribute keys that will map to roles of individual proxied users. + */ + public static final String ATTRIBUTE_PROXIED_USER_KEYS = "PROXIED_USER_KEYS"; + + /** + * The roles of the terminal server (if any) present in the proxied user. + */ + public static final String ATTRIBUTE_TERMINAL_SERVER_ROLES = "TERMINAL_SERVER_ROLES"; + + public static final String ROLE_AUTHORIZED_USER = "AuthorizedUser"; + public static final String ROLE_AUTHORIZED_SERVER = "AuthorizedServer"; + public static final String ROLE_AUTHORIZED_QUERY_SERVER = "AuthorizedQueryServer"; + public static final String ROLE_AUTHORIZED_PROXIED_SERVER = "AuthorizedProxiedServer"; + + private AttributeConstants() { + throw new UnsupportedOperationException(); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/DatawaveEvidenceDecoder.java b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/DatawaveEvidenceDecoder.java new file mode 100644 index 00000000000..bfea2be49f4 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/DatawaveEvidenceDecoder.java @@ -0,0 +1,300 @@ +package datawave.security.realm; + +import java.security.Principal; +import java.util.Collection; +import java.util.Map; +import java.util.StringJoiner; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.wildfly.security.auth.server.EvidenceDecoder; +import org.wildfly.security.evidence.Evidence; + +import datawave.security.authorization.DatawavePrincipal; +import datawave.security.authorization.DatawaveUser; +import datawave.security.authorization.SubjectIssuerDNPair; +import datawave.security.cache.DatawaveUserCache; +import datawave.security.cache.ElytronCacheManager; +import datawave.security.evidence.DatawaveEvidence; +import datawave.security.evidence.JWTEvidence; +import datawave.security.evidence.TrustedHeaderEvidence; +import datawave.security.evidence.X509CertificateEvidence; +import datawave.security.system.SecurityEJBProvider; +import datawave.security.utils.ConfigUtils; + +/** + * A {@link EvidenceDecoder} that will decode instances of {@link DatawaveEvidence} to a corresponding {@link DatawavePrincipal}. This evidence decoder has + * support for caching, and can be configured to enable/disable support for JWT authentication and trusted header authentication. + */ +public class DatawaveEvidenceDecoder implements EvidenceDecoder { + + private static final Logger log = LoggerFactory.getLogger(DatawaveEvidenceDecoder.class); + + /** + * The configuration for this {@link DatawaveEvidenceDecoder}. + */ + private Config config = Config.fromMap(Map.of()); + + /** + * The provider used to obtain {@link DatawaveUser} instances from a remote service. + */ + private DatawaveUserProvider datawaveUserProvider; + + /** + * The provider used to obtain EJB instances. + */ + private SecurityEJBProvider securityEJBProvider; + + /** + * A cache of {@link DatawaveUser}. + */ + private DatawaveUserCache cache; + + /** + * Whether initialization is complete. + */ + private boolean initializationComplete = false; + + /** + * Initializes this role decoder with the given configuration options. This method is invoked once by the Wildfly Elytron subsystem when the decoder is + * first created to provide configuration parameters. These parameters are typically defined in the Wildfly configuration files, such as jboss .cli files or + * in the standalone.xml. + * + * @param configMap + * the configuration + */ + @SuppressWarnings("unused") + public void initialize(Map configMap) { + this.config = Config.fromMap(configMap); + this.initializationComplete = false; + } + + /** + * Perform any initialization that could not be completed until EJBs are available for use. + */ + private void completeInitialization() { + if (!initializationComplete) { + // Log the configuration. + if (log.isDebugEnabled()) { + log.debug("Initialized with config {}", config); + } + + // Add the user cache to the elytron cache manager so that the CredentialsCacheBean can invoke operations on it as needed. + this.cache = new DatawaveUserCache(this.config.getMaxCacheEntries(), this.config.getMaxCacheAge()); + ElytronCacheManager cacheManager = getSecurityEJBProvider().getElytronCacheManager(); + if (cacheManager != null) { + log.debug("Adding user cache to the elytron cache manager"); + cacheManager.addCache(this.cache); + } else { + log.debug("No elytron cache manager configured."); + } + + initializationComplete = true; + } + } + + /** + * Return a {@link DatawavePrincipal} decoded from the given evidence. + * + * @param evidence + * the evidence to decode + * @return a {@link DatawavePrincipal}, or null if one could not be decoded + */ + @Override + public Principal getPrincipal(Evidence evidence) { + completeInitialization(); + + if (evidence instanceof DatawaveEvidence) { + // Check if JWT is enabled if this is a JWT evidence. + if (!config.isJwtEnabled() && evidence instanceof JWTEvidence) { + log.trace("JWT evidence provided, but JWT authentication is disabled"); + return null; + } + + // Check if trusted headers is enabled if this is a trusted header evidence. + if (!config.isTrustedHeadersEnabled() && evidence instanceof TrustedHeaderEvidence) { + log.trace("Trusted header evidence provided, but trusted header authentication is disabled"); + return null; + } + + try { + // Check if we already have users cached for the evidence. + String cacheKey = getCacheKey(evidence); + Collection users = cache.get(cacheKey); + + // Otherwise, fetch users from the user provider for the evidence and cache them. + if (users == null) { + log.trace("No users found in cache, fetching users from user provider"); + DatawaveUserProvider userProvider = getDatawaveUserProvider(); + users = userProvider.getUsers(evidence); + cache.put(cacheKey, users); + } else { + log.trace("Users loaded from cache"); + } + + DatawavePrincipal principal = new DatawavePrincipal(users); + if (log.isTraceEnabled()) { + log.trace("Decoded evidence {} into principal {}", evidence, principal); + } + return principal; + } catch (Exception e) { + log.error("Failed to decode principal", e); + throw new RuntimeException(e); + } + } else { + if (log.isWarnEnabled()) { + log.warn("Unable to decoded evidence {} which is not a {}", evidence, DatawaveEvidence.class.getName()); + } + } + + return null; + } + + /** + * Return a cache key representing the given evidence + * + * @param evidence + * the evidence + * @return the cache key + */ + private String getCacheKey(Evidence evidence) { + if (evidence instanceof JWTEvidence) { + return ((JWTEvidence) evidence).getToken(); + } else if (evidence instanceof TrustedHeaderEvidence) { + return getEntitiesAsString(((TrustedHeaderEvidence) evidence).getEntities()); + } else if (evidence instanceof X509CertificateEvidence) { + return getEntitiesAsString(((X509CertificateEvidence) evidence).getEntities()); + } + return null; + } + + /** + * Return the given list of {@link SubjectIssuerDNPair} as a string. + * + * @param entities + * the entities + * @return the entities as a string + */ + private String getEntitiesAsString(Collection entities) { + return entities.stream().map(SubjectIssuerDNPair::toString).collect(Collectors.joining()); + } + + /** + * Return the {@link DatawaveUserProvider} set for this {@link DatawaveEvidenceDecoder}. If no user provider has been set, the user provider will be set to + * the result of {@link DatawaveUserProvider#getInstance()} and returned. + * + * @return the user provider + * @throws Exception + * if an error occurs while fetching the default instance of {@link DatawaveUserProvider} + */ + DatawaveUserProvider getDatawaveUserProvider() throws Exception { + if (this.datawaveUserProvider == null) { + this.datawaveUserProvider = DatawaveUserProvider.getInstance(); + } + return this.datawaveUserProvider; + } + + /** + * Exposed to allow a {@link DatawaveUserProvider} to be set for testing purposes. In production, the {@link DatawaveUserProvider} returned by + * {@link DatawaveUserProvider#getInstance()} should be used. + * + * @param datawaveUserProvider + * the user provider + */ + void setDatawaveUserProvider(DatawaveUserProvider datawaveUserProvider) { + this.datawaveUserProvider = datawaveUserProvider; + } + + /** + * Return the {@link SecurityEJBProvider} set for this {@link DatawaveEvidenceDecoder}. If no provider has been set, the provider will be set to the result + * of {@link SecurityEJBUtils#getSecurityEJBProvider()} and returned. + * + * @return the provider + */ + private SecurityEJBProvider getSecurityEJBProvider() { + if (this.securityEJBProvider == null) { + this.securityEJBProvider = SecurityEJBUtils.getSecurityEJBProvider(); + } + return this.securityEJBProvider; + } + + /** + * Exposed to allow a {@link SecurityEJBProvider} to be set for testing purposes. In production, the {@link SecurityEJBProvider} returned by + * {@link SecurityEJBUtils#getSecurityEJBProvider()} should be used. + * + * @param securityEJBProvider + * the EJB provider + */ + public void setSecurityEJBProvider(SecurityEJBProvider securityEJBProvider) { + this.securityEJBProvider = securityEJBProvider; + } + + public static class Config { + + /** + * Whether JWT authentication is enabled. + */ + public static final String OPTION_JWT_ENABLED = "jwtEnabled"; + + /** + * Whether trusted header authentication is enabled. + */ + public static final String OPTION_TRUSTED_HEADERS_ENABLED = "trustedHeadersEnabled"; + + /** + * The maximum size allowed for the user cache. A negative value implies no limit. + */ + public static final String OPTION_MAX_CACHE_ENTRIES = "maxCacheEntries"; + + /** + * The maximum age (TTL) in milliseconds for entries in the user cache. A negative value implies no limit. + */ + public static final String OPTION_MAX_CACHE_AGE = "maxCacheAge"; + + public static Config fromMap(Map config) { + boolean jwtEnabled = ConfigUtils.getBoolean(config.get(OPTION_JWT_ENABLED), false); + boolean trustedHeadersEnabled = ConfigUtils.getBoolean(config.get(OPTION_TRUSTED_HEADERS_ENABLED), false); + long maxCacheEntries = ConfigUtils.getLong(config.get(OPTION_MAX_CACHE_ENTRIES), -1L); + long maxCacheAge = ConfigUtils.getLong(config.get(OPTION_MAX_CACHE_AGE), -1L); + + return new Config(jwtEnabled, trustedHeadersEnabled, maxCacheEntries, maxCacheAge); + } + + private final boolean jwtEnabled; + private final boolean trustedHeadersEnabled; + private final long maxCacheEntries; + private final long maxCacheAge; + + public Config(boolean jwtEnabled, boolean trustedHeadersEnabled, long maxCacheEntries, long maxCacheAge) { + this.jwtEnabled = jwtEnabled; + this.trustedHeadersEnabled = trustedHeadersEnabled; + this.maxCacheEntries = maxCacheEntries; + this.maxCacheAge = maxCacheAge; + } + + public boolean isJwtEnabled() { + return jwtEnabled; + } + + public boolean isTrustedHeadersEnabled() { + return trustedHeadersEnabled; + } + + public long getMaxCacheEntries() { + return maxCacheEntries; + } + + public long getMaxCacheAge() { + return maxCacheAge; + } + + @Override + public String toString() { + return new StringJoiner(", ", Config.class.getSimpleName() + "[", "]").add("jwtEnabled=" + jwtEnabled) + .add("trustedHeadersEnabled=" + trustedHeadersEnabled).add("maxCacheEntries=" + maxCacheEntries).add("maxCacheAge=" + maxCacheAge) + .toString(); + } + } + +} diff --git a/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/DatawaveRoleDecoder.java b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/DatawaveRoleDecoder.java new file mode 100644 index 00000000000..7fc20dd033b --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/DatawaveRoleDecoder.java @@ -0,0 +1,251 @@ +package datawave.security.realm; + +import static datawave.security.realm.AttributeConstants.ATTRIBUTE_PRIMARY_USER_ROLES; +import static datawave.security.realm.AttributeConstants.ATTRIBUTE_PROXIED_USER_KEYS; +import static datawave.security.realm.AttributeConstants.ATTRIBUTE_TERMINAL_SERVER_ROLES; +import static datawave.security.realm.AttributeConstants.ATTRIBUTE_USERNAME; +import static datawave.security.realm.AttributeConstants.ROLE_AUTHORIZED_PROXIED_SERVER; +import static datawave.security.realm.AttributeConstants.ROLE_AUTHORIZED_QUERY_SERVER; +import static datawave.security.realm.AttributeConstants.ROLE_AUTHORIZED_SERVER; +import static datawave.security.realm.AttributeConstants.ROLE_AUTHORIZED_USER; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.wildfly.security.authz.Attributes; +import org.wildfly.security.authz.AuthorizationIdentity; +import org.wildfly.security.authz.RoleDecoder; +import org.wildfly.security.authz.Roles; + +/** + * A {@link RoleDecoder} implementation that will decode roles from the attributes of an {@link AuthorizationIdentity}. The role decoder expects the following + * mappings in the attributes: + *

    + *
  1. {@value AttributeConstants#ATTRIBUTE_USERNAME}: The identity's username.
  2. + *
  3. {@value AttributeConstants#ATTRIBUTE_PRIMARY_USER_ROLES}: The primary user's roles.
  4. + *
  5. {@value AttributeConstants#ATTRIBUTE_TERMINAL_SERVER_ROLES}: The roles of the terminal server present in the proxied users, if any.
  6. + *
  7. {@value AttributeConstants#ATTRIBUTE_PROXIED_USER_KEYS}: The attribute keys that will map to the roles of individual proxied users.
  8. + *
+ * NOTE: In the case where an empty role set is returned, login will automatically fail when the security domain is configured with the default permissions + * mapper provided by Wildfly. + */ +public class DatawaveRoleDecoder implements RoleDecoder { + + private static final Logger log = LoggerFactory.getLogger(DatawaveRoleDecoder.class); + + // @formatter:off + public static final Set defaultRequiredRoles = Set.of( + ROLE_AUTHORIZED_USER, + ROLE_AUTHORIZED_SERVER, + ROLE_AUTHORIZED_QUERY_SERVER, + ROLE_AUTHORIZED_PROXIED_SERVER); + // @formatter:on + + // @formatter:off + public static final Set defaultTerminalServerRoles = Set.of( + ROLE_AUTHORIZED_SERVER, + ROLE_AUTHORIZED_QUERY_SERVER); + // @formatter:on + + /** + * The configuration for this {@link RoleDecoder}. This will be overridden if configuration properties are passed in from Wildfly via + * {@link #initialize(Map)}. + */ + private Config config = Config.fromMap(Map.of()); + + /** + * Initializes this role decoder with the given configuration options. This method is invoked by the Wildfly Elytron subsystem when the decoder is first + * created to provide configuration parameters. These parameters are typically defined in the Wildfly configuration files, such as jboss .cli files or in + * the standalone.xml. + * + * @param configMap + * the configuration + */ + @SuppressWarnings("unused") + public void initialize(Map configMap) { + this.config = Config.fromMap(configMap); + } + + /** + * Decode the role set for the given identity. The role set will consist of the values present in the attributes + * {@value AttributeConstants#ATTRIBUTE_PRIMARY_USER_ROLES}. The final role set can be affected if any of the following scenarios occur: + *
    + *
  • If identity does not have the attribute {@value AttributeConstants#ATTRIBUTE_PRIMARY_USER_ROLES}, an empty role set will be returned.
  • + *
  • If the role decoder has been configured with required terminal server roles, and the identity has terminal server roles where none of them match a + * required terminal server role, an empty role set will be returned.
  • + *
  • If the role decoder has been configured with an access-denied role, and any of the proxied users in the identity have the access-denied role, an + * empty role set will be returned.
  • + *
  • If the role decoder has been configured with required roles, and any of the proxied users in the identity do not have at least one of the required + * roles, the required roles will be removed from the final role set.
  • + *
+ * + * @param identity + * the authorization identity (not {@code null}) + * @return the final role set + */ + @Override + public Roles decodeRoles(AuthorizationIdentity identity) { + Attributes attributes = identity.getAttributes(); + if (attributes.containsKey(ATTRIBUTE_PRIMARY_USER_ROLES)) { + // If there was a terminal server present in the list of proxied users, and it does not have at least one of the required terminal server roles, + // return an empty role set so that login will fail. + if (attributes.containsKey(ATTRIBUTE_TERMINAL_SERVER_ROLES) && config.hasTerminalServerRoles()) { + if (Collections.disjoint(config.getTerminalServerRoles(), attributes.get(ATTRIBUTE_TERMINAL_SERVER_ROLES))) { + if (log.isWarnEnabled()) { + log.warn("User '{}' has terminal server without any required terminal server roles {}, returning empty role set", + attributes.get(ATTRIBUTE_USERNAME), config.getTerminalServerRoles()); + } + return Roles.NONE; + } + } + + // Create an initial role set from the primary user roles. + Set userRoles = new HashSet<>(attributes.get(ATTRIBUTE_PRIMARY_USER_ROLES)); + + // If required roles or an access-denied role was configured, check the proxied users. + boolean requiredRolesRemoved = false; + if (config.hasRequiredRoles() || config.hasAccessDeniedRole()) { + Set requiredRoles = config.getRequiredRoles(); + String accessDeniedRole = config.getAccessDeniedRole(); + for (String proxyUserKey : attributes.get(ATTRIBUTE_PROXIED_USER_KEYS)) { + Set roles = new HashSet<>(attributes.get(proxyUserKey)); + + // If a proxied user has the access-denied role, return an empty role set. + if (accessDeniedRole != null && roles.contains(accessDeniedRole)) { + if (log.isWarnEnabled()) { + log.warn("User '{}' has access denied role {}, returning empty role set", attributes.get(ATTRIBUTE_USERNAME), accessDeniedRole); + } + return Roles.NONE; + } + + // If the proxied user does not have any of the required roles, remove all required roles from the final role set. + if (!requiredRolesRemoved && !requiredRoles.isEmpty() && Collections.disjoint(requiredRoles, roles)) { + if (log.isWarnEnabled()) { + log.warn("User '{}' has proxied user without any required roles. Removing all required roles {} from final role set.", + attributes.get(ATTRIBUTE_USERNAME), requiredRoles); + } + userRoles.removeAll(requiredRoles); + + // If no access denied role was configured, we do not need to examine any other proxied users. + if (!config.hasAccessDeniedRole()) { + break; + } + requiredRolesRemoved = true; + } + } + } + + if (log.isTraceEnabled()) { + log.trace("Returning final role set for user {}: {}", attributes.get(ATTRIBUTE_USERNAME), userRoles); + } + + // Return the final role set for the user. + return userRoles.isEmpty() ? Roles.NONE : Roles.fromSet(userRoles); + } else { + if (log.isWarnEnabled()) { + log.warn("Unable to decode role without presence of attribute {}", ATTRIBUTE_PRIMARY_USER_ROLES); + } + return Roles.NONE; + } + } + + /** + * Configuration options for {@link DatawaveRoleDecoder}. + */ + public static class Config { + + /** + * A colon-delimited list of roles that all proxied users are required to have at least one of. + */ + public static final String OPTION_REQUIRED_ROLES = "requiredRoles"; + + /** + * A role that if present in any of the proxied user roles, will result in no roles being returned for the user. No roles for the user will result in a + * failed login. + */ + public static final String OPTION_ACCESS_DENIED_ROLE = "accessDeniedRole"; + + /** + * A colon-delimited list of roles that the terminal server present in the proxied users is required to have at least one of. + */ + public static final String OPTION_TERMINAL_SERVER_ROLES = "terminalServerRoles"; + + public static Config fromMap(Map map) { + // Parse the required roles. + String requiredProxiedRoleStr = map.get(OPTION_REQUIRED_ROLES); + Set requiredProxiedRoles; + if (requiredProxiedRoleStr == null) { + requiredProxiedRoles = defaultRequiredRoles; + } else { + // @formatter:off + List roles = Arrays.stream(requiredProxiedRoleStr.split(":")) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .collect(Collectors.toList()); + requiredProxiedRoles = roles.isEmpty() ? Set.of() : Set.copyOf(roles); + } + + // Parse the access denied role. + String accessDeniedRoleOption = map.get(OPTION_ACCESS_DENIED_ROLE); + String accessDeniedRole = null; + if (accessDeniedRoleOption != null && !accessDeniedRoleOption.isBlank()) { + accessDeniedRole = accessDeniedRoleOption.trim(); + } + + // Parse the terminal server roles. + String terminalServerRolesOption = map.get(OPTION_TERMINAL_SERVER_ROLES); + Set terminalServerRoles = defaultTerminalServerRoles; + if (terminalServerRolesOption != null) { + List roles = Arrays.stream(terminalServerRolesOption.split(":")) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .collect(Collectors.toList()); + terminalServerRoles = roles.isEmpty() ? Set.of() : Set.copyOf(roles); + } + + return new Config(requiredProxiedRoles, accessDeniedRole, terminalServerRoles); + } + + private final Set requiredRoles; + private final String accessDeniedRole; + private final Set terminalServerRoles; + + private Config(Set requiredRoles, String accessDeniedRole, Set terminalServerRoles) { + this.requiredRoles = Set.copyOf(requiredRoles); + this.accessDeniedRole = accessDeniedRole; + this.terminalServerRoles = Set.copyOf(terminalServerRoles); + } + + public Set getRequiredRoles() { + return requiredRoles; + } + + public boolean hasRequiredRoles() { + return !requiredRoles.isEmpty(); + } + + public String getAccessDeniedRole() { + return accessDeniedRole; + } + + public boolean hasAccessDeniedRole() { + return accessDeniedRole != null; + } + + public Set getTerminalServerRoles() { + return terminalServerRoles; + } + + private boolean hasTerminalServerRoles() { + return !terminalServerRoles.isEmpty(); + } + } +} diff --git a/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/DatawaveSecurityRealm.java b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/DatawaveSecurityRealm.java new file mode 100644 index 00000000000..b9722d5e885 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/DatawaveSecurityRealm.java @@ -0,0 +1,730 @@ +package datawave.security.realm; + +import static datawave.security.realm.AttributeConstants.ATTRIBUTE_PRIMARY_USER_ROLES; +import static datawave.security.realm.AttributeConstants.ATTRIBUTE_PROXIED_USER_KEYS; +import static datawave.security.realm.AttributeConstants.ATTRIBUTE_TERMINAL_SERVER_ROLES; +import static datawave.security.realm.AttributeConstants.ATTRIBUTE_USERNAME; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.Principal; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.StringJoiner; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.wildfly.security.auth.SupportLevel; +import org.wildfly.security.auth.principal.NamePrincipal; +import org.wildfly.security.auth.server.RealmIdentity; +import org.wildfly.security.auth.server.RealmUnavailableException; +import org.wildfly.security.auth.server.SecurityRealm; +import org.wildfly.security.authz.Attributes; +import org.wildfly.security.authz.AuthorizationIdentity; +import org.wildfly.security.authz.MapAttributes; +import org.wildfly.security.credential.Credential; +import org.wildfly.security.evidence.Evidence; + +import com.google.common.base.Preconditions; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; + +import datawave.security.authorization.DatawavePrincipal; +import datawave.security.authorization.DatawaveUser; +import datawave.security.cache.DatawaveRealmIdentityCache; +import datawave.security.cache.ElytronCacheManager; +import datawave.security.cert.DatawaveCertVerifier; +import datawave.security.cert.SSLStores; +import datawave.security.cert.X509CertificateVerifier; +import datawave.security.evidence.DatawaveEvidence; +import datawave.security.evidence.X509CertificateEvidence; +import datawave.security.evidence.X509EvidenceValidator; +import datawave.security.system.SecurityEJBProvider; +import datawave.security.utils.ConfigUtils; + +/** + * A {@link SecurityRealm} implementation that will handle the creation of {@link RealmIdentity} when authentication users against the Datawave security domain. + * This realm is responsible for converting a {@link DatawavePrincipal} to a {@link RealmIdentity} with an {@link Attributes} containing attributes with mapped + * roles that can later be used by the {@link DatawaveRoleDecoder} to map the roles for the final security identity. + */ +public class DatawaveSecurityRealm implements SecurityRealm { + + private static final Logger log = LoggerFactory.getLogger(DatawaveSecurityRealm.class); + + /** + * Base attribute name when mapping roles for individual proxied users in an {@link Attributes}. + */ + private static final String ATTRIBUTE_PROXY_USER_KEY_BASE = "PROXIED_USER_"; + + /** + * The original configuration map passed to {@link #initialize(Map)}. + */ + private Map configMap; + + /** + * The configuration properties. This is updated if {@link #initialize(Map)} is called. + */ + private Config config = Config.fromMap(Map.of()); + + /** + * Indicates whether {@link #completeInitialization()} was called. + */ + private boolean initializationComplete = false; + + /** + * The validator that, if configured, will validate the certificate of any {@link X509CertificateEvidence} instances passed to this realm for verification. + */ + private X509EvidenceValidator x509EvidenceValidator = null; + + /** + * The realm identity cache. This cache will hold mappings of principals to realm identities to improve performance, and removes the need to wrap this + * security realm in a Wildfly caching realm. + */ + private DatawaveRealmIdentityCache realmIdentityCache; + + /** + * A provider for EJBs not normally injectable within an Elytron context. + */ + private SecurityEJBProvider securityEJBProvider; + + /** + * The local user roles loaded from the properties file specified in the {@value Config#OPTION_ROLE_PROPERTIES} config option. + */ + private Multimap localUserRoles = ImmutableMultimap.of(); + + /** + * This method is invoked by the Wildfly Elytron subsystem when the realm is first created to provide configuration parameters. These parameters are + * typically defined in the Wildfly configuration files, such as jboss .cli files or in the standalone.xml. + *

+ * NOTE: Any configuration parameters passed into here will be parsed and stored, but any configuration steps requiring the presence of CDI beans will not + * take effect until the first time {@link #getRealmIdentity(Principal)} is called. + *

+ * + * @param config + * the configuration parameters + * @see Config#fromMap(Map) The list of supported configuration parameters + */ + @SuppressWarnings("unused") + public void initialize(Map config) { + // Parse the configuration properties. + this.configMap = config; + this.config = Config.fromMap(config); + this.initializationComplete = false; + + } + + /** + * Complete the initialization of this security realm. This initialization is expected to be completed the first time {@link #getRealmIdentity(Principal)} + * is called. Some of these initialization steps require access to EJBs that will not be available during the initial start up of Wildfly, so we must wait + * until the first authentication attempt. + * + * @throws RealmUnavailableException + * if an error occurs while completing initialization + */ + private void completeInitialization() throws RealmUnavailableException { + if (!initializationComplete) { + try { + logConfig(); + initCache(); + initX509EvidenceValidator(); + loadLocalUserRoles(); + this.initializationComplete = true; + } catch (Exception e) { + log.error("Failed to complete realm initialization", e); + throw new RealmUnavailableException("Failed to complete realm initialization", e); + } + } + } + + /** + * Log the configuration that was loaded when {@link #initialize(Map)} was invoked. Logging statements in that method are not captured by the logging + * framework, so we'll do it once here. + */ + private void logConfig() { + if (log.isDebugEnabled()) { + log.debug("configMap={}", this.configMap); + log.debug("config={}", this.config); + } + } + + /** + * Initialize the realm identity cache used by this realm. The cache will be added to the {@link ElytronCacheManager} returned by the security EJB provider + * so that we can invoke lookup and eviction methods on it in the credentials cache bean. + */ + private void initCache() { + this.realmIdentityCache = new DatawaveRealmIdentityCache(this.config.getMaxCacheEntries(), this.config.getMaxCacheAge()); + ElytronCacheManager elytronCacheManager = getSecurityEJBProvider().getElytronCacheManager(); + if (elytronCacheManager != null) { + elytronCacheManager.addCache(this.realmIdentityCache); + log.debug("Added realm idenitity cache to the cache manager"); + } else { + log.debug("No elytron cache manager configured"); + } + } + + /** + * Initialize a {@link X509EvidenceValidator} if required. This will handle validating the certificate of any {@link X509CertificateEvidence} passed to this + * realm for verification. + */ + private void initX509EvidenceValidator() { + // If a verifier class was configured, attempt to create an instance of it. + String certVerifierClass = config.getCertVerifierClass(); + if (certVerifierClass != null) { + X509CertificateVerifier certificateVerifier; + try { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + Class verifierClass = loader.loadClass(certVerifierClass); + certificateVerifier = (X509CertificateVerifier) verifierClass.getDeclaredConstructor().newInstance(); + + // If this is a DatawaveCertVerifier, additional configuration is required. + if (certificateVerifier instanceof DatawaveCertVerifier) { + ((DatawaveCertVerifier) certificateVerifier).setLogger(log); + String oscpLevel = config.getOscpLevel(); + if (oscpLevel != null && !oscpLevel.isBlank()) { + ((DatawaveCertVerifier) certificateVerifier).setOcspLevel(oscpLevel); + } + } + + SSLStores sslStores = getSecurityEJBProvider().getSSLStores(); + this.x509EvidenceValidator = new X509EvidenceValidator(certificateVerifier, sslStores.getKeyStore(), sslStores.getTrustStore()); + if (log.isDebugEnabled()) { + log.debug("Initialized X509 evidence validator with certificate validator {}", certificateVerifier.getClass().getName()); + } + } catch (Throwable e) { + log.error("Failed to create create X509 evidence validator", e); + throw new IllegalStateException("Failed to create X509 evidence validator", e); + } + } + } + + /** + * Return the {@link X509EvidenceValidator} configured for this {@link DatawaveSecurityRealm}. + * + * @return the validator + */ + X509EvidenceValidator getX509EvidenceValidator() { + return this.x509EvidenceValidator; + } + + /** + * Loads a map of usernames to local server roles from a properties file if configured. + */ + private void loadLocalUserRoles() { + // If no role properties path was specified, return early. + String rolePropertiesPath = config.getRolePropertiesPath(); + if (rolePropertiesPath == null) { + return; + } + + // Load properties from the file. + Properties properties = new Properties(); + try (InputStream fis = Files.newInputStream(Paths.get(rolePropertiesPath))) { + properties.load(fis); + } catch (IOException e) { + log.error("Failed to load local role properties file {}", rolePropertiesPath, e); + throw new IllegalStateException("Failed to load local role properties file " + rolePropertiesPath, e); + } + + // Extract the roles for each username. + Multimap localRoles = HashMultimap.create(); + for (String username : properties.stringPropertyNames()) { + // Ensure the username is lowercase to facilitate case-insensitive matching. + username = username.toLowerCase(); + String rolesStr = properties.getProperty(username); + // Roles are expected to be comma-delimited. + if (rolesStr != null && !rolesStr.isBlank()) { + String[] roles = rolesStr.split(","); + for (String role : roles) { + role = role.trim(); + if (!role.isBlank()) { + localRoles.put(username, role); + } + } + } + } + + this.localUserRoles = ImmutableMultimap.copyOf(localRoles); + if (log.isDebugEnabled()) { + log.debug("Successfully loaded local user roles from properties file {}", rolePropertiesPath); + } + } + + /** + * Return the local user roles loaded for this {@link DatawaveSecurityRealm}. + * + * @return the local user roles. + */ + Multimap getLocalUserRoles() { + return this.localUserRoles; + } + + /** + * Exposed for the purpose of allowing a {@link SecurityEJBProvider} to be set for testing purposes without needing to worry about JNDI. + * + * @param securityEJBProvider + * the provider + */ + public void setSecurityEJBProvider(SecurityEJBProvider securityEJBProvider) { + this.securityEJBProvider = securityEJBProvider; + } + + /** + * Return the {@link SecurityEJBProvider} instance. If no provider was configured for this realm, the instance loaded from JNDI will be returned. + * + * @return the provider + */ + private SecurityEJBProvider getSecurityEJBProvider() { + return this.securityEJBProvider == null ? SecurityEJBUtils.getSecurityEJBProvider() : this.securityEJBProvider; + } + + /** + * Always returns {@link SupportLevel#UNSUPPORTED}. + * + * @return {@link SupportLevel#UNSUPPORTED} + */ + @Override + public SupportLevel getCredentialAcquireSupport(Class credentialType, String algorithmName, AlgorithmParameterSpec parameterSpec) { + Preconditions.checkNotNull(credentialType, "Parameter credentialType cannot be null"); + return SupportLevel.UNSUPPORTED; + } + + /** + * Return a {@link SupportLevel} indicating whether the given evidence type is supported by this realm. + * + * @return {@link SupportLevel#POSSIBLY_SUPPORTED} if the evidence type is a {@link DatawaveEvidence}, or {@link SupportLevel#UNSUPPORTED} otherwise + */ + @Override + public SupportLevel getEvidenceVerifySupport(Class evidenceType, String algorithmName) { + Preconditions.checkNotNull(evidenceType, "Parameter evidenceType may not be null"); + return evidenceType.isAssignableFrom(DatawaveEvidence.class) ? SupportLevel.POSSIBLY_SUPPORTED : SupportLevel.UNSUPPORTED; + } + + /** + * Return a realm identity for the given principal. It is expected that this principal will be a {@link DatawavePrincipal} that was decoded from a piece of + * {@link Evidence} using {@link DatawaveEvidenceDecoder}. If a realm identity is already cached for the principal, that identity will be returned. + * Otherwise, a new realm identity will be created and returned after caching it. + * + * @param principal + * the principal which identifies the identity within the realm (must not be {@code null}) + * @return the realm identity + * @throws RealmUnavailableException + * if an error occurs + */ + @Override + public RealmIdentity getRealmIdentity(Principal principal) throws RealmUnavailableException { + // Ensure we complete initialization for this realm. + completeInitialization(); + + // If we already have a realm identity cached for the principal, return the realm identity. + RealmIdentity cached = realmIdentityCache.get(principal); + if (cached != null) { + if (log.isTraceEnabled()) { + log.trace("Returning cached identity for principal {}", principal.getName()); + } + return cached; + } + + // Otherwise create a new realm identity for the principal. + if (principal instanceof DatawavePrincipal) { + // Create a CachedRealmIdentity that wraps around a DatawaveRealmIdentity and add it to the cache before returning it. + RealmIdentity realmIdentity = new DatawaveRealmIdentity((DatawavePrincipal) principal); + CachedRealmIdentity cachedIdentity = new CachedRealmIdentity(realmIdentity); + realmIdentityCache.put(principal, cachedIdentity); + if (log.isTraceEnabled()) { + log.trace("Created realm identity for principal {} with realm identity principal {} and attributes {}", principal, + cachedIdentity.getRealmIdentityPrincipal(), cachedIdentity.getAttributes().entries()); + } + return cachedIdentity; + } else { + // If somehow we were given a non-DatawavePrincipal, log a warning and return a NON_EXISTENT realm identity. + if (log.isWarnEnabled()) { + log.warn("Returning NON_EXISTENT identity for a principal that is not an instance of {} but is a {}: {}", DatawavePrincipal.class.getName(), + principal.getClass().getName(), principal); + } + return RealmIdentity.NON_EXISTENT; + } + } + + /** + * A wrapper {@link RealmIdentity} class that will cache some aspects of the information + */ + private static class CachedRealmIdentity implements RealmIdentity { + + private final RealmIdentity identity; + private AuthorizationIdentity authorizationIdentity; + private Attributes attributes; + + public CachedRealmIdentity(RealmIdentity identity) { + this.identity = identity; + } + + @Override + public Principal getRealmIdentityPrincipal() { + return identity.getRealmIdentityPrincipal(); + } + + @Override + public SupportLevel getCredentialAcquireSupport(Class credentialType, String algorithmName, AlgorithmParameterSpec parameterSpec) + throws RealmUnavailableException { + return identity.getCredentialAcquireSupport(credentialType, algorithmName, parameterSpec); + } + + @Override + public C getCredential(Class credentialType) throws RealmUnavailableException { + return identity.getCredential(credentialType); + } + + @Override + public SupportLevel getEvidenceVerifySupport(Class evidenceType, String algorithmName) throws RealmUnavailableException { + return identity.getEvidenceVerifySupport(evidenceType, algorithmName); + } + + @Override + public boolean verifyEvidence(Evidence evidence) throws RealmUnavailableException { + // Nullify the attributes and authorization identity so that the next time their getters are called, we store fresh instances from the wrapped + // identity. The attributes (and authorization identity) can be changed as a side effect of the call to verifyEvidence here. + this.attributes = null; + this.authorizationIdentity = null; + return identity.verifyEvidence(evidence); + } + + @Override + public boolean exists() throws RealmUnavailableException { + return identity.exists(); + } + + @Override + public void dispose() { + identity.dispose(); + } + + @Override + public AuthorizationIdentity getAuthorizationIdentity() throws RealmUnavailableException { + if (authorizationIdentity == null) { + authorizationIdentity = identity.getAuthorizationIdentity(); + } + return authorizationIdentity; + } + + @Override + public Attributes getAttributes() throws RealmUnavailableException { + if (attributes == null) { + attributes = identity.getAttributes(); + } + return attributes; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + CachedRealmIdentity that = (CachedRealmIdentity) o; + return Objects.equals(identity, that.identity); + } + + @Override + public int hashCode() { + return Objects.hashCode(identity); + } + } + + /** + * A {@link RealmIdentity} implementation that represents a specific authentication attempt against this security realm. + */ + private class DatawaveRealmIdentity implements RealmIdentity { + + private final String principalName; + + private Attributes attributes; + + public DatawaveRealmIdentity(DatawavePrincipal principal) { + this.principalName = principal.getName(); + this.attributes = loadAttributes(principal); + } + + /** + * Return a new {@link Attributes} instance with mappings for the username and roles found within the given principal. + * + * @param principal + * the principal + * @return the new {@link Attributes} + */ + private Attributes loadAttributes(DatawavePrincipal principal) { + if (principal == null) { + return Attributes.EMPTY; + } + + MapAttributes mapAttributes = new MapAttributes(); + + // Add the username. + mapAttributes.addLast(ATTRIBUTE_USERNAME, principal.getName()); + + // Add all roles for the primary user. + DatawaveUser primaryUser = principal.getPrimaryUser(); + if (primaryUser != null) { + mapAttributes.addAll(ATTRIBUTE_PRIMARY_USER_ROLES, primaryUser.getRoles()); + } + + // Add all roles for the proxied users. + Iterator proxiedUsers = principal.getProxiedUsers().iterator(); + Set proxyUserKeys = new HashSet<>(); + int proxyUserNum = 0; + while (proxiedUsers.hasNext()) { + DatawaveUser proxiedUser = proxiedUsers.next(); + + // Add the proxy user's roles to a mapping with a key for the specific user. + Collection userRoles = proxiedUser.getRoles(); + String proxyUserKey = ATTRIBUTE_PROXY_USER_KEY_BASE + proxyUserNum; + mapAttributes.addAll(proxyUserKey, userRoles); + proxyUserKeys.add(proxyUserKey); + proxyUserNum++; + + // If the last proxied user is a terminal server, add the terminal server roles. + if (!proxiedUsers.hasNext()) { + if (proxiedUser.getUserType() == DatawaveUser.UserType.SERVER) { + mapAttributes.addAll(ATTRIBUTE_TERMINAL_SERVER_ROLES, userRoles); + } + } + } + + // Add all local roles associated with the principal name to the primary user's roles. + Collection localRoles = localUserRoles.get(principalName.toLowerCase()); + if (!localRoles.isEmpty()) { + if (log.isTraceEnabled()) { + log.trace("Added local roles {} for principal username {}", localRoles, principalName); + } + mapAttributes.addAll(ATTRIBUTE_PRIMARY_USER_ROLES, localRoles); + } + + // Add a mapping for the proxy user keys. + mapAttributes.addAll(ATTRIBUTE_PROXIED_USER_KEYS, proxyUserKeys); + + return mapAttributes.asReadOnly(); + } + + @Override + public Principal getRealmIdentityPrincipal() { + try { + if (exists()) { + return new NamePrincipal(principalName); + } + } catch (Exception e) { + log.error("Failed to obtain realm identity principal", e); + } + return null; + } + + @Override + public SupportLevel getCredentialAcquireSupport(Class credentialType, String algorithmName, + AlgorithmParameterSpec parameterSpec) { + return SupportLevel.UNSUPPORTED; + } + + @Override + public C getCredential(Class credentialType) { + return null; + } + + @Override + public SupportLevel getEvidenceVerifySupport(Class evidenceType, String algorithmName) { + Preconditions.checkNotNull(evidenceType, "Parameter evidenceType may not be null"); + + if (evidenceType.isAssignableFrom(DatawaveEvidence.class)) { + return SupportLevel.POSSIBLY_SUPPORTED; + } else { + return SupportLevel.UNSUPPORTED; + } + } + + @Override + public boolean verifyEvidence(Evidence evidence) { + Preconditions.checkNotNull(evidence, "Parameter evidence may not be null"); + + // Check if the evidence type is supported. + if (!(evidence instanceof DatawaveEvidence)) { + if (log.isTraceEnabled()) { + log.trace("Evidence {} is an unsupported type: {}", evidence, evidence.getClass().getName()); + } + return false; + } + + // If the evidence has a certificate, validate the certificate. + if (evidence instanceof X509CertificateEvidence && !hasValidCertificate((X509CertificateEvidence) evidence)) { + if (log.isTraceEnabled()) { + log.trace("Evidence certificate failed validation"); + } + return false; + } + + DatawaveEvidence datawaveEvidence = (DatawaveEvidence) evidence; + try { + // If the evidence username is different from the principal name, add any local roles associated with the evidence's username to the overall + // identity's attributes. + if (!datawaveEvidence.getUsername().equalsIgnoreCase(principalName)) { + Collection localRoles = localUserRoles.get(datawaveEvidence.getUsername()); + if (!localRoles.isEmpty()) { + MapAttributes updatedAttributes = new MapAttributes(this.attributes); + updatedAttributes.addAll(AttributeConstants.ATTRIBUTE_PRIMARY_USER_ROLES, localRoles); + this.attributes = updatedAttributes.asReadOnly(); + if (log.isTraceEnabled()) { + log.trace("Added local roles {} associated with evidence username {}", localRoles, datawaveEvidence.getUsername()); + } + } + } + } catch (Exception e) { + log.error("Failed to load local user roles for evidence username {}", datawaveEvidence.getUsername(), e); + throw new IllegalStateException("Failed to load local user roles using evidence username " + datawaveEvidence.getUsername(), e); + } + + return true; + } + + /** + * Return whether the given evidence has a valid certificate + * + * @param evidence + * the evidence + * @return true if no certificate validator has been configured or the evidence has a valid certificate, or false otherwise + */ + private boolean hasValidCertificate(X509CertificateEvidence evidence) { + // If the evidence is an X509CertificateEvidence, and a x509 evidence validator is configured, validate the evidence. + if (x509EvidenceValidator != null) { + try { + // If the evidence is not valid, nullify the attributes to negate the existence of the realm identity. + if (!x509EvidenceValidator.validate(evidence)) { + return false; + } + } catch (Exception e) { + log.error("Error occurred while validating certificate evidence {}", evidence, e); + return false; + } + } + return true; + } + + @Override + public AuthorizationIdentity getAuthorizationIdentity() { + return AuthorizationIdentity.basicIdentity(attributes); + } + + @Override + public Attributes getAttributes() { + return attributes; + } + + @Override + public boolean exists() { + return true; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + DatawaveRealmIdentity that = (DatawaveRealmIdentity) o; + return Objects.equals(principalName, that.principalName); + } + + @Override + public int hashCode() { + return Objects.hashCode(principalName); + } + } + + /** + * Configuration options for {@link DatawaveSecurityRealm}. + */ + public static class Config { + + /** + * The fully qualified name of the certificate verifier class to use to verify the user cert during SSL authentication. Must be a class that implements + * 4 {@link datawave.security.cert.X509CertificateVerifier}. + */ + public static final String OPTION_CERT_VERIFIER = "certVerifier"; + + /** + * The OSCP level to set in the certificate verifier instance if it is an instance of {@link DatawaveCertVerifier}. + */ + public static final String OPTION_OSCP_LEVEL = "oscpLevel"; + + /** + * The maximum number of entries allowed for the realm identity cache. A negative value implies no limit. + */ + public static final String OPTION_MAX_CACHE_ENTRIES = "maxCacheEntries"; + + /** + * The maximum age in milliseconds an entry in the cache may reach before it expires and is evicted from the cache. A negative value implies no limit. + */ + public static final String OPTION_MAX_CACHE_AGE = "maxCacheAge"; + + /** + * The fully qualified path to a properties file that contains mappings of usernames to comma-delimited local roles that should be added to the role set + * of matching users. The usernames must either match the username returned by {@link DatawaveEvidence#getUsername()} or + * {@link DatawavePrincipal#getName()}. Matching is case-insensitive. + */ + public static final String OPTION_ROLE_PROPERTIES = "roleProperties"; + + public static Config fromMap(Map config) { + String certVerifierClass = ConfigUtils.getString(config.get(OPTION_CERT_VERIFIER), null); + String oscpLevel = ConfigUtils.getString(config.get(OPTION_OSCP_LEVEL), null); + long maxCacheEntries = ConfigUtils.getLong(config.get(OPTION_MAX_CACHE_ENTRIES), -1L); + long maxCacheAge = ConfigUtils.getLong(config.get(OPTION_MAX_CACHE_AGE), -1L); + String rolePropertiesPath = ConfigUtils.getString(config.get(OPTION_ROLE_PROPERTIES), null); + + return new Config(certVerifierClass, oscpLevel, maxCacheEntries, maxCacheAge, rolePropertiesPath); + } + + private final String certVerifierClass; + private final String oscpLevel; + private final long maxCacheEntries; + private final long maxCacheAge; + private final String rolePropertiesPath; + + public Config(String certVerifierClass, String oscpLevel, long maxCacheEntries, long maxCacheAge, String roleProperties) { + this.certVerifierClass = certVerifierClass; + this.oscpLevel = oscpLevel; + this.maxCacheEntries = maxCacheEntries; + this.maxCacheAge = maxCacheAge; + this.rolePropertiesPath = roleProperties; + } + + public String getCertVerifierClass() { + return certVerifierClass; + } + + public String getOscpLevel() { + return oscpLevel; + } + + public long getMaxCacheEntries() { + return maxCacheEntries; + } + + public long getMaxCacheAge() { + return maxCacheAge; + } + + public String getRolePropertiesPath() { + return rolePropertiesPath; + } + + @Override + public String toString() { + return new StringJoiner(", ", Config.class.getSimpleName() + "[", "]").add("certVerifierClass='" + certVerifierClass + "'") + .add("oscpLevel='" + oscpLevel + "'").add("maxCacheEntries=" + maxCacheEntries).add("maxCacheAge=" + maxCacheAge) + .add("rolePropertiesPath=" + rolePropertiesPath).toString(); + } + } + +} diff --git a/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/DatawaveUserProvider.java b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/DatawaveUserProvider.java new file mode 100644 index 00000000000..894cdd95582 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/DatawaveUserProvider.java @@ -0,0 +1,126 @@ +package datawave.security.realm; + +import java.security.Key; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.X509KeyManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.wildfly.security.evidence.Evidence; + +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.guava.GuavaModule; +import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule; + +import datawave.security.authorization.DatawaveUser; +import datawave.security.authorization.DatawaveUserService; +import datawave.security.authorization.JWTTokenHandler; +import datawave.security.cert.SSLStores; +import datawave.security.evidence.JWTEvidence; +import datawave.security.evidence.TrustedHeaderEvidence; +import datawave.security.evidence.X509CertificateEvidence; +import datawave.security.system.SecurityEJBProvider; + +/** + * This class is responsible for delegating the lookup up of {@link DatawaveUser} instances to a configured {@link DatawaveUserService}. + */ +public class DatawaveUserProvider { + + private static final Logger log = LoggerFactory.getLogger(DatawaveUserProvider.class); + + private static DatawaveUserProvider instance; + + /** + * Return a static instance of {@link DatawaveUserProvider} that is configured to use the {@link DatawaveUserService} and {@link SSLStores} provided by + * {@link SecurityEJBUtils}. + * + * @return the instance + * @throws Exception + * if the instance could not be created + */ + public static DatawaveUserProvider getInstance() throws Exception { + // If an instance has not been created yet, do so. + if (instance == null) { + // Fetch the EJB provider. + SecurityEJBProvider ejbProvider; + try { + ejbProvider = SecurityEJBUtils.getSecurityEJBProvider(); + } catch (Exception e) { + log.error("Failed to fetch SecurityEJBProvider", e); + throw e; + } + + // Ensure the EJB provider has the datawave user service and SSL keystore and truststore. + DatawaveUserService userService = ejbProvider.getDatawaveUserService(); + SSLStores sslStores = ejbProvider.getSSLStores(); + if (userService == null) { + throw new IllegalStateException("EJBProvider returned null user service"); + } + if (sslStores == null) { + throw new IllegalStateException("EJBProvider returned null ssl context"); + } + + // Create the JWT Token handler. + JWTTokenHandler tokenHandler; + try { + // @formatter:off + ObjectMapper mapper = JsonMapper.builder() + .enable(MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME) + .build() + .registerModules(new GuavaModule()) + .registerModules(new JaxbAnnotationModule()); + // @formatter:on + String alias = sslStores.getKeyStore().aliases().nextElement(); + X509KeyManager keyManager = (X509KeyManager) sslStores.getKeyManagers()[0]; + X509Certificate[] certs = keyManager.getCertificateChain(alias); + Key signingKey = keyManager.getPrivateKey(alias); + + tokenHandler = new JWTTokenHandler(certs[0], signingKey, 24, TimeUnit.HOURS, JWTTokenHandler.TtlMode.RELATIVE_TO_CURRENT_TIME, mapper); + } catch (Exception e) { + log.error("Failed to create JWTTokenHandler", e); + throw e; + } + + // Create the user provider. + instance = new DatawaveUserProvider(userService, tokenHandler); + } + return instance; + } + + private final DatawaveUserService userService; + private final JWTTokenHandler jwtTokenHandler; + + public DatawaveUserProvider(DatawaveUserService userService, JWTTokenHandler jwtTokenHandler) { + this.userService = userService; + this.jwtTokenHandler = jwtTokenHandler; + } + + /** + * Look up and return the set of {@link DatawaveUser} associated with the users represented by the given {@link Evidence}. + * + * @param evidence + * the evidence + * @return the user set + * @throws Exception + * if an error occurred while fetching the users + */ + public Collection getUsers(Evidence evidence) throws Exception { + if (evidence instanceof JWTEvidence) { + return jwtTokenHandler.createUsersFromToken(((JWTEvidence) evidence).getToken()); + } + if (evidence instanceof TrustedHeaderEvidence) { + return userService.lookup(((TrustedHeaderEvidence) evidence).getEntities()); + } + if (evidence instanceof X509CertificateEvidence) { + return userService.lookup(((X509CertificateEvidence) evidence).getEntities()); + } + + return Set.of(); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/SecurityEJBUtils.java b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/SecurityEJBUtils.java new file mode 100644 index 00000000000..13f9a98b3da --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/realm/SecurityEJBUtils.java @@ -0,0 +1,54 @@ +package datawave.security.realm; + +import javax.naming.InitialContext; +import javax.naming.NamingException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import datawave.security.system.SecurityEJBProvider; + +/** + * A utility class responsible for loading a {@link SecurityEJBProvider} instance from JNDI. Due to the constraints of Elytron, we cannot deploy the Elytron + * custom components directly with the Datawave EAR deployment, and thus do not have injectable access to any beans defined therein. We will instead use JNDI as + * a workaround to look up the {@link SecurityEJBProvider} bean. + */ +public final class SecurityEJBUtils { + + public static final String SECURITY_EJB_JNDI_SYSTEM_PROPERTY = "dw.security.ejb.provider.jndi"; + + private static final Logger log = LoggerFactory.getLogger(SecurityEJBUtils.class); + + private static SecurityEJBProvider providerInstance; + + /** + * Returns a {@link SecurityEJBProvider} that was loaded from the JNDI binding {@value SECURITY_EJB_JNDI_SYSTEM_PROPERTY}. + * + * @return the {@link SecurityEJBProvider} instance + */ + public static SecurityEJBProvider getSecurityEJBProvider() { + if (providerInstance == null) { + String ejbJndi = System.getProperty(SECURITY_EJB_JNDI_SYSTEM_PROPERTY); + try { + InitialContext context = new InitialContext(); + providerInstance = (SecurityEJBProvider) context.lookup(ejbJndi); + if (log.isDebugEnabled()) { + if (providerInstance != null) { + log.debug("Successfully looked up instance of {} from JNDI: {}", SecurityEJBProvider.class.getName(), ejbJndi); + } else { + log.debug("Null instance of {} loaded from JNDI: {}", SecurityEJBProvider.class.getName(), ejbJndi); + } + } + } catch (NamingException e) { + log.error("Failed to look up instance of {} from JNDI {}", SecurityEJBProvider.class.getName(), ejbJndi, e); + throw new RuntimeException(e); + } + } + + return providerInstance; + } + + private SecurityEJBUtils() { + throw new UnsupportedOperationException(); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/utils/ConfigUtils.java b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/utils/ConfigUtils.java new file mode 100644 index 00000000000..e6084b0dbc5 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/main/java/datawave/security/utils/ConfigUtils.java @@ -0,0 +1,62 @@ +package datawave.security.utils; + +/** + * Utility class for parsing configuration options. + */ +public final class ConfigUtils { + + /** + * Return the trimmed value if it is non-blank, or the default value otherwise. + * + * @param value + * the value + * @param defaultValue + * the default value + * @return the string + */ + public static String getString(String value, String defaultValue) { + if (value != null && !value.isBlank()) { + return value.trim(); + } else { + return defaultValue; + } + } + + /** + * Return the given value as a boolean if it is non-blank, or the default value otherwise + * + * @param value + * the value + * @param defaultValue + * the default value + * @return the boolean + */ + public static Boolean getBoolean(String value, boolean defaultValue) { + if (value != null && !value.isBlank()) { + return Boolean.valueOf(value.trim()); + } else { + return defaultValue; + } + } + + /** + * Return the given value as a long if it is non-blank, or the default value otherwise. + * + * @param value + * the value + * @param defaultValue + * the default value + * @return the long + */ + public static Long getLong(String value, long defaultValue) { + if (value != null && !value.isBlank()) { + return Long.valueOf(value.trim()); + } else { + return defaultValue; + } + } + + private ConfigUtils() { + throw new UnsupportedOperationException(); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/main/resources/META-INF/services/org.wildfly.security.http.HttpServerAuthenticationMechanismFactory b/web-services/security-parent/security-elytron-module/src/main/resources/META-INF/services/org.wildfly.security.http.HttpServerAuthenticationMechanismFactory new file mode 100644 index 00000000000..57b5343a394 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/main/resources/META-INF/services/org.wildfly.security.http.HttpServerAuthenticationMechanismFactory @@ -0,0 +1 @@ +datawave.security.auth.DatawaveHttpAuthenticationMechanismFactory diff --git a/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/auth/DatawaveHttpAuthenticationMechanismConfigTests.java b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/auth/DatawaveHttpAuthenticationMechanismConfigTests.java new file mode 100644 index 00000000000..2b3a13a804b --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/auth/DatawaveHttpAuthenticationMechanismConfigTests.java @@ -0,0 +1,68 @@ +package datawave.security.auth; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.ClearSystemProperty; +import org.junitpioneer.jupiter.SetSystemProperty; + +import datawave.security.util.SecurityConstants; + +/** + * Tests for {@link DatawaveHttpAuthenticationMechanism.Config}. + */ +public class DatawaveHttpAuthenticationMechanismConfigTests { + + /** + * Verify the default values expected from {@link DatawaveHttpAuthenticationMechanism.Config#fromMap(Map)} when no system properties are configured with + * trusted headers. + */ + @ClearSystemProperty(key = SecurityConstants.TRUSTED_SUBJECT_DN_HEADER_SYSTEM_PROPERTY) + @ClearSystemProperty(key = SecurityConstants.TRUSTED_ISSUER_DN_HEADER_SYSTEM_PROPERTY) + @Test + void testDefaultValuesGivenNoSystemPropertiesSet() { + DatawaveHttpAuthenticationMechanism.Config config = DatawaveHttpAuthenticationMechanism.Config.fromMap(Map.of()); + assertThat(config.getTrustedSubjectDnHeader()).isEqualTo(SecurityConstants.DEFAULT_TRUSTED_SUBJECT_DN_HEADER); + assertThat(config.getTrustedIssuerDnHeader()).isEqualTo(SecurityConstants.DEFAULT_TRUSTED_ISSUER_DN_HEADER); + assertThat(config.isIdentityRestorationEnabled()).isTrue(); + assertThat(config.isSessionIdChangeEnabled()).isTrue(); + } + + /** + * Verify the values expected from {@link DatawaveHttpAuthenticationMechanism.Config#fromMap(Map)} when system properties are configured with trusted + * headers. + */ + @SetSystemProperty(key = SecurityConstants.TRUSTED_SUBJECT_DN_HEADER_SYSTEM_PROPERTY, value = "Alt-TrustedSubject") + @SetSystemProperty(key = SecurityConstants.TRUSTED_ISSUER_DN_HEADER_SYSTEM_PROPERTY, value = "Alt-TrustedIssuer") + @Test + void testDefaultValuesGivenSystemPropertiesSet() { + DatawaveHttpAuthenticationMechanism.Config config = DatawaveHttpAuthenticationMechanism.Config.fromMap(Map.of()); + assertThat(config.getTrustedSubjectDnHeader()).isEqualTo("Alt-TrustedSubject"); + assertThat(config.getTrustedIssuerDnHeader()).isEqualTo("Alt-TrustedIssuer"); + assertThat(config.isIdentityRestorationEnabled()).isTrue(); + assertThat(config.isSessionIdChangeEnabled()).isTrue(); + } + + /** + * Verify that when options are configured via the property map, they override everything else, including system properties. + */ + @SetSystemProperty(key = SecurityConstants.TRUSTED_SUBJECT_DN_HEADER_SYSTEM_PROPERTY, value = "Alt-TrustedSubject") + @SetSystemProperty(key = SecurityConstants.TRUSTED_ISSUER_DN_HEADER_SYSTEM_PROPERTY, value = "Alt-TrustedIssuer") + @Test + void testValuesGivenOverrides() { + Map properties = new HashMap<>(); + properties.put(DatawaveHttpAuthenticationMechanism.Config.OPTION_TRUSTED_SUBJECT_DN_HEADER, "Override-TrustedSubject"); + properties.put(DatawaveHttpAuthenticationMechanism.Config.OPTION_TRUSTED_ISSUER_DN_HEADER, "Override-TrustedIssuer"); + properties.put(DatawaveHttpAuthenticationMechanism.Config.OPTION_ENABLE_RESTORE_IDENTITY, "false"); + properties.put(DatawaveHttpAuthenticationMechanism.Config.OPTION_ENABLE_SESSION_ID_CHANGE, "false"); + + DatawaveHttpAuthenticationMechanism.Config config = DatawaveHttpAuthenticationMechanism.Config.fromMap(properties); + assertThat(config.getTrustedSubjectDnHeader()).isEqualTo("Override-TrustedSubject"); + assertThat(config.getTrustedIssuerDnHeader()).isEqualTo("Override-TrustedIssuer"); + assertThat(config.isIdentityRestorationEnabled()).isFalse(); + assertThat(config.isSessionIdChangeEnabled()).isFalse(); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/auth/DatawaveHttpAuthenticationMechanismFactoryTest.java b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/auth/DatawaveHttpAuthenticationMechanismFactoryTest.java new file mode 100644 index 00000000000..ed99c2c49cf --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/auth/DatawaveHttpAuthenticationMechanismFactoryTest.java @@ -0,0 +1,49 @@ +package datawave.security.auth; + +import static datawave.security.auth.DatawaveHttpAuthenticationMechanismFactory.DATAWAVE_AUTH_NAME; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; + +import javax.security.auth.callback.CallbackHandler; + +import org.junit.jupiter.api.Test; +import org.wildfly.security.http.HttpServerAuthenticationMechanism; + +class DatawaveHttpAuthenticationMechanismFactoryTest { + + /** + * Verify that {@link DatawaveHttpAuthenticationMechanismFactory#getMechanismNames(Map)} returns an array containing only + * {@value DatawaveHttpAuthenticationMechanismFactory#DATAWAVE_AUTH_NAME}. + */ + @Test + void testGetMechanismNames() { + DatawaveHttpAuthenticationMechanismFactory factory = new DatawaveHttpAuthenticationMechanismFactory(); + String[] mechanismNames = factory.getMechanismNames(Map.of()); + assertThat(mechanismNames).containsExactly(DATAWAVE_AUTH_NAME); + } + + /** + * Verify that when {@link DatawaveHttpAuthenticationMechanismFactory#createAuthenticationMechanism(String, Map, CallbackHandler)} is called with the + * mechanism {@value DatawaveHttpAuthenticationMechanismFactory#DATAWAVE_AUTH_NAME}, a {@link DatawaveHttpAuthenticationMechanism} is returned. + */ + @Test + void testCreateAuthenticationMechanismGivenDatawaveAuthMechanism() { + CallbackHandler callbackHandler = callbacks -> {}; + DatawaveHttpAuthenticationMechanismFactory factory = new DatawaveHttpAuthenticationMechanismFactory(); + HttpServerAuthenticationMechanism authenticationMechanism = factory.createAuthenticationMechanism(DATAWAVE_AUTH_NAME, Map.of(), callbackHandler); + assertThat(authenticationMechanism).isInstanceOf(DatawaveHttpAuthenticationMechanism.class); + } + + /** + * Verify that when {@link DatawaveHttpAuthenticationMechanismFactory#createAuthenticationMechanism(String, Map, CallbackHandler)} is called with a + * mechanism other than {@value DatawaveHttpAuthenticationMechanismFactory#DATAWAVE_AUTH_NAME}, null is returned. + */ + @Test + void testCreateAuthenticationMechanismGivenNonDatawaveAuthMechanism() { + CallbackHandler callbackHandler = callbacks -> {}; + DatawaveHttpAuthenticationMechanismFactory factory = new DatawaveHttpAuthenticationMechanismFactory(); + HttpServerAuthenticationMechanism authenticationMechanism = factory.createAuthenticationMechanism("BASIC", Map.of(), callbackHandler); + assertThat(authenticationMechanism).isNull(); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/auth/DatawaveHttpAuthenticationMechanismTest.java b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/auth/DatawaveHttpAuthenticationMechanismTest.java new file mode 100644 index 00000000000..1bdad9e4b9c --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/auth/DatawaveHttpAuthenticationMechanismTest.java @@ -0,0 +1,782 @@ +package datawave.security.auth; + +import static java.util.Arrays.asList; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.atMostOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLSession; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.SetSystemProperty; +import org.wildfly.security.auth.callback.AuthenticationCompleteCallback; +import org.wildfly.security.auth.callback.CachedIdentityAuthorizeCallback; +import org.wildfly.security.auth.callback.EvidenceVerifyCallback; +import org.wildfly.security.auth.callback.PrincipalAuthorizeCallback; +import org.wildfly.security.auth.server.SecurityDomain; +import org.wildfly.security.auth.server.SecurityIdentity; +import org.wildfly.security.authz.RoleDecoder; +import org.wildfly.security.cache.CachedIdentity; +import org.wildfly.security.evidence.Evidence; +import org.wildfly.security.http.HttpAuthenticationException; +import org.wildfly.security.http.HttpScope; +import org.wildfly.security.http.HttpServerRequest; +import org.wildfly.security.http.Scope; + +import datawave.security.authorization.DatawavePrincipal; +import datawave.security.authorization.SubjectIssuerDNPair; +import datawave.security.evidence.JWTEvidence; +import datawave.security.evidence.TrustedHeaderEvidence; +import datawave.security.evidence.X509CertificateEvidence; +import datawave.security.test.TestRealm; +import datawave.security.util.SecurityConstants; + +/** + * Unit tests for {@link DatawaveHttpAuthenticationMechanism}. + */ +@SetSystemProperty(key = SecurityConstants.TRUSTED_PROXIED_ENTITIES_SYSTEM_PROPERTY, value = "cn=trustedServer") // Configure the trusted proxied entities. +class DatawaveHttpAuthenticationMechanismTest { + + private final Map mechanismProperties = new HashMap<>(); + private final HttpServerRequestWrapper requestWrapper = new HttpServerRequestWrapper(); + private DatawaveHttpAuthenticationMechanism mechanism; + private MockCallbackHandler callbackHandler; + private HttpScopeHandler scopeWrapper; + + private static SecurityDomain securityDomain; + + @BeforeAll + static void beforeAll() { + // Create a security domain for creating test security identities. + // @formatter:off + securityDomain = SecurityDomain.builder() + .addRealm("testRealm", new TestRealm()) + .setRoleDecoder(RoleDecoder.simple(TestRealm.ROLES_ATTRIBUTE)) + .build() + .build(); + // @formatter:on + } + + @BeforeEach + void setUp() { + this.callbackHandler = new MockCallbackHandler(); + this.mechanismProperties.clear(); + this.mechanism = null; + } + + /** + * Covers test scenarios where caching the identity in the session scope is disabled. + */ + @DisplayName("When caching the identity in the HTTP session is disabled") + @Nested + class NonCachingTests { + + @BeforeEach + void setUp() { + // Disable session caching features. + givenIdentityRestorationEnabled(false); + givenSessionIdChangeEnabled(false); + } + + /** + * Verify that given a request with no information that can be turned into evidence for authorization, it fails authentication. + */ + @DisplayName("A request should fail when evidence cannot be extracted from it") + @Test + void givenNoEvidence() throws HttpAuthenticationException { + // Trigger the request. + evaluateRequest(); + + // Verify the request failed. + requestWrapper.assertRequestFailed(); + + // Verify only a single callback occurred, an authentication complete with a failed status. + List callbacks = callbackHandler.getCapturedCallbacks(); + assertThat(callbacks).hasSize(1); + assertThat(callbacks.get(0)).isInstanceOf(AuthenticationCompleteCallback.class); + assertTrue(((AuthenticationCompleteCallback) callbacks.get(0)).failed()); + } + + /** + * Verify that given a request with evidence that failed verification, it fails authentication. + */ + @DisplayName("A request should fail when evidence verification fails") + @Test + void givenEvidenceVerificationFailed() throws HttpAuthenticationException { + // Ensure JWT evidence is created. + requestWrapper.setHeader("Authorization", "Bearer iamatoken"); + + // Make the evidence fail verification. + callbackHandler.actLikeEvidenceIsNotVerified(); + + // Trigger the request. + evaluateRequest(); + + // Verify the request failed. + requestWrapper.assertRequestFailed(); + + // Verify two callbacks occurred, one for evidence verification, and one for authentication complete with a failed status. + List callbacks = callbackHandler.getCapturedCallbacks(); + assertThat(callbacks).hasSize(2); + assertThat(callbacks.get(0)).isInstanceOf(EvidenceVerifyCallback.class); + assertThat(callbacks.get(1)).isInstanceOf(AuthenticationCompleteCallback.class); + assertTrue(((AuthenticationCompleteCallback) callbacks.get(1)).failed()); + } + + /** + * Verify that given a request with no information that can be turned into evidence for authorization, it fails authentication. + */ + @DisplayName("A request should fail when principal authorization fails") + @Test + void givenPrincipalAuthorizeFailed() throws HttpAuthenticationException { + // Ensure JWT evidence is created. + requestWrapper.setHeader("Authorization", "Bearer iamatoken"); + + // Allow the evidence to be verified, but have it fail authorization (for instance, if we had a user with invalid roles). + callbackHandler.actLikeEvidenceIsVerified(); + callbackHandler.actLikeFirstAuthorizationAttemptFails(); + + // Trigger the request. + evaluateRequest(); + + // Verify the request failed. + requestWrapper.assertRequestFailed(); + + // Verify three callbacks occurred, one for evidence verification, one for authorization of a principal, and one for authentication complete with a + // failed status. + List callbacks = callbackHandler.getCapturedCallbacks(); + assertThat(callbacks).hasSize(3); + assertThat(callbacks.get(0)).isInstanceOf(EvidenceVerifyCallback.class); + assertThat(callbacks.get(1)).isInstanceOf(PrincipalAuthorizeCallback.class); + assertThat(callbacks.get(2)).isInstanceOf(AuthenticationCompleteCallback.class); + assertTrue(((AuthenticationCompleteCallback) callbacks.get(2)).failed()); + } + + /** + * Verify a scenario where JWT evidence is given, and authorizations succeeds. + */ + @DisplayName("A request should succeed with valid JWT") + @Test + void givenJwtWithSuccess() throws HttpAuthenticationException { + requestWrapper.setHeader("Authorization", "Bearer iamatoken"); + + // Act like the evidence passes verification, and the final principal is authorized. + callbackHandler.actLikeEvidenceIsVerified(); + callbackHandler.actLikeFirstAuthorizationAttemptSucceeds(); + + // Trigger the request. + evaluateRequest(); + + // Verify the request succeeded. + requestWrapper.assertRequestSucceeded(); + + // Verify three callbacks occurred, one for evidence verification, one for authorization of a principal, and one for authentication complete with a + // success status. + List callbacks = callbackHandler.getCapturedCallbacks(); + assertThat(callbacks).hasSize(3); + assertThat(callbacks.get(0)).isInstanceOf(EvidenceVerifyCallback.class); + assertThat(callbacks.get(1)).isInstanceOf(PrincipalAuthorizeCallback.class); + assertThat(callbacks.get(2)).isInstanceOf(AuthenticationCompleteCallback.class); + assertTrue(((AuthenticationCompleteCallback) callbacks.get(2)).succeeded()); + + // Validate the evidence that was passed to the EvidenceVerifyCallback. + EvidenceVerifyCallback evidenceVerifyCallback = (EvidenceVerifyCallback) callbacks.get(0); + Evidence evidence = evidenceVerifyCallback.getEvidence(); + assertInstanceOf(JWTEvidence.class, evidence); + assertEquals("iamatoken", ((JWTEvidence) evidence).getToken()); + } + + /** + * Verify an exception is thrown when proxied entities are given without proxied issuers. + */ + @DisplayName("A request should fail when proxied entities are given without issuers") + @Test + void givenProxiedEntitiesWithoutIssuers() { + // Configure the trusted headers. Add a trusted entity to the proxy chain to test its removal. + requestWrapper.setHeader(SecurityConstants.PROXIED_ENTITIES_HEADER, "cn=server1"); + + // @formatter:off + assertThatThrownBy(DatawaveHttpAuthenticationMechanismTest.this::evaluateRequest) + .isInstanceOf(HttpAuthenticationException.class) + .hasMessage("Error occurred when obtaining evidence for authentication") + .hasRootCauseInstanceOf(MissingHeaderException.class) + .hasRootCauseMessage("X-ProxiedEntitiesChain provided, but missing X-ProxiedIssuersChain"); + // @formatter:off + } + + /** + * Verify an exception is thrown when a trusted subject is given without a trusted issuer. + */ + @DisplayName("A request should fail when a trusted subject is given without a trusted issuer") + @Test + void givenTrustedSubjectWithoutIssuer() { + // Configure the trusted headers. Add a trusted entity to the proxy chain to test its removal. + requestWrapper.setHeader(SecurityConstants.DEFAULT_TRUSTED_SUBJECT_DN_HEADER, "cn=testUser"); + requestWrapper.setHeader(SecurityConstants.PROXIED_ENTITIES_HEADER, "cn=server1"); + requestWrapper.setHeader(SecurityConstants.PROXIED_ISSUERS_HEADER, "cn=issuer1"); + + // @formatter:off + assertThatThrownBy(DatawaveHttpAuthenticationMechanismTest.this::evaluateRequest) + .isInstanceOf(HttpAuthenticationException.class) + .hasMessage("Error occurred when obtaining evidence for authentication") + .hasRootCauseInstanceOf(MissingHeaderException.class) + .hasRootCauseMessage("Missing trusted subject DN (cn=testUser) or issuer DN (null) for trusted header authentication"); + // @formatter:off + } + + /** + * Verify an exception is thrown when a trusted issuer is given without a trusted subject. + */ + @DisplayName("A request should fail when a trusted issuer is given without a trusted subject") + @Test + void givenTrustedIssuerWithoutSubject() { + // Configure the trusted headers. Add a trusted entity to the proxy chain to test its removal. + requestWrapper.setHeader(SecurityConstants.DEFAULT_TRUSTED_ISSUER_DN_HEADER, "cn=testIssuer"); + requestWrapper.setHeader(SecurityConstants.PROXIED_ENTITIES_HEADER, "cn=server1"); + requestWrapper.setHeader(SecurityConstants.PROXIED_ISSUERS_HEADER, "cn=issuer1"); + + // @formatter:off + assertThatThrownBy(DatawaveHttpAuthenticationMechanismTest.this::evaluateRequest) + .isInstanceOf(HttpAuthenticationException.class) + .hasMessage("Error occurred when obtaining evidence for authentication") + .hasRootCauseInstanceOf(MissingHeaderException.class) + .hasRootCauseMessage("Missing trusted subject DN (null) or issuer DN (cn=testIssuer) for trusted header authentication"); + // @formatter:off + } + + /** + * Verify a scenario where trusted header evidence is given, and authorizations succeeds. + */ + @DisplayName("A request should succeed with valid trusted headers") + @Test + void givenTrustedHeadersWithSuccess() throws HttpAuthenticationException { + // Configure the trusted headers. Add a trusted entity to the proxy chain to test its removal. + requestWrapper.setHeader(SecurityConstants.DEFAULT_TRUSTED_SUBJECT_DN_HEADER, "cn=testUser"); + requestWrapper.setHeader(SecurityConstants.DEFAULT_TRUSTED_ISSUER_DN_HEADER, "cn=testIssuer"); + requestWrapper.setHeader(SecurityConstants.PROXIED_ENTITIES_HEADER, "cn=server1"); + requestWrapper.setHeader(SecurityConstants.PROXIED_ISSUERS_HEADER, "cn=issuer1"); + + // Act like the evidence passes verification, and the final principal is authorized. + callbackHandler.actLikeEvidenceIsVerified(); + callbackHandler.actLikeFirstAuthorizationAttemptSucceeds(); + + // Trigger the request. + evaluateRequest(); + + // Verify the request succeeded. + requestWrapper.assertRequestSucceeded(); + + // Verify three callbacks occurred, one for evidence verification, one for authorization of a principal, and one for authentication complete with a + // success status. + List callbacks = callbackHandler.getCapturedCallbacks(); + assertThat(callbacks).hasSize(3); + assertThat(callbacks.get(0)).isInstanceOf(EvidenceVerifyCallback.class); + assertThat(callbacks.get(1)).isInstanceOf(PrincipalAuthorizeCallback.class); + assertThat(callbacks.get(2)).isInstanceOf(AuthenticationCompleteCallback.class); + assertTrue(((AuthenticationCompleteCallback) callbacks.get(2)).succeeded()); + + // Validate the evidence that was passed to the EvidenceVerifyCallback. + EvidenceVerifyCallback evidenceVerifyCallback = (EvidenceVerifyCallback)callbacks.get(0); + Evidence evidence = evidenceVerifyCallback.getEvidence(); + assertInstanceOf(TrustedHeaderEvidence.class, evidence); + TrustedHeaderEvidence trustedHeaderEvidence = (TrustedHeaderEvidence) evidence; + assertEquals("cn=server1", trustedHeaderEvidence.getUsername()); + List entities = new ArrayList<>(); + entities.add(SubjectIssuerDNPair.of("cn=server1", "cn=issuer1")); + entities.add(SubjectIssuerDNPair.of("cn=server2", "cn=issuer2")); + entities.add(SubjectIssuerDNPair.of("cn=testuser", "cn=testissuer")); + assertEquals(entities, trustedHeaderEvidence.getEntities()); + } + + /** + * Verify a scenario where a certificate is given, and authorizations succeeds. + */ + @DisplayName("A request should succeed with valid certs") + @Test + void givenCertificateWithSuccess() throws Exception { + // Create an X509 certificate. + Security.addProvider(new BouncyCastleProvider()); + + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "BC"); + generator.initialize(2048); + KeyPair keyPair = generator.generateKeyPair(); + + X500Name subjectDn = new X500Name("CN=certUser"); + X500Name issuerDn = new X500Name("CN=certIssuer"); + BigInteger serialNumber = BigInteger.valueOf(new SecureRandom().nextInt()); + Date validFrom = new Date(); + Date validTo = new Date(validFrom.getTime() + 365 * 25 * 60 * 60 * 1000L); + + X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(issuerDn, serialNumber, validFrom, validTo, subjectDn, keyPair.getPublic()); + ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(keyPair.getPrivate()); + X509Certificate certificate = new JcaX509CertificateConverter().setProvider("BC").getCertificate(builder.build(signer)); + + // Configure the trusted headers. Add a trusted entity to the proxy chain to test its removal. + requestWrapper.setCertificate(certificate); + requestWrapper.setHeader(SecurityConstants.PROXIED_ENTITIES_HEADER, "cn=server1"); + requestWrapper.setHeader(SecurityConstants.PROXIED_ISSUERS_HEADER, "cn=issuer1"); + + // Act like the evidence passes verification, and the final principal is authorized. + callbackHandler.actLikeEvidenceIsVerified(); + callbackHandler.actLikeFirstAuthorizationAttemptSucceeds(); + + // Trigger the request. + evaluateRequest(); + + // Verify the request succeeded. + requestWrapper.assertRequestSucceeded(); + + // Verify three callbacks occurred, one for evidence verification, one for authorization of a principal, and one for authentication complete with a + // success status. + List callbacks = callbackHandler.getCapturedCallbacks(); + assertThat(callbacks).hasSize(3); + assertThat(callbacks.get(0)).isInstanceOf(EvidenceVerifyCallback.class); + assertThat(callbacks.get(1)).isInstanceOf(PrincipalAuthorizeCallback.class); + assertThat(callbacks.get(2)).isInstanceOf(AuthenticationCompleteCallback.class); + assertTrue(((AuthenticationCompleteCallback) callbacks.get(2)).succeeded()); + + + // Validate the evidence that was passed to the EvidenceVerifyCallback. + EvidenceVerifyCallback evidenceVerifyCallback = (EvidenceVerifyCallback)callbacks.get(0); + Evidence evidence = evidenceVerifyCallback.getEvidence(); + assertInstanceOf(X509CertificateEvidence.class, evidence); + X509CertificateEvidence x509Evidence = (X509CertificateEvidence) evidence; + assertEquals(certificate, x509Evidence.getCertificate()); + assertEquals("cn=server1", x509Evidence.getUsername()); + List entities = new ArrayList<>(); + entities.add(SubjectIssuerDNPair.of("cn=server1", "cn=issuer1")); + entities.add(SubjectIssuerDNPair.of("cn=server2", "cn=issuer2")); + entities.add(SubjectIssuerDNPair.of("cn=certuser", "cn=certissuer")); + assertEquals(entities, x509Evidence.getEntities()); + } + } + + /** + * Covers test scenarios where caching the identity in the session scope is enabled. + */ + @DisplayName("When caching the identity in the HTTP session is enabled") + @Nested + class CachingTests { + + @BeforeEach + void setUp() { + // Disable session caching features. + givenIdentityRestorationEnabled(true); + givenSessionIdChangeEnabled(true); + + // Set up the mock session scope for the request. + HttpScope scope = mock(HttpScope.class); + scopeWrapper = new HttpScopeHandler(scope); + requestWrapper.setSessionScope(scope); + } + + @DisplayName("The cached identity should be used if already in the scope") + @Test + void givenIdentityAlreadyCached()throws HttpAuthenticationException { + scopeWrapper.supportAttachments(); + scopeWrapper.setExists(true); + + SecurityIdentity identity = securityDomain.createAdHocIdentity("test"); + CachedIdentity cachedIdentity = new CachedIdentity("DATAWAVE-AUTH", false, identity); + scopeWrapper.setCachedIdentity(cachedIdentity); + + callbackHandler.actLikeFirstAuthorizationAttemptSucceeds(); + + evaluateRequest(); + + // Verify the request succeeded. + requestWrapper.assertRequestSucceeded(); + + // Verify two callbacks occurred, one for authorizing a cached identity, and one for authentication complete with a success status. + List callbacks = callbackHandler.getCapturedCallbacks(); + assertThat(callbacks).hasSize(2); + assertThat(callbacks.get(0)).isInstanceOf(CachedIdentityAuthorizeCallback.class); + assertThat(callbacks.get(1)).isInstanceOf(AuthenticationCompleteCallback.class); + assertTrue(((AuthenticationCompleteCallback) callbacks.get(1)).succeeded()); + + // The session id should not have been changed. + scopeWrapper.assertSessionIdNotChanged(); + } + + @DisplayName("The identity should not be cached if the scope does not support attachments.") + @Test + void givenSessionScopeDoesNotSupportAttachments()throws HttpAuthenticationException { + // Tell the scope to not support attachments. + scopeWrapper.doNotSupportAttachments(); + + // Configure JWT. + requestWrapper.setHeader("Authorization", "Bearer iamatoken"); + + // The first authorization attempt with the identity cache should fail. + callbackHandler.actLikeFirstAuthorizationAttemptFails(); + // Act like the evidence passes verification, and the final principal is authorized. + callbackHandler.actLikeEvidenceIsVerified(); + callbackHandler.actLikeSecondAuthorizationAttemptSucceeds(); + + // Trigger the request. + evaluateRequest(); + + // Verify the request succeeded. + requestWrapper.assertRequestSucceeded(); + + // Verify four callbacks occurred, one for at attempt to use the empty identity cache, one for evidence verification, one for authorization of a + // principal, and one for authentication complete with a success status. + List callbacks = callbackHandler.getCapturedCallbacks(); + assertThat(callbacks).hasSize(4); + assertThat(callbacks.get(0)).isInstanceOf(CachedIdentityAuthorizeCallback.class); + assertThat(callbacks.get(1)).isInstanceOf(EvidenceVerifyCallback.class); + assertThat(callbacks.get(2)).isInstanceOf(CachedIdentityAuthorizeCallback.class); + assertThat(callbacks.get(3)).isInstanceOf(AuthenticationCompleteCallback.class); + assertTrue(((AuthenticationCompleteCallback) callbacks.get(3)).succeeded()); + + // Validate the evidence that was passed to the EvidenceVerifyCallback. + EvidenceVerifyCallback evidenceVerifyCallback = (EvidenceVerifyCallback) callbacks.get(1); + Evidence evidence = evidenceVerifyCallback.getEvidence(); + assertInstanceOf(JWTEvidence.class, evidence); + assertEquals("iamatoken", ((JWTEvidence) evidence).getToken()); + + // Nothing should have been attached to the scope. + scopeWrapper.assertNothingAttachedToScope(); + } + + @DisplayName("The identity should be cached if the scope support attachments.") + @Test + void givenSessionScopeSupportsAttachments()throws HttpAuthenticationException { + // Tell the scope to support attachments. + scopeWrapper.supportAttachments(); + + // Configure JWT. + requestWrapper.setHeader("Authorization", "Bearer iamatoken"); + + // The first authorization attempt with the identity cache should fail. + callbackHandler.actLikeFirstAuthorizationAttemptFails(); + // Act like the evidence passes verification, and the final principal is authorized. + callbackHandler.actLikeEvidenceIsVerified(); + callbackHandler.actLikeSecondAuthorizationAttemptSucceeds(); + + // Trigger the request. + evaluateRequest(); + + // Verify the request succeeded. + requestWrapper.assertRequestSucceeded(); + + // Verify four callbacks occurred, one for at attempt to use the empty identity cache, one for evidence verification, one for authorization of a + // principal, and one for authentication complete with a success status. + List callbacks = callbackHandler.getCapturedCallbacks(); + assertThat(callbacks).hasSize(4); + assertThat(callbacks.get(0)).isInstanceOf(CachedIdentityAuthorizeCallback.class); + assertThat(callbacks.get(1)).isInstanceOf(EvidenceVerifyCallback.class); + assertThat(callbacks.get(2)).isInstanceOf(CachedIdentityAuthorizeCallback.class); + assertThat(callbacks.get(3)).isInstanceOf(AuthenticationCompleteCallback.class); + assertTrue(((AuthenticationCompleteCallback) callbacks.get(3)).succeeded()); + + // Validate the evidence that was passed to the EvidenceVerifyCallback. + EvidenceVerifyCallback evidenceVerifyCallback = (EvidenceVerifyCallback) callbacks.get(1); + Evidence evidence = evidenceVerifyCallback.getEvidence(); + assertInstanceOf(JWTEvidence.class, evidence); + assertEquals("iamatoken", ((JWTEvidence) evidence).getToken()); + + // The identity should have been attached to the scope. + scopeWrapper.assertIdentityAttachedToScope(); + + // The session id should have been changed. + scopeWrapper.assertSessionIdChanged(); + } + + @DisplayName("The session ID should not be changed if disabled.") + @Test + void givenSessionIdChangeDisabled()throws HttpAuthenticationException { + givenSessionIdChangeEnabled(false); + + // Tell the scope to support attachments. + scopeWrapper.supportAttachments(); + + // Configure JWT. + requestWrapper.setHeader("Authorization", "Bearer iamatoken"); + + // The first authorization attempt with the identity cache should fail. + callbackHandler.actLikeFirstAuthorizationAttemptFails(); + // Act like the evidence passes verification, and the final principal is authorized. + callbackHandler.actLikeEvidenceIsVerified(); + callbackHandler.actLikeSecondAuthorizationAttemptSucceeds(); + + // Trigger the request. + evaluateRequest(); + + // Verify the request succeeded. + requestWrapper.assertRequestSucceeded(); + + // Verify four callbacks occurred, one for at attempt to use the empty identity cache, one for evidence verification, one for authorization of a + // principal, and one for authentication complete with a success status. + List callbacks = callbackHandler.getCapturedCallbacks(); + assertThat(callbacks).hasSize(4); + assertThat(callbacks.get(0)).isInstanceOf(CachedIdentityAuthorizeCallback.class); + assertThat(callbacks.get(1)).isInstanceOf(EvidenceVerifyCallback.class); + assertThat(callbacks.get(2)).isInstanceOf(CachedIdentityAuthorizeCallback.class); + assertThat(callbacks.get(3)).isInstanceOf(AuthenticationCompleteCallback.class); + assertTrue(((AuthenticationCompleteCallback) callbacks.get(3)).succeeded()); + + // Validate the evidence that was passed to the EvidenceVerifyCallback. + EvidenceVerifyCallback evidenceVerifyCallback = (EvidenceVerifyCallback) callbacks.get(1); + Evidence evidence = evidenceVerifyCallback.getEvidence(); + assertInstanceOf(JWTEvidence.class, evidence); + assertEquals("iamatoken", ((JWTEvidence) evidence).getToken()); + + // The identity should have been attached to the scope. + scopeWrapper.assertIdentityAttachedToScope(); + + // The session id should not have been changed. + scopeWrapper.assertSessionIdNotChanged(); + } + } + + /** + * Configures whether the mechanism should support identity restoration from the request's session scope. + */ + private void givenIdentityRestorationEnabled(boolean enabled) { + mechanismProperties.put(DatawaveHttpAuthenticationMechanism.Config.OPTION_ENABLE_RESTORE_IDENTITY, String.valueOf(enabled)); + } + + /** + * Configures whether the mechanism should support changing the session ID after attaching a cached identity to the session scope. + */ + private void givenSessionIdChangeEnabled(boolean enabled) { + mechanismProperties.put(DatawaveHttpAuthenticationMechanism.Config.OPTION_ENABLE_SESSION_ID_CHANGE, String.valueOf(enabled)); + } + + /** + * Creates the mechanism and evaluates the mock request. + */ + protected void evaluateRequest() throws HttpAuthenticationException { + mechanism = new DatawaveHttpAuthenticationMechanism(mechanismProperties, callbackHandler); + mechanism.evaluateRequest(requestWrapper.request); + } + + /** + * A mock implementation of {@link CallbackHandler} that allows us to handle and set attributes in {@link Callback} during the request evaluation. + */ + private static class MockCallbackHandler implements CallbackHandler { + // The callbacks passed to this handler. + private final List capturedCallbacks = new ArrayList<>(); + + // Whether to act like evidence verification succeeded. + private boolean actLikeEvidenceIsVerified; + + // Whether to act like caller authorization succeeded. In the case of supporting cached identities, the handler may receive up to two separate + // authorization callbacks. + private final boolean[] actLikeCallerIsAuthorized = {false, false}; + // The number of authorization attempts we've seen. Corresponds to the index in the actLikeCallerIsAuthorized array. + private int authorizationCallbackAttempts = 0; + + + public void actLikeEvidenceIsVerified() { + this.actLikeEvidenceIsVerified = true; + } + + public void actLikeEvidenceIsNotVerified() { + this.actLikeEvidenceIsVerified = false; + } + + public void actLikeFirstAuthorizationAttemptSucceeds() { + this.actLikeCallerIsAuthorized[0] = true; + } + + public void actLikeFirstAuthorizationAttemptFails() { + this.actLikeCallerIsAuthorized[0] = false; + } + + public void actLikeSecondAuthorizationAttemptSucceeds() { + this.actLikeCallerIsAuthorized[1] = true; + } + + @Override + public void handle(Callback[] callbacks) { + Callback callback = callbacks[0]; + capturedCallbacks.add(callback); + if (callback instanceof EvidenceVerifyCallback) { + handleEvidenceVerifyCallback((EvidenceVerifyCallback) callback); + } else if (callback instanceof CachedIdentityAuthorizeCallback) { + handleCachedIdentityAuthorizeCallback((CachedIdentityAuthorizeCallback) callback); + } else if (callback instanceof PrincipalAuthorizeCallback) { + handlePrincipalAuthorizeCallback((PrincipalAuthorizeCallback) callback); + } else if (!(callback instanceof AuthenticationCompleteCallback)) { + throw new IllegalStateException("Unknown callback type: " + callback.getClass().getName()); + } + } + + private void handleEvidenceVerifyCallback(EvidenceVerifyCallback callback) { + if(actLikeEvidenceIsVerified) { + callback.getEvidence().setDecodedPrincipal(new DatawavePrincipal("principal")); + callback.setVerified(true); + } + } + + private void handleCachedIdentityAuthorizeCallback(CachedIdentityAuthorizeCallback callback) { + if(actLikeCallerIsAuthorized[authorizationCallbackAttempts]){ + callback.setSecurityDomain(securityDomain); + SecurityIdentity identity = callback.getIdentity(); + if(identity == null) { + identity = securityDomain.createAdHocIdentity("test"); + } + callback.setAuthorized(identity); + } + authorizationCallbackAttempts++; + } + + private void handlePrincipalAuthorizeCallback(PrincipalAuthorizeCallback callback) { + callback.setAuthorized(actLikeCallerIsAuthorized[authorizationCallbackAttempts]); + authorizationCallbackAttempts++; + } + + public List getCapturedCallbacks() { + return capturedCallbacks; + } + + } + + /** + * Wrapper around a mock {@link HttpServerRequest} that allows us to configure expected behavior and assert method calls. + */ + private static class HttpServerRequestWrapper { + + private final HttpServerRequest request; + + public HttpServerRequestWrapper() { + this.request = mock(HttpServerRequest.class); + } + + public void setSessionScope(HttpScope scope) { + when(request.getScope(Scope.SESSION)).thenReturn(scope); + } + + public void setHeader(String header, String... values) { + when(request.getRequestHeaderValues(header)).thenReturn(asList(values)); + } + + public void setCertificate(X509Certificate certificate) { + SSLSession session = mock(SSLSession.class); + when(request.getSSLSession()).thenReturn(session); + Certificate[] peerCertificates = new Certificate[] {certificate}; + when(request.getPeerCertificates()).thenReturn(peerCertificates); + } + + public void assertRequestSucceeded() { + verify(request).authenticationComplete(isNull(), any()); + } + + public void assertRequestFailed() { + verify(request).authenticationFailed(any()); + } + } + + /** + * Wrapper around a mock {@link HttpScope} that allows us to configure expected behavior and assert method calls. + */ + private static class HttpScopeHandler { + + private final HttpScope scope; + private final Map attachments = new HashMap<>(); + private boolean exists; + + public HttpScopeHandler(HttpScope scope) { + this.scope = scope; + + // When the method HttpScope.exists() is called, return the value of exists. + doAnswer(invocation -> exists).when(scope).exists(); + + // When the method HttpScope.create() is called, set exists to true. + doAnswer(invocation -> { + exists = true; + return null; + }).when(scope).create(); + + // When the method HttpScope.setAttachment() is called, set the mapping in the attachment map. + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + String key = (String) args[0]; + Object value = args[1]; + attachments.put(key, value); + return null; + }).when(scope).setAttachment(any(), any()); + + // When the method HttpScope.getAttachment() is called, return the mapping from the attachment map. + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + String key = (String) args[0]; + return attachments.get(key); + }).when(scope).getAttachment(any()); + } + + public void setExists(boolean exists) { + this.exists = exists; + } + + public void doNotSupportAttachments() { + when(scope.supportsAttachments()).thenReturn(false); + } + + public void supportAttachments() { + when(scope.supportsAttachments()).thenReturn(true); + } + + public void setCachedIdentity(CachedIdentity identity){ + attachments.put(DatawaveHttpAuthenticationMechanism.CACHED_IDENTITY_KEY, identity); + } + + public void assertNothingAttachedToScope() { + verify(scope, never()).setAttachment(any(), any()); + } + + public void assertIdentityAttachedToScope() { + verify(scope, atLeastOnce()).setAttachment(eq(DatawaveHttpAuthenticationMechanism.CACHED_IDENTITY_KEY), isA(CachedIdentity.class)); + } + + public void assertSessionIdChanged() { + verify(scope, atMostOnce()).changeID(); + } + + public void assertSessionIdNotChanged() { + verify(scope, never()).changeID(); + } + } +} diff --git a/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/cache/DatawaveRealmIdentityCacheTest.java b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/cache/DatawaveRealmIdentityCacheTest.java new file mode 100644 index 00000000000..4597096f9be --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/cache/DatawaveRealmIdentityCacheTest.java @@ -0,0 +1,433 @@ +package datawave.security.cache; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.security.Principal; +import java.security.spec.AlgorithmParameterSpec; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.wildfly.security.auth.SupportLevel; +import org.wildfly.security.auth.principal.NamePrincipal; +import org.wildfly.security.auth.server.RealmIdentity; +import org.wildfly.security.credential.Credential; +import org.wildfly.security.evidence.Evidence; + +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; + +import datawave.security.authorization.DatawavePrincipal; +import datawave.security.authorization.DatawaveUser; +import datawave.security.authorization.SubjectIssuerDNPair; + +class DatawaveRealmIdentityCacheTest { + + private static final String TEST_ISSUER = "cn=testissuer"; + private static final Set auths = Set.of("A", "B", "C"); + private static final Set roles = Set.of("Administrator", "InternalUser"); + private static final Multimap rolesToAuths = ImmutableMultimap.of("Administrator", "A", "Administrator", "B", "Administrator", "C", + "InternalUser", "A"); + + /** + * Verify {@link DatawaveRealmIdentityCache#put(Principal, RealmIdentity)} throws an exception given a non {@link DatawavePrincipal} key. + */ + @Test + void testPutGivenNonDatawavePrincipalKey() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + + // @formatter:off + assertThatThrownBy(() -> cache.put(new NamePrincipal("user"), new SimpleNameRealmIdentity("user"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("principal must be of type datawave.security.authorization.DatawavePrincipal"); + // @formatter:on + } + + /** + * Verify that {@link DatawaveRealmIdentityCache#get(Principal)} given a domain principal with a match returns the match. + */ + @Test + void testGetByDomainPrincipal() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + + DatawavePrincipal principal = new DatawavePrincipal("datawaveUser"); + RealmIdentity identity = new SimpleNameRealmIdentity("realmUser"); + cache.put(principal, identity); + + assertThat(cache.get(principal)).isEqualTo(identity); + } + + /** + * Verify that {@link DatawaveRealmIdentityCache#get(Principal)} given a realm principal with a match returns the match. + */ + @Test + void testGetByRealmPrincipal() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + + DatawavePrincipal principal = new DatawavePrincipal("datawaveUser"); + RealmIdentity identity = new SimpleNameRealmIdentity("realmUser"); + cache.put(principal, identity); + + assertThat(cache.get(identity.getRealmIdentityPrincipal())).isEqualTo(identity); + } + + /** + * Verify that {@link DatawaveRealmIdentityCache#remove(Principal)} given a domain principal with a single match removes the match. + */ + @Test + void testRemoveByDomainPrincipal() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + + DatawavePrincipal principal = new DatawavePrincipal("datawaveUser"); + RealmIdentity identity = new SimpleNameRealmIdentity("realmUser"); + + cache.put(principal, identity); + assertThat(cache.get(principal)).isEqualTo(identity); + + cache.remove(principal); + + assertThat(cache.get(principal)).isNull(); + } + + /** + * Verify that {@link DatawaveRealmIdentityCache#remove(Principal)} given a domain principal with cross-matches removes all matches. + */ + @Test + void testRemoveByDomainPrincipalWithCrossMatches() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + + DatawavePrincipal principal1 = new DatawavePrincipal("datawaveUser"); + DatawavePrincipal principal2 = new DatawavePrincipal("datawaveUser2"); + DatawavePrincipal principal3 = new DatawavePrincipal("datawaveUser3"); + + RealmIdentity identity1 = new SimpleNameRealmIdentity("realmUser"); + RealmIdentity identity3 = new SimpleNameRealmIdentity("realmUser3"); + + cache.put(principal1, identity1); + cache.put(principal2, identity1); // Add a cross-match between principal 1 and principal 2. + cache.put(principal3, identity3); + + cache.remove(principal1); + + assertThat(cache.get(principal1)).isNull(); + assertThat(cache.get(principal2)).isNull(); // Principal 2 should also have been removed. + assertThat(cache.get(principal3)).isNotNull(); + } + + /** + * Verify that {@link DatawaveRealmIdentityCache#remove(Principal)} given a realm principal with a single match removes the match. + */ + @Test + void testRemoveByRealmPrincipal() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + + DatawavePrincipal principal = new DatawavePrincipal("datawaveUser"); + RealmIdentity identity = new SimpleNameRealmIdentity("realmUser"); + cache.put(principal, identity); + + Principal realmPrincipal = identity.getRealmIdentityPrincipal(); + assertThat(cache.get(realmPrincipal)).isEqualTo(identity); + + cache.remove(realmPrincipal); + + assertThat(cache.get(realmPrincipal)).isNull(); + } + + /** + * Verify that {@link DatawaveRealmIdentityCache#remove(Principal)} given a realm principal with cross-matches removes all matches. + */ + @Test + void testRemoveByRealmPrincipalWithCrossMatches() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + + DatawavePrincipal principal1 = new DatawavePrincipal("datawaveUser"); + DatawavePrincipal principal2 = new DatawavePrincipal("datawaveUser2"); + DatawavePrincipal principal3 = new DatawavePrincipal("datawaveUser3"); + + RealmIdentity identity1 = new SimpleNameRealmIdentity("realmUser"); + RealmIdentity identity3 = new SimpleNameRealmIdentity("realmUser3"); + + cache.put(principal1, identity1); + cache.put(principal2, identity1); // Add a cross-match between principal 1 and principal 2. + cache.put(principal3, identity3); + + cache.remove(identity1.getRealmIdentityPrincipal()); + + assertThat(cache.get(principal1)).isNull(); + assertThat(cache.get(principal2)).isNull(); // Principal 2 should also have been removed. + assertThat(cache.get(principal3)).isNotNull(); + } + + /** + * Verify that {@link DatawaveRealmIdentityCache#clear()} clears the cache. + */ + @Test + void testClearCache() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + + DatawavePrincipal principal1 = new DatawavePrincipal("datawaveUser"); + DatawavePrincipal principal2 = new DatawavePrincipal("datawaveUser2"); + DatawavePrincipal principal3 = new DatawavePrincipal("datawaveUser3"); + + RealmIdentity identity1 = new SimpleNameRealmIdentity("realmUser"); + RealmIdentity identity2 = new SimpleNameRealmIdentity("realmUser2"); + RealmIdentity identity3 = new SimpleNameRealmIdentity("realmUser3"); + + cache.put(principal1, identity1); + cache.put(principal2, identity2); + cache.put(principal3, identity3); + + cache.clear(); + + assertThat(cache.get(principal1)).isNull(); + assertThat(cache.get(principal2)).isNull(); + assertThat(cache.get(principal3)).isNull(); + } + + /** + * Verify that {@link DatawaveRealmIdentityCache#getUsers()} returns an empty set when the cache is empty. + */ + @Test + void testGetUsersGivenEmptyCache() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + assertTrue(cache.getUsers().isEmpty()); + } + + /** + * Verify that {@link DatawaveRealmIdentityCache#getUsers()} returns all users from the domain principals in the cache. + */ + @Test + void testGetUsers() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + + DatawavePrincipal principal1 = createPrincipal(createUser("cn=user1"), createUser("cn=proxyuser1")); + DatawavePrincipal principal2 = createPrincipal(createUser("cn=user2"), createUser("cn=proxyuser2")); + DatawavePrincipal principal3 = createPrincipal(createUser("cn=user3"), createUser("cn=proxyuser3")); + + cache.put(principal1, new SimpleNameRealmIdentity("realmUser1")); + cache.put(principal2, new SimpleNameRealmIdentity("realmUser2")); + cache.put(principal3, new SimpleNameRealmIdentity("realmUser3")); + + Set users = cache.getUsers(); + assertEquals(6, users.size()); + assertTrue(users.containsAll(principal1.getProxiedUsers())); + assertTrue(users.containsAll(principal2.getProxiedUsers())); + assertTrue(users.containsAll(principal3.getProxiedUsers())); + } + + /** + * Verify that {@link DatawaveRealmIdentityCache#getUsersWhereNameContains(String)} returns an empty set when the cache is empty. + */ + @Test + void testGetUsersWhereNameContainsGivenEmptyCache() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + assertTrue(cache.getUsersWhereNameContains("a").isEmpty()); + } + + /** + * Verify that {@link DatawaveRealmIdentityCache#getUsersWhereNameContains(String)} returns an empty set when there are no matches. + */ + @Test + void testGetUsersWhereNameContainsGivenNoMatches() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + + DatawavePrincipal principal1 = createPrincipal(createUser("cn=user1"), createUser("cn=proxyuser1")); + DatawavePrincipal principal2 = createPrincipal(createUser("cn=user2"), createUser("cn=proxyuser2")); + DatawavePrincipal principal3 = createPrincipal(createUser("cn=user3"), createUser("cn=proxyuser3")); + + cache.put(principal1, new SimpleNameRealmIdentity("realmUser1")); + cache.put(principal2, new SimpleNameRealmIdentity("realmUser2")); + cache.put(principal3, new SimpleNameRealmIdentity("realmUser3")); + + assertTrue(cache.getUsersWhereNameContains("a").isEmpty()); + } + + /** + * Verify that {@link DatawaveRealmIdentityCache#getUsersWhereNameContains(String)} returns matches. + */ + @Test + void testGetUsersWhereNameContainsGivenMatches() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + + DatawaveUser userA = createUser("cn=userA"); + DatawaveUser proxyUserA = createUser("cn=proxyUserA"); + + DatawavePrincipal principal1 = createPrincipal(userA, createUser("cn=proxyuser1")); + DatawavePrincipal principal2 = createPrincipal(createUser("cn=user2"), proxyUserA); + DatawavePrincipal principal3 = createPrincipal(createUser("cn=user3"), createUser("cn=proxyuser3")); + + cache.put(principal1, new SimpleNameRealmIdentity("realmUser1")); + cache.put(principal2, new SimpleNameRealmIdentity("realmUser2")); + cache.put(principal3, new SimpleNameRealmIdentity("realmUser3")); + + Set matchingUsers = cache.getUsersWhereNameContains("a"); + assertEquals(2, matchingUsers.size()); + assertTrue(matchingUsers.contains(userA)); + assertTrue(matchingUsers.contains(proxyUserA)); + } + + /** + * Verify {@link DatawaveRealmIdentityCache#getUserWithName(String)} returns null when the cache is empty. + */ + @Test + void testGetUserWithNameGivenEmptyCache() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + assertNull(cache.getUserWithName("cn=user1")); + } + + /** + * Verify {@link DatawaveRealmIdentityCache#getUserWithName(String)} returns null when there is no match. + */ + @Test + void testGetUserWithNameGivenNoMatch() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + + DatawavePrincipal principal1 = createPrincipal(createUser("cn=user1"), createUser("cn=proxyuser1")); + DatawavePrincipal principal2 = createPrincipal(createUser("cn=user2"), createUser("cn=proxyuser2")); + DatawavePrincipal principal3 = createPrincipal(createUser("cn=user3"), createUser("cn=proxyuser3")); + + cache.put(principal1, new SimpleNameRealmIdentity("realmUser1")); + cache.put(principal2, new SimpleNameRealmIdentity("realmUser2")); + cache.put(principal3, new SimpleNameRealmIdentity("realmUser3")); + + assertNull(cache.getUserWithName("cn=user4")); + } + + /** + * Verify {@link DatawaveRealmIdentityCache#getUserWithName(String)} returns a user when there is no match. + */ + @Test + void testGetUserWithNameGivenMatch() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + + DatawavePrincipal principal1 = createPrincipal(createUser("cn=user1"), createUser("cn=proxyuser1")); + DatawavePrincipal principal2 = createPrincipal(createUser("cn=user2"), createUser("cn=proxyuser2")); + DatawavePrincipal principal3 = createPrincipal(createUser("cn=user3"), createUser("cn=proxyuser3")); + + cache.put(principal1, new SimpleNameRealmIdentity("realmUser1")); + cache.put(principal2, new SimpleNameRealmIdentity("realmUser2")); + cache.put(principal3, new SimpleNameRealmIdentity("realmUser3")); + + assertNotNull(cache.getUserWithName("cn=user1<" + TEST_ISSUER + ">")); + } + + /** + * Verify that {@link DatawaveRealmIdentityCache#evictUsersWithName(String)} with no matches evicts no entries. + */ + @Test + void testEvictUserGivenNoMatch() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + + DatawavePrincipal principal1 = createPrincipal(createUser("cn=user1"), createUser("cn=proxyuser1")); + DatawavePrincipal principal2 = createPrincipal(createUser("cn=user2"), createUser("cn=proxyuser2")); + DatawavePrincipal principal3 = createPrincipal(createUser("cn=user3"), createUser("cn=proxyuser3")); + + cache.put(principal1, new SimpleNameRealmIdentity("realmUser1")); + cache.put(principal2, new SimpleNameRealmIdentity("realmUser2")); + cache.put(principal3, new SimpleNameRealmIdentity("realmUser3")); + + cache.evictUsersWithName("cn=user4<" + TEST_ISSUER + ">"); + + assertNotNull(cache.get(principal1)); + assertNotNull(cache.get(principal2)); + assertNotNull(cache.get(principal3)); + } + + /** + * Verify that {@link DatawaveRealmIdentityCache#evictUsersWithName(String)} with matches evicts matching entries. + */ + @Test + void testEvictUserGivenMatches() { + DatawaveRealmIdentityCache cache = new DatawaveRealmIdentityCache(-1, -1); + + DatawavePrincipal principal1 = createPrincipal(createUser("cn=user1"), createUser("cn=proxyuser1")); + DatawavePrincipal principal2 = createPrincipal(createUser("cn=user2"), createUser("cn=proxyuser2")); + DatawavePrincipal principal3 = createPrincipal(createUser("cn=user3"), createUser("cn=proxyuser3")); + DatawavePrincipal principal4 = createPrincipal(createUser("cn=user4"), createUser("cn=proxyuser2")); + + cache.put(principal1, new SimpleNameRealmIdentity("realmUser1")); + cache.put(principal2, new SimpleNameRealmIdentity("realmUser2")); + cache.put(principal3, new SimpleNameRealmIdentity("realmUser3")); + cache.put(principal4, new SimpleNameRealmIdentity("realmUser4")); + + cache.evictUsersWithName("cn=proxyuser2<" + TEST_ISSUER + ">"); + + assertNotNull(cache.get(principal1)); + assertNotNull(cache.get(principal3)); + + assertNull(cache.get(principal2)); + assertNull(cache.get(principal4)); + } + + private DatawaveUser createUser(String subjectDn) { + SubjectIssuerDNPair dnPair = SubjectIssuerDNPair.of(subjectDn, TEST_ISSUER); + return new DatawaveUser(dnPair, DatawaveUser.UserType.USER, auths, roles, rolesToAuths, System.currentTimeMillis()); + } + + private DatawavePrincipal createPrincipal(DatawaveUser... users) { + return new DatawavePrincipal(List.of(users)); + } + + /** + * A simple {@link RealmIdentity} that returns a {@link NamePrincipal} as its realm identity principal. + */ + private static class SimpleNameRealmIdentity implements RealmIdentity { + + private final String name; + + public SimpleNameRealmIdentity(String name) { + this.name = name; + } + + @Override + public Principal getRealmIdentityPrincipal() { + return new NamePrincipal(name); + } + + @Override + public SupportLevel getCredentialAcquireSupport(Class credentialType, String algorithmName, + AlgorithmParameterSpec parameterSpec) { + return null; + } + + @Override + public C getCredential(Class credentialType) { + return null; + } + + @Override + public SupportLevel getEvidenceVerifySupport(Class evidenceType, String algorithmName) { + return null; + } + + @Override + public boolean verifyEvidence(Evidence evidence) { + return false; + } + + @Override + public boolean exists() { + return false; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + SimpleNameRealmIdentity that = (SimpleNameRealmIdentity) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + } +} diff --git a/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/cache/DatawaveUserCacheTest.java b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/cache/DatawaveUserCacheTest.java new file mode 100644 index 00000000000..fd0e9527c75 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/cache/DatawaveUserCacheTest.java @@ -0,0 +1,216 @@ +package datawave.security.cache; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; + +import datawave.security.authorization.DatawaveUser; +import datawave.security.authorization.SubjectIssuerDNPair; + +/** + * Tests for {@link DatawaveUserCache}. + */ +class DatawaveUserCacheTest { + + private static final String TEST_ISSUER = "cn=testissuer"; + private static final Set auths = Set.of("A", "B", "C"); + private static final Set roles = Set.of("Administrator", "InternalUser"); + private static final Multimap rolesToAuths = ImmutableMultimap.of("Administrator", "A", "Administrator", "B", "Administrator", "C", + "InternalUser", "A"); + + /** + * Verify putting and getting entries to/from the cache works as expected. + */ + @Test + void testPutAndGet() { + DatawaveUserCache cache = new DatawaveUserCache(-1, -1); + Set users = Set.of(createUser("cn=user"), createUser("cn=proxyUser")); + cache.put("abc", users); + assertEquals(users, cache.get("abc")); + } + + /** + * Test {@link DatawaveUserCache#getUsers()} given an empty cache. + */ + @Test + void testGetUsersWithEmptyCache() { + DatawaveUserCache cache = new DatawaveUserCache(-1, -1); + assertTrue(cache.getUsers().isEmpty()); + } + + /** + * Test {@link DatawaveUserCache#getUsers()} given a non-empty cache. + */ + @Test + void testGetUsers() { + DatawaveUserCache cache = new DatawaveUserCache(-1, -1); + + Set users1 = Set.of(createUser("cn=user"), createUser("cn=proxyUser1")); + cache.put("a", users1); + + Set users2 = Set.of(createUser("cn=user2"), createUser("cn=proxyUser2")); + cache.put("b", users2); + + Set users3 = Set.of(createUser("cn=user3"), createUser("cn=proxyUser3")); + cache.put("c", users3); + + Set allUsers = cache.getUsers(); + assertEquals(6, allUsers.size()); + assertTrue(allUsers.containsAll(users1)); + assertTrue(allUsers.containsAll(users2)); + assertTrue(allUsers.containsAll(users3)); + } + + /** + * Test {@link DatawaveUserCache#getUsersWhereNameContains(String)} given an empty cache. + */ + @Test + void testGetUsersWhereNameContainsGivenEmptyCache() { + DatawaveUserCache cache = new DatawaveUserCache(-1, -1); + assertTrue(cache.getUsersWhereNameContains("a").isEmpty()); + } + + /** + * Test {@link DatawaveUserCache#getUsersWhereNameContains(String)} given no matches. + */ + @Test + void testGetUsersWhereNameContainsGivenNoMatches() { + DatawaveUserCache cache = new DatawaveUserCache(-1, -1); + + cache.put("a", Set.of(createUser("cn=user1"), createUser("cn=proxyUser1"))); + cache.put("b", Set.of(createUser("cn=user2"), createUser("cn=proxyUser2"))); + cache.put("c", Set.of(createUser("cn=user3"), createUser("cn=proxyUser3"))); + + Set matchingUsers = cache.getUsersWhereNameContains("a"); + assertTrue(matchingUsers.isEmpty()); + } + + /** + * Test {@link DatawaveUserCache#getUsersWhereNameContains(String)} given matches. + */ + @Test + void testGetUsersWhereNameContainsGivenMatches() { + DatawaveUserCache cache = new DatawaveUserCache(-1, -1); + + DatawaveUser userA = createUser("cn=userA"); + DatawaveUser proxyUserA = createUser("cn=proxyUserA"); + + cache.put("a", Set.of(userA, createUser("cn=proxyUser1"))); + cache.put("b", Set.of(createUser("cn=user2"), proxyUserA)); + cache.put("c", Set.of(createUser("cn=user3"), createUser("cn=proxyUser3"))); + + Set matchingUsers = cache.getUsersWhereNameContains("a"); + assertEquals(2, matchingUsers.size()); + assertTrue(matchingUsers.contains(userA)); + assertTrue(matchingUsers.contains(proxyUserA)); + } + + /** + * Test {@link DatawaveUserCache#getUserWithName(String)} given an empty cache. + */ + @Test + void testGetUserWithNameGivenEmptyCache() { + DatawaveUserCache cache = new DatawaveUserCache(-1, -1); + assertNull(cache.getUserWithName("cn=user1")); + } + + /** + * Test {@link DatawaveUserCache#getUserWithName(String)} given no match. + */ + @Test + void testGetUserWithNameGivenNoMatch() { + DatawaveUserCache cache = new DatawaveUserCache(-1, -1); + + cache.put("a", Set.of(createUser("cn=user1"), createUser("cn=proxyUser1"))); + cache.put("b", Set.of(createUser("cn=user2"), createUser("cn=proxyUser2"))); + cache.put("c", Set.of(createUser("cn=user3"), createUser("cn=proxyUser3"))); + + assertNull(cache.getUserWithName("cn=user4<" + TEST_ISSUER + ">")); + } + + /** + * Test {@link DatawaveUserCache#getUserWithName(String)} given a match. + */ + @Test + void testGetUserWithNameGivenMatch() { + DatawaveUserCache cache = new DatawaveUserCache(-1, -1); + + cache.put("a", Set.of(createUser("cn=user1"), createUser("cn=proxyUser1"))); + cache.put("b", Set.of(createUser("cn=user2"), createUser("cn=proxyUser2"))); + cache.put("c", Set.of(createUser("cn=user3"), createUser("cn=proxyUser3"))); + cache.put("d", Set.of(createUser("cn=user4"), createUser("cn=proxyUser2"))); + + assertNotNull(cache.getUserWithName("cn=proxyuser2<" + TEST_ISSUER + ">")); + } + + /** + * Test {@link DatawaveUserCache#evictUsersWithName(String)} given no match. + */ + @Test + void testEvictUserWithNameGivenNoMatch() { + DatawaveUserCache cache = new DatawaveUserCache(-1, -1); + + cache.put("a", Set.of(createUser("cn=user1"), createUser("cn=proxyUser1"))); + cache.put("b", Set.of(createUser("cn=user2"), createUser("cn=proxyUser2"))); + cache.put("c", Set.of(createUser("cn=user3"), createUser("cn=proxyUser3"))); + + cache.evictUsersWithName("cn=proxyuser4<" + TEST_ISSUER + ">"); + assertNotNull(cache.get("a")); + assertNotNull(cache.get("b")); + assertNotNull(cache.get("c")); + } + + /** + * Test {@link DatawaveUserCache#evictUsersWithName(String)} given matches. + */ + @Test + void testEvictUserWithNameGivenMatch() { + DatawaveUserCache cache = new DatawaveUserCache(-1, -1); + + cache.put("a", Set.of(createUser("cn=user1"), createUser("cn=proxyUser1"))); + cache.put("b", Set.of(createUser("cn=user2"), createUser("cn=proxyUser2"))); + cache.put("c", Set.of(createUser("cn=user3"), createUser("cn=proxyUser3"))); + cache.put("d", Set.of(createUser("cn=user4"), createUser("cn=proxyUser2"))); + + cache.evictUsersWithName("cn=proxyuser2<" + TEST_ISSUER + ">"); + assertNotNull(cache.get("a")); + assertNotNull(cache.get("c")); + + assertNull(cache.get("b")); + assertNull(cache.get("d")); + } + + /** + * Test {@link DatawaveUserCache#clear()}. + */ + @Test + void testClear() { + DatawaveUserCache cache = new DatawaveUserCache(-1, -1); + + cache.put("a", Set.of(createUser("cn=user1"), createUser("cn=proxyUser1"))); + cache.put("b", Set.of(createUser("cn=user2"), createUser("cn=proxyUser2"))); + cache.put("c", Set.of(createUser("cn=user3"), createUser("cn=proxyUser3"))); + cache.put("d", Set.of(createUser("cn=user4"), createUser("cn=proxyUser2"))); + + cache.clear(); + + assertNull(cache.get("a")); + assertNull(cache.get("b")); + assertNull(cache.get("c")); + assertNull(cache.get("d")); + } + + private DatawaveUser createUser(String subjectDn) { + SubjectIssuerDNPair dnPair = SubjectIssuerDNPair.of(subjectDn, TEST_ISSUER); + return new DatawaveUser(dnPair, DatawaveUser.UserType.USER, auths, roles, rolesToAuths, System.currentTimeMillis()); + } + +} diff --git a/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/evidence/X509EvidenceValidatorTest.java b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/evidence/X509EvidenceValidatorTest.java new file mode 100644 index 00000000000..e32078a8d26 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/evidence/X509EvidenceValidatorTest.java @@ -0,0 +1,77 @@ +package datawave.security.evidence; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import datawave.security.authorization.SubjectIssuerDNPair; +import datawave.security.cert.X509CertificateVerifier; + +class X509EvidenceValidatorTest { + + private static X509Certificate certificate; + private static X509CertificateEvidence evidence; + + @BeforeAll + static void beforeAll() throws Exception { + Security.addProvider(new BouncyCastleProvider()); + + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "BC"); + generator.initialize(2048); + KeyPair keyPair = generator.generateKeyPair(); + + X500Name subjectDn = new X500Name("CN=certUser"); + X500Name issuerDn = new X500Name("CN=certIssuer"); + BigInteger serialNumber = BigInteger.valueOf(new SecureRandom().nextInt()); + Date validFrom = new Date(); + Date validTo = new Date(validFrom.getTime() + 365 * 25 * 60 * 60 * 1000L); + + X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(issuerDn, serialNumber, validFrom, validTo, subjectDn, keyPair.getPublic()); + ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(keyPair.getPrivate()); + certificate = new JcaX509CertificateConverter().setProvider("BC").getCertificate(builder.build(signer)); + + List entities = new ArrayList<>(); + entities.add(SubjectIssuerDNPair.of("cn=server", "cn=issuer")); + entities.add(SubjectIssuerDNPair.of("cn=certuser", "cn=certissuer")); + evidence = new X509CertificateEvidence("cn=server", entities, certificate); + } + + @Test + void testValidateGivenSuccess() { + X509CertificateVerifier verifier = Mockito.mock(X509CertificateVerifier.class); + when(verifier.verify(certificate, "CN=certIssuer", null, null)).thenReturn(true); + + X509EvidenceValidator validator = new X509EvidenceValidator(verifier, null, null); + assertTrue(validator.validate(evidence)); + } + + @Test + void testValidateGivenFailure() { + X509CertificateVerifier verifier = Mockito.mock(X509CertificateVerifier.class); + when(verifier.verify(certificate, "CN=certIssuer", null, null)).thenReturn(false); + + X509EvidenceValidator validator = new X509EvidenceValidator(verifier, null, null); + assertFalse(validator.validate(evidence)); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveEvidenceDecoderConfigTests.java b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveEvidenceDecoderConfigTests.java new file mode 100644 index 00000000000..4062dd20d34 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveEvidenceDecoderConfigTests.java @@ -0,0 +1,46 @@ +package datawave.security.realm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link DatawaveEvidenceDecoder.Config}. + */ +class DatawaveEvidenceDecoderConfigTests { + + /** + * Verify the default values from {@link DatawaveEvidenceDecoder.Config#fromMap(Map)}. + */ + @Test + void testDefaultValues() { + DatawaveEvidenceDecoder.Config config = DatawaveEvidenceDecoder.Config.fromMap(Map.of()); + assertFalse(config.isJwtEnabled()); + assertFalse(config.isTrustedHeadersEnabled()); + assertEquals(-1, config.getMaxCacheEntries()); + assertEquals(-1, config.getMaxCacheAge()); + } + + /** + * Verify non-default values for {@link DatawaveEvidenceDecoder.Config#fromMap(Map)}. + */ + @Test + void testNonDefaultValues() { + Map configMap = new HashMap<>(); + configMap.put(DatawaveEvidenceDecoder.Config.OPTION_JWT_ENABLED, "true"); + configMap.put(DatawaveEvidenceDecoder.Config.OPTION_TRUSTED_HEADERS_ENABLED, "true"); + configMap.put(DatawaveEvidenceDecoder.Config.OPTION_MAX_CACHE_ENTRIES, "100"); + configMap.put(DatawaveEvidenceDecoder.Config.OPTION_MAX_CACHE_AGE, "1000"); + + DatawaveEvidenceDecoder.Config config = DatawaveEvidenceDecoder.Config.fromMap(configMap); + assertTrue(config.isJwtEnabled()); + assertTrue(config.isTrustedHeadersEnabled()); + assertEquals(100L, config.getMaxCacheEntries()); + assertEquals(1000L, config.getMaxCacheAge()); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveEvidenceDecoderTest.java b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveEvidenceDecoderTest.java new file mode 100644 index 00000000000..67d48b8f2ea --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveEvidenceDecoderTest.java @@ -0,0 +1,192 @@ +package datawave.security.realm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.atMostOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.security.Principal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.wildfly.security.evidence.Evidence; +import org.wildfly.security.evidence.PasswordGuessEvidence; + +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; + +import datawave.security.authorization.DatawavePrincipal; +import datawave.security.authorization.DatawaveUser; +import datawave.security.authorization.SubjectIssuerDNPair; +import datawave.security.cache.ElytronCacheManager; +import datawave.security.evidence.JWTEvidence; +import datawave.security.evidence.TrustedHeaderEvidence; +import datawave.security.evidence.X509CertificateEvidence; +import datawave.security.system.SecurityEJBProvider; + +class DatawaveEvidenceDecoderTest { + + private static final String TEST_SUBJECT = "cn=testuser"; + private static final String TEST_ISSUER = "cn=testissuer"; + private static final Set auths = Set.of("A", "B", "C"); + private static final Set roles = Set.of("Administrator", "InternalUser"); + private static final Multimap rolesToAuths = ImmutableMultimap.of("Administrator", "A", "Administrator", "B", "Administrator", "C", + "InternalUser", "A"); + + private final Map configMap = new HashMap<>(); + private ElytronCacheManager cacheManager; + private SecurityEJBProvider securityEJBProvider; + private DatawaveUserProvider datawaveUserProvider; + private DatawaveEvidenceDecoder evidenceDecoder; + + @BeforeEach + void setUp() { + evidenceDecoder = null; + configMap.clear(); + cacheManager = mock(ElytronCacheManager.class); + securityEJBProvider = mock(SecurityEJBProvider.class); + when(securityEJBProvider.getElytronCacheManager()).thenReturn(cacheManager); + datawaveUserProvider = mock(DatawaveUserProvider.class); + } + + /** + * Verify that when {@link DatawaveEvidenceDecoder#getPrincipal(Evidence)} is called, initialization is complete and the user cache is added to the elytron + * cache manager exactly once. + */ + @Test + void verifyUserCacheAddedToElytronCacheManager() { + decode(new PasswordGuessEvidence("password".toCharArray())); + verify(cacheManager, atLeastOnce()).addCache(any()); + + // Verify the cache is not added to the manager again on subsequent calls. + decode(new PasswordGuessEvidence("password".toCharArray())); + decode(new PasswordGuessEvidence("password".toCharArray())); + verify(cacheManager, atMostOnce()).addCache(any()); + } + + /** + * Verify that {@link DatawaveEvidenceDecoder#getPrincipal(Evidence)} returns null given evidence that is not an instance of + * {@link datawave.security.evidence.DatawaveEvidence}. + */ + @Test + void testNonDatawaveEvidence() { + assertNull(decode(new PasswordGuessEvidence("password".toCharArray()))); + } + + /** + * Verify that {@link DatawaveEvidenceDecoder#getPrincipal(Evidence)} returns null given a {@link JWTEvidence} when JWT is disabled. + */ + @Test + void testJwtEvidenceGivenJwtDisabled() { + configMap.put(DatawaveEvidenceDecoder.Config.OPTION_JWT_ENABLED, "false"); + + assertNull(decode(new JWTEvidence("token"))); + } + + /** + * Verify that {@link DatawaveEvidenceDecoder#getPrincipal(Evidence)} returns a {@link DatawavePrincipal} given a {@link JWTEvidence} when JWT is enabled. + */ + @Test + void testJwtEvidenceGivenJwtEnabled() throws Exception { + configMap.put(DatawaveEvidenceDecoder.Config.OPTION_JWT_ENABLED, "true"); + + JWTEvidence evidence = new JWTEvidence("token"); + + when(datawaveUserProvider.getUsers(evidence)).thenReturn(Set.of(createUser())); + + DatawavePrincipal principal = (DatawavePrincipal) decode(evidence); + + assertEquals(TEST_SUBJECT + "<" + TEST_ISSUER + ">", principal.getName()); + } + + /** + * Verify that {@link DatawaveEvidenceDecoder#getPrincipal(Evidence)} returns null given a {@link TrustedHeaderEvidence} when trusted header authentication + * is disabled. + */ + @Test + void testTrustedHeaderEvidenceGivenTrustedHeadersDisabled() { + configMap.put(DatawaveEvidenceDecoder.Config.OPTION_TRUSTED_HEADERS_ENABLED, "false"); + + assertNull(decode(new TrustedHeaderEvidence(TEST_SUBJECT + "<" + TEST_ISSUER + ">", List.of(SubjectIssuerDNPair.of(TEST_SUBJECT, TEST_ISSUER))))); + } + + /** + * Verify that {@link DatawaveEvidenceDecoder#getPrincipal(Evidence)} returns a {@link DatawavePrincipal} given a {@link TrustedHeaderEvidence} when trusted + * header authentication is enabled. + */ + @Test + void testTrustedHeaderEvidenceGivenTrustedHeadersEnabled() throws Exception { + configMap.put(DatawaveEvidenceDecoder.Config.OPTION_TRUSTED_HEADERS_ENABLED, "true"); + + TrustedHeaderEvidence evidence = new TrustedHeaderEvidence(TEST_SUBJECT + "<" + TEST_ISSUER + ">", + List.of(SubjectIssuerDNPair.of(TEST_SUBJECT, TEST_ISSUER))); + + when(datawaveUserProvider.getUsers(evidence)).thenReturn(Set.of(createUser())); + + DatawavePrincipal principal = (DatawavePrincipal) decode(evidence); + + assertEquals(TEST_SUBJECT + "<" + TEST_ISSUER + ">", principal.getName()); + } + + /** + * Verify that {@link DatawaveEvidenceDecoder#getPrincipal(Evidence)} returns a {@link DatawavePrincipal} given a {@link X509CertificateEvidence} when + * trusted header authentication is enabled. + */ + @Test + void testX509CertificateEvidence() throws Exception { + X509CertificateEvidence evidence = new X509CertificateEvidence(TEST_SUBJECT + "<" + TEST_ISSUER + ">", + List.of(SubjectIssuerDNPair.of(TEST_SUBJECT, TEST_ISSUER)), null); + + when(datawaveUserProvider.getUsers(evidence)).thenReturn(Set.of(createUser())); + + DatawavePrincipal principal = (DatawavePrincipal) decode(evidence); + + assertEquals(TEST_SUBJECT + "<" + TEST_ISSUER + ">", principal.getName()); + } + + /** + * Verify that when {@link DatawaveEvidenceDecoder#getPrincipal(Evidence)} is called multiple times for the same evidence, that the user cache is used after + * the first call to fetch the users. + */ + @Test + void testCaching() throws Exception { + configMap.put(DatawaveEvidenceDecoder.Config.OPTION_JWT_ENABLED, "true"); + + JWTEvidence evidence = new JWTEvidence("token"); + + when(datawaveUserProvider.getUsers(evidence)).thenReturn(Set.of(createUser())); + + assertNotNull(decode(evidence)); + assertNotNull(decode(evidence)); + assertNotNull(decode(evidence)); + + // The users should have been fetch from the provider only once, and then the cache should have been used for the subsequent calls. + verify(datawaveUserProvider, atMostOnce()).getUsers(evidence); + } + + private Principal decode(Evidence evidence) { + // Initialize the decoder if needed. + if (evidenceDecoder == null) { + evidenceDecoder = new DatawaveEvidenceDecoder(); + evidenceDecoder.setDatawaveUserProvider(datawaveUserProvider); + evidenceDecoder.setSecurityEJBProvider(securityEJBProvider); + evidenceDecoder.initialize(configMap); + } + + return evidenceDecoder.getPrincipal(evidence); + } + + private DatawaveUser createUser() { + SubjectIssuerDNPair dnPair = SubjectIssuerDNPair.of(TEST_SUBJECT, TEST_ISSUER); + return new DatawaveUser(dnPair, DatawaveUser.UserType.USER, auths, roles, rolesToAuths, System.currentTimeMillis()); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveRoleDecoderConfigTest.java b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveRoleDecoderConfigTest.java new file mode 100644 index 00000000000..f77978cc420 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveRoleDecoderConfigTest.java @@ -0,0 +1,64 @@ +package datawave.security.realm; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link DatawaveRoleDecoder.Config}. + */ +class DatawaveRoleDecoderConfigTest { + + /** + * Verify the default values of {@link DatawaveRoleDecoder.Config#fromMap(Map)}. + */ + @Test + void testDefaultValues() { + DatawaveRoleDecoder.Config config = DatawaveRoleDecoder.Config.fromMap(Map.of()); + + assertThat(config.getRequiredRoles()).containsExactlyInAnyOrderElementsOf(DatawaveRoleDecoder.defaultRequiredRoles); + assertThat(config.getTerminalServerRoles()).containsExactlyInAnyOrderElementsOf(DatawaveRoleDecoder.defaultTerminalServerRoles); + assertFalse(config.hasAccessDeniedRole()); + assertNull(config.getAccessDeniedRole()); + } + + /** + * Verify that when no valid roles are given for required roles or terminal server roles, they are empty. + */ + @Test + void testEmptyRequiredRolesAndEmptyTerminalServerRoles() { + Map configMap = new HashMap<>(); + configMap.put(DatawaveRoleDecoder.Config.OPTION_REQUIRED_ROLES, ": : :"); + configMap.put(DatawaveRoleDecoder.Config.OPTION_ACCESS_DENIED_ROLE, "AccessDenied"); + configMap.put(DatawaveRoleDecoder.Config.OPTION_TERMINAL_SERVER_ROLES, ": : :"); + + DatawaveRoleDecoder.Config config = DatawaveRoleDecoder.Config.fromMap(configMap); + + assertTrue(config.getRequiredRoles().isEmpty()); + assertTrue(config.getTerminalServerRoles().isEmpty()); + assertThat(config.getAccessDeniedRole()).isEqualTo("AccessDenied"); + } + + /** + * Verify the values of {@link DatawaveRoleDecoder.Config#fromMap(Map)}. + */ + @Test + void testNonDefaultValues() { + Map configMap = new HashMap<>(); + configMap.put(DatawaveRoleDecoder.Config.OPTION_REQUIRED_ROLES, "RequiredA: : RequiredB:"); + configMap.put(DatawaveRoleDecoder.Config.OPTION_ACCESS_DENIED_ROLE, "AccessDenied"); + configMap.put(DatawaveRoleDecoder.Config.OPTION_TERMINAL_SERVER_ROLES, "TerminalServerA: : TerminalServerB:"); + + DatawaveRoleDecoder.Config config = DatawaveRoleDecoder.Config.fromMap(configMap); + + assertThat(config.getRequiredRoles()).containsExactlyInAnyOrder("RequiredA", "RequiredB"); + assertThat(config.getTerminalServerRoles()).containsExactlyInAnyOrder("TerminalServerA", "TerminalServerB"); + assertThat(config.getAccessDeniedRole()).isEqualTo("AccessDenied"); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveRoleDecoderTest.java b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveRoleDecoderTest.java new file mode 100644 index 00000000000..e1d728e00cd --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveRoleDecoderTest.java @@ -0,0 +1,149 @@ +package datawave.security.realm; + +import static datawave.security.realm.AttributeConstants.ATTRIBUTE_PRIMARY_USER_ROLES; +import static datawave.security.realm.AttributeConstants.ATTRIBUTE_PROXIED_USER_KEYS; +import static datawave.security.realm.AttributeConstants.ATTRIBUTE_TERMINAL_SERVER_ROLES; +import static datawave.security.realm.AttributeConstants.ROLE_AUTHORIZED_SERVER; +import static datawave.security.realm.AttributeConstants.ROLE_AUTHORIZED_USER; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.wildfly.security.authz.AuthorizationIdentity; +import org.wildfly.security.authz.MapAttributes; +import org.wildfly.security.authz.Roles; + +/** + * Tests for {@link DatawaveRoleDecoder}. + */ +class DatawaveRoleDecoderTest { + + private static final String ACCESS_DENIED_ROLE = "AccessDenied"; + + private DatawaveRoleDecoder decoder; + + @BeforeEach + void setUp() { + decoder = new DatawaveRoleDecoder(); + Map configMap = new HashMap<>(); + configMap.put(DatawaveRoleDecoder.Config.OPTION_ACCESS_DENIED_ROLE, ACCESS_DENIED_ROLE); + decoder.initialize(configMap); + } + + /** + * Verify that given an identity without the attribute {@link AttributeConstants#ATTRIBUTE_PRIMARY_USER_ROLES}, an empty role set is returned. + */ + @Test + void testDecodeRolesGivenNoPrimaryRolesAttribute() { + MapAttributes attributes = new MapAttributes(); + AuthorizationIdentity identity = AuthorizationIdentity.basicIdentity(attributes); + + Roles roles = decoder.decodeRoles(identity); + assertTrue(roles.isEmpty()); + } + + /** + * Verify that given an identity with the attribute {@link AttributeConstants#ATTRIBUTE_TERMINAL_SERVER_ROLES} that does not contain any of the required + * terminal server roles, an empty role set is returned. + */ + @Test + void testDecodeRolesGivenMissingTerminalServerRole() { + MapAttributes attributes = new MapAttributes(); + attributes.addLast(ATTRIBUTE_PRIMARY_USER_ROLES, ROLE_AUTHORIZED_USER); + attributes.addLast(ATTRIBUTE_TERMINAL_SERVER_ROLES, "Foo"); + + AuthorizationIdentity identity = AuthorizationIdentity.basicIdentity(attributes); + + Roles roles = decoder.decodeRoles(identity); + assertTrue(roles.isEmpty()); + } + + /** + * Verify that given an identity with a proxied user that has the access-denied role, that an empty role set is returned. + */ + @Test + void testDecodeRolesGivenAccessDeniedRole() { + MapAttributes attributes = new MapAttributes(); + attributes.addLast(ATTRIBUTE_PRIMARY_USER_ROLES, "Foo"); + attributes.addLast(ATTRIBUTE_PRIMARY_USER_ROLES, "Bar"); + attributes.addLast(ATTRIBUTE_PRIMARY_USER_ROLES, ROLE_AUTHORIZED_USER); + + attributes.addLast(ATTRIBUTE_TERMINAL_SERVER_ROLES, ROLE_AUTHORIZED_SERVER); + + attributes.addLast(ATTRIBUTE_PROXIED_USER_KEYS, "PROXIED_USER_1"); + attributes.addLast(ATTRIBUTE_PROXIED_USER_KEYS, "PROXIED_USER_2"); + attributes.addLast("PROXIED_USER_1", "Foo"); + attributes.addLast("PROXIED_USER_1", "Bar"); + attributes.addLast("PROXIED_USER_1", ROLE_AUTHORIZED_USER); + attributes.addLast("PROXIED_USER_2", "Foo"); + attributes.addLast("PROXIED_USER_2", ACCESS_DENIED_ROLE); + + AuthorizationIdentity identity = AuthorizationIdentity.basicIdentity(attributes); + + Roles roles = decoder.decodeRoles(identity); + assertTrue(roles.isEmpty()); + } + + /** + * Verify that given an identity with a proxied user's roles that does not contain any of the required roles, the final role set does not have any of the + * required roles in it. + */ + @Test + void testDecodeRolesGivenProxiedUserMissingRequiredRole() { + MapAttributes attributes = new MapAttributes(); + attributes.addLast(ATTRIBUTE_PRIMARY_USER_ROLES, "Foo"); + attributes.addLast(ATTRIBUTE_PRIMARY_USER_ROLES, "Bar"); + attributes.addLast(ATTRIBUTE_PRIMARY_USER_ROLES, ROLE_AUTHORIZED_USER); + + attributes.addLast(ATTRIBUTE_TERMINAL_SERVER_ROLES, ROLE_AUTHORIZED_SERVER); + + attributes.addLast(ATTRIBUTE_PROXIED_USER_KEYS, "PROXIED_USER_1"); + attributes.addLast(ATTRIBUTE_PROXIED_USER_KEYS, "PROXIED_USER_2"); + attributes.addLast("PROXIED_USER_1", "Foo"); + attributes.addLast("PROXIED_USER_1", "Bar"); + attributes.addLast("PROXIED_USER_1", ROLE_AUTHORIZED_USER); + attributes.addLast("PROXIED_USER_2", "Foo"); + + AuthorizationIdentity identity = AuthorizationIdentity.basicIdentity(attributes); + + Roles roles = decoder.decodeRoles(identity); + assertTrue(roles.containsAll(Set.of("Foo", "Bar"))); + assertFalse(roles.contains(ROLE_AUTHORIZED_USER)); + } + + /** + * Verify that given an identity with valid roles, a role set with all expected roles are returned. + */ + @Test + void testDecodeRolesGivenValidAttributes() { + MapAttributes attributes = new MapAttributes(); + attributes.addLast(ATTRIBUTE_PRIMARY_USER_ROLES, "Foo"); + attributes.addLast(ATTRIBUTE_PRIMARY_USER_ROLES, ROLE_AUTHORIZED_USER); + + attributes.addLast(ATTRIBUTE_TERMINAL_SERVER_ROLES, ROLE_AUTHORIZED_SERVER); + attributes.addLast(ATTRIBUTE_TERMINAL_SERVER_ROLES, "Bar"); + attributes.addLast(ATTRIBUTE_TERMINAL_SERVER_ROLES, "Hat"); + + attributes.addLast(ATTRIBUTE_PROXIED_USER_KEYS, "PROXIED_USER_1"); + attributes.addLast(ATTRIBUTE_PROXIED_USER_KEYS, "PROXIED_USER_2"); + + attributes.addLast("PROXIED_USER_1", "Foo"); + attributes.addLast("PROXIED_USER_1", ROLE_AUTHORIZED_USER); + + attributes.addLast("PROXIED_USER_2", "Bar"); + attributes.addLast("PROXIED_USER_2", "Hat"); + attributes.addLast("PROXIED_USER_2", ROLE_AUTHORIZED_SERVER); + + AuthorizationIdentity identity = AuthorizationIdentity.basicIdentity(attributes); + Roles roles = decoder.decodeRoles(identity); + + Set expectedRoles = Set.of(ROLE_AUTHORIZED_USER, "Foo"); + + assertTrue(roles.containsAll(expectedRoles)); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveSecurityRealmConfigTest.java b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveSecurityRealmConfigTest.java new file mode 100644 index 00000000000..80f9a9835e0 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveSecurityRealmConfigTest.java @@ -0,0 +1,50 @@ +package datawave.security.realm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import datawave.security.cert.DatawaveCertVerifier; + +/** + * Tests for {@link DatawaveSecurityRealm.Config}. + */ +public class DatawaveSecurityRealmConfigTest { + + /** + * Verify the default values returned from {@link DatawaveSecurityRealm.Config#fromMap(Map)}. + */ + @Test + void testDefaultValues() { + DatawaveSecurityRealm.Config config = DatawaveSecurityRealm.Config.fromMap(Map.of()); + assertNull(config.getCertVerifierClass()); + assertNull(config.getOscpLevel()); + assertEquals(-1L, config.getMaxCacheEntries()); + assertEquals(-1L, config.getMaxCacheAge()); + assertNull(config.getRolePropertiesPath()); + } + + /** + * Verify the configuration returned from {@link DatawaveSecurityRealm.Config#fromMap(Map)} given non-default values. + */ + @Test + void testNonDefaultValues() { + Map configMap = new HashMap<>(); + configMap.put(DatawaveSecurityRealm.Config.OPTION_CERT_VERIFIER, " " + DatawaveCertVerifier.class.getName() + " "); + configMap.put(DatawaveSecurityRealm.Config.OPTION_OSCP_LEVEL, " " + DatawaveCertVerifier.OcspLevel.OFF + " "); + configMap.put(DatawaveSecurityRealm.Config.OPTION_MAX_CACHE_ENTRIES, " 100 "); + configMap.put(DatawaveSecurityRealm.Config.OPTION_MAX_CACHE_AGE, " 1000 "); + configMap.put(DatawaveSecurityRealm.Config.OPTION_ROLE_PROPERTIES, " /path/to/roles.properties "); + + DatawaveSecurityRealm.Config config = DatawaveSecurityRealm.Config.fromMap(configMap); + assertEquals(DatawaveCertVerifier.class.getName(), config.getCertVerifierClass()); + assertEquals(DatawaveCertVerifier.OcspLevel.OFF.toString(), config.getOscpLevel()); + assertEquals(100L, config.getMaxCacheEntries()); + assertEquals(1000L, config.getMaxCacheAge()); + assertEquals("/path/to/roles.properties", config.getRolePropertiesPath()); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveSecurityRealmTest.java b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveSecurityRealmTest.java new file mode 100644 index 00000000000..561e3931a40 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveSecurityRealmTest.java @@ -0,0 +1,475 @@ +package datawave.security.realm; + +import static datawave.security.realm.AttributeConstants.ATTRIBUTE_USERNAME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atMostOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.security.auth.x500.X500Principal; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; +import org.wildfly.security.auth.principal.NamePrincipal; +import org.wildfly.security.auth.server.RealmIdentity; +import org.wildfly.security.authz.Attributes; +import org.wildfly.security.evidence.Evidence; +import org.wildfly.security.evidence.PasswordGuessEvidence; + +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; + +import datawave.security.authorization.DatawavePrincipal; +import datawave.security.authorization.DatawaveUser; +import datawave.security.authorization.SubjectIssuerDNPair; +import datawave.security.cache.ElytronCacheManager; +import datawave.security.cert.DatawaveCertVerifier; +import datawave.security.cert.SSLStores; +import datawave.security.cert.X509CertificateVerifier; +import datawave.security.evidence.TrustedHeaderEvidence; +import datawave.security.evidence.X509CertificateEvidence; +import datawave.security.system.SecurityEJBProvider; + +/** + * Tests for {@link DatawaveSecurityRealm}. + */ +class DatawaveSecurityRealmTest { + + private static final Set auths = Set.of("A", "B", "C"); + private static final String ISSUER_DN = "cn=testissuer"; + private static final String USER_1_PRINCIPAL_NAME = "cn=testuserprincipal<" + ISSUER_DN + ">"; + private static final String USER_1_EVIDENCE_NAME = "cn=testuserevidence<" + ISSUER_DN + ">"; + + @TempDir + static Path tempDir; + + static Path propertiesFile; + + private Properties roleProperties; + private final Map configMap = new HashMap<>(); + private ElytronCacheManager cacheManager; + private SecurityEJBProvider securityEJBProvider; + + @BeforeAll + static void beforeAll() throws IOException { + propertiesFile = Files.createFile(tempDir.resolve("roles.properties")); + } + + @BeforeEach + void setUp() { + configMap.clear(); + roleProperties = new Properties(); + securityEJBProvider = Mockito.mock(SecurityEJBProvider.class); + cacheManager = Mockito.mock(ElytronCacheManager.class); + SSLStores sslStores = Mockito.mock(SSLStores.class); + when(securityEJBProvider.getElytronCacheManager()).thenReturn(cacheManager); + when(securityEJBProvider.getSSLStores()).thenReturn(sslStores); + } + + /** + * Verify that when the security realm is configured without an evidence validator or a role properties file, that it does not load those after completing + * initialization. + */ + @Test + void testInitializationGivenNoValidatorOrRolePropertiesFile() throws Exception { + DatawaveSecurityRealm realm = createSecurityRealm(); + + // Call getRealmIdentity to trigger initialization. + realm.getRealmIdentity(new NamePrincipal("username")); + + // Verify the identity cache was added to the cache manager. + verify(cacheManager).addCache(any()); + + // Verify a certificate validator was not created. + assertNull(realm.getX509EvidenceValidator()); + + // Verify that no local roles were loaded. + assertTrue(realm.getLocalUserRoles().isEmpty()); + + // Verify the identity cache is only ever added to the cache manager once, even on subsequent calls. + realm.getRealmIdentity(new NamePrincipal("anotheruser")); + verify(cacheManager, atMostOnce()).addCache(any()); + } + + /** + * Verify that when the security realm is configured with an evidence validator and a role properties file, that it loads those after completing + * initialization. + */ + @Test + void testInitializationGivenValidatorAndRolePropertiesFile() throws Exception { + configMap.put(DatawaveSecurityRealm.Config.OPTION_CERT_VERIFIER, DatawaveCertVerifier.class.getName()); + configMap.put(DatawaveSecurityRealm.Config.OPTION_OSCP_LEVEL, "OFF"); + configMap.put(DatawaveSecurityRealm.Config.OPTION_ROLE_PROPERTIES, propertiesFile.toAbsolutePath().toString()); + + roleProperties.put(USER_1_PRINCIPAL_NAME, "LocalRoleA, , LocalRoleB, LocalRoleC ,"); + roleProperties.put(USER_1_EVIDENCE_NAME, "LocalRoleD, , LocalRoleE,"); + + DatawaveSecurityRealm realm = createSecurityRealm(); + + // Call getRealmIdentity to trigger initialization. + realm.getRealmIdentity(new NamePrincipal("username")); + + // Verify the identity cache was added to the cache manager. + verify(cacheManager).addCache(any()); + + // Verify a certificate validator was created. + assertNotNull(realm.getX509EvidenceValidator()); + + // Verify that local roles were loaded, and that only non-blank, trimmed roles were stored. + Multimap localUserRoles = realm.getLocalUserRoles(); + assertTrue(localUserRoles.get(USER_1_PRINCIPAL_NAME).containsAll(Set.of("LocalRoleA", "LocalRoleB", "LocalRoleC"))); + assertTrue(localUserRoles.get(USER_1_EVIDENCE_NAME).containsAll(Set.of("LocalRoleD", "LocalRoleE"))); + + // Verify the identity cache is only ever added to the cache manager once, even on subsequent calls. + realm.getRealmIdentity(new NamePrincipal("anotheruser")); + verify(cacheManager, atMostOnce()).addCache(any()); + } + + /** + * Verify that when {@link DatawaveSecurityRealm#getRealmIdentity(Principal)} is given a non-DatawavePrincipal, a non-existent identity is returned. + */ + @Test + void testGetRealmIdentityGivenNonDatawavePrincipal() throws Exception { + DatawaveSecurityRealm realm = createSecurityRealm(); + RealmIdentity identity = realm.getRealmIdentity(new NamePrincipal("name")); + assertFalse(identity.exists()); + } + + /** + * Verify that when {@link DatawaveSecurityRealm#getRealmIdentity(Principal)} is called for a principal that does not have a cached identity, a new identity + * is returned. + */ + @Test + void testCachingOfRealmIdentity() throws Exception { + DatawaveSecurityRealm realm = createSecurityRealm(); + DatawavePrincipal principal = new DatawavePrincipal(USER_1_PRINCIPAL_NAME); + RealmIdentity identity1 = realm.getRealmIdentity(principal); + RealmIdentity identity2 = realm.getRealmIdentity(principal); + assertSame(identity1, identity2); + } + + /** + * Verify that when a non {@link datawave.security.evidence.DatawaveEvidence} is supplied to {@link RealmIdentity#verifyEvidence(Evidence)}, false is + * returned. + */ + @Test + void testVerifyEvidenceGivenNonDatawaveEvidence() throws Exception { + DatawaveSecurityRealm realm = createSecurityRealm(); + DatawavePrincipal principal = new DatawavePrincipal(USER_1_PRINCIPAL_NAME); + RealmIdentity realmIdentity = realm.getRealmIdentity(principal); + + assertFalse(realmIdentity.verifyEvidence(new PasswordGuessEvidence("password".toCharArray()))); + assertTrue(realmIdentity.exists()); + } + + /** + * Verify that when no X509 certificate verifier is configured for the realm, {@link RealmIdentity#verifyEvidence(Evidence)} returns true when given a + * {@link X509CertificateEvidence}. + */ + @Test + void testVerifyEvidenceGivenX509CertificateEvidenceWithNoVerifierConfigured() throws Exception { + DatawaveSecurityRealm realm = createSecurityRealm(); + DatawavePrincipal principal = new DatawavePrincipal(USER_1_PRINCIPAL_NAME); + RealmIdentity realmIdentity = realm.getRealmIdentity(principal); + + assertTrue(realmIdentity.verifyEvidence(new X509CertificateEvidence(USER_1_PRINCIPAL_NAME, List.of(), null))); + assertTrue(realmIdentity.exists()); + } + + /** + * Verify that when a X509 certificate verifier is configured for the realm, {@link RealmIdentity#verifyEvidence(Evidence)} returns false when given a + * {@link X509CertificateEvidence} with an invalid certificate. + */ + @Test + void testVerifyEvidenceGivenX509CertificateEvidenceThatFailsVerification() throws Exception { + configMap.put(DatawaveSecurityRealm.Config.OPTION_CERT_VERIFIER, FailingX509Verifier.class.getName()); + + DatawaveSecurityRealm realm = createSecurityRealm(); + DatawavePrincipal principal = new DatawavePrincipal(USER_1_PRINCIPAL_NAME); + RealmIdentity realmIdentity = realm.getRealmIdentity(principal); + + assertFalse(realmIdentity.verifyEvidence(new X509CertificateEvidence(USER_1_PRINCIPAL_NAME, List.of(), null))); + assertTrue(realmIdentity.exists()); + } + + /** + * Verify that when a X509 certificate verifier is configured for the realm, {@link RealmIdentity#verifyEvidence(Evidence)} returns true when given a + * {@link X509CertificateEvidence} with a valid certificate. + */ + @Test + void testVerifyEvidenceGivenX509CertificateEvidenceThatPassesVerification() throws Exception { + configMap.put(DatawaveSecurityRealm.Config.OPTION_CERT_VERIFIER, PassingX509Verifier.class.getName()); + + DatawaveSecurityRealm realm = createSecurityRealm(); + DatawavePrincipal principal = new DatawavePrincipal(USER_1_PRINCIPAL_NAME); + RealmIdentity realmIdentity = realm.getRealmIdentity(principal); + + X509Certificate certificate = Mockito.mock(X509Certificate.class); + when(certificate.getIssuerX500Principal()).thenReturn(new X500Principal("CN=TestIssuer")); + + assertTrue(realmIdentity.verifyEvidence(new X509CertificateEvidence(USER_1_PRINCIPAL_NAME, List.of(), certificate))); + assertTrue(realmIdentity.exists()); + } + + /** + * Verify that when no local roles are found for the principal or evidence, they are not added to the primary user roles. + */ + @Test + void testAttributesWithNoLocalRolesForPrincipalAndNoLocalRolesForEvidence() throws Exception { + configMap.put(DatawaveSecurityRealm.Config.OPTION_ROLE_PROPERTIES, propertiesFile.toAbsolutePath().toString()); + + // Add local roles for some usernames. + roleProperties.put(USER_1_PRINCIPAL_NAME, "LocalRoleA, , LocalRoleB, LocalRoleC ,"); + roleProperties.put(USER_1_EVIDENCE_NAME, "LocalRoleD, , LocalRoleE,"); + + List users = new ArrayList<>(); + users.add(createUser("cn=otheruser", DatawaveUser.UserType.USER, "RoleA", "RoleB")); + users.add(createUser("cn=proxyserver1", DatawaveUser.UserType.SERVER, "RoleC", "RoleD")); + users.add(createUser("cn=proxyserver2", DatawaveUser.UserType.SERVER, "RoleE", "RoleF")); + + DatawavePrincipal principal = new DatawavePrincipal(users); + DatawaveSecurityRealm realm = createSecurityRealm(); + + // Verify the realm identity principal is a name principal of the datawave principal's name. + RealmIdentity identity = realm.getRealmIdentity(principal); + assertEquals(identity.getRealmIdentityPrincipal(), new NamePrincipal(principal.getName())); + + // Verify the initial attributes. + Attributes attributes = identity.getAttributes(); + assertEquals(principal.getName(), attributes.getLast(ATTRIBUTE_USERNAME)); + assertThat(attributes.get(AttributeConstants.ATTRIBUTE_PRIMARY_USER_ROLES)).containsExactlyInAnyOrder("RoleA", "RoleB"); + assertThat(attributes.get(AttributeConstants.ATTRIBUTE_TERMINAL_SERVER_ROLES)).containsExactlyInAnyOrder("RoleE", "RoleF"); + assertThat(attributes.get(AttributeConstants.ATTRIBUTE_PROXIED_USER_KEYS)).containsExactlyInAnyOrder("PROXIED_USER_0", "PROXIED_USER_1", + "PROXIED_USER_2"); + assertThat(attributes.get("PROXIED_USER_0")).containsExactlyInAnyOrder("RoleA", "RoleB"); + assertThat(attributes.get("PROXIED_USER_1")).containsExactlyInAnyOrder("RoleC", "RoleD"); + assertThat(attributes.get("PROXIED_USER_2")).containsExactlyInAnyOrder("RoleE", "RoleF"); + + // Pass in evidence where the evidence username will match against local roles loaded in via a properties. + TrustedHeaderEvidence trustedHeaderEvidence = new TrustedHeaderEvidence("cn=otheruser", List.of()); + identity.verifyEvidence(trustedHeaderEvidence); + + // Verify the attribute post-verification. It should be the same. + Attributes postVerificationAttributes = identity.getAttributes(); + assertEquals(principal.getName(), postVerificationAttributes.getLast(ATTRIBUTE_USERNAME)); + assertThat(postVerificationAttributes.get(AttributeConstants.ATTRIBUTE_PRIMARY_USER_ROLES)).containsExactlyInAnyOrder("RoleA", "RoleB"); + assertThat(postVerificationAttributes.get(AttributeConstants.ATTRIBUTE_TERMINAL_SERVER_ROLES)).containsExactlyInAnyOrder("RoleE", "RoleF"); + assertThat(postVerificationAttributes.get(AttributeConstants.ATTRIBUTE_PROXIED_USER_KEYS)).containsExactlyInAnyOrder("PROXIED_USER_0", "PROXIED_USER_1", + "PROXIED_USER_2"); + assertThat(postVerificationAttributes.get("PROXIED_USER_0")).containsExactlyInAnyOrder("RoleA", "RoleB"); + assertThat(postVerificationAttributes.get("PROXIED_USER_1")).containsExactlyInAnyOrder("RoleC", "RoleD"); + assertThat(postVerificationAttributes.get("PROXIED_USER_2")).containsExactlyInAnyOrder("RoleE", "RoleF"); + } + + /** + * Verify that when no local roles are found for the principal, they are not added to the primary user roles, but when local roles are found for evidence, + * they are added after verification. + */ + @Test + void testAttributesWithNoLocalRolesForPrincipalAndLocalRolesForEvidence() throws Exception { + configMap.put(DatawaveSecurityRealm.Config.OPTION_ROLE_PROPERTIES, propertiesFile.toAbsolutePath().toString()); + + // Add local roles for the evidence username. + roleProperties.put(USER_1_EVIDENCE_NAME, "LocalRoleD, , LocalRoleE,"); + + List users = new ArrayList<>(); + users.add(createUser("cn=otheruser", DatawaveUser.UserType.USER, "RoleA", "RoleB")); + users.add(createUser("cn=proxyserver1", DatawaveUser.UserType.SERVER, "RoleC", "RoleD")); + users.add(createUser("cn=proxyserver2", DatawaveUser.UserType.SERVER, "RoleE", "RoleF")); + + DatawavePrincipal principal = new DatawavePrincipal(users); + DatawaveSecurityRealm realm = createSecurityRealm(); + + // Verify the realm identity principal is a name principal of the datawave principal's name. + RealmIdentity identity = realm.getRealmIdentity(principal); + assertEquals(identity.getRealmIdentityPrincipal(), new NamePrincipal(principal.getName())); + + // Verify the initial attributes. + Attributes attributes = identity.getAttributes(); + assertEquals(principal.getName(), attributes.getLast(ATTRIBUTE_USERNAME)); + assertThat(attributes.get(AttributeConstants.ATTRIBUTE_PRIMARY_USER_ROLES)).containsExactlyInAnyOrder("RoleA", "RoleB"); + assertThat(attributes.get(AttributeConstants.ATTRIBUTE_TERMINAL_SERVER_ROLES)).containsExactlyInAnyOrder("RoleE", "RoleF"); + assertThat(attributes.get(AttributeConstants.ATTRIBUTE_PROXIED_USER_KEYS)).containsExactlyInAnyOrder("PROXIED_USER_0", "PROXIED_USER_1", + "PROXIED_USER_2"); + assertThat(attributes.get("PROXIED_USER_0")).containsExactlyInAnyOrder("RoleA", "RoleB"); + assertThat(attributes.get("PROXIED_USER_1")).containsExactlyInAnyOrder("RoleC", "RoleD"); + assertThat(attributes.get("PROXIED_USER_2")).containsExactlyInAnyOrder("RoleE", "RoleF"); + + // Pass in evidence where the evidence username will match against local roles loaded in via a properties. + TrustedHeaderEvidence trustedHeaderEvidence = new TrustedHeaderEvidence(USER_1_EVIDENCE_NAME, List.of()); + identity.verifyEvidence(trustedHeaderEvidence); + + // Verify the attribute post-verification. + Attributes postVerificationAttributes = identity.getAttributes(); + assertEquals(principal.getName(), postVerificationAttributes.getLast(ATTRIBUTE_USERNAME)); + // The primary user roles attribute should now also have the local roles matching the evidence username. + assertThat(postVerificationAttributes.get(AttributeConstants.ATTRIBUTE_PRIMARY_USER_ROLES)).containsExactlyInAnyOrder("RoleA", "RoleB", "LocalRoleE", + "LocalRoleD"); + assertThat(postVerificationAttributes.get(AttributeConstants.ATTRIBUTE_TERMINAL_SERVER_ROLES)).containsExactlyInAnyOrder("RoleE", "RoleF"); + assertThat(postVerificationAttributes.get(AttributeConstants.ATTRIBUTE_PROXIED_USER_KEYS)).containsExactlyInAnyOrder("PROXIED_USER_0", "PROXIED_USER_1", + "PROXIED_USER_2"); + assertThat(postVerificationAttributes.get("PROXIED_USER_0")).containsExactlyInAnyOrder("RoleA", "RoleB"); + assertThat(postVerificationAttributes.get("PROXIED_USER_1")).containsExactlyInAnyOrder("RoleC", "RoleD"); + assertThat(postVerificationAttributes.get("PROXIED_USER_2")).containsExactlyInAnyOrder("RoleE", "RoleF"); + } + + /** + * Verify that when local roles are found for the principal, they are added to the primary user roles. Additionally, when no local roles are found for + * evidence, the attributes are not modified post-verification. + */ + @Test + void testAttributesWithLocalRolesForPrincipalAndNoLocalRolesForEvidence() throws Exception { + configMap.put(DatawaveSecurityRealm.Config.OPTION_ROLE_PROPERTIES, propertiesFile.toAbsolutePath().toString()); + + List users = new ArrayList<>(); + users.add(createUser(USER_1_PRINCIPAL_NAME, DatawaveUser.UserType.USER, "RoleA", "RoleB")); + users.add(createUser("cn=proxyserver1", DatawaveUser.UserType.SERVER, "RoleC", "RoleD")); + users.add(createUser("cn=proxyserver2", DatawaveUser.UserType.SERVER, "RoleE", "RoleF")); + + DatawavePrincipal principal = new DatawavePrincipal(users); + + // Add local roles for the principal name. + roleProperties.put(principal.getName(), "LocalRoleA, , LocalRoleB, LocalRoleC ,"); + + DatawaveSecurityRealm realm = createSecurityRealm(); + + // Verify the realm identity principal is a name principal of the datawave principal's name. + RealmIdentity identity = realm.getRealmIdentity(principal); + assertEquals(identity.getRealmIdentityPrincipal(), new NamePrincipal(principal.getName())); + + // Verify the initial attributes. + Attributes attributes = identity.getAttributes(); + assertEquals(principal.getName(), attributes.getLast(ATTRIBUTE_USERNAME)); + assertThat(attributes.get(AttributeConstants.ATTRIBUTE_PRIMARY_USER_ROLES)).containsExactlyInAnyOrder("RoleA", "RoleB", "LocalRoleA", "LocalRoleB", + "LocalRoleC"); + assertThat(attributes.get(AttributeConstants.ATTRIBUTE_TERMINAL_SERVER_ROLES)).containsExactlyInAnyOrder("RoleE", "RoleF"); + assertThat(attributes.get(AttributeConstants.ATTRIBUTE_PROXIED_USER_KEYS)).containsExactlyInAnyOrder("PROXIED_USER_0", "PROXIED_USER_1", + "PROXIED_USER_2"); + assertThat(attributes.get("PROXIED_USER_0")).containsExactlyInAnyOrder("RoleA", "RoleB"); + assertThat(attributes.get("PROXIED_USER_1")).containsExactlyInAnyOrder("RoleC", "RoleD"); + assertThat(attributes.get("PROXIED_USER_2")).containsExactlyInAnyOrder("RoleE", "RoleF"); + + // Pass in evidence where the evidence username will not match against local roles loaded in via a properties. + TrustedHeaderEvidence trustedHeaderEvidence = new TrustedHeaderEvidence("cn=otheruser", List.of()); + identity.verifyEvidence(trustedHeaderEvidence); + + // Verify the attributes post-verification. They should be unchanged. + Attributes postVerificationAttributes = identity.getAttributes(); + assertEquals(principal.getName(), postVerificationAttributes.getLast(ATTRIBUTE_USERNAME)); + assertThat(postVerificationAttributes.get(AttributeConstants.ATTRIBUTE_PRIMARY_USER_ROLES)).containsExactlyInAnyOrder("RoleA", "RoleB", "LocalRoleA", + "LocalRoleB", "LocalRoleC"); + assertThat(postVerificationAttributes.get(AttributeConstants.ATTRIBUTE_TERMINAL_SERVER_ROLES)).containsExactlyInAnyOrder("RoleE", "RoleF"); + assertThat(postVerificationAttributes.get(AttributeConstants.ATTRIBUTE_PROXIED_USER_KEYS)).containsExactlyInAnyOrder("PROXIED_USER_0", "PROXIED_USER_1", + "PROXIED_USER_2"); + assertThat(postVerificationAttributes.get("PROXIED_USER_0")).containsExactlyInAnyOrder("RoleA", "RoleB"); + assertThat(postVerificationAttributes.get("PROXIED_USER_1")).containsExactlyInAnyOrder("RoleC", "RoleD"); + assertThat(postVerificationAttributes.get("PROXIED_USER_2")).containsExactlyInAnyOrder("RoleE", "RoleF"); + } + + /** + * Verify that when local roles are found for the principal, they are added to the primary user roles. Additionally, when local roles are found for the + * evidence, they are added post-verification. + */ + @Test + void testAttributesWithLocalRolesForPrincipalAndLocalRolesForEvidence() throws Exception { + configMap.put(DatawaveSecurityRealm.Config.OPTION_ROLE_PROPERTIES, propertiesFile.toAbsolutePath().toString()); + + List users = new ArrayList<>(); + users.add(createUser(USER_1_PRINCIPAL_NAME, DatawaveUser.UserType.USER, "RoleA", "RoleB")); + users.add(createUser("cn=proxyserver1", DatawaveUser.UserType.SERVER, "RoleC", "RoleD")); + users.add(createUser("cn=proxyserver2", DatawaveUser.UserType.SERVER, "RoleE", "RoleF")); + + DatawavePrincipal principal = new DatawavePrincipal(users); + + // Add local roles for the principal name, and the evidence username. + roleProperties.put(principal.getName(), "LocalRoleA, , LocalRoleB, LocalRoleC ,"); + roleProperties.put(USER_1_EVIDENCE_NAME, "LocalRoleD, , LocalRoleE,"); + + DatawaveSecurityRealm realm = createSecurityRealm(); + + // Verify the realm identity principal is a name principal of the datawave principal's name. + RealmIdentity identity = realm.getRealmIdentity(principal); + assertEquals(identity.getRealmIdentityPrincipal(), new NamePrincipal(principal.getName())); + + // Verify the initial attributes. + Attributes attributes = identity.getAttributes(); + assertEquals(principal.getName(), attributes.getLast(ATTRIBUTE_USERNAME)); + assertThat(attributes.get(AttributeConstants.ATTRIBUTE_PRIMARY_USER_ROLES)).containsExactlyInAnyOrder("RoleA", "RoleB", "LocalRoleA", "LocalRoleB", + "LocalRoleC"); + assertThat(attributes.get(AttributeConstants.ATTRIBUTE_TERMINAL_SERVER_ROLES)).containsExactlyInAnyOrder("RoleE", "RoleF"); + assertThat(attributes.get(AttributeConstants.ATTRIBUTE_PROXIED_USER_KEYS)).containsExactlyInAnyOrder("PROXIED_USER_0", "PROXIED_USER_1", + "PROXIED_USER_2"); + assertThat(attributes.get("PROXIED_USER_0")).containsExactlyInAnyOrder("RoleA", "RoleB"); + assertThat(attributes.get("PROXIED_USER_1")).containsExactlyInAnyOrder("RoleC", "RoleD"); + assertThat(attributes.get("PROXIED_USER_2")).containsExactlyInAnyOrder("RoleE", "RoleF"); + + // Pass in evidence where the evidence username will match against local roles loaded in via a properties. + TrustedHeaderEvidence trustedHeaderEvidence = new TrustedHeaderEvidence(USER_1_EVIDENCE_NAME, List.of()); + identity.verifyEvidence(trustedHeaderEvidence); + + // Verify the attributes post-verification. + Attributes postVerificationAttributes = identity.getAttributes(); + assertEquals(principal.getName(), postVerificationAttributes.getLast(ATTRIBUTE_USERNAME)); + // The primary user roles attribute should now also have the local roles matching the evidence username. + assertThat(postVerificationAttributes.get(AttributeConstants.ATTRIBUTE_PRIMARY_USER_ROLES)).containsExactlyInAnyOrder("RoleA", "RoleB", "LocalRoleA", + "LocalRoleB", "LocalRoleC", "LocalRoleD", "LocalRoleE"); + assertThat(postVerificationAttributes.get(AttributeConstants.ATTRIBUTE_TERMINAL_SERVER_ROLES)).containsExactlyInAnyOrder("RoleE", "RoleF"); + assertThat(postVerificationAttributes.get(AttributeConstants.ATTRIBUTE_PROXIED_USER_KEYS)).containsExactlyInAnyOrder("PROXIED_USER_0", "PROXIED_USER_1", + "PROXIED_USER_2"); + assertThat(postVerificationAttributes.get("PROXIED_USER_0")).containsExactlyInAnyOrder("RoleA", "RoleB"); + assertThat(postVerificationAttributes.get("PROXIED_USER_1")).containsExactlyInAnyOrder("RoleC", "RoleD"); + assertThat(postVerificationAttributes.get("PROXIED_USER_2")).containsExactlyInAnyOrder("RoleE", "RoleF"); + } + + public DatawaveSecurityRealm createSecurityRealm() throws IOException { + roleProperties.store(Files.newBufferedWriter(propertiesFile), null); + DatawaveSecurityRealm realm = new DatawaveSecurityRealm(); + realm.setSecurityEJBProvider(securityEJBProvider); + realm.initialize(configMap); + return realm; + } + + private DatawaveUser createUser(String subjectDn, DatawaveUser.UserType userType, String... roles) { + SubjectIssuerDNPair dnPair = SubjectIssuerDNPair.of(subjectDn, ISSUER_DN); + return new DatawaveUser(dnPair, userType, auths, Set.of(roles), ImmutableMultimap.of(), System.currentTimeMillis()); + } + + public static class FailingX509Verifier implements X509CertificateVerifier { + + @Override + public boolean verify(X509Certificate cert, String alias, KeyStore keyStore, KeyStore trustStore) { + return false; + } + } + + public static class PassingX509Verifier implements X509CertificateVerifier { + + @Override + public boolean verify(X509Certificate cert, String alias, KeyStore keyStore, KeyStore trustStore) { + return true; + } + } +} diff --git a/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveUserProviderTest.java b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveUserProviderTest.java new file mode 100644 index 00000000000..f90650d9d0e --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/realm/DatawaveUserProviderTest.java @@ -0,0 +1,115 @@ +package datawave.security.realm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.wildfly.security.evidence.Evidence; +import org.wildfly.security.evidence.PasswordGuessEvidence; + +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; + +import datawave.security.authorization.DatawaveUser; +import datawave.security.authorization.DatawaveUserService; +import datawave.security.authorization.JWTTokenHandler; +import datawave.security.authorization.SubjectIssuerDNPair; +import datawave.security.evidence.JWTEvidence; +import datawave.security.evidence.TrustedHeaderEvidence; +import datawave.security.evidence.X509CertificateEvidence; + +/** + * Tests for {@link DatawaveUserProvider}. + */ +class DatawaveUserProviderTest { + + private static final String TEST_SUBJECT = "cn=testuser"; + private static final String TEST_ISSUER = "cn=testissuer"; + private static final Set auths = Set.of("A", "B", "C"); + private static final Set roles = Set.of("Administrator", "InternalUser"); + private static final Multimap rolesToAuths = ImmutableMultimap.of("Administrator", "A", "Administrator", "B", "Administrator", "C", + "InternalUser", "A"); + + private DatawaveUserService userService; + private JWTTokenHandler tokenHandler; + private DatawaveUserProvider userProvider; + + @BeforeEach + void setUp() { + userService = Mockito.mock(DatawaveUserService.class); + tokenHandler = Mockito.mock(JWTTokenHandler.class); + userProvider = new DatawaveUserProvider(userService, tokenHandler); + } + + /** + * Verify that {@link DatawaveUserProvider#getUsers(Evidence)} returns an empty set when given evidence that is not an instance of + * {@link datawave.security.evidence.DatawaveEvidence}. + */ + @Test + void testGetUsersGivenNonDatawaveEvidence() throws Exception { + Evidence evidence = new PasswordGuessEvidence("password".toCharArray()); + assertTrue(userProvider.getUsers(evidence).isEmpty()); + } + + /** + * Verify that when {@link DatawaveUserProvider#getUsers(Evidence)} is given a {@link JWTEvidence}, that the token handler is used to look up the users. + */ + @Test + void testGetUsersGivenJWTEvidence() throws Exception { + Collection expected = Set.of(createUser()); + when(tokenHandler.createUsersFromToken("token")).thenReturn(expected); + + JWTEvidence evidence = new JWTEvidence("token"); + Collection actual = userProvider.getUsers(evidence); + assertEquals(expected, actual); + + verify(tokenHandler).createUsersFromToken("token"); + } + + /** + * Verify that when {@link DatawaveUserProvider#getUsers(Evidence)} is given a {@link TrustedHeaderEvidence}, that the user service is used to look up the + * users. + */ + @Test + void testGetUsersGivenTrustedHeaderEvidence() throws Exception { + List entities = List.of(SubjectIssuerDNPair.of(TEST_SUBJECT, TEST_ISSUER)); + Collection expected = Set.of(createUser()); + when(userService.lookup(entities)).thenReturn(expected); + + TrustedHeaderEvidence evidence = new TrustedHeaderEvidence(TEST_SUBJECT + "<" + TEST_ISSUER + ">", entities); + Collection actual = userProvider.getUsers(evidence); + assertEquals(expected, actual); + + verify(userService).lookup(entities); + } + + /** + * Verify that when {@link DatawaveUserProvider#getUsers(Evidence)} is given a {@link TrustedHeaderEvidence}, that the user service is used to look up the + * users. + */ + @Test + void testGetUsersGivenX509CertificateEvidence() throws Exception { + List entities = List.of(SubjectIssuerDNPair.of(TEST_SUBJECT, TEST_ISSUER)); + Collection expected = Set.of(createUser()); + when(userService.lookup(entities)).thenReturn(expected); + + X509CertificateEvidence evidence = new X509CertificateEvidence(TEST_SUBJECT + "<" + TEST_ISSUER + ">", entities, null); + Collection actual = userProvider.getUsers(evidence); + assertEquals(expected, actual); + + verify(userService).lookup(entities); + } + + private DatawaveUser createUser() { + SubjectIssuerDNPair dnPair = SubjectIssuerDNPair.of(TEST_SUBJECT, TEST_ISSUER); + return new DatawaveUser(dnPair, DatawaveUser.UserType.USER, auths, roles, rolesToAuths, System.currentTimeMillis()); + } +} diff --git a/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/test/TestRealm.java b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/test/TestRealm.java new file mode 100644 index 00000000000..717f294e697 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/test/TestRealm.java @@ -0,0 +1,92 @@ +package datawave.security.test; + +import java.security.Principal; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Set; + +import org.wildfly.security.auth.SupportLevel; +import org.wildfly.security.auth.principal.NamePrincipal; +import org.wildfly.security.auth.server.RealmIdentity; +import org.wildfly.security.auth.server.SecurityRealm; +import org.wildfly.security.authz.Attributes; +import org.wildfly.security.authz.AuthorizationIdentity; +import org.wildfly.security.authz.MapAttributes; +import org.wildfly.security.credential.Credential; +import org.wildfly.security.evidence.Evidence; + +/** + * A simple {@link SecurityRealm} that will return a user with the given principal name and the roles Administrator and InternalUser. + */ +public class TestRealm implements SecurityRealm { + + public static final String ROLES_ATTRIBUTE = "Roles"; + + @Override + public SupportLevel getCredentialAcquireSupport(Class credentialType, String algorithmName, AlgorithmParameterSpec parameterSpec) { + return SupportLevel.POSSIBLY_SUPPORTED; + } + + @Override + public SupportLevel getEvidenceVerifySupport(Class evidenceType, String algorithmName) { + return SupportLevel.POSSIBLY_SUPPORTED; + } + + @Override + public RealmIdentity getRealmIdentity(Principal principal) { + return new TestRealmIdentity(principal.getName()); + } + + private static class TestRealmIdentity implements RealmIdentity { + + private final String name; + private final Attributes attributes; + + public TestRealmIdentity(String name) { + this.name = name; + MapAttributes mapAttributes = new MapAttributes(); + mapAttributes.addAll(ROLES_ATTRIBUTE, Set.of("Administrator", "InternalUser")); + this.attributes = mapAttributes.asReadOnly(); + } + + @Override + public Principal getRealmIdentityPrincipal() { + return new NamePrincipal(name); + } + + @Override + public SupportLevel getCredentialAcquireSupport(Class credentialType, String algorithmName, + AlgorithmParameterSpec parameterSpec) { + return SupportLevel.POSSIBLY_SUPPORTED; + } + + @Override + public C getCredential(Class credentialType) { + return null; + } + + @Override + public SupportLevel getEvidenceVerifySupport(Class evidenceType, String algorithmName) { + return SupportLevel.POSSIBLY_SUPPORTED; + } + + @Override + public boolean verifyEvidence(Evidence evidence) { + return true; + } + + @Override + public boolean exists() { + return true; + } + + @Override + public AuthorizationIdentity getAuthorizationIdentity() { + return exists() ? AuthorizationIdentity.basicIdentity(attributes) : AuthorizationIdentity.EMPTY; + } + + @Override + public Attributes getAttributes() { + return attributes; + } + } +} diff --git a/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/utils/ConfigUtilsTest.java b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/utils/ConfigUtilsTest.java new file mode 100644 index 00000000000..c17cc9f2be0 --- /dev/null +++ b/web-services/security-parent/security-elytron-module/src/test/java/datawave/security/utils/ConfigUtilsTest.java @@ -0,0 +1,58 @@ +package datawave.security.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link ConfigUtils}. + */ +class ConfigUtilsTest { + + @Test + void testGetStringGivenNullValue() { + assertEquals("defaultValue", ConfigUtils.getString(null, "defaultValue")); + } + + @Test + void testGetStringGivenBlankValue() { + assertEquals("defaultValue", ConfigUtils.getString(" ", "defaultValue")); + } + + @Test + void testGetStringGivenNonBlankValue() { + assertEquals("value", ConfigUtils.getString(" value ", "defaultValue")); + } + + @Test + void testGetBooleanGivenNullValue() { + assertTrue(ConfigUtils.getBoolean(null, true)); + } + + @Test + void testGetBooleanGivenBlankValue() { + assertTrue(ConfigUtils.getBoolean(" ", true)); + } + + @Test + void testGetBooleanGivenNonBlankValue() { + assertFalse(ConfigUtils.getBoolean(" false ", true)); + } + + @Test + void testGetLongGivenNullValue() { + assertEquals(10L, ConfigUtils.getLong(null, 10L)); + } + + @Test + void testGetLongGivenBlankValue() { + assertEquals(10L, ConfigUtils.getLong(" ", 10L)); + } + + @Test + void testGetLongGivenNonBlankValue() { + assertEquals(1000L, ConfigUtils.getLong(" 1000 ", 10L)); + } +} diff --git a/web-services/security/src/test/resources/dnutils.properties b/web-services/security-parent/security-elytron-module/src/test/resources/dnutils.properties similarity index 100% rename from web-services/security/src/test/resources/dnutils.properties rename to web-services/security-parent/security-elytron-module/src/test/resources/dnutils.properties diff --git a/web-services/security-parent/security-elytron/pom.xml b/web-services/security-parent/security-elytron/pom.xml new file mode 100644 index 00000000000..66e77853697 --- /dev/null +++ b/web-services/security-parent/security-elytron/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + gov.nsa.datawave.webservices + datawave-ws-security-parent + 7.40.0-SNAPSHOT + + + datawave-ws-security-elytron + ${project.artifactId} + + + + org.assertj + assertj-core + + + com.google.guava + guava + provided + + + gov.nsa.datawave.commons + datawave-commons-security + provided + + + org.apache.commons + commons-lang3 + provided + + + org.slf4j + slf4j-api + provided + + + org.wildfly + wildfly-undertow + provided + + + org.bouncycastle + bcpkix-jdk15on + 1.67 + test + + + org.bouncycastle + bcprov-jdk15on + 1.67 + test + + + org.junit-pioneer + junit-pioneer + test + + + + diff --git a/web-services/security-parent/security-elytron/src/main/java/datawave/security/cache/ElytronCache.java b/web-services/security-parent/security-elytron/src/main/java/datawave/security/cache/ElytronCache.java new file mode 100644 index 00000000000..b17494fb641 --- /dev/null +++ b/web-services/security-parent/security-elytron/src/main/java/datawave/security/cache/ElytronCache.java @@ -0,0 +1,49 @@ +package datawave.security.cache; + +import java.util.Set; + +import datawave.security.authorization.DatawaveUser; + +/** + * Represents a cache used by Wildfly that contains entries with {@link DatawaveUser} instances. + */ +public interface ElytronCache { + + /** + * Return the set of all {@link DatawaveUser} instances found in the cache. + * + * @return the users + */ + Set getUsers(); + + /** + * Return the set of all {@link DatawaveUser} instances found in the cache where the name contains the given substring. + * + * @param substring + * the substring + * @return the users + */ + Set getUsersWhereNameContains(String substring); + + /** + * Return the first {@link DatawaveUser} found with the given name. + * + * @param name + * the name + * @return the user + */ + DatawaveUser getUserWithName(String name); + + /** + * Evict all users from the cache that have the given name + * + * @param name + * the name + */ + void evictUsersWithName(String name); + + /** + * Clear the cache. + */ + void clear(); +} diff --git a/web-services/security-parent/security-elytron/src/main/java/datawave/security/cache/ElytronCacheManager.java b/web-services/security-parent/security-elytron/src/main/java/datawave/security/cache/ElytronCacheManager.java new file mode 100644 index 00000000000..35a8beedb8a --- /dev/null +++ b/web-services/security-parent/security-elytron/src/main/java/datawave/security/cache/ElytronCacheManager.java @@ -0,0 +1,57 @@ +package datawave.security.cache; + +import java.util.Set; + +import datawave.security.authorization.DatawaveUser; + +/** + * Represents a manager that can delegate and aggregate operations for a collection of {@link ElytronCache} instances. + */ +public interface ElytronCacheManager { + + /** + * Add a cache to this collection. + * + * @param cache + * the cache + */ + void addCache(ElytronCache cache); + + /** + * Return the set of all {@link DatawaveUser} instances found across all caches in this collection. + * + * @return the users + */ + Set getUsers(); + + /** + * Return the set of all {@link DatawaveUser} instances found across all caches in this collection where the name contains the given substring. + * + * @param substring + * the substring + * @return the users + */ + Set getUsersWhereNameContains(String substring); + + /** + * Return the first {@link DatawaveUser} found across all caches in this collection with the given name. + * + * @param name + * the name + * @return the user + */ + DatawaveUser getUserWithName(String name); + + /** + * Evict all users with the given name from all caches in this collection. + * + * @param name + * the name + */ + void evictUsersWithName(String name); + + /** + * Clear all caches in this collection. + */ + void clear(); +} diff --git a/web-services/security-parent/security-elytron/src/main/java/datawave/security/evidence/DatawaveEvidence.java b/web-services/security-parent/security-elytron/src/main/java/datawave/security/evidence/DatawaveEvidence.java new file mode 100644 index 00000000000..912941fc376 --- /dev/null +++ b/web-services/security-parent/security-elytron/src/main/java/datawave/security/evidence/DatawaveEvidence.java @@ -0,0 +1,49 @@ +package datawave.security.evidence; + +import java.security.Principal; + +import org.wildfly.security.evidence.Evidence; + +/** + * Base implementation of {@link Evidence} that is capable of storing a decoded principal. + */ +public abstract class DatawaveEvidence implements Evidence { + + // The decoded principal. + protected Principal decodedPrincipal; + + /** + * Return the username associated with this evidence. + * + * @return the username + */ + public abstract String getUsername(); + + /** + * Return the default principal (i.e., the decoded principal). + * + * @return the default principal, possibly null + */ + @Override + public Principal getDefaultPrincipal() { + return getDecodedPrincipal(); + } + + /** + * Return the decoded principal + * + * @return the decoded principal, possibly null + */ + @Override + public Principal getDecodedPrincipal() { + return decodedPrincipal; + } + + /** + * Set the decoded principal + */ + @Override + public void setDecodedPrincipal(Principal principal) { + this.decodedPrincipal = principal; + } +} diff --git a/web-services/security-parent/security-elytron/src/main/java/datawave/security/evidence/EvidenceFactory.java b/web-services/security-parent/security-elytron/src/main/java/datawave/security/evidence/EvidenceFactory.java new file mode 100644 index 00000000000..5eed640b60e --- /dev/null +++ b/web-services/security-parent/security-elytron/src/main/java/datawave/security/evidence/EvidenceFactory.java @@ -0,0 +1,194 @@ +package datawave.security.evidence; + +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.tuple.Pair; + +import com.google.common.base.Preconditions; + +import datawave.security.authorization.SubjectIssuerDNPair; +import datawave.security.util.DnProperties; +import datawave.security.util.DnUtils; +import datawave.security.util.SecurityConstants; + +/** + * A factory for creating {@link org.wildfly.security.evidence.Evidence} instances in a standardized manner. This factory should be used when we need to create + * a piece of evidence to programmatically obtain a {@link org.wildfly.security.auth.server.SecurityIdentity} for the security domain "datawave". See the + * following example where a {@link TrustedHeaderEvidence} is created and used to obtain a security identity. + * + *

+ * String subjectDn = "cn=Test A. User, c=US, o=Example Corp, ou=Example Developers";
+ * String issuerDn = "cn=EXAMPLE CORP CA, c=US, o=Example Corp";
+ * TrustedHeaderEvidence evidence = EvidenceFactory.getDefault().createTrustedHeaderEvidence(subjectDn, issuerDn, null, null);
+ * SecurityDomain domain = SecurityDomain.getCurrent();
+ * SecurityIdentity identity = domain.authenticate(evidence);
+ * identity.runAs((Callable<Void>) -> {
+ *     // Operations to execute as the authenticated user.
+ * })
+ * 
+ */ +public class EvidenceFactory { + + private static final EvidenceFactory defaultInstance = of(System.getProperty(SecurityConstants.TRUSTED_PROXIED_ENTITIES_SYSTEM_PROPERTY)); + + /** + * Return the default instance of {@link EvidenceFactory} where trusted proxied entities were extracted from the system property + * {@value SecurityConstants#TRUSTED_PROXIED_ENTITIES_SYSTEM_PROPERTY}. + * + * @return the default {@link EvidenceFactory} instance + */ + public static EvidenceFactory getDefault() { + return defaultInstance; + } + + /** + * Create and return an {@link EvidenceFactory} with trusted proxied entities parsed from the given string. + * + * @param trustedProxiedEntities + * the trusted proxied entities + * @return the new {@link EvidenceFactory} instance + */ + public static EvidenceFactory of(String trustedProxiedEntities) { + if (trustedProxiedEntities != null && !trustedProxiedEntities.isBlank()) { + // @formatter:off + Set entities = Arrays.stream(DnUtils.splitProxiedDNs(trustedProxiedEntities, false)) + .map(String::toLowerCase) + .collect(Collectors.toSet()); + // @formatter:on + return new EvidenceFactory(entities); + } else { + return new EvidenceFactory(); + } + } + + /** + * The set of trusted proxied entities. + */ + private final Set trustedProxiedEntities; + + public EvidenceFactory() { + this(null); + } + + public EvidenceFactory(Set trustedProxiedEntities) { + this.trustedProxiedEntities = trustedProxiedEntities == null ? Set.of() : Set.copyOf(trustedProxiedEntities); + } + + /** + * Return the set of trusted proxied entities configured for this {@link EvidenceFactory}. + * + * @return the trusted proxied entities + */ + public Set getTrustedProxiedEntities() { + return trustedProxiedEntities; + } + + /** + * Create and return a new {@link JWTEvidence} with the given token. + * + * @param token + * the JSON web token + * @return the evidence + */ + public JWTEvidence createJwtEvidence(String token) { + Preconditions.checkArgument((token != null && !token.isBlank()), "token must not be null or blank"); + return new JWTEvidence(token.trim()); + } + + /** + * Create and return a new {@link TrustedHeaderEvidence} created from the given subject DN, issuer DN, and proxied subjects and issuers. Any trusted proxied + * entities configured for this {@link EvidenceFactory} will be pruned from the final set of entities in the returned evidence. + * + * @param subjectDn + * the subject DN + * @param issuerDn + * the issuer DN + * @param proxiedSubjects + * the proxied subject DNs + * @param proxiedIssuers + * the proxied issuer DNs + * @return the evidence + */ + public TrustedHeaderEvidence createTrustedHeadersEvidence(String subjectDn, String issuerDn, String proxiedSubjects, String proxiedIssuers) { + Preconditions.checkArgument((subjectDn != null && !subjectDn.isBlank()), "subject DN must not be null or blank"); + Preconditions.checkArgument((issuerDn != null && !issuerDn.isBlank()), "issuer DN must not be null or blank"); + + Pair> pair = getUsernameAndEntities(subjectDn.trim(), issuerDn.trim(), proxiedSubjects, proxiedIssuers); + return new TrustedHeaderEvidence(pair.getLeft(), pair.getRight()); + } + + /** + * Create and return a new {@link X509CertificateEvidence} created from the given certificate and proxied subjects and issuers. Any trusted proxied entities + * configured for this {@link EvidenceFactory} will be pruned from the final set of entities in the returned evidence. + * + * @param certificate + * the certificate + * @param proxiedSubjects + * the proxied subject DNs + * @param proxiedIssuers + * the proxied issuer DNs + * @return the evidence + */ + public X509CertificateEvidence createX509CertificateEvidence(X509Certificate certificate, String proxiedSubjects, String proxiedIssuers) { + Preconditions.checkNotNull(certificate, "certificate must not be null"); + + Pair> pair = getUsernameAndEntities(certificate.getSubjectX500Principal().getName(), + certificate.getIssuerX500Principal().getName(), proxiedSubjects, proxiedIssuers); + return new X509CertificateEvidence(pair.getLeft(), pair.getRight(), certificate); + } + + /** + * Extract and return the username and entities from the given information. + * + * @param subjectDn + * the subject DN + * @param issuerDn + * the issuer DN + * @param proxiedSubjects + * the proxied subjects + * @param proxiedIssuers + * the proxied issuers + * @return a {@link Pair} with the username (left) and the entities (right) + */ + private Pair> getUsernameAndEntities(String subjectDn, String issuerDn, String proxiedSubjects, String proxiedIssuers) { + Preconditions.checkArgument(proxiedSubjects == null || proxiedIssuers != null, "proxied subjects provided without proxied issuers"); + Preconditions.checkArgument(proxiedIssuers == null || proxiedSubjects != null, "proxied issuers provided without proxied subjects"); + + List entities = new ArrayList<>(); + + // Extract the proxied entities (if any). + if (proxiedSubjects != null && !proxiedSubjects.isBlank()) { + String[] subjects = DnUtils.splitProxiedDNs(proxiedSubjects, true); + String[] issuers = DnUtils.splitProxiedDNs(proxiedIssuers, true); + if (subjects.length != issuers.length) { + throw new IllegalArgumentException( + "Unequal number of proxied subjects and issuers. Subjects=" + proxiedSubjects + ", Issuers" + "=" + proxiedIssuers); + } + for (int i = 0; i < subjects.length; ++i) { + entities.add(SubjectIssuerDNPair.of(subjects[i], issuers[i])); + } + } + + // Add an entity with the subject DN and issuer DN to the entity list. + entities.add(SubjectIssuerDNPair.of(subjectDn, issuerDn)); + String username = DnUtils.buildNormalizedProxyDN(subjectDn, issuerDn, proxiedSubjects, proxiedIssuers, + DnProperties.getDefaultInstance().getSubjectDnPattern()); + + // If the entities contain any trusted proxied entities, remove them from the final entity set. + if (!trustedProxiedEntities.isEmpty()) { + int originalSize = entities.size(); + entities = entities.stream().filter(entity -> !trustedProxiedEntities.contains(entity.subjectDN().toLowerCase())).collect(Collectors.toList()); + // If any entities were pruned, rebuild the username. + if (originalSize != entities.size()) { + username = DnUtils.buildNormalizedProxyDN(entities); + } + } + + return Pair.of(username, entities); + } +} diff --git a/web-services/security-parent/security-elytron/src/main/java/datawave/security/evidence/JWTEvidence.java b/web-services/security-parent/security-elytron/src/main/java/datawave/security/evidence/JWTEvidence.java new file mode 100644 index 00000000000..2676c228666 --- /dev/null +++ b/web-services/security-parent/security-elytron/src/main/java/datawave/security/evidence/JWTEvidence.java @@ -0,0 +1,52 @@ +package datawave.security.evidence; + +import java.util.Objects; + +/** + * Represents evidence consisting of a JSON web token. + */ +public class JWTEvidence extends DatawaveEvidence { + + /** + * The JWT token. + */ + private final String token; + + public JWTEvidence(String token) { + this.token = token; + } + + @Override + public String getUsername() { + return token; + } + + /** + * Return the JWT token. + * + * @return the token + */ + public String getToken() { + return token; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) + return false; + JWTEvidence evidence = (JWTEvidence) o; + return Objects.equals(token, evidence.token); + } + + @Override + public int hashCode() { + return Objects.hashCode(token); + } + + @Override + public String toString() { + // todo - Maybe have a system property to turn off obscuration for debugging? + // The token is obscured here to prevent leaking it to logs/output. + return "JWTEvidence{" + "token='" + (token == null ? null : "") + '\'' + ", decodedPrincipal=" + decodedPrincipal + '}'; + } +} diff --git a/web-services/security-parent/security-elytron/src/main/java/datawave/security/evidence/TrustedHeaderEvidence.java b/web-services/security-parent/security-elytron/src/main/java/datawave/security/evidence/TrustedHeaderEvidence.java new file mode 100644 index 00000000000..5b28147d98a --- /dev/null +++ b/web-services/security-parent/security-elytron/src/main/java/datawave/security/evidence/TrustedHeaderEvidence.java @@ -0,0 +1,42 @@ +package datawave.security.evidence; + +import java.util.List; +import java.util.StringJoiner; + +import datawave.security.authorization.SubjectIssuerDNPair; + +/** + * Represents evidence consisting of trusted headers. + */ +public class TrustedHeaderEvidence extends DatawaveEvidence { + + /** + * The username. + */ + private final String username; + + /** + * The set of entities consisting of the user entity and any proxied entities. + */ + private final List entities; + + public TrustedHeaderEvidence(String username, List entities) { + this.username = username; + this.entities = List.copyOf(entities); + } + + @Override + public String getUsername() { + return username; + } + + public List getEntities() { + return entities; + } + + @Override + public String toString() { + return new StringJoiner(", ", TrustedHeaderEvidence.class.getSimpleName() + "[", "]").add("username='" + username + "'").add("entities=" + entities) + .add("decodedPrincipal=" + decodedPrincipal).toString(); + } +} diff --git a/web-services/security-parent/security-elytron/src/main/java/datawave/security/evidence/X509CertificateEvidence.java b/web-services/security-parent/security-elytron/src/main/java/datawave/security/evidence/X509CertificateEvidence.java new file mode 100644 index 00000000000..b90749c8663 --- /dev/null +++ b/web-services/security-parent/security-elytron/src/main/java/datawave/security/evidence/X509CertificateEvidence.java @@ -0,0 +1,57 @@ +package datawave.security.evidence; + +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.StringJoiner; + +import datawave.security.authorization.SubjectIssuerDNPair; + +/** + * Represents evidence extracted from a PKI certificate. + */ +public class X509CertificateEvidence extends DatawaveEvidence { + + /** + * The username. + */ + private final String username; + + /** + * The set of entities consisting of the user entity and any proxied entities. + */ + private final List entities; + + /** + * The certificate + */ + private final X509Certificate certificate; + + public X509CertificateEvidence(String username, List entities, X509Certificate certificate) { + this.username = username; + this.entities = entities; + this.certificate = certificate; + } + + @Override + public String getUsername() { + return username; + } + + public List getEntities() { + return entities; + } + + public X509Certificate getCertificate() { + return certificate; + } + + @Override + public String toString() { + return new StringJoiner(", ", X509CertificateEvidence.class.getSimpleName() + "[", "]").add("username='" + username + "'").add("entities=" + entities) + .add("certificate=" + formatCert()).add("decodedPrincipal=" + decodedPrincipal).toString(); + } + + private String formatCert() { + return certificate == null ? null : "<" + certificate.getSubjectDN().getName() + ":" + certificate.getIssuerDN().getName() + ">"; + } +} diff --git a/web-services/security-parent/security-elytron/src/main/java/datawave/security/system/SecurityEJBProvider.java b/web-services/security-parent/security-elytron/src/main/java/datawave/security/system/SecurityEJBProvider.java new file mode 100644 index 00000000000..16c6229717b --- /dev/null +++ b/web-services/security-parent/security-elytron/src/main/java/datawave/security/system/SecurityEJBProvider.java @@ -0,0 +1,34 @@ +package datawave.security.system; + +import datawave.security.authorization.DatawaveUserService; +import datawave.security.cache.ElytronCacheManager; +import datawave.security.cert.SSLStores; + +/** + * Defines methods for providing EJBs that need to be accessed when performing authentication via Wildfly in the datawave elytron module. Wildfly does not + * support EJB injection within security components such as elytron security realms. It is expected that an instance of this class will be implemented and bound + * to JNDI for lookup in the datawave elytron module. + */ +public interface SecurityEJBProvider { + + /** + * Return a {@link DatawaveUserService} + * + * @return the user service + */ + DatawaveUserService getDatawaveUserService(); + + /** + * Return a {@link SSLStores} + * + * @return the SSL context + */ + SSLStores getSSLStores(); + + /** + * Return a {@link ElytronCacheManager} + * + * @return the cache collection + */ + ElytronCacheManager getElytronCacheManager(); +} diff --git a/web-services/security-parent/security-elytron/src/test/java/datawave/security/evidence/EvidenceFactoryTest.java b/web-services/security-parent/security-elytron/src/test/java/datawave/security/evidence/EvidenceFactoryTest.java new file mode 100644 index 00000000000..8922d513739 --- /dev/null +++ b/web-services/security-parent/security-elytron/src/test/java/datawave/security/evidence/EvidenceFactoryTest.java @@ -0,0 +1,318 @@ +package datawave.security.evidence; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.SetSystemProperty; + +import datawave.security.authorization.SubjectIssuerDNPair; +import datawave.security.util.SecurityConstants; + +@SetSystemProperty(key = SecurityConstants.TRUSTED_PROXIED_ENTITIES_SYSTEM_PROPERTY, + value = "cn=server1, c=\\, o=my org, ou=\\") +class EvidenceFactoryTest { + + private static X509Certificate certificate; + + @BeforeAll + static void beforeAll() throws Exception { + Security.addProvider(new BouncyCastleProvider()); + + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "BC"); + generator.initialize(2048); + KeyPair keyPair = generator.generateKeyPair(); + + X500Name subjectDn = new X500Name("CN=certUser"); + X500Name issuerDn = new X500Name("CN=certIssuer"); + BigInteger serialNumber = BigInteger.valueOf(new SecureRandom().nextInt()); + Date validFrom = new Date(); + Date validTo = new Date(validFrom.getTime() + 365 * 25 * 60 * 60 * 1000L); + + X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(issuerDn, serialNumber, validFrom, validTo, subjectDn, keyPair.getPublic()); + ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(keyPair.getPrivate()); + certificate = new JcaX509CertificateConverter().setProvider("BC").getCertificate(builder.build(signer)); + } + + /** + * Verify that {@link EvidenceFactory#getDefault()} returns an instance with trusted proxied entities parsed from the system property + * {@value SecurityConstants#TRUSTED_PROXIED_ENTITIES_SYSTEM_PROPERTY}. + */ + @Test + void testDefaultInstance() { + EvidenceFactory factory = EvidenceFactory.getDefault(); + assertThat(factory.getTrustedProxiedEntities()).containsExactlyInAnyOrder("cn=server1, c=\\, o=my org, ou=\\", + "cn=server2, c=us, o=my org, ou=my dept"); + } + + /** + * Verify that {@link EvidenceFactory#of(String)} has no trusted entities given a null string. + */ + @Test + void testOfGivenNull() { + EvidenceFactory factory = EvidenceFactory.of(null); + assertTrue(factory.getTrustedProxiedEntities().isEmpty()); + } + + /** + * Verify that {@link EvidenceFactory#of(String)} has no trusted entities given a blank string. + */ + @Test + void testOfGivenBlank() { + EvidenceFactory factory = EvidenceFactory.of(" "); + assertTrue(factory.getTrustedProxiedEntities().isEmpty()); + } + + /** + * Verify that {@link EvidenceFactory#of(String)} has trusted entities given string with entities. + */ + @Test + void testOfGivenEntities() { + EvidenceFactory factory = EvidenceFactory.of("cn=server1, c=\\, o=my org, ou=\\"); + assertThat(factory.getTrustedProxiedEntities()).containsExactlyInAnyOrder("cn=server1, c=\\, o=my org, ou=\\", + "cn=server2, c=us, o=my org, ou=my dept"); + } + + /** + * Tests for {@link EvidenceFactory#createJwtEvidence(String)}. + */ + @Nested + class CreateJwtEvidence { + + /** + * Verify that {@link EvidenceFactory#createJwtEvidence(String)} throws an exception given a null token. + */ + @Test + void givenNullToken() { + assertThatThrownBy(() -> EvidenceFactory.getDefault().createJwtEvidence(null)).isInstanceOf(IllegalArgumentException.class) + .hasMessage("token must not be null or blank"); + } + + /** + * Verify that {@link EvidenceFactory#createJwtEvidence(String)} throws an exception given a blank token. + */ + @Test + void givenBlankToken() { + assertThatThrownBy(() -> EvidenceFactory.getDefault().createJwtEvidence(" ")).isInstanceOf(IllegalArgumentException.class) + .hasMessage("token must not be null or blank"); + } + + /** + * Verify that {@link EvidenceFactory#createJwtEvidence(String)} throws an exception given a blank token. + */ + @Test + void givenValidToken() { + JWTEvidence evidence = EvidenceFactory.getDefault().createJwtEvidence("token"); + assertEquals("token", evidence.getToken()); + assertEquals("token", evidence.getUsername()); + } + } + + /** + * Tests for {@link EvidenceFactory#createTrustedHeadersEvidence(String, String, String, String)}. + */ + @Nested + class CreateTrustedHeaderEvidence { + + /** + * Verify that {@link EvidenceFactory#createTrustedHeadersEvidence(String, String, String, String)} throws an exception given a null subject DN. + */ + @Test + void givenNullSubjectDn() { + assertThatThrownBy(() -> EvidenceFactory.getDefault().createTrustedHeadersEvidence(null, null, null, null)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("subject DN must not be null or blank"); + } + + /** + * Verify that {@link EvidenceFactory#createTrustedHeadersEvidence(String, String, String, String)} throws an exception given a blank subject DN. + */ + @Test + void givenBlankSubjectDn() { + assertThatThrownBy(() -> EvidenceFactory.getDefault().createTrustedHeadersEvidence(" ", null, null, null)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("subject DN must not be null or blank"); + } + + /** + * Verify that {@link EvidenceFactory#createTrustedHeadersEvidence(String, String, String, String)} throws an exception given a null issuer DN. + */ + @Test + void givenNullIssuerDn() { + assertThatThrownBy(() -> EvidenceFactory.getDefault().createTrustedHeadersEvidence("cn=user1", null, null, null)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("issuer DN must not be null or blank"); + } + + /** + * Verify that {@link EvidenceFactory#createTrustedHeadersEvidence(String, String, String, String)} throws an exception given a blank issuer DN. + */ + @Test + void givenBlankIssuerDn() { + assertThatThrownBy(() -> EvidenceFactory.getDefault().createTrustedHeadersEvidence("cn=user1", " ", null, null)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("issuer DN must not be null or blank"); + } + + /** + * Verify that {@link EvidenceFactory#createTrustedHeadersEvidence(String, String, String, String)} throws an exception given proxied subjects without + * proxied issuers. + */ + @Test + void givenProxiedSubjectsWithoutProxiedIssuers() { + assertThatThrownBy(() -> EvidenceFactory.getDefault().createTrustedHeadersEvidence("cn=user1", "cn=issuer1", "cn=proxiedUser1", null)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("proxied subjects provided without proxied issuers"); + } + + /** + * Verify that {@link EvidenceFactory#createTrustedHeadersEvidence(String, String, String, String)}} throws an exception given proxied issuers without + * proxied subjects. + */ + @Test + void givenProxiedIssuersWithoutProxiedSubjects() { + assertThatThrownBy(() -> EvidenceFactory.getDefault().createTrustedHeadersEvidence("cn=user1", "cn=issuer1", null, "cn=proxiedIssuer1")) + .isInstanceOf(IllegalArgumentException.class).hasMessage("proxied issuers provided without proxied subjects"); + } + + /** + * Verify that {@link EvidenceFactory#createTrustedHeadersEvidence(String, String, String, String)} throws an exception given an unequal number of + * proxied subjects and issuers. + */ + @Test + void givenUnequalNumberOfProxiedSubjectsAndIssuers() { + assertThatThrownBy(() -> EvidenceFactory.getDefault().createTrustedHeadersEvidence("cn=user1", "cn=issuer1", + "cn=proxiedSubject1", "cn=proxiedIssuer1")).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unequal number of proxied subjects and issuers. Subjects=cn=proxiedSubject1, Issuers=cn=proxiedIssuer1"); + } + + /** + * Verify that {@link EvidenceFactory#createTrustedHeadersEvidence(String, String, String, String)} prunes any trusted entities. + */ + @Test + void givenValidEntitiesWithTrustedEntitiesInChain() { + TrustedHeaderEvidence evidence = EvidenceFactory.getDefault().createTrustedHeadersEvidence("cn=user1", "cn=issuer1", + "cn=server2, c=us, o=my org, ou=my dept", + "cn=issuer2, c=us, o=my org, ou=my dept"); + assertEquals("cn=server3, c=us, o=my org, ou=my dept", evidence.getUsername()); + + List entities = new ArrayList<>(); + entities.add(SubjectIssuerDNPair.of("cn=server3, c=us, o=my org, ou=my dept", "cn=issuer3, c=us, o=my org, ou=my dept")); + entities.add(SubjectIssuerDNPair.of("cn=user1", "cn=issuer1")); + assertEquals(entities, evidence.getEntities()); + } + + /** + * Verify that {@link EvidenceFactory#createX509CertificateEvidence(X509Certificate, String, String)} does not prune any entities when there are no + * trusted entities. + */ + @Test + void givenValidEntitiesWithNoTrustedEntitiesInChain() { + TrustedHeaderEvidence evidence = EvidenceFactory.getDefault().createTrustedHeadersEvidence("cn=user1", "cn=issuer1", "cn=server4", + "cn=issuer2"); + assertEquals("cn=server4", evidence.getUsername()); + + List entities = new ArrayList<>(); + entities.add(SubjectIssuerDNPair.of("cn=server4", "cn=issuer2")); + entities.add(SubjectIssuerDNPair.of("cn=server5", "cn=issuer3")); + entities.add(SubjectIssuerDNPair.of("cn=user1", "cn=issuer1")); + assertEquals(entities, evidence.getEntities()); + } + } + + /** + * Tests for {@link EvidenceFactory#createX509CertificateEvidence(X509Certificate, String, String)}. + */ + @Nested + class CreatedX509CertificateEvidence { + + /** + * Verify that {@link EvidenceFactory#createX509CertificateEvidence(X509Certificate, String, String)} throws an exception given a null cert. + */ + @Test + void givenNullCertificate() { + assertThatThrownBy(() -> EvidenceFactory.getDefault().createX509CertificateEvidence(null, null, null)).isInstanceOf(NullPointerException.class) + .hasMessage("certificate must not be null"); + } + + /** + * Verify that {@link EvidenceFactory#createX509CertificateEvidence(X509Certificate, String, String)} throws an exception given proxied subjects without + * proxied issuers. + */ + @Test + void givenProxiedSubjectsWithoutProxiedIssuers() { + assertThatThrownBy(() -> EvidenceFactory.getDefault().createX509CertificateEvidence(certificate, "cn=proxiedUser1", null)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("proxied subjects provided without proxied issuers"); + } + + /** + * Verify that {@link EvidenceFactory#createX509CertificateEvidence(X509Certificate, String, String)} throws an exception given proxied issuers without + * proxied subjects. + */ + @Test + void givenProxiedIssuersWithoutProxiedSubjects() { + assertThatThrownBy(() -> EvidenceFactory.getDefault().createX509CertificateEvidence(certificate, null, "cn=proxiedIssuer1")) + .isInstanceOf(IllegalArgumentException.class).hasMessage("proxied issuers provided without proxied subjects"); + } + + /** + * Verify that {@link EvidenceFactory#createX509CertificateEvidence(X509Certificate, String, String)} throws an exception given an unequal number of + * proxied subjects and issuers. + */ + @Test + void givenUnequalNumberOfProxiedSubjectsAndIssuers() { + assertThatThrownBy(() -> EvidenceFactory.getDefault().createX509CertificateEvidence(certificate, "cn=proxiedSubject1", + "cn=proxiedIssuer1")).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unequal number of proxied subjects and issuers. Subjects=cn=proxiedSubject1, Issuers=cn=proxiedIssuer1"); + } + + /** + * Verify that {@link EvidenceFactory#createX509CertificateEvidence(X509Certificate, String, String)} prunes any trusted entities. + */ + @Test + void givenValidEntitiesWithTrustedEntitiesInChain() { + X509CertificateEvidence evidence = EvidenceFactory.getDefault().createX509CertificateEvidence(certificate, + "cn=server2, c=us, o=my org, ou=my dept", + "cn=issuer2, c=us, o=my org, ou=my dept"); + assertEquals("cn=server3, c=us, o=my org, ou=my dept", evidence.getUsername()); + + List entities = new ArrayList<>(); + entities.add(SubjectIssuerDNPair.of("cn=server3, c=us, o=my org, ou=my dept", "cn=issuer3, c=us, o=my org, ou=my dept")); + entities.add(SubjectIssuerDNPair.of("cn=certuser", "cn=certissuer")); + assertEquals(entities, evidence.getEntities()); + } + + /** + * Verify that {@link EvidenceFactory#createX509CertificateEvidence(X509Certificate, String, String)} does not prune any entities when there are no + * trusted entities. + */ + @Test + void givenValidEntitiesWithNoTrustedEntitiesInChain() { + X509CertificateEvidence evidence = EvidenceFactory.getDefault().createX509CertificateEvidence(certificate, "cn=server4", + "cn=issuer2"); + assertEquals("cn=server4", evidence.getUsername()); + + List entities = new ArrayList<>(); + entities.add(SubjectIssuerDNPair.of("cn=server4", "cn=issuer2")); + entities.add(SubjectIssuerDNPair.of("cn=server5", "cn=issuer3")); + entities.add(SubjectIssuerDNPair.of("cn=certuser", "cn=certissuer")); + assertEquals(entities, evidence.getEntities()); + } + } +} diff --git a/web-services/security-parent/security-elytron/src/test/resources/dnutils.properties b/web-services/security-parent/security-elytron/src/test/resources/dnutils.properties new file mode 100644 index 00000000000..0506661bd8f --- /dev/null +++ b/web-services/security-parent/security-elytron/src/test/resources/dnutils.properties @@ -0,0 +1,2 @@ +npe.ou.entries=iamnotaperson,npe,stillnotaperson +subject.dn.pattern=(?:^|,)\\s*OU\\s*=\\s*Subject DN OU\\s*(?:,|$) \ No newline at end of file diff --git a/web-services/security/pom.xml b/web-services/security-parent/security/pom.xml similarity index 59% rename from web-services/security/pom.xml rename to web-services/security-parent/security/pom.xml index 7b82dca403e..e9a39682d60 100644 --- a/web-services/security/pom.xml +++ b/web-services/security-parent/security/pom.xml @@ -3,91 +3,53 @@ 4.0.0 gov.nsa.datawave.webservices - datawave-ws-parent + datawave-ws-security-parent 7.40.0-SNAPSHOT datawave-ws-security ejb ${project.artifactId} + com.fasterxml.jackson.datatype jackson-datatype-guava - com.spotify - dns + com.google.guava + guava - commons-beanutils - commons-beanutils - - - commons-cli - commons-cli - - - commons-collections - commons-collections + gov.nsa.datawave + datawave-common-test - commons-configuration - commons-configuration + gov.nsa.datawave.commons + datawave-commons-security gov.nsa.datawave.microservice authorization-api - - gov.nsa.datawave.webservices - datawave-ws-client - ${project.version} - gov.nsa.datawave.webservices datawave-ws-common-util ${project.version} + + + gov.nsa.datawave.core + datawave-core-common-util + + - javax.enterprise - cdi-api - - - org.apache.accumulo - accumulo-core - - - org.apache.commons - commons-digester3 - - - org.apache.curator - curator-framework - - - org.apache.curator - curator-recipes - - - org.apache.curator - curator-test - - - org.apache.deltaspike.core - deltaspike-core-api - - - org.apache.httpcomponents - httpclient + gov.nsa.datawave.webservices + datawave-ws-security-elytron org.bouncycastle bcprov-jdk16 - - org.easymock - easymock - org.infinispan infinispan-commons @@ -96,6 +58,10 @@ org.infinispan infinispan-core + + org.jboss.arquillian.container + arquillian-weld-embedded + org.powermock powermock-api-easymock @@ -125,28 +91,8 @@ provided - org.apache.commons - commons-configuration2 - provided - - - org.apache.hadoop.thirdparty - hadoop-shaded-guava - provided - - - org.apache.logging.log4j - log4j-1.2-api - provided - - - org.apache.logging.log4j - log4j-api - provided - - - org.apache.logging.log4j - log4j-core + jakarta.enterprise + jakarta.enterprise.cdi-api provided @@ -154,56 +100,6 @@ resteasy-client provided - - org.jboss.resteasy - resteasy-jaxrs - provided - - - org.jboss.spec.javax.annotation - jboss-annotations-api_1.3_spec - provided - - - org.jboss.spec.javax.ejb - jboss-ejb-api_3.2_spec - provided - - - org.jboss.spec.javax.enterprise.concurrent - jboss-concurrency-api_1.0_spec - provided - - - org.jboss.spec.javax.interceptor - jboss-interceptors-api_1.2_spec - provided - - - org.jboss.spec.javax.security.jacc - jboss-jacc-api_1.5_spec - provided - - - org.jboss.spec.javax.transaction - jboss-transaction-api_1.2_spec - provided - - - org.jboss.spec.javax.websocket - jboss-websocket-api_1.1_spec - provided - - - org.picketbox - picketbox - provided - - - org.picketbox - picketbox-infinispan - provided - org.wildfly wildfly-security @@ -219,6 +115,21 @@ datawave-in-memory-accumulo test + + gov.nsa.datawave.microservice + accumulo-api + test + + + gov.nsa.datawave.microservice + dictionary-api + test + + + gov.nsa.datawave.microservice + query-api + test + gov.nsa.datawave.webservices datawave-ws-common @@ -228,13 +139,8 @@ test - org.javassist - javassist - test - - - org.jboss.arquillian.container - arquillian-weld-ee-embedded-1.1 + org.easymock + easymock test @@ -242,11 +148,6 @@ arquillian-junit-container test - - org.jboss.weld - weld-core - test - org.jboss.weld weld-core-impl @@ -255,6 +156,7 @@ org.mockito mockito-core + ${version.mockito} test @@ -265,7 +167,7 @@ org.springframework - spring-expression + spring-beans test @@ -274,6 +176,7 @@ test + ${project.artifactId} @@ -287,6 +190,8 @@ src/test/resources *.pkcs12 + *.p12 + *.jks @@ -304,7 +209,7 @@ lib - commons-cli,httpclient,httpcore,commons-digester3,bcprov-jdk16 + jackson-datatype-guava,guava,bcprov-jdk16 false @@ -326,6 +231,18 @@ + + org.apache.maven.plugins + maven-jar-plugin + + + + **/datawave/security/examples/**/* + + +
+ diff --git a/web-services/security/src/main/java/datawave/security/authorization/remote/RemoteDatawaveUserService.java b/web-services/security-parent/security/src/main/java/datawave/security/authorization/remote/RemoteDatawaveUserService.java similarity index 100% rename from web-services/security/src/main/java/datawave/security/authorization/remote/RemoteDatawaveUserService.java rename to web-services/security-parent/security/src/main/java/datawave/security/authorization/remote/RemoteDatawaveUserService.java diff --git a/web-services/security/src/main/java/datawave/security/authorization/remote/RemoteUserOperationsImpl.java b/web-services/security-parent/security/src/main/java/datawave/security/authorization/remote/RemoteUserOperationsImpl.java similarity index 95% rename from web-services/security/src/main/java/datawave/security/authorization/remote/RemoteUserOperationsImpl.java rename to web-services/security-parent/security/src/main/java/datawave/security/authorization/remote/RemoteUserOperationsImpl.java index 22e5bdd09fe..fdd520880c7 100644 --- a/web-services/security/src/main/java/datawave/security/authorization/remote/RemoteUserOperationsImpl.java +++ b/web-services/security-parent/security/src/main/java/datawave/security/authorization/remote/RemoteUserOperationsImpl.java @@ -17,11 +17,11 @@ import com.fasterxml.jackson.databind.ObjectReader; import datawave.authorization.remote.RemoteAuthorizationException; -import datawave.security.auth.DatawaveAuthenticationMechanism; import datawave.security.authorization.AuthorizationException; import datawave.security.authorization.DatawavePrincipal; import datawave.security.authorization.ProxiedUserDetails; import datawave.security.authorization.UserOperations; +import datawave.security.util.SecurityConstants; import datawave.user.AuthorizationsListBase; import datawave.webservice.common.remote.RemoteHttpService; import datawave.webservice.result.GenericResponse; @@ -30,8 +30,8 @@ public class RemoteUserOperationsImpl extends RemoteHttpService implements UserOperations { private static final Logger log = LoggerFactory.getLogger(RemoteUserOperationsImpl.class); - public static final String PROXIED_ENTITIES_HEADER = DatawaveAuthenticationMechanism.PROXIED_ENTITIES_HEADER; - public static final String PROXIED_ISSUERS_HEADER = DatawaveAuthenticationMechanism.PROXIED_ISSUERS_HEADER; + public static final String PROXIED_ENTITIES_HEADER = SecurityConstants.PROXIED_ENTITIES_HEADER; + public static final String PROXIED_ISSUERS_HEADER = SecurityConstants.PROXIED_ISSUERS_HEADER; private static final String LIST_EFFECTIVE_AUTHS = "listEffectiveAuthorizations"; diff --git a/web-services/security/src/main/java/datawave/security/authorization/simple/DatabaseUserService.java b/web-services/security-parent/security/src/main/java/datawave/security/authorization/simple/DatabaseUserService.java similarity index 100% rename from web-services/security/src/main/java/datawave/security/authorization/simple/DatabaseUserService.java rename to web-services/security-parent/security/src/main/java/datawave/security/authorization/simple/DatabaseUserService.java diff --git a/web-services/security/src/main/java/datawave/security/authorization/test/TestDatawaveUserService.java b/web-services/security-parent/security/src/main/java/datawave/security/authorization/test/TestDatawaveUserService.java similarity index 99% rename from web-services/security/src/main/java/datawave/security/authorization/test/TestDatawaveUserService.java rename to web-services/security-parent/security/src/main/java/datawave/security/authorization/test/TestDatawaveUserService.java index ae8d4ecbcdc..c6f8c55f506 100644 --- a/web-services/security/src/main/java/datawave/security/authorization/test/TestDatawaveUserService.java +++ b/web-services/security-parent/security/src/main/java/datawave/security/authorization/test/TestDatawaveUserService.java @@ -42,7 +42,7 @@ import datawave.webservice.util.NotEqualPropertyExpressionInterpreter; /** - * A {@link CachedDatawaveUserService} for testing purposes. This version will only be active if the syste property {@code dw.security.use.testuserservice} is + * A {@link CachedDatawaveUserService} for testing purposes. This version will only be active if the system property {@code dw.security.use.testuserservice} is * set to {@code true}. When active, any incoming requests will first check a map of "canned" users and use the canned result if found. When no result is found, * it will delegate to the highest priority other {@link CachedDatawaveUserService} or {@link DatawaveUserService} that can be found. If no other instance is * found, then construction of this bean will fail. diff --git a/web-services/security/src/main/java/datawave/security/cache/AccumuloCacheStore.java b/web-services/security-parent/security/src/main/java/datawave/security/cache/AccumuloCacheStore.java similarity index 100% rename from web-services/security/src/main/java/datawave/security/cache/AccumuloCacheStore.java rename to web-services/security-parent/security/src/main/java/datawave/security/cache/AccumuloCacheStore.java diff --git a/web-services/security/src/main/java/datawave/security/cache/AccumuloCacheStoreConfiguration.java b/web-services/security-parent/security/src/main/java/datawave/security/cache/AccumuloCacheStoreConfiguration.java similarity index 100% rename from web-services/security/src/main/java/datawave/security/cache/AccumuloCacheStoreConfiguration.java rename to web-services/security-parent/security/src/main/java/datawave/security/cache/AccumuloCacheStoreConfiguration.java diff --git a/web-services/security/src/main/java/datawave/security/cache/AccumuloCacheStoreConfigurationBuilder.java b/web-services/security-parent/security/src/main/java/datawave/security/cache/AccumuloCacheStoreConfigurationBuilder.java similarity index 100% rename from web-services/security/src/main/java/datawave/security/cache/AccumuloCacheStoreConfigurationBuilder.java rename to web-services/security-parent/security/src/main/java/datawave/security/cache/AccumuloCacheStoreConfigurationBuilder.java diff --git a/web-services/security/src/main/java/datawave/security/cache/CredentialsCacheBean.java b/web-services/security-parent/security/src/main/java/datawave/security/cache/CredentialsCacheBean.java similarity index 84% rename from web-services/security/src/main/java/datawave/security/cache/CredentialsCacheBean.java rename to web-services/security-parent/security/src/main/java/datawave/security/cache/CredentialsCacheBean.java index 384db07c9b7..2bf8d3a7fdc 100644 --- a/web-services/security/src/main/java/datawave/security/cache/CredentialsCacheBean.java +++ b/web-services/security-parent/security/src/main/java/datawave/security/cache/CredentialsCacheBean.java @@ -31,7 +31,6 @@ import org.apache.accumulo.core.security.Authorizations; import org.apache.deltaspike.core.api.jmx.JmxManaged; import org.apache.deltaspike.core.api.jmx.MBean; -import org.jboss.security.CacheableManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,10 +39,8 @@ import datawave.core.common.connection.AccumuloConnectionFactory; import datawave.security.DnList; import datawave.security.authorization.CachedDatawaveUserService; -import datawave.security.authorization.DatawavePrincipal; import datawave.security.authorization.DatawaveUser; import datawave.security.authorization.DatawaveUserInfo; -import datawave.security.system.AuthorizationCache; import datawave.webservice.common.exception.DatawaveWebApplicationException; import datawave.webservice.query.exception.QueryException; import datawave.webservice.result.GenericResponse; @@ -66,16 +63,13 @@ @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) // transactions not supported directly by this bean public class CredentialsCacheBean { - protected Logger log = LoggerFactory.getLogger(getClass()); - @Inject - @AuthorizationCache - private CacheableManager authManager; + private static final Logger log = LoggerFactory.getLogger(CredentialsCacheBean.class); + + private ElytronCacheManager elytronCacheManager; - @Inject private Instance cachedDatawaveUserServiceInstance; - @Inject private AccumuloConnectionFactory accumuloConnectionFactory; private Set accumuloUserAuths = new HashSet<>(); @@ -91,6 +85,21 @@ protected void postConstruct() { } } + @Inject + public void setElytronCache(ElytronCacheManager elytronCacheManager) { + this.elytronCacheManager = elytronCacheManager; + } + + @Inject + public void setCachedDatawaveUserServiceInstance(Instance cachedDatawaveUserServiceInstance) { + this.cachedDatawaveUserServiceInstance = cachedDatawaveUserServiceInstance; + } + + @Inject + public void setAccumuloConnectionFactory(AccumuloConnectionFactory accumuloConnectionFactory) { + this.accumuloConnectionFactory = accumuloConnectionFactory; + } + /** * Removes all cached {@link DatawaveUser}s. There are potentially two caches in use. First, Wildfly uses a security cache that stores {@link Principal}s * under the incoming credential key. This is normally a very short-lived cache (5-30 minutes). Second, a {@link CachedDatawaveUserService} may be in use, @@ -103,9 +112,12 @@ protected void postConstruct() { @JmxManaged public String flushAll() { try { - // Remove principals from the Wildfly cached authentication manager, if we have one in use. - authManager.flushCache(); + log.trace("flushAll()"); + // Remove principals from the Wildfly cache if we have one in use. + log.trace("Flushing from the elytron cache"); + elytronCacheManager.clear(); if (!cachedDatawaveUserServiceInstance.isUnsatisfied()) { + log.trace("Flushing from the cached datawave user service"); cachedDatawaveUserServiceInstance.get().evictAll(); } return "All credentials caches cleared."; @@ -131,21 +143,15 @@ public String flushAll() { @Produces({"text/plain"}) @JmxManaged public String evict(@PathParam("dn") String dn) { - String result = "Evicted " + dn + " from the credentials cache."; if (!cachedDatawaveUserServiceInstance.isUnsatisfied()) { + log.trace("Flushing {} from the cached datawave user service", dn); result = cachedDatawaveUserServiceInstance.get().evictMatching(dn); } - // @formatter:off - // Flush all principals from the Wildfly cache if the getName() of any of the contained DatawaveUser objects matches the supplied DN. - authManager.getCachedKeys().parallelStream() - .filter(p -> p instanceof DatawavePrincipal) - .filter(p -> ((DatawavePrincipal) p).getProxiedUsers().stream().anyMatch(u -> u.getName().equals(dn))) - .forEach(p -> { - log.debug("Evicting {} from the Wildfly authentication cache.", p); - authManager.flushCache(p); - }); - // @formatter:on + + log.trace("Flushing {} from the elytron cache", dn); + // Flush all entities from the elytron if they reference a DatawaveUser where the name matches the supplied DN. + elytronCacheManager.evictUsersWithName(dn); return result; } @@ -165,14 +171,14 @@ public String evict(@PathParam("dn") String dn) { public DnList listDNs(@QueryParam("localOnly") boolean localOnly) { DnList result; if (!cachedDatawaveUserServiceInstance.isUnsatisfied() && !localOnly) { + log.trace("Fetching users from cached datawave user service"); result = new DnList(cachedDatawaveUserServiceInstance.get().listAll()); } else { + log.trace("Fetching users from the elytron cache"); // @formatter:off - Set userList = authManager.getCachedKeys().parallelStream() - .filter(p -> p instanceof DatawavePrincipal) - .flatMap(p -> ((DatawavePrincipal) p).getProxiedUsers().stream()) - .map(DatawaveUserInfo::new) - .collect(Collectors.toSet()); + Set userList = elytronCacheManager.getUsers().parallelStream() + .map(DatawaveUserInfo::new) + .collect(Collectors.toSet()); // @formatter:on result = new DnList(userList); } @@ -193,16 +199,14 @@ public DnList listDNs(@QueryParam("localOnly") boolean localOnly) { public DnList listDNsMatching(@QueryParam("substring") final String substr) { DnList result; if (!cachedDatawaveUserServiceInstance.isUnsatisfied()) { + log.trace("Fetching users from cached datawave user service"); result = new DnList(cachedDatawaveUserServiceInstance.get().listMatching(substr)); } else { - Set principals = authManager.getCachedKeys(); + log.trace("Fetching users from the elytron cache"); // @formatter:off - Set userList = principals.parallelStream() - .filter(p -> p instanceof DatawavePrincipal) - .flatMap(p -> ((DatawavePrincipal) p).getProxiedUsers().stream()) - .filter(u -> u.getName().contains(substr)) - .map(DatawaveUserInfo::new) - .collect(Collectors.toSet()); + Set userList = elytronCacheManager.getUsersWhereNameContains(substr).parallelStream() + .map(DatawaveUserInfo::new) + .collect(Collectors.toSet()); // @formatter:on result = new DnList(userList); } @@ -224,15 +228,12 @@ public DnList listDNsMatching(@QueryParam("substring") final String substr) { public DatawaveUser list(@PathParam("dn") String dn) { DatawaveUser user; if (!cachedDatawaveUserServiceInstance.isUnsatisfied()) { + log.trace("Fetching user from cached datawave user service"); user = cachedDatawaveUserServiceInstance.get().list(dn); } else { + log.trace("Fetching users from the elytron cache"); // @formatter:off - user = authManager.getCachedKeys().parallelStream() - .filter(p -> p instanceof DatawavePrincipal) - .flatMap(p -> ((DatawavePrincipal) p).getProxiedUsers().stream()) - .filter(p -> p.getName().equals(dn)) - .findFirst() - .orElse(null); + user = elytronCacheManager.getUserWithName(dn); // @formatter:on } return user; diff --git a/web-services/security-parent/security/src/main/java/datawave/security/cache/ElytronCacheManagerImpl.java b/web-services/security-parent/security/src/main/java/datawave/security/cache/ElytronCacheManagerImpl.java new file mode 100644 index 00000000000..ceb7f80ce1a --- /dev/null +++ b/web-services/security-parent/security/src/main/java/datawave/security/cache/ElytronCacheManagerImpl.java @@ -0,0 +1,87 @@ +package datawave.security.cache; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.annotation.security.PermitAll; +import javax.ejb.Singleton; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Preconditions; + +import datawave.security.authorization.DatawaveUser; + +/** + * Implementation of {@link ElytronCacheManager} that is expected to be injected into a {@link CredentialsCacheBean}. In order to allow the methods here to be + * invoked in the datawave elytron module from an unauthenticated context, we must use {@link PermitAll}. + */ +@PermitAll +@Singleton +public class ElytronCacheManagerImpl implements ElytronCacheManager { + + private static final Logger log = LoggerFactory.getLogger(ElytronCacheManagerImpl.class); + + private final List caches = new ArrayList<>(); + + @Override + public void addCache(ElytronCache cache) { + Preconditions.checkNotNull(cache, "cache cannot be null"); + if (log.isTraceEnabled()) { + log.trace("Adding cache {}", cache.getClass().getName()); + } + this.caches.add(cache); + } + + @Override + public Set getUsers() { + // @formatter:off + return caches.stream() + .map(ElytronCache::getUsers) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + // @formatter:on + } + + @Override + public Set getUsersWhereNameContains(String substring) { + Preconditions.checkNotNull(substring, "substring cannot be null"); + // @formatter:off + return caches.stream() + .map(cache -> cache.getUsersWhereNameContains(substring)) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + // @formatter:on + } + + @Override + public DatawaveUser getUserWithName(String name) { + Preconditions.checkNotNull(name, "name cannot be null"); + + for (ElytronCache cache : caches) { + DatawaveUser user = cache.getUserWithName(name); + if (user != null) { + return user; + } + } + return null; + } + + @Override + public void evictUsersWithName(String name) { + if (log.isTraceEnabled()) { + log.trace("Evicting all users with name {}", name); + } + caches.forEach(delegate -> delegate.evictUsersWithName(name)); + } + + @Override + public void clear() { + log.trace("Clearing all caches"); + caches.forEach(ElytronCache::clear); + } +} diff --git a/web-services/security-parent/security/src/main/java/datawave/security/servlet/HeaderEnrichmentServletExtension.java b/web-services/security-parent/security/src/main/java/datawave/security/servlet/HeaderEnrichmentServletExtension.java new file mode 100644 index 00000000000..0f5edcdf7a4 --- /dev/null +++ b/web-services/security-parent/security/src/main/java/datawave/security/servlet/HeaderEnrichmentServletExtension.java @@ -0,0 +1,28 @@ +package datawave.security.servlet; + +import javax.servlet.ServletContext; + +import datawave.security.util.SecurityConstants; +import io.undertow.servlet.ServletExtension; +import io.undertow.servlet.api.DeploymentInfo; + +/** + * A {@link ServletExtension} that will enrich incoming requests by adding the following headers: + *
    + *
  • {@value SecurityConstants#REQUEST_START_TIME_HEADER}: This header will be added before authentication occurs.
  • + *
  • {@value SecurityConstants#REQUEST_LOGIN_TIME_HEADER}: This header will be added after the authentication attempt.
  • + *
+ * To register this extension with Undertow, the fully qualified class name must be added to a file named {@code io.undertow.servlet.ServletExtension} in the + * META-INF/services directory. + */ +public class HeaderEnrichmentServletExtension implements ServletExtension { + + @Override + public void handleDeployment(final DeploymentInfo deploymentInfo, final ServletContext servletContext) { + // Add the request start time header handler. This will run after the servlet request context has been set up, but before any other handlers. + deploymentInfo.addOuterHandlerChainWrapper(RequestStartTimeHeaderHandler::new); + + // Add the request login time header handler. This will run after the security handlers, but before the request is dispatched to deployment code. + deploymentInfo.addInnerHandlerChainWrapper(RequestLoginTimeHeaderHandler::new); + } +} diff --git a/web-services/security-parent/security/src/main/java/datawave/security/servlet/RequestLoginTimeHeaderHandler.java b/web-services/security-parent/security/src/main/java/datawave/security/servlet/RequestLoginTimeHeaderHandler.java new file mode 100644 index 00000000000..bccb86261c4 --- /dev/null +++ b/web-services/security-parent/security/src/main/java/datawave/security/servlet/RequestLoginTimeHeaderHandler.java @@ -0,0 +1,50 @@ +package datawave.security.servlet; + +import java.util.concurrent.TimeUnit; + +import datawave.security.util.SecurityConstants; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderMap; +import io.undertow.util.HttpString; + +/** + * A {@link HttpHandler} that will add a {@value SecurityConstants#REQUEST_LOGIN_TIME_HEADER} request header with the delta of the request start time stored in + * the header {@link SecurityConstants#REQUEST_START_TIME_HEADER} and the current system time in milliseconds. This handler is expected to be configured to + * execute after the calling user for the request is authenticated. + */ +public class RequestLoginTimeHeaderHandler implements HttpHandler { + + private static final HttpString header = new HttpString(SecurityConstants.REQUEST_LOGIN_TIME_HEADER); + + private final HttpHandler next; + + /** + * Create a new {@link RequestLoginTimeHeaderHandler} with a reference to the next handler that the incoming exchange should be passed to. + * + * @param next + * the next handler + */ + public RequestLoginTimeHeaderHandler(HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + try { + // Add a header with the current time as the login time. + HeaderMap headers = exchange.getRequestHeaders(); + String startTimeValue = headers.getFirst(SecurityConstants.REQUEST_START_TIME_HEADER); + if (startTimeValue != null) { + long startTime = Long.parseLong(startTimeValue); + long loginTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime); + headers.add(header, loginTime); + } + } finally { + // Pass the exchange to the next handler. + if (next != null) { + next.handleRequest(exchange); + } + } + } +} diff --git a/web-services/security-parent/security/src/main/java/datawave/security/servlet/RequestStartTimeHeaderHandler.java b/web-services/security-parent/security/src/main/java/datawave/security/servlet/RequestStartTimeHeaderHandler.java new file mode 100644 index 00000000000..2ad5ed55de3 --- /dev/null +++ b/web-services/security-parent/security/src/main/java/datawave/security/servlet/RequestStartTimeHeaderHandler.java @@ -0,0 +1,41 @@ +package datawave.security.servlet; + +import datawave.security.util.SecurityConstants; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HttpString; + +/** + * A {@link HttpHandler} that will add a {@value SecurityConstants#REQUEST_START_TIME_HEADER} request header with the current system time in nanoseconds. This + * handler is expected to be configured to execute before the calling user for the request is authenticated. + */ +public class RequestStartTimeHeaderHandler implements HttpHandler { + + private static final HttpString header = new HttpString(SecurityConstants.REQUEST_START_TIME_HEADER); + + private final HttpHandler next; + + /** + * Create a new {@link RequestStartTimeHeaderHandler} with a reference to the next handler that the incoming exchange should be passed to. + * + * @param next + * the next handler + */ + public RequestStartTimeHeaderHandler(HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + try { + // Add the header. + long startTime = System.nanoTime(); + exchange.getRequestHeaders().add(header, startTime); + } finally { + // Pass the exchange to the next handler. + if (this.next != null) { + this.next.handleRequest(exchange); + } + } + } +} diff --git a/web-services/security-parent/security/src/main/java/datawave/security/system/SSLStoresProducer.java b/web-services/security-parent/security/src/main/java/datawave/security/system/SSLStoresProducer.java new file mode 100644 index 00000000000..8cc1905b7ee --- /dev/null +++ b/web-services/security-parent/security/src/main/java/datawave/security/system/SSLStoresProducer.java @@ -0,0 +1,42 @@ +package datawave.security.system; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Default; +import javax.enterprise.inject.Produces; + +import datawave.security.cert.SSLStores; +import datawave.security.cert.SSLStoresImpl; + +/** + * A producer class for producing server-security related artifacts. + */ +@ApplicationScoped +public class SSLStoresProducer { + + /** + * Allow injection of an {@link SSLStores} instance that is instantiated via system properties set via wildfly. This is intended to be the default way to + * configure the instance of {@link SSLStores} used throughout the application. + * + * @return the new {@link SSLStores} instance + * @throws Exception + * if an error occurs while creating the {@link SSLStores} instance + */ + @Produces + @Default + public SSLStores produceSSlStores() throws Exception { + String keyStoreUrl = System.getProperty("dw.ssl.context.info.keyStoreURL"); + String keyStorePassword = System.getProperty("dw.ssl.context.info.keyStorePassword"); + String keyStoreType = System.getProperty("dw.ssl.context.info.keyStoreType"); + String trustStoreUrl = System.getProperty("dw.ssl.context.info.trustStoreURL"); + String trustStorePassword = System.getProperty("dw.ssl.context.info.trustStorePassword"); + String trustStoreType = System.getProperty("dw.ssl.context.info.trustStoreType"); + + // @formatter:off + return SSLStoresImpl.builder() + .withKeystore(keyStoreUrl, keyStorePassword, keyStoreType) + .withTruststore(trustStoreUrl, trustStorePassword, trustStoreType) + .build(); + // @formatter:on + } + +} diff --git a/web-services/security-parent/security/src/main/java/datawave/security/system/SecurityEJBProviderImpl.java b/web-services/security-parent/security/src/main/java/datawave/security/system/SecurityEJBProviderImpl.java new file mode 100644 index 00000000000..2b60f67ebcc --- /dev/null +++ b/web-services/security-parent/security/src/main/java/datawave/security/system/SecurityEJBProviderImpl.java @@ -0,0 +1,71 @@ +package datawave.security.system; + +import javax.annotation.security.PermitAll; +import javax.ejb.Singleton; +import javax.inject.Inject; + +import datawave.configuration.spring.BeanProvider; +import datawave.security.authorization.DatawaveUserService; +import datawave.security.cache.ElytronCacheManager; +import datawave.security.cert.SSLStores; + +/** + * Implementation of {@link SecurityEJBProvider} that will inject EJBs and make them accessible to the datawave elytron module. In order to allow methods on + * this class to be invoked before authentication occurs in the elytron module, we must use {@link PermitAll}. It is expected that this singleton will be bound + * to JNDI for lookup by the elytron module. + */ +@PermitAll +@Singleton +public class SecurityEJBProviderImpl implements SecurityEJBProvider { + + private DatawaveUserService datawaveUserService; + + private SSLStores sslStores; + + private ElytronCacheManager elytronCacheManager; + + private boolean injectedBeans = false; + + @Inject + public void setDatawaveUserService(DatawaveUserService datawaveUserService) { + this.datawaveUserService = datawaveUserService; + } + + @Override + public DatawaveUserService getDatawaveUserService() { + injectBeans(); + return datawaveUserService; + } + + @Inject + public void setSSLContextInfo(SSLStores sslStores) { + this.sslStores = sslStores; + } + + @Override + public SSLStores getSSLStores() { + injectBeans(); + return sslStores; + } + + @Inject + public void setElytronCacheRegister(ElytronCacheManager elytronCacheManager) { + this.elytronCacheManager = elytronCacheManager; + } + + @Override + public ElytronCacheManager getElytronCacheManager() { + injectBeans(); + return elytronCacheManager; + } + + /** + * Injects beans here if not yet injected. This is necessary when the getters are being invoked within the external Datawave Elytron module. + */ + private void injectBeans() { + if (!injectedBeans) { + BeanProvider.injectFields(this); + injectedBeans = true; + } + } +} diff --git a/web-services/security/src/main/java/datawave/security/system/ServerSecurityProducer.java b/web-services/security-parent/security/src/main/java/datawave/security/system/ServerSecurityProducer.java similarity index 74% rename from web-services/security/src/main/java/datawave/security/system/ServerSecurityProducer.java rename to web-services/security-parent/security/src/main/java/datawave/security/system/ServerSecurityProducer.java index 50583176d5d..9bf8172d61e 100644 --- a/web-services/security/src/main/java/datawave/security/system/ServerSecurityProducer.java +++ b/web-services/security-parent/security/src/main/java/datawave/security/system/ServerSecurityProducer.java @@ -5,29 +5,25 @@ import java.security.cert.X509Certificate; import java.util.Collections; -import javax.annotation.Resource; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.RequestScoped; import javax.enterprise.inject.Produces; import javax.inject.Inject; -import org.jboss.security.JSSESecurityDomain; - import datawave.security.authorization.DatawavePrincipal; import datawave.security.authorization.DatawaveUserService; import datawave.security.authorization.SubjectIssuerDNPair; +import datawave.security.cert.SSLStores; import datawave.security.user.UserOperationsBean; /** - * A producer class for generating server-security related artifacts. For one, we produce the server DN of the server that we are running inside of. We allso - * produce the {@link JSSESecurityDomain} for our application. We use this rather than directly injecting at each site using {@link Resource} since the producer - * allows us to use a plain {@link javax.inject.Inject} annotation versus having to specify the resource name each time we inject with {@link Resource}. This - * way, we only name the resource once. + * A producer class for generating server-security related artifacts. For one, we produce the server DN of the server that we are running inside of. */ @ApplicationScoped public class ServerSecurityProducer { + @Inject - private JSSESecurityDomain domain; + private SSLStores sslStores; @Inject private DatawaveUserService datawaveUserService; @@ -40,13 +36,11 @@ public class ServerSecurityProducer { * from the {@link javax.ejb.EJBContext} of an EJB. * * @return the principal of the calling user - * @throws Exception - * if there are issues */ @Produces @CallerPrincipal @RequestScoped - public DatawavePrincipal produceCallerPrincipal() throws Exception { + public DatawavePrincipal produceCallerPrincipal() { DatawavePrincipal dp = userOperationsBean.getCurrentPrincipal(); return dp == null ? DatawavePrincipal.anonymousPrincipal() : dp; } @@ -67,11 +61,11 @@ public DatawavePrincipal produceServerPrincipal() throws Exception { } private SubjectIssuerDNPair lookupServerDN() throws KeyStoreException { - if (domain == null) { - throw new IllegalArgumentException("Unable to find security domain."); + if (sslStores == null) { + throw new IllegalStateException("SSL Context not injected."); } - KeyStore keystore = domain.getKeyStore(); + KeyStore keystore = sslStores.getKeyStore(); final X509Certificate cert = (X509Certificate) keystore.getCertificate(keystore.aliases().nextElement()); final String serverDN = cert.getSubjectX500Principal().getName(); final String serverIssuerDN = cert.getIssuerX500Principal().getName(); diff --git a/web-services/security/src/main/java/datawave/security/user/TextMessageBodyWriter.java b/web-services/security-parent/security/src/main/java/datawave/security/user/TextMessageBodyWriter.java similarity index 100% rename from web-services/security/src/main/java/datawave/security/user/TextMessageBodyWriter.java rename to web-services/security-parent/security/src/main/java/datawave/security/user/TextMessageBodyWriter.java diff --git a/web-services/security/src/main/java/datawave/security/user/UserOperationsBean.java b/web-services/security-parent/security/src/main/java/datawave/security/user/UserOperationsBean.java similarity index 100% rename from web-services/security/src/main/java/datawave/security/user/UserOperationsBean.java rename to web-services/security-parent/security/src/main/java/datawave/security/user/UserOperationsBean.java diff --git a/web-services/security-parent/security/src/main/java/datawave/security/websocket/WebsocketSecurityConfigurator.java b/web-services/security-parent/security/src/main/java/datawave/security/websocket/WebsocketSecurityConfigurator.java new file mode 100644 index 00000000000..8b51511fba1 --- /dev/null +++ b/web-services/security-parent/security/src/main/java/datawave/security/websocket/WebsocketSecurityConfigurator.java @@ -0,0 +1,57 @@ +package datawave.security.websocket; + +import static datawave.security.util.SecurityConstants.REQUEST_LOGIN_TIME_HEADER; + +import java.util.List; +import java.util.Map; + +import javax.websocket.HandshakeResponse; +import javax.websocket.server.HandshakeRequest; +import javax.websocket.server.ServerEndpointConfig; +import javax.websocket.server.ServerEndpointConfig.Configurator; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.wildfly.security.auth.server.SecurityDomain; +import org.wildfly.security.auth.server.SecurityIdentity; + +/** + * A JBOSS AS/Wildfly-specific {@link Configurator} that stores the invoking user's security identity and the request login time into the user session so that + * websocket handler methods can be invoked via the security identity. This covers a hole in the specifications that does not allow for the propagation of + * security credentials to websocket handlers. See Jakarta EE #238 for more details. + */ +public class WebsocketSecurityConfigurator extends Configurator { + + public static final String SESSION_SECURITY_IDENTITY = "websocket.security.identity"; + + private static final Logger log = LoggerFactory.getLogger(WebsocketSecurityConfigurator.class); + + @Override + public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { + super.modifyHandshake(sec, request, response); + + // Store the current security identity in the user properties so that it will be accessible later. This method will be invoked when the http request is + // upgraded to websocket after authentication has completed. The current security identity will represent the calling user who started the websocket + // session. + SecurityIdentity identity = SecurityDomain.getCurrent().getCurrentSecurityIdentity(); + Map userProperties = sec.getUserProperties(); + if (userProperties != null) { + userProperties.put(SESSION_SECURITY_IDENTITY, identity); + if (log.isTraceEnabled()) { + log.trace("Stored security identity in user property {}", SESSION_SECURITY_IDENTITY); + } + } + + // Store the request login time in the user properties so that it will be accessible later. + Map> headers = request.getHeaders(); + if (headers != null) { + List loginHeader = headers.get(REQUEST_LOGIN_TIME_HEADER); + if (loginHeader != null && !loginHeader.isEmpty()) { + sec.getUserProperties().put(REQUEST_LOGIN_TIME_HEADER, loginHeader.get(0)); + if (log.isTraceEnabled()) { + log.trace("Stored request login time in user property {}", REQUEST_LOGIN_TIME_HEADER); + } + } + } + } +} diff --git a/web-services/security/src/main/resources/META-INF/beans.xml b/web-services/security-parent/security/src/main/resources/META-INF/beans.xml similarity index 100% rename from web-services/security/src/main/resources/META-INF/beans.xml rename to web-services/security-parent/security/src/main/resources/META-INF/beans.xml diff --git a/web-services/security/src/main/resources/META-INF/jboss-ejb3.xml b/web-services/security-parent/security/src/main/resources/META-INF/jboss-ejb3.xml similarity index 100% rename from web-services/security/src/main/resources/META-INF/jboss-ejb3.xml rename to web-services/security-parent/security/src/main/resources/META-INF/jboss-ejb3.xml diff --git a/web-services/security-parent/security/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension b/web-services/security-parent/security/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension new file mode 100644 index 00000000000..3c690e73608 --- /dev/null +++ b/web-services/security-parent/security/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension @@ -0,0 +1 @@ +datawave.security.servlet.HeaderEnrichmentServletExtension diff --git a/web-services/security-parent/security/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/web-services/security-parent/security/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension new file mode 100644 index 00000000000..0f579d4c03a --- /dev/null +++ b/web-services/security-parent/security/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -0,0 +1 @@ +datawave.configuration.ConfigExtension diff --git a/web-services/security/src/test/java/datawave/security/authorization/remote/ConditionalRemoteUserOperationsTest.java b/web-services/security-parent/security/src/test/java/datawave/security/authorization/remote/ConditionalRemoteUserOperationsTest.java similarity index 100% rename from web-services/security/src/test/java/datawave/security/authorization/remote/ConditionalRemoteUserOperationsTest.java rename to web-services/security-parent/security/src/test/java/datawave/security/authorization/remote/ConditionalRemoteUserOperationsTest.java diff --git a/web-services/security/src/test/java/datawave/security/authorization/remote/RemoteUserOperationsImplHttpTest.java b/web-services/security-parent/security/src/test/java/datawave/security/authorization/remote/RemoteUserOperationsImplHttpTest.java similarity index 96% rename from web-services/security/src/test/java/datawave/security/authorization/remote/RemoteUserOperationsImplHttpTest.java rename to web-services/security-parent/security/src/test/java/datawave/security/authorization/remote/RemoteUserOperationsImplHttpTest.java index 5b620cd4ae9..82ae7bff514 100644 --- a/web-services/security/src/test/java/datawave/security/authorization/remote/RemoteUserOperationsImplHttpTest.java +++ b/web-services/security-parent/security/src/test/java/datawave/security/authorization/remote/RemoteUserOperationsImplHttpTest.java @@ -27,7 +27,6 @@ import org.apache.accumulo.core.security.Authorizations; import org.apache.commons.io.IOUtils; -import org.jboss.security.JSSESecurityDomain; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -57,12 +56,13 @@ import datawave.security.authorization.ProxiedUserDetails; import datawave.security.authorization.SubjectIssuerDNPair; import datawave.security.authorization.UserOperations; -import datawave.security.util.DnUtils; +import datawave.security.cert.SSLStores; +import datawave.security.util.DnProperties; import datawave.user.AuthorizationsListBase; import datawave.user.DefaultAuthorizationsList; import datawave.webservice.common.json.DefaultMapperDecorator; import datawave.webservice.common.json.ObjectMapperDecorator; -import datawave.webservice.common.remote.TestJSSESecurityDomain; +import datawave.webservice.common.remote.TestSSLStores; import datawave.webservice.dictionary.data.DataDictionaryBase; import datawave.webservice.dictionary.data.DescriptionBase; import datawave.webservice.dictionary.data.FieldsBase; @@ -112,7 +112,7 @@ public ManagedExecutorService executorService() { } @Bean - public JSSESecurityDomain jsseSecurityDomain() throws CertificateException, NoSuchAlgorithmException { + public SSLStores sslStores() throws CertificateException, NoSuchAlgorithmException { String alias = "tomcat"; char[] keyPass = "changeit".toCharArray(); int keysize = 2048; @@ -131,7 +131,7 @@ public JSSESecurityDomain jsseSecurityDomain() throws CertificateException, NoSu .setSigningKey(keypair.getPrivate()).setSignatureAlgorithmName("SHA256withRSA"); chain[0] = builder.build(); - return new TestJSSESecurityDomain(alias, privKey, keyPass, chain); + return new TestSSLStores(alias, privKey, keyPass, chain); } @Bean @@ -179,7 +179,7 @@ public RemoteUserOperationsImpl remote(HttpServer server) { @Before public void setup() throws Exception { final ObjectMapper objectMapper = new DefaultMapperDecorator().decorate(new ObjectMapper()); - System.setProperty(DnUtils.SUBJECT_DN_PATTERN_PROPERTY, ".*ou=server.*"); + System.setProperty(DnProperties.SUBJECT_DN_PATTERN_PROPERTY, ".*ou=server.*"); setListEffectiveAuthResponse(userDN, auths); diff --git a/web-services/security/src/test/java/datawave/security/authorization/remote/RemoteUserOperationsImplIT.java b/web-services/security-parent/security/src/test/java/datawave/security/authorization/remote/RemoteUserOperationsImplIT.java similarity index 100% rename from web-services/security/src/test/java/datawave/security/authorization/remote/RemoteUserOperationsImplIT.java rename to web-services/security-parent/security/src/test/java/datawave/security/authorization/remote/RemoteUserOperationsImplIT.java diff --git a/web-services/security/src/test/java/datawave/security/authorization/remote/RemoteUserOperationsUtil.java b/web-services/security-parent/security/src/test/java/datawave/security/authorization/remote/RemoteUserOperationsUtil.java similarity index 91% rename from web-services/security/src/test/java/datawave/security/authorization/remote/RemoteUserOperationsUtil.java rename to web-services/security-parent/security/src/test/java/datawave/security/authorization/remote/RemoteUserOperationsUtil.java index 5b2be32f4d0..62404910e6e 100644 --- a/web-services/security/src/test/java/datawave/security/authorization/remote/RemoteUserOperationsUtil.java +++ b/web-services/security-parent/security/src/test/java/datawave/security/authorization/remote/RemoteUserOperationsUtil.java @@ -18,16 +18,16 @@ import javax.ws.rs.core.MediaType; import org.apache.commons.io.IOUtils; -import org.jboss.security.JSSESecurityDomain; import org.wildfly.security.x500.cert.X509CertificateBuilder; import com.fasterxml.jackson.databind.ObjectMapper; import com.sun.net.httpserver.HttpHandler; +import datawave.security.cert.SSLStores; import datawave.user.DefaultAuthorizationsList; import datawave.webservice.common.json.DefaultMapperDecorator; import datawave.webservice.common.remote.RemoteServiceUtil; -import datawave.webservice.common.remote.TestJSSESecurityDomain; +import datawave.webservice.common.remote.TestSSLStores; public class RemoteUserOperationsUtil extends RemoteServiceUtil { private AtomicBoolean interrupt; @@ -59,12 +59,13 @@ public RemoteUserOperationsImpl getUserOperations() throws CertificateException, remote.setExecutorService(null); remote.setObjectMapperDecorator(new DefaultMapperDecorator()); remote.setResponseObjectFactory(new RemoteUserOperationsImplHttpTest.MockResponseObjectFactory()); - remote.setJsseSecurityDomain(jsseSecurityDomain()); + + remote.setSslStores(sslStores()); return remote; } - private JSSESecurityDomain jsseSecurityDomain() throws CertificateException, NoSuchAlgorithmException { + private SSLStores sslStores() throws CertificateException, NoSuchAlgorithmException { String alias = "tomcat"; char[] keyPass = "changeit".toCharArray(); int keysize = 2048; @@ -83,7 +84,7 @@ private JSSESecurityDomain jsseSecurityDomain() throws CertificateException, NoS .setSigningKey(keypair.getPrivate()).setSignatureAlgorithmName("SHA256withRSA"); chain[0] = builder.build(); - return new TestJSSESecurityDomain(alias, privKey, keyPass, chain); + return new TestSSLStores(alias, privKey, keyPass, chain); } public HttpHandler getEmptyResponseHandler() { diff --git a/web-services/security/src/test/java/datawave/security/authorization/test/TestDatawaveUserServiceTest.java b/web-services/security-parent/security/src/test/java/datawave/security/authorization/test/TestDatawaveUserServiceTest.java similarity index 100% rename from web-services/security/src/test/java/datawave/security/authorization/test/TestDatawaveUserServiceTest.java rename to web-services/security-parent/security/src/test/java/datawave/security/authorization/test/TestDatawaveUserServiceTest.java diff --git a/web-services/security-parent/security/src/test/java/datawave/security/cache/CredentialsCacheBeanTest.java b/web-services/security-parent/security/src/test/java/datawave/security/cache/CredentialsCacheBeanTest.java new file mode 100644 index 00000000000..a60c24efc1c --- /dev/null +++ b/web-services/security-parent/security/src/test/java/datawave/security/cache/CredentialsCacheBeanTest.java @@ -0,0 +1,248 @@ +package datawave.security.cache; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.lang.annotation.Annotation; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.enterprise.inject.Instance; +import javax.enterprise.util.TypeLiteral; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import datawave.core.common.connection.AccumuloConnectionFactory; +import datawave.security.DnList; +import datawave.security.authorization.CachedDatawaveUserService; +import datawave.security.authorization.DatawavePrincipal; +import datawave.security.authorization.DatawaveUser; +import datawave.security.authorization.DatawaveUserInfo; +import datawave.security.authorization.SubjectIssuerDNPair; + +@ExtendWith(MockitoExtension.class) +public class CredentialsCacheBeanTest { + + private CredentialsCacheBean bean; + + @Mock + private ElytronCacheManager elytronCacheManager; + + @Mock + private CachedDatawaveUserService cachedDatawaveUserService; + + @Mock + private AccumuloConnectionFactory connectionFactory; + + private final DatawaveUser user1 = new DatawaveUser(SubjectIssuerDNPair.of("user1", "issuer1"), DatawaveUser.UserType.USER, null, null, null, -1); + private final DatawaveUser user2 = new DatawaveUser(SubjectIssuerDNPair.of("user2", "issuer2"), DatawaveUser.UserType.USER, null, null, null, -1); + private final DatawaveUser server1 = new DatawaveUser(SubjectIssuerDNPair.of("server1", "issuer1"), DatawaveUser.UserType.SERVER, null, null, null, -1); + + private final DatawavePrincipal principal1 = new DatawavePrincipal(List.of(user1, server1)); + private final DatawavePrincipal principal2 = new DatawavePrincipal(List.of(user1)); + private final DatawavePrincipal principal3 = new DatawavePrincipal(List.of(user2, server1)); + + @BeforeEach + public void setUp() { + bean = new CredentialsCacheBean(); + bean.setElytronCache(elytronCacheManager); + bean.setCachedDatawaveUserServiceInstance(new TestInstance()); + bean.setAccumuloConnectionFactory(connectionFactory); + } + + /** + * When a {@link CachedDatawaveUserService} is not set in the bean, verify that all cache-related methods delegate to the elytron cache only. + */ + @DisplayName("When a CachedDatawaveUserService is not present") + @Nested + class NoCachedDatawaveUserService { + + @BeforeEach + void setUp() { + bean.setCachedDatawaveUserServiceInstance(new TestInstance()); + } + + /** + * When {@link CredentialsCacheBean#flushAll()} is called, verify that the elytron cache evicts all entries. + */ + @Test + public void testFlushAll() { + bean.flushAll(); + verify(elytronCacheManager).clear(); + verifyNoInteractions(cachedDatawaveUserService); + } + + /** + * When {@link CredentialsCacheBean#evict(String)} is called, verify that the elytron cache evicts entries matching the given DN. + */ + @Test + void testEvict() { + bean.evict("user1"); + verify(elytronCacheManager).evictUsersWithName("user1"); + verifyNoInteractions(cachedDatawaveUserService); + } + + /** + * When {@link CredentialsCacheBean#listDNs(boolean)}, regardless of the value of localOnly, verify that DNs are only fetched from the elytron cache. + * @param localOnly whether only the local cache should be used. + */ + @ParameterizedTest() + @ValueSource(booleans = {true, false}) + void testListDns(boolean localOnly) { + when(elytronCacheManager.getUsers()).thenReturn(Set.of(user1, user2, server1)); + DnList dnList = bean.listDNs(localOnly); + assertEquals(List.of("user2", "server1", "user1"), List.copyOf(dnList.getDns())); + verifyNoInteractions(cachedDatawaveUserService); + } + + /** + * When {@link CredentialsCacheBean#listDNsMatching(String)} is called, verify that the elytron cache is used. + */ + @Test + void testListDnsMatching() { + when(elytronCacheManager.getUsersWhereNameContains("issuer1")).thenReturn(Set.of(user1, server1)); + DnList dnList = bean.listDNsMatching("issuer1"); + assertEquals(List.of("server1", "user1"), List.copyOf(dnList.getDns())); + verifyNoInteractions(cachedDatawaveUserService); + } + + /** + * When {@link CredentialsCacheBean#list(String)} is called, verify that the elytron cache is used. + */ + @Test + void testList() { + when(elytronCacheManager.getUserWithName("user1")).thenReturn(user1); + DatawaveUser user = bean.list("user1"); + assertSame(user1, user); + verifyNoInteractions(cachedDatawaveUserService); + } + } + + /** + * When a {@link CachedDatawaveUserService} is present, verify that caching methods are for the most part, delegated to the user service only except in some + * cases. + */ + @DisplayName("When a CachedDatawaveUserService is present") + @Nested + class CachedDatawaveUserServicePresent { + + @BeforeEach + void setUp() { + bean.setCachedDatawaveUserServiceInstance(new TestInstance(cachedDatawaveUserService)); + } + + @Test + public void testFlushAll() { + bean.flushAll(); + verify(elytronCacheManager).clear(); + verify(cachedDatawaveUserService).evictAll(); + } + + @Test + void testEvict() { + bean.evict("user1"); + verify(elytronCacheManager).evictUsersWithName("user1"); + verify(cachedDatawaveUserService).evictMatching("user1"); + } + + @Test + void testListDnsGivenNotLocalOnly() { + List userInfos = Stream.of(user1, user2, server1).map(DatawaveUserInfo::new).collect(Collectors.toList()); + Mockito.doReturn(userInfos).when(cachedDatawaveUserService).listAll(); + DnList dnList = bean.listDNs(false); + assertEquals(List.of("user2", "server1", "user1"), List.copyOf(dnList.getDns())); + verifyNoInteractions(elytronCacheManager); + } + + @Test + void testListDnsGivenLocalOnly() { + when(elytronCacheManager.getUsers()).thenReturn(Set.of(user1, user2, server1)); + DnList dnList = bean.listDNs(true); + assertEquals(List.of("user2", "server1", "user1"), List.copyOf(dnList.getDns())); + verifyNoInteractions(cachedDatawaveUserService); + } + + @Test + void testListDnsMatching() { + List userInfos = Stream.of(user1, server1).map(DatawaveUserInfo::new).collect(Collectors.toList()); + Mockito.doReturn(userInfos).when(cachedDatawaveUserService).listMatching("issuer1"); + DnList dnList = bean.listDNsMatching("issuer1"); + assertEquals(List.of("server1", "user1"), List.copyOf(dnList.getDns())); + verifyNoInteractions(elytronCacheManager); + } + + @Test + void testList() { + when(cachedDatawaveUserService.list("user1")).thenReturn(user1); + DatawaveUser user = bean.list("user1"); + assertSame(user1, user); + verifyNoInteractions(elytronCacheManager); + } + } + + private static class TestInstance implements Instance { + + public CachedDatawaveUserService cachedDatawaveUserService; + + public TestInstance() {} + + public TestInstance(CachedDatawaveUserService cachedDatawaveUserService) { + this.cachedDatawaveUserService = cachedDatawaveUserService; + } + + @Override + public Instance select(Annotation... qualifiers) { + return null; + } + + @Override + public Instance select(Class subtype, Annotation... qualifiers) { + return null; + } + + @Override + public Instance select(TypeLiteral subtype, Annotation... qualifiers) { + return null; + } + + @Override + public boolean isUnsatisfied() { + return this.cachedDatawaveUserService == null; + } + + @Override + public boolean isAmbiguous() { + return false; + } + + @Override + public void destroy(CachedDatawaveUserService instance) { + + } + + @Override + public Iterator iterator() { + return null; + } + + @Override + public CachedDatawaveUserService get() { + return this.cachedDatawaveUserService; + } + } +} diff --git a/web-services/security-parent/security/src/test/java/datawave/security/servlet/RequestLoginTimeHeaderHandlerTest.java b/web-services/security-parent/security/src/test/java/datawave/security/servlet/RequestLoginTimeHeaderHandlerTest.java new file mode 100644 index 00000000000..3614f492055 --- /dev/null +++ b/web-services/security-parent/security/src/test/java/datawave/security/servlet/RequestLoginTimeHeaderHandlerTest.java @@ -0,0 +1,65 @@ +package datawave.security.servlet; + +import static io.smallrye.common.constraint.Assert.assertFalse; +import static io.smallrye.common.constraint.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import datawave.security.util.SecurityConstants; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderMap; +import io.undertow.util.HeaderValues; +import io.undertow.util.HttpString; + +class RequestLoginTimeHeaderHandlerTest { + + /** + * Verify that when an http request is passed to {@link RequestLoginTimeHeaderHandler#handleRequest(HttpServerExchange)} with a request start time header, + * that a request login time header is added with the delta in milliseconds. + */ + @Test + void testHandleRequestGivenStartTimeHeader() throws Exception { + HttpServerExchange exchange = new HttpServerExchange(null); + + // Add a start time header with a time 5 seconds ago. + HeaderMap headerMap = exchange.getRequestHeaders(); + long startTime = System.nanoTime() - (TimeUnit.SECONDS.toNanos(5)); + headerMap.put(new HttpString(SecurityConstants.REQUEST_START_TIME_HEADER), String.valueOf(startTime)); + + RequestLoginTimeHeaderHandler handler = new RequestLoginTimeHeaderHandler(null); + handler.handleRequest(exchange); + + // Verify that one value was added for the request start header. + headerMap = exchange.getRequestHeaders(); + assertTrue(headerMap.contains(SecurityConstants.REQUEST_LOGIN_TIME_HEADER)); + HeaderValues values = headerMap.get(SecurityConstants.REQUEST_LOGIN_TIME_HEADER); + assertEquals(1, values.size()); + + // Verify that the value is approximately 5 seconds in milliseconds. + long requestStartTime = Long.parseLong(values.iterator().next()); + long fiveSeconds = TimeUnit.SECONDS.toMillis(5); + long sixSeconds = TimeUnit.SECONDS.toMillis(6); + + assertTrue(requestStartTime >= fiveSeconds); + assertTrue(requestStartTime <= sixSeconds); + } + + /** + * Verify that when an http request is passed to {@link RequestLoginTimeHeaderHandler#handleRequest(HttpServerExchange)} without a request start time + * header, that a request login time header is not added. + */ + @Test + void testHandleRequestGivenNoStartTimeHeader() throws Exception { + HttpServerExchange exchange = new HttpServerExchange(null); + + RequestLoginTimeHeaderHandler handler = new RequestLoginTimeHeaderHandler(null); + handler.handleRequest(exchange); + + HeaderMap headerMap = exchange.getRequestHeaders(); + // Verify that one value was added for the request start header. + assertFalse(headerMap.contains(SecurityConstants.REQUEST_LOGIN_TIME_HEADER)); + } +} diff --git a/web-services/security-parent/security/src/test/java/datawave/security/servlet/RequestStartTimeHeaderHandlerTest.java b/web-services/security-parent/security/src/test/java/datawave/security/servlet/RequestStartTimeHeaderHandlerTest.java new file mode 100644 index 00000000000..d8cdf1c1abe --- /dev/null +++ b/web-services/security-parent/security/src/test/java/datawave/security/servlet/RequestStartTimeHeaderHandlerTest.java @@ -0,0 +1,41 @@ +package datawave.security.servlet; + +import static io.smallrye.common.constraint.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import datawave.security.util.SecurityConstants; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderMap; +import io.undertow.util.HeaderValues; + +class RequestStartTimeHeaderHandlerTest { + + /** + * Verify that when an http request is passed to {@link RequestStartTimeHeaderHandler#handleRequest(HttpServerExchange)}, that the request start time is + * added with the system's current nano time. + */ + @Test + void testHandleRequest() throws Exception { + HttpServerExchange exchange = new HttpServerExchange(null); + + long timeBeforeAddition = System.nanoTime(); + + RequestStartTimeHeaderHandler handler = new RequestStartTimeHeaderHandler(null); + handler.handleRequest(exchange); + + long timeAfterAddition = System.nanoTime(); + + // Verify that one value was added for the request start header. + HeaderMap headerMap = exchange.getRequestHeaders(); + assertTrue(headerMap.contains(SecurityConstants.REQUEST_START_TIME_HEADER)); + HeaderValues values = headerMap.get(SecurityConstants.REQUEST_START_TIME_HEADER); + assertEquals(1, values.size()); + + // Verify that the value was the current system time. + long requestStartTime = Long.parseLong(values.iterator().next()); + assertTrue(requestStartTime >= timeBeforeAddition); + assertTrue(requestStartTime <= timeAfterAddition); + } +} diff --git a/web-services/security/src/test/java/datawave/security/user/ListEffectiveAuthorizationsTest.java b/web-services/security-parent/security/src/test/java/datawave/security/user/ListEffectiveAuthorizationsTest.java similarity index 95% rename from web-services/security/src/test/java/datawave/security/user/ListEffectiveAuthorizationsTest.java rename to web-services/security-parent/security/src/test/java/datawave/security/user/ListEffectiveAuthorizationsTest.java index c1b1de950b0..48a6bffd944 100644 --- a/web-services/security/src/test/java/datawave/security/user/ListEffectiveAuthorizationsTest.java +++ b/web-services/security-parent/security/src/test/java/datawave/security/user/ListEffectiveAuthorizationsTest.java @@ -4,7 +4,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import java.security.Principal; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; @@ -16,7 +15,6 @@ import org.easymock.EasyMock; import org.easymock.EasyMockSupport; -import org.jboss.security.CacheableManager; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; @@ -40,7 +38,7 @@ import datawave.security.authorization.SubjectIssuerDNPair; import datawave.security.authorization.UserOperations; import datawave.security.cache.CredentialsCacheBean; -import datawave.security.system.AuthorizationCache; +import datawave.security.cache.ElytronCacheManager; import datawave.user.AuthorizationsListBase; import datawave.user.DefaultAuthorizationsList; import datawave.webservice.query.result.event.ResponseObjectFactory; @@ -51,7 +49,7 @@ public class ListEffectiveAuthorizationsTest extends EasyMockSupport { private static ResponseObjectFactory mockResponseObjectFactory; private static EJBContext mockEJBContext; private static CredentialsCacheBean mockCredentialsCache; - private static CacheableManager mockCacheManager; + private static ElytronCacheManager mockElytronCacheManager; private static Instance mockCachedDatawaveUserService; private static AccumuloConnectionFactory mockAccumuloConnectionFactory; private static UserOperations mockRemoteUserOperations1; @@ -61,7 +59,7 @@ public static void setupStatic() { mockResponseObjectFactory = EasyMock.createMock(ResponseObjectFactory.class); mockEJBContext = EasyMock.createMock(EJBContext.class); mockCredentialsCache = EasyMock.createMock(CredentialsCacheBean.class); - mockCacheManager = EasyMock.createMock(CacheableManager.class); + mockElytronCacheManager = EasyMock.createMock(ElytronCacheManager.class); mockCachedDatawaveUserService = EasyMock.createMock(Instance.class); mockAccumuloConnectionFactory = EasyMock.createMock(AccumuloConnectionFactory.class); mockRemoteUserOperations1 = EasyMock.createMock(UserOperations.class); @@ -70,21 +68,21 @@ public static void setupStatic() { @Override public void replayAll() { super.replayAll(); - EasyMock.replay(mockResponseObjectFactory, mockEJBContext, mockCredentialsCache, mockCacheManager, mockCachedDatawaveUserService, + EasyMock.replay(mockResponseObjectFactory, mockEJBContext, mockCredentialsCache, mockElytronCacheManager, mockCachedDatawaveUserService, mockAccumuloConnectionFactory, mockRemoteUserOperations1); } @Override public void verifyAll() { super.verifyAll(); - EasyMock.verify(mockResponseObjectFactory, mockEJBContext, mockCredentialsCache, mockCacheManager, mockCachedDatawaveUserService, + EasyMock.verify(mockResponseObjectFactory, mockEJBContext, mockCredentialsCache, mockElytronCacheManager, mockCachedDatawaveUserService, mockAccumuloConnectionFactory, mockRemoteUserOperations1); } @Override public void resetAll() { super.resetAll(); - EasyMock.reset(mockResponseObjectFactory, mockEJBContext, mockCredentialsCache, mockCacheManager, mockCachedDatawaveUserService, + EasyMock.reset(mockResponseObjectFactory, mockEJBContext, mockCredentialsCache, mockElytronCacheManager, mockCachedDatawaveUserService, mockAccumuloConnectionFactory, mockRemoteUserOperations1); } @@ -122,9 +120,8 @@ public CredentialsCacheBean credentialsCache() { } @Bean - @AuthorizationCache - public CacheableManager authManager() { - return mockCacheManager; + public ElytronCacheManager elytronCacheManager() { + return mockElytronCacheManager; } @Bean diff --git a/web-services/security/src/test/resources/TestAuthServiceTestUsers.xml b/web-services/security-parent/security/src/test/resources/TestAuthServiceTestUsers.xml similarity index 100% rename from web-services/security/src/test/resources/TestAuthServiceTestUsers.xml rename to web-services/security-parent/security/src/test/resources/TestAuthServiceTestUsers.xml diff --git a/web-services/security/src/test/resources/ca.pkcs12 b/web-services/security-parent/security/src/test/resources/ca.pkcs12 similarity index 100% rename from web-services/security/src/test/resources/ca.pkcs12 rename to web-services/security-parent/security/src/test/resources/ca.pkcs12 diff --git a/web-services/security-parent/security/src/test/resources/datawave-server-keystore.jks b/web-services/security-parent/security/src/test/resources/datawave-server-keystore.jks new file mode 100644 index 00000000000..ca02e0bbb77 Binary files /dev/null and b/web-services/security-parent/security/src/test/resources/datawave-server-keystore.jks differ diff --git a/web-services/security-parent/security/src/test/resources/datawave-server-keystore.p12 b/web-services/security-parent/security/src/test/resources/datawave-server-keystore.p12 new file mode 100644 index 00000000000..b4ab0f3cb7f Binary files /dev/null and b/web-services/security-parent/security/src/test/resources/datawave-server-keystore.p12 differ diff --git a/web-services/security-parent/security/src/test/resources/datawave-server-truststore.jks b/web-services/security-parent/security/src/test/resources/datawave-server-truststore.jks new file mode 100644 index 00000000000..95feb046172 Binary files /dev/null and b/web-services/security-parent/security/src/test/resources/datawave-server-truststore.jks differ diff --git a/web-services/security-parent/security/src/test/resources/datawave-server-truststore.p12 b/web-services/security-parent/security/src/test/resources/datawave-server-truststore.p12 new file mode 100644 index 00000000000..ebd02da5fd5 Binary files /dev/null and b/web-services/security-parent/security/src/test/resources/datawave-server-truststore.p12 differ diff --git a/web-services/security-parent/security/src/test/resources/dnutils.properties b/web-services/security-parent/security/src/test/resources/dnutils.properties new file mode 100644 index 00000000000..0506661bd8f --- /dev/null +++ b/web-services/security-parent/security/src/test/resources/dnutils.properties @@ -0,0 +1,2 @@ +npe.ou.entries=iamnotaperson,npe,stillnotaperson +subject.dn.pattern=(?:^|,)\\s*OU\\s*=\\s*Subject DN OU\\s*(?:,|$) \ No newline at end of file diff --git a/web-services/security/src/test/resources/log4j.properties b/web-services/security-parent/security/src/test/resources/log4j.properties similarity index 59% rename from web-services/security/src/test/resources/log4j.properties rename to web-services/security-parent/security/src/test/resources/log4j.properties index cacd01b436c..b5c76e72bbb 100644 --- a/web-services/security/src/test/resources/log4j.properties +++ b/web-services/security-parent/security/src/test/resources/log4j.properties @@ -1,6 +1,9 @@ log4j.rootCategory=INFO, CONSOLE log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender -log4j.appender.CONSOLE.Threshold=INFO log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout log4j.appender.CONSOLE.layout.ConversionPattern=%-5p [%C{1}:%M] %m%n + +# See TRACE level events for the datawave.security package. +log4j.logger.datawave.security=TRACE, CONSOLE +log4j.additivity.datawave.security=false diff --git a/web-services/security/src/test/resources/roles.properties b/web-services/security-parent/security/src/test/resources/roles.properties similarity index 100% rename from web-services/security/src/test/resources/roles.properties rename to web-services/security-parent/security/src/test/resources/roles.properties diff --git a/web-services/security/src/test/resources/rolesNoIssuer.properties b/web-services/security-parent/security/src/test/resources/rolesNoIssuer.properties similarity index 100% rename from web-services/security/src/test/resources/rolesNoIssuer.properties rename to web-services/security-parent/security/src/test/resources/rolesNoIssuer.properties diff --git a/web-services/security/src/test/resources/springFrameworkBeanRefContext.xml b/web-services/security-parent/security/src/test/resources/springFrameworkBeanRefContext.xml similarity index 100% rename from web-services/security/src/test/resources/springFrameworkBeanRefContext.xml rename to web-services/security-parent/security/src/test/resources/springFrameworkBeanRefContext.xml diff --git a/web-services/security/src/test/resources/testAuthServiceBeanRefContext.xml b/web-services/security-parent/security/src/test/resources/testAuthServiceBeanRefContext.xml similarity index 100% rename from web-services/security/src/test/resources/testAuthServiceBeanRefContext.xml rename to web-services/security-parent/security/src/test/resources/testAuthServiceBeanRefContext.xml diff --git a/web-services/security/src/test/resources/testServer.pkcs12 b/web-services/security-parent/security/src/test/resources/testServer.pkcs12 similarity index 100% rename from web-services/security/src/test/resources/testServer.pkcs12 rename to web-services/security-parent/security/src/test/resources/testServer.pkcs12 diff --git a/web-services/security/src/test/resources/testUser.pkcs12 b/web-services/security-parent/security/src/test/resources/testUser.pkcs12 similarity index 100% rename from web-services/security/src/test/resources/testUser.pkcs12 rename to web-services/security-parent/security/src/test/resources/testUser.pkcs12 diff --git a/web-services/security/src/test/resources/users.properties b/web-services/security-parent/security/src/test/resources/users.properties similarity index 100% rename from web-services/security/src/test/resources/users.properties rename to web-services/security-parent/security/src/test/resources/users.properties diff --git a/web-services/security/.gitignore b/web-services/security/.gitignore deleted file mode 100644 index b83d22266ac..00000000000 --- a/web-services/security/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target/ diff --git a/web-services/security/src/main/java/datawave/security/auth/DatawaveAuthenticationMechanism.java b/web-services/security/src/main/java/datawave/security/auth/DatawaveAuthenticationMechanism.java deleted file mode 100644 index 60c5f23343d..00000000000 --- a/web-services/security/src/main/java/datawave/security/auth/DatawaveAuthenticationMechanism.java +++ /dev/null @@ -1,310 +0,0 @@ -package datawave.security.auth; - -import static datawave.webservice.metrics.Constants.REQUEST_LOGIN_TIME_HEADER; -import static datawave.webservice.metrics.Constants.REQUEST_START_TIME_HEADER; - -import java.io.IOException; -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.security.auth.login.AccountLockedException; -import javax.security.auth.login.CredentialException; -import javax.security.auth.login.FailedLoginException; -import javax.security.auth.login.LoginException; - -import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpStatus; -import org.jboss.security.SecurityContextAssociation; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.xnio.SslClientAuthMode; - -import datawave.security.util.ProxiedEntityUtils; -import io.undertow.security.api.AuthenticationMechanism; -import io.undertow.security.api.AuthenticationMechanismFactory; -import io.undertow.security.api.SecurityContext; -import io.undertow.security.idm.Account; -import io.undertow.security.idm.IdentityManager; -import io.undertow.security.impl.ClientCertAuthenticationMechanism; -import io.undertow.server.HttpServerExchange; -import io.undertow.server.RenegotiationRequiredException; -import io.undertow.server.SSLSessionInfo; -import io.undertow.server.handlers.form.FormParserFactory; -import io.undertow.util.HeaderMap; -import io.undertow.util.HeaderValues; -import io.undertow.util.HttpString; - -/** - * The custom DATAWAVE servlet authentication mechanism. This auth mechanism acts just like CLIENT-CERT if there is an SSL session found, and otherwise uses - * trusted headers. - */ -public class DatawaveAuthenticationMechanism implements AuthenticationMechanism { - public static final String PROXIED_ENTITIES_HEADER = "X-ProxiedEntitiesChain"; - public static final String PROXIED_ISSUERS_HEADER = "X-ProxiedIssuersChain"; - public static final String MECHANISM_NAME = "DATAWAVE-AUTH"; - private static final HttpString HEADER_START_TIME = new HttpString(REQUEST_START_TIME_HEADER); - private static final HttpString HEADER_LOGIN_TIME = new HttpString(REQUEST_LOGIN_TIME_HEADER); - protected static final HttpString HEADER_PROXIED_ENTITIES = new HttpString(PROXIED_ENTITIES_HEADER); - protected static final HttpString HEADER_PROXIED_ENTITIES_ACCEPTED = new HttpString("X-ProxiedEntitiesAccepted"); - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - private final String name; - /** - * If we should force a renegotiation if client certs were not supplied. true by default - */ - private final boolean forceRenegotiation; - private final IdentityManager identityManager; - protected final String SUBJECT_DN_HEADER; - protected final String ISSUER_DN_HEADER; - private final boolean trustedHeaderAuthentication; - private final boolean jwtHeaderAuthentication; - private final Set dnsToPrune; - private final Map,Integer> returnCodeMap = new HashMap(); - - @SuppressWarnings("UnusedDeclaration") - public DatawaveAuthenticationMechanism() { - this(MECHANISM_NAME, true, null); - } - - @SuppressWarnings("UnusedDeclaration") - public DatawaveAuthenticationMechanism(String mechanismName) { - this(mechanismName, true, null); - } - - @SuppressWarnings("UnusedDeclaration") - public DatawaveAuthenticationMechanism(boolean forceRenegotiation) { - this(MECHANISM_NAME, forceRenegotiation, null); - } - - public DatawaveAuthenticationMechanism(String mechanismName, boolean forceRenegotiation, IdentityManager identityManager) { - this.name = mechanismName; - this.forceRenegotiation = forceRenegotiation; - this.identityManager = identityManager; - trustedHeaderAuthentication = Boolean.valueOf(System.getProperty("dw.trusted.header.authentication", "false")); - jwtHeaderAuthentication = Boolean.valueOf(System.getProperty("dw.jwt.header.authentication", "false")); - String dns = System.getProperty("dw.trusted.proxied.entities", null); - if (!StringUtils.isEmpty(dns)) { - dnsToPrune = new HashSet<>(Arrays.asList(ProxiedEntityUtils.splitProxiedDNs(dns, false))); - } else { - dnsToPrune = null; - } - SUBJECT_DN_HEADER = System.getProperty("dw.trusted.header.subjectDn", "X-SSL-ClientCert-Subject".toLowerCase()); - ISSUER_DN_HEADER = System.getProperty("dw.trusted.header.issuerDn", "X-SSL-ClientCert-Issuer".toLowerCase()); - // These LoginExceptions are thrown from DatawavePrincipalLoginModule and - // caught and saved in the SecurityContext in JBossCachedAuthenticationManager. - - // there was some problem with the credential that prevented evaluation - returnCodeMap.put(CredentialException.class, HttpStatus.SC_UNAUTHORIZED); - // credential was evaluated and rejected - returnCodeMap.put(AccountLockedException.class, HttpStatus.SC_FORBIDDEN); - returnCodeMap.put(FailedLoginException.class, HttpStatus.SC_FORBIDDEN); - // there was a system error that prevented evaluation of the credential - returnCodeMap.put(LoginException.class, HttpStatus.SC_SERVICE_UNAVAILABLE); - } - - @Override - public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) { - // Pull proxied entity info from the headers. If proxied entities are there, but proxied issuers are missing, then fail authentication immediately. - String proxiedEntities; - String proxiedIssuers; - try { - proxiedEntities = getSingleHeader(exchange.getRequestHeaders(), PROXIED_ENTITIES_HEADER); - proxiedIssuers = getSingleHeader(exchange.getRequestHeaders(), PROXIED_ISSUERS_HEADER); - logger.trace("Authenticating with proxiedEntities={} and proxiedIssuers={}", proxiedEntities, proxiedIssuers); - if (proxiedEntities != null && proxiedIssuers == null) { - return notAuthenticated(exchange, securityContext, - PROXIED_ENTITIES_HEADER + " supplied, but missing " + PROXIED_ISSUERS_HEADER + " is missing!"); - } - } catch (MultipleHeaderException e) { - return notAuthenticated(exchange, securityContext, e.getMessage()); - } - - // Pull subject/issuer DN from the SSL client certificate if it's there. - DatawaveCredential credential = null; - SSLSessionInfo sslSession = exchange.getConnection().getSslSessionInfo(); - if (sslSession != null) { - try { - Certificate[] clientCerts = getPeerCertificates(exchange, sslSession, securityContext); - if (clientCerts[0] instanceof X509Certificate) { - X509Certificate certificate = (X509Certificate) clientCerts[0]; - credential = new DatawaveCredential(certificate, proxiedEntities, proxiedIssuers); - } - } catch (SSLPeerUnverifiedException e) { - // No action - this mechanism can not attempt authentication without peer certificates, so allow it to drop out to NOT_ATTEMPTED - } - } - // If we're using JWT authentication, then trust the JWT in the header. - if (jwtHeaderAuthentication) { - try { - String authorizationHeader = getSingleHeader(exchange.getRequestHeaders(), "Authorization"); - if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { - credential = new DatawaveCredential(authorizationHeader.substring(7)); - } - } catch (MultipleHeaderException e) { - return notAuthenticated(exchange, securityContext, e.getMessage()); - } - } - // If we're either not using SSL and/or didn't get user info from the SSL session, then get it from the trusted headers, if we're configured to do so. - if (credential == null && trustedHeaderAuthentication) { - try { - String subjectDN = getSingleHeader(exchange.getRequestHeaders(), SUBJECT_DN_HEADER); - String issuerDN = getSingleHeader(exchange.getRequestHeaders(), ISSUER_DN_HEADER); - logger.trace("Authenticating with trusted subject header={} and trusted issuer header={}", subjectDN, issuerDN); - // If no DN headers supplied, then report that we did not authenticate - if (subjectDN == null && issuerDN == null) { - return notAttempted(exchange); - } - // If one of subject or issuer is missing, then report authentication failure. - if (subjectDN == null || issuerDN == null) { - return notAuthenticated(exchange, securityContext, - "Missing trusted subject DN (" + subjectDN + ") or issuer DN (" + issuerDN + ") for trusted header authentication."); - } - credential = new DatawaveCredential(subjectDN, issuerDN, proxiedEntities, proxiedIssuers); - } catch (MultipleHeaderException e) { - return notAuthenticated(exchange, securityContext, e.getMessage()); - } - } - - logger.trace("Computed credential = {}", credential); - if (credential != null) { - if (dnsToPrune != null) { - credential.pruneEntities(dnsToPrune); - logger.trace("Computed credential after pruning = {}", credential); - } - String username = credential.getUserName(); - - IdentityManager idm = getIdentityManager(securityContext); - Account account = idm.verify(username, credential); - if (account != null) { - return authenticated(exchange, securityContext, account); - } - } - - return notAttempted(exchange); - } - - private AuthenticationMechanismOutcome notAttempted(HttpServerExchange exchange) { - addTimingRequestHeaders(exchange); - return AuthenticationMechanismOutcome.NOT_ATTEMPTED; - } - - private AuthenticationMechanismOutcome notAuthenticated(HttpServerExchange exchange, SecurityContext securityContext, String reason) { - securityContext.authenticationFailed(reason, name); - addTimingRequestHeaders(exchange); - return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; - } - - private AuthenticationMechanismOutcome authenticated(HttpServerExchange exchange, SecurityContext securityContext, Account account) { - if (exchange.getRequestHeaders().contains(HEADER_PROXIED_ENTITIES)) { - exchange.getResponseHeaders().add(HEADER_PROXIED_ENTITIES_ACCEPTED, "true"); - } - - securityContext.authenticationComplete(account, name, false); - addTimingRequestHeaders(exchange); - return AuthenticationMechanismOutcome.AUTHENTICATED; - } - - private void addTimingRequestHeaders(HttpServerExchange exchange) { - long requestStartTime = exchange.getRequestStartTime(); - long loginTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - requestStartTime); - HeaderMap headers = exchange.getRequestHeaders(); - headers.add(HEADER_START_TIME, requestStartTime); - headers.add(HEADER_LOGIN_TIME, loginTime); - } - - // impl copied from io.undertow.security.impl.ClientCertAuthenticationMechanism - private Certificate[] getPeerCertificates(HttpServerExchange exchange, SSLSessionInfo sslSession, SecurityContext securityContext) - throws SSLPeerUnverifiedException { - try { - return sslSession.getPeerCertificates(); - } catch (RenegotiationRequiredException e) { - // we only renegotiate if authentication is required - if (forceRenegotiation && securityContext.isAuthenticationRequired()) { - try { - sslSession.renegotiate(exchange, SslClientAuthMode.REQUESTED); - return sslSession.getPeerCertificates(); - } catch (IOException | RenegotiationRequiredException e1) { - // ignore - } - } - } - throw new SSLPeerUnverifiedException(""); - } - - @Override - public ChallengeResult sendChallenge(HttpServerExchange httpServerExchange, SecurityContext securityContext) { - // FORBIDDEN (403) was the previous default response code returned when an exception happened - // in the DatawavePrincipalLoginModule and this method returned ChallengeResult(false) - int returnCode = HttpStatus.SC_FORBIDDEN; - org.jboss.security.SecurityContext sc = SecurityContextAssociation.getSecurityContext(); - if (sc != null) { - // A LoginException is thrown from DatawavePrincipalLoginModule and caught - // and saved in the SecurityContext in JBossCachedAuthenticationManager. - Exception e = (Exception) sc.getData().get("org.jboss.security.exception"); - if (e != null) { - if (returnCodeMap.containsKey(e.getClass())) { - returnCode = returnCodeMap.get(e.getClass()); - } - if (logger.isTraceEnabled()) { - logger.trace("exception class: {} returnCode: {}", e.getClass().getCanonicalName(), returnCode); - } - } - } - // The ChallengeResult is evaluated in SecurityContextImpl.transition() - return new ChallengeResult(true, returnCode); - } - - private String getSingleHeader(HeaderMap headers, String headerName) throws MultipleHeaderException { - String value = null; - HeaderValues values = (headers == null) ? null : headers.get(headerName); - if (values != null) { - if (values.size() > 1) - throw new MultipleHeaderException(headerName + " was specified multiple times, which is not allowed!"); - value = values.getFirst(); - } - return value; - } - - @SuppressWarnings("deprecation") - private IdentityManager getIdentityManager(SecurityContext securityContext) { - return identityManager != null ? identityManager : securityContext.getIdentityManager(); - } - - private static final class MultipleHeaderException extends Exception { - private static final long serialVersionUID = 456918082878759453L; - - public MultipleHeaderException(String message) { - super(message); - } - } - - protected static final class Factory implements AuthenticationMechanismFactory { - - private final IdentityManager identityManager; - - public Factory(IdentityManager identityManager) { - this.identityManager = identityManager; - } - - @Override - @Deprecated - public AuthenticationMechanism create(String mechanismName, FormParserFactory formParserFactory, Map properties) { - return create(mechanismName, identityManager, formParserFactory, properties); - } - - @Override - public AuthenticationMechanism create(String mechanismName, IdentityManager identityManager, FormParserFactory formParserFactory, - Map properties) { - String forceRenegotiation = properties.get(ClientCertAuthenticationMechanism.FORCE_RENEGOTIATION); - return new DatawaveAuthenticationMechanism(mechanismName, (forceRenegotiation == null) || "true".equals(forceRenegotiation), identityManager); - } - } -} diff --git a/web-services/security/src/main/java/datawave/security/auth/DatawaveCredential.java b/web-services/security/src/main/java/datawave/security/auth/DatawaveCredential.java deleted file mode 100644 index ed004d64304..00000000000 --- a/web-services/security/src/main/java/datawave/security/auth/DatawaveCredential.java +++ /dev/null @@ -1,117 +0,0 @@ -package datawave.security.auth; - -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.builder.CompareToBuilder; - -import datawave.security.authorization.SubjectIssuerDNPair; -import datawave.security.util.DnUtils; -import io.undertow.security.idm.Credential; - -/** - * A {@link Credential} for use with Datawave authentication. The main reason for using this credential is to ensure that the JAAS cache inside of Wildfly works - * correctly. It checks that an object implements {@link Comparable} and uses that before using an equality test. When we come through with a certificate, the - * cert will change every time by reference, so we'd never recognize the same user. Since we authenticate using the computed username (certificate - * subject/issuer DN along with proxied entities) and trust the source of this information (either a header or X509Certificate), it is sufficient to compare - * these for identity caching purposes. - */ -public class DatawaveCredential implements Credential, Comparable { - private X509Certificate certificate; - private String userName; - private List entities = new ArrayList<>(); - private String jwtToken; - - /** - * Constructs a {@link DatawaveCredential} using only DN information. This means there is no supplied certificate, and this credential will only be trusted - * if the {@link datawave.security.login.DatawavePrincipalLoginModule} is configured with trusted header login (i.e., it is configured to trust the incoming - * DN information without a supplied certificate). - * - * @param subjectDN - * the subject DN of the calling entity's certificate - * @param issuerDN - * the issuer DN of the calling entity's certificate - * @param proxiedSubjects - * any additional subject DNs (in the form <DN><DN>...) indicating a chain of proxied users the calling entity represents - * @param proxiedIssuers - * any additional issuer DNs (in the form <DN><DN>...), one per subject listed in {@code proxiedSubjects} - */ - public DatawaveCredential(String subjectDN, String issuerDN, String proxiedSubjects, String proxiedIssuers) { - extractEntities(subjectDN, issuerDN, proxiedSubjects, proxiedIssuers); - } - - /** - * Constructs a {@link DatawaveCredential} using a certificate. The certificate is fully trusted to identify the calling entity. - * - * @param certificate - * the {@link X509Certificate} that represents the calling entity - * @param proxiedSubjects - * any additional subject DNs (in the form <DN><DN>...) indicating a chain of proxied users the calling entity represents - * @param proxiedIssuers - * any additional issuer DNs (in the form <DN><DN>...), one per subject listed in {@code proxiedSubjects} - */ - public DatawaveCredential(X509Certificate certificate, String proxiedSubjects, String proxiedIssuers) { - this.certificate = certificate; - String subjectDN = certificate.getSubjectDN().getName(); - String issuerDN = certificate.getIssuerDN().getName(); - extractEntities(subjectDN, issuerDN, proxiedSubjects, proxiedIssuers); - } - - public DatawaveCredential(String jwtToken) { - this.userName = jwtToken; - this.jwtToken = jwtToken; - } - - private void extractEntities(String subjectDN, String issuerDN, String proxiedSubjects, String proxiedIssuers) { - if (proxiedSubjects != null) { - String[] subjects = DnUtils.splitProxiedDNs(proxiedSubjects, true); - if (proxiedIssuers == null) - throw new IllegalArgumentException("Proxied issuers must be supplied if proxied subjects are supplied"); - String[] issuers = DnUtils.splitProxiedDNs(proxiedIssuers, true); - if (subjects.length != issuers.length) - throw new IllegalArgumentException("Proxied subjects and issuers don't match up. Subjects=" + proxiedSubjects + ", Issuers=" + proxiedIssuers); - - for (int i = 0; i < subjects.length; ++i) { - entities.add(SubjectIssuerDNPair.of(subjects[i], issuers[i])); - } - } - entities.add(SubjectIssuerDNPair.of(subjectDN, issuerDN)); - userName = DnUtils.buildNormalizedProxyDN(subjectDN, issuerDN, proxiedSubjects, proxiedIssuers); - } - - public void pruneEntities(Set entitiesToPrune) { - Set normalizedEntities = entitiesToPrune.stream().map(e -> e.toLowerCase()).collect(Collectors.toSet()); - entities = entities.stream().filter(e -> !normalizedEntities.contains(e.subjectDN().toLowerCase())).collect(Collectors.toList()); - userName = DnUtils.buildNormalizedProxyDN(entities); - } - - public String getUserName() { - return userName; - } - - public List getEntities() { - return Collections.unmodifiableList(entities); - } - - public X509Certificate getCertificate() { - return certificate; - } - - public String getJwtToken() { - return jwtToken; - } - - @Override - public int compareTo(DatawaveCredential o) { - return new CompareToBuilder().append(userName, o.getUserName()).append(jwtToken, o.getJwtToken()).toComparison(); - } - - @Override - public String toString() { - return "DatawaveCredential[userName=\"" + getUserName() + "\", certificate=\"" + getCertificate() + "\"]"; - } -} diff --git a/web-services/security/src/main/java/datawave/security/auth/DatawaveServletExtension.java b/web-services/security/src/main/java/datawave/security/auth/DatawaveServletExtension.java deleted file mode 100644 index 0c60f2fc6c9..00000000000 --- a/web-services/security/src/main/java/datawave/security/auth/DatawaveServletExtension.java +++ /dev/null @@ -1,19 +0,0 @@ -package datawave.security.auth; - -import javax.servlet.ServletContext; - -import io.undertow.security.api.AuthenticationMechanismFactory; -import io.undertow.servlet.ServletExtension; -import io.undertow.servlet.api.DeploymentInfo; - -/** - * Datawave Servlet Extension that is here simply to register {@link DatawaveAuthenticationMechanism} as an acceptable authentication mechanism for use in - * web.xml files. - */ -public class DatawaveServletExtension implements ServletExtension { - @Override - public void handleDeployment(DeploymentInfo deploymentInfo, ServletContext servletContext) { - AuthenticationMechanismFactory factory = new DatawaveAuthenticationMechanism.Factory(deploymentInfo.getIdentityManager()); - deploymentInfo.addAuthenticationMechanism(DatawaveAuthenticationMechanism.MECHANISM_NAME, factory); - } -} diff --git a/web-services/security/src/main/java/datawave/security/login/ClientCertLoginModule.java b/web-services/security/src/main/java/datawave/security/login/ClientCertLoginModule.java deleted file mode 100644 index 4fb4c77caf4..00000000000 --- a/web-services/security/src/main/java/datawave/security/login/ClientCertLoginModule.java +++ /dev/null @@ -1,106 +0,0 @@ -package datawave.security.login; - -import java.io.IOException; -import java.security.Principal; -import java.security.cert.X509Certificate; -import java.util.Map; - -import javax.security.auth.Subject; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.callback.UnsupportedCallbackException; -import javax.security.auth.login.LoginException; -import javax.security.auth.spi.LoginModule; - -import org.jboss.logging.Logger; -import org.jboss.security.ClientLoginModule; -import org.jboss.security.SimplePrincipal; -import org.jboss.security.auth.callback.ObjectCallback; - -import datawave.security.auth.DatawaveCredential; - -/** - * A {@link LoginModule} intended for use with {@link ClientLoginModule}. Normally, when a client wishes to make a call to a secured EJB, it authenticates to - * the {@link ClientLoginModule}, which saves the supplied username/password and then passes them along when the secured call is attempted and requests at login - * time. However, if the eventual server login module doesn't want a password as its credential (e.g., because it wants an {@link X509Certificate}) then this - * approach doesn't work. This module bridges that gap by requesting the username via a {@link NameCallback} and a credential using an {@link ObjectCallback}. - * These are saved in the shared state. When used with the {@link ClientLoginModule} with password-stacking set to useFirstPass, the credentials supplied to - * this module will be passed along by the client login module. - */ -public class ClientCertLoginModule implements LoginModule { - - private static Logger log = Logger.getLogger(ClientCertLoginModule.class); - private Map sharedState; - private boolean trace; - - private CallbackHandler callbackHandler; - - @SuppressWarnings("unchecked") - @Override - public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { - - trace = log.isTraceEnabled(); - this.sharedState = (Map) sharedState; - this.callbackHandler = callbackHandler; - } - - @Override - public boolean login() throws LoginException { - if (trace) - log.trace("Begin login"); - - if (callbackHandler == null) - throw new LoginException("Error: no CallbackHandler available for collecting authentication information."); - - NameCallback nc = new NameCallback("DN"); - ObjectCallback oc = new ObjectCallback("Certificate"); - Callback[] callbacks = new Callback[] {nc, oc}; - - try { - callbackHandler.handle(callbacks); - - Principal loginPrincipal = new SimplePrincipal(nc.getName()); - Object credential = oc.getCredential(); - X509Certificate loginCredential; - if (credential instanceof X509Certificate) { - loginCredential = (X509Certificate) oc.getCredential(); - } else { - String clazz = (credential == null) ? "null" : credential.getClass().getName(); - throw new LoginException("Supplied credential is a " + clazz + " but needs to be an " + X509Certificate.class.getName()); - } - - sharedState.put("javax.security.auth.login.name", loginPrincipal); - sharedState.put("javax.security.auth.login.password", new DatawaveCredential(loginCredential, null, null)); - - } catch (IOException e) { - LoginException le = new LoginException(e.toString()); - le.initCause(e); - throw le; - } catch (UnsupportedCallbackException e) { - LoginException le = new LoginException("Error: " + e.getCallback() + ", not available to use this callback."); - le.initCause(e); - throw le; - } - - if (trace) - log.trace("End login"); - return true; - } - - @Override - public boolean commit() throws LoginException { - return true; - } - - @Override - public boolean abort() throws LoginException { - return true; - } - - @Override - public boolean logout() throws LoginException { - return true; - } - -} diff --git a/web-services/security/src/main/java/datawave/security/login/DatawaveCertRolesLoginModule.java b/web-services/security/src/main/java/datawave/security/login/DatawaveCertRolesLoginModule.java deleted file mode 100644 index 00620dd1881..00000000000 --- a/web-services/security/src/main/java/datawave/security/login/DatawaveCertRolesLoginModule.java +++ /dev/null @@ -1,165 +0,0 @@ -package datawave.security.login; - -import java.io.IOException; -import java.security.Principal; -import java.security.acl.Group; -import java.security.cert.X509Certificate; -import java.util.Enumeration; -import java.util.Map; - -import javax.security.auth.Subject; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.callback.UnsupportedCallbackException; -import javax.security.auth.login.LoginException; - -import org.jboss.logging.Logger; -import org.jboss.security.PicketBoxLogger; -import org.jboss.security.PicketBoxMessages; -import org.jboss.security.SimplePrincipal; -import org.jboss.security.auth.callback.ObjectCallback; -import org.jboss.security.auth.spi.CertRolesLoginModule; - -import datawave.security.auth.DatawaveCredential; - -/** - * A specialized version of {@link CertRolesLoginModule} that fails the login if there are no roles for a given user. Even if the user has a valid certificate, - * if they don't appear in the roles file, we'll fail the login. - */ -public class DatawaveCertRolesLoginModule extends CertRolesLoginModule { - - private static final String TRUSTED_HEADER_OPT = "trustedHeaderLogin"; - - private ThreadLocal createSimplePrincipal = new ThreadLocal<>(); - private boolean trustedHeaderLogin; - - public DatawaveCertRolesLoginModule() { - log = Logger.getLogger(getClass()); - } - - @Override - public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { - addValidOptions(new String[] {TRUSTED_HEADER_OPT}); - super.initialize(subject, callbackHandler, sharedState, options); - - String option = (String) options.get(TRUSTED_HEADER_OPT); - if (option != null) - trustedHeaderLogin = Boolean.valueOf(option); - } - - @Override - public boolean login() throws LoginException { - // This login module should do nothing if we're using the trusted header login (since a cert won't be supplied) - // The reason for having this option is so that the module can be on the stack and support either configuration -- - // normal SSL certificate or SSL-terminated trusted header - if (trustedHeaderLogin) { - log.trace("trustedHeaderLogin is true - returning false for login success"); - return false; - } - - boolean success = super.login(); - - int roleCount = 0; - Group[] roleSets = getRoleSets(); - if (roleSets != null) { - for (Group roleSet : roleSets) { - for (Enumeration e = roleSet.members(); e.hasMoreElements(); e.nextElement()) { - ++roleCount; - } - } - } - - // Fail the login if there are no roles. This way we can try - // another module potentially. - if (roleCount == 0) { - loginOk = false; - success = false; - } - - return success; - } - - @Override - protected Group[] getRoleSets() throws LoginException { - // Set a thread local to indicate that we should create a SimplePrincipal when asked to create an identity. This is needed - // because the parent class uses a utility to create the groups and that utility delegates back to our own createIdentity - // method for adding each member to the group. Since our group members won't be valid names for use with DatawavePrincipal - // we need to ensure that we use a SimplePrincipal to represent the group members instead. - createSimplePrincipal.set(Boolean.TRUE); - try { - return super.getRoleSets(); - } finally { - createSimplePrincipal.remove(); - } - } - - @Override - protected Principal createIdentity(String username) throws Exception { - // Create a simple principal if our thread-local indicates we are supposed to, - // which only happens during the getRolesSets method call. - if (Boolean.TRUE.equals(createSimplePrincipal.get())) { - return new SimplePrincipal(username); - } else { - return super.createIdentity(DatawaveUsersRolesLoginModule.normalizeUsername(username)); - } - } - - // Copied from org.jboss.security.auth.spi.BaseCertLoginModule to handle the addition of DatawaveCredential - @Override - protected Object[] getAliasAndCert() throws LoginException { - PicketBoxLogger.LOGGER.traceBeginGetAliasAndCert(); - Object[] info = {null, null}; - // prompt for a username and password - if (callbackHandler == null) { - throw PicketBoxMessages.MESSAGES.noCallbackHandlerAvailable(); - } - NameCallback nc = new NameCallback("Alias: "); - ObjectCallback oc = new ObjectCallback("Certificate: "); - Callback[] callbacks = {nc, oc}; - String alias; - X509Certificate cert = null; - X509Certificate[] certChain; - try { - callbackHandler.handle(callbacks); - alias = nc.getName(); - Object tmpCert = oc.getCredential(); - if (tmpCert != null) { - if (tmpCert instanceof X509Certificate) { - cert = (X509Certificate) tmpCert; - PicketBoxLogger.LOGGER.traceCertificateFound(cert.getSerialNumber().toString(16), cert.getSubjectDN().getName()); - } else if (tmpCert instanceof X509Certificate[]) { - certChain = (X509Certificate[]) tmpCert; - if (certChain.length > 0) - cert = certChain[0]; - } else if (tmpCert instanceof DatawaveCredential) { - DatawaveCredential dwCredential = (DatawaveCredential) tmpCert; - cert = dwCredential.getCertificate(); - if (cert == null) { - String msg = "No certificate supplied with login credential for " + dwCredential.getUserName(); - log.warn(msg); - throw new LoginException(msg); - } - PicketBoxLogger.LOGGER.traceCertificateFound(cert.getSerialNumber().toString(16), cert.getSubjectDN().getName()); - } else { - throw PicketBoxMessages.MESSAGES.unableToGetCertificateFromClass(tmpCert.getClass()); - } - } else { - PicketBoxLogger.LOGGER.warnNullCredentialFromCallbackHandler(); - } - } catch (IOException e) { - LoginException le = PicketBoxMessages.MESSAGES.failedToInvokeCallbackHandler(); - le.initCause(e); - throw le; - } catch (UnsupportedCallbackException uce) { - LoginException le = new LoginException(); - le.initCause(uce); - throw le; - } - - info[0] = alias; - info[1] = cert; - PicketBoxLogger.LOGGER.traceEndGetAliasAndCert(); - return info; - } -} diff --git a/web-services/security/src/main/java/datawave/security/login/DatawaveCertVerifier.java b/web-services/security/src/main/java/datawave/security/login/DatawaveCertVerifier.java deleted file mode 100644 index 25712e187d0..00000000000 --- a/web-services/security/src/main/java/datawave/security/login/DatawaveCertVerifier.java +++ /dev/null @@ -1,69 +0,0 @@ -package datawave.security.login; - -import java.security.KeyStore; -import java.security.cert.X509Certificate; - -import org.jboss.logging.Logger; -import org.jboss.security.auth.certs.X509CertificateVerifier; - -public class DatawaveCertVerifier implements X509CertificateVerifier { - - public enum OcspLevel { - OFF, OPTIONAL, REQUIRED - } - - protected Logger log; - protected boolean trace; - protected OcspLevel ocspLevel = OcspLevel.OFF; - - @Override - public boolean verify(X509Certificate cert, String alias, KeyStore keystore, KeyStore truststore) { - boolean validity = false; - try { - cert.checkValidity(); - validity = checkOCSP(cert, alias, truststore); - } catch (Exception e) { - if (trace) - log.trace("Validity exception", e); - } - return validity; - } - - protected void initOcsp() {} - - protected boolean checkOCSP(X509Certificate cert, String alias, KeyStore truststore) { - switch (ocspLevel) { - case OFF: - break; - default: - log.error("OCSP level " + ocspLevel + " is not supported!"); - throw new IllegalArgumentException("OCSP level " + ocspLevel + " is not supported!"); - } - return true; - } - - public boolean isIssuerSupported(String issuerSubjectDn, KeyStore trustStore) { - return true; - } - - public void setLogger(Logger log) { - this.log = log; - if (log.isTraceEnabled()) - trace = true; - } - - public OcspLevel getOcspLevel() { - return ocspLevel; - } - - public void setOcspLevel(String level) { - ocspLevel = OcspLevel.valueOf(level.toUpperCase()); - switch (ocspLevel) { - case REQUIRED: - case OPTIONAL: - initOcsp(); - break; - } - } - -} diff --git a/web-services/security/src/main/java/datawave/security/login/DatawavePrincipalLoginModule.java b/web-services/security/src/main/java/datawave/security/login/DatawavePrincipalLoginModule.java deleted file mode 100644 index d3d45ff6cf1..00000000000 --- a/web-services/security/src/main/java/datawave/security/login/DatawavePrincipalLoginModule.java +++ /dev/null @@ -1,536 +0,0 @@ -package datawave.security.login; - -import java.io.IOException; -import java.security.Key; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.Principal; -import java.security.acl.Group; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; -import javax.net.ssl.X509KeyManager; -import javax.security.auth.Subject; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.callback.UnsupportedCallbackException; -import javax.security.auth.login.AccountLockedException; -import javax.security.auth.login.CredentialException; -import javax.security.auth.login.FailedLoginException; -import javax.security.auth.login.LoginException; - -import org.jboss.logging.Logger; -import org.jboss.security.JSSESecurityDomain; -import org.jboss.security.SimpleGroup; -import org.jboss.security.SimplePrincipal; -import org.jboss.security.auth.callback.ObjectCallback; -import org.jboss.security.auth.certs.X509CertificateVerifier; -import org.jboss.security.auth.spi.AbstractServerLoginModule; -import org.picketbox.util.StringUtil; - -import com.fasterxml.jackson.databind.MapperFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.guava.GuavaModule; -import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule; - -import datawave.configuration.spring.BeanProvider; -import datawave.security.auth.DatawaveCredential; -import datawave.security.authorization.AuthorizationException; -import datawave.security.authorization.DatawavePrincipal; -import datawave.security.authorization.DatawaveUser; -import datawave.security.authorization.DatawaveUserService; -import datawave.security.authorization.JWTTokenHandler; -import datawave.util.StringUtils; - -@SuppressWarnings("SpringAutowiredFieldsWarningInspection") -public class DatawavePrincipalLoginModule extends AbstractServerLoginModule { - - private Principal identity; - private X509Certificate certificateCredential; - private DatawaveCredential datawaveCredential; - private X509CertificateVerifier verifier; - private boolean datawaveVerifier; - - private boolean trustedHeaderLogin; - private boolean jwtHeaderLogin; - - private String disallowlistUserRole = null; - - /** - * Required roles are a set of roles such that each entity in a proxy chain must have at least one of the required roles. If that is not the case, then the - * login module will ensure that none of the required roles are included in the response to getRoleSets. - */ - private Set requiredRoles = new HashSet<>(); - - private Set directRoles = new HashSet<>(); - - @Inject - private DatawaveUserService datawaveUserService; - @Inject - private JSSESecurityDomain domain; - - private JWTTokenHandler jwtTokenHandler; - - private boolean trace; - - public DatawavePrincipalLoginModule() { - log = Logger.getLogger(getClass()); - } - - @Override - public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { - - trace = log.isTraceEnabled(); - - super.initialize(subject, callbackHandler, sharedState, options); - - // Have the bean container do injection for us so we don't have to do JNDI lookup. - performFieldInjection(); - - String option = (String) options.get("verifier"); - if (option != null) { - try { - ClassLoader loader = Thread.currentThread().getContextClassLoader(); - Class verifierClass = loader.loadClass(option); - verifier = (X509CertificateVerifier) verifierClass.getDeclaredConstructor().newInstance(); - if (verifier instanceof DatawaveCertVerifier) { - ((DatawaveCertVerifier) verifier).setLogger(log); - ((DatawaveCertVerifier) verifier).setOcspLevel((String) options.get("ocspLevel")); - datawaveVerifier = true; - } - } catch (Throwable e) { - if (trace) { - log.trace("Failed to create X509CertificateVerifier", e); - } - throw new IllegalArgumentException("Invalid verifier: " + option, e); - } - } - - option = (String) options.get("trustedHeaderLogin"); - if (option != null) - trustedHeaderLogin = Boolean.valueOf(option); - - option = (String) options.get("jwtHeaderLogin"); - if (option != null) - jwtHeaderLogin = Boolean.valueOf(option); - - disallowlistUserRole = (String) options.get("disallowlistUserRole"); - if (disallowlistUserRole != null && "".equals(disallowlistUserRole.trim())) - disallowlistUserRole = null; - - option = (String) options.get("requiredRoles"); - if (option != null) { - requiredRoles.clear(); - requiredRoles.addAll(Arrays.asList(StringUtils.split(option, ':', false))); - } else { - requiredRoles.add("AuthorizedUser"); - requiredRoles.add("AuthorizedServer"); - requiredRoles.add("AuthorizedQueryServer"); - requiredRoles.add("AuthorizedProxiedServer"); - } - - /** - * the directRoles check is restricted to UserType.SERVER so the AuthorizedUser is not required in this set. There is no explicit check to verify that - * there is overlap between requiredRoles and directRoles. If that check is wanted it could be added in the #getRoleSets() - */ - - option = (String) options.get("directRoles"); - if (option != null) { - directRoles.clear(); - directRoles.addAll(Arrays.asList(StringUtils.split(option, ':', false))); - } else { - directRoles.add("AuthorizedServer"); - directRoles.add("AuthorizedQueryServer"); - } - - try { - ObjectMapper mapper = new ObjectMapper(); - mapper.enable(MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME); - mapper.registerModule(new GuavaModule()); - mapper.registerModule(new JaxbAnnotationModule()); - - String alias = domain.getKeyStore().aliases().nextElement(); - X509KeyManager keyManager = (X509KeyManager) domain.getKeyManagers()[0]; - X509Certificate[] certs = keyManager.getCertificateChain(alias); - Key signingKey = keyManager.getPrivateKey(alias); - - jwtTokenHandler = new JWTTokenHandler(certs[0], signingKey, 24, TimeUnit.HOURS, JWTTokenHandler.TtlMode.RELATIVE_TO_CURRENT_TIME, mapper); - } catch (KeyStoreException e) { - throw new RuntimeException(e.getMessage(), e); - } - - if (trace) { - log.trace("exit: initialize(Subject, CallbackHandler, Map, Map)"); - } - } - - protected void performFieldInjection() { - if (datawaveUserService == null) { - BeanProvider.injectFields(this); - } - } - - @Override - protected Principal getIdentity() { - return identity; - } - - protected String getUsername() { - String username = null; - if (getIdentity() != null) - username = getIdentity().getName(); - return username; - } - - @Override - protected Group[] getRoleSets() throws LoginException { - Group groups[]; - try { - Set roles = new TreeSet<>(); - String targetUser = getUsername(); - DatawavePrincipal principal = (DatawavePrincipal) getIdentity(); - - Collection cpRoleSets = principal.getPrimaryUser().getRoles(); - if (cpRoleSets != null) { - roles.addAll(cpRoleSets); - // We are requiring that at every entity in the call chain has at least one of the required roles. - // If any entity has none of them, then exclude all of the required roles from the computed final set. - if (principal.getProxiedUsers().stream().anyMatch(u -> Collections.disjoint(u.getRoles(), requiredRoles))) { - roles.removeAll(requiredRoles); - } - - } - StringBuilder buf = new StringBuilder("[" + roles.size() + "] Groups for " + targetUser + " {"); - if (!roles.isEmpty()) { - Group group = new SimpleGroup("Roles"); - boolean first = true; - for (String r : roles) - try { - if (!first) { - buf.append(":"); - } - first = false; - group.addMember(new SimplePrincipal(r)); - buf.append(" ").append(r).append(" "); - } catch (Exception e) { - log.debug("Failed to create principal for: " + r, e); - } - - groups = new Group[2]; - groups[0] = group; - groups[1] = new SimpleGroup("CallerPrincipal"); - groups[1].addMember(getIdentity()); - } else { - groups = new Group[0]; - } - buf.append("}"); - log.debug(buf.toString()); - } catch (RuntimeException e) { - groups = new Group[0]; - log.warn("Exception in getRoleSets: " + e.getMessage(), e); - abort(); - } - return groups; - } - - @Override - public boolean commit() throws LoginException { - // If our login is ok, then remove any principals from the subject principals list that match our type. - // If another login module produces a DatawavePrincipal before us, it will be associated with the subject - // and later retrieved instead of the one we produce here. Therefore we remove any DatawavePrincipals - // associated with the subject so that doesn't happen. - log.trace("Committing login for " + getIdentity() + "@" + System.identityHashCode(getIdentity()) + ". loginOk=" + loginOk); - if (loginOk) { - DatawavePrincipal dp = (DatawavePrincipal) getIdentity(); - for (DatawavePrincipal p : subject.getPrincipals(DatawavePrincipal.class)) { - if (dp.getName().equals(p.getName())) { - log.trace("Removing duplicate principal " + p + "@" + System.identityHashCode(p)); - subject.getPrincipals().remove(p); - } else { - log.trace("Skipping " + p + "@" + System.identityHashCode(p) + " since [" + p.getName() + "] != [" + p.getName() + "]"); - } - } - - // There is also a CallerPrincipal group that login modules create and add the identity to. Other login modules will - // then maybe add a DatawavePrincipal to this group. The identity manager uses the CallerPrincipal group and the - // principals on the subject to determine the true caller principal, so we need to be sure to remove the previously - // created DatawavePrincipal from the CallerPrincipal group as well. - Group callerGroup = getCallerPrincipalGroup(subject.getPrincipals()); - if (callerGroup != null) { - Set principalsToRemove = new HashSet<>(); - for (Enumeration e = callerGroup.members(); e.hasMoreElements();) { - Principal p = e.nextElement(); - if (p instanceof DatawavePrincipal) { - if (dp.getName().equals(p.getName())) { - principalsToRemove.add(p); - } else { - log.trace("Skipping from CallerPrincipal group " + p + "@" + System.identityHashCode(p) + " since [" + p.getName() + "] != [" - + p.getName() + "]"); - } - } - } - for (Principal p : principalsToRemove) { - log.trace("Removing from CallerPrincipal group duplicate principal " + p + "@" + System.identityHashCode(p)); - callerGroup.removeMember(p); - } - } - } - boolean ok = super.commit(); - if (ok && certificateCredential != null) - subject.getPublicCredentials().add(certificateCredential); - return ok; - } - - @Override - public boolean login() throws LoginException { - try { - // We don't really place nice with other login modules. If the other module sticks a cert - // in the shared state for login, then we're ok. Otherwise, we are going to reject the login. - if (super.login()) { - Object username = sharedState.get("javax.security.auth.login.name"); - if (username instanceof Principal) { - identity = (Principal) username; - if (trace) { - log.trace("**** Username is a principal"); - } - } else { - if (trace) { - log.trace("**** Username is not a principal"); - } - String name = username.toString(); - try { - identity = createIdentity(name); - } catch (Exception e) { - loginOk = false; - log.debug("Failed to create principal", e); - // should result in a FORBIDDEN (403) response code in DatawaveAuthenticationMechanism.sendChallenge - throw new FailedLoginException("Failed to create principal: " + e.getMessage()); - } - } - Object password = sharedState.get("javax.security.auth.login.password"); - if (password instanceof X509Certificate) { - if (trace) { - log.trace("**** Credential is a X509Certificate"); - } - certificateCredential = (X509Certificate) password; - } else if (password instanceof X509Certificate[]) { - if (trace) { - log.trace("**** Credential is an X509Certificate array"); - } - certificateCredential = ((X509Certificate[]) password)[0]; - } else if (password instanceof DatawaveCredential) { - if (trace) { - log.trace("**** Credential is a DatawaveCredential"); - } - datawaveCredential = (DatawaveCredential) password; - certificateCredential = datawaveCredential.getCertificate(); - } else { - log.warn("Login failed due to unknown password."); - return false; - } - } else { - DatawaveCredential credential = getDatawaveCredential(); - loginOk = validateCredential(credential); - if (trace) { - log.trace("User '" + identity + "' authenticated, loginOk=" + loginOk); - log.debug("exit: login()"); - } - } - - if (disallowlistUserRole != null && loginOk && identity != null) { - DatawavePrincipal principal = (DatawavePrincipal) getIdentity(); - - if (principal.getProxiedUsers().stream().anyMatch(u -> u.getRoles().contains(disallowlistUserRole))) { - loginOk = false; // this is critical as it is what the parent class uses to actually deny login - String message = "Login denied for " + principal.getUserDN() + " due to membership in the deny-access group " + disallowlistUserRole; - log.debug(message); - // should result in a FORBIDDEN (403) response code in DatawaveAuthenticationMechanism.sendChallenge - throw new AccountLockedException(message); - } - } - - /** - * Check terminal server to verify that it can directly connect. This requires the positive check that the terminal server has an approved role - * AuthorizedServer or AuthorizedQueryServer. If TerminalServer does not have the correct role we will fail the login. Currently this only checks - * for UserType.SERVER. However the predicate could be modified to include a check for UserType.USER. Logic just streams through the list of - * ProxiedUsers to get the last element (terminal server). Make sure it's not null and if it is a server then we want it to have a direct role. - */ - - DatawavePrincipal principal = (DatawavePrincipal) getIdentity(); - DatawaveUser terminalServer = principal.getProxiedUsers().stream().reduce((prev, next) -> next).orElse(null); - // terminalUser should never be null, at a minimum the PrincipalUser should be in the chain - if (terminalServer == null || (terminalServer.getUserType() == DatawaveUser.UserType.SERVER - && !(terminalServer.getRoles().stream().anyMatch(directRoles::contains)))) { - loginOk = false; // this is critical as it is what the parent class uses to actually deny login - String message = "Login denied for terminal server " + terminalServer.getDn() + " due to missing role. Needs one of: " + directRoles - + " but has roles: " + terminalServer.getRoles(); - log.debug(message); - // should result in a FORBIDDEN (403) response code in DatawaveAuthenticationMechanism.sendChallenge - throw new FailedLoginException(message); - } - } catch (LoginException e) { - log.warn("Login failed due to LoginException: " + e.getMessage(), e); - throw e; - } catch (RuntimeException e) { - log.warn("Login failed due to RuntimeException: " + e.getMessage(), e); - // should result in a SERVICE_UNAVAILABLE (503) response code in DatawaveAuthenticationMechanism.sendChallenge - throw new LoginException(e.getMessage()); - } - return true; - } - - protected DatawaveCredential getDatawaveCredential() throws LoginException { - if (trace) { - log.trace("enter: getDatawaveCredential()"); - } - if (callbackHandler == null) { - log.error("Error: no CallbackHandler available to collect authentication information"); - throw new LoginException("Error: no CallbackHandler available to collect authentication information"); - } - NameCallback nc = new NameCallback("Username: "); - ObjectCallback oc = new ObjectCallback("Credentials: "); - Callback callbacks[] = {nc, oc}; - try { - callbackHandler.handle(callbacks); - - // We use a custom authentication mechanism to convert the certificate into a DatawaveCredential. - // The custom authentication mechanism checks the request for the X-ProxiedEntitiesChain/X-ProxiedIssuersChain - // headers and uses them along with either the certificate subject and issuer DNs or trusted headers - // (supplied by the load balancer) containing the subject and issuer DNs to construct a list of entities. - Object tmpCreds = oc.getCredential(); - if (tmpCreds instanceof DatawaveCredential) { - return (DatawaveCredential) tmpCreds; - } else { - String credentialClass = tmpCreds == null ? "null" : tmpCreds.getClass().getName(); - String msg = "Unknown credential class " + credentialClass + " is not a " + DatawaveCredential.class.getName(); - log.warn(msg); - throw new LoginException(msg); - } - } catch (IOException e) { - log.debug("Failed to invoke callback", e); - throw new LoginException("Failed to invoke callback: " + e); - } catch (UnsupportedCallbackException uce) { - log.debug("CallbackHandler does not support: " + uce.getCallback()); - throw new LoginException("CallbackHandler does not support: " + uce.getCallback()); - } finally { - if (trace) { - log.trace("exit: getDatawaveCredential()"); - } - } - } - - @SuppressWarnings("unchecked") - protected boolean validateCredential(DatawaveCredential credential) throws LoginException { - if (trace) { - log.trace("enter: validateCredential"); - } - - datawaveCredential = credential; - - String alias = credential.getUserName(); - if (trace) { - log.trace("alias = " + alias); - } - if (StringUtil.isNullOrEmpty(alias)) { - identity = unauthenticatedIdentity; - log.trace("Authenticating as unauthenticatedIdentity=" + identity); - } - if (trace) { - log.trace("identity = " + identity); - } - if (identity == null) { - if (credential.getCertificate() != null || (!trustedHeaderLogin && !jwtHeaderLogin)) { - if (!validateCertificateCredential(credential)) { - log.debug("Bad credential for alias=" + credential.getUserName()); - throw new CredentialException("Validation of credential failed"); - } - } - - if (!jwtHeaderLogin || credential.getJwtToken() == null) { - try { - identity = new DatawavePrincipal(datawaveUserService.lookup(credential.getEntities())); - } catch (AuthorizationException e) { - Throwable cause = e.getCause(); - String message = cause != null ? cause.getMessage() : e.getMessage(); - log.debug("Failing login due to datawave user service exception " + e.getMessage(), e); - // should result in a SERVICE_UNAVAILABLE (503) response code in DatawaveAuthenticationMechanism.sendChallenge - throw new LoginException("Unable to authenticate: " + message); - } catch (Exception e) { - log.debug("Failing login due to datawave user service exception " + e.getMessage(), e); - // should result in a SERVICE_UNAVAILABLE (503) response code in DatawaveAuthenticationMechanism.sendChallenge - throw new LoginException("Unable to authenticate: " + e.getMessage()); - } - } else { - try { - identity = new DatawavePrincipal(jwtTokenHandler.createUsersFromToken(credential.getJwtToken())); - } catch (Exception e) { - log.debug("Failing login due to JWT token exception " + e.getMessage(), e); - // should result in an UNAUTHORIZED (401) response code in DatawaveAuthenticationMechanism.sendChallenge - throw new CredentialException("Unable to authenticate: " + e.getMessage()); - } - } - } - if (getUseFirstPass()) { - sharedState.put("javax.security.auth.login.name", alias); - sharedState.put("javax.security.auth.login.password", credential.getCertificate()); - } - if (trace) { - log.trace("exit: validateCredential"); - } - return true; - } - - protected boolean validateCertificateCredential(DatawaveCredential credential) { - if (trace) { - log.trace("enter: validateCertificateCredential(DatawaveCredential)[" + verifier + "]"); - } - boolean isValid = false; - KeyStore keyStore = null; - KeyStore trustStore = null; - if (domain != null) { - keyStore = domain.getKeyStore(); - trustStore = domain.getTrustStore(); - } - if (trustStore == null) { - trustStore = keyStore; - } - if (verifier != null) { - String issuerSubjectDn = credential.getCertificate().getIssuerX500Principal().getName(); - if (datawaveVerifier) { - if (((DatawaveCertVerifier) verifier).isIssuerSupported(issuerSubjectDn, trustStore)) { - isValid = verifier.verify(credential.getCertificate(), issuerSubjectDn, keyStore, trustStore); - } else { - if (trace) { - log.trace("Unsupported issuer: " + issuerSubjectDn); - } - } - } else { - if (trace) { - log.trace("Validating using non datawave cert verifier."); - } - isValid = verifier.verify(credential.getCertificate(), issuerSubjectDn, keyStore, trustStore); - } - if (trace) { - log.trace("Cert Validation result : " + isValid); - } - } else if (credential.getCertificate() != null) { - isValid = true; - } else { - log.warn("Certificate is null - unable to validate"); - } - if (trace) { - log.trace("exit: validateCertificateCredential(DatawaveCredential)"); - } - return isValid; - } -} diff --git a/web-services/security/src/main/java/datawave/security/login/DatawaveUsersRolesLoginModule.java b/web-services/security/src/main/java/datawave/security/login/DatawaveUsersRolesLoginModule.java deleted file mode 100644 index c1ce51d563d..00000000000 --- a/web-services/security/src/main/java/datawave/security/login/DatawaveUsersRolesLoginModule.java +++ /dev/null @@ -1,98 +0,0 @@ -package datawave.security.login; - -import java.security.Principal; -import java.security.acl.Group; -import java.util.Enumeration; - -import javax.security.auth.login.LoginException; - -import org.jboss.logging.Logger; -import org.jboss.security.SimplePrincipal; -import org.jboss.security.auth.spi.UsersRolesLoginModule; - -import datawave.security.util.DnUtils; - -/** - * A specialized version of {@link UsersRolesLoginModule} that fails the login if there are no roles for a given user. The parent module will take the supplied - * credential, which in our case will be a {@link datawave.security.auth.DatawaveCredential}, and turn it to a string, so the password used in the associated - * properties file must match the {@code toString()} version of the credential. - */ -public class DatawaveUsersRolesLoginModule extends UsersRolesLoginModule { - private ThreadLocal createSimplePrincipal = new ThreadLocal<>(); - - public DatawaveUsersRolesLoginModule() { - log = Logger.getLogger(getClass()); - } - - @Override - public boolean login() throws LoginException { - boolean success = super.login(); - - int roleCount = 0; - Group[] roleSets = getRoleSets(); - if (roleSets != null) { - for (Group roleSet : roleSets) { - for (Enumeration e = roleSet.members(); e.hasMoreElements(); e.nextElement()) { - ++roleCount; - } - } - } - - // Fail the login if there are no roles. This way we can try - // another module potentially. - if (roleCount == 0) { - loginOk = false; - success = false; - } - - return success; - } - - @Override - protected Group[] getRoleSets() throws LoginException { - // Set a thread local to indicate that we should create a SimplePrincipal when asked to create an identity. This is needed - // because the parent class uses a utility to create the groups and that utility delegates back to our own createIdentity - // method for adding each member to the group. Since our group members won't be valid names for use with DatawavePrincipal - // we need to ensure that we use a SimplePrincipal to represent the group members instead. - createSimplePrincipal.set(Boolean.TRUE); - try { - return super.getRoleSets(); - } finally { - createSimplePrincipal.remove(); - } - } - - @Override - protected Principal createIdentity(String username) throws Exception { - // Create a simple principal if our thread-local indicates we are supposed to, - // which only happens during the getRolesSets method call. - if (Boolean.TRUE.equals(createSimplePrincipal.get())) { - if (log.isTraceEnabled()) { - log.trace("Creating simple principal, passing username: " + username); - } - return new SimplePrincipal(username); - } else { - String normalizedUsername = normalizeUsername(username); - if (log.isTraceEnabled()) { - log.trace("original username: " + username + " normalizedUsername: " + normalizedUsername); - } - return super.createIdentity(normalizedUsername); - } - } - - protected static String normalizeUsername(String username) { - StringBuilder result = new StringBuilder(); - - String[] splitDns = DnUtils.splitProxiedSubjectIssuerDNs(username); - for (int i = 0; i < splitDns.length; i++) { - if (i > 0) { - result.append("<"); - result.append(DnUtils.normalizeDN(splitDns[i])); - result.append(">"); - } else { - result.append(DnUtils.normalizeDN(splitDns[i])); - } - } - return result.toString(); - } -} diff --git a/web-services/security/src/main/java/datawave/security/system/SecurityDomainProducer.java b/web-services/security/src/main/java/datawave/security/system/SecurityDomainProducer.java deleted file mode 100644 index b1216d96690..00000000000 --- a/web-services/security/src/main/java/datawave/security/system/SecurityDomainProducer.java +++ /dev/null @@ -1,39 +0,0 @@ -package datawave.security.system; - -import java.security.Principal; - -import javax.annotation.Resource; -import javax.enterprise.context.ApplicationScoped; -import javax.enterprise.inject.Produces; -import javax.inject.Inject; - -import org.jboss.security.AuthenticationManager; -import org.jboss.security.CacheableManager; -import org.jboss.security.JSSESecurityDomain; - -/** - * A producer class for generating server-security related artifacts. For one, we produce the server DN of the server that we are running inside of. We allso - * produce the {@link JSSESecurityDomain} for our application. We use this rather than directly injecting at each site using {@link Resource} since the producer - * allows us to use a plain {@link Inject} annotation versus having to specify the resource name each time we inject with {@link Resource}. This way, we only - * name the resource once. - */ -@ApplicationScoped -public class SecurityDomainProducer { - // Allow injection of JSSESecurityDomain without having to specify the JNDI name at each injection point. - // Instead, users can simply do: - // @Inject private JSSESecurityDomain jsseSecurityDomain - // and the specification of the resource location is limited to this class. - @Produces - @Resource(name = "java:jboss/jaas/datawave/jsse") - private JSSESecurityDomain domain; - - @Resource(name = "java:jboss/jaas/datawave") - private AuthenticationManager authenticationManager; - - @Produces - @AuthorizationCache - @SuppressWarnings("unchecked") - public CacheableManager produceAuthManager() { - return (authenticationManager instanceof CacheableManager) ? (CacheableManager) authenticationManager : null; - } -} diff --git a/web-services/security/src/main/java/datawave/security/websocket/WebsocketSecurityConfigurator.java b/web-services/security/src/main/java/datawave/security/websocket/WebsocketSecurityConfigurator.java deleted file mode 100644 index 27536197c13..00000000000 --- a/web-services/security/src/main/java/datawave/security/websocket/WebsocketSecurityConfigurator.java +++ /dev/null @@ -1,36 +0,0 @@ -package datawave.security.websocket; - -import static datawave.webservice.metrics.Constants.REQUEST_LOGIN_TIME_HEADER; - -import java.util.List; -import java.util.Map; - -import javax.websocket.HandshakeResponse; -import javax.websocket.server.HandshakeRequest; -import javax.websocket.server.ServerEndpointConfig; -import javax.websocket.server.ServerEndpointConfig.Configurator; - -import org.jboss.security.SecurityContextAssociation; - -/** - * A JBoss AS/Wildfly-specific {@link Configurator} that saves the incoming JAAS credentials into the user session so that WebSocket handler methods can be - * invoked using this security context. This covers a hole in the specification that doesn't allow for the propagation of security credentials to the WebSocket - * handlers. See WEBSOCKET_SPEC-238 for more details. - */ -public class WebsocketSecurityConfigurator extends Configurator { - @Override - public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { - super.modifyHandshake(sec, request, response); - - sec.getUserProperties().put(WebsocketSecurityInterceptor.SESSION_PRINCIPAL, request.getUserPrincipal()); - sec.getUserProperties().put(WebsocketSecurityInterceptor.SESSION_SUBJECT, SecurityContextAssociation.getSubject()); - sec.getUserProperties().put(WebsocketSecurityInterceptor.SESSION_CREDENTIAL, SecurityContextAssociation.getPrincipal()); - Map> headers = request.getHeaders(); - if (headers != null) { - List loginHeader = headers.get(REQUEST_LOGIN_TIME_HEADER); - if (loginHeader != null && !loginHeader.isEmpty()) { - sec.getUserProperties().put(REQUEST_LOGIN_TIME_HEADER, loginHeader.get(0)); - } - } - } -} diff --git a/web-services/security/src/main/java/datawave/security/websocket/WebsocketSecurityInterceptor.java b/web-services/security/src/main/java/datawave/security/websocket/WebsocketSecurityInterceptor.java deleted file mode 100644 index e55cc0c1e07..00000000000 --- a/web-services/security/src/main/java/datawave/security/websocket/WebsocketSecurityInterceptor.java +++ /dev/null @@ -1,83 +0,0 @@ -package datawave.security.websocket; - -import java.security.Principal; -import java.security.acl.Group; - -import javax.interceptor.AroundInvoke; -import javax.interceptor.InvocationContext; -import javax.security.auth.Subject; -import javax.websocket.Session; -import javax.websocket.server.ServerEndpoint; - -import org.jboss.security.SecurityContext; -import org.jboss.security.SecurityContextAssociation; -import org.jboss.security.identity.Identity; -import org.jboss.security.identity.Role; -import org.jboss.security.identity.extensions.CredentialIdentityFactory; -import org.jboss.security.identity.plugins.SimpleRoleGroup; - -/** - * A JBoss AS/Wildfly-specific interceptor that looks for saved security context information in the WebSocket user {@link Session} and uses that information to - * execute a WebSocket handler method in the saved security context. This workaround is necessary to cover a shortcoming in the WebSocket specification. See - * WEBSOCKET_SPEC-238 for more details. - *

- * To use this interceptor, annotate your {@link ServerEndpoint}-annotated class with - * - *

- * 
- *     {@literal @}Interceptors({ WebsocketSecurityInterceptor.class })
- * 
- * 
- * - * and also ensure that the server enpoint annotation sets {@link WebsocketSecurityConfigurator} as the {@link ServerEndpoint#configurator()} class. - */ -public class WebsocketSecurityInterceptor { - public static final String SESSION_PRINCIPAL = "websocket.security.principal"; - public static final String SESSION_SUBJECT = "websocket.security.subject"; - public static final String SESSION_CREDENTIAL = "websocket.security.credential"; - - @AroundInvoke - public Object intercept(InvocationContext ctx) throws Exception { - Session session = findSessionParameter(ctx); - if (session != null) { - final Principal principal = (Principal) session.getUserProperties().get(SESSION_PRINCIPAL); - final Subject subject = (Subject) session.getUserProperties().get(SESSION_SUBJECT); - final Object credential = session.getUserProperties().get(SESSION_CREDENTIAL); - - if (principal != null && subject != null) { - setSubjectInfo(principal, subject, credential); - } - } - - return ctx.proceed(); - } - - protected Session findSessionParameter(InvocationContext ctx) { - Session session = null; - for (Object param : ctx.getParameters()) { - if (param instanceof Session) { - session = (Session) param; - break; - } - } - return session; - } - - protected void setSubjectInfo(final Principal principal, final Subject subject, final Object credential) { - SecurityContext securityContext = SecurityContextAssociation.getSecurityContext(); - Role roleGroup = getRoleGroup(subject); - Identity identity = CredentialIdentityFactory.createIdentity(principal, credential, roleGroup); - securityContext.getUtil().createSubjectInfo(identity, subject); - } - - protected Role getRoleGroup(final Subject subject) { - Role roleGroup = null; - for (Group group : subject.getPrincipals(Group.class)) { - if ("Roles".equals(group.getName())) { - roleGroup = new SimpleRoleGroup(group); - break; - } - } - return roleGroup; - } -} diff --git a/web-services/security/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension b/web-services/security/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension deleted file mode 100644 index 61047505349..00000000000 --- a/web-services/security/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension +++ /dev/null @@ -1 +0,0 @@ -datawave.security.auth.DatawaveServletExtension \ No newline at end of file diff --git a/web-services/security/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/web-services/security/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension deleted file mode 100644 index 759e9fe12ec..00000000000 --- a/web-services/security/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension +++ /dev/null @@ -1 +0,0 @@ -datawave.configuration.ConfigExtension \ No newline at end of file diff --git a/web-services/security/src/test/java/datawave/security/auth/DatawaveAuthenticationMechanismTest.java b/web-services/security/src/test/java/datawave/security/auth/DatawaveAuthenticationMechanismTest.java deleted file mode 100644 index 13bf702bdcb..00000000000 --- a/web-services/security/src/test/java/datawave/security/auth/DatawaveAuthenticationMechanismTest.java +++ /dev/null @@ -1,384 +0,0 @@ -package datawave.security.auth; - -import static datawave.security.util.DnUtils.normalizeDN; -import static io.undertow.security.api.AuthenticationMechanism.AuthenticationMechanismOutcome; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.mockito.Mockito.when; - -import java.security.KeyStore; -import java.security.PublicKey; -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; -import java.util.Enumeration; - -import javax.net.ssl.SSLPeerUnverifiedException; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; - -import io.undertow.security.api.SecurityContext; -import io.undertow.security.idm.Account; -import io.undertow.security.idm.Credential; -import io.undertow.security.idm.IdentityManager; -import io.undertow.server.HttpServerExchange; -import io.undertow.server.SSLSessionInfo; -import io.undertow.server.ServerConnection; -import io.undertow.util.HeaderMap; -import io.undertow.util.HttpString; - -@ExtendWith(MockitoExtension.class) -public class DatawaveAuthenticationMechanismTest { - - private static final HttpString PROXIED_ENTITIES_HEADER = new HttpString(DatawaveAuthenticationMechanism.PROXIED_ENTITIES_HEADER); - private static final HttpString PROXIED_ISSUERS_HEADER = new HttpString(DatawaveAuthenticationMechanism.PROXIED_ISSUERS_HEADER); - private static final HttpString SUBJECT_DN_HEADER = new HttpString("X-SSL-ClientCert-Subject"); - private static final HttpString ISSUER_DN_HEADER = new HttpString("X-SSL-ClientCert-Issuer"); - - private DatawaveAuthenticationMechanism datawaveAuthenticationMechanism; - - private HeaderMap httpRequestHeaders; - private HeaderMap httpResponseHeaders; - - @Mock - private SecurityContext securityContext; - - @Mock - private HttpServerExchange httpServerExchange; - - @Mock - private ServerConnection serverConnection; - - @Mock - private SSLSessionInfo sslSessionInfo; - - @Mock - private IdentityManager identityManager; - - @Mock - private Account account; - - private KeyStore truststore; - private KeyStore keystore; - private KeyStore serverKeystore; - private X509Certificate testUserCert; - private X509Certificate testServerCert; - private X509Certificate[] testUserCertChain; - private X509Certificate[] testServerCertChain; - - @BeforeEach - public void beforeEach() throws Exception { - System.setProperty("dw.trusted.header.authentication", "true"); - datawaveAuthenticationMechanism = new DatawaveAuthenticationMechanism(); - httpRequestHeaders = new HeaderMap(); - httpResponseHeaders = new HeaderMap(); - - truststore = KeyStore.getInstance("PKCS12"); - truststore.load(getClass().getResourceAsStream("/ca.pkcs12"), "secret".toCharArray()); - keystore = KeyStore.getInstance("PKCS12"); - keystore.load(getClass().getResourceAsStream("/testUser.pkcs12"), "secret".toCharArray()); - serverKeystore = KeyStore.getInstance("PKCS12"); - serverKeystore.load(getClass().getResourceAsStream("/testServer.pkcs12"), "secret".toCharArray()); - testUserCert = (X509Certificate) keystore.getCertificate("testuser"); - testServerCert = (X509Certificate) serverKeystore.getCertificate("testserver"); - - testUserCertChain = new X509Certificate[2]; - testUserCertChain[0] = testUserCert; - for (Enumeration e = truststore.aliases(); e.hasMoreElements();) { - X509Certificate cert = (X509Certificate) truststore.getCertificate(e.nextElement()); - if (cert.getSubjectDN().getName().equals(testUserCert.getIssuerDN().getName())) { - testUserCertChain[1] = cert; - break; - } - } - - testServerCertChain = new X509Certificate[2]; - testServerCertChain[0] = testServerCert; - for (Enumeration e = truststore.aliases(); e.hasMoreElements();) { - X509Certificate cert = (X509Certificate) truststore.getCertificate(e.nextElement()); - if (cert.getSubjectDN().getName().equals(testServerCert.getIssuerDN().getName())) { - testServerCertChain[1] = cert; - break; - } - } - - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - } - - @Test - public void testSSLSimpleLogin() throws Exception { - when(httpServerExchange.getConnection()).thenReturn(serverConnection); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - - long requestStartTime = System.nanoTime(); - when(httpServerExchange.getRequestStartTime()).thenReturn(requestStartTime); - - when(sslSessionInfo.getPeerCertificates()).thenReturn(new X509Certificate[] {testUserCert}); - - when(serverConnection.getSslSessionInfo()).thenReturn(sslSessionInfo); - - when(securityContext.getIdentityManager()).thenReturn(identityManager); - - ArgumentCaptor idCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor credentialCaptor = ArgumentCaptor.forClass(DatawaveCredential.class); - when(identityManager.verify(idCaptor.capture(), credentialCaptor.capture())).thenReturn(account); - - securityContext.authenticationComplete(account, "DATAWAVE-AUTH", false); - - AuthenticationMechanismOutcome outcome = datawaveAuthenticationMechanism.authenticate(httpServerExchange, securityContext); - assertEquals(AuthenticationMechanismOutcome.AUTHENTICATED, outcome); - assertEquals(testUserCert, credentialCaptor.getValue().getCertificate()); - assertFalse(httpResponseHeaders.contains(DatawaveAuthenticationMechanism.HEADER_PROXIED_ENTITIES_ACCEPTED)); - } - - @Test - public void testSSLProxiedLogin() throws Exception { - String expectedID = normalizeDN(testUserCert.getSubjectDN().getName()) + "<" + normalizeDN(testUserCert.getIssuerDN().getName()) + "><" - + normalizeDN(testServerCert.getSubjectDN().getName()) + "><" + normalizeDN(testServerCert.getIssuerDN().getName()) + ">"; - httpRequestHeaders.add(PROXIED_ENTITIES_HEADER, testUserCert.getSubjectDN().toString()); - httpRequestHeaders.add(PROXIED_ISSUERS_HEADER, testUserCert.getIssuerDN().toString()); - - when(httpServerExchange.getConnection()).thenReturn(serverConnection); - when(serverConnection.getSslSessionInfo()).thenReturn(sslSessionInfo); - when(sslSessionInfo.getPeerCertificates()).thenReturn(new X509Certificate[] {testServerCert}); - when(securityContext.getIdentityManager()).thenReturn(identityManager); - - ArgumentCaptor idCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor credentialCaptor = ArgumentCaptor.forClass(DatawaveCredential.class); - when(identityManager.verify(idCaptor.capture(), credentialCaptor.capture())).thenReturn(account); - securityContext.authenticationComplete(account, "DATAWAVE-AUTH", false); - - long requestStartTime = System.nanoTime(); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - when(httpServerExchange.getResponseHeaders()).thenReturn(httpResponseHeaders); - when(httpServerExchange.getRequestStartTime()).thenReturn(requestStartTime); - - AuthenticationMechanismOutcome outcome = datawaveAuthenticationMechanism.authenticate(httpServerExchange, securityContext); - assertEquals(AuthenticationMechanismOutcome.AUTHENTICATED, outcome); - assertEquals(testServerCert, credentialCaptor.getValue().getCertificate()); - assertEquals("true", httpResponseHeaders.getFirst(DatawaveAuthenticationMechanism.HEADER_PROXIED_ENTITIES_ACCEPTED)); - assertEquals(expectedID, idCaptor.getValue()); - } - - @Test - public void testSSLWithoutPeerCerts() throws Exception { - httpRequestHeaders.add(SUBJECT_DN_HEADER, testUserCert.getSubjectDN().toString()); - httpRequestHeaders.add(ISSUER_DN_HEADER, testUserCert.getIssuerDN().toString()); - - String expectedID = normalizeDN(testUserCert.getSubjectDN().getName()) + "<" + normalizeDN(testUserCert.getIssuerDN().getName()) + ">"; - - when(httpServerExchange.getConnection()).thenReturn(serverConnection); - when(serverConnection.getSslSessionInfo()).thenReturn(sslSessionInfo); - when(sslSessionInfo.getPeerCertificates()).thenThrow(new SSLPeerUnverifiedException("no client cert")); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - when(securityContext.getIdentityManager()).thenReturn(identityManager); - - ArgumentCaptor idCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor credentialCaptor = ArgumentCaptor.forClass(Credential.class); - when(identityManager.verify(idCaptor.capture(), credentialCaptor.capture())).thenReturn(account); - - securityContext.authenticationComplete(account, "DATAWAVE-AUTH", false); - long requestStartTime = System.nanoTime(); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - when(httpServerExchange.getRequestStartTime()).thenReturn(requestStartTime); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - - AuthenticationMechanismOutcome outcome = datawaveAuthenticationMechanism.authenticate(httpServerExchange, securityContext); - assertEquals(AuthenticationMechanismOutcome.AUTHENTICATED, outcome); - assertFalse(httpResponseHeaders.contains(DatawaveAuthenticationMechanism.HEADER_PROXIED_ENTITIES_ACCEPTED)); - assertEquals(expectedID, idCaptor.getValue()); - } - - @Test - public void testSSLWithoutPeerCertsNoTrustedHeaderAuthentication() throws Exception { - - ReflectionTestUtils.setField(datawaveAuthenticationMechanism, "trustedHeaderAuthentication", false); - - Certificate cert = new Certificate("DUMMY") { - @Override - public byte[] getEncoded() { - return new byte[0]; - } - - @Override - public void verify(PublicKey key) {} - - @Override - public void verify(PublicKey key, String sigProvider) {} - - @Override - public String toString() { - return null; - } - - @Override - public PublicKey getPublicKey() { - return null; - } - }; - - when(httpServerExchange.getConnection()).thenReturn(serverConnection); - when(serverConnection.getSslSessionInfo()).thenReturn(sslSessionInfo); - when(sslSessionInfo.getPeerCertificates()).thenReturn(new Certificate[] {cert}); - long requestStartTime = System.nanoTime(); - when(httpServerExchange.getRequestStartTime()).thenReturn(requestStartTime); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - - AuthenticationMechanismOutcome outcome = datawaveAuthenticationMechanism.authenticate(httpServerExchange, securityContext); - assertEquals(AuthenticationMechanismOutcome.NOT_ATTEMPTED, outcome); - } - - @Test - public void testMissingIssuersHeader() { - httpRequestHeaders.add(PROXIED_ENTITIES_HEADER, "foo"); - - securityContext.authenticationFailed("X-ProxiedEntitiesChain supplied, but missing X-ProxiedIssuersChain is missing!", "DATAWAVE-AUTH"); - when(httpServerExchange.getRequestStartTime()).thenReturn(System.nanoTime()); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - - AuthenticationMechanismOutcome outcome = datawaveAuthenticationMechanism.authenticate(httpServerExchange, securityContext); - assertEquals(AuthenticationMechanismOutcome.NOT_AUTHENTICATED, outcome); - } - - @Test - public void testNonSSLSimpleLogin() { - httpRequestHeaders.add(SUBJECT_DN_HEADER, testUserCert.getSubjectDN().toString()); - httpRequestHeaders.add(ISSUER_DN_HEADER, testUserCert.getIssuerDN().toString()); - - String expectedID = normalizeDN(testUserCert.getSubjectDN().getName()) + "<" + normalizeDN(testUserCert.getIssuerDN().getName()) + ">"; - - when(httpServerExchange.getConnection()).thenReturn(serverConnection); - when(serverConnection.getSslSessionInfo()).thenReturn(null); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - when(securityContext.getIdentityManager()).thenReturn(identityManager); - - ArgumentCaptor idCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor credentialCaptor = ArgumentCaptor.forClass(Credential.class); - when(identityManager.verify(idCaptor.capture(), credentialCaptor.capture())).thenReturn(account); - - securityContext.authenticationComplete(account, "DATAWAVE-AUTH", false); - long requestStartTime = System.nanoTime(); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - when(httpServerExchange.getRequestStartTime()).thenReturn(requestStartTime); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - - AuthenticationMechanismOutcome outcome = datawaveAuthenticationMechanism.authenticate(httpServerExchange, securityContext); - assertEquals(AuthenticationMechanismOutcome.AUTHENTICATED, outcome); - assertEquals(expectedID, idCaptor.getValue()); - } - - @Test - public void testNonSSLProxiedLogin() { - httpRequestHeaders.add(PROXIED_ENTITIES_HEADER, testUserCert.getSubjectDN().toString()); - httpRequestHeaders.add(PROXIED_ISSUERS_HEADER, testUserCert.getIssuerDN().toString()); - httpRequestHeaders.add(SUBJECT_DN_HEADER, testServerCert.getSubjectDN().toString()); - httpRequestHeaders.add(ISSUER_DN_HEADER, testServerCert.getIssuerDN().toString()); - - String expectedID = normalizeDN(testUserCert.getSubjectDN().getName()) + "<" + normalizeDN(testUserCert.getIssuerDN().getName()) + "><" - + normalizeDN(testServerCert.getSubjectDN().getName()) + "><" + normalizeDN(testServerCert.getIssuerDN().getName()) + ">"; - - when(httpServerExchange.getConnection()).thenReturn(serverConnection); - when(serverConnection.getSslSessionInfo()).thenReturn(null); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - when(securityContext.getIdentityManager()).thenReturn(identityManager); - - ArgumentCaptor idCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor credentialCaptor = ArgumentCaptor.forClass(DatawaveCredential.class); - when(identityManager.verify(idCaptor.capture(), credentialCaptor.capture())).thenReturn(account); - - securityContext.authenticationComplete(account, "DATAWAVE-AUTH", false); - long requestStartTime = System.nanoTime(); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - when(httpServerExchange.getResponseHeaders()).thenReturn(httpResponseHeaders); - when(httpServerExchange.getRequestStartTime()).thenReturn(requestStartTime); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - - AuthenticationMechanismOutcome outcome = datawaveAuthenticationMechanism.authenticate(httpServerExchange, securityContext); - assertEquals(AuthenticationMechanismOutcome.AUTHENTICATED, outcome); - assertEquals("true", httpResponseHeaders.getFirst(DatawaveAuthenticationMechanism.HEADER_PROXIED_ENTITIES_ACCEPTED)); - assertEquals(expectedID, idCaptor.getValue()); - } - - @Test - public void testNonSSLoginWithoutIssuerHeaderFails() { - httpRequestHeaders.add(SUBJECT_DN_HEADER, testUserCert.getSubjectDN().toString()); - - when(httpServerExchange.getConnection()).thenReturn(serverConnection); - when(serverConnection.getSslSessionInfo()).thenReturn(null); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - securityContext.authenticationFailed( - "Missing trusted subject DN (" + testUserCert.getSubjectDN() + ") or issuer DN (null) for trusted header authentication.", - "DATAWAVE-AUTH"); - when(httpServerExchange.getRequestStartTime()).thenReturn(System.nanoTime()); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - - AuthenticationMechanismOutcome outcome = datawaveAuthenticationMechanism.authenticate(httpServerExchange, securityContext); - assertEquals(AuthenticationMechanismOutcome.NOT_AUTHENTICATED, outcome); - } - - @Test - public void testNonSSLoginWithoutSubjectHeaderFails() { - httpRequestHeaders.add(ISSUER_DN_HEADER, testUserCert.getIssuerDN().toString()); - - when(httpServerExchange.getConnection()).thenReturn(serverConnection); - when(serverConnection.getSslSessionInfo()).thenReturn(null); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - securityContext.authenticationFailed( - "Missing trusted subject DN (null) or issuer DN (" + testUserCert.getIssuerDN() + ") for trusted header authentication.", - "DATAWAVE-AUTH"); - when(httpServerExchange.getRequestStartTime()).thenReturn(System.nanoTime()); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - - AuthenticationMechanismOutcome outcome = datawaveAuthenticationMechanism.authenticate(httpServerExchange, securityContext); - assertEquals(AuthenticationMechanismOutcome.NOT_AUTHENTICATED, outcome); - } - - @Test - public void testNonSSLSimpleLoginFails() { - System.clearProperty("dw.trusted.header.authentication"); - datawaveAuthenticationMechanism = new DatawaveAuthenticationMechanism(); - - httpRequestHeaders.add(SUBJECT_DN_HEADER, testUserCert.getSubjectDN().toString()); - httpRequestHeaders.add(ISSUER_DN_HEADER, testUserCert.getIssuerDN().toString()); - - when(httpServerExchange.getConnection()).thenReturn(serverConnection); - when(serverConnection.getSslSessionInfo()).thenReturn(null); - long requestStartTime = System.nanoTime(); - when(httpServerExchange.getRequestStartTime()).thenReturn(requestStartTime); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - - AuthenticationMechanismOutcome outcome = datawaveAuthenticationMechanism.authenticate(httpServerExchange, securityContext); - assertEquals(AuthenticationMechanismOutcome.NOT_ATTEMPTED, outcome); - } - - @Test - public void testJWTHeaderAuthentication() { - ReflectionTestUtils.setField(datawaveAuthenticationMechanism, "trustedHeaderAuthentication", false); - ReflectionTestUtils.setField(datawaveAuthenticationMechanism, "jwtHeaderAuthentication", true); - - httpRequestHeaders.add(new HttpString("Authorization"), "Bearer 1234"); - - when(httpServerExchange.getConnection()).thenReturn(serverConnection); - when(serverConnection.getSslSessionInfo()).thenReturn(null); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - when(securityContext.getIdentityManager()).thenReturn(identityManager); - - ArgumentCaptor idCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor credentialCaptor = ArgumentCaptor.forClass(DatawaveCredential.class); - when(identityManager.verify(idCaptor.capture(), credentialCaptor.capture())).thenReturn(account); - - securityContext.authenticationComplete(account, "DATAWAVE-AUTH", false); - when(httpServerExchange.getRequestStartTime()).thenReturn(System.nanoTime()); - when(httpServerExchange.getRequestHeaders()).thenReturn(httpRequestHeaders); - - AuthenticationMechanismOutcome outcome = datawaveAuthenticationMechanism.authenticate(httpServerExchange, securityContext); - assertEquals(AuthenticationMechanismOutcome.AUTHENTICATED, outcome); - assertEquals("1234", idCaptor.getValue()); - } -} diff --git a/web-services/security/src/test/java/datawave/security/cache/CredentialsCacheBeanTest.java b/web-services/security/src/test/java/datawave/security/cache/CredentialsCacheBeanTest.java deleted file mode 100644 index 3a3d7c6c7c6..00000000000 --- a/web-services/security/src/test/java/datawave/security/cache/CredentialsCacheBeanTest.java +++ /dev/null @@ -1,215 +0,0 @@ -package datawave.security.cache; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - -import java.security.Principal; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.enterprise.context.ApplicationScoped; -import javax.enterprise.inject.Default; -import javax.inject.Inject; - -import org.apache.accumulo.core.client.AccumuloClient; -import org.jboss.arquillian.container.test.api.Deployment; -import org.jboss.arquillian.junit.Arquillian; -import org.jboss.security.CacheableManager; -import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.asset.EmptyAsset; -import org.jboss.shrinkwrap.api.spec.JavaArchive; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.collect.Lists; - -import datawave.configuration.spring.BeanProvider; -import datawave.core.common.connection.AccumuloConnectionFactory; -import datawave.core.common.result.ConnectionPool; -import datawave.security.DnList; -import datawave.security.authorization.DatawavePrincipal; -import datawave.security.authorization.DatawaveUser; -import datawave.security.authorization.DatawaveUser.UserType; -import datawave.security.authorization.SubjectIssuerDNPair; -import datawave.security.system.AuthorizationCache; - -@RunWith(Arquillian.class) -public class CredentialsCacheBeanTest { - - private CredentialsCacheBean ccb; - - @Inject - private CacheableManager authManager; - - private Cache cache; - - @Deployment - public static JavaArchive createDeployment() { - System.setProperty("cdi.bean.context", "springFrameworkBeanRefContext.xml"); - // @formatter:off - return ShrinkWrap - .create(JavaArchive.class) - .addPackages(true, "org.apache.deltaspike", "io.astefanutti.metrics.cdi") - .addClasses(CredentialsCacheBean.class) - .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"); - // @formatter:on - } - - @Before - public void setUp() { - // With Arquillian we would normally inject this bean into the test class. However there seems to be - // an incompatibility with Arquillian and the @Singleton annotation on the bean where any method - // invoked on the bean throws a NullPointerException. Instead, we instantiate the bean manually and - // force CDI field injection. This gets everything loaded as we want for testing. - // TODO: identify and resolve the underlying issue - ccb = new CredentialsCacheBean(); - BeanProvider.injectFields(ccb); - - cache = CacheBuilder.newBuilder().build(); - authManager.setCache(cache); - - DatawaveUser u1 = new DatawaveUser(SubjectIssuerDNPair.of("user1", "issuer1"), UserType.USER, null, null, null, -1); - DatawaveUser u2 = new DatawaveUser(SubjectIssuerDNPair.of("user2", "issuer2"), UserType.USER, null, null, null, -1); - DatawaveUser s1 = new DatawaveUser(SubjectIssuerDNPair.of("server1", "issuer1"), UserType.SERVER, null, null, null, -1); - - DatawavePrincipal dp1 = new DatawavePrincipal(Arrays.asList(u1, s1)); - DatawavePrincipal dp2 = new DatawavePrincipal(Collections.singleton(u1)); - DatawavePrincipal dp3 = new DatawavePrincipal(Arrays.asList(u2, s1)); - - cache.put(dp1, dp1); - cache.put(dp2, dp2); - cache.put(dp3, dp3); - } - - @After - public void tearDown() {} - - @Test - public void testFlushAll() { - assertEquals(3, cache.size()); - - ccb.flushAll(); - - assertEquals(0, cache.size()); - } - - @Test - public void testEvict() { - Principal expected = cache.asMap().keySet().stream().filter(p -> p.getName().startsWith("user2")).findFirst().orElse(null); - assertNotNull(expected); - assertEquals(3, cache.size()); - ccb.evict("user2"); - assertNull(cache.getIfPresent(expected)); - assertEquals(2, cache.size()); - } - - @Test - public void testListDNs() { - ArrayList expectedDns = Lists.newArrayList("user2", "server1", "user1"); - DnList dnList = ccb.listDNs(false); - assertEquals(3, dnList.getDns().size()); - assertEquals(expectedDns, new ArrayList<>(dnList.getDns())); - } - - @Test - public void testListMatching() { - ArrayList expectedDns = Lists.newArrayList("server1", "user1"); - DnList dnList = ccb.listDNsMatching("issuer1"); - assertEquals(2, dnList.getDns().size()); - assertEquals(expectedDns, new ArrayList<>(dnList.getDns())); - } - - @Test - public void testList() { - DatawaveUser u = ccb.list("user2"); - assertNotNull(u); - assertEquals("user2", u.getName()); - } - - @Default - @ApplicationScoped - @AuthorizationCache - private static class TestCacheableManager implements CacheableManager { - private Cache cache; - - @Override - public void setCache(Object o) { - // noinspection unchecked - cache = (Cache) o; - } - - @Override - public void flushCache() { - cache.invalidateAll(); - cache.cleanUp(); - } - - @Override - public void flushCache(Principal principal) { - cache.invalidate(principal); - } - - @Override - public boolean containsKey(Principal principal) { - return cache.getIfPresent(principal) != null; - } - - @Override - public Set getCachedKeys() { - return new HashSet<>(cache.asMap().values()); - } - } - - private static class MockAccumuloConnectionFactory implements AccumuloConnectionFactory { - @Override - public AccumuloClient getClient(String userDN, Collection proxiedDNs, Priority priority, Map trackingMap) { - return null; - } - - @Override - public AccumuloClient getClient(String userDN, Collection proxiedDNs, String poolName, Priority priority, Map trackingMap) { - return null; - } - - @Override - public void returnClient(AccumuloClient client) { - - } - - @Override - public String report() { - return null; - } - - @Override - public List getConnectionPools() { - return null; - } - - @Override - public int getConnectionUsagePercent() { - return 0; - } - - @Override - public Map getTrackingMap(StackTraceElement[] stackTrace) { - return null; - } - - @Override - public void close() throws Exception { - - } - } -} diff --git a/web-services/security/src/test/java/datawave/security/login/DatawaveCertRolesLoginModuleTest.java b/web-services/security/src/test/java/datawave/security/login/DatawaveCertRolesLoginModuleTest.java deleted file mode 100644 index ff1ac93fc78..00000000000 --- a/web-services/security/src/test/java/datawave/security/login/DatawaveCertRolesLoginModuleTest.java +++ /dev/null @@ -1,106 +0,0 @@ -package datawave.security.login; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.security.KeyStore; -import java.security.cert.X509Certificate; -import java.util.HashMap; - -import javax.security.auth.Subject; - -import org.jboss.security.SimplePrincipal; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.test.util.ReflectionTestUtils; - -import datawave.security.authorization.DatawavePrincipal; -import datawave.security.util.MockCallbackHandler; -import datawave.security.util.MockDatawaveCertVerifier; - -public class DatawaveCertRolesLoginModuleTest { - - private DatawaveCertRolesLoginModule loginModule; - private MockCallbackHandler callbackHandler; - - private X509Certificate testUserCert; - - @BeforeEach - public void beforeEach() throws Exception { - callbackHandler = new MockCallbackHandler("Alias: ", "Certificate: "); - - HashMap sharedState = new HashMap<>(); - HashMap options = new HashMap<>(); - options.put("rolesProperties", "roles.properties"); - options.put("principalClass", "datawave.security.authorization.DatawavePrincipal"); - options.put("verifier", MockDatawaveCertVerifier.class.getName()); - - loginModule = new DatawaveCertRolesLoginModule(); - loginModule.initialize(new Subject(), callbackHandler, sharedState, options); - - KeyStore truststore = KeyStore.getInstance("PKCS12"); - truststore.load(getClass().getResourceAsStream("/ca.pkcs12"), "secret".toCharArray()); - KeyStore keystore = KeyStore.getInstance("PKCS12"); - keystore.load(getClass().getResourceAsStream("/testUser.pkcs12"), "secret".toCharArray()); - testUserCert = (X509Certificate) keystore.getCertificate("testuser"); - } - - @Test - public void testSuccessfulLogin() throws Exception { - String name = testUserCert.getSubjectDN().getName() + "<" + testUserCert.getIssuerDN().getName() + ">"; - callbackHandler.name = name; - callbackHandler.credential = testUserCert; - - boolean result = loginModule.login(); - assertTrue(result, "Login didn't succeed for alias in roles.properties"); - DatawavePrincipal principal = (DatawavePrincipal) ReflectionTestUtils.getField(loginModule, "identity"); - assertNotNull(principal); - assertEquals(name.toLowerCase(), principal.getName()); - } - - @Test - public void testFailedLogin() throws Exception { - callbackHandler.name = "fakeUser"; - callbackHandler.credential = testUserCert; - - boolean result = loginModule.login(); - assertFalse(result, "Login succeed for alias not in roles.properties"); - } - - @Test - public void testSuccessfulLoginNoIssuer() throws Exception { - HashMap sharedState = new HashMap<>(); - HashMap options = new HashMap<>(); - options.put("rolesProperties", "rolesNoIssuer.properties"); - options.put("principalClass", SimplePrincipal.class.getName()); - options.put("verifier", MockDatawaveCertVerifier.class.getName()); - options.put("addIssuerDN", Boolean.FALSE.toString()); - - loginModule = new DatawaveCertRolesLoginModule(); - loginModule.initialize(new Subject(), callbackHandler, sharedState, options); - - callbackHandler.name = testUserCert.getSubjectDN().getName(); - callbackHandler.credential = testUserCert; - - boolean result = loginModule.login(); - assertTrue(result, "Login didn't succeed for alias in rolesNoIssuer.properties"); - SimplePrincipal principal = (SimplePrincipal) ReflectionTestUtils.getField(loginModule, "identity"); - assertNotNull(principal); - assertEquals(testUserCert.getSubjectDN().getName().toLowerCase(), principal.getName()); - } - - @Test - public void testTrustedHeaderLogin() throws Exception { - HashMap sharedState = new HashMap<>(); - HashMap options = new HashMap<>(); - options.put("trustedHeaderLogin", Boolean.TRUE.toString()); - - loginModule = new DatawaveCertRolesLoginModule(); - loginModule.initialize(new Subject(), callbackHandler, sharedState, options); - - boolean result = loginModule.login(); - assertFalse(result); - } -} diff --git a/web-services/security/src/test/java/datawave/security/login/DatawaveCertVerifierTest.java b/web-services/security/src/test/java/datawave/security/login/DatawaveCertVerifierTest.java deleted file mode 100644 index 7148c357963..00000000000 --- a/web-services/security/src/test/java/datawave/security/login/DatawaveCertVerifierTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package datawave.security.login; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.security.KeyStore; -import java.security.cert.X509Certificate; - -import org.jboss.logging.Logger; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class DatawaveCertVerifierTest { - - private DatawaveCertVerifier verifier; - - private KeyStore truststore; - private KeyStore keystore; - private X509Certificate testUserCert; - - @BeforeEach - public void beforeEach() throws Exception { - verifier = new DatawaveCertVerifier(); - - truststore = KeyStore.getInstance("PKCS12"); - truststore.load(getClass().getResourceAsStream("/ca.pkcs12"), "secret".toCharArray()); - keystore = KeyStore.getInstance("PKCS12"); - keystore.load(getClass().getResourceAsStream("/testUser.pkcs12"), "secret".toCharArray()); - testUserCert = (X509Certificate) keystore.getCertificate("testuser"); - - verifier.setLogger(Logger.getLogger(DatawaveCertVerifier.class)); - } - - @Test - public void testVerifyNoOCSP() { - verifier.setOcspLevel(DatawaveCertVerifier.OcspLevel.OFF.name()); - boolean valid = verifier.verify(testUserCert, testUserCert.getSubjectDN().getName(), keystore, truststore); - assertTrue(valid, "Verify failed unexpectedly."); - } -} diff --git a/web-services/security/src/test/java/datawave/security/login/DatawavePrincipalLoginModuleTest.java b/web-services/security/src/test/java/datawave/security/login/DatawavePrincipalLoginModuleTest.java deleted file mode 100644 index 95b7545a168..00000000000 --- a/web-services/security/src/test/java/datawave/security/login/DatawavePrincipalLoginModuleTest.java +++ /dev/null @@ -1,627 +0,0 @@ -package datawave.security.login; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertThrowsExactly; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; -import static org.mockito.Mockito.when; - -import java.net.Socket; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.Principal; -import java.security.PrivateKey; -import java.security.UnrecoverableKeyException; -import java.security.acl.Group; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.List; -import java.util.stream.Collectors; - -import javax.net.ssl.KeyManager; -import javax.net.ssl.X509KeyManager; -import javax.security.auth.Subject; -import javax.security.auth.login.AccountLockedException; -import javax.security.auth.login.CredentialException; -import javax.security.auth.login.FailedLoginException; -import javax.security.auth.login.LoginException; - -import org.jboss.security.JSSESecurityDomain; -import org.jboss.security.SimpleGroup; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; - -import com.google.common.collect.Lists; - -import datawave.security.auth.DatawaveCredential; -import datawave.security.authorization.AuthorizationException; -import datawave.security.authorization.DatawavePrincipal; -import datawave.security.authorization.DatawaveUser; -import datawave.security.authorization.DatawaveUser.UserType; -import datawave.security.authorization.DatawaveUserService; -import datawave.security.authorization.JWTTokenHandler; -import datawave.security.authorization.SubjectIssuerDNPair; -import datawave.security.util.DnUtils; -import datawave.security.util.MockCallbackHandler; -import datawave.security.util.MockDatawaveCertVerifier; - -@ExtendWith(MockitoExtension.class) -public class DatawavePrincipalLoginModuleTest { - - private static final String DISALLOWLIST_ROLE = "DISALLOWLIST_ROLE"; - - @InjectMocks - private DatawavePrincipalLoginModule datawaveLoginModule = new TestDatawavePrincipalLoginModule(); - - @Mock - private JSSESecurityDomain securityDomain; - - @Mock - private DatawaveUserService datawaveUserService; - private MockCallbackHandler callbackHandler; - - private KeyStore truststore; - private KeyStore keystore; - private KeyStore serverKeystore; - private X509Certificate testUserCert; - private X509Certificate testServerCert; - - private SubjectIssuerDNPair userDN; - private DatawavePrincipal defaultPrincipal; - - @BeforeEach - public void beforeEach() throws Exception { - System.setProperty(DnUtils.NPE_OU_PROPERTY, "iamnotaperson"); - MockDatawaveCertVerifier.issuerSupported = true; - MockDatawaveCertVerifier.verify = true; - - callbackHandler = new MockCallbackHandler("Username: ", "Credentials: "); - - truststore = KeyStore.getInstance("PKCS12"); - truststore.load(getClass().getResourceAsStream("/ca.pkcs12"), "secret".toCharArray()); - keystore = KeyStore.getInstance("PKCS12"); - keystore.load(getClass().getResourceAsStream("/testUser.pkcs12"), "secret".toCharArray()); - serverKeystore = KeyStore.getInstance("PKCS12"); - serverKeystore.load(getClass().getResourceAsStream("/testServer.pkcs12"), "secret".toCharArray()); - testUserCert = (X509Certificate) keystore.getCertificate("testuser"); - testServerCert = (X509Certificate) serverKeystore.getCertificate("testserver"); - - KeyManager keyManager = new X509KeyManager() { - @Override - public String[] getClientAliases(String s, Principal[] principals) { - return new String[0]; - } - - @Override - public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) { - return null; - } - - @Override - public String[] getServerAliases(String s, Principal[] principals) { - return new String[0]; - } - - @Override - public String chooseServerAlias(String s, Principal[] principals, Socket socket) { - return null; - } - - @Override - public X509Certificate[] getCertificateChain(String s) { - try { - return Arrays.stream(keystore.getCertificateChain(s)).map(X509Certificate.class::cast).toArray(X509Certificate[]::new); - } catch (KeyStoreException e) { - fail(e.getMessage()); - return null; - } - } - - @Override - public PrivateKey getPrivateKey(String s) { - try { - return (PrivateKey) keystore.getKey(s, "secret".toCharArray()); - } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) { - fail(e.getMessage()); - return null; - } - } - }; - - when(securityDomain.getKeyStore()).thenReturn(keystore); - when(securityDomain.getKeyManagers()).thenReturn(new KeyManager[] {keyManager}); - - HashMap sharedState = new HashMap<>(); - HashMap options = new HashMap<>(); - options.put("principalClass", "datawave.security.authorization.DatawavePrincipal"); - options.put("verifier", MockDatawaveCertVerifier.class.getName()); - options.put("passwordStacking", "useFirstPass"); - options.put("ocspLevel", "required"); - options.put("disallowlistUserRole", DISALLOWLIST_ROLE); - options.put("requiredRoles", "AuthorizedUser:AuthorizedServer:AuthorizedQueryServer:OtherRequiredRole"); - options.put("directRoles", "AuthorizedQueryServer:AuthorizedServer"); - - ReflectionTestUtils.setField(datawaveLoginModule, "datawaveUserService", datawaveUserService); - ReflectionTestUtils.setField(datawaveLoginModule, "domain", securityDomain); - datawaveLoginModule.initialize(new Subject(), callbackHandler, sharedState, options); - - userDN = SubjectIssuerDNPair.of(testUserCert.getSubjectDN().getName(), testUserCert.getIssuerDN().getName()); - DatawaveUser defaultUser = new DatawaveUser(userDN, UserType.USER, null, null, null, System.currentTimeMillis()); - defaultPrincipal = new DatawavePrincipal(Lists.newArrayList(defaultUser)); - } - - @Test - public void testValidLogin() throws Exception { - DatawaveCredential datawaveCredential = new DatawaveCredential(testUserCert, null, null); - callbackHandler.name = datawaveCredential.getUserName(); - callbackHandler.credential = datawaveCredential; - - when(securityDomain.getKeyStore()).thenReturn(keystore); - when(securityDomain.getTrustStore()).thenReturn(truststore); - when(datawaveUserService.lookup(datawaveCredential.getEntities())).thenReturn(defaultPrincipal.getProxiedUsers()); - - boolean result = datawaveLoginModule.login(); - assertTrue(result, "Login did not succeed."); - } - - @Test - public void testGetRoleSets() throws Exception { - DatawaveCredential datawaveCredential = new DatawaveCredential(testUserCert, null, null); - callbackHandler.name = datawaveCredential.getUserName(); - callbackHandler.credential = datawaveCredential; - - String[] expectedRoles = new String[] {"Role1", "Role2", "Role3"}; - - DatawaveUser user = new DatawaveUser(userDN, UserType.USER, Arrays.asList("a", "b", "c"), Arrays.asList(expectedRoles), null, - System.currentTimeMillis()); - DatawavePrincipal expected = new DatawavePrincipal(Lists.newArrayList(user)); - - when(securityDomain.getKeyStore()).thenReturn(keystore); - when(securityDomain.getTrustStore()).thenReturn(truststore); - when(datawaveUserService.lookup(datawaveCredential.getEntities())).thenReturn(expected.getProxiedUsers()); - - boolean result = datawaveLoginModule.login(); - assertTrue(result, "Login did not succeed."); - - Group[] roleSets = datawaveLoginModule.getRoleSets(); - assertEquals(2, roleSets.length); - - SimpleGroup roles = (SimpleGroup) roleSets[0]; - assertEquals("Roles", roles.getName()); - ArrayList rolesList = new ArrayList<>(); - for (Enumeration members = roles.members(); members.hasMoreElements(); /* empty */) { - rolesList.add(members.nextElement().getName()); - } - Collections.sort(rolesList); - assertEquals(3, rolesList.size()); - assertArrayEquals(expectedRoles, rolesList.toArray()); - - SimpleGroup callerPrincipal = (SimpleGroup) roleSets[1]; - assertEquals("CallerPrincipal", callerPrincipal.getName()); - Enumeration members = callerPrincipal.members(); - assertTrue(members.hasMoreElements(), "CallerPrincipal group has no members"); - Principal p = members.nextElement(); - assertEquals(expected, p); - assertFalse(members.hasMoreElements(), "CallerPrincipal group has too many members"); - } - - @Test - public void testGetRoleSetsLeavesRequiredRoles() throws Exception { - // Proxied entities has the original user DN, plus it came through a server and - // the request is being made by a second server. Make sure that the resulting - // principal has all 3 server DNs in its list, and the user DN is not one of the - // server DNs. - String issuerDN = DnUtils.normalizeDN(testServerCert.getIssuerDN().getName()); - String serverDN = DnUtils.normalizeDN("CN=testServer.example.com, OU=iamnotaperson, OU=acme"); - SubjectIssuerDNPair server1 = SubjectIssuerDNPair.of(serverDN, issuerDN); - String otherServerDN = DnUtils.normalizeDN("CN=otherServer.example.com, OU=iamnotaperson, OU=acme"); - SubjectIssuerDNPair server2 = SubjectIssuerDNPair.of(otherServerDN, issuerDN); - String proxiedSubjects = "<" + userDN.subjectDN() + "><" + otherServerDN + ">"; - String proxiedIssuers = "<" + userDN.issuerDN() + "><" + issuerDN + ">"; - DatawaveCredential datawaveCredential = new DatawaveCredential(testServerCert, proxiedSubjects, proxiedIssuers); - callbackHandler.name = datawaveCredential.getUserName(); - callbackHandler.credential = datawaveCredential; - - List userRoles = Arrays.asList("Role1", "AuthorizedUser"); - List s1Roles = Arrays.asList("Role2", "AuthorizedServer"); - List s2Roles = Arrays.asList("Role3", "OtherRequiredRole"); - - DatawaveUser user = new DatawaveUser(userDN, UserType.USER, null, userRoles, null, System.currentTimeMillis()); - DatawaveUser s1 = new DatawaveUser(server1, UserType.SERVER, null, s1Roles, null, System.currentTimeMillis()); - DatawaveUser s2 = new DatawaveUser(server2, UserType.SERVER, null, s2Roles, null, System.currentTimeMillis()); - DatawavePrincipal expected = new DatawavePrincipal(Lists.newArrayList(user, s2, s1)); - - when(securityDomain.getKeyStore()).thenReturn(serverKeystore); - when(securityDomain.getTrustStore()).thenReturn(truststore); - when(datawaveUserService.lookup(datawaveCredential.getEntities())).thenReturn(expected.getProxiedUsers()); - - boolean result = datawaveLoginModule.login(); - assertTrue(result, "Login did not succeed."); - assertEquals(userDN, expected.getUserDN()); - - Group[] roleSets = datawaveLoginModule.getRoleSets(); - assertEquals(2, roleSets.length); - assertEquals("Roles", roleSets[0].getName()); - List groupSetRoles = Collections.list(roleSets[0].members()).stream().map(Principal::getName).collect(Collectors.toList()); - assertEquals(Lists.newArrayList("Role1", "AuthorizedUser"), groupSetRoles); - } - - @Test - public void testProxiedEntitiesLoginNoRole() throws Exception { - // Call Chain is U -> S1 -> S2. S2 will have no role. This test case tests - // the case of no role for the terminal service. This should fail with - // CredentialException thrown. - String issuerDN = DnUtils.normalizeDN(testServerCert.getIssuerDN().getName()); - String serverDN = DnUtils.normalizeDN("CN=testServer.example.com, OU=iamnotaperson, OU=acme"); - SubjectIssuerDNPair server1 = SubjectIssuerDNPair.of(serverDN, issuerDN); - String otherServerDN = DnUtils.normalizeDN("CN=otherServer.example.com, OU=iamnotaperson, OU=acme"); - SubjectIssuerDNPair server2 = SubjectIssuerDNPair.of(otherServerDN, issuerDN); - String proxiedSubjects = "<" + userDN.subjectDN() + "><" + otherServerDN + ">"; - String proxiedIssuers = "<" + userDN.issuerDN() + "><" + issuerDN + ">"; - DatawaveCredential datawaveCredential = new DatawaveCredential(testServerCert, proxiedSubjects, proxiedIssuers); - callbackHandler.name = datawaveCredential.getUserName(); - callbackHandler.credential = datawaveCredential; - - List s1Roles = List.of("AuthorizedServer"); - - DatawaveUser s1 = new DatawaveUser(server1, UserType.SERVER, null, s1Roles, null, System.currentTimeMillis()); - DatawaveUser s2 = new DatawaveUser(server2, UserType.SERVER, null, null, null, System.currentTimeMillis()); - DatawavePrincipal expected = new DatawavePrincipal(Lists.newArrayList(defaultPrincipal.getPrimaryUser(), s1, s2)); - - when(securityDomain.getKeyStore()).thenReturn(serverKeystore); - when(securityDomain.getTrustStore()).thenReturn(truststore); - when(datawaveUserService.lookup(datawaveCredential.getEntities())).thenReturn(expected.getProxiedUsers()); - - assertThrows(FailedLoginException.class, datawaveLoginModule::login); - } - - @Test - public void testDirectRolesFailServer() throws Exception { - /* - * Chain is User -> S1 -> S2. S2 is terminal server. Verified that s2 does not have the appropriate authorized role for terminal server (directRole). - * This will fail the check in the #DatawavePrincipalLoginModule.login() This will prevent the chain from accessing any endpoint which requires - * authorized roles - */ - - String issuerDN = DnUtils.normalizeDN(testServerCert.getIssuerDN().getName()); - String serverDN = DnUtils.normalizeDN("CN=testServer.example.com, OU=iamnotaperson, OU=acme"); - SubjectIssuerDNPair server1 = SubjectIssuerDNPair.of(serverDN, issuerDN); - String otherServerDN = DnUtils.normalizeDN("CN=otherServer.example.com, OU=iamnotaperson, OU=acme"); - SubjectIssuerDNPair server2 = SubjectIssuerDNPair.of(otherServerDN, issuerDN); - String proxiedSubjects = "<" + userDN.subjectDN() + "><" + otherServerDN + ">"; - String proxiedIssuers = "<" + userDN.issuerDN() + "><" + issuerDN + ">"; - DatawaveCredential datawaveCredential = new DatawaveCredential(testServerCert, proxiedSubjects, proxiedIssuers); - callbackHandler.name = datawaveCredential.getUserName(); - callbackHandler.credential = datawaveCredential; - - List userRoles = Arrays.asList("Role1", "AuthorizedUser"); - List s1Roles = Arrays.asList("Role2", "AuthorizedServer"); - List s2Roles = Arrays.asList("Role3", "OtherRequiredRole"); - - DatawaveUser user = new DatawaveUser(userDN, UserType.USER, null, userRoles, null, System.currentTimeMillis()); - DatawaveUser s1 = new DatawaveUser(server1, UserType.SERVER, null, s1Roles, null, System.currentTimeMillis()); - DatawaveUser s2 = new DatawaveUser(server2, UserType.SERVER, null, s2Roles, null, System.currentTimeMillis()); - - /* - * s2 has OtherRequiredRole is an authorizedRole, but not a directRole so this will fail the check in #DatawavePrincipalLoginModule.login() so chain - * User -> S1 -> S2 will fail - */ - - DatawavePrincipal expected = new DatawavePrincipal(Lists.newArrayList(user, s1, s2)); - - when(securityDomain.getKeyStore()).thenReturn(serverKeystore); - when(securityDomain.getTrustStore()).thenReturn(truststore); - when(datawaveUserService.lookup(datawaveCredential.getEntities())).thenReturn(expected.getProxiedUsers()); - - assertThrows(FailedLoginException.class, datawaveLoginModule::login); - } - - @Test - public void testDirectRolesSuccessServer() throws Exception { - /* - * Chain is User -> S1 -> S2. S2 is terminal server. Verified that s2 does have the appropriate authorized role for terminal server. This will pass the - * check in #DatawavePrincipalLoginModule.login(). This will allow the chain from accessing any endpoint which requires authorized roles - */ - - String issuerDN = DnUtils.normalizeDN(testServerCert.getIssuerDN().getName()); - String serverDN = DnUtils.normalizeDN("CN=testServer.example.com, OU=iamnotaperson, OU=acme"); - SubjectIssuerDNPair server1 = SubjectIssuerDNPair.of(serverDN, issuerDN); - String otherServerDN = DnUtils.normalizeDN("CN=otherServer.example.com, OU=iamnotaperson, OU=acme"); - SubjectIssuerDNPair server2 = SubjectIssuerDNPair.of(otherServerDN, issuerDN); - String proxiedSubjects = "<" + userDN.subjectDN() + "><" + otherServerDN + ">"; - String proxiedIssuers = "<" + userDN.issuerDN() + "><" + issuerDN + ">"; - DatawaveCredential datawaveCredential = new DatawaveCredential(testServerCert, proxiedSubjects, proxiedIssuers); - callbackHandler.name = datawaveCredential.getUserName(); - callbackHandler.credential = datawaveCredential; - - List userRoles = Arrays.asList("Role1", "AuthorizedUser"); - List s1Roles = Arrays.asList("Role3", "OtherRequiredRole"); - List s2Roles = Arrays.asList("Role2", "AuthorizedServer"); - - DatawaveUser user = new DatawaveUser(userDN, UserType.USER, null, userRoles, null, System.currentTimeMillis()); - DatawaveUser s1 = new DatawaveUser(server1, UserType.SERVER, null, s1Roles, null, System.currentTimeMillis()); - DatawaveUser s2 = new DatawaveUser(server2, UserType.SERVER, null, s2Roles, null, System.currentTimeMillis()); - - /* - * s2 has AuthorizedServer role which is a directRole s1 has OtherRequiredRole which is not a directRole. This is the chain we want to make sure passes. - * so this will pass the check in #DatawavePrincipalLoginModule.login() so chain User -> S1 -> S2 will pass - */ - - DatawavePrincipal expected = new DatawavePrincipal(Lists.newArrayList(user, s1, s2)); - - when(securityDomain.getKeyStore()).thenReturn(serverKeystore); - when(securityDomain.getTrustStore()).thenReturn(truststore); - when(datawaveUserService.lookup(datawaveCredential.getEntities())).thenReturn(expected.getProxiedUsers()); - - boolean result = datawaveLoginModule.login(); - assertTrue(result, "Login did not succeed."); - } - - @Test - public void testDirectRolesSuccessUser() throws Exception { - /* - * Chain is just User. This will not get hit by the terminal server check as it only runs on UserType.SERVER - */ - String issuerDN = DnUtils.normalizeDN(testServerCert.getIssuerDN().getName()); - String otherServerDN = DnUtils.normalizeDN("CN=otherServer.example.com, OU=iamnotaperson, OU=acme"); - String proxiedSubjects = "<" + userDN.subjectDN() + "><" + otherServerDN + ">"; - String proxiedIssuers = "<" + userDN.issuerDN() + "><" + issuerDN + ">"; - DatawaveCredential datawaveCredential = new DatawaveCredential(testServerCert, proxiedSubjects, proxiedIssuers); - callbackHandler.name = datawaveCredential.getUserName(); - callbackHandler.credential = datawaveCredential; - - List userRoles = Arrays.asList("Role1", "AuthorizedUser"); - - DatawaveUser user = new DatawaveUser(userDN, UserType.USER, null, userRoles, null, System.currentTimeMillis()); - - DatawavePrincipal expected = new DatawavePrincipal(Lists.newArrayList(user)); - - when(securityDomain.getKeyStore()).thenReturn(serverKeystore); - when(securityDomain.getTrustStore()).thenReturn(truststore); - when(datawaveUserService.lookup(datawaveCredential.getEntities())).thenReturn(expected.getProxiedUsers()); - - boolean result = datawaveLoginModule.login(); - assertTrue(result, "Login did not succeed."); - } - - @Test - public void testGetRoleSetsFiltersRequiredRoles() throws Exception { - // Proxied entities has the original user DN, plus it came through a server and - // the request is being made by a second server. Make sure that the resulting - // principal has all 3 server DNs in its list, and the user DN is not one of the - // server DNs. - String issuerDN = DnUtils.normalizeDN(testServerCert.getIssuerDN().getName()); - String serverDN = DnUtils.normalizeDN("CN=testServer.example.com, OU=iamnotaperson, OU=acme"); - SubjectIssuerDNPair server1 = SubjectIssuerDNPair.of(serverDN, issuerDN); - String otherServerDN = DnUtils.normalizeDN("CN=otherServer.example.com, OU=iamnotaperson, OU=acme"); - SubjectIssuerDNPair server2 = SubjectIssuerDNPair.of(otherServerDN, issuerDN); - String proxiedSubjects = "<" + userDN.subjectDN() + "><" + otherServerDN + ">"; - String proxiedIssuers = "<" + userDN.issuerDN() + "><" + issuerDN + ">"; - DatawaveCredential datawaveCredential = new DatawaveCredential(testServerCert, proxiedSubjects, proxiedIssuers); - callbackHandler.name = datawaveCredential.getUserName(); - callbackHandler.credential = datawaveCredential; - - List userRoles = Arrays.asList("Role1", "AuthorizedUser"); - List s1Roles = Arrays.asList("Role3", "AuthorizedServer"); - List s2Roles = Arrays.asList("Role2"); - - DatawaveUser user = new DatawaveUser(userDN, UserType.USER, null, userRoles, null, System.currentTimeMillis()); - DatawaveUser s1 = new DatawaveUser(server1, UserType.SERVER, null, s1Roles, null, System.currentTimeMillis()); - DatawaveUser s2 = new DatawaveUser(server2, UserType.SERVER, null, s2Roles, null, System.currentTimeMillis()); - /* - * changed order of roles for the servers so this test will pass the directRole check - */ - DatawavePrincipal expected = new DatawavePrincipal(Lists.newArrayList(user, s2, s1)); - - when(securityDomain.getKeyStore()).thenReturn(serverKeystore); - when(securityDomain.getTrustStore()).thenReturn(truststore); - when(datawaveUserService.lookup(datawaveCredential.getEntities())).thenReturn(expected.getProxiedUsers()); - - boolean result = datawaveLoginModule.login(); - assertTrue(result, "Login did not succeed."); - assertEquals(userDN, expected.getUserDN()); - - Group[] roleSets = datawaveLoginModule.getRoleSets(); - assertEquals(2, roleSets.length); - assertEquals("Roles", roleSets[0].getName()); - List groupSetRoles = Collections.list(roleSets[0].members()).stream().map(Principal::getName).collect(Collectors.toList()); - assertEquals(Lists.newArrayList("Role1"), groupSetRoles); - } - - @Test - public void testDisAllowlistedUser() throws Exception { - DatawaveCredential datawaveCredential = new DatawaveCredential(testUserCert, null, null); - callbackHandler.name = datawaveCredential.getUserName(); - callbackHandler.credential = datawaveCredential; - - List roles = Collections.singletonList(DISALLOWLIST_ROLE); - DatawaveUser user = new DatawaveUser(userDN, UserType.USER, null, roles, null, System.currentTimeMillis()); - DatawavePrincipal expected = new DatawavePrincipal(Lists.newArrayList(user)); - - when(securityDomain.getKeyStore()).thenReturn(keystore); - when(securityDomain.getTrustStore()).thenReturn(truststore); - when(datawaveUserService.lookup(datawaveCredential.getEntities())).thenReturn(expected.getProxiedUsers()); - - assertThrows(AccountLockedException.class, datawaveLoginModule::login); - } - - @Test - public void testDisAllowlistedProxiedUser() throws Exception { - // Proxied entities has the original user DN, plus it came through a server and - // the request is being made by a second server. Make sure that the resulting - // principal has all 3 server DNs in its list, and the user DN is not one of the - // server DNs. - String issuerDN = DnUtils.normalizeDN(testServerCert.getIssuerDN().getName()); - String serverDN = DnUtils.normalizeDN("CN=testServer.example.com, OU=iamnotaperson, OU=acme"); - SubjectIssuerDNPair server1 = SubjectIssuerDNPair.of(serverDN, issuerDN); - String otherServerDN = DnUtils.normalizeDN("CN=otherServer.example.com, OU=iamnotaperson, OU=acme"); - SubjectIssuerDNPair server2 = SubjectIssuerDNPair.of(otherServerDN, issuerDN); - String proxiedSubjects = "<" + userDN.subjectDN() + "><" + otherServerDN + ">"; - String proxiedIssuers = "<" + userDN.issuerDN() + "><" + issuerDN + ">"; - DatawaveCredential datawaveCredential = new DatawaveCredential(testServerCert, proxiedSubjects, proxiedIssuers); - callbackHandler.name = datawaveCredential.getUserName(); - callbackHandler.credential = datawaveCredential; - - List disallowlistRoles = Arrays.asList(DISALLOWLIST_ROLE, "TEST_ROLE"); - List otherRoles = Collections.singletonList("TEST_ROLE"); - - DatawaveUser user = new DatawaveUser(userDN, UserType.USER, null, otherRoles, null, System.currentTimeMillis()); - DatawaveUser s1 = new DatawaveUser(server1, UserType.SERVER, null, otherRoles, null, System.currentTimeMillis()); - DatawaveUser s2 = new DatawaveUser(server2, UserType.SERVER, null, disallowlistRoles, null, System.currentTimeMillis()); - - DatawavePrincipal expected = new DatawavePrincipal(Lists.newArrayList(user, s2, s1)); - - when(securityDomain.getKeyStore()).thenReturn(serverKeystore); - when(securityDomain.getTrustStore()).thenReturn(truststore); - when(datawaveUserService.lookup(datawaveCredential.getEntities())).thenReturn(expected.getProxiedUsers()); - - assertThrows(AccountLockedException.class, datawaveLoginModule::login); - } - - @Test - public void testAuthorizationExceptionOnLookup() throws Exception { - // Ensure that an AuthorizationException from the DatawaveUserService results - // in a LoginException being thrown from DatawavePrincipalLOginModule.login() - String issuerDN = DnUtils.normalizeDN(testServerCert.getIssuerDN().getName()); - String otherServerDN = DnUtils.normalizeDN("CN=otherServer.example.com, OU=iamnotaperson, OU=acme"); - String proxiedSubjects = "<" + userDN.subjectDN() + "><" + otherServerDN + ">"; - String proxiedIssuers = "<" + userDN.issuerDN() + "><" + issuerDN + ">"; - DatawaveCredential datawaveCredential = new DatawaveCredential(testServerCert, proxiedSubjects, proxiedIssuers); - callbackHandler.name = datawaveCredential.getUserName(); - callbackHandler.credential = datawaveCredential; - - when(securityDomain.getKeyStore()).thenReturn(serverKeystore); - when(securityDomain.getTrustStore()).thenReturn(truststore); - when(datawaveUserService.lookup(datawaveCredential.getEntities())).thenThrow(new AuthorizationException()); - - // there are many subclasses of LoginException - // but JUnit5's assertThrowsExactly will fail the exception is a subclass - assertThrowsExactly(LoginException.class, datawaveLoginModule::login); - } - - @Test - public void testProxiedEntitiesLogin() throws Exception { - // Proxied entities has the original user DN, plus it came through a server and - // the request is being made by a second server. Make sure that the resulting - // principal has all 3 server DNs in its list, and the user DN is not one of the - // server DNs. - String issuerDN = DnUtils.normalizeDN(testServerCert.getIssuerDN().getName()); - String serverDN = DnUtils.normalizeDN("CN=testServer.example.com, OU=iamnotaperson, OU=acme"); - SubjectIssuerDNPair server1 = SubjectIssuerDNPair.of(serverDN, issuerDN); - String otherServerDN = DnUtils.normalizeDN("CN=otherServer.example.com, OU=iamnotaperson, OU=acme"); - SubjectIssuerDNPair server2 = SubjectIssuerDNPair.of(otherServerDN, issuerDN); - String proxiedSubjects = "<" + userDN.subjectDN() + "><" + otherServerDN + ">"; - String proxiedIssuers = "<" + userDN.issuerDN() + "><" + issuerDN + ">"; - DatawaveCredential datawaveCredential = new DatawaveCredential(testServerCert, proxiedSubjects, proxiedIssuers); - callbackHandler.name = datawaveCredential.getUserName(); - callbackHandler.credential = datawaveCredential; - - /* - * Need to add an directRole for s1 to allow the login check to pass - */ - List s1Roles = List.of("AuthorizedServer"); - - DatawaveUser s1 = new DatawaveUser(server1, UserType.SERVER, null, s1Roles, null, System.currentTimeMillis()); - DatawaveUser s2 = new DatawaveUser(server2, UserType.SERVER, null, null, null, System.currentTimeMillis()); - DatawavePrincipal expected = new DatawavePrincipal(Lists.newArrayList(defaultPrincipal.getPrimaryUser(), s2, s1)); - - when(securityDomain.getKeyStore()).thenReturn(serverKeystore); - when(securityDomain.getTrustStore()).thenReturn(truststore); - when(datawaveUserService.lookup(datawaveCredential.getEntities())).thenReturn(expected.getProxiedUsers()); - - boolean result = datawaveLoginModule.login(); - assertTrue(result, "Login did not succeed."); - assertEquals(userDN, expected.getUserDN()); - } - - @Test - public void testJWTLogin() throws Exception { - ReflectionTestUtils.setField(datawaveLoginModule, "jwtHeaderLogin", true); - - JWTTokenHandler tokenHandler = (JWTTokenHandler) ReflectionTestUtils.getField(datawaveLoginModule, "jwtTokenHandler"); - - // Proxied entities has the original user DN, plus it came through a server and - // the request is being made by a second server. Make sure that the resulting - // principal has all 3 server DNs in its list, and the user DN is not one of the - // server DNs. - String issuerDN = DnUtils.normalizeDN(testServerCert.getIssuerDN().getName()); - String serverDN = DnUtils.normalizeDN("CN=testServer.example.com, OU=iamnotaperson, OU=acme"); - SubjectIssuerDNPair server1 = SubjectIssuerDNPair.of(serverDN, issuerDN); - String otherServerDN = DnUtils.normalizeDN("CN=otherServer.example.com, OU=iamnotaperson, OU=acme"); - SubjectIssuerDNPair server2 = SubjectIssuerDNPair.of(otherServerDN, issuerDN); - - DatawaveUser s1 = new DatawaveUser(server1, UserType.SERVER, null, null, null, System.currentTimeMillis()); - DatawaveUser s2 = new DatawaveUser(server2, UserType.SERVER, null, null, null, System.currentTimeMillis()); - DatawavePrincipal expected = new DatawavePrincipal(Lists.newArrayList(s1, s2, defaultPrincipal.getPrimaryUser())); - - String token = tokenHandler.createTokenFromUsers(expected.getName(), expected.getProxiedUsers()); - DatawaveCredential datawaveCredential = new DatawaveCredential(token); - callbackHandler.name = datawaveCredential.getUserName(); - callbackHandler.credential = datawaveCredential; - - boolean result = datawaveLoginModule.login(); - assertTrue(result, "Login did not succeed."); - assertEquals(userDN, expected.getUserDN()); - } - - @Test - public void testInvalidLoginCertIssuerDenied() { - MockDatawaveCertVerifier.issuerSupported = false; - DatawaveCredential datawaveCredential = new DatawaveCredential(testUserCert, null, null); - callbackHandler.name = datawaveCredential.getUserName(); - callbackHandler.credential = datawaveCredential; - - when(securityDomain.getKeyStore()).thenReturn(keystore); - when(securityDomain.getTrustStore()).thenReturn(truststore); - - assertThrows(CredentialException.class, datawaveLoginModule::login); - } - - @Test - public void testInvalidLoginCertVerificationFailed() { - MockDatawaveCertVerifier.verify = false; - DatawaveCredential datawaveCredential = new DatawaveCredential(testUserCert, null, null); - callbackHandler.name = datawaveCredential.getUserName(); - callbackHandler.credential = datawaveCredential; - - when(securityDomain.getKeyStore()).thenReturn(keystore); - when(securityDomain.getTrustStore()).thenReturn(truststore); - - assertThrows(CredentialException.class, datawaveLoginModule::login); - } - - @Test - public void testInvalidLoginAuthorizationLookupFailed() throws Exception { - DatawaveCredential datawaveCredential = new DatawaveCredential(testUserCert, null, null); - callbackHandler.name = datawaveCredential.getUserName(); - callbackHandler.credential = datawaveCredential; - - when(securityDomain.getKeyStore()).thenReturn(keystore); - when(securityDomain.getTrustStore()).thenReturn(truststore); - when(datawaveUserService.lookup(datawaveCredential.getEntities())).thenThrow(new AuthorizationException("Unable to authenticate")); - - // there are many subclasses of LoginException - // but JUnit5's assertThrowsExactly will fail the exception is a subclass - assertThrowsExactly(LoginException.class, datawaveLoginModule::login); - } - - private static class TestDatawavePrincipalLoginModule extends DatawavePrincipalLoginModule { - @Override - protected void performFieldInjection() { - // do nothing - we're using @TestSubject to inject - } - } -} diff --git a/web-services/security/src/test/java/datawave/security/login/DatawaveUsersRolesLoginModuleTest.java b/web-services/security/src/test/java/datawave/security/login/DatawaveUsersRolesLoginModuleTest.java deleted file mode 100644 index fcdaa2cc4eb..00000000000 --- a/web-services/security/src/test/java/datawave/security/login/DatawaveUsersRolesLoginModuleTest.java +++ /dev/null @@ -1,122 +0,0 @@ -package datawave.security.login; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.security.KeyStore; -import java.security.cert.X509Certificate; -import java.util.HashMap; - -import javax.security.auth.Subject; -import javax.security.auth.login.FailedLoginException; - -import org.jboss.security.SimplePrincipal; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.picketbox.plugins.PicketBoxCallbackHandler; -import org.springframework.test.util.ReflectionTestUtils; - -import datawave.security.auth.DatawaveCredential; -import datawave.security.authorization.DatawavePrincipal; - -public class DatawaveUsersRolesLoginModuleTest { - - private static final String NORMALIZED_SUBJECT_DN = "cn=testuser, ou=my department, o=my company, st=some-state, c=us"; - private static final String NORMALIZED_SUBJECT_DN_WITH_ISSUER_DN = "cn=testuser, ou=my department, o=my company, st=some-state, c=us"; - private static final String SUBJECT_DN_WITH_CN_FIRST = "CN=testUser, OU=My Department, O=My Company, ST=Some-State, C=US"; - private static final String SUBJECT_DN_WITH_CN_LAST = "C=US, ST=Some-State, O=My Company, OU=My Department, CN=testUser"; - private static final String ISSUER_DN_WITH_CN_FIRST = "CN=TEST CA, OU=My Department, O=My Company, ST=Some-State, C=US"; - private static final String ISSUER_DN_WITH_CN_LAST = "C=US, ST=Some-State, O=My Company, OU=My Department, CN=TEST CA"; - - private DatawaveUsersRolesLoginModule loginModule; - private PicketBoxCallbackHandler callbackHandler; - - private X509Certificate testUserCert; - - @BeforeEach - public void beforeEach() throws Exception { - callbackHandler = new PicketBoxCallbackHandler(); - - HashMap sharedState = new HashMap<>(); - HashMap options = new HashMap<>(); - options.put("usersProperties", "users.properties"); - options.put("rolesProperties", "roles.properties"); - options.put("principalClass", "datawave.security.authorization.DatawavePrincipal"); - - loginModule = new DatawaveUsersRolesLoginModule(); - loginModule.initialize(new Subject(), callbackHandler, sharedState, options); - - KeyStore truststore = KeyStore.getInstance("PKCS12"); - truststore.load(getClass().getResourceAsStream("/ca.pkcs12"), "secret".toCharArray()); - KeyStore keystore = KeyStore.getInstance("PKCS12"); - keystore.load(getClass().getResourceAsStream("/testUser.pkcs12"), "secret".toCharArray()); - testUserCert = (X509Certificate) keystore.getCertificate("testuser"); - } - - @Test - public void testSuccessfulLogin() throws Exception { - String name = testUserCert.getSubjectDN().getName() + "<" + testUserCert.getIssuerDN().getName() + ">"; - callbackHandler.setSecurityInfo(new SimplePrincipal(name), - new DatawaveCredential(testUserCert.getSubjectDN().getName(), testUserCert.getIssuerDN().getName(), null, null).toString()); - - boolean result = loginModule.login(); - assertTrue(result, "Login didn't succeed for alias in users/roles.properties"); - - DatawavePrincipal principal = (DatawavePrincipal) ReflectionTestUtils.getField(loginModule, "identity"); - assertNotNull(principal); - assertEquals(NORMALIZED_SUBJECT_DN_WITH_ISSUER_DN, principal.getName()); - } - - @Test - public void testReverseDnSuccessfulLogin() throws Exception { - String name = SUBJECT_DN_WITH_CN_LAST + "<" + ISSUER_DN_WITH_CN_LAST + ">"; - callbackHandler.setSecurityInfo(new SimplePrincipal(name), - new DatawaveCredential(SUBJECT_DN_WITH_CN_LAST, ISSUER_DN_WITH_CN_LAST, null, null).toString()); - - boolean result = loginModule.login(); - assertTrue(result, "Login didn't succeed for alias in users/roles.properties"); - - DatawavePrincipal principal = (DatawavePrincipal) ReflectionTestUtils.getField(loginModule, "identity"); - assertNotNull(principal); - assertEquals(NORMALIZED_SUBJECT_DN_WITH_ISSUER_DN, principal.getName()); - } - - @Test - public void testFailedLoginBadPassword() { - callbackHandler.setSecurityInfo(new SimplePrincipal("testUser"), new DatawaveCredential("testUser", "testIssuer", null, null).toString()); - - assertThrows(FailedLoginException.class, () -> loginModule.login(), "Login succeed for alias in users.properties with bad password"); - } - - @Test - public void normalizeDnWithCnLast() { - assertEquals(NORMALIZED_SUBJECT_DN, DatawaveUsersRolesLoginModule.normalizeUsername(SUBJECT_DN_WITH_CN_LAST)); - } - - @Test - public void normalizeDnWithCnFirst() { - assertEquals(NORMALIZED_SUBJECT_DN, DatawaveUsersRolesLoginModule.normalizeUsername(SUBJECT_DN_WITH_CN_FIRST)); - } - - @Test - public void normalizeSubjectIssuerCombinations() { - String username = SUBJECT_DN_WITH_CN_FIRST + "<" + ISSUER_DN_WITH_CN_FIRST + ">"; - assertUsernameNormalization(username); - - username = SUBJECT_DN_WITH_CN_FIRST + "<" + ISSUER_DN_WITH_CN_LAST + ">"; - assertUsernameNormalization(username); - - username = SUBJECT_DN_WITH_CN_LAST + "<" + ISSUER_DN_WITH_CN_FIRST + ">"; - assertUsernameNormalization(username); - - username = SUBJECT_DN_WITH_CN_LAST + "<" + ISSUER_DN_WITH_CN_LAST + ">"; - assertUsernameNormalization(username); - } - - private void assertUsernameNormalization(String input) { - String normalized = DatawaveUsersRolesLoginModule.normalizeUsername(input); - assertEquals(NORMALIZED_SUBJECT_DN_WITH_ISSUER_DN, normalized); - } -} diff --git a/web-services/security/src/test/java/datawave/security/util/MockCallbackHandler.java b/web-services/security/src/test/java/datawave/security/util/MockCallbackHandler.java deleted file mode 100644 index f405949eb41..00000000000 --- a/web-services/security/src/test/java/datawave/security/util/MockCallbackHandler.java +++ /dev/null @@ -1,41 +0,0 @@ -package datawave.security.util; - -import static org.junit.Assert.assertEquals; - -import java.io.IOException; - -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.callback.UnsupportedCallbackException; - -import org.jboss.security.auth.callback.ObjectCallback; - -public class MockCallbackHandler implements CallbackHandler { - public String name; - public Object credential; - - private String nameCallbackPrompt; - private String credentialsCallbackPrompt; - - public MockCallbackHandler(String nameCallbackPrompt, String credentialsCallbackPrompt) { - this.nameCallbackPrompt = nameCallbackPrompt; - this.credentialsCallbackPrompt = credentialsCallbackPrompt; - } - - @Override - public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { - assertEquals(2, callbacks.length); - assertEquals(NameCallback.class, callbacks[0].getClass()); - assertEquals(ObjectCallback.class, callbacks[1].getClass()); - - NameCallback nc = (NameCallback) callbacks[0]; - ObjectCallback oc = (ObjectCallback) callbacks[1]; - - assertEquals(nameCallbackPrompt, nc.getPrompt()); - assertEquals(credentialsCallbackPrompt, oc.getPrompt()); - - nc.setName(name); - oc.setCredential(credential); - } -} diff --git a/web-services/security/src/test/java/datawave/security/util/MockDatawaveCertVerifier.java b/web-services/security/src/test/java/datawave/security/util/MockDatawaveCertVerifier.java deleted file mode 100644 index 40a2f4700b6..00000000000 --- a/web-services/security/src/test/java/datawave/security/util/MockDatawaveCertVerifier.java +++ /dev/null @@ -1,21 +0,0 @@ -package datawave.security.util; - -import java.security.KeyStore; -import java.security.cert.X509Certificate; - -import datawave.security.login.DatawaveCertVerifier; - -public class MockDatawaveCertVerifier extends DatawaveCertVerifier { - public static boolean issuerSupported = true; - public static boolean verify = true; - - @Override - public boolean isIssuerSupported(String issuerSubjectDn, KeyStore trustStore) { - return issuerSupported; - } - - @Override - public boolean verify(X509Certificate cert, String alias, KeyStore keystore, KeyStore truststore) { - return verify; - } -} diff --git a/web-services/security/src/test/java/datawave/util/MockInitialContextFactory.java b/web-services/security/src/test/java/datawave/util/MockInitialContextFactory.java deleted file mode 100644 index c879f35cc5a..00000000000 --- a/web-services/security/src/test/java/datawave/util/MockInitialContextFactory.java +++ /dev/null @@ -1,23 +0,0 @@ -package datawave.util; - -import java.util.Hashtable; - -import javax.naming.Context; -import javax.naming.NamingException; -import javax.naming.spi.InitialContextFactory; - -public class MockInitialContextFactory implements InitialContextFactory { - private static Context mockContext = null; - - public static void setMockContext(Context context) { - mockContext = context; - } - - @Override - public Context getInitialContext(Hashtable environment) throws NamingException { - if (mockContext == null) - throw new IllegalStateException("mock context must be set first"); - - return mockContext; - } -} diff --git a/web-services/security/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/web-services/security/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker deleted file mode 100644 index 1f0955d450f..00000000000 --- a/web-services/security/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +++ /dev/null @@ -1 +0,0 @@ -mock-maker-inline diff --git a/web-services/web-root/src/main/webapp/WEB-INF/web.xml b/web-services/web-root/src/main/webapp/WEB-INF/web.xml index fc0c3cb303d..c4aed40125d 100644 --- a/web-services/web-root/src/main/webapp/WEB-INF/web.xml +++ b/web-services/web-root/src/main/webapp/WEB-INF/web.xml @@ -12,7 +12,7 @@ --> DATAWAVE-AUTH - DATAWAVE Web Services + datawave