Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ export interface Cors {
enabled?: boolean;
exposeHeaders?: string[];
maxAge?: number;
allowPrivateNetwork?: boolean;
runPolicies?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,22 @@
</mat-form-field>
</div>

<!-- Allow Private Network -->
<div class="cors-card__allow-private-network" [class.disabled]="!corsForm.get('enabled').value">
<gio-form-slide-toggle class="cors-card__allow-private-network__enable-toggle">
<gio-form-label>Access-Control-Allow-Private-Network</gio-form-label>
When enabled, the gateway responds with <code>Access-Control-Allow-Private-Network: true</code> to preflight requests that include
<code>Access-Control-Request-Private-Network: true</code>. This is required for public websites to access private network
resources.
<mat-slide-toggle
gioFormSlideToggle
formControlName="allowPrivateNetwork"
aria-label="CORS allow private network toggle"
name="allowPrivateNetwork"
></mat-slide-toggle>
</gio-form-slide-toggle>
</div>

<!-- Run policies -->
<div class="cors-card__run-policies" [class.disabled]="!corsForm.get('enabled').value">
<gio-form-slide-toggle class="cors-card__run-policies__enable-toggle">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ describe('ApiCorsComponent', () => {
allowCredentials: false,
exposeHeaders: [],
maxAge: -1,
allowPrivateNetwork: false,
runPolicies: false,
});
});
Expand All @@ -143,6 +144,7 @@ describe('ApiCorsComponent', () => {
allowCredentials: true,
maxAge: 10,
exposeHeaders: ['exposeHeaders'],
allowPrivateNetwork: true,
runPolicies: true,
},
},
Expand Down Expand Up @@ -179,6 +181,12 @@ describe('ApiCorsComponent', () => {
expect(await exposeHeadersInput.getTags()).toEqual(['exposeHeaders']);
await exposeHeadersInput.addTag('exposeHeaders2');

const allowPrivateNetworkInput = await loader.getHarness(
MatSlideToggleHarness.with({ selector: '[formControlName="allowPrivateNetwork"]' }),
);
expect(await allowPrivateNetworkInput.isChecked()).toEqual(true);
await allowPrivateNetworkInput.toggle();

const runPoliciesInput = await loader.getHarness(MatSlideToggleHarness.with({ selector: '[formControlName="runPolicies"]' }));
expect(await runPoliciesInput.isChecked()).toEqual(true);
await runPoliciesInput.toggle();
Expand All @@ -197,6 +205,7 @@ describe('ApiCorsComponent', () => {
allowCredentials: false,
maxAge: 20,
exposeHeaders: ['exposeHeaders', 'exposeHeaders2'],
allowPrivateNetwork: false,
runPolicies: false,
});
});
Expand Down Expand Up @@ -238,6 +247,7 @@ describe('ApiCorsComponent', () => {
allowCredentials: false,
exposeHeaders: [],
maxAge: -1,
allowPrivateNetwork: false,
runPolicies: false,
});
});
Expand Down Expand Up @@ -310,6 +320,7 @@ describe('ApiCorsComponent', () => {
allowCredentials: false,
exposeHeaders: [],
maxAge: -1,
allowPrivateNetwork: false,
runPolicies: false,
});
});
Expand Down Expand Up @@ -361,6 +372,7 @@ describe('ApiCorsComponent', () => {
allowCredentials: false,
exposeHeaders: [],
maxAge: -1,
allowPrivateNetwork: false,
runPolicies: false,
});
});
Expand All @@ -382,6 +394,7 @@ describe('ApiCorsComponent', () => {
allowCredentials: true,
maxAge: 10,
exposeHeaders: ['exposeHeaders'],
allowPrivateNetwork: true,
runPolicies: true,
},
},
Expand Down Expand Up @@ -419,6 +432,12 @@ describe('ApiCorsComponent', () => {
expect(await exposeHeadersInput.getTags()).toEqual(['exposeHeaders']);
await exposeHeadersInput.addTag('exposeHeaders2');

const allowPrivateNetworkInput = await loader.getHarness(
MatSlideToggleHarness.with({ selector: '[formControlName="allowPrivateNetwork"]' }),
);
expect(await allowPrivateNetworkInput.isChecked()).toEqual(true);
await allowPrivateNetworkInput.toggle();

const runPoliciesInput = await loader.getHarness(MatSlideToggleHarness.with({ selector: '[formControlName="runPolicies"]' }));
expect(await runPoliciesInput.isChecked()).toEqual(true);
await runPoliciesInput.toggle();
Expand All @@ -437,6 +456,7 @@ describe('ApiCorsComponent', () => {
allowCredentials: false,
maxAge: 20,
exposeHeaders: ['exposeHeaders', 'exposeHeaders2'],
allowPrivateNetwork: false,
runPolicies: false,
});
});
Expand Down Expand Up @@ -475,6 +495,7 @@ describe('ApiCorsComponent', () => {
allowCredentials: false,
exposeHeaders: [],
maxAge: -1,
allowPrivateNetwork: false,
runPolicies: false,
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ export class ApiCorsComponent implements OnInit, OnDestroy {
value: cors.exposeHeaders ?? [],
disabled: isCorsDisabled,
}),
allowPrivateNetwork: new UntypedFormControl({
value: cors.allowPrivateNetwork ?? false,
disabled: isCorsDisabled,
}),
runPolicies: new UntypedFormControl({
value: cors.runPolicies ?? false,
disabled: isCorsDisabled,
Expand All @@ -131,7 +135,16 @@ export class ApiCorsComponent implements OnInit, OnDestroy {
this.initialCorsFormValue = this.corsForm.getRawValue();

// Disable all Control if enabled is not checked
const controlKeys = ['allowOrigin', 'allowMethods', 'allowHeaders', 'allowCredentials', 'maxAge', 'exposeHeaders', 'runPolicies'];
const controlKeys = [
'allowOrigin',
'allowMethods',
'allowHeaders',
'allowCredentials',
'maxAge',
'exposeHeaders',
'allowPrivateNetwork',
'runPolicies',
];
this.corsForm.get('enabled').valueChanges.subscribe(checked => {
controlKeys.forEach(k => {
return checked ? this.corsForm.get(k).enable() : this.corsForm.get(k).disable();
Expand Down Expand Up @@ -205,6 +218,7 @@ export class ApiCorsComponent implements OnInit, OnDestroy {
allowCredentials: corsFormValue.allowCredentials,
maxAge: corsFormValue.maxAge,
exposeHeaders: corsFormValue.exposeHeaders,
allowPrivateNetwork: corsFormValue.allowPrivateNetwork,
runPolicies: corsFormValue.runPolicies,
},
},
Expand All @@ -223,6 +237,7 @@ export class ApiCorsComponent implements OnInit, OnDestroy {
allowCredentials: corsFormValue.allowCredentials,
maxAge: corsFormValue.maxAge,
exposeHeaders: corsFormValue.exposeHeaders,
allowPrivateNetwork: corsFormValue.allowPrivateNetwork,
runPolicies: corsFormValue.runPolicies,
};
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public Cors deserialize(JsonParser jp, DeserializationContext ctxt) throws IOExc
cors.setAccessControlMaxAge(-1);
}
cors.setRunPolicies(node.path("runPolicies").asBoolean(false));
cors.setAllowPrivateNetwork(node.path("allowPrivateNetwork").asBoolean(false));
}

return cors;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ public void serialize(Cors cors, JsonGenerator jgen, SerializerProvider provider
jgen.writeBooleanField("runPolicies", cors.isRunPolicies());
}

if (cors.isAllowPrivateNetwork()) {
jgen.writeBooleanField("allowPrivateNetwork", cors.isAllowPrivateNetwork());
}

jgen.writeEndObject();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ public class Cors implements Serializable {
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
private boolean runPolicies;

@JsonProperty("allowPrivateNetwork")
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
private boolean allowPrivateNetwork;

public static int getDefaultErrorStatusCode() {
return DEFAULT_ERROR_STATUS_CODE;
}
Expand Down Expand Up @@ -164,4 +168,12 @@ public boolean isRunPolicies() {
public void setRunPolicies(boolean runPolicies) {
this.runPolicies = runPolicies;
}

public boolean isAllowPrivateNetwork() {
return allowPrivateNetwork;
}

public void setAllowPrivateNetwork(boolean allowPrivateNetwork) {
this.allowPrivateNetwork = allowPrivateNetwork;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ private void handlePreflightRequest(Request request, Response response) {
.headers()
.set(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, String.join(JOINER_CHAR_SEQUENCE, cors.getAccessControlAllowHeaders()));

// 11. Private Network Access: if enabled and request includes Access-Control-Request-Private-Network,
// respond with Access-Control-Allow-Private-Network: true
if (cors.isAllowPrivateNetwork()) {
String pnaRequest = request.headers().getFirst(HttpHeaders.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK);
if ("true".equalsIgnoreCase(pnaRequest)) {
response.headers().set(HttpHeaders.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK, "true");
}
}

response.status(HttpStatusCode.OK_200);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
* @param ctx current context
* @return true if the Preflight Request pass, false otherwise
*/
private boolean handlePreflightRequest(final Cors cors, final HttpBaseExecutionContext ctx) {

Check failure on line 92 in gravitee-apim-gateway/gravitee-apim-gateway-handlers/gravitee-apim-gateway-handlers-api/src/main/java/io/gravitee/gateway/reactive/handlers/api/processor/cors/CorsPreflightRequestProcessor.java

View check run for this annotation

SonarQubeCloud / [Gravitee.io APIM - Gateway] SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=gravitee-io_gravitee-api-management_gateway&issues=AZ0kktgW0ICNWsssip-E&open=AZ0kktgW0ICNWsssip-E&pullRequest=15957
final HttpBaseRequest request = ctx.request();
final HttpBaseResponse response = ctx.response();

Expand Down Expand Up @@ -177,6 +177,16 @@
.headers()
.set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, String.join(JOINER_CHAR_SEQUENCE, cors.getAccessControlAllowHeaders()));
}

// 11. Private Network Access: if enabled and request includes Access-Control-Request-Private-Network,
// respond with Access-Control-Allow-Private-Network: true
if (cors.isAllowPrivateNetwork()) {
String pnaRequest = request.headers().get(HttpHeaderNames.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK);
if ("true".equalsIgnoreCase(pnaRequest)) {
response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK, "true");
}
}

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
*/
package io.gravitee.gateway.reactive.handlers.api.processor.cors;

import static io.gravitee.gateway.api.http.HttpHeaderNames.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK;
import static io.gravitee.gateway.api.http.HttpHeaderNames.ACCESS_CONTROL_REQUEST_METHOD;
import static io.gravitee.gateway.api.http.HttpHeaderNames.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK;
import static io.gravitee.gateway.api.http.HttpHeaderNames.ORIGIN;
import static io.gravitee.gateway.reactive.api.context.InternalContextAttributes.ATTR_INTERNAL_INVOKER;
import static io.gravitee.gateway.reactive.api.context.InternalContextAttributes.ATTR_INTERNAL_SECURITY_SKIP;
Expand Down Expand Up @@ -318,6 +320,35 @@ public void shouldInterruptWithWildcardHeadersWhenCorsEnabledAndValidRequest() {
assertThat(spyCtx.<CorsPreflightInvoker>getInternalAttribute(ATTR_INTERNAL_INVOKER)).isNull();
}

@Test
public void shouldSetAllowPrivateNetworkHeaderWhenEnabledAndRequestHasPnaHeader() {
api.getProxy().getCors().setAllowPrivateNetwork(true);
spyRequestHeaders.set(ORIGIN, "origin");
spyRequestHeaders.set(ACCESS_CONTROL_REQUEST_METHOD, "GET");
spyRequestHeaders.set(ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true");
corsPreflightRequestProcessor.execute(spyCtx).test().assertError(InterruptionException.class);
assertThat(spyResponseHeaders.get(ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isEqualTo("true");
}

@Test
public void shouldNotSetAllowPrivateNetworkHeaderWhenEnabledButRequestMissingPnaHeader() {
api.getProxy().getCors().setAllowPrivateNetwork(true);
spyRequestHeaders.set(ORIGIN, "origin");
spyRequestHeaders.set(ACCESS_CONTROL_REQUEST_METHOD, "GET");
corsPreflightRequestProcessor.execute(spyCtx).test().assertError(InterruptionException.class);
assertThat(spyResponseHeaders.get(ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isNull();
}

@Test
public void shouldNotSetAllowPrivateNetworkHeaderWhenDisabledAndRequestHasPnaHeader() {
api.getProxy().getCors().setAllowPrivateNetwork(false);
spyRequestHeaders.set(ORIGIN, "origin");
spyRequestHeaders.set(ACCESS_CONTROL_REQUEST_METHOD, "GET");
spyRequestHeaders.set(ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true");
corsPreflightRequestProcessor.execute(spyCtx).test().assertError(InterruptionException.class);
assertThat(spyResponseHeaders.get(ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isNull();
}

@Test
public void shouldCompleteWithoutAddingHeadersWhenCorsWildcardAndInvalidRequest() {
api.getProxy().getCors().setAccessControlAllowMethods(Set.of("*"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4179,6 +4179,9 @@ components:
default: -1
runPolicies:
type: boolean
allowPrivateNetwork:
type: boolean
description: Allow private network access (PNA) requests during CORS preflight
Dlq:
type: object
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ private CorsFixtures() {}
.enabled(true)
.exposeHeaders(Set.of("exposeHeader1", "exposeHeader2"))
.maxAge(10)
.runPolicies(true);
.runPolicies(true)
.allowPrivateNetwork(true);

public static Cors aCors() {
return BASE_CORS.get();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ void shouldMapToCorsEntity() {
assertThat(corsEntity.getAccessControlExposeHeaders()).isEqualTo(cors.getExposeHeaders());
assertThat(corsEntity.getAccessControlMaxAge()).isEqualTo(cors.getMaxAge());
assertThat(corsEntity.isRunPolicies()).isEqualTo(cors.getRunPolicies());
assertThat(corsEntity.isAllowPrivateNetwork()).isEqualTo(cors.getAllowPrivateNetwork());
}

@Test
Expand All @@ -55,5 +56,6 @@ void shouldMapFromCorsEntity() {
assertThat(cors.getExposeHeaders()).isEqualTo(corsEntity.getAccessControlExposeHeaders());
assertThat(cors.getMaxAge()).isEqualTo(corsEntity.getAccessControlMaxAge());
assertThat(cors.getRunPolicies()).isEqualTo(corsEntity.isRunPolicies());
assertThat(cors.getAllowPrivateNetwork()).isEqualTo(corsEntity.isAllowPrivateNetwork());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ private CorsModelFixtures() {}
.enabled(true)
.accessControlExposeHeaders(Set.of("exposeHeader1", "exposeHeader2"))
.accessControlMaxAge(10)
.runPolicies(true);
.runPolicies(true)
.allowPrivateNetwork(true);

public static io.gravitee.definition.model.Cors aModelCors() {
return BASE_MODEL_CORS.build();
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@
<gravitee-cockpit-api.version>3.11.0</gravitee-cockpit-api.version>
<gravitee-cloud-initializer.version>2.3.1</gravitee-cloud-initializer.version>
<logback-ecs-encoder.version>1.7.0</logback-ecs-encoder.version>
<gravitee-common.version>4.9.0</gravitee-common.version>
<gravitee-common.version>4.9.1</gravitee-common.version>
<gravitee-common-mcp.version>1.0.0</gravitee-common-mcp.version>
<gravitee-connector-api.version>1.1.5</gravitee-connector-api.version>
<gravitee-exchange.version>2.0.1</gravitee-exchange.version>
<gravitee-expression-language.version>4.3.0</gravitee-expression-language.version>
<gravitee-fetcher-api.version>2.1.0</gravitee-fetcher-api.version>
<gravitee-gateway-api.version>5.1.0</gravitee-gateway-api.version>
<gravitee-gateway-api.version>5.1.1</gravitee-gateway-api.version>
<gravitee-integration-api.version>5.1.0</gravitee-integration-api.version>
<gravitee-json-validation.version>2.2.0</gravitee-json-validation.version>
<gravitee-kubernetes.version>3.7.1</gravitee-kubernetes.version>
Expand Down