Skip to content

Commit

Permalink
feat: migrate to the new version of Jupiter's SecurityPolicy
Browse files Browse the repository at this point in the history
  • Loading branch information
Marc authored and marcambier committed Aug 16, 2022
1 parent 6a6cba2 commit 467fab6
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 45 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

<properties>
<gravitee-bom.version>2.6</gravitee-bom.version>
<gravitee-gateway-api.version>1.38.0</gravitee-gateway-api.version>
<gravitee-gateway-api.version>1.39.0</gravitee-gateway-api.version>
<gravitee-policy-api.version>1.11.0</gravitee-policy-api.version>
<json-schema-generator-maven-plugin.version>1.1.0</json-schema-generator-maven-plugin.version>
<json-schema-generator-maven-plugin.outputDirectory>${project.build.directory}/schemas
Expand Down
28 changes: 10 additions & 18 deletions src/main/java/io/gravitee/policy/apikey/ApiKeyPolicy.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package io.gravitee.policy.apikey;

import static io.gravitee.common.http.HttpStatusCode.UNAUTHORIZED_401;
import static io.gravitee.gateway.jupiter.api.context.ExecutionContext.*;

import io.gravitee.common.http.GraviteeHttpHeader;
Expand All @@ -28,10 +27,11 @@
import io.gravitee.gateway.jupiter.api.context.MessageExecutionContext;
import io.gravitee.gateway.jupiter.api.context.RequestExecutionContext;
import io.gravitee.gateway.jupiter.api.policy.SecurityPolicy;
import io.gravitee.gateway.jupiter.api.policy.SecurityToken;
import io.gravitee.policy.apikey.configuration.ApiKeyPolicyConfiguration;
import io.gravitee.policy.v3.apikey.ApiKeyPolicyV3;
import io.reactivex.Completable;
import io.reactivex.Single;
import io.reactivex.Maybe;
import java.util.Date;
import java.util.Optional;
import org.slf4j.Logger;
Expand Down Expand Up @@ -64,17 +64,14 @@ public String id() {
return "api-key";
}

