Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Basic HTTP responses in the SCGW #3697

Merged
merged 22 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/security-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ jobs:
name: Identify security related PR
runs-on: ubuntu-latest
timeout-minutes: 20
permissions: write-all

steps:
- uses: actions/github-script@v6
Expand Down
42 changes: 42 additions & 0 deletions apiml-common/src/main/resources/common-log-messages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,58 @@ messages:
# HTTP,Protocol messages
# 400-499

- key: org.zowe.apiml.common.badRequest
number: ZWEAO400
type: ERROR
reason: "A value in the request is missing or contains an invalid value."
action: "Fix the request and try again."
text: "The structure of the request is invalid: %s"

- key: org.zowe.apiml.common.unknownHttpsConfigError
number: ZWEAO401
type: ERROR
text: "Unknown error in HTTPS configuration: '%s'"
reason: "An Unknown error occurred while setting up an HTTP client during service initialization, followed by a system exit."
action: "Start the service again in debug mode to get a more descriptive message. This error indicates it is not a configuration issue."

- key: org.zowe.apiml.common.unauthorized
number: ZWEAO402
type: ERROR
text: "The request has not been applied because it lacks valid authentication credentials."
reason: "The accessed resource requires authentication. The request is missing valid authentication credentials or the token expired."
action: "Review the product documentation for more details about acceptable authentication. Verify that your credentials are valid and contact security administrator to obtain valid credentials."

- key: org.zowe.apiml.common.notFound
number: ZWEAO404
type: ERROR
text: "The service can not find the requested resource."

- key: org.zowe.apiml.common.methodNotAllowed
number: ZWEAO405
type: ERROR
text: "The request method has been disabled and cannot be used for the requested resource."

- key: org.zowe.apiml.common.unsupportedMediaType
number: ZWEAO415
type: ERROR
text: "The media format of the requested data is not supported by the service, so the service has rejected the request."

# TLS,Certificate messages
# 500-599

- key: org.zowe.apiml.common.internalServerError
number: ZWEAO500
type: ERROR
text: "The service has encountered a situation it doesn't know how to handle. Please contact support for further assistance. More details are available in the log under the provided message instance ID"

- key: org.zowe.apiml.common.serviceUnavailable
number: ZWEAO503
type: ERROR
text: "The server is not ready to handle the request: %s"
reason: "The service is not ready to handle the request, it is being initialized or waiting for another service to start."
action: "Repeat the request later. Please contact support for further assistance."


