diff --git a/pom.xml b/pom.xml index 2f0ae4e9..ac329f11 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.springframework.boot spring-boot-starter-parent - 2.7.17 + 2.7.18 @@ -32,14 +32,13 @@ 17 - 2.7.17 + 2.7.18 italiangrid_storm-webdav italiangrid https://sonarcloud.io - 0.4.6.v20220506 2.7.1.7 2.3 @@ -55,9 +54,9 @@ 1.0.5.1 2.3.3.RELEASE - 6.0.2 5.5.1 1.76 + 3.3.2 org.italiangrid.storm.webdav.WebdavService @@ -348,34 +347,6 @@ metrics-servlets - - org.italiangrid - jetty-utils - ${jetty-utils.version} - - - javax.activation - activation - - - javax.mail - mail - - - org.eclipse.jetty.aggregate - jetty-all - - - ch.qos.logback - logback-core - - - ch.qos.logback - logback-classic - - - - org.bouncycastle bcpkix-jdk18on @@ -388,7 +359,6 @@ ${bouncycastle.version} - ch.qos.logback logback-access @@ -404,6 +374,11 @@ logback-classic + + org.slf4j + log4j-over-slf4j + + org.apache.httpcomponents httpclient @@ -419,6 +394,34 @@ org.eclipse.jetty jetty-rewrite + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-http + + + org.eclipse.jetty + jetty-util + + + org.eclipse.jetty + jetty-alpn-server + + + org.eclipse.jetty.http2 + http2-server + + + org.eclipse.jetty.http2 + http2-common + + + org.eclipse.jetty + jetty-alpn-conscrypt-server + commons-lang @@ -463,6 +466,12 @@ ${milton.version} + + org.italiangrid + voms-api-java + ${voms-api-java.version} + + commons-cli commons-cli diff --git a/src/main/java/org/italiangrid/storm/webdav/config/OAuthProperties.java b/src/main/java/org/italiangrid/storm/webdav/config/OAuthProperties.java index bd833381..aab8d1f6 100644 --- a/src/main/java/org/italiangrid/storm/webdav/config/OAuthProperties.java +++ b/src/main/java/org/italiangrid/storm/webdav/config/OAuthProperties.java @@ -96,6 +96,9 @@ public boolean isEnforceAudienceChecks() { @Min(value = 1, message = "The refresh period must be a positive integer") int refreshPeriodMinutes = 60; + @Min(value = 1, message = "The refresh timeout must be a positive integer") + int refreshTimeoutSeconds = 30; + public List getIssuers() { return issuers; } @@ -112,6 +115,14 @@ public void setRefreshPeriodMinutes(int refreshPeriodMinutes) { this.refreshPeriodMinutes = refreshPeriodMinutes; } + public int getRefreshTimeoutSeconds() { + return refreshTimeoutSeconds; + } + + public void setRefreshTimeoutSeconds(int refreshTimeoutSeconds) { + this.refreshTimeoutSeconds = refreshTimeoutSeconds; + } + public void setEnableOidc(boolean enableOidc) { this.enableOidc = enableOidc; } diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/utils/DefaultOidcConfigurationFetcher.java b/src/main/java/org/italiangrid/storm/webdav/oauth/utils/DefaultOidcConfigurationFetcher.java index 090ee2db..73fa6277 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oauth/utils/DefaultOidcConfigurationFetcher.java +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/utils/DefaultOidcConfigurationFetcher.java @@ -18,34 +18,45 @@ import static java.lang.String.format; import java.net.URI; +import java.time.Duration; +import java.util.Arrays; import java.util.Map; +import org.italiangrid.storm.webdav.config.OAuthProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import com.nimbusds.jose.RemoteKeySourceException; + @Service public class DefaultOidcConfigurationFetcher implements OidcConfigurationFetcher { public static final String WELL_KNOWN_FRAGMENT = "/.well-known/openid-configuration"; public static final String ISSUER_MISMATCH_ERROR_TEMPLATE = "Issuer in metadata '%s' does not match with requested issuer '%s'"; - public static final String NO_JWKS_URI_ERROR_TEMPLATE = + public static final String NO_JWKS_URI_ERROR_TEMPLATE = "No jwks_uri found in metadata for issuer '%s'"; + private static final MediaType APPLICATION_JWK_SET_JSON = + new MediaType("application", "jwk-set+json"); + public static final Logger LOG = LoggerFactory.getLogger(DefaultOidcConfigurationFetcher.class); - final RestTemplateBuilder restBuilder; + final RestTemplate restTemplate; - @Autowired - public DefaultOidcConfigurationFetcher(RestTemplateBuilder restBuilder) { - this.restBuilder = restBuilder; + public DefaultOidcConfigurationFetcher(RestTemplateBuilder restBuilder, + OAuthProperties oAuthProperties) { + final Duration TIMEOUT = Duration.ofSeconds(oAuthProperties.getRefreshTimeoutSeconds()); + this.restTemplate = restBuilder.setConnectTimeout(TIMEOUT).setReadTimeout(TIMEOUT).build(); } private void metadataChecks(String issuer, Map oidcConfiguration) { @@ -59,29 +70,27 @@ private void metadataChecks(String issuer, Map oidcConfiguration throw new OidcConfigurationResolutionError( format(ISSUER_MISMATCH_ERROR_TEMPLATE, metadataIssuer, issuer)); } - + if (!oidcConfiguration.containsKey("jwks_uri")) { - throw new OidcConfigurationResolutionError(format(NO_JWKS_URI_ERROR_TEMPLATE,issuer)); + throw new OidcConfigurationResolutionError(format(NO_JWKS_URI_ERROR_TEMPLATE, issuer)); } } @Override public Map loadConfigurationForIssuer(String issuer) { LOG.debug("Fetching OpenID configuration for {}", issuer); - + ParameterizedTypeReference> typeReference = new ParameterizedTypeReference>() {}; - RestTemplate rest = restBuilder.build(); - URI uri = UriComponentsBuilder.fromUriString(issuer + WELL_KNOWN_FRAGMENT).build().toUri(); try { RequestEntity request = RequestEntity.get(uri).build(); - Map conf = rest.exchange(request, typeReference).getBody(); + Map conf = restTemplate.exchange(request, typeReference).getBody(); metadataChecks(issuer, conf); - return conf; + return conf; } catch (RuntimeException e) { final String errorMsg = format("Unable to resolve OpenID configuration for issuer '%s' from '%s': %s", issuer, @@ -95,4 +104,30 @@ public Map loadConfigurationForIssuer(String issuer) { } } + @Override + public String loadJWKSourceForURL(URI uri) throws RemoteKeySourceException { + + LOG.debug("Fetching JWK from {}", uri); + + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON)); + ResponseEntity response = null; + try { + RequestEntity request = RequestEntity.get(uri).headers(headers).build(); + response = restTemplate.exchange(request, String.class); + if (response.getStatusCodeValue() != 200) { + throw new RuntimeException( + format("Received status code: %s", response.getStatusCodeValue())); + } + } catch (RuntimeException e) { + final String errorMsg = format("Unable to get JWK from '%s'", uri); + if (LOG.isDebugEnabled()) { + LOG.error("{}: {}", errorMsg, e.getMessage()); + } + throw new RemoteKeySourceException(errorMsg, e); + } + + return response.getBody(); + } + } diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/utils/NoExpirationStringCache.java b/src/main/java/org/italiangrid/storm/webdav/oauth/utils/NoExpirationStringCache.java new file mode 100644 index 00000000..29def745 --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/utils/NoExpirationStringCache.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.oauth.utils; + +import java.util.concurrent.Callable; + +import org.springframework.cache.support.AbstractValueAdaptingCache; +import org.springframework.lang.Nullable; + +public class NoExpirationStringCache extends AbstractValueAdaptingCache { + + private static final String NAME = "NoExpirationCache"; + private final String value; + + public NoExpirationStringCache(String value) { + super(false); + this.value = value; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public Object getNativeCache() { + return this; + } + + @Override + @Nullable + protected Object lookup(Object key) { + return value; + } + + @Override + public void put(Object key, Object value) { + return; + } + + @Override + public void evict(Object key) { + return; + } + + @Override + public void clear() { + return; + } + + @SuppressWarnings("unchecked") + @Override + public T get(Object key, Callable valueLoader) { + return (T) fromStoreValue(value); + } +} diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/utils/OidcConfigurationFetcher.java b/src/main/java/org/italiangrid/storm/webdav/oauth/utils/OidcConfigurationFetcher.java index 786e9f1b..f1632e65 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oauth/utils/OidcConfigurationFetcher.java +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/utils/OidcConfigurationFetcher.java @@ -15,10 +15,15 @@ */ package org.italiangrid.storm.webdav.oauth.utils; +import java.net.URI; import java.util.Map; +import com.nimbusds.jose.RemoteKeySourceException; + public interface OidcConfigurationFetcher { Map loadConfigurationForIssuer(String issuer); + String loadJWKSourceForURL(URI uri) throws RemoteKeySourceException; + } diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/utils/TrustedJwtDecoderCacheLoader.java b/src/main/java/org/italiangrid/storm/webdav/oauth/utils/TrustedJwtDecoderCacheLoader.java index efde8ee9..68d18d10 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oauth/utils/TrustedJwtDecoderCacheLoader.java +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/utils/TrustedJwtDecoderCacheLoader.java @@ -15,6 +15,7 @@ */ package org.italiangrid.storm.webdav.oauth.utils; +import java.net.URI; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; @@ -28,8 +29,8 @@ import org.italiangrid.storm.webdav.oauth.validator.WlcgProfileValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cache.Cache; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.jwt.Jwt; @@ -53,7 +54,6 @@ public class TrustedJwtDecoderCacheLoader extends CacheLoader oidcConfiguration = fetcher.loadConfigurationForIssuer(issuer); + URI jwksUri = URI.create(oidcConfiguration.get("jwks_uri").toString()); + Cache noExpirationCache = + new NoExpirationStringCache(fetcher.loadJWKSourceForURL(jwksUri).toString()); NimbusJwtDecoder decoder = - NimbusJwtDecoder.withJwkSetUri((oidcConfiguration.get("jwks_uri").toString())).build(); + NimbusJwtDecoder.withJwkSetUri((oidcConfiguration.get("jwks_uri").toString())) + .cache(noExpirationCache) + .build(); OAuth2TokenValidator jwtValidator = JwtValidators.createDefaultWithIssuer(issuer); OAuth2TokenValidator wlcgProfileValidator = new WlcgProfileValidator(); diff --git a/src/main/java/org/italiangrid/storm/webdav/server/DefaultJettyServerCustomizer.java b/src/main/java/org/italiangrid/storm/webdav/server/DefaultJettyServerCustomizer.java index 4e798c4e..8bbd9b1a 100644 --- a/src/main/java/org/italiangrid/storm/webdav/server/DefaultJettyServerCustomizer.java +++ b/src/main/java/org/italiangrid/storm/webdav/server/DefaultJettyServerCustomizer.java @@ -32,7 +32,7 @@ import org.italiangrid.storm.webdav.config.ServiceConfiguration; import org.italiangrid.storm.webdav.config.ServiceConfigurationProperties; import org.italiangrid.storm.webdav.config.StorageAreaConfiguration; -import org.italiangrid.utils.jetty.TLSServerConnectorBuilder; +import org.italiangrid.storm.webdav.server.util.TLSServerConnectorBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.web.ServerProperties; diff --git a/src/main/java/org/italiangrid/storm/webdav/server/DefaultWebServerFactory.java b/src/main/java/org/italiangrid/storm/webdav/server/DefaultWebServerFactory.java index b2fb8dfb..b5535a66 100644 --- a/src/main/java/org/italiangrid/storm/webdav/server/DefaultWebServerFactory.java +++ b/src/main/java/org/italiangrid/storm/webdav/server/DefaultWebServerFactory.java @@ -16,7 +16,7 @@ package org.italiangrid.storm.webdav.server; import org.italiangrid.storm.webdav.config.ServiceConfiguration; -import org.italiangrid.utils.jetty.ThreadPoolBuilder; +import org.italiangrid.storm.webdav.server.util.ThreadPoolBuilder; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; diff --git a/src/main/java/org/italiangrid/storm/webdav/server/util/TLSConnectorBuilderError.java b/src/main/java/org/italiangrid/storm/webdav/server/util/TLSConnectorBuilderError.java new file mode 100644 index 00000000..ef68feba --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/server/util/TLSConnectorBuilderError.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.server.util; + +public class TLSConnectorBuilderError extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public TLSConnectorBuilderError(Throwable cause) { + super(cause); + } + + public TLSConnectorBuilderError(String message, Throwable cause) { + super(message, cause); + } + + public TLSConnectorBuilderError(String message) { + super(message); + } + +} diff --git a/src/main/java/org/italiangrid/storm/webdav/server/util/TLSServerConnectorBuilder.java b/src/main/java/org/italiangrid/storm/webdav/server/util/TLSServerConnectorBuilder.java new file mode 100644 index 00000000..d5fae8a5 --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/server/util/TLSServerConnectorBuilder.java @@ -0,0 +1,662 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.server.util; + +import static java.util.Objects.isNull; + +import java.io.File; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; +import java.security.cert.CertificateException; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.conscrypt.OpenSSLProvider; +import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http2.HTTP2Cipher; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.jetty9.InstrumentedConnectionFactory; + +import eu.emi.security.authn.x509.X509CertChainValidatorExt; +import eu.emi.security.authn.x509.helpers.ssl.SSLTrustManager; +import eu.emi.security.authn.x509.impl.PEMCredential; + +/** + * A builder that configures a Jetty server TLS connector integrated with CANL + * {@link X509CertChainValidatorExt} certificate validation services. + * + */ +public class TLSServerConnectorBuilder { + + /** + * Conscrypt provider name. + */ + public static final String CONSCRYPT_PROVIDER = "Conscrypt"; + + /** + * Default service certificate file. + */ + public static final String DEFAULT_CERTIFICATE_FILE = "/etc/grid-security/hostcert.pem"; + + /** + * Default service certificate private key file. + */ + public static final String DEFAULT_CERTIFICATE_KEY_FILE = "/etc/grid-security/hostcert.pem"; + + /** + * The port for this connector. + */ + private int port; + + /** + * The certificate file location. + */ + private String certificateFile = DEFAULT_CERTIFICATE_FILE; + + /** + * The certificate private key file location. + */ + private String certificateKeyFile = DEFAULT_CERTIFICATE_KEY_FILE; + + /** + * The password to decrypt the certificate private key file. + */ + private char[] certicateKeyPassword = null; + + /** + * The certificate validator used by this connector builder. + */ + private final X509CertChainValidatorExt certificateValidator; + + /** + * Whether client auth will be required for this connector. + */ + private boolean tlsNeedClientAuth = false; + + /** + * Whether cluent auth is supported for this connector. + */ + private boolean tlsWantClientAuth = true; + + /** + * Supported SSL protocols. + */ + private String[] includeProtocols; + + /** + * Disabled SSL protocols. + */ + private String[] excludeProtocols; + + /** + * Supported cipher suites. + */ + private String[] includeCipherSuites; + + /** + * Disabled cipher suites. + */ + private String[] excludeCipherSuites; + + /** + * The HTTP configuration for the connector being created. + */ + private HttpConfiguration httpConfiguration; + + /** + * The key manager to use for the connector being created. + */ + private KeyManager keyManager; + + /** + * The server for which the connector is being created. + */ + private final Server server; + + /** + * The metric name to associate to the connector being built. + */ + private String metricName; + + /** + * The metric registry. + */ + private MetricRegistry registry; + + /** + * Whether the Conscrypt provider should be used instead of the default JSSE implementation + */ + private boolean useConscrypt = false; + + + /** + * Whether HTTP/2 should be configured + */ + private boolean enableHttp2 = false; + + /** + * Which TLS protocol string should be used + */ + private String tlsProtocol = "TLSv1.2"; + + /** + * Custom TLS hostname verifier + */ + private HostnameVerifier hostnameVerifier = null; + + /** + * Disable JSSE hostname verification + */ + private boolean disableJsseHostnameVerification = false; + + /** + * Number of acceptors threads for the connector + */ + private int acceptors = -1; + + /** + * Number of selector threads for the connector + */ + private int selectors = -1; + + /** + * Returns an instance of the {@link TLSServerConnectorBuilder}. + * + * @param s the {@link Server} for which the connector is being created + * @param certificateValidator a {@link X509CertChainValidatorExt} used to validate certificates + * @return an instance of the {@link TLSServerConnectorBuilder} + */ + public static TLSServerConnectorBuilder instance(Server s, + X509CertChainValidatorExt certificateValidator) { + + return new TLSServerConnectorBuilder(s, certificateValidator); + } + + /** + * Private ctor. + * + * @param s the {@link Server} for which the connector is being created + * @param certificateValidator a {@link X509CertChainValidatorExt} used to validate certificates + */ + private TLSServerConnectorBuilder(Server s, X509CertChainValidatorExt certificateValidator) { + + if (s == null) { + throw new IllegalArgumentException("Server cannot be null"); + } + + if (certificateValidator == null) { + throw new IllegalArgumentException("certificateValidator cannot be null"); + } + + this.server = s; + this.certificateValidator = certificateValidator; + } + + private void credentialsSanityChecks() { + + checkFileExistsAndIsReadable(new File(certificateFile), "Error accessing certificate file"); + + checkFileExistsAndIsReadable(new File(certificateKeyFile), + "Error accessing certificate key file"); + + } + + private void loadCredentials() { + + credentialsSanityChecks(); + + PEMCredential serviceCredentials = null; + + try { + + serviceCredentials = + new PEMCredential(certificateKeyFile, certificateFile, certicateKeyPassword); + + } catch (KeyStoreException | CertificateException | IOException e) { + + throw new TLSConnectorBuilderError("Error setting up service credentials", e); + } + + keyManager = serviceCredentials.getKeyManager(); + } + + /** + * Configures SSL session parameters for the jetty {@link SslContextFactory}. + * + * @param contextFactory the {@link SslContextFactory} being configured + */ + private void configureContextFactory(SslContextFactory.Server contextFactory) { + + if (excludeProtocols != null) { + contextFactory.setExcludeProtocols(excludeProtocols); + } + + if (includeProtocols != null) { + contextFactory.setIncludeProtocols(includeProtocols); + } + + if (excludeCipherSuites != null) { + contextFactory.setExcludeCipherSuites(excludeCipherSuites); + } + + if (includeCipherSuites != null) { + contextFactory.setIncludeCipherSuites(includeCipherSuites); + } + + contextFactory.setWantClientAuth(tlsWantClientAuth); + contextFactory.setNeedClientAuth(tlsNeedClientAuth); + + if (useConscrypt) { + contextFactory.setProvider(CONSCRYPT_PROVIDER); + } else { + contextFactory.setProvider(BouncyCastleProvider.PROVIDER_NAME); + } + + if (hostnameVerifier != null) { + contextFactory.setHostnameVerifier(hostnameVerifier); + } + + if (disableJsseHostnameVerification) { + contextFactory.setEndpointIdentificationAlgorithm(null); + } + + } + + /** + * Builds a default {@link HttpConfiguration} for the TLS-enabled connector being created + * + * @return the default {@link HttpConfiguration} + */ + private HttpConfiguration defaultHttpConfiguration() { + + HttpConfiguration httpsConfig = new HttpConfiguration(); + + httpsConfig.setSecureScheme("https"); + + httpsConfig.setSecurePort(port); + + httpsConfig.setOutputBufferSize(32768); + httpsConfig.setRequestHeaderSize(8192); + httpsConfig.setResponseHeaderSize(8192); + + httpsConfig.setSendServerVersion(true); + httpsConfig.setSendDateHeader(false); + + httpsConfig.addCustomizer(new SecureRequestCustomizer()); + + return httpsConfig; + + } + + /** + * Gives access to the {@link HttpConfiguration} used for the TLS-enabled connector being created. + * If the configuration is not set, it creates it using {@link #defaultHttpConfiguration()}. + * + * @return the {@link HttpConfiguration} being used for the TLS-enabled connector. + */ + public HttpConfiguration httpConfiguration() { + + if (httpConfiguration == null) { + httpConfiguration = defaultHttpConfiguration(); + } + + return httpConfiguration; + + } + + /** + * Sets the port for the connector being created. + * + * @param port the port for the connector + * @return this builder + */ + public TLSServerConnectorBuilder withPort(int port) { + + this.port = port; + return this; + } + + /** + * Sets the certificate file for the connector being created. + * + * @param certificateFile the certificate file + * @return this builder + */ + public TLSServerConnectorBuilder withCertificateFile(String certificateFile) { + + this.certificateFile = certificateFile; + return this; + } + + /** + * Sets the certificate key file for the connector being created. + * + * @param certificateKeyFile the certificate key file + * @return this builder + */ + public TLSServerConnectorBuilder withCertificateKeyFile(String certificateKeyFile) { + + this.certificateKeyFile = certificateKeyFile; + return this; + } + + /** + * The the certificate key password for the connector being built + * + * @param certificateKeyPassword the certificate key password + * @return this builder + */ + public TLSServerConnectorBuilder withCertificateKeyPassword(char[] certificateKeyPassword) { + + this.certicateKeyPassword = certificateKeyPassword; + return this; + } + + /** + * Sets the if client authentication is required or not. + * + * @param needClientAuth true if client authentication is required + * @return this builder + */ + public TLSServerConnectorBuilder withNeedClientAuth(boolean needClientAuth) { + + this.tlsNeedClientAuth = needClientAuth; + return this; + } + + /** + * Sets if client authentication is wanted or not. + * + * @param wantClientAuth true if client authentication is wanted + * @return this builder + */ + public TLSServerConnectorBuilder withWantClientAuth(boolean wantClientAuth) { + + this.tlsWantClientAuth = wantClientAuth; + return this; + } + + /** + * Sets SSL included protocols. See {@link SslContextFactory#setIncludeProtocols(String...)}. + * + * @param includeProtocols the array of included protocol names + * @return this builder + */ + public TLSServerConnectorBuilder withIncludeProtocols(String... includeProtocols) { + + this.includeProtocols = includeProtocols; + return this; + } + + /** + * Sets SSL excluded protocols. See {@link SslContextFactory#setExcludeProtocols(String...)}. + * + * @param excludeProtocols the array of excluded protocol names + * @return this builder + */ + public TLSServerConnectorBuilder withExcludeProtocols(String... excludeProtocols) { + + this.excludeProtocols = excludeProtocols; + return this; + } + + /** + * Sets the SSL included cipher suites. + * + * @param includeCipherSuites the array of included cipher suites. + * @return this builder + */ + public TLSServerConnectorBuilder withIncludeCipherSuites(String... includeCipherSuites) { + + this.includeCipherSuites = includeCipherSuites; + return this; + } + + /** + * Sets the SSL ecluded cipher suites. + * + * @param excludeCipherSuites the array of excluded cipher suites. + * @return this builder + */ + public TLSServerConnectorBuilder withExcludeCipherSuites(String... excludeCipherSuites) { + + this.excludeCipherSuites = excludeCipherSuites; + return this; + } + + /** + * Sets the {@link HttpConfiguration} for the connector being built. + * + * @param conf the {@link HttpConfiguration} to use + * @return this builder + */ + public TLSServerConnectorBuilder withHttpConfiguration(HttpConfiguration conf) { + + this.httpConfiguration = conf; + return this; + } + + /** + * Sets the {@link KeyManager} for the connector being built. + * + * @param km the {@link KeyManager} to use + * @return this builder + */ + public TLSServerConnectorBuilder withKeyManager(KeyManager km) { + + this.keyManager = km; + return this; + } + + public TLSServerConnectorBuilder withConscrypt(boolean conscryptEnabled) { + this.useConscrypt = conscryptEnabled; + return this; + } + + public TLSServerConnectorBuilder withHttp2(boolean http2Enabled) { + this.enableHttp2 = http2Enabled; + return this; + } + + public TLSServerConnectorBuilder metricRegistry(MetricRegistry registry) { + this.registry = registry; + return this; + } + + public TLSServerConnectorBuilder metricName(String metricName) { + this.metricName = metricName; + return this; + } + + public TLSServerConnectorBuilder withTlsProtocol(String tlsProtocol) { + this.tlsProtocol = tlsProtocol; + return this; + } + + public TLSServerConnectorBuilder withHostnameVerifier(HostnameVerifier verifier) { + this.hostnameVerifier = verifier; + return this; + } + + public TLSServerConnectorBuilder withDisableJsseHostnameVerification( + boolean disableJsseHostnameVerification) { + this.disableJsseHostnameVerification = disableJsseHostnameVerification; + return this; + } + + public TLSServerConnectorBuilder withAcceptors(int acceptors) { + this.acceptors = acceptors; + return this; + } + + public TLSServerConnectorBuilder withSelectors(int selectors) { + this.selectors = selectors; + return this; + } + + private SSLContext buildSSLContext() { + + SSLContext sslCtx; + + try { + + KeyManager[] kms = new KeyManager[] {keyManager}; + SSLTrustManager tm = new SSLTrustManager(certificateValidator); + + if (useConscrypt) { + + if (isNull(Security.getProvider(CONSCRYPT_PROVIDER))) { + Security.addProvider(new OpenSSLProvider()); + } + + sslCtx = SSLContext.getInstance(tlsProtocol, CONSCRYPT_PROVIDER); + } else { + sslCtx = SSLContext.getInstance(tlsProtocol); + } + + sslCtx.init(kms, new TrustManager[] {tm}, null); + + } catch (NoSuchAlgorithmException e) { + + throw new TLSConnectorBuilderError("TLS protocol not supported: " + e.getMessage(), e); + } catch (KeyManagementException e) { + throw new TLSConnectorBuilderError(e); + } catch (NoSuchProviderException e) { + throw new TLSConnectorBuilderError("TLS provider error: " + e.getMessage(), e); + } + + return sslCtx; + } + + /** + * Builds a {@link ServerConnector} based on the {@link TLSServerConnectorBuilder} parameters + * + * @return a {@link ServerConnector} + */ + public ServerConnector build() { + + if (keyManager == null) { + loadCredentials(); + } + + SSLContext sslContext = buildSSLContext(); + SslContextFactory.Server cf = new SslContextFactory.Server(); + + cf.setSslContext(sslContext); + + + configureContextFactory(cf); + + if (httpConfiguration == null) { + httpConfiguration = defaultHttpConfiguration(); + } + + + HttpConnectionFactory httpConnFactory = new HttpConnectionFactory(httpConfiguration); + ConnectionFactory connFactory = null; + + if (registry != null) { + connFactory = new InstrumentedConnectionFactory(httpConnFactory, registry.timer(metricName)); + } else { + connFactory = httpConnFactory; + } + + + ConnectionFactory h2ConnFactory = null; + ServerConnector connector = null; + + if (enableHttp2) { + + HTTP2ServerConnectionFactory h2cf = new HTTP2ServerConnectionFactory(httpConfiguration); + + if (registry != null) { + h2ConnFactory = new InstrumentedConnectionFactory(h2cf, registry.timer(metricName)); + } else { + h2ConnFactory = h2cf; + } + ALPNServerConnectionFactory alpn = createAlpnProtocolFactory(httpConnFactory); + cf.setCipherComparator(HTTP2Cipher.COMPARATOR); + cf.setUseCipherSuitesOrder(true); + + SslConnectionFactory sslCf = new SslConnectionFactory(cf, alpn.getProtocol()); + + connector = new ServerConnector(server, acceptors, selectors, sslCf, alpn, h2ConnFactory, + httpConnFactory); + + } else { + + connector = new ServerConnector(server, acceptors, selectors, + new SslConnectionFactory(cf, HttpVersion.HTTP_1_1.asString()), connFactory); + } + + connector.setPort(port); + return connector; + } + + private ALPNServerConnectionFactory createAlpnProtocolFactory( + HttpConnectionFactory httpConnectionFactory) { + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol(httpConnectionFactory.getProtocol()); + return alpn; + } + + + /** + * Checks that file exists and is readable. + * + * @param f the {@link File} to be checked + * @param prefix A prefix string for the error message, in case the file does not exist and is not + * readable + * @throws RuntimeException if the file does not exist or is not readable + */ + private void checkFileExistsAndIsReadable(File f, String prefix) { + + String errorMessage = null; + + if (!f.exists()) { + errorMessage = "File does not exists"; + } else if (!f.canRead()) { + errorMessage = "File is not readable"; + } else if (f.isDirectory()) { + errorMessage = "File is a directory"; + } + + if (errorMessage != null) { + String msg = String.format("%s: %s [%s]", prefix, errorMessage, f.getAbsolutePath()); + throw new TLSConnectorBuilderError(msg); + } + + } +} diff --git a/src/main/java/org/italiangrid/storm/webdav/server/util/ThreadPoolBuilder.java b/src/main/java/org/italiangrid/storm/webdav/server/util/ThreadPoolBuilder.java new file mode 100644 index 00000000..52e13a35 --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/server/util/ThreadPoolBuilder.java @@ -0,0 +1,206 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.server.util; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ThreadPool; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.jetty9.InstrumentedQueuedThreadPool; + +/** + * + * A builder to support thread pool configuration for a Jetty server. + * + */ +public class ThreadPoolBuilder { + + /** + * + */ + public static final int DEFAULT_MAX_REQUEST_QUEUE_SIZE = 200; + + public static final int DEFAULT_MAX_THREADS = 50; + public static final int DEFAULT_MIN_THREADS = 1; + + public static final int DEFAULT_IDLE_TIMEOUT = (int) TimeUnit.MINUTES.toMillis(60); + + public static final String DEFAULT_THREAD_POOL_METRIC_NAME = "thread-pool"; + public static final String DEFAULT_THREAD_POOL_METRIC_PREFIX = "jetty"; + + private int maxThreads = DEFAULT_MAX_THREADS; + private int minThreads = DEFAULT_MIN_THREADS; + + private int idleTimeout = DEFAULT_IDLE_TIMEOUT; + private int maxRequestQueueSize = DEFAULT_MAX_REQUEST_QUEUE_SIZE; + + private String name = DEFAULT_THREAD_POOL_METRIC_NAME; + private String prefix = DEFAULT_THREAD_POOL_METRIC_PREFIX; + + private MetricRegistry registry; + + /** + * Returns a new {@link ThreadPoolBuilder} instance. + * + * @return a {@link ThreadPoolBuilder} + */ + public static ThreadPoolBuilder instance() { + + return new ThreadPoolBuilder(); + } + + /** + * Sets the max number of threads for the thread pool. + * + * @param maxNumberOfThreads the max number of threads + * + * @return this builder + * + */ + public ThreadPoolBuilder withMaxThreads(int maxNumberOfThreads) { + + this.maxThreads = maxNumberOfThreads; + return this; + } + + /** + * Sets the minimum number of threads for the thread pool. + * + * @param minNumberOfThreads the minimum number of threads + * @return this builder + */ + public ThreadPoolBuilder withMinThreads(int minNumberOfThreads) { + + this.minThreads = minNumberOfThreads; + return this; + } + + /** + * Sets the maximum request queue size for this thread pool. + * + * @param queueSize the maximum request queue size. + * + * @return this builder + */ + public ThreadPoolBuilder withMaxRequestQueueSize(int queueSize) { + + this.maxRequestQueueSize = queueSize; + return this; + } + + /** + * Sets the registry for this thread pool + * + * @param registry the metric registry + * @return this builder + */ + public ThreadPoolBuilder registry(MetricRegistry registry) { + this.registry = registry; + return this; + } + + /** + * Sets the idle timeout in msec for this thread pool + * + * @param idleTimeout the timeout in milliseconds + * @return this builder + */ + public ThreadPoolBuilder withIdleTimeoutMsec(int idleTimeout) { + this.idleTimeout = idleTimeout; + return this; + } + + /** + * Sets the name for this thread pool + * + * @param name the thread pool name + * @return this builder + */ + public ThreadPoolBuilder withName(String name) { + this.name = name; + return this; + } + + /** + * Sets the name prefix for this thread pool + * + * @param prefix the thread pool name prefix + * @return this builder + */ + public ThreadPoolBuilder withPrefix(String prefix) { + this.prefix = prefix; + return this; + } + + + /** + * ctor. + * + */ + private ThreadPoolBuilder() { + + } + + /** + * Builds a {@link ThreadPool} based on the parameters of this builder + * + * @return a {@link ThreadPool} + */ + public ThreadPool build() { + + if (maxRequestQueueSize <= 0) { + maxRequestQueueSize = DEFAULT_MAX_REQUEST_QUEUE_SIZE; + } + + if (maxThreads <= 0) { + maxThreads = DEFAULT_MAX_THREADS; + } + + if (minThreads <= 0) { + minThreads = DEFAULT_MIN_THREADS; + } + + if (idleTimeout <= 0) { + idleTimeout = DEFAULT_IDLE_TIMEOUT; + } + + if (prefix.isEmpty()) { + prefix = DEFAULT_THREAD_POOL_METRIC_PREFIX; + } + + BlockingQueue queue = new ArrayBlockingQueue(maxRequestQueueSize); + + QueuedThreadPool tp = null; + + if (registry == null) { + tp = new QueuedThreadPool(maxThreads, minThreads, idleTimeout, queue); + } else { + tp = new InstrumentedQueuedThreadPool(registry, maxThreads, minThreads, idleTimeout, queue, prefix); + } + + if (name.isEmpty()) { + name = DEFAULT_THREAD_POOL_METRIC_NAME; + } + tp.setName(name); + + return tp; + + } +} diff --git a/src/main/java/org/italiangrid/storm/webdav/spring/AppConfig.java b/src/main/java/org/italiangrid/storm/webdav/spring/AppConfig.java index ce9a7a88..f3c56080 100644 --- a/src/main/java/org/italiangrid/storm/webdav/spring/AppConfig.java +++ b/src/main/java/org/italiangrid/storm/webdav/spring/AppConfig.java @@ -16,7 +16,6 @@ package org.italiangrid.storm.webdav.spring; import static java.util.Objects.isNull; -import static org.italiangrid.utils.jetty.TLSServerConnectorBuilder.CONSCRYPT_PROVIDER; import java.io.IOException; import java.net.MalformedURLException; @@ -129,6 +128,7 @@ public class AppConfig implements TransferConstants { public static final Logger LOG = LoggerFactory.getLogger(AppConfig.class); + public static final String CONSCRYPT_PROVIDER = "Conscrypt"; @Bean Clock systemClock() { @@ -307,11 +307,9 @@ CloseableHttpClient transferClient(ThirdPartyCopyProperties props, ClientRegistrationRepository clientRegistrationRepository( OAuth2ClientProperties clientProperties, OAuthProperties props, ExecutorService executor) { - ClientRegistrationCacheLoader loader = new ClientRegistrationCacheLoader(clientProperties, props, executor); - LoadingCache clients = CacheBuilder.newBuilder() .refreshAfterWrite(props.getRefreshPeriodMinutes(), TimeUnit.MINUTES) .build(loader); @@ -346,7 +344,6 @@ ClientRegistrationRepository clientRegistrationRepository( JwtDecoder jwtDecoder(OAuthProperties props, ServiceConfigurationProperties sProps, RestTemplateBuilder builder, OidcConfigurationFetcher fetcher, ExecutorService executor) { - TrustedJwtDecoderCacheLoader loader = new TrustedJwtDecoderCacheLoader(sProps, props, builder, fetcher, executor); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6eaaa236..f6e54eac 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -50,7 +50,8 @@ tpc: enable-expect-continue-threshold: ${STORM_WEBDAV_TPC_ENABLE_EXPECT_CONTINUE_THRESHOLD:1048576} oauth: - refresh-period-minutes: 60 + refresh-period-minutes: ${STORM_WEBDAV_OAUTH_REFRESH_PERIOD_MINUTES:60} + refresh-timeout-seconds: ${STORM_WEBDAV_OAUTH_REFRESH_TIMEOUT_SECONDS:30} issuers: storm: diff --git a/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwk/OidcConfigurationFetcherTests.java b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwk/OidcConfigurationFetcherTests.java new file mode 100644 index 00000000..779e7a52 --- /dev/null +++ b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwk/OidcConfigurationFetcherTests.java @@ -0,0 +1,128 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.test.oauth.jwk; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.text.ParseException; +import java.util.Map; + +import org.apache.commons.io.FileUtils; +import org.italiangrid.storm.webdav.config.OAuthProperties; +import org.italiangrid.storm.webdav.oauth.utils.DefaultOidcConfigurationFetcher; +import org.italiangrid.storm.webdav.oauth.utils.OidcConfigurationFetcher; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import com.google.common.collect.Maps; +import com.nimbusds.jose.RemoteKeySourceException; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.KeyType; + +@ExtendWith(MockitoExtension.class) +public class OidcConfigurationFetcherTests { + + final static String ISSUER = "https://iam-dev.cloud.cnaf.infn.it/"; + final static String JWK_URI = "https://iam-dev.cloud.cnaf.infn.it/jwk"; + + final static String KID = "rsa1"; + + + final ParameterizedTypeReference> typeReference = + new ParameterizedTypeReference>() {}; + + @Mock + RestTemplate restTemplate; + @Mock + RestTemplateBuilder restBuilder; + @Mock + OAuthProperties oAuthProperties; + + @SuppressWarnings("unchecked") + private ResponseEntity> getMockedResponseFromWellKnown() { + + Map wellKnownMap = Maps.newHashMap(); + wellKnownMap.put("issuer", ISSUER); + wellKnownMap.put("jwks_uri", JWK_URI); + ResponseEntity> mockedEntity = (ResponseEntity>) Mockito.mock(ResponseEntity.class); + lenient().when(mockedEntity.getBody()).thenReturn(wellKnownMap); + return mockedEntity; + } + + @SuppressWarnings("unchecked") + private ResponseEntity getMockedResponseFromJWKURI() throws IOException { + + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource("jwk/test-keystore.jwks").getFile()); + String data = FileUtils.readFileToString(file, "UTF-8"); + ResponseEntity mockedEntity = (ResponseEntity) Mockito.mock(ResponseEntity.class); + lenient().when(mockedEntity.getBody()).thenReturn(data); + lenient().when(mockedEntity.getStatusCodeValue()).thenReturn(200); + return mockedEntity; + } + + private OidcConfigurationFetcher getFetcher() throws RestClientException, IOException { + + ResponseEntity> mockedResponseMapEntity = getMockedResponseFromWellKnown(); + lenient().when(restTemplate.exchange(any(), eq(typeReference))).thenReturn(mockedResponseMapEntity); + ResponseEntity mockedResponseStringEntity = getMockedResponseFromJWKURI(); + lenient().when(restTemplate.exchange(any(), eq(String.class))).thenReturn(mockedResponseStringEntity); + + lenient().when(restBuilder.build()).thenReturn(restTemplate); + lenient().when(restBuilder.setConnectTimeout(any())).thenReturn(restBuilder); + lenient().when(restBuilder.setReadTimeout(any())).thenReturn(restBuilder); + lenient().when(oAuthProperties.getRefreshTimeoutSeconds()).thenReturn(30); + lenient().when(oAuthProperties.getRefreshPeriodMinutes()).thenReturn(1); + + return new DefaultOidcConfigurationFetcher(restBuilder, oAuthProperties); + } + + @Test + public void fetchWellKnownEndpointTests() throws RestClientException, IOException { + + OidcConfigurationFetcher fetcher = getFetcher(); + Map conf = fetcher.loadConfigurationForIssuer(ISSUER); + assertNotNull(conf); + assertThat(conf.get("issuer"), is(ISSUER)); + assertThat(conf.get("jwks_uri"), is(JWK_URI)); + } + + @Test + public void fetchJWKEndpointTests() throws RestClientException, IOException, RemoteKeySourceException, ParseException { + + OidcConfigurationFetcher fetcher = getFetcher(); + JWKSet key = JWKSet.parse(fetcher.loadJWKSourceForURL(URI.create(JWK_URI))); + + assertNotNull(key.getKeyByKeyId(KID)); + assertThat(key.getKeyByKeyId(KID).getKeyType(), is(KeyType.RSA)); + } +} diff --git a/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwk/TrustedJwtDecoderCacheLoaderTests.java b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwk/TrustedJwtDecoderCacheLoaderTests.java new file mode 100644 index 00000000..43d46ae5 --- /dev/null +++ b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwk/TrustedJwtDecoderCacheLoaderTests.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.test.oauth.jwk; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.lenient; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.apache.commons.io.FileUtils; +import org.italiangrid.storm.webdav.config.OAuthProperties; +import org.italiangrid.storm.webdav.config.ServiceConfigurationProperties; +import org.italiangrid.storm.webdav.config.ServiceConfigurationProperties.AuthorizationServerProperties; +import org.italiangrid.storm.webdav.config.OAuthProperties.AuthorizationServer; +import org.italiangrid.storm.webdav.oauth.utils.OidcConfigurationFetcher; +import org.italiangrid.storm.webdav.oauth.utils.TrustedJwtDecoderCacheLoader; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.util.concurrent.ListenableFuture; +import com.nimbusds.jose.RemoteKeySourceException; + +@ExtendWith(MockitoExtension.class) +public class TrustedJwtDecoderCacheLoaderTests { + + private final String ISSUER = "https://wlcg.cloud.cnaf.infn.it/"; + private final String JWK_URI = "https://wlcg.cloud.cnaf.infn.it/jwks"; + + @Mock + ServiceConfigurationProperties properties; + @Mock + OAuthProperties oauthProperties; + @Mock + RestTemplateBuilder builder; + @Mock + OidcConfigurationFetcher fetcher; + + private ExecutorService executor; + private TrustedJwtDecoderCacheLoader jwtLoader; + + @BeforeEach + public void setup() throws IOException, RemoteKeySourceException { + + AuthorizationServer as = new AuthorizationServer(); + as.setIssuer(ISSUER); + as.setJwkUri(JWK_URI); + List issuerServers = Lists.newArrayList(as); + lenient().when(oauthProperties.getIssuers()).thenReturn(issuerServers); + + Map oidcConfiguration = Maps.newHashMap(); + oidcConfiguration.put("issuer", ISSUER); + oidcConfiguration.put("jwks_uri", JWK_URI); + + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource("jwk/test-keystore.jwks").getFile()); + String data = FileUtils.readFileToString(file, "UTF-8"); + + lenient().when(fetcher.loadConfigurationForIssuer(ISSUER)).thenReturn(oidcConfiguration); + lenient().when(fetcher.loadJWKSourceForURL(URI.create(JWK_URI))).thenReturn(data); + + AuthorizationServerProperties props = new AuthorizationServerProperties(); + props.setEnabled(false); + props.setIssuer("http://localhost"); + lenient().when(properties.getAuthzServer()).thenReturn(props); + + executor = Executors.newScheduledThreadPool(1); + + jwtLoader = + new TrustedJwtDecoderCacheLoader(properties, oauthProperties, builder, fetcher, executor); + + } + + @Test + public void testLoadRemoteIssuerConfiguration() throws Exception { + + JwtDecoder decoder = jwtLoader.load(ISSUER); + assertTrue(decoder instanceof NimbusJwtDecoder); + ListenableFuture reloaded = jwtLoader.reload(ISSUER, decoder); + JwtDecoder newDecoder = reloaded.get(); + assertTrue(newDecoder instanceof NimbusJwtDecoder); + } +} diff --git a/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwt/NoExpirationStringCacheTests.java b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwt/NoExpirationStringCacheTests.java new file mode 100644 index 00000000..45e3e991 --- /dev/null +++ b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwt/NoExpirationStringCacheTests.java @@ -0,0 +1,30 @@ +package org.italiangrid.storm.webdav.test.oauth.jwt; + +import static org.junit.Assert.assertEquals; + +import org.italiangrid.storm.webdav.oauth.utils.NoExpirationStringCache; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class NoExpirationStringCacheTests { + + final String CACHED_VALUE = "this-is-my-cached-value"; + final String FAKE_ISSUER = "http://localhost"; + + @Test + public void noExpirationCacheWorks() { + + NoExpirationStringCache cache = new NoExpirationStringCache(CACHED_VALUE); + + assertEquals("NoExpirationCache", cache.getName()); + assertEquals(cache, cache.getNativeCache()); + assertEquals(CACHED_VALUE, cache.get(FAKE_ISSUER).get()); + cache.clear(); + cache.put(FAKE_ISSUER, CACHED_VALUE); + assertEquals(CACHED_VALUE, cache.get(FAKE_ISSUER).get()); + cache.evict(FAKE_ISSUER); + assertEquals(CACHED_VALUE, cache.get(FAKE_ISSUER).get()); + } +} diff --git a/src/test/java/org/italiangrid/storm/webdav/test/utils/TLSConnectorBuilderTests.java b/src/test/java/org/italiangrid/storm/webdav/test/utils/TLSConnectorBuilderTests.java new file mode 100644 index 00000000..b7185abf --- /dev/null +++ b/src/test/java/org/italiangrid/storm/webdav/test/utils/TLSConnectorBuilderTests.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.test.utils; + +import static org.junit.Assert.assertThrows; + +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.KeyManager; + +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.Server; +import org.italiangrid.storm.webdav.server.util.CANLListener; +import org.italiangrid.storm.webdav.server.util.TLSConnectorBuilderError; +import org.italiangrid.storm.webdav.server.util.TLSServerConnectorBuilder; +import org.italiangrid.voms.util.CertificateValidatorBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import eu.emi.security.authn.x509.CrlCheckingMode; +import eu.emi.security.authn.x509.NamespaceCheckingMode; +import eu.emi.security.authn.x509.OCSPCheckingMode; +import eu.emi.security.authn.x509.X509CertChainValidatorExt; + +@ExtendWith(MockitoExtension.class) +public class TLSConnectorBuilderTests { + + @Test + public void tlsConnectorBuilderErrorTests() { + + new TLSConnectorBuilderError("This is an error!"); + new TLSConnectorBuilderError("This is an error!", new RuntimeException()); + new TLSConnectorBuilderError(new RuntimeException()); + } + + @Test + public void illegalArgumentExceptionThrown() { + + Server server = Mockito.mock(Server.class); + X509CertChainValidatorExt validator = Mockito.mock(X509CertChainValidatorExt.class); + + assertThrows(IllegalArgumentException.class, () -> { + TLSServerConnectorBuilder.instance(null, validator); + }); + assertThrows(IllegalArgumentException.class, () -> { + TLSServerConnectorBuilder.instance(server, null); + }); + assertThrows(IllegalArgumentException.class, () -> { + TLSServerConnectorBuilder.instance(null, null); + }); + } + + private X509CertChainValidatorExt getValidator() { + + CANLListener l = new org.italiangrid.storm.webdav.server.util.CANLListener(); + CertificateValidatorBuilder builder = new CertificateValidatorBuilder(); + + long refreshInterval = TimeUnit.SECONDS.toMillis(3600); + + return builder.namespaceChecks(NamespaceCheckingMode.EUGRIDPMA_AND_GLOBUS_REQUIRE) + .crlChecks(CrlCheckingMode.IF_VALID) + .ocspChecks(OCSPCheckingMode.IGNORE) + .lazyAnchorsLoading(false) + .storeUpdateListener(l) + .validationErrorListener(l) + .trustAnchorsDir("src/test/resources/trust-anchors") + .trustAnchorsUpdateInterval(refreshInterval) + .build(); + } + + @Test + public void tlsConnectorBuilderTests() { + + Server server = new Server(); + X509CertChainValidatorExt validator = getValidator(); + TLSServerConnectorBuilder builder = TLSServerConnectorBuilder.instance(server, validator); + HttpConfiguration httpConfiguration = builder.httpConfiguration(); + KeyManager keyManager = Mockito.mock(KeyManager.class); + builder.withPort(1234) + .withCertificateFile("fake-certificate") + .withCertificateKeyFile("fake-key") + .withHttpConfiguration(httpConfiguration) + .withKeyManager(keyManager) + .withExcludeCipherSuites("one", "two") + .withIncludeProtocols("protocol", "another-protocol"); + + builder.build(); + } +} diff --git a/src/test/java/org/italiangrid/storm/webdav/test/utils/ThreadPoolBuilderTests.java b/src/test/java/org/italiangrid/storm/webdav/test/utils/ThreadPoolBuilderTests.java new file mode 100644 index 00000000..35187463 --- /dev/null +++ b/src/test/java/org/italiangrid/storm/webdav/test/utils/ThreadPoolBuilderTests.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.test.utils; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.italiangrid.storm.webdav.server.util.ThreadPoolBuilder.DEFAULT_IDLE_TIMEOUT; +import static org.italiangrid.storm.webdav.server.util.ThreadPoolBuilder.DEFAULT_MAX_THREADS; +import static org.italiangrid.storm.webdav.server.util.ThreadPoolBuilder.DEFAULT_MIN_THREADS; +import static org.italiangrid.storm.webdav.server.util.ThreadPoolBuilder.DEFAULT_THREAD_POOL_METRIC_NAME; +import static org.junit.Assert.assertNotNull; + +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ThreadPool; +import org.italiangrid.storm.webdav.server.util.ThreadPoolBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.jetty9.InstrumentedQueuedThreadPool; + +@ExtendWith(MockitoExtension.class) +public class ThreadPoolBuilderTests { + + final static String PREFIX = "iam"; + final static String NAME = "queue"; + final static int IDLE_TIMEOUT = 6; + final static int MIN_THREADS = 10; + final static int MAX_THREADS = 20; + final static int MAX_QUEUE_SIZE = 40; + + @Mock + MetricRegistry registry; + + @Test + public void threadPoolBuilderTests() { + + ThreadPool tp = ThreadPoolBuilder.instance() + .withMinThreads(MIN_THREADS) + .withMaxThreads(MAX_THREADS) + .withIdleTimeoutMsec(IDLE_TIMEOUT) + .withMaxRequestQueueSize(MAX_QUEUE_SIZE) + .withName(NAME) + .withPrefix(PREFIX) + .build(); + assertNotNull(tp); + assertThat(tp.getClass(), is(QueuedThreadPool.class)); + QueuedThreadPool qtp = (QueuedThreadPool) tp; + assertThat(qtp.getMinThreads(), is(MIN_THREADS)); + assertThat(qtp.getMaxThreads(), is(MAX_THREADS)); + assertThat(qtp.getIdleTimeout(), is(IDLE_TIMEOUT)); + assertThat(qtp.getMaxAvailableThreads(), is(MAX_THREADS)); + assertThat(qtp.getName(), is(NAME)); + } + + @Test + public void threadPoolBuilderWithDefaultValuesTests() { + + ThreadPool tp = ThreadPoolBuilder.instance().build(); + assertNotNull(tp); + assertThat(tp.getClass(), is(QueuedThreadPool.class)); + QueuedThreadPool qtp = (QueuedThreadPool) tp; + assertThat(qtp.getMinThreads(), is(DEFAULT_MIN_THREADS)); + assertThat(qtp.getMaxThreads(), is(DEFAULT_MAX_THREADS)); + assertThat(qtp.getIdleTimeout(), is(DEFAULT_IDLE_TIMEOUT)); + assertThat(qtp.getMaxAvailableThreads(), is(DEFAULT_MAX_THREADS)); + assertThat(qtp.getName(), is(DEFAULT_THREAD_POOL_METRIC_NAME)); + } + + @Test + public void threadPoolBuilderWithRegistryTests() { + + ThreadPool tp = ThreadPoolBuilder.instance().registry(registry).build(); + assertNotNull(tp); + assertThat(tp.getClass(), is(InstrumentedQueuedThreadPool.class)); + InstrumentedQueuedThreadPool qtp = (InstrumentedQueuedThreadPool) tp; + assertThat(qtp.getMinThreads(), is(DEFAULT_MIN_THREADS)); + assertThat(qtp.getMaxThreads(), is(DEFAULT_MAX_THREADS)); + assertThat(qtp.getIdleTimeout(), is(DEFAULT_IDLE_TIMEOUT)); + assertThat(qtp.getMaxAvailableThreads(), is(DEFAULT_MAX_THREADS)); + assertThat(qtp.getName(), is(DEFAULT_THREAD_POOL_METRIC_NAME)); + } +} \ No newline at end of file diff --git a/src/test/resources/jwk/test-keystore.jwks b/src/test/resources/jwk/test-keystore.jwks new file mode 100644 index 00000000..b0d97f4e --- /dev/null +++ b/src/test/resources/jwk/test-keystore.jwks @@ -0,0 +1,16 @@ +{ + "keys": [ + { + "p": "-qdvzeHU7w_ToV2RlS2QlVggXNL2YfpRWQxvrO8pHZC_dVgYFwKz5nadOMzR1BK0tPuCTWuuI66sFgaA9VENGypdIYoCF2O1FBLFK6GjOO-uc0LZEbIDa6Xn0G7UYOWcLaiYriHTtC_Pzp11L7VGjrUlX4HRgU_B3X1oeGn0mbM", + "kty": "RSA", + "q": "5S2b9tYHi9zBcNGZ8X6GM4TAL4UU9mABH0rKIyzbudkG7Wxxbj6I18skuHzfOOPI4c8sTQSv6IVAr2n1bn3_E5RSyPpbtDSCTYGzhijXl9wZ0ba2NidFrVjnL-KPx_gcHKnUHebKvsIEdjxeuqaaZ1kqEJX326b450Frghd78p8", + "d": "oDb1qfQTaP73jEZHgkOG5C9dY5EJZW57fX_BYUJ-yYTuTHPWZBVDKw9I_Ir1tSYyuTF-Bfb4iPim46gnEBM3AdvMian2iajvrN_rJFUJHo65vtY9xCXCD0d_Jct5JMyOafP5LF3cP38yDcyZRS_JeyKGB6U1KhbL-gG4hrQGS8qO3rdY_JQiLDLVdRRptHsPphS44JHXdP2qeVNJ41-CTfPWKiMIUOC0fj-As-dbTzRXuLXs04NayAdM-yhvRiwKujEfL8YbKW9CDJIgJfm2vzWHXFus5Y11S2Zr65cWxxVvRfnAFbFO1AkIkJc2jHZ4xLxfDU2kTi20sOMq1UFrvQ", + "e": "AQAB", + "kid": "rsa1", + "qi": "bN1wnq_0VkJlECMGPmeRFdZCX2LgAMrgwbJpysRw5J04vO9YsVmAcB_4xqoDjDUg7koioAp3IOhMGjOJWpYzCqWzsaA_84kX4WKGsr2xz6oaFSgt1FvtBY4GqEeZj8RG0LMtEtKSyjHGieA0hd4TUcqSdNZ4osT98Bfd7z3peYc", + "dp": "YfyaxI2IRHyXavm9M-hAIWH2JNOD5gGJU5p8_cnw9NHlRuZNZJF16p5sEAxh6tn1MtsvsTxrMx_RvjqEp2IsEXaaOcZN0v7zhwlfcxMZT-TC-eQkH7rLg4Wz_dOVytt4FpFWPpySuloGjusXKLNhBeDi31dMo5SeYQvpj0k8iek", + "dq": "9_lhyLPNdohmxqwE5kkA7L23NbPJ-svmavWBwo3HMlCiLkQoeCEx8EzebsCux9-wfKSuSqfHrtCALU15QxUR6x2SdeRvVY17cGHm3kNTA_4j8cbBYdccjXSksitzZ-wOfvVDjxcqST2llkm8NjoO18Siv0-F4SXKLG-c5CaE9w", + "n": "4GRvJuFantVV3JdjwQOAkfREnwUFp2znRBTOIJhPamyH4gf4YlI5PQT79415NV4_HrWYzgooH5AK6-7WE-TLLGEAVK5vdk4vv79bG7ukvjvBPxAjEhQn6-Amln88iXtvicEGbh--3CKbQj1jryVU5aWM6jzweaabFSeCILVEd6ZT7ofXaAqan9eLzU5IEtTPy5MfrrOvWw5Q7D2yzMqc5LksmaQSw8XtmhA8gnENnIqjAMmPtRltf93wjtmiamgVENOVPdN-93Nd5w-pnMwEyoO6Q9JqXxV6lD6qBRxI7_5t4_vmVxcbbxcZbSAMoHqA2pbSMJ4Jcw-27Hct9jesLQ" + } + ] +}