/**
* {@inheritDoc}
* The {@link ApiKeyPolicy} is assignable if an api key is passed in the request headers.
*/
@Override
public Single<Boolean> support(HttpExecutionContext ctx) {
final Optional<String> optApiKey = extractApiKey(ctx);

optApiKey.ifPresent(apiKey -> ctx.setInternalAttribute(ATTR_INTERNAL_API_KEY, apiKey));

return Single.just(optApiKey.isPresent());
public Maybe<SecurityToken> extractSecurityToken(HttpExecutionContext ctx) {
final Optional<String> apiKey = extractApiKey(ctx);
if (apiKey.isPresent()) {
ctx.setInternalAttribute(ATTR_INTERNAL_API_KEY, apiKey.get());
return Maybe.just(SecurityToken.forApiKey(apiKey.get()));
}
return Maybe.empty();
}

/**
Expand All @@ -88,11 +85,6 @@ public boolean requireSubscription() {
return true;
}

@Override
public Completable onInvalidSubscription(HttpExecutionContext ctx) {
return ctx.interruptWith(new ExecutionFailure(UNAUTHORIZED_401).key(API_KEY_INVALID_KEY).message(API_KEY_INVALID_MESSAGE));
}

/**
* Order set to 500 to make sure it will be executed before lower security policies such a Keyless but after higher security policies such as Jwt or OAuth2.
*
Expand Down Expand Up @@ -191,7 +183,7 @@ private void cleanupApiKey(HttpExecutionContext ctx) {
if (!propagateApiKey) {
ctx.request().headers().remove(API_KEY_HEADER);
ctx.request().parameters().remove(API_KEY_QUERY_PARAMETER);
ctx.removeInternalAttribute(ATTR_INTERNAL_API_KEY);
}
ctx.removeInternalAttribute(ATTR_INTERNAL_API_KEY);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
package io.gravitee.policy.apikey;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static io.gravitee.gateway.jupiter.api.policy.SecurityToken.TokenType.API_KEY;
import static java.time.temporal.ChronoUnit.HOURS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.when;

import io.gravitee.apim.gateway.tests.sdk.AbstractPolicyTest;
Expand All @@ -32,6 +33,7 @@
import io.gravitee.gateway.api.service.ApiKeyService;
import io.gravitee.gateway.api.service.Subscription;
import io.gravitee.gateway.api.service.SubscriptionService;
import io.gravitee.gateway.jupiter.api.policy.SecurityToken;
import io.gravitee.policy.apikey.configuration.ApiKeyPolicyConfiguration;
import io.reactivex.observers.TestObserver;
import io.vertx.reactivex.core.buffer.Buffer;
Expand All @@ -44,12 +46,12 @@
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.stubbing.OngoingStubbing;

/**
* @author Yann TAVERNIER (yann.tavernier at graviteesource.com)
* @author GraviteeSource Team
*/
@Disabled("Disabled because failing on CCI. There is a dependency loop with the SDK and the policy.")
@GatewayTest
@DeployApi("/apis/api-key.json")
public class ApiKeyPolicyIntegrationTest extends AbstractPolicyTest<ApiKeyPolicy, ApiKeyPolicyConfiguration> {
Expand All @@ -58,7 +60,6 @@ public class ApiKeyPolicyIntegrationTest extends AbstractPolicyTest<ApiKeyPolicy
protected void configureGateway(GatewayConfigurationBuilder gatewayConfigurationBuilder) {
super.configureGateway(gatewayConfigurationBuilder);
gatewayConfigurationBuilder.set("api.jupiterMode.enabled", "true");
gatewayConfigurationBuilder.set("http.instances", "1");
}

/**
Expand Down Expand Up @@ -104,15 +105,15 @@ void shouldGet401IfNoSubscription(WebClient client) {
final ApiKey apiKey = fakeApiKeyFromCache();

when(getBean(ApiKeyService.class).getByApiAndKey(any(), any())).thenReturn(Optional.of(apiKey));
when(getBean(SubscriptionService.class).getById(any())).thenReturn(Optional.empty());
when(getBean(SubscriptionService.class).getByApiAndSecurityToken(any(), any(), any())).thenReturn(Optional.empty());

final TestObserver<HttpResponse<Buffer>> obs = client.get("/test").putHeader("X-Gravitee-Api-Key", "apiKeyValue").rxSend().test();

awaitTerminalEvent(obs)
.assertComplete()
.assertValue(response -> {
assertThat(response.statusCode()).isEqualTo(401);
assertThat(response.bodyAsString()).isEqualTo("API Key is not valid or is expired / revoked.");
assertUnauthorizedResponseBody(response.bodyAsString());
return true;
})
.assertNoErrors();
Expand All @@ -128,15 +129,15 @@ void shouldGet401IfExpiredSubscription(WebClient client) {
final ApiKey apiKey = fakeApiKeyFromCache();

when(getBean(ApiKeyService.class).getByApiAndKey(any(), any())).thenReturn(Optional.of(apiKey));
when(getBean(SubscriptionService.class).getById(any())).thenReturn(Optional.of(fakeSubscriptionFromCache(true)));
whenSearchingSubscription(apiKey).thenReturn(Optional.of(fakeSubscriptionFromCache(true)));

final TestObserver<HttpResponse<Buffer>> obs = client.get("/test").putHeader("X-Gravitee-Api-Key", "apiKeyValue").rxSend().test();

awaitTerminalEvent(obs)
.assertComplete()
.assertValue(response -> {
assertThat(response.statusCode()).isEqualTo(401);
assertThat(response.bodyAsString()).isEqualTo("API Key is not valid or is expired / revoked.");
assertUnauthorizedResponseBody(response.bodyAsString());
return true;
})
.assertNoErrors();
Expand All @@ -152,8 +153,7 @@ void shouldAccessApiWithApiKeyHeader(WebClient client) {
final ApiKey apiKey = fakeApiKeyFromCache();

when(getBean(ApiKeyService.class).getByApiAndKey(any(), any())).thenReturn(Optional.of(apiKey));
when(getBean(SubscriptionService.class).getById(apiKey.getSubscription()))
.thenReturn(Optional.of(fakeSubscriptionFromCache(false)));
whenSearchingSubscription(apiKey).thenReturn(Optional.of(fakeSubscriptionFromCache(false)));

final TestObserver<HttpResponse<Buffer>> obs = client.get("/test").putHeader("X-Gravitee-Api-Key", "apiKeyValue").rxSend().test();

Expand All @@ -177,7 +177,7 @@ void shouldAccessApiWithApiKeyQueryParam(WebClient client) {
final ApiKey apiKey = fakeApiKeyFromCache();

when(getBean(ApiKeyService.class).getByApiAndKey(any(), any())).thenReturn(Optional.of(apiKey));
when(getBean(SubscriptionService.class).getById("subscription-id")).thenReturn(Optional.of(fakeSubscriptionFromCache(false)));
whenSearchingSubscription(apiKey).thenReturn(Optional.of(fakeSubscriptionFromCache(false)));

final TestObserver<HttpResponse<Buffer>> obs = client.get("/test").addQueryParam("api-key", "apiKeyValue").rxSend().test();

Expand All @@ -199,10 +199,11 @@ void shouldAccessApiWithApiKeyQueryParam(WebClient client) {
*/
private ApiKey fakeApiKeyFromCache() {
final ApiKey apiKey = new ApiKey();
apiKey.setApi("my-api");
apiKey.setApplication("application-id");
apiKey.setSubscription("subscription-id");
apiKey.setPlan("plan-id");
apiKey.setKey("key-id");
apiKey.setKey("apiKeyValue");
return apiKey;
}

Expand All @@ -220,4 +221,21 @@ private Subscription fakeSubscriptionFromCache(boolean isExpired) {
}
return subscription;
}

protected void assertUnauthorizedResponseBody(String responseBody) {
assertThat(responseBody).isEqualTo("Unauthorized");
}

protected OngoingStubbing<Optional<Subscription>> whenSearchingSubscription(ApiKey apiKey) {
return when(
getBean(SubscriptionService.class)
.getByApiAndSecurityToken(eq(apiKey.getApi()), securityTokenMatcher(apiKey.getKey()), eq(apiKey.getPlan()))
);
}

private SecurityToken securityTokenMatcher(String apiKeyValue) {
return argThat(securityToken ->
securityToken.getTokenType().equals(API_KEY.name()) && securityToken.getTokenValue().equals(apiKeyValue)
);
}
}
33 changes: 20 additions & 13 deletions src/test/java/io/gravitee/policy/apikey/ApiKeyPolicyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static io.gravitee.common.http.GraviteeHttpHeader.X_GRAVITEE_API_KEY;
import static io.gravitee.gateway.api.ExecutionContext.ATTR_API;
import static io.gravitee.gateway.jupiter.api.context.ExecutionContext.*;
import static io.gravitee.gateway.jupiter.api.policy.SecurityToken.TokenType.API_KEY;
import static io.gravitee.policy.apikey.ApiKeyPolicy.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
Expand All @@ -31,8 +32,8 @@
import io.gravitee.gateway.jupiter.api.context.Request;
import io.gravitee.gateway.jupiter.api.context.RequestExecutionContext;
import io.gravitee.gateway.jupiter.api.context.Response;
import io.gravitee.gateway.jupiter.api.policy.SecurityToken;
import io.gravitee.policy.apikey.configuration.ApiKeyPolicyConfiguration;
import io.gravitee.policy.v3.apikey.ApiKeyPolicyV3;
import io.reactivex.Completable;
import io.reactivex.observers.TestObserver;
import java.util.Date;
Expand Down Expand Up @@ -420,47 +421,53 @@ void shouldInterruptWith401WhenExceptionOccurred() {
}