# Various messages
# 600-699

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ public class AuthConfigurationProperties {
private String gatewayQueryEndpoint = "/gateway/api/v1/auth/query";
private String gatewayTicketEndpoint = "/gateway/api/v1/auth/ticket";

private String gatewayLoginEndpointOldFormat = "/api/v1/gateway/auth/login";
private String gatewayLogoutEndpointOldFormat = "/api/v1/gateway/auth/logout";
private String gatewayQueryEndpointOldFormat = "/api/v1/gateway/auth/query";
private String gatewayTicketEndpointOldFormat = "/api/v1/gateway/auth/ticket";

private String zaasLoginEndpoint = "/zaas/api/v1/auth/login";
private String zaasLogoutEndpoint = "/zaas/api/v1/auth/logout";
private String zaasQueryEndpoint = "/zaas/api/v1/auth/query";
Expand All @@ -58,7 +53,6 @@ public class AuthConfigurationProperties {
private String gatewayEvictAccessTokensAndRules = "/gateway/auth/access-token/evict";
private String zaasEvictAccessTokensAndRules = "/zaas/api/v1/auth/access-token/evict";

private String gatewayRefreshEndpointOldFormat = "/api/v1/gateway/auth/refresh";
private String gatewayRefreshEndpoint = "/gateway/api/v1/auth/refresh";
private String zaasRefreshEndpoint = "/zaas/api/v1/auth/refresh";

Expand Down
Empty file removed ehcache/.lock
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
Expand Down Expand Up @@ -53,6 +54,7 @@
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.web.server.ServerWebExchange;
import org.zowe.apiml.gateway.config.oidc.ClientConfiguration;
import org.zowe.apiml.gateway.controllers.GatewayExceptionHandler;
import org.zowe.apiml.gateway.filters.security.BasicAuthFilter;
import org.zowe.apiml.gateway.filters.security.TokenAuthFilter;
import org.zowe.apiml.gateway.service.BasicAuthProvider;
Expand Down Expand Up @@ -115,6 +117,8 @@ public class WebSecurity {
private final TokenProvider tokenProvider;
private final BasicAuthProvider basicAuthProvider;

private final ApplicationContext applicationContext;

private Predicate<String> usernameAuthorizationTester;

@PostConstruct
Expand Down Expand Up @@ -304,13 +308,17 @@ private List<ClientRegistration> getClientRegistrations() {
}

public ServerHttpSecurity defaultSecurityConfig(ServerHttpSecurity http) {
var gatewayExceptionHandler = applicationContext.getBean(GatewayExceptionHandler.class);
return http
.headers(customizer -> customizer.frameOptions(ServerHttpSecurity.HeaderSpec.FrameOptionsSpec::disable))
.x509(x509 -> x509
.principalExtractor(X509Util.x509PrincipalExtractor())
.authenticationManager(X509Util.x509ReactiveAuthenticationManager())
)
.csrf(ServerHttpSecurity.CsrfSpec::disable);
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.exceptionHandling(exceptionHandlingSpec -> exceptionHandlingSpec.authenticationEntryPoint(
gatewayExceptionHandler::handleAuthenticationException)
);
achmelo marked this conversation as resolved.
Show resolved Hide resolved
}

@Bean
Expand All @@ -319,7 +327,6 @@ public SecurityWebFilterChain defaultSecurityWebFilterChain(ServerHttpSecurity h
return defaultSecurityConfig(http).build();
}

// TODO the security for the endpoints below is still not working
@Bean
@Order(1)
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, AuthConfigurationProperties authConfigurationProperties) {
Expand All @@ -334,34 +341,17 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, Au
CONFORMANCE_SHORT_URL,
CONFORMANCE_LONG_URL,
VALIDATE_SHORT_URL,
VALIDATE_LONG_URL
))
.authorizeExchange(authorizeExchangeSpec ->
authorizeExchangeSpec
.anyExchange().authenticated()
)
.addFilterAfter(new TokenAuthFilter(tokenProvider, authConfigurationProperties), SecurityWebFiltersOrder.AUTHENTICATION)
.addFilterAfter(new BasicAuthFilter(basicAuthProvider), SecurityWebFiltersOrder.AUTHENTICATION)
.build();
}

