Skip to content
27 changes: 18 additions & 9 deletions apiml/src/main/java/org/zowe/apiml/WebSecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
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.filter.CategorizeCertsWebFilter;
import org.zowe.apiml.security.common.token.OIDCProvider;
import org.zowe.apiml.security.common.util.X509Util;
Expand Down Expand Up @@ -81,6 +82,9 @@ public class WebSecurityConfig {
private static final String APPLICATION = "/application/**";
private static final String APPLICATION_HEALTH = "/application/health";
private static final String APPLICATION_INFO = "/application/info";
private static final String APPLICATION_VERSION = "/application/version";
private static final String APPLICATION_GW_VERSION = "/gateway/version";
private static final String APPLICATION_GW_VERSION_ROUTE = "/gateway/api/v1/version";

private final CompoundAuthProvider compoundAuthProvider;
private final X509AuthenticationProvider x509AuthenticationProvider;
Expand Down Expand Up @@ -122,14 +126,12 @@ public class WebSecurityConfig {
private boolean isOidcEnabled;

private static final List<String> UNAUTHENTICATED_PATTERNS = List.of(
"/application/",
"/application/version",
"/eureka/css/**",
"/eureka/js/**",
"/eureka/fonts/**",
"/eureka/images/**",
APPLICATION_INFO,
"/favicon.ico");
"/favicon.ico"
);

private final ServerWebExchangeMatcher discoveryPortMatcher = exchange -> exchange.getRequest().getURI().getPort() == internalDiscoveryPort ? MatchResult.match() : MatchResult.notMatch();
private final ServerWebExchangeMatcher isInUnauthenticatedPaths = pathMatchers(UNAUTHENTICATED_PATTERNS.toArray(new String[]{}));
Expand Down Expand Up @@ -272,6 +274,10 @@ private ServerHttpSecurity addOidcFilterIfEnabled(ServerHttpSecurity http, AuthC
@Order(9)
SecurityWebFilterChain discoveryAllowedEndpoints(ServerHttpSecurity http) {
http
.headers(headers -> headers
.hsts(ServerHttpSecurity.HeaderSpec.HstsSpec::disable)
.writer(new CustomHstsServerHttpHeadersWriter())
)
.securityMatcher(new AndServerWebExchangeMatcher(
discoveryPortMatcher,
isInUnauthenticatedPaths
Expand Down Expand Up @@ -308,11 +314,15 @@ SecurityWebFilterChain healthEndpointFilterChain(ServerHttpSecurity http,
AuthConfigurationProperties authConfigurationProperties,
AuthExceptionHandlerReactive authExceptionHandlerReactive) {
http
.securityMatcher(pathMatchers(APPLICATION_HEALTH))
.headers(headers -> headers
.hsts(ServerHttpSecurity.HeaderSpec.HstsSpec::disable)
.writer(new CustomHstsServerHttpHeadersWriter())
)
.securityMatcher(pathMatchers(APPLICATION_HEALTH, APPLICATION_GW_VERSION, APPLICATION_GW_VERSION_ROUTE, APPLICATION_INFO, APPLICATION_VERSION))
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(exchange -> {
if (!isHealthEndpointProtected) {
exchange.pathMatchers(APPLICATION_HEALTH).permitAll();
exchange.anyExchange().permitAll();
} else {
exchange.anyExchange().authenticated();
}
Expand All @@ -332,7 +342,7 @@ SafAuthorizationManager<AuthorizationContext> actuatorAuthorizationManager(SafRe

/**
* Security filter chain that protects all endpoints under the path "/application/**",
* except for "/application/health" - which is handled separately based on the configuration - and "/application/info".
* except for "/application/health" - which is handled separately based on the configuration.
* <p>
* This chain requires that all incoming requests to the matched paths are authenticated,
* either via Basic Authentication or Bearer JWT token.
Expand All @@ -357,7 +367,7 @@ SecurityWebFilterChain applicationEndpointsProtected(ServerHttpSecurity http,
return http
.securityMatcher(new AndServerWebExchangeMatcher(
pathMatchers(APPLICATION),
new NegatedServerWebExchangeMatcher(pathMatchers(APPLICATION_HEALTH, APPLICATION_INFO, "/application/version"))
new NegatedServerWebExchangeMatcher(pathMatchers(APPLICATION_HEALTH, APPLICATION_INFO, APPLICATION_VERSION))
))
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
Expand All @@ -374,7 +384,6 @@ SecurityWebFilterChain applicationEndpointsProtected(ServerHttpSecurity http,
.addFilterAfter(new TokenAuthFilter(localTokenProvider, authConfigurationProperties, authExceptionHandlerReactive), SecurityWebFiltersOrder.AUTHENTICATION)
.addFilterAfter(new BasicLoginFilter(compoundAuthProvider, failedAuthenticationWebHandler), SecurityWebFiltersOrder.AUTHENTICATION)
.build();

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,12 @@ void setUp() {
@ParameterizedTest
@CsvSource({
"/application/loggers",
"/application/gateway"
"/application/gateway",
"/application/version",
"/application/health",
"/application/info",
"/gateway/version",
"/gateway/api/v1/version"
})
void whenAccessDangerousActuatorWithoutCredentials_thenBlock(String endpoint) {
given()
Expand All @@ -136,6 +141,21 @@ void whenAccessDangerousActuatorWithoutCredentials_thenBlock(String endpoint) {
.statusCode(SC_UNAUTHORIZED);
}

@ParameterizedTest
@CsvSource({
"/application/version",
"/application/health",
"/application/info"
})
void whenAccessInfoActuatorWithCredentials_thenAllow(String endpoint) {
given()
.cookie(AUTH_COOKIE, login(USER))
.when()
.get(basePath + endpoint)
.then()
.statusCode(SC_OK);
}

@ParameterizedTest
@CsvSource({
"/application/loggers",
Expand All @@ -161,10 +181,10 @@ void whenAccessDangerousActuatorWithCredentials_thenBlock(String endpoint) {
"server.ssl.trustStore=../keystore/service/service.truststore.p12",
"apiml.security.auth.provider=dummy",
"logging.level.reactor.netty=ERROR",
"org.springframework.http.server.reactive=DEBUG",
"org.springframework.security=DEBUG",
"org.springframework.web.reactive=DEBUG",
"org.springframework.web.reactive.socket=DEBUG"
"logging.level.org.springframework.http.server.reactive=DEBUG",
"logging.level.org.springframework.security=DEBUG",
"logging.level.org.springframework.web.reactive=DEBUG",
"logging.level.org.springframework.web.reactive.socket=DEBUG"
}
)
@DirtiesContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ private String getGatewayUrlWithPath(String hostname, int port, String scheme, S
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@TestInstance(Lifecycle.PER_CLASS)
@TestPropertySource(
properties = {
"apiml.health.protected=false"
}
)
class GivenAttlsProfile {

@LocalServerPort
Expand Down Expand Up @@ -157,6 +162,7 @@ void requestFailsWithAttlsReasonWithHttp() {
@Nested
@TestPropertySource(
properties = {
"apiml.health.protected=false",
"server.ssl.keyStoreType=",
"server.ssl.keyStorePassword=",
"server.ssl.keyPassword=",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.web.server.i18n.FixedLocaleContextResolver;
import org.springframework.web.server.i18n.LocaleContextResolver;

Expand All @@ -32,12 +33,17 @@
@AcceptanceTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ActiveProfiles({ "test", "ApimlModulithAcceptanceTest", "AvailabilityTest" })
@TestPropertySource(
properties = {
"apiml.health.protected=false",
}
)
class AvailabilityTest extends AcceptanceTestWithBasePath {

@ParameterizedTest(name = "{0} is available at port {1} with status {2}")
@CsvSource({
"Gateway, 0, 200",
"Discovery, 10011, 401"
"Discovery, 10011, 200"
})
void serviceIsAvailable(String serviceName, int servicePort, int expectedStatus) {
int actualPort = servicePort == 0 ? port : servicePort;
Expand All @@ -47,7 +53,7 @@ void serviceIsAvailable(String serviceName, int servicePort, int expectedStatus)
.untilAsserted(() ->
given()
.when()
.get("https://localhost:" + actualPort)
.get("https://localhost:" + actualPort + "/application/info")
.then()
.statusCode(expectedStatus)
.header("Strict-Transport-Security", notNullValue())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class GatewayCorsTest {
@Nested
@AcceptanceTest
@TestPropertySource(properties = {
"apiml.health.protected=false",
"apiml.service.corsDefaultAllowedOrigins=https://foo.bar.org",
"apiml.service.corsEnabled=true"
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,16 @@
@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";
public static final String COOKIE_STATE = "oidc_state";
public static final String COOKIE_RETURN_URL = "oidc_return_url";

private static final String APPLICATION = "/application/**";
private static final String APPLICATION_VERSION_GW = "/gateway/version";
private static final String APPLICATION_VERSION_GW_ROUTE = "/gateway/api/v1/version";

private static final Pattern CLIENT_REG_ID = Pattern.compile("^" + CONTEXT_PATH + "/login/oauth2/code/([^/]+)$");
private static final Predicate<HttpCookie> HAS_NO_VALUE = cookie -> cookie == null || StringUtils.isEmpty(cookie.getValue());
private static final List<String> COOKIES = Arrays.asList(COOKIE_NONCE, COOKIE_STATE, COOKIE_RETURN_URL);
Expand Down Expand Up @@ -460,16 +464,14 @@ SecurityWebFilterChain securityWebFilterChain(
SERVICES_SHORT_URL + "/**",
SERVICES_FULL_URL,
SERVICES_FULL_URL + "/**",
APPLICATION
APPLICATION,
APPLICATION_VERSION_GW,
APPLICATION_VERSION_GW_ROUTE
))
.authorizeExchange(authorizeExchangeSpec -> {
if (!isHealthEndpointProtected) {
authorizeExchangeSpec
.pathMatchers("/application/info", "/application/version", "/application/health")
.permitAll();
} else {
authorizeExchangeSpec
.pathMatchers("/application/info", "/application/version")
.pathMatchers("/application/info", "/application/version", "/application/health", APPLICATION_VERSION_GW, APPLICATION_VERSION_GW_ROUTE)
.permitAll();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,12 @@ void setUp() {
@ParameterizedTest
@CsvSource({
"/application/loggers",
"/application/gateway"
"/application/gateway",
"/application/version",
"/application/health",
"/application/info",
"/gateway/version",
"/gateway/api/v1/version"
})
void whenAccessDangerousActuatorWithoutCredentials_thenBlock(String endpoint) {
given()
Expand All @@ -127,6 +132,21 @@ void whenAccessDangerousActuatorWithoutCredentials_thenBlock(String endpoint) {
.statusCode(SC_UNAUTHORIZED);
}

@ParameterizedTest
@CsvSource({
"/application/version",
"/application/health",
"/application/info"
})
void whenAccessInfoActuatorWithCredentials_thenAllow(String endpoint) {
given()
.cookie(AUTH_COOKIE, login(USER))
.when()
.get(basePath + "/application/info")
.then()
.statusCode(SC_OK);
}

@ParameterizedTest
@CsvSource({
"/application/loggers",
Expand Down Expand Up @@ -241,10 +261,10 @@ void whenAccessDangerousActuator_thenAllowRead(String endpoint) {
"server.ssl.trustStore=../keystore/service/service.truststore.p12",
"apiml.security.auth.provider=dummy",
"logging.level.reactor.netty=ERROR",
"org.springframework.http.server.reactive=DEBUG",
"org.springframework.security=DEBUG",
"org.springframework.web.reactive=DEBUG",
"org.springframework.web.reactive.socket=DEBUG"
"logging.level.org.springframework.http.server.reactive=DEBUG",
"logging.level.org.springframework.security=DEBUG",
"logging.level.org.springframework.web.reactive=DEBUG",
"logging.level.org.springframework.web.reactive.socket=DEBUG"
}
)
@MicroservicesAcceptanceTest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ void requestFailsWithAttlsReasonWithHttp() {
@Nested
@TestPropertySource(
properties = {
"apiml.health.protected=false",
"server.ssl.keyStoreType=",
"server.ssl.keyStorePassword=",
"server.ssl.keyPassword=",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.test.context.TestPropertySource;
import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithBasePath;
import org.zowe.apiml.gateway.acceptance.common.MicroservicesAcceptanceTest;

Expand All @@ -25,6 +26,11 @@
*/
@MicroservicesAcceptanceTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestPropertySource(
properties = {
"apiml.health.protected=false"
}
)
class RequestLimitTest extends AcceptanceTestWithBasePath {

private static final String HEADER_10KB = StringUtils.repeat("1", 10_000);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ class GatewayCorsTest {
@ActiveProfiles({"GatewayCorsEnabledWithProvidedDefaultTest"})
@TestPropertySource(properties = {
"apiml.service.corsDefaultAllowedOrigins=https://foo.bar.org",
"apiml.service.corsEnabled=true"
"apiml.service.corsEnabled=true",
"apiml.health.protected=false"
})
class GatewayCorsEnabledWithProvidedDefaultTest extends AcceptanceTestWithMockServices {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.zowe.apiml.util.config.ConfigReader;
import org.zowe.apiml.util.config.Credentials;
import org.zowe.apiml.util.http.HttpRequestUtils;

import static io.restassured.RestAssured.given;
import static org.apache.http.HttpStatus.SC_OK;
import static org.apache.http.HttpStatus.SC_UNAUTHORIZED;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
Expand Down Expand Up @@ -49,15 +52,41 @@ class WhenRequestingVersion {
void returnValidVersion(String endpoint) {
// Gateway request to url
given()
.when()
.when()
.get(HttpRequestUtils.getUriFromGateway(endpoint))
.then()
.then()
.statusCode(SC_UNAUTHORIZED);
}

}

}

@Nested
class GivenAuthentication {

private static final Credentials CREDENTIALS = ConfigReader.environmentConfiguration().getCredentials();

@Nested
class WhenRequestingVersion {

@ParameterizedTest(name = "ReturnValidVersion {index} {0} ")
@MethodSource("org.zowe.apiml.functional.gateway.VersionTest#versionUrls")
void returnValidVersion(String endpoint) {
// Gateway request to url
given()
.auth().basic(CREDENTIALS.getUser(), CREDENTIALS.getPassword())
.when()
.get(HttpRequestUtils.getUriFromGateway(endpoint))
.then()
.statusCode(SC_OK)
.body("apiml.version", is(not(nullValue())))
.body("apiml.buildNumber", is(not(nullValue())))
.body("apiml.commitHash", is(not(nullValue())));
}

}

}
}

}
Loading
Loading