@Test
void shouldReturnCanHandleWhenApiKeyInternalAttributeIsFound() {
void extractSecurityToken_shouldReturnSecurityToken_whenApiKeyInternalAttributeIsFound() {
when(ctx.getInternalAttribute(ATTR_INTERNAL_API_KEY)).thenReturn(API_KEY);

final ApiKeyPolicy cut = new ApiKeyPolicy(configuration);
final TestObserver<Boolean> obs = cut.support(ctx).test();
final TestObserver<SecurityToken> obs = cut.extractSecurityToken(ctx).test();

obs.assertResult(true);
obs.assertValue(token ->
token.getTokenType().equals(SecurityToken.TokenType.API_KEY.name()) && token.getTokenValue().equals(API_KEY)
);
}

@Test
void shouldReturnCanHandleWhenApiKeyHeaderIsFound() {
void extractSecurityToken_shouldReturnSecurityToken_whenApiKeyHeaderIsFound() {
final HttpHeaders headers = buildHttpHeaders(X_GRAVITEE_API_KEY);
when(request.headers()).thenReturn(headers);

final ApiKeyPolicy cut = new ApiKeyPolicy(configuration);
final TestObserver<Boolean> obs = cut.support(ctx).test();
final TestObserver<SecurityToken> obs = cut.extractSecurityToken(ctx).test();

obs.assertResult(true);
obs.assertValue(token ->
token.getTokenType().equals(SecurityToken.TokenType.API_KEY.name()) && token.getTokenValue().equals(API_KEY)
);
}

@Test
void shouldReturnCanHandleWhenApiKeyQueryParamIsFound() {
void extractSecurityToken_shouldReturnSecurityToken_whenApiKeyQueryParamIsFound() {
final MultiValueMap<String, String> parameters = buildQueryParameters(DEFAULT_API_KEY_QUERY_PARAMETER);
when(request.headers()).thenReturn(HttpHeaders.create());
when(request.parameters()).thenReturn(parameters);

final ApiKeyPolicy cut = new ApiKeyPolicy(configuration);
final TestObserver<Boolean> obs = cut.support(ctx).test();
final TestObserver<SecurityToken> obs = cut.extractSecurityToken(ctx).test();

obs.assertResult(true);
obs.assertValue(token ->
token.getTokenType().equals(SecurityToken.TokenType.API_KEY.name()) && token.getTokenValue().equals(API_KEY)
);
}

@Test
void shouldReturnCannotHandleWhenNoApiKeyIsFound() {
void extractSecurityToken_shouldReturnEmpty_whenNoApiKeyIsFound() {
when(request.headers()).thenReturn(HttpHeaders.create());
when(request.parameters()).thenReturn(new LinkedMultiValueMap<>());

final ApiKeyPolicy cut = new ApiKeyPolicy(configuration);
final TestObserver<Boolean> obs = cut.support(ctx).test();
final TestObserver<SecurityToken> obs = cut.extractSecurityToken(ctx).test();

obs.assertResult(false);
obs.assertComplete().assertValueCount(0);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,54 @@
*/
package io.gravitee.policy.apikey;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

import io.gravitee.apim.gateway.tests.sdk.configuration.GatewayConfigurationBuilder;
import io.gravitee.definition.model.Api;
import io.gravitee.definition.model.ExecutionMode;
import org.junit.jupiter.api.Disabled;
import io.gravitee.gateway.api.service.ApiKey;
import io.gravitee.gateway.api.service.Subscription;
import io.gravitee.gateway.api.service.SubscriptionService;
import java.util.Optional;
import org.mockito.stubbing.OngoingStubbing;

/**
* @author Jeoffrey HAEYAERT (jeoffrey.haeyaert at graviteesource.com)
* @author GraviteeSource Team
*/
@Disabled("Disabled because failing on CCI. There is a dependency loop with the SDK and the policy.")
public class ApiKeyPolicyV3CompatibilityIntegrationTest extends ApiKeyPolicyIntegrationTest {

@Override
protected void configureGateway(GatewayConfigurationBuilder gatewayConfigurationBuilder) {
super.configureGateway(gatewayConfigurationBuilder);
gatewayConfigurationBuilder.set("api.jupiterMode.enabled", "true");
}

@Override
public void configureApi(Api api) {
super.configureApi(api);
api.setExecutionMode(ExecutionMode.V3);
}

/**
* This overrides subscription search :
* - in jupiter its searched with getByApiAndSecurityToken
* - in V3 its searches with getById
*/
@Override
protected OngoingStubbing<Optional<Subscription>> whenSearchingSubscription(ApiKey apiKey) {
return when(getBean(SubscriptionService.class).getById(apiKey.getSubscription()));
}

/**
* This overrides 401 response HTTP body content assertion :
* - in jupiter, it's "unauthorized"
* - in V3, it contains more information
*/
@Override
protected void assertUnauthorizedResponseBody(String responseBody) {
assertThat(responseBody).isEqualTo("API Key is not valid or is expired / revoked.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,18 @@
*/
package io.gravitee.policy.v3.apikey;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

import io.gravitee.apim.gateway.tests.sdk.configuration.GatewayConfigurationBuilder;
import io.gravitee.definition.model.Api;
import io.gravitee.definition.model.ExecutionMode;
import io.gravitee.gateway.api.service.ApiKey;
import io.gravitee.gateway.api.service.Subscription;
import io.gravitee.gateway.api.service.SubscriptionService;
import io.gravitee.policy.apikey.ApiKeyPolicyIntegrationTest;
import java.util.Optional;
import org.mockito.stubbing.OngoingStubbing;

/**
* @author Jeoffrey HAEYAERT (jeoffrey.haeyaert at graviteesource.com)
Expand All @@ -37,4 +45,24 @@ public void configureApi(Api api) {
super.configureApi(api);
api.setExecutionMode(ExecutionMode.V3);
}

/**
* This overrides subscription search :
* - in jupiter its searched with getByApiAndSecurityToken
* - in V3 its searches with getById
*/
@Override
protected OngoingStubbing<Optional<Subscription>> whenSearchingSubscription(ApiKey apiKey) {
return when(getBean(SubscriptionService.class).getById(apiKey.getSubscription()));
}

/**
* This overrides 401 response HTTP body content assertion :
* - in jupiter, it's "unauthorized"
* - in V3, it contains more information
*/
@Override
protected void assertUnauthorizedResponseBody(String responseBody) {
assertThat(responseBody).isEqualTo("API Key is not valid or is expired / revoked.");
}
}

0 comments on commit 467fab6

Please sign in to comment.