diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 3a70d09cdd..15adcb6f1e 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -2979,7 +2979,6 @@ jobs: path: | build/reports/problems/problems-report.html results/** - apiml/build/reports/tests/** - apiml-security-common/build/reports/tests/** + **/build/reports/tests/** - uses: ./.github/actions/teardown diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/SafAuthorizationManager.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/SafAuthorizationManager.java new file mode 100644 index 0000000000..5e454cbb7f --- /dev/null +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/SafAuthorizationManager.java @@ -0,0 +1,45 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.security.common.auth.saf; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.authorization.AuthorityAuthorizationDecision; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.util.List; + +@RequiredArgsConstructor +public class SafAuthorizationManager implements ReactiveAuthorizationManager { + + private final SafResourceAccessVerifying safResourceAccessVerifying; + private final String safResourceClass; + private final String safResourceName; + private final String safResourceAccess; + + @Override + public Mono check(Mono authentication, T object) { + // @formatter:off + return authentication.filter(Authentication::isAuthenticated) + .flatMap(auth -> Mono.fromCallable(() -> + safResourceAccessVerifying.hasSafResourceAccess(auth, safResourceClass, safResourceName, safResourceAccess)) + .subscribeOn(Schedulers.boundedElastic())) + .filter(value -> value != null && value) + .map(auth -> ((AuthorizationDecision) new AuthorityAuthorizationDecision(true, List.of(new SimpleGrantedAuthority(String.format("%s.%s:%s", safResourceClass, safResourceName, safResourceAccess)))))) + .defaultIfEmpty(new AuthorityAuthorizationDecision(false, List.of())); + // @formatter:on + } + +} diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/SafMethodSecurityExpressionRoot.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/SafMethodSecurityExpressionRoot.java index 09815105bc..a90427e3e1 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/SafMethodSecurityExpressionRoot.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/SafMethodSecurityExpressionRoot.java @@ -18,6 +18,7 @@ @Component @RequiredArgsConstructor public class SafMethodSecurityExpressionRoot { + private final SafSecurityConfigurationProperties safSecurityConfigurationProperties; private final SafResourceAccessVerifying safResourceAccessVerifying; diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/SafResourceAccessDummy.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/SafResourceAccessDummy.java index d749402db7..fcd4d301bb 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/SafResourceAccessDummy.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/auth/saf/SafResourceAccessDummy.java @@ -12,6 +12,7 @@ import lombok.Builder; import lombok.Value; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; @@ -30,6 +31,7 @@ * the file from applications classpath. It is highly recommended to locate empty file `mock-saf` in each application * using this feature to allow start application without any other action. */ +@Slf4j public class SafResourceAccessDummy implements SafResourceAccessVerifying { private static final String SAF_ACCESS = "safAccess"; @@ -50,6 +52,7 @@ public class SafResourceAccessDummy implements SafResourceAccessVerifying { public SafResourceAccessDummy() throws IOException { File file = getFile(); if (file != null) { + log.info("Load Dummy SAF definition from file: {}", file.getAbsolutePath()); try ( FileInputStream fis = new FileInputStream(file); BufferedInputStream bis = new BufferedInputStream(fis) @@ -57,6 +60,8 @@ public SafResourceAccessDummy() throws IOException { loadDefinition(bis); } } else { + var resource = this.getClass().getClassLoader().getResource(DEFAULT_RESOURCE_LOCATION); + log.info("Attempt to load Dummy SAF definition from resource {}" + resource); try (InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(DEFAULT_RESOURCE_LOCATION)) { loadDefinition(inputStream); } @@ -78,14 +83,18 @@ public SafResourceAccessDummy(InputStream inputStream) { @Override public boolean hasSafResourceAccess(Authentication authentication, String resourceClass, String resourceName, String accessLevel) { - ResourceUser resourceUser = ResourceUser.builder() + log.info("Verify access of : {} to SAF class: {}, resource: {}, access level: {}", authentication, resourceClass, resourceName, accessLevel); + var resourceUser = ResourceUser.builder() .resourceClass(resourceClass) .resourceName(resourceName) .userId(authentication.getName()) .build(); - AccessLevel currentLevel = resourceUserToAccessLevel.get(resourceUser); - if (currentLevel == null) return false; - return currentLevel.compareTo(AccessLevel.valueOf(accessLevel)) >= 0; + var currentLevel = resourceUserToAccessLevel.get(resourceUser); + var hasAccess = currentLevel != null && currentLevel.compareTo(AccessLevel.valueOf(accessLevel)) >= 0; + + log.info("Has access: {}, current level: {}", hasAccess, currentLevel); + + return hasAccess; } /** @@ -142,6 +151,7 @@ private T getSafAccess(Map data) { } private void loadDefinition(Map data) { + log.info("load definitions: {}", data); Map>>> classes = getSafAccess(data); if (classes == null) return; diff --git a/apiml-security-common/src/main/resources/mock-saf.yml b/apiml-security-common/src/main/resources/mock-saf.yml index 07008d74fb..4f38abd1bf 100644 --- a/apiml-security-common/src/main/resources/mock-saf.yml +++ b/apiml-security-common/src/main/resources/mock-saf.yml @@ -8,3 +8,7 @@ # This file is stored in `src/main/resources/mock-saf.yml` which means that it will be used by the service and its unit tests. # If you can create a different file in `src/test/resources/mock-saf.yml` then unit tests will use different definitions. safAccess: + ZOWE: + APIML.DEBUG: + CONTROL: + - USER diff --git a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/auth/saf/SafAuthorizationManagerTest.java b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/auth/saf/SafAuthorizationManagerTest.java new file mode 100644 index 0000000000..2c42a4620c --- /dev/null +++ b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/auth/saf/SafAuthorizationManagerTest.java @@ -0,0 +1,92 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.security.common.auth.saf; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +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.security.authorization.AuthorityAuthorizationDecision; +import org.springframework.security.core.Authentication; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SafAuthorizationManagerTest { + + private static final String RESOURCE_CLASS = "ZOWE"; + private static final String RESOURCE_NAME = "APIML.SERVICES"; + private static final String RESOURCE_ACCESS = "READ"; + + @Mock + private SafResourceAccessVerifying safResourceAccessVerifying; + + @Mock + private Authentication authentication; + + private SafAuthorizationManager safAuthorizationManager; + + @BeforeEach + void setUp() { + safAuthorizationManager = new SafAuthorizationManager<>(safResourceAccessVerifying, RESOURCE_CLASS, RESOURCE_NAME, RESOURCE_ACCESS); + } + + @Nested + class GivenSafAuthorization { + + @Test + void whenAuthenticatedAndHasAccess_thenGrantWithAuthority() { + when(authentication.isAuthenticated()).thenReturn(true); + when(safResourceAccessVerifying.hasSafResourceAccess(authentication, RESOURCE_CLASS, RESOURCE_NAME, RESOURCE_ACCESS)).thenReturn(true); + + StepVerifier.create(safAuthorizationManager.check(Mono.just(authentication), new Object())) + .assertNext(decision -> { + assertTrue(decision.isGranted()); + var authorities = ((AuthorityAuthorizationDecision) decision).getAuthorities(); + assertEquals(1, authorities.size()); + assertEquals(String.format("%s.%s:%s", RESOURCE_CLASS, RESOURCE_NAME, RESOURCE_ACCESS), authorities.iterator().next().getAuthority()); + }) + .verifyComplete(); + } + + @Test + void whenAuthenticatedButNoAccess_thenDenyWithoutAuthorities() { + when(authentication.isAuthenticated()).thenReturn(true); + when(safResourceAccessVerifying.hasSafResourceAccess(authentication, RESOURCE_CLASS, RESOURCE_NAME, RESOURCE_ACCESS)).thenReturn(false); + + StepVerifier.create(safAuthorizationManager.check(Mono.just(authentication), new Object())) + .assertNext(decision -> { + assertFalse(decision.isGranted()); + assertTrue(((AuthorityAuthorizationDecision) decision).getAuthorities().isEmpty()); + }) + .verifyComplete(); + } + + @Test + void whenNotAuthenticated_thenDenyWithoutCallingVerifier() { + when(authentication.isAuthenticated()).thenReturn(false); + + StepVerifier.create(safAuthorizationManager.check(Mono.just(authentication), new Object())) + .assertNext(decision -> assertFalse(decision.isGranted())) + .verifyComplete(); + + verify(safResourceAccessVerifying, never()).hasSafResourceAccess(any(), any(), any(), any()); + } + + } + +} diff --git a/apiml-security-common/src/test/resources/mock-saf.yml b/apiml-security-common/src/test/resources/mock-saf.yml index 29fd6fbe06..98bfcd4aa4 100644 --- a/apiml-security-common/src/test/resources/mock-saf.yml +++ b/apiml-security-common/src/test/resources/mock-saf.yml @@ -15,6 +15,9 @@ safAccess: APIML.RESOURCE: READ: - user + APIML.DEBUG: + CONTROL: + - USER CLASS: RESOURCE: READ: diff --git a/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java b/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java index 1aaccd9418..e404f82b58 100644 --- a/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java +++ b/apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java @@ -24,8 +24,11 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.SecurityWebFiltersOrder; @@ -42,7 +45,6 @@ import org.zowe.apiml.constants.ApimlConstants; import org.zowe.apiml.filter.BasicLoginFilter; import org.zowe.apiml.filter.CachedBodyFilter; -import org.zowe.apiml.security.common.filter.CategorizeCertsWebFilter; import org.zowe.apiml.filter.LogoutHandler; import org.zowe.apiml.filter.OIDCAuthFilter; import org.zowe.apiml.filter.QueryWebFilter; @@ -52,7 +54,10 @@ import org.zowe.apiml.handler.FailedAuthenticationWebHandler; import org.zowe.apiml.handler.LocalTokenProvider; import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.security.common.auth.saf.SafAuthorizationManager; +import org.zowe.apiml.security.common.auth.saf.SafResourceAccessVerifying; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.filter.CategorizeCertsWebFilter; import org.zowe.apiml.security.common.token.OIDCProvider; import org.zowe.apiml.security.common.util.X509Util; import org.zowe.apiml.security.common.verify.CertificateValidator; @@ -61,16 +66,12 @@ import org.zowe.apiml.zaas.security.login.x509.X509AuthenticationProvider; import org.zowe.apiml.zaas.security.mapping.AuthenticationMapper; import org.zowe.apiml.zaas.security.query.TokenAuthenticationProvider; +import reactor.core.publisher.Mono; import java.util.Arrays; import java.util.List; import java.util.Set; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.ReactiveAuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import reactor.core.publisher.Mono; - import static org.springframework.http.HttpMethod.DELETE; import static org.springframework.http.HttpMethod.GET; import static org.springframework.http.HttpMethod.POST; @@ -87,6 +88,7 @@ public class WebSecurityConfig { private static final String CONTEXT_PATH = String.format("/%s", CoreService.GATEWAY.getServiceId()); private static final String REGISTRY_PATH = CONTEXT_PATH + "/api/v1/registry"; + private static final String APPLICATION = "/application/**"; private static final String APPLICATION_HEALTH = "/application/health"; private static final String APPLICATION_INFO = "/application/info"; @@ -354,15 +356,26 @@ SecurityWebFilterChain healthEndpointFilterChain(ServerHttpSecurity http, @Bean SecurityWebFilterChain applicationEndpointsProtected(ServerHttpSecurity http, AuthConfigurationProperties authConfigurationProperties, - AuthExceptionHandlerReactive authExceptionHandlerReactive) { + AuthExceptionHandlerReactive authExceptionHandlerReactive, + SafResourceAccessVerifying safResourceAccessVerifying) { + return http .securityMatcher(new AndServerWebExchangeMatcher( - pathMatchers("/application/**"), + pathMatchers(APPLICATION), new NegatedServerWebExchangeMatcher(pathMatchers(APPLICATION_HEALTH, APPLICATION_INFO, "/application/version")) )) .csrf(ServerHttpSecurity.CsrfSpec::disable) - .authorizeExchange(exchange -> exchange.anyExchange().authenticated()) .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) + .authorizeExchange(exchange -> exchange.matchers( + new OrServerWebExchangeMatcher( + pathMatchers(HttpMethod.GET, APPLICATION), + pathMatchers(HttpMethod.HEAD, APPLICATION) + )) + .authenticated() + ) + .authorizeExchange(exchange -> exchange.matchers(pathMatchers(APPLICATION)) + .access(new SafAuthorizationManager<>(safResourceAccessVerifying, "ZOWE", "APIML.DEBUG", "CONTROL")) + ) .addFilterAfter(new TokenAuthFilter(localTokenProvider, authConfigurationProperties, authExceptionHandlerReactive), SecurityWebFiltersOrder.AUTHENTICATION) .addFilterAfter(new BasicLoginFilter(compoundAuthProvider, failedAuthenticationWebHandler), SecurityWebFiltersOrder.AUTHENTICATION) .build(); diff --git a/apiml/src/main/resources/application.yml b/apiml/src/main/resources/application.yml index ad8ffbd392..57ff19f66e 100644 --- a/apiml/src/main/resources/application.yml +++ b/apiml/src/main/resources/application.yml @@ -1,6 +1,4 @@ # for back-compatibility -spring.profiles.group.attls: attlsServer,attlsClient - eureka: dashboard: path: /eureka @@ -18,6 +16,10 @@ eureka: useReadOnlyResponseCache: false peer-node-read-timeout-ms: 15000 spring: + profiles: + group: + attls: attlsServer,attlsClient + debug-control: debug cloud: gateway: server: @@ -379,11 +381,24 @@ logging: management: endpoint: gateway: - access: unrestricted + access: read-only + loggers: + access: read-only endpoints: web: exposure: include: health,info,gateway,loggers + +--- +spring.config.activate.on-profile: debug-control + +management: + endpoint: + gateway: + access: unrestricted + loggers: + access: unrestricted + --- spring.config.activate.on-profile: wiretap diff --git a/apiml/src/main/resources/mock-saf.yml b/apiml/src/main/resources/mock-saf.yml new file mode 100644 index 0000000000..4f38abd1bf --- /dev/null +++ b/apiml/src/main/resources/mock-saf.yml @@ -0,0 +1,14 @@ +# This file is used by the SafResourceAccessDummy class for testing and when the service is running outside of z/OS +# It defines what are the access levels for SAF resources and users +# +# There are to special values of access level: +# - `FAILURE` - the check request will fail with an internal error +# - `NONE` - there is no access to the resource but the resource is defined +# +# This file is stored in `src/main/resources/mock-saf.yml` which means that it will be used by the service and its unit tests. +# If you can create a different file in `src/test/resources/mock-saf.yml` then unit tests will use different definitions. +safAccess: + ZOWE: + APIML.DEBUG: + CONTROL: + - USER diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTest.java index a50fa6b730..8382207a0d 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/AcceptanceTest.java @@ -61,7 +61,7 @@ } ) @Execution(ExecutionMode.SAME_THREAD) -@ActiveProfiles("ApimlModulithAcceptanceTest") +@ActiveProfiles({"ApimlModulithAcceptanceTest", "test" }) @AutoConfigureWebTestClient @DirtiesContext public @interface AcceptanceTest { diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/ActuatorConfigTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/ActuatorConfigTest.java new file mode 100644 index 0000000000..fc4b4dc077 --- /dev/null +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/ActuatorConfigTest.java @@ -0,0 +1,340 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.acceptance; + +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.PlainJWT; +import io.restassured.RestAssured; +import io.restassured.config.HttpClientConfig; +import io.restassured.config.RestAssuredConfig; +import org.apache.http.NoHttpResponseException; +import org.apache.http.client.HttpRequestRetryHandler; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.zowe.apiml.handler.LocalTokenProvider; +import org.zowe.apiml.security.common.token.QueryResponse; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.Date; + +import static io.restassured.RestAssured.given; +import static org.apache.hc.core5.http.HttpStatus.SC_FORBIDDEN; +import static org.apache.hc.core5.http.HttpStatus.SC_METHOD_NOT_ALLOWED; +import static org.apache.hc.core5.http.HttpStatus.SC_NOT_FOUND; +import static org.apache.hc.core5.http.HttpStatus.SC_NO_CONTENT; +import static org.apache.hc.core5.http.HttpStatus.SC_OK; +import static org.apache.hc.core5.http.HttpStatus.SC_UNAUTHORIZED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +class ActuatorConfigTest { + + private static final String USER = "USER"; + + private static final String USER_NO_PERMISSION = "APIMTST"; + + private static final String AUTH_COOKIE = "apimlAuthenticationToken"; + + private abstract static class ActuatorAcceptanceTest extends AcceptanceTestWithBasePath { + + @MockitoBean + private LocalTokenProvider tokenProvider; + + private RestAssuredConfig config; + + @BeforeEach + void mockTokenValidation() { + when(tokenProvider.validateToken(any())).thenAnswer(invocation -> { + String jwt = invocation.getArgument(0); + String userId = JWTParser.parse(jwt).getJWTClaimsSet().getSubject(); + return Mono.just(new QueryResponse( + null, userId, new Date(), Date.from(Instant.now().plus(Duration.ofHours(1))), + QueryResponse.Source.ZOWE.value, Collections.emptyList(), QueryResponse.Source.ZOWE + )); + }); + } + + @BeforeEach + void retryOnDroppedConnection() { + HttpRequestRetryHandler retryHandler = (exception, executionCount, context) -> + exception instanceof NoHttpResponseException && executionCount < 3; + this.config = RestAssured.config; + RestAssured.config = RestAssured.config + .httpClient(HttpClientConfig.httpClientConfig().setParam("http.method.retry-handler", retryHandler)); + } + + @AfterEach + void restoreConfig() { + RestAssured.config = this.config; + } + + } + + private String login(String user) { + var now = Instant.now(); + var claims = new JWTClaimsSet.Builder() + .subject(user) + .issuer(QueryResponse.Source.ZOWE.value) + .issueTime(Date.from(now)) + .expirationTime(Date.from(now.plus(Duration.ofHours(1)))) + .build(); + return new PlainJWT(claims).serialize(); + } + + @Nested + @TestPropertySource( + properties = { + "server.ssl.keyStore=../keystore/localhost/localhost.keystore.p12", + "server.ssl.trustStore=../keystore/localhost/localhost.truststore.p12", + "apiml.security.auth.provider=dummy", + "caching.storage.mode=inMemory" + } + ) + @ActiveProfiles("default") + @AcceptanceTest + class GivenDefaultProfile extends ActuatorAcceptanceTest { + + @Autowired + private Environment environment; + + @BeforeEach + void setUp() { + var property = environment.getProperty("management.endpoints.web.exposure.include"); + assertEquals("health,info", property); + } + + @ParameterizedTest + @CsvSource({ + "/application/loggers", + "/application/gateway" + }) + void whenAccessDangerousActuatorWithoutCredentials_thenBlock(String endpoint) { + given() + .when() + .get(basePath + endpoint) + .then() + .statusCode(SC_UNAUTHORIZED); + } + + @ParameterizedTest + @CsvSource({ + "/application/loggers", + "/application/gateway" + }) + void whenAccessDangerousActuatorWithCredentials_thenBlock(String endpoint) { + given() + .cookie(AUTH_COOKIE, login(USER)) + .when() + .get(basePath + endpoint) + .then() + .log().ifValidationFails() + .statusCode(SC_NOT_FOUND); + } + + } + + @Nested + @ActiveProfiles("debug") + @TestPropertySource( + properties = { + "server.ssl.keyStore=../keystore/localhost/localhost.keystore.p12", + "server.ssl.trustStore=../keystore/localhost/localhost.truststore.p12", + "apiml.security.auth.provider=dummy", + "caching.storage.mode=inMemory" + } + ) + @AcceptanceTest + class GivenDebugProfile extends ActuatorAcceptanceTest { + + @Autowired + private Environment environment; + + @BeforeEach + void setUp() { + var exposure = environment.getProperty("management.endpoints.web.exposure.include"); + assertEquals("health,info,gateway,loggers", exposure); + var gatewayAccess = environment.getProperty("management.endpoint.gateway.access"); + assertEquals("read-only", gatewayAccess); + var loggersAccess = environment.getProperty("management.endpoint.loggers.access"); + assertEquals("read-only", loggersAccess); + } + + @ParameterizedTest + @CsvSource({ + "/application/loggers", + "/application/gateway" + }) + void whenAccessDangerousActuatorWithCredentials_thenBlockModify(String endpoint) { + var jwt = login(USER); + // change the level of the ROOT logger + given() + .cookie("apimlAuthenticationToken", jwt) + .contentType("application/json") + .body("{\"configuredLevel\":\"DEBUG\"}") + .when() + .post(basePath + "/application/loggers/ROOT") + .then() + .statusCode(SC_METHOD_NOT_ALLOWED); + + // refresh the gateway's routes + given() + .cookie("apimlAuthenticationToken", jwt) + .when() + .post(basePath + "/application/gateway/refresh") + .then() + .statusCode(SC_NOT_FOUND); + } + + @ParameterizedTest + @CsvSource({ + "/application/loggers", + "/application/gateway" + }) + void whenAccessDangerousActuatorWithoutCredentials_thenBlock(String endpoint) { + given() + .when() + .get(basePath + endpoint) + .then() + .statusCode(SC_UNAUTHORIZED); + } + + @ParameterizedTest + @CsvSource({ + "/application/loggers", + "/application/gateway/routes", + "/application/info" + }) + void whenAccessDangerousActuator_thenAllowRead(String endpoint) { + given() + .cookie(AUTH_COOKIE, login(USER)) + .when() + .get(basePath + endpoint) + .then() + .statusCode(SC_OK); + } + + } + + @Nested + @ActiveProfiles({"debug", "debug-control"}) + @TestPropertySource( + properties = { + "server.ssl.keyStore=../keystore/localhost/localhost.keystore.p12", + "server.ssl.trustStore=../keystore/localhost/localhost.truststore.p12", + "apiml.security.auth.provider=dummy", + "caching.storage.mode=inMemory" + } + ) + @AcceptanceTest + class GivenDebugControlProfile extends ActuatorAcceptanceTest { + + @Autowired + private Environment environment; + + @BeforeEach + void setUp() { + var exposure = environment.getProperty("management.endpoints.web.exposure.include"); + assertEquals("health,info,gateway,loggers", exposure); + var gatewayAccess = environment.getProperty("management.endpoint.gateway.access"); + assertEquals("unrestricted", gatewayAccess); + var loggersAccess = environment.getProperty("management.endpoint.loggers.access"); + assertEquals("unrestricted", loggersAccess); + } + + @Test + void whenAccessDangerousActuatorWithCredentialsWithPermission_thenAllowModify() { + var jwt = login(USER); + // change the level of the ROOT logger + given() + .cookie("apimlAuthenticationToken", jwt) + .contentType("application/json") + .body("{\"configuredLevel\":\"DEBUG\"}") + .when() + .post(basePath + "/application/loggers/ROOT") + .then() + .statusCode(SC_NO_CONTENT); + + // refresh the gateway's routes + given() + .cookie("apimlAuthenticationToken", jwt) + .when() + .post(basePath + "/application/gateway/refresh") + .then() + .statusCode(SC_OK); + } + + @Test + void whenAccessDangerousActuatorWithCredentialsWithoutPermission_thenBlock() { + var jwt = login(USER_NO_PERMISSION); + String endpoint = "/application/loggers"; + // update a logger level + given() + .cookie("apimlAuthenticationToken", jwt) + .when() + .post(basePath + endpoint) + .then() + .statusCode(SC_FORBIDDEN); + + endpoint = "/application/gateway"; + // update routes + given() + .cookie("apimlAuthenticationToken", jwt) + .when() + .post(basePath + endpoint) + .then() + .statusCode(SC_FORBIDDEN); + } + + @ParameterizedTest + @CsvSource({ + "/application/loggers", + "/application/gateway" + }) + void whenAccessDangerousActuatorWithoutCredentials_thenBlock(String endpoint) { + given() + .when() + .get(basePath + endpoint) + .then() + .statusCode(SC_UNAUTHORIZED); + } + + @ParameterizedTest + @CsvSource({ + "/application/loggers", + "/application/gateway/routes" + }) + void whenAccessDangerousActuator_thenAllowRead(String endpoint) { + var jwt = login(USER_NO_PERMISSION); + given() + .cookie(AUTH_COOKIE, jwt) + .when() + .get(basePath + endpoint) + .then() + .statusCode(SC_OK); + } + + } + +} diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/AttlsConfigTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/AttlsConfigTest.java index 00be8e1018..993f6e4523 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/AttlsConfigTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/AttlsConfigTest.java @@ -80,7 +80,7 @@ private String getGatewayUrlWithPath(String hostname, int port, String scheme, S } @Nested - @ActiveProfiles({"attlsClient", "attlsServer"}) + @ActiveProfiles({"test", "attlsClient", "attlsServer"}) @DirtiesContext @SpringBootTest( classes = ApimlApplication.class, @@ -164,7 +164,7 @@ void requestFailsWithAttlsReasonWithHttp() { "server.ssl.keyStore=" } ) - @ActiveProfiles({"attlsServer", "attlsClient", "ApimlModulithAcceptanceTest"}) + @ActiveProfiles({"test", "attlsServer", "attlsClient", "ApimlModulithAcceptanceTest"}) @DirtiesContext @SpringBootTest( classes = { diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/AvailabilityTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/AvailabilityTest.java index ab1cc1c1e2..989b86d2f6 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/AvailabilityTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/AvailabilityTest.java @@ -31,7 +31,7 @@ */ @AcceptanceTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) -@ActiveProfiles({ "ApimlModulithAcceptanceTest", "AvailabilityTest" }) +@ActiveProfiles({ "test", "ApimlModulithAcceptanceTest", "AvailabilityTest" }) class AvailabilityTest extends AcceptanceTestWithBasePath { @ParameterizedTest(name = "{0} is available at port {1} with status {2}") diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesNonZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesNonZosTest.java index 62872e13bb..910a8f5118 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesNonZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesNonZosTest.java @@ -27,7 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; @AcceptanceTest -@ActiveProfiles({ "OpenTelemetryTest" }) +@ActiveProfiles({ "test", "OpenTelemetryTest" }) @TestPropertySource( properties = { "otel.sdk.disabled=false", diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java index f4ecffb8fd..07332ceb16 100644 --- a/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java +++ b/apiml/src/test/java/org/zowe/apiml/acceptance/OpenTelemetryResourceAttributesZosTest.java @@ -86,7 +86,7 @@ private boolean assertAttributesBase(Attributes attributes, int port) { @Nested @AcceptanceTest - @ActiveProfiles({"OpenTelemetryTest", "zos"}) + @ActiveProfiles({"test", "OpenTelemetryTest", "zos"}) @TestPropertySource( properties = { "otel.sdk.disabled=false", @@ -135,7 +135,7 @@ void thenLogCustomAttributes() { "apiml.security.personalAccessToken.enabled=true" } ) - @ActiveProfiles({"OpenTelemetryTest", "zos"}) + @ActiveProfiles({"test", "OpenTelemetryTest", "zos"}) @TestInstance(TestInstance.Lifecycle.PER_CLASS) @NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) class WhenOnboardedService extends AcceptanceTestWithMockServices { diff --git a/apiml/src/test/resources/application-test.yml b/apiml/src/test/resources/application-test.yml new file mode 100644 index 0000000000..c1a50f1463 --- /dev/null +++ b/apiml/src/test/resources/application-test.yml @@ -0,0 +1,58 @@ +logging: + level: + ROOT: DEBUG + org.springframework.cloud.gateway: DEBUG + org.springframework.http.server.reactive: DEBUG + org.springframework.web.reactive: DEBUG + org.zowe.apiml: DEBUG + redisratelimiter: DEBUG + +eureka: + server: + max-threads-for-peer-replication: 2 +management: + endpoint: + gateway: + access: unrestricted + endpoints: + web: + base-path: /application + exposure: + include: "*" + +apiml: + discovery: + allPeersUrls: https://${apiml.service.hostname}:${apiml.service.port}/eureka/ + service: + ignoredHeadersWhenCorsEnabled: Access-Control-Request-Method,Access-Control-Request-Headers,Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Credentials,Origin + port: 40985 + security: + allowedDomains: ${apiml.service.hostname},zowe.github.io,www.zowe.org,zowe.github.io,www.ibm.com + allowTokenRefresh: true + auth: + zosmf: + jwtAutoconfiguration: ltpa + serviceId: zosmf + saf: + urls: + authenticate: https://mock-services:10013/zss/saf/authenticate + verify: https://localhost:10013/zss/saf/verify +caching: + storage: + mode: inMemory + +infinispan: + embedded: + enabled: false + +server: + ssl: + keyStore: ../keystore/localhost/localhost.keystore.p12 + trustStore: ../keystore/localhost/localhost.truststore.p12 + +spring: + main: + allow-circular-references: true + banner-mode: ${apiml.banner:"console"} + web-application-type: reactive + allow-bean-definition-overriding: true diff --git a/apiml/src/test/resources/application.yml b/apiml/src/test/resources/application.yml deleted file mode 100644 index 1fa98ecdc1..0000000000 --- a/apiml/src/test/resources/application.yml +++ /dev/null @@ -1,237 +0,0 @@ -logging: - level: - ROOT: DEBUG - org.springframework.cloud.gateway: DEBUG - org.springframework.http.server.reactive: DEBUG - org.springframework.web.reactive: DEBUG - org.zowe.apiml: DEBUG - redisratelimiter: DEBUG - -eureka: - dashboard: - path: /eureka - client: - fetchRegistry: false - registerWithEureka: false - instanceInfoReplicationIntervalSeconds: 30 - region: default - serviceUrl: - defaultZone: ${apiml.discovery.allPeersUrls} - healthcheck: - enabled: true - server: - max-threads-for-peer-replication: 2 - useReadOnlyResponseCache: false -management: - endpoint: - gateway: - access: unrestricted - endpoints: - web: - base-path: /application - exposure: - include: "*" - -otel: - sdk: - disabled: true - -apiml: - catalog: - serviceId: apicatalog - discovery: - staticApiDefinitionsDirectories: config/local/api-defs - allPeersUrls: https://${apiml.service.hostname}:${apiml.service.port}/eureka/ - internal-discovery: - address: ${server.address} - port: 10011 - gateway: - eureka: - instance: - instanceId: ${apiml.service.hostname}:${apiml.service.id}:${apiml.service.port} - hostname: ${apiml.service.hostname} - #ports are computed in code - homePageUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/ - healthCheckUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/application/health - healthCheckUrlPath: /application/health - port: ${apiml.service.port} - securePort: ${apiml.service.port} - nonSecurePortEnabled: ${apiml.service.nonSecurePortEnabled} - securePortEnabled: ${apiml.service.securePortEnabled} - statusPageUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}/application/info - statusPageUrlPath: /application/info - metadata-map: - apiml: - registrationType: primary - apiBasePath: /gateway/api/v1 - catalog: - tile: - id: apimediationlayer - title: API Mediation Layer API - description: The API Mediation Layer for z/OS internal API services. The API Mediation Layer provides a single point of access to mainframe REST APIs and offers enterprise cloud-like features such as high-availability, scalability, dynamic API discovery, and documentation. - version: 1.0.0 - routes: - api_v1: - gatewayUrl: / - serviceUrl: / - apiInfo: - - apiId: zowe.apiml.gateway - version: 1.0.0 - gatewayUrl: api/v1 - swaggerUrl: https://${apiml.service.hostname}:${apiml.service.port}/v3/api-docs/gateway - documentationUrl: https://zowe.github.io/docs-site/ - service: - title: API Gateway - description: API Gateway service to route requests to services registered in the API Mediation Layer and provides an API for mainframe security. - supportClientCertForwarding: true - apimlId: ${apiml.service.apimlId:${apiml.service.hostname}_${apiml.service.port}} - externalUrl: ${apiml.service.externalUrl:${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port}} - authentication: - sso: true - registry: - enabled: false - metadata-key-allow-list: zos.sysname,zos.system,zos.sysplex,zos.cpcName,zos.zosName,zos.lpar - caching: - eureka: - instance: - metadata-map: - apiml: - registrationType: primary - apiBasePath: /cachingservice/api/v1 - catalog: - tile: - id: apimediationlayer - title: API Mediation Layer API - description: The API Mediation Layer for z/OS internal API services. The API Mediation Layer provides a single point of access to mainframe REST APIs and offers enterprise cloud-like features such as high-availability, scalability, dynamic API discovery, and documentation. - version: 1.0.0 - routes: - api_v1: - gatewayUrl: / - serviceUrl: /cachingservice - apiInfo: - - apiId: zowe.apiml.cachingservice - version: 1.0.0 - gatewayUrl: api/v1 - swaggerUrl: https://${apiml.service.hostname}:${apiml.service.port}/v3/api-docs/cachingservice - documentationUrl: https://zowe.github.io/docs-site/ - service: - title: Caching service for internal usage. - description: Service that provides caching API. - authentication: - sso: true - - connection: - timeout: 60000 - idleConnectionTimeoutSeconds: 5 - timeToLive: 10000 - routing: - instanceIdHeader: false - ignoredServices: discovery,zaas,apicatalog,cachingservice # to disable routing to the Discovery and ZAAS service + local services on Modulith - service: - apimlId: apiml1 - corsEnabled: true - corsAllowedEndpoints: /gateway/**,/apicatalog/**,/cachingservice/**,/v3/api-docs/** - discoveryServiceUrls: https://localhost:10011/eureka/ - forwardClientCertEnabled: false - hostname: localhost - id: ${spring.application.name} - ignoredHeadersWhenCorsEnabled: Access-Control-Request-Method,Access-Control-Request-Headers,Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Credentials,Origin - port: 40985 - scheme: https # "https" or "http" - preferIpAddress: false - nonSecurePortEnabled: false - securePortEnabled: true - externalUrl: ${apiml.service.scheme}://${apiml.service.hostname}:${apiml.service.port} - security: - allowedDomains: ${apiml.service.hostname},zowe.github.io,www.zowe.org,zowe.github.io,www.ibm.com - headersToBeCleared: X-Certificate-Public,X-Certificate-DistinguishedName,X-Certificate-CommonName - ssl: - nonStrictVerifySslCertificatesOfServices: false - filterChainConfiguration: new - allowTokenRefresh: true - jwtInitializerTimeout: 5 - personalAccessToken: - enabled: false - useInternalMapper: false - auth: - provider: zosmf - zosmf: - jwtAutoconfiguration: ltpa - serviceId: zosmf - saf: - urls: - authenticate: https://mock-services:10013/zss/saf/authenticate - verify: https://localhost:10013/zss/saf/verify - webClientConfig.enabled: true -caching: - storage: - mode: inMemory -jgroups: - bind: - port: 7600 - address: localhost - -server: - address: 0.0.0.0 - port: ${apiml.service.port} - ssl: - clientAuth: want - keyAlias: localhost - keyPassword: password - keyStore: ../keystore/localhost/localhost.keystore.p12 - keyStorePassword: password - keyStoreType: PKCS12 - trustStore: ../keystore/localhost/localhost.truststore.p12 - trustStorePassword: password - trustStoreType: PKCS12 - max-http-request-header-size: 48000 - -spring: - cloud: - gateway: - server: - webflux: - route-refresh-listener.enabled: false - x-forwarded: - prefix-append: false - prefix-enabled: true - filter: - secure-headers: - disable: content-security-policy,permitted-cross-domain-policies,download-options - referrer-policy: strict-origin-when-cross-origin - frame-options: sameorigin - application: - name: gateway - main: - allow-circular-references: true - banner-mode: ${apiml.banner:"console"} - web-application-type: reactive - allow-bean-definition-overriding: true - ---- -spring.config.activate.on-profile: attlsServer - -server: - attlsServer: - enabled: true - ssl: - enabled: false - -apiml: - service: - corsEnabled: true - ---- -spring.config.activate.on-profile: attlsClient - -server: - attlsClient: - enabled: true - service: - scheme: http - -apiml: - service: - scheme: http - nonSecurePortEnabled: true - securePortEnabled: false diff --git a/config/docker/apiml.yml b/config/docker/apiml.yml index 7635608721..defc681153 100644 --- a/config/docker/apiml.yml +++ b/config/docker/apiml.yml @@ -62,7 +62,7 @@ management: shutdown: access: unrestricted loggers: - enabled: true + access: read-only server: address: 0.0.0.0 diff --git a/config/saf.yml b/config/saf.yml index 3b79b40403..b13acbe7b5 100644 --- a/config/saf.yml +++ b/config/saf.yml @@ -25,3 +25,7 @@ safAccess: UPDATE: - user - USER + APIML.DEBUG: + CONTROL: + - user + - USER diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java index 072a4bd042..c586b856f2 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/WebSecurity.java @@ -31,6 +31,7 @@ import org.springframework.core.annotation.Order; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseCookie; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -44,11 +45,20 @@ import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.oauth2.client.*; +import org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.server.*; +import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.server.DefaultServerOAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; @@ -60,6 +70,7 @@ import org.springframework.security.web.server.firewall.StrictServerWebExchangeFirewall; import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter; import org.springframework.security.web.server.savedrequest.CookieServerRequestCache; +import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; import org.springframework.web.server.ServerWebExchange; @@ -77,6 +88,8 @@ import org.zowe.apiml.gateway.service.TokenProvider; import org.zowe.apiml.product.constants.CoreService; import org.zowe.apiml.security.HttpsConfig; +import org.zowe.apiml.security.common.auth.saf.SafAuthorizationManager; +import org.zowe.apiml.security.common.auth.saf.SafResourceAccessVerifying; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.security.common.config.CustomHstsServerHttpHeadersWriter; import org.zowe.apiml.security.common.config.SafSecurityConfigurationProperties; @@ -90,12 +103,19 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers; import static org.zowe.apiml.constants.ApimlConstants.DEFAULT_ALLOWED_DOMAINS; import static org.zowe.apiml.gateway.services.ServicesInfoController.SERVICES_FULL_URL; import static org.zowe.apiml.gateway.services.ServicesInfoController.SERVICES_SHORT_URL; @@ -108,6 +128,7 @@ @EnableConfigurationProperties(SafSecurityConfigurationProperties.class) public class WebSecurity { + private static final String APPLICATION = "/application/**"; public static final String CONTEXT_PATH = "/" + CoreService.GATEWAY.getServiceId(); public static final String REGISTRY_PATH = CONTEXT_PATH + "/api/v1/registry"; public static final String COOKIE_NONCE = "oidc_nonce"; @@ -419,7 +440,12 @@ SecurityWebFilterChain defaultSecurityWebFilterChain(ServerHttpSecurity http) { @Bean @Order(1) @ConditionalOnMissingBean(name = "modulithConfig") - SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfigurationProperties authConfigurationProperties, AuthExceptionHandlerReactive authExceptionHandlerReactive) { + SecurityWebFilterChain securityWebFilterChain( + ServerHttpSecurity http, + AuthConfigurationProperties authConfigurationProperties, + AuthExceptionHandlerReactive authExceptionHandlerReactive, + SafResourceAccessVerifying safResourceAccessVerifying + ) { return defaultSecurityConfig(http) .securityMatcher(ServerWebExchangeMatchers.pathMatchers( REGISTRY_PATH, @@ -428,7 +454,7 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfi SERVICES_SHORT_URL + "/**", SERVICES_FULL_URL, SERVICES_FULL_URL + "/**", - "/application/**" + APPLICATION )) .authorizeExchange(authorizeExchangeSpec -> { if (!isHealthEndpointProtected) { @@ -442,6 +468,16 @@ SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfi } } ) + .authorizeExchange(exchange -> exchange.matchers( + new OrServerWebExchangeMatcher( + pathMatchers(HttpMethod.GET, APPLICATION), + pathMatchers(HttpMethod.HEAD, APPLICATION) + )) + .authenticated() + ) + .authorizeExchange(exchange -> exchange.matchers(pathMatchers(APPLICATION)) + .access(new SafAuthorizationManager<>(safResourceAccessVerifying, "ZOWE", "APIML.DEBUG", "CONTROL")) + ) .authorizeExchange(authorizeExchangeSpec -> authorizeExchangeSpec .anyExchange().authenticated() diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index 9b6b95d125..5e6ad04b69 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -221,14 +221,28 @@ logging: management: endpoint: gateway: - access: unrestricted + access: read-only + loggers: + access: read-only endpoints: web: exposure: include: health,info,gateway,loggers +--- +spring.config.activate.on-profile: debug-control +spring.profiles.active: debug + +management: + endpoint: + gateway: + access: unrestricted + loggers: + access: unrestricted + --- spring.config.activate.on-profile: wiretap + spring: cloud: gateway: diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/GatewayServiceApplicationTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/GatewayServiceApplicationTest.java index a2864606c0..a5174ea7bb 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/GatewayServiceApplicationTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/GatewayServiceApplicationTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; import org.zowe.apiml.util.Recorder; import org.zowe.apiml.util.TestLogger; @@ -47,6 +48,7 @@ void stopRecording() { }, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT ) + @ActiveProfiles("test") class TomcatInitialization { @ParameterizedTest(name = "Check if {0} is initialized on startup") diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/ActuatorConfigTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/ActuatorConfigTest.java new file mode 100644 index 0000000000..2c99f54f9c --- /dev/null +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/ActuatorConfigTest.java @@ -0,0 +1,346 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml.gateway.acceptance; + +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.PlainJWT; +import io.restassured.RestAssured; +import io.restassured.config.HttpClientConfig; +import io.restassured.config.RestAssuredConfig; +import org.apache.http.NoHttpResponseException; +import org.apache.http.client.HttpRequestRetryHandler; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithBasePath; +import org.zowe.apiml.gateway.acceptance.common.MicroservicesAcceptanceTest; +import org.zowe.apiml.gateway.service.TokenProvider; +import org.zowe.apiml.security.common.token.QueryResponse; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.Date; + +import static io.restassured.RestAssured.given; +import static org.apache.hc.core5.http.HttpStatus.SC_FORBIDDEN; +import static org.apache.hc.core5.http.HttpStatus.SC_METHOD_NOT_ALLOWED; +import static org.apache.hc.core5.http.HttpStatus.SC_NOT_FOUND; +import static org.apache.hc.core5.http.HttpStatus.SC_NO_CONTENT; +import static org.apache.hc.core5.http.HttpStatus.SC_OK; +import static org.apache.hc.core5.http.HttpStatus.SC_UNAUTHORIZED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@MicroservicesAcceptanceTest +@TestPropertySource( + properties = { + "server.ssl.keyStore=../keystore/localhost/localhost.keystore.p12", + "server.ssl.trustStore=../keystore/localhost/localhost.truststore.p12", + "apiml.security.auth.provider=dummy" + } +) +@NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) +class ActuatorConfigTest { + + private static final String USER = "USER"; + + private static final String USER_NO_PERMISSION = "APIMTST"; + + private static final String AUTH_COOKIE = "apimlAuthenticationToken"; + + private abstract static class ActuatorAcceptanceTest extends AcceptanceTestWithBasePath { + + @MockitoBean + private TokenProvider tokenProvider; + private RestAssuredConfig config; + + @BeforeEach + void mockTokenValidation() { + when(tokenProvider.validateToken(any())).thenAnswer(invocation -> { + String jwt = invocation.getArgument(0); + String userId = JWTParser.parse(jwt).getJWTClaimsSet().getSubject(); + return Mono.just(new QueryResponse( + null, userId, new Date(), Date.from(Instant.now().plus(Duration.ofHours(1))), + QueryResponse.Source.ZOWE.value, Collections.emptyList(), QueryResponse.Source.ZOWE + )); + }); + } + + @BeforeEach + void retryOnDroppedConnection() { + HttpRequestRetryHandler retryHandler = (exception, executionCount, context) -> + exception instanceof NoHttpResponseException && executionCount < 3; + this.config = RestAssured.config; + RestAssured.config = RestAssured.config + .httpClient(HttpClientConfig.httpClientConfig().setParam("http.method.retry-handler", retryHandler)); + } + + @AfterEach + void restoreConfig() { + RestAssured.config = this.config; + } + + } + + private String login(String user) { + var now = Instant.now(); + var claims = new JWTClaimsSet.Builder() + .subject(user) + .issuer(QueryResponse.Source.ZOWE.value) + .issueTime(Date.from(now)) + .expirationTime(Date.from(now.plus(Duration.ofHours(1)))) + .build(); + return new PlainJWT(claims).serialize(); + } + + @Nested + @TestPropertySource( + properties = { + "server.ssl.keyStore=../keystore/localhost/localhost.keystore.p12", + "server.ssl.trustStore=../keystore/localhost/localhost.truststore.p12", + "apiml.security.auth.provider=dummy" + } + ) + @ActiveProfiles("default") + class GivenDefaultProfile extends ActuatorAcceptanceTest { + + @Autowired + private Environment environment; + + @BeforeEach + void setUp() { + var property = environment.getProperty("management.endpoints.web.exposure.include"); + assertEquals("health,info", property); + } + + @ParameterizedTest + @CsvSource({ + "/application/loggers", + "/application/gateway" + }) + void whenAccessDangerousActuatorWithoutCredentials_thenBlock(String endpoint) { + given() + .when() + .get(basePath + endpoint) + .then() + .statusCode(SC_UNAUTHORIZED); + } + + @ParameterizedTest + @CsvSource({ + "/application/loggers", + "/application/gateway" + }) + void whenAccessDangerousActuatorWithCredentials_thenBlock(String endpoint) { + given() + .cookie(AUTH_COOKIE, login(USER)) + .when() + .get(basePath + endpoint) + .then() + .log().ifValidationFails() + .statusCode(SC_NOT_FOUND); + } + + } + + @Nested + @ActiveProfiles("debug") + @TestPropertySource( + properties = { + "server.ssl.keyStore=../keystore/localhost/localhost.keystore.p12", + "server.ssl.trustStore=../keystore/localhost/localhost.truststore.p12", + "apiml.security.auth.provider=dummy" + } + ) + class GivenDebugProfile extends ActuatorAcceptanceTest { + + @Autowired + private Environment environment; + + @BeforeEach + void setUp() { + var exposure = environment.getProperty("management.endpoints.web.exposure.include"); + assertEquals("health,info,gateway,loggers", exposure); + var gatewayAccess = environment.getProperty("management.endpoint.gateway.access"); + assertEquals("read-only", gatewayAccess); + var loggersAccess = environment.getProperty("management.endpoint.loggers.access"); + assertEquals("read-only", loggersAccess); + } + + @ParameterizedTest + @CsvSource({ + "/application/loggers", + "/application/gateway" + }) + void whenAccessDangerousActuatorWithCredentials_thenBlockModify(String endpoint) { + var jwt = login(USER); + // change the level of the ROOT logger + given() + .cookie("apimlAuthenticationToken", jwt) + .contentType("application/json") + .body("{\"configuredLevel\":\"DEBUG\"}") + .when() + .post(basePath + "/application/loggers/ROOT") + .then() + .statusCode(SC_METHOD_NOT_ALLOWED); + + // refresh the gateway's routes + given() + .cookie("apimlAuthenticationToken", jwt) + .when() + .post(basePath + "/application/gateway/refresh") + .then() + .statusCode(SC_NOT_FOUND); + } + + @ParameterizedTest + @CsvSource({ + "/application/loggers", + "/application/gateway" + }) + void whenAccessDangerousActuatorWithoutCredentials_thenBlock(String endpoint) { + given() + .when() + .get(basePath + endpoint) + .then() + .statusCode(SC_UNAUTHORIZED); + } + + @ParameterizedTest + @CsvSource({ + "/application/loggers", + "/application/gateway/routes", + "/application/info" + }) + void whenAccessDangerousActuator_thenAllowRead(String endpoint) { + given() + .cookie(AUTH_COOKIE, login(USER)) + .when() + .get(basePath + endpoint) + .then() + .statusCode(SC_OK); + } + + } + + @Nested + @ActiveProfiles({"debug", "debug-control"}) + @TestPropertySource( + properties = { + "server.ssl.keyStore=../keystore/localhost/localhost.keystore.p12", + "server.ssl.trustStore=../keystore/localhost/localhost.truststore.p12", + "apiml.security.auth.provider=dummy" + } + ) + class GivenDebugControlProfile extends ActuatorAcceptanceTest { + + @Autowired + private Environment environment; + + @BeforeEach + void setUp() { + var exposure = environment.getProperty("management.endpoints.web.exposure.include"); + assertEquals("health,info,gateway,loggers", exposure); + var gatewayAccess = environment.getProperty("management.endpoint.gateway.access"); + assertEquals("unrestricted", gatewayAccess); + var loggersAccess = environment.getProperty("management.endpoint.loggers.access"); + assertEquals("unrestricted", loggersAccess); + } + + @Test + void whenAccessDangerousActuatorWithCredentialsWithPermission_thenAllowModify() { + var jwt = login(USER); + // change the level of the ROOT logger + given() + .cookie("apimlAuthenticationToken", jwt) + .contentType("application/json") + .body("{\"configuredLevel\":\"DEBUG\"}") + .when() + .post(basePath + "/application/loggers/ROOT") + .then() + .statusCode(SC_NO_CONTENT); + + // refresh the gateway's routes + given() + .cookie("apimlAuthenticationToken", jwt) + .when() + .post(basePath + "/application/gateway/refresh") + .then() + .statusCode(SC_OK); + } + + @Test + void whenAccessDangerousActuatorWithCredentialsWithoutPermission_thenBlock() { + var jwt = login(USER_NO_PERMISSION); + String endpoint = "/application/loggers"; + // update a logger level + given() + .cookie("apimlAuthenticationToken", jwt) + .when() + .post(basePath + endpoint) + .then() + .statusCode(SC_FORBIDDEN); + + endpoint = "/application/gateway"; + // update routes + given() + .cookie("apimlAuthenticationToken", jwt) + .when() + .post(basePath + endpoint) + .then() + .statusCode(SC_FORBIDDEN); + } + + @ParameterizedTest + @CsvSource({ + "/application/loggers", + "/application/gateway" + }) + void whenAccessDangerousActuatorWithoutCredentials_thenBlock(String endpoint) { + given() + .when() + .get(basePath + endpoint) + .then() + .statusCode(SC_UNAUTHORIZED); + } + + @ParameterizedTest + @CsvSource({ + "/application/loggers", + "/application/gateway/routes" + }) + void whenAccessDangerousActuator_thenAllowRead(String endpoint) { + var jwt = login(USER_NO_PERMISSION); + given() + .cookie(AUTH_COOKIE, jwt) + .when() + .get(basePath + endpoint) + .then() + .statusCode(SC_OK); + } + + } + +} diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/AttlsConfigTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/AttlsConfigTest.java index e35102e28f..a4cdc2f4ba 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/AttlsConfigTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/AttlsConfigTest.java @@ -59,7 +59,7 @@ private String getGatewayUrlWithPath(String hostname, int port, String scheme, S } @Nested - @ActiveProfiles({ "attlsServer", "attlsClient" }) + @ActiveProfiles({ "test", "attlsServer", "attlsClient" }) @DirtiesContext @SpringBootTest( classes = GatewayServiceApplication.class, @@ -133,7 +133,7 @@ void requestFailsWithAttlsReasonWithHttp() { "server.ssl.keyStore=" } ) - @ActiveProfiles({ "attlsServer", "attlsClient" }) + @ActiveProfiles({ "test", "attlsServer", "attlsClient" }) @DirtiesContext @SpringBootTest( classes = GatewayServiceApplication.class, diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/common/MicroservicesAcceptanceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/common/MicroservicesAcceptanceTest.java index 6d66f09c62..8430f0f13b 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/common/MicroservicesAcceptanceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/common/MicroservicesAcceptanceTest.java @@ -14,6 +14,7 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Import; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; import org.zowe.apiml.gateway.GatewayServiceApplication; import org.zowe.apiml.gateway.acceptance.config.DiscoveryClientTestConfig; @@ -34,5 +35,6 @@ ) @Import(DiscoveryClientTestConfig.class) @DirtiesContext +@ActiveProfiles("test") public @interface MicroservicesAcceptanceTest { } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java index bec5d51354..e406d82872 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ConnectionsConfigTest.java @@ -32,6 +32,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.server.reactive.SslInfo; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.util.ReflectionTestUtils; @@ -85,6 +86,7 @@ class ConnectionsConfigTest { @Nested @SpringBootTest @ComponentScan(basePackages = "org.zowe.apiml.gateway") + @ActiveProfiles("test") class WhenCreateEurekaJerseyClientBuilder { @Autowired @@ -100,6 +102,7 @@ void thenIsNotNull() { @Nested @SpringBootTest @ComponentScan(basePackages = "org.zowe.apiml.gateway") + @ActiveProfiles("test") class WhenInitializeEurekaClient { @Autowired @@ -127,6 +130,7 @@ void thenCreateIt() { properties = {"management.port=-1"}, classes = {GatewayServiceApplication.class, ConnectionsConfigTest.SslDetectorConfig.class} ) + @ActiveProfiles("test") class ChooseAlias { @LocalServerPort @@ -424,6 +428,7 @@ WebFilter sslDetector() { properties = {"apiml.service.corsEnabled=true"} ) @ComponentScan(basePackages = "org.zowe.apiml.gateway") + @ActiveProfiles("test") class GivenCorsEnabled { @Nested diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ProtectedHealthEndpointTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ProtectedHealthEndpointTest.java index bb10f04596..b6ad70c439 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ProtectedHealthEndpointTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/config/ProtectedHealthEndpointTest.java @@ -18,15 +18,22 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; import org.zowe.apiml.gateway.GatewayServiceApplication; import org.zowe.apiml.gateway.acceptance.config.DiscoveryClientTestConfig; import static io.restassured.RestAssured.given; import static org.hamcrest.core.Is.is; -@SpringBootTest(classes = GatewayServiceApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - properties = {"apiml.health.protected=false"}) +@SpringBootTest( + classes = GatewayServiceApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "apiml.health.protected=false" + } +) @Import(DiscoveryClientTestConfig.class) +@ActiveProfiles("test") public class ProtectedHealthEndpointTest { @Value("${apiml.service.hostname:localhost}") @@ -52,4 +59,5 @@ void givenNoCredentials_thenReturnServiceUnavailable() { .then() .statusCode(is(HttpStatus.SC_SERVICE_UNAVAILABLE)); } + } diff --git a/gateway-service/src/test/resources/application-test.yml b/gateway-service/src/test/resources/application-test.yml index 17386e4de2..ff15b11d0c 100644 --- a/gateway-service/src/test/resources/application-test.yml +++ b/gateway-service/src/test/resources/application-test.yml @@ -1,9 +1,3 @@ -eureka: - client: - registerWithEureka: false - serviceUrl: - defaultZone: https://localhost:999/eureka/ - apiml: service: id: gateway @@ -13,13 +7,11 @@ server: port: ${apiml.service.port} ssl: keyAlias: localhost - keyPassword: password keyStore: ../keystore/localhost/localhost.keystore.p12 - keyStorePassword: password - keyStoreType: PKCS12 trustStore: ../keystore/localhost/localhost.truststore.p12 - trustStorePassword: password - trustStoreType: PKCS12 + clientKeyStore: ../keystore/client_cert/client-certs.p12 + clientCN: apimtst # case-sensitive + clientKeyStorePassword: password spring: main: @@ -40,13 +32,7 @@ logging: org.springframework.cloud.gateway: DEBUG reactor.netty: DEBUG +test: + proxyAddress: 6.6.6.6 + trustedProxiesPattern: 6\.6\.6\.6 -management: - endpoint: - gateway: - access: unrestricted - endpoints: - web: - base-path: /application - exposure: - include: health,gateway diff --git a/gateway-service/src/test/resources/application.yml b/gateway-service/src/test/resources/application.yml deleted file mode 100644 index 8c78496ced..0000000000 --- a/gateway-service/src/test/resources/application.yml +++ /dev/null @@ -1,121 +0,0 @@ -eureka: - client: - registerWithEureka: false - serviceUrl: - defaultZone: https://localhost:999/eureka/ - -apiml: - service: - id: gateway - port: 10010 - hostname: localhost - scheme: https - corsEnabled: true - ignoredHeadersWhenCorsEnabled: Access-Control-Request-Method,Access-Control-Request-Headers,Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Credentials,Origin - gateway: - serviceRegistryEnabled: false - forwardClientCertEnabled: false - webClientConfig.enabled: true - security: - allowedDomains: localhost,localhost2 - -server: - port: ${apiml.service.port} - ssl: - clientAuth: want - keyAlias: localhost - keyPassword: password - keyStore: ../keystore/localhost/localhost.keystore.p12 - keyStorePassword: password - keyStoreType: PKCS12 - trustStore: ../keystore/localhost/localhost.truststore.p12 - trustStorePassword: password - trustStoreType: PKCS12 - # Custom properties for tests only - clientKeyStore: ../keystore/client_cert/client-certs.p12 - clientCN: apimtst # case-sensitive - clientKeyStorePassword: password - max-http-request-header-size: 48000 - -#For testing forwarded headers with (un)trusted proxies -test: - proxyAddress: 6.6.6.6 - trustedProxiesPattern: 6\.6\.6\.6 - - -spring: - main: - allow-bean-definition-overriding: true - allow-circular-references: true - cloud: - gateway: - server: - webflux: - discovery: - locator: - enabled: false - lower-case-service-id: true - application: - name: gateway - -logging: - level: - ROOT: DEBUG - # org.apache.coyote: TRACE - org.apache.tomcat.websocket: DEBUG - org.springframework: DEBUG - org.springframework.beans: WARN - org.springframework.cloud.gateway: DEBUG - org.springframework.http.server.reactive: DEBUG - org.springframework.security: DEBUG - org.springframework.web: DEBUG - org.springframework.web.reactive: DEBUG -otel: - sdk: - disabled: true - -management: - endpoint: - gateway: - access: unrestricted - endpoints: - web: - base-path: /application - exposure: - include: health,gateway - ---- -spring.config.activate.on-profile: attlsServer - -server: - attlsServer: - enabled: true - ssl: - enabled: false - - service: - scheme: http -apiml: - service: - corsEnabled: true - scheme: http - nonSecurePortEnabled: true - securePortEnabled: false - ---- -spring.config.activate.on-profile: attlsClient - -server: - attlsClient: - enabled: true - -apiml: - service: - discoveryServiceUrls: http://localhost:10011/eureka/ - -eureka: - instance: - metadata-map: - apiml: - apiInfo[0]: - swaggerUrl: http://${apiml.service.hostname}:${apiml.service.port}/gateway/api-docs diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/requests/GatewayRequests.java b/integration-tests/src/test/java/org/zowe/apiml/util/requests/GatewayRequests.java index a4e44b6e58..4fcbabcad3 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/requests/GatewayRequests.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/requests/GatewayRequests.java @@ -12,6 +12,7 @@ import com.jayway.jsonpath.ReadContext; import io.restassured.RestAssured; +import io.restassured.config.RestAssuredConfig; import lombok.extern.slf4j.Slf4j; import org.apache.http.client.utils.URIBuilder; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; @@ -64,6 +65,7 @@ public void shutdown() { try { given() + .config(RestAssuredConfig.newConfig()) .contentType(JSON) .auth().basic(credentials.getUser(), new String(credentials.getPassword())) .when() diff --git a/zaas-service/saf.yml b/zaas-service/saf.yml index 3b79b40403..09db4a28df 100644 --- a/zaas-service/saf.yml +++ b/zaas-service/saf.yml @@ -25,3 +25,6 @@ safAccess: UPDATE: - user - USER + APIML.DEBUG: + CONTROL: + - USER diff --git a/zaas-service/src/main/resources/mock-saf.yml b/zaas-service/src/main/resources/mock-saf.yml index 4e9ec34e93..fdc8b08a0e 100644 --- a/zaas-service/src/main/resources/mock-saf.yml +++ b/zaas-service/src/main/resources/mock-saf.yml @@ -10,3 +10,7 @@ # # This file could be replaced with `saf.yml` located in root folder of application (outside the JAR file), which replaces this file. safAccess: + ZOWE: + APIML.DEBUG: + CONTROL: + - USER