diff --git a/pom.xml b/pom.xml index ac329f11..562e14e0 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,7 @@ 2.7.18 + 2.5.2.RELEASE italiangrid_storm-webdav @@ -264,6 +265,12 @@ test + + org.springframework.security.oauth + spring-security-oauth2 + ${spring-security-oauth2.version} + + org.springframework.boot spring-boot-starter-thymeleaf diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 578bee2d..f8452aac 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -1,9 +1,58 @@ +spring: + main: + banner-mode: "off" + security: + oauth2: + client: + provider: + wlcg: + issuer-uri: https://wlcg.cloud.cnaf.infn.it/ + registration: + wlcg: + provider: wlcg + authorization-grant-type: authorization_code + client-name: WLCG IAM + client-id: xfer.cr.cnaf.infn.it + client-secret: ANR7a3cYRFfXV3r8CXHVzA7xYSXFA7gQ6kEcQBKEESvJrdBXYZMaSaLXAlp3RXd5dfWAs1b1K4EGxbEJWTH4coU + scope: + - openid + - profile + - wlcg.groups + + session: + store-type: none + +server: + jetty: + accesslog: + enabled: false oauth: - + enable-oidc: true + refresh-period-minutes: 1 issuers: - - name: iam-test - issuer: https://iam-test.indigo-datacloud.eu/ - - name: iam-wlcg + - name: wlcg issuer: https://wlcg.cloud.cnaf.infn.it/ - - name: iam-local - issuer: https://iam.local.io/ \ No newline at end of file + enforce-audience-checks: true + audiences: + - https://wlcg.cern.ch/jwt/v1/any + - https://xfer.cr.cnaf.infn.it:8443 + +storm: + connector: + port: 8086 + securePort: 9443 + sa: + config-dir: src/test/resources/conf/sa.d + tls: + trust-anchors-dir: src/test/resources/trust-anchors + certificate-path: src/test/resources/hostcert/hostcert.pem + private-key-path: src/test/resources/hostcert/hostkey.pem + authz-server: + enabled: true + voms: + trust-store: + dir: src/test/resources/vomsdir + tape: + well-known: + source: src/test/resources/well-known/wlcg-tape-rest-api.json + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8761ed40..0d61b1d7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -50,3 +50,5 @@ logging.level.org.italiangrid.storm=INFO #logging.level.org.eclipse.jetty.io=ERROR #logging.level.org.eclipse.jetty.server=DEBUG #logging.level.org.eclipse.jetty.server.HttpOutput=ERROR + +logging.level.org.springframework.security.oauth2.jwt.NimbusJwtDecoder=DEBUG \ No newline at end of file diff --git a/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwk/JWKCachingTests.java b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwk/JWKCachingTests.java index 626f8189..8b134ec9 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwk/JWKCachingTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwk/JWKCachingTests.java @@ -15,61 +15,122 @@ */ package org.italiangrid.storm.webdav.test.oauth.jwk; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import java.io.BufferedReader; +import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.text.ParseException; +import java.net.URI; +import java.nio.file.Path; import java.time.Clock; import java.time.Instant; import java.time.ZoneId; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.TimeUnit; +import java.util.Map; -import org.italiangrid.storm.webdav.config.OAuthProperties; -import org.italiangrid.storm.webdav.oauth.CompositeJwtDecoder; +import org.apache.commons.io.FileUtils; +import org.italiangrid.storm.webdav.authz.VOMSAuthenticationFilter; +import org.italiangrid.storm.webdav.fs.attrs.ExtendedAttributesHelper; +import org.italiangrid.storm.webdav.oauth.StormJwtAuthoritiesConverter; import org.italiangrid.storm.webdav.oauth.utils.OidcConfigurationFetcher; -import org.italiangrid.storm.webdav.oauth.utils.TrustedJwtDecoderCacheLoader; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; +import org.italiangrid.storm.webdav.server.servlet.MiltonFilter; +import org.italiangrid.storm.webdav.test.utils.oauth.WithMockOAuthUser; +import org.italiangrid.storm.webdav.test.utils.voms.WithMockVOMSUser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.internal.verification.VerificationModeFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.test.context.support.WithAnonymousUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.LoadingCache; -import com.nimbusds.jose.jwk.JWKSet; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.RemoteKeySourceException; -@ExtendWith(MockitoExtension.class) +import io.lettuce.core.GetExArgs.Builder; +import io.milton.http.HttpManager; + +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("jwk-test") public class JWKCachingTests { public static final Instant NOW = Instant.parse("2018-01-01T00:00:00.00Z"); + public static final String SLASH_WLCG_SLASH_FILE = "/wlcg/example"; + public static final String ISSUER = "https://wlcg.cloud.cnaf.infn.it/"; + public static final URI JWKS_URI = URI.create("https://wlcg.cloud.cnaf.infn.it/jwk"); + Clock fixedClock = Clock.fixed(NOW, ZoneId.systemDefault()); + ObjectMapper mapper = new ObjectMapper(); - @Mock - TrustedJwtDecoderCacheLoader loader; + @Autowired + private MockMvc mvc; - @Mock + @MockBean OidcConfigurationFetcher fetcher; - @Mock - ExecutorService executor; + @MockBean + JwtDecoder decoder; + + @MockBean + ExtendedAttributesHelper helper; + + @Autowired + private VOMSAuthenticationFilter filter; - @Mock - OAuthProperties oauthProperties; + @Autowired + private FilterRegistrationBean miltonFilter; + + @SuppressWarnings("unchecked") + @BeforeEach + public void setup() throws IOException, RemoteKeySourceException { - private JWKSet getTestJWKSet() throws IOException, ParseException { + ClassLoader classLoader = getClass().getClassLoader(); + String jwks = FileUtils.readFileToString(new File(classLoader.getResource("jwk-test/well-known/jwk/keystore.jwks").getFile()), "UTF-8"); + String oidcConfiguration = FileUtils.readFileToString(new File(classLoader.getResource("jwk-test/well-known/opeind-configuration").getFile()), "UTF-8"); + Map configurationMap = (Map) mapper.readValue(oidcConfiguration, Map.class); + configurationMap.entrySet().forEach(e -> System.out.println(e.getKey() + "-" + e.getValue())); + lenient().when(fetcher.loadConfigurationForIssuer(eq(ISSUER))).thenReturn(configurationMap); + lenient().when(fetcher.loadJWKSourceForURL(eq(JWKS_URI))).thenReturn(jwks); + + lenient().when(helper.getChecksumAttribute(Mockito.any(File.class))).thenReturn(""); + lenient().when(helper.getChecksumAttribute(Mockito.any(Path.class))).thenReturn(""); - String data = - new String(getClass().getResourceAsStream("jwk/test-keystore.jwks").readAllBytes()); - return JWKSet.parse(data); + filter.setCheckForPrincipalChanges(false); + + HttpManager httpManager = Mockito.mock(HttpManager.class); + miltonFilter.getFilter().setMiltonHTTPManager(httpManager); } - public void testRefreshPeriodExpired() { + @Test + public void testRefreshPeriodExpired() throws Exception { + + Jwt token = Jwt.withTokenValue("test") + .header("kid", "rsa1") + .issuer(ISSUER) + .subject("123") + .claim("scope", "storage.read:/ storage.modify:/") + .build(); - + Mockito.verify(fetcher, VerificationModeFactory.times(1)).loadConfigurationForIssuer(ISSUER); + mvc.perform(put("/wlcg/test").with(jwt().jwt(token))).andExpect(status().isOk()); + Mockito.verify(fetcher, VerificationModeFactory.times(1)).loadJWKSourceForURL(JWKS_URI); + Thread.sleep(61000); // 61 seconds + mvc.perform(get(SLASH_WLCG_SLASH_FILE)).andExpect(status().isOk()); + Mockito.verify(fetcher, VerificationModeFactory.times(2)).loadJWKSourceForURL(JWKS_URI); } } 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..44fc9e49 --- /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)); + } +} diff --git a/src/test/resources/application-authz-test.yml b/src/test/resources/application-authz-test.yml index 3d9ceafd..d1b3a46b 100644 --- a/src/test/resources/application-authz-test.yml +++ b/src/test/resources/application-authz-test.yml @@ -4,6 +4,10 @@ server: enabled: false oauth: enable-oidc: false + refresh-period-minutes: 1 + issuers: + - name: iam-dev + issuer: https://iam-dev.cloud.cnaf.infn.it/ storm: sa: diff --git a/src/test/resources/conf/sa.d/test.properties b/src/test/resources/conf/sa.d/test.properties index 513bfacc..0e0400fe 100644 --- a/src/test/resources/conf/sa.d/test.properties +++ b/src/test/resources/conf/sa.d/test.properties @@ -18,7 +18,8 @@ name=test rootPath=src/test/resources/storage/test filesystemType=posixfs accessPoints=/test -vos=test.vo +vos=wlcg +orgs=https://wlcg.cloud.cnaf.infn.it/ authenticatedReadEnabled=true anonymousReadEnabled=false voMapGrantsWritePermission=false