Skip to content

Commit

Permalink
add message service
Browse files Browse the repository at this point in the history
  • Loading branch information
kishkinova committed Oct 31, 2024
1 parent 6e41a19 commit 5e921e4
Show file tree
Hide file tree
Showing 11 changed files with 55 additions and 32 deletions.
1 change: 1 addition & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ jobs:
SERVER_MAX_HTTP_REQUEST_HEADER_SIZE: 16348
SERVER_WEBSOCKET_REQUESTBUFFERSIZE: 16348
APIML_ROUTING_SERVICESTOLIMITREQUESTRATE: discoverableclient
APIML_ROUTING_COOKIENAMEFORRATELIMIT: apimlAuthenticationToken
zaas-service:
image: ghcr.io/balhar-jakub/zaas-service:${{ github.run_id }}-${{ github.run_number }}
env:
Expand Down
1 change: 1 addition & 0 deletions gateway-service/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ dependencies {
implementation libs.nimbus.jose.jwt
implementation libs.bcpkix
implementation libs.caffeine
implementation libs.bucket4j.core

implementation libs.swagger2.parser
implementation libs.swagger3.parser
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class InMemoryRateLimiter implements RateLimiter<InMemoryRateLimiter.Conf
@Value("${apiml.gateway.rateLimiterTokens:20}")
int tokens;
@Value("${apiml.gateway.rateLimiterRefillDuration:1}")
Long refillDuration;
Integer refillDuration;

@Override
public Mono<Response> isAllowed(String routeId, String id) {
Expand Down Expand Up @@ -86,6 +86,6 @@ public Config newConfig() {
public static class Config {
private int capacity;
private int tokens;
private Long refillDuration;
private int refillDuration;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,46 @@

package org.zowe.apiml.gateway.filters;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
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 reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.List;

@Component
public class InMemoryRateLimiterFilterFactory extends AbstractGatewayFilterFactory<InMemoryRateLimiterFilterFactory.Config> {

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

private final InMemoryRateLimiter rateLimiter;

private final KeyResolver keyResolver;

@Value("${apiml.routing.servicesToLimitRequestRate:-}")
List<String> serviceIds;

public InMemoryRateLimiterFilterFactory(InMemoryRateLimiter rateLimiter, KeyResolver keyResolver) {
private final ObjectMapper mapper;

private final MessageService messageService;

public InMemoryRateLimiterFilterFactory(InMemoryRateLimiter rateLimiter, KeyResolver keyResolver, ObjectMapper mapper, MessageService messageService) {
super(Config.class);
this.rateLimiter = rateLimiter;
this.keyResolver = keyResolver;
this.mapper = mapper;
this.messageService = messageService;
}

@Override
Expand All @@ -56,12 +67,13 @@ public GatewayFilter apply(Config config) {
} else {
apimlLog.log("org.zowe.apiml.gateway.connectionsLimitApproached", "Connections limit exceeded for service '{}'", requestPath);
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
exchange.getResponse().getHeaders().setContentType(MediaType.TEXT_HTML);
String errorHtml = "<html><body><h1>429 Too Many Requests</h1>" +
"<p>The connection limit for the service '" + requestPath + "' has been exceeded. Please try again later.</p>" +
"</body></html>";
byte[] bytes = errorHtml.getBytes(StandardCharsets.UTF_8);
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(bytes)));
Message message = messageService.createMessage("org.zowe.apiml.gateway.connectionsLimitApproached", "Connections limit exceeded for service '{}'", requestPath);
try {
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(mapper.writeValueAsBytes(message.mapToView()))));
} catch (JsonProcessingException e) {
apimlLog.log("org.zowe.apiml.security.errorWritingResponse", e.getMessage());
return Mono.error(e);
}
}
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package org.zowe.apiml.gateway.filters;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpCookie;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
Expand All @@ -19,11 +20,8 @@
@Component
public class KeyResolver implements org.springframework.cloud.gateway.filter.ratelimit.KeyResolver {

private final String cookieName;

public KeyResolver() {
this.cookieName = "apimlAuthenticationToken";
}
@Value("${apiml.routing.cookieNameForRateLimit:-}")
private String cookieName;

@Override
public Mono<String> resolve(org.springframework.web.server.ServerWebExchange exchange) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@
import static org.zowe.apiml.constants.EurekaMetadataDefinition.*;

@Service
public class
RouteLocator implements RouteDefinitionLocator {
public class RouteLocator implements RouteDefinitionLocator {

private static final EurekaMetadataParser metadataParser = new EurekaMetadataParser();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

package org.zowe.apiml.gateway.filters;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
Expand All @@ -19,6 +21,9 @@
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import org.zowe.apiml.message.api.ApiMessageView;
import org.zowe.apiml.message.core.Message;
import org.zowe.apiml.message.core.MessageService;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

Expand All @@ -38,13 +43,19 @@ public class InMemoryRateLimiterFilterFactoryTest {
private String serviceId;
private MockServerHttpRequest request;
private InMemoryRateLimiterFilterFactory.Config config;
private MessageService messageService;
private ObjectMapper objectMapper;
private Message message;

@BeforeEach
public void setUp() {
serviceId = "testService";
rateLimiter = mock(InMemoryRateLimiter.class);
keyResolver = mock(KeyResolver.class);
filterFactory = new InMemoryRateLimiterFilterFactory(rateLimiter, keyResolver);
messageService = mock(MessageService.class);
message = mock(Message.class);
objectMapper = mock(ObjectMapper.class);
filterFactory = new InMemoryRateLimiterFilterFactory(rateLimiter, keyResolver,objectMapper, messageService);
filterFactory.serviceIds = List.of(serviceId);
request = MockServerHttpRequest.get("/" + serviceId).build();
exchange = MockServerWebExchange.from(request);
Expand All @@ -66,7 +77,10 @@ public void apply_shouldAllowRequest_whenTokensAreAvailable() {
}

@Test
public void apply_shouldReturn429_whenTokensAreExhausted() {
public void apply_shouldReturn429_whenTokensAreExhausted() throws JsonProcessingException {
when(messageService.createMessage(anyString(),anyString(),any())).thenReturn(message);
when(message.mapToView()).thenReturn(new ApiMessageView());
when(objectMapper.writeValueAsBytes(any())).thenReturn("Serialized Message".getBytes());
when(keyResolver.resolve(exchange)).thenReturn(Mono.just("testKey"));
when(rateLimiter.isAllowed(anyString(), anyString())).thenReturn(Mono.just(new InMemoryRateLimiter.Response(false, Map.of())));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public void setUp() {
rateLimiter = new InMemoryRateLimiter();
rateLimiter.capacity = 3;
rateLimiter.tokens = 3;
rateLimiter.refillDuration = 1L;
rateLimiter.refillDuration = 1;
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

Expand All @@ -32,6 +32,7 @@ public class KeyResolverTest {
public void setUp() {
keyResolver = new KeyResolver();
exchange = mock(ServerWebExchange.class);
ReflectionTestUtils.setField(keyResolver, "cookieName", "apimlAuthenticationToken");
}

@Test
Expand All @@ -42,7 +43,6 @@ public void resolve_shouldReturnCookieValue_whenCookieIsPresent() {
var cookies = new LinkedMultiValueMap<String, HttpCookie>();
cookies.add("apimlAuthenticationToken", cookie);
when(request.getCookies()).thenReturn(cookies);

Mono<String> result = keyResolver.resolve(exchange);

assertEquals("testToken", result.block());
Expand Down
2 changes: 2 additions & 0 deletions gradle/versions.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ dependencyResolutionManagement {
version('jacoco', '0.8.11')
version('gradle', '8.6')
version('commonsCompress', '1.27.1')
version('bucket4j', '8.7.0')

library('zowe_zos_utils', 'org.zowe.apiml.sdk', 'zos-utils').versionRef('zosUtils')
library('spring_boot_configuration_processor', 'org.springframework.boot', 'spring-boot-configuration-processor').versionRef('springBoot')
Expand Down Expand Up @@ -216,6 +217,7 @@ dependencyResolutionManagement {
library('thymeleaf', 'org.thymeleaf', 'thymeleaf').versionRef('thymeleaf')
library('woodstox_core', 'com.fasterxml.woodstox', 'woodstox-core').versionRef('woodstoxCore')
library('spring_boot_starter_graphql', 'org.springframework.boot', 'spring-boot-starter-graphql').versionRef('springBootGraphQl')
library('bucket4j_core', 'com.bucket4j', 'bucket4j-core').versionRef('bucket4j')

// Sample apps only
library('jersey_client4', 'com.sun.jersey.contribs', 'jersey-apache-client4').versionRef('jerseySun')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package org.zowe.apiml.functional.gateway;

import com.fasterxml.jackson.core.JsonProcessingException;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
Expand Down Expand Up @@ -59,7 +60,7 @@ void testRateLimitingWhenAllowedWithCookie() {
}

@Test
void testRateLimitingWhenExceeded() {
void testRateLimitingWhenExceeded() throws JsonProcessingException {
for (int i = 0; i < bucketCapacity; i++) {
client.get()
.cookie("apimlAuthenticationToken", "validTokenValue")
Expand All @@ -70,9 +71,7 @@ void testRateLimitingWhenExceeded() {
.cookie("apimlAuthenticationToken", "validTokenValue")
.exchange()
.expectStatus().isEqualTo(HttpStatus.TOO_MANY_REQUESTS)
.expectHeader().contentType("text/html")
.expectBody(String.class)
.value(body -> body.contains("429 Too Many Requests"));
.expectBody(String.class);
}

@Test
Expand All @@ -87,10 +86,7 @@ void testRateLimiterAllowsAccessToAnotherUser() {
client.get()
.cookie("apimlAuthenticationToken", "theFirstUser")
.exchange()
.expectStatus().isEqualTo(HttpStatus.TOO_MANY_REQUESTS)
.expectHeader().contentType("text/html")
.expectBody(String.class)
.value(body -> body.contains("429 Too Many Requests"));
.expectStatus().isEqualTo(HttpStatus.TOO_MANY_REQUESTS);

// the second user requires access
client.get()
Expand Down

0 comments on commit 5e921e4

Please sign in to comment.