@Bean
@Order(2)
public SecurityWebFilterChain securityWebFilterChainForActuator(ServerHttpSecurity http, AuthConfigurationProperties authConfigurationProperties) {

return defaultSecurityConfig(http)
.securityMatcher(ServerWebExchangeMatchers.pathMatchers(
VALIDATE_LONG_URL,
"/application/**"
))
.authorizeExchange(authorizeExchangeSpec -> {
if (!isHealthEndpointProtected) {
authorizeExchangeSpec
.pathMatchers( "/application/info", "/application/version", "/application/health")
.permitAll();
}
else {
if (!isHealthEndpointProtected) {
authorizeExchangeSpec
.pathMatchers("/application/info", "/application/version", "/application/health")
.permitAll();
} else {
authorizeExchangeSpec
.pathMatchers( "/application/info", "/application/version")
.pathMatchers("/application/info", "/application/version")
.permitAll();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* 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.controllers;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.util.Assert;
import org.springframework.web.HttpMediaTypeException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.web.reactive.resource.NoResourceFoundException;
import org.springframework.web.server.MethodNotAllowedException;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.i18n.LocaleContextResolver;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.zowe.apiml.gateway.filters.ForbidCharacterException;
import org.zowe.apiml.gateway.filters.ForbidSlashException;
import org.zowe.apiml.message.core.Message;
import org.zowe.apiml.message.core.MessageService;
import org.zowe.apiml.message.log.ApimlLogger;
import org.zowe.apiml.product.logging.annotations.InjectApimlLogger;
import org.zowe.apiml.security.common.error.ServiceNotAccessibleException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import static org.apache.http.HttpStatus.*;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class GatewayExceptionHandler {

private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
private static String WWW_AUTHENTICATE_FORMAT = "Basic realm=\"%s\"";
private static final String DEFAULT_REALM = "Realm";

private final ObjectMapper mapper;
private final MessageService messageService;
private final LocaleContextResolver localeContextResolver;

@InjectApimlLogger
private final ApimlLogger apimlLog = ApimlLogger.empty();

private static String createHeaderValue(String realm) {
Assert.notNull(realm, "realm cannot be null");
return String.format(WWW_AUTHENTICATE_FORMAT, realm);
}

public Mono<Void> setBodyResponse(ServerWebExchange exchange, int responseCode, String messageCode, Object... args) {
var sessionManager = new DefaultWebSessionManager();
var serverCodecConfigurer = ServerCodecConfigurer.create();

var serverWebExchange = new DefaultServerWebExchange(exchange.getRequest(), exchange.getResponse(), sessionManager, serverCodecConfigurer, localeContextResolver);
serverWebExchange.getResponse().setRawStatusCode(responseCode);
serverWebExchange.getResponse().getHeaders().add(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_VALUE);

Message message = messageService.createMessage(messageCode, args);
try {
DataBuffer buffer = serverWebExchange.getResponse().bufferFactory().wrap(mapper.writeValueAsBytes(message.mapToView()));
return serverWebExchange.getResponse().writeWith(Flux.just(buffer));
} catch (JsonProcessingException e) {
apimlLog.log("org.zowe.apiml.security.errorWritingResponse", e.getMessage());
throw new RuntimeException(e);
}
}

public void setWwwAuthenticateResponse(ServerWebExchange exchange) {
exchange.getResponse().getHeaders().add(WWW_AUTHENTICATE, createHeaderValue(DEFAULT_REALM));
}

@ExceptionHandler(WebClientResponseException.BadRequest.class)
public Mono<Void> handleBadRequestException(ServerWebExchange exchange, WebClientResponseException.BadRequest ex) {
log.debug("Invalid request structure on {}: {}", exchange.getRequest().getURI(), ex.getMessage());
return setBodyResponse(exchange, SC_BAD_REQUEST, "org.zowe.apiml.common.badRequest");
}

@ExceptionHandler(ForbidCharacterException.class)
public Mono<Void> handleForbidCharacterException(ServerWebExchange exchange, ForbidCharacterException ex) {
log.debug("Forbidden character in the URI {}: {}", exchange.getRequest().getURI(), ex.getMessage());
return setBodyResponse(exchange, SC_BAD_REQUEST, "org.zowe.apiml.gateway.requestContainEncodedCharacter");
}

@ExceptionHandler(ForbidSlashException.class)
public Mono<Void> handleForbidSlashException(ServerWebExchange exchange, ForbidSlashException ex) {
log.debug("Forbidden slash in the URI {}: {}", exchange.getRequest().getURI(), ex.getMessage());
return setBodyResponse(exchange, SC_BAD_REQUEST, "org.zowe.apiml.gateway.requestContainEncodedSlash");
}

@ExceptionHandler({AuthenticationException.class, WebClientResponseException.Unauthorized.class})
public Mono<Void> handleAuthenticationException(ServerWebExchange exchange, Exception ex) {
log.debug("Unauthorized access on {}: {}", exchange.getRequest().getURI(), ex.getMessage());
setWwwAuthenticateResponse(exchange);
return setBodyResponse(exchange, SC_UNAUTHORIZED, "org.zowe.apiml.common.unauthorized");
}

@ExceptionHandler({AccessDeniedException.class, WebClientResponseException.Forbidden.class})
public Mono<Void> handleAccessDeniedException(ServerWebExchange exchange, Exception ex) {
log.debug("Unauthenticated access on {}: {}", exchange.getRequest().getURI(), ex.getMessage());
return setBodyResponse(exchange, SC_FORBIDDEN, "org.zowe.apiml.security.forbidden", exchange.getRequest().getURI());
}

@ExceptionHandler({NoResourceFoundException.class, WebClientResponseException.NotFound.class})
public Mono<Void> handleNoResourceFoundException(ServerWebExchange exchange, Exception ex) {
log.debug("Resource {} not found: {}", exchange.getRequest().getURI(), ex.getMessage());
return setBodyResponse(exchange, SC_NOT_FOUND, "org.zowe.apiml.common.notFound");
}

@ExceptionHandler({MethodNotAllowedException.class, WebClientResponseException.MethodNotAllowed.class})
public Mono<Void> handleMethodNotAllowedException(ServerWebExchange exchange, Exception ex) {
log.debug("Method not allowed on {}: {}", exchange.getRequest().getURI(), ex.getMessage());
return setBodyResponse(exchange, SC_METHOD_NOT_ALLOWED, "org.zowe.apiml.common.methodNotAllowed");
}

@ExceptionHandler({HttpMediaTypeException.class, WebClientResponseException.UnsupportedMediaType.class})
public Mono<Void> handleHttpMediaTypeException(ServerWebExchange exchange, Exception ex) {
log.debug("Invalid media type on {}: {}", exchange.getRequest().getURI(), ex.getMessage());
return setBodyResponse(exchange, SC_UNSUPPORTED_MEDIA_TYPE, "org.zowe.apiml.common.unsupportedMediaType");
}

@ExceptionHandler({Exception.class})
public Mono<Void> handleInternalError(ServerWebExchange exchange, Exception ex) {
log.debug("Unhandled internal error on {}: {}", exchange.getRequest().getURI(), ex.getMessage());
return setBodyResponse(exchange, SC_INTERNAL_SERVER_ERROR, "org.zowe.apiml.common.internalServerError");
}

@ExceptionHandler({ResponseStatusException.class})
public Mono<Void> handleStatusError(ServerWebExchange exchange, ResponseStatusException ex) {
log.debug("Unexpected response status on {}: {}", exchange.getRequest().getURI(), ex.getMessage());
return setBodyResponse(exchange, ex.getStatusCode().value(), "org.zowe.apiml.gateway.responseStatusError");
}

@ExceptionHandler({ServiceNotAccessibleException.class, WebClientResponseException.ServiceUnavailable.class})
public Mono<Void> handleServiceNotAccessibleException(ServerWebExchange exchange, Exception ex) {
log.debug("A service is not available at the moment to finish request {}: {}", exchange.getRequest().getURI(), ex.getMessage());
return setBodyResponse(exchange, SC_SERVICE_UNAVAILABLE, "org.zowe.apiml.common.serviceUnavailable");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.zowe.apiml.gateway.service.InstanceInfoService;
import org.zowe.apiml.gateway.x509.X509Util;
import org.zowe.apiml.message.core.MessageService;
import org.zowe.apiml.security.common.error.ServiceNotAccessibleException;
import org.zowe.apiml.util.CookieUtil;
import reactor.core.publisher.Mono;

Expand Down Expand Up @@ -188,7 +189,7 @@ protected Mono<Void> invoke(
) {
Iterator<ServiceInstance> i = robinRound.getIterator(serviceInstances);
if (!i.hasNext()) {
throw new IllegalArgumentException("No ZAAS is available");
throw new ServiceNotAccessibleException("There are no instance of ZAAS available");
}

return requestWithHa(i, requestCreator).flatMap(responseProcessor);
Expand Down
Loading
Loading