-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Cloud Gateway SAF IDT auth scheme (#3234)
* chore: move babel to dev, modify webpack config, spring security Signed-off-by: achmelo <[email protected]> * read keys from common httpsconfig Signed-off-by: achmelo <[email protected]> * New ZAAS safIdt endpoint to generate SAF ID tokens for authenticated user. Signed-off-by: Petr Weinfurt <[email protected]> * IT tests for safIdt endpoint. Signed-off-by: Petr Weinfurt <[email protected]> * initial safidt filter impl Signed-off-by: achmelo <[email protected]> * Address code review comments. Signed-off-by: Petr Weinfurt <[email protected]> * Remove unused class Signed-off-by: Petr Weinfurt <[email protected]> * Fix tests Signed-off-by: Petr Weinfurt <[email protected]> * Fix IT tests Signed-off-by: Petr Weinfurt <[email protected]> * Fix unit tests Signed-off-by: Petr Weinfurt <[email protected]> * Add Controller Advice to handle exceptions. Signed-off-by: Petr Weinfurt <[email protected]> * refactor, unit tests Signed-off-by: achmelo <[email protected]> * Handle more exceptions. Signed-off-by: Petr Weinfurt <[email protected]> * safidt service in test Signed-off-by: achmelo <[email protected]> * Add content type and body to negative test. Signed-off-by: Petr Weinfurt <[email protected]> * Add content type and body to negative test. Signed-off-by: Petr Weinfurt <[email protected]> * Add content type and body to negative test. Signed-off-by: Petr Weinfurt <[email protected]> * Add negative tests with valid Okta token and no mapping. Signed-off-by: Petr Weinfurt <[email protected]> * Fix Rest assured RequestSpec preparation. Signed-off-by: Petr Weinfurt <[email protected]> * Checkout the main branch before Sonar scan to resolve issue 'Could not find ref 'v2.x.x' in refs/heads, refs/remotes/upstream or refs/remotes/origin.' Signed-off-by: Petr Weinfurt <[email protected]> * Fetch the main branch before Sonar scan to resolve issue 'Could not find ref 'v2.x.x' in refs/heads, refs/remotes/upstream or refs/remotes/origin.' Signed-off-by: Petr Weinfurt <[email protected]> * Replace deprecated sonar.login property Signed-off-by: Petr Weinfurt <[email protected]> * Replace deprecated sonar.login property Signed-off-by: Petr Weinfurt <[email protected]> * remove git fetch Signed-off-by: Petr Weinfurt <[email protected]> * Fetch depth 0 Signed-off-by: Petr Weinfurt <[email protected]> * Fetch depth 0 Signed-off-by: Petr Weinfurt <[email protected]> * Add SAF IDT request to negative IT tests Signed-off-by: Petr Weinfurt <[email protected]> * Handle TokenNotValid and TokenExpired exception with 401 response. Signed-off-by: Petr Weinfurt <[email protected]> * Handle TokenNotValid and TokenExpired exception with 401 response. Signed-off-by: Petr Weinfurt <[email protected]> * remove duplicated code Signed-off-by: achmelo <[email protected]> * delete unused imports Signed-off-by: achmelo <[email protected]> --------- Signed-off-by: achmelo <[email protected]> Signed-off-by: Petr Weinfurt <[email protected]> Co-authored-by: Petr Weinfurt <[email protected]>
- Loading branch information
Showing
9 changed files
with
336 additions
and
42 deletions.
There are no files selected for viewing
66 changes: 66 additions & 0 deletions
66
...java/org/zowe/apiml/cloudgatewayservice/filters/AbstractRequestBodyAuthSchemeFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
/* | ||
* 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.cloudgatewayservice.filters; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.fasterxml.jackson.databind.ObjectWriter; | ||
import lombok.Data; | ||
import lombok.EqualsAndHashCode; | ||
import org.apache.http.HttpHeaders; | ||
import org.springframework.cloud.client.ServiceInstance; | ||
import org.springframework.cloud.gateway.filter.GatewayFilter; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.http.server.reactive.ServerHttpRequest; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.web.reactive.function.client.WebClient; | ||
import org.zowe.apiml.cloudgatewayservice.service.InstanceInfoService; | ||
import org.zowe.apiml.message.core.MessageService; | ||
import org.zowe.apiml.ticket.TicketRequest; | ||
|
||
@Service | ||
public abstract class AbstractRequestBodyAuthSchemeFactory<R> extends AbstractAuthSchemeFactory<AbstractRequestBodyAuthSchemeFactory.Config, R, String> { | ||
private static final ObjectWriter WRITER = new ObjectMapper().writer(); | ||
|
||
public AbstractRequestBodyAuthSchemeFactory(WebClient webClient, InstanceInfoService instanceInfoService, MessageService messageService) { | ||
super(Config.class, webClient, instanceInfoService, messageService); | ||
} | ||
|
||
public abstract String getEndpointUrl(ServiceInstance instance); | ||
|
||
@Override | ||
protected WebClient.RequestHeadersSpec<?> createRequest(ServiceInstance instance, String requestBody) { | ||
String url = getEndpointUrl(instance); | ||
return webClient.post() | ||
.uri(url).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) | ||
.bodyValue(requestBody); | ||
} | ||
|
||
@Override | ||
public GatewayFilter apply(Config config) { | ||
try { | ||
return createGatewayFilter(config, WRITER.writeValueAsString(new TicketRequest(config.getApplicationName()))); | ||
} catch (JsonProcessingException e) { | ||
return ((exchange, chain) -> { | ||
ServerHttpRequest request = updateHeadersForError(exchange, e.getMessage()); | ||
return chain.filter(exchange.mutate().request(request).build()); | ||
}); | ||
} | ||
} | ||
|
||
@Data | ||
@EqualsAndHashCode(callSuper = true) | ||
public static class Config extends AbstractAuthSchemeFactory.AbstractConfig { | ||
|
||
private String applicationName; | ||
|
||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
63 changes: 63 additions & 0 deletions
63
...service/src/main/java/org/zowe/apiml/cloudgatewayservice/filters/SafIdtFilterFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/* | ||
* 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.cloudgatewayservice.filters; | ||
|
||
import org.springframework.beans.factory.annotation.Qualifier; | ||
import org.springframework.cloud.client.ServiceInstance; | ||
import org.springframework.cloud.gateway.filter.GatewayFilterChain; | ||
import org.springframework.http.server.reactive.ServerHttpRequest; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.web.reactive.function.client.WebClient; | ||
import org.springframework.web.server.ServerWebExchange; | ||
import org.zowe.apiml.cloudgatewayservice.service.InstanceInfoService; | ||
import org.zowe.apiml.constants.ApimlConstants; | ||
import org.zowe.apiml.message.core.MessageService; | ||
import org.zowe.apiml.zaas.ZaasTokenResponse; | ||
import reactor.core.publisher.Mono; | ||
|
||
@Service | ||
public class SafIdtFilterFactory extends AbstractRequestBodyAuthSchemeFactory<ZaasTokenResponse> { | ||
|
||
public SafIdtFilterFactory(@Qualifier("webClientClientCert") WebClient webClient, InstanceInfoService instanceInfoService, MessageService messageService) { | ||
super(webClient, instanceInfoService, messageService); | ||
} | ||
|
||
@Override | ||
public String getEndpointUrl(ServiceInstance instance) { | ||
return String.format("%s://%s:%d/%s/zaas/safIdt", instance.getScheme(), instance.getHost(), instance.getPort(), instance.getServiceId().toLowerCase()); | ||
} | ||
|
||
@Override | ||
protected Mono<Void> processResponse(ServerWebExchange exchange, GatewayFilterChain chain, ZaasTokenResponse response) { | ||
ServerHttpRequest request; | ||
if (response.getToken() != null) { | ||
request = exchange.getRequest().mutate().headers(headers -> | ||
headers.add(ApimlConstants.SAF_TOKEN_HEADER, response.getToken()) | ||
).build(); | ||
} else { | ||
request = updateHeadersForError(exchange, "Invalid or missing authentication"); | ||
} | ||
|
||
exchange = exchange.mutate().request(request).build(); | ||
return chain.filter(exchange); | ||
} | ||
|
||
@Override | ||
protected Class<ZaasTokenResponse> getResponseClass() { | ||
return ZaasTokenResponse.class; | ||
} | ||
|
||
@Override | ||
protected ZaasTokenResponse getResponseFor401() { | ||
return new ZaasTokenResponse(); | ||
} | ||
|
||
} |
37 changes: 37 additions & 0 deletions
37
...teway-service/src/main/java/org/zowe/apiml/cloudgatewayservice/service/scheme/SafIdt.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
/* | ||
* 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.cloudgatewayservice.service.scheme; | ||
|
||
import org.apache.commons.lang3.StringUtils; | ||
import org.springframework.cloud.client.ServiceInstance; | ||
import org.springframework.cloud.gateway.filter.FilterDefinition; | ||
import org.springframework.cloud.gateway.route.RouteDefinition; | ||
import org.springframework.stereotype.Component; | ||
import org.zowe.apiml.auth.Authentication; | ||
import org.zowe.apiml.auth.AuthenticationScheme; | ||
|
||
@Component | ||
public class SafIdt implements SchemeHandler { | ||
|
||
@Override | ||
public AuthenticationScheme getAuthenticationScheme() { | ||
return AuthenticationScheme.SAF_IDT; | ||
} | ||
|
||
@Override | ||
public void apply(ServiceInstance serviceInstance, RouteDefinition routeDefinition, Authentication auth) { | ||
FilterDefinition filterDef = new FilterDefinition(); | ||
filterDef.setName("SafIdtFilterFactory"); | ||
filterDef.addArg("applicationName", auth.getApplid()); | ||
filterDef.addArg("serviceId", StringUtils.lowerCase(serviceInstance.getServiceId())); | ||
routeDefinition.getFilters().add(filterDef); | ||
} | ||
} |
135 changes: 135 additions & 0 deletions
135
...service/src/test/java/org/zowe/apiml/cloudgatewayservice/acceptance/SafIdtSchemeTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/* | ||
* 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.cloudgatewayservice.acceptance; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.fasterxml.jackson.databind.ObjectWriter; | ||
import com.sun.net.httpserver.HttpExchange; | ||
import org.apache.commons.io.IOUtils; | ||
import org.hamcrest.Matchers; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Nested; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.TestInstance; | ||
import org.springframework.http.HttpHeaders; | ||
import org.zowe.apiml.auth.AuthenticationScheme; | ||
import org.zowe.apiml.cloudgatewayservice.acceptance.common.AcceptanceTest; | ||
import org.zowe.apiml.cloudgatewayservice.acceptance.common.AcceptanceTestWithMockServices; | ||
import org.zowe.apiml.cloudgatewayservice.acceptance.common.MockService; | ||
import org.zowe.apiml.constants.ApimlConstants; | ||
import org.zowe.apiml.ticket.TicketRequest; | ||
import org.zowe.apiml.zaas.ZaasTokenResponse; | ||
|
||
import java.io.IOException; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
import static io.restassured.RestAssured.given; | ||
import static org.apache.http.HttpStatus.SC_OK; | ||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertNull; | ||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
||
public class SafIdtSchemeTest { | ||
|
||
private static final String SERVICE_ID = "service"; | ||
private static final String SAF_IDT = "eyJhbGciOiJub25lIn0.eyJzdWIiOiJVU0VSIiwiZXhwIjoxNzAxMjc2NTUyfQ."; | ||
private static final ObjectWriter WRITER = new ObjectMapper().writer(); | ||
|
||
|
||
private String getHeaderValue(HttpExchange httpExchange, String headerName) { | ||
List<String> headerValue = Optional.ofNullable(httpExchange.getRequestHeaders().get(headerName)) | ||
.orElse(Collections.emptyList()); | ||
assertTrue(headerValue.size() <= 1); | ||
return headerValue.isEmpty() ? null : headerValue.get(0); | ||
} | ||
|
||
@Nested | ||
@AcceptanceTest | ||
@TestInstance(TestInstance.Lifecycle.PER_CLASS) | ||
class GivenValidAuth extends AcceptanceTestWithMockServices { | ||
MockService zaas; | ||
MockService service; | ||
|
||
@BeforeEach | ||
void setup() throws IOException { | ||
ZaasTokenResponse response = new ZaasTokenResponse(); | ||
response.setToken(SAF_IDT); | ||
|
||
zaas = mockService("gateway").scope(MockService.Scope.TEST) | ||
.addEndpoint("/gateway/zaas/safIdt") | ||
.responseCode(200) | ||
.assertion(he -> assertEquals("Bearer userJwt", he.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION))) | ||
.assertion(he -> { | ||
try { | ||
assertEquals(WRITER.writeValueAsString(new TicketRequest("IZUDFLT")), IOUtils.toString(he.getRequestBody(), StandardCharsets.UTF_8)); | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
}) | ||
.bodyJson(response) | ||
.and().start(); | ||
service = mockService("service").scope(MockService.Scope.TEST) | ||
.authenticationScheme(AuthenticationScheme.SAF_IDT).applid("IZUDFLT") | ||
.addEndpoint("/service/test") | ||
.assertion(he -> assertEquals(SAF_IDT, getHeaderValue(he, "x-saf-token"))) | ||
.and().start(); | ||
} | ||
|
||
@Test | ||
void thenReturnSAFIDtoken() { | ||
given() | ||
.header(HttpHeaders.AUTHORIZATION, "Bearer userJwt") | ||
.when() | ||
.get(basePath + "/" + SERVICE_ID + "/api/v1/test") | ||
.then() | ||
.statusCode(Matchers.is(SC_OK)); | ||
} | ||
} | ||
|
||
@Nested | ||
@AcceptanceTest | ||
@TestInstance(TestInstance.Lifecycle.PER_CLASS) | ||
class GivenNoAuth extends AcceptanceTestWithMockServices { | ||
MockService zaas; | ||
MockService service; | ||
|
||
@BeforeEach | ||
void setup() throws IOException { | ||
|
||
zaas = mockService("gateway").scope(MockService.Scope.CLASS) | ||
.addEndpoint("/gateway/zaas/safIdt") | ||
.responseCode(401) | ||
.assertion(he -> assertNull(he.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION))) | ||
.and().start(); | ||
service = mockService("service").scope(MockService.Scope.CLASS) | ||
.authenticationScheme(AuthenticationScheme.SAF_IDT).applid("IZUDFLT") | ||
.addEndpoint("/service/test") | ||
.assertion(he -> assertNull(getHeaderValue(he, "x-saf-token"))) | ||
.assertion(he -> assertEquals("Invalid or missing authentication", getHeaderValue(he, ApimlConstants.AUTH_FAIL_HEADER))) | ||
.and().start(); | ||
} | ||
|
||
@Test | ||
void thenReturnError() { | ||
|
||
given() | ||
.when() | ||
.get(basePath + "/" + SERVICE_ID + "/api/v1/test") | ||
.then() | ||
.statusCode(Matchers.is(SC_OK)); | ||
} | ||
} | ||
|
||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.