Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,25 @@
import static com.here.naksha.app.common.CommonApiTestSetup.setupHandlerAndSpace;
import static com.here.naksha.app.common.TestUtil.generateJWT;
import static com.here.naksha.app.common.TestUtil.loadFileOrFail;
import static com.here.naksha.app.common.TestUtil.readOnlyJwt;
import static com.here.naksha.app.common.TestUtil.xyzHubReadOnlyJwt;
import static com.here.naksha.app.common.assertions.ResponseAssertions.assertThat;

import com.here.naksha.app.common.ApiTest;
import com.here.naksha.app.common.NakshaTestWebClient;
import com.here.naksha.app.common.TestUtil;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Stream;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

public class JwtTest extends ApiTest {
// For this test suite, the default test-config.json denotes that
Expand Down Expand Up @@ -99,4 +109,68 @@ public void testDummyModeExpiredJWT() throws Exception {
HttpResponse<String> response = getNakshaClient().get("hub/storages", streamId, "Bearer "+jwt);
assertThat(response).hasStatus(401);
}

/**
* Parameterized auth matrix test.
* Inputs per scenario:
* - HTTP method (GET/POST/PUT/DELETE)
* - endpoint
* - JSON body file (nullable)
* - JWT token
* - expected response status
*/
@ParameterizedTest
@MethodSource("authorizationScenarios")
void testAuthorizationMatrix(
@NotNull String httpMethod,
@NotNull String endpoint,
@Nullable String bodyFile,
@NotNull String jwt,
int expectedStatus) throws Exception {
final String streamId = UUID.randomUUID().toString();
final String authHeader = "Bearer " + jwt;
final String body = bodyFile != null ? loadFileOrFail(bodyFile) : "{}";

HttpResponse<String> response = switch (httpMethod) {
case "GET" -> getNakshaClient().get(endpoint, streamId, authHeader);
case "POST" -> getNakshaClient().post(endpoint, body, streamId, authHeader);
case "PUT" -> getNakshaClient().put(endpoint, body, streamId, authHeader);
case "DELETE" -> getNakshaClient().delete(endpoint, streamId, authHeader);
default -> throw new IllegalArgumentException("Unsupported HTTP method: " + httpMethod);
};

assertThat(response).hasStatus(expectedStatus);
}

static @NotNull Stream<Arguments> authorizationScenarios() {
List<Arguments> scenarios = new ArrayList<>();
final List<String> readOnlyJwts = List.of(readOnlyJwt(), xyzHubReadOnlyJwt());
final String storageId = "auth_test_storage";
final String handlerId = "auth_test_handler";
final String featureId = "auth_test_feature_id";

for (String jwt : readOnlyJwts) {
// Positive scenario: read-only token can read.
scenarios.add(Arguments.of("GET", "hub/storages", null, jwt, 200));

// Negative scenarios: read-only token cannot write.
scenarios.add(Arguments.of("POST", "hub/storages", "Auth/WriteAuthorizationNegative/create_storage.json", jwt, 403));
scenarios.add(Arguments.of("PUT", "hub/storages/" + storageId, "Auth/WriteAuthorizationNegative/update_storage.json", jwt, 403));
scenarios.add(Arguments.of("DELETE", "hub/storages/" + storageId, null, jwt, 403));

scenarios.add(Arguments.of("POST", "hub/handlers", "Auth/WriteAuthorizationNegative/create_event_handler.json", jwt, 403));
scenarios.add(Arguments.of("PUT", "hub/handlers/" + handlerId, "Auth/WriteAuthorizationNegative/update_event_handler.json", jwt, 403));
scenarios.add(Arguments.of("DELETE", "hub/handlers/" + handlerId, null, jwt, 403));

scenarios.add(Arguments.of("POST", "hub/spaces", "Auth/WriteAuthorizationNegative/create_space.json", jwt, 403));
scenarios.add(Arguments.of("PUT", "hub/spaces/" + SPACE_ID, "Auth/WriteAuthorizationNegative/update_space.json", jwt, 403));
scenarios.add(Arguments.of("DELETE", "hub/spaces/" + SPACE_ID, null, jwt, 403));

scenarios.add(Arguments.of("POST", "hub/spaces/" + SPACE_ID + "/features", "Auth/WriteAuthorizationNegative/create_features.json", jwt, 403));
scenarios.add(Arguments.of("PUT", "hub/spaces/" + SPACE_ID + "/features", "Auth/WriteAuthorizationNegative/update_features.json", jwt, 403));
scenarios.add(Arguments.of("DELETE", "hub/spaces/" + SPACE_ID + "/features/" + featureId, null, jwt, 403));
}

return scenarios.stream();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,20 @@ public HttpResponse<String> post(String subPath, String jsonBody, String streamI

public HttpResponse<String> put(String subPath, String jsonBody, String streamId)
throws URISyntaxException, IOException, InterruptedException {
HttpRequest putRequest = requestBuilder(SOCKET_TIMEOUT)
return this.put(subPath, jsonBody, streamId, null);
}

public HttpResponse<String> put(String subPath, String jsonBody, String streamId, @Nullable String jwt)
throws URISyntaxException, IOException, InterruptedException {
Builder requestBuilder = requestBuilder(SOCKET_TIMEOUT)
.uri(nakshaPath(subPath))
.PUT(BodyPublishers.ofString(jsonBody))
.header("Content-Type", "application/json")
.header(HDR_STREAM_ID, streamId)
.build();
return sendOnce(putRequest);
.header(HDR_STREAM_ID, streamId);
if (jwt != null) {
requestBuilder.header(AUTHORIZATION, jwt);
}
return sendOnce(requestBuilder.build());
}

public HttpResponse<String> options(String subPath, String originHeader, String requestMethod, String reqHeaderKeys)
Expand Down Expand Up @@ -154,13 +161,20 @@ public HttpResponse<String> patch(String subPath, String jsonBody, String stream

public HttpResponse<String> delete(String subPath, String streamId)
throws URISyntaxException, IOException, InterruptedException {
HttpRequest deleteRequest = requestBuilder(SOCKET_TIMEOUT)
return this.delete(subPath, streamId, null);
}

public HttpResponse<String> delete(String subPath, String streamId, @Nullable String jwt)
throws URISyntaxException, IOException, InterruptedException {
Builder requestBuilder = requestBuilder(SOCKET_TIMEOUT)
.uri(nakshaPath(subPath))
.DELETE()
.header("Content-Type", "application/json")
.header(HDR_STREAM_ID, streamId)
.build();
return sendOnce(deleteRequest);
.header(HDR_STREAM_ID, streamId);
if (jwt != null) {
requestBuilder.header(AUTHORIZATION, jwt);
}
return sendOnce(requestBuilder.build());
}

private HttpResponse<String> sendOnce(HttpRequest request) throws IOException, InterruptedException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,14 @@ public static String generateJWT(String payload, String privateKeyPath) {
// Sign the following JWT payload
return nakshaAuthProvider.generateToken(new JsonObject(payload));
}

public static String readOnlyJwt() {
final String readOnlyJwtClaims = TestUtil.loadFileOrFail("Auth/readOnlyJwtClaims.json");
return generateJWT(readOnlyJwtClaims);
}

public static String xyzHubReadOnlyJwt() {
final String xyzHubReadOnlyJwtClaims = TestUtil.loadFileOrFail("Auth/xyzHubReadOnlyJwtClaims.json");
return generateJWT(xyzHubReadOnlyJwtClaims);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"id": "auth_readonly_handler_denied",
"type": "EventHandler",
"title": "Readonly JWT denied handler",
"description": "Event handler payload for write authorization negative test",
"className": "com.here.naksha.lib.handlers.DefaultStorageHandler",
"active": true,
"extensionId": null,
"properties": {
"storageId": "auth_readonly_storage_denied"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"speedLimit": "60"
},
"geometry": {
"type": "Point",
"coordinates": [8.68872, 50.0561, 292.94377758]
}
},
{
"type": "Feature",
"properties": {
"speedLimit": "80"
},
"geometry": {
"type": "Point",
"coordinates": [9.68872, 51.0561, 293.94377758]
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"id": "auth_readonly_space_denied",
"type": "Space",
"title": "Readonly JWT denied space",
"description": "Space payload for write authorization negative test",
"eventHandlerIds": [
"auth_readonly_handler_denied"
],
"properties": {
"collection": {
"id": "auth_readonly_collection_denied",
"type": "NakshaCollection"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"id": "auth_readonly_storage_denied",
"type": "Storage",
"title": "Readonly JWT denied storage",
"description": "Storage payload for write authorization negative test",
"className": "naksha.psql.PsqlStorage",
"master": {
"host": "${dataDb.host}",
"db": "${dataDb.db}",
"user": "${dataDb.user}",
"password": "${dataDb.password}",
"port": ${dataDb.port}
},
"properties": {
"schema": "${dataDb.schema}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"id": "auth_test_handler",
"type": "EventHandler",
"title": "Storage Handler for Auth Test (updated)",
"description": "Default Naksha Storage Handler - PUT auth denial",
"className": "com.here.naksha.lib.handlers.DefaultStorageHandler",
"active": true,
"extensionId": null,
"properties": {
"storageId": "auth_test_storage"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"type": "FeatureCollection",
"features": [
{
"id": "auth_test_feature_id",
"type": "Feature",
"properties": {
"speedLimit": "60"
},
"geometry": {
"type": "Point",
"coordinates": [8.68872, 50.0561, 292.94377758]
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"id": "auth_test_space",
"type": "Space",
"title": "Auth Test Space (updated)",
"description": "Space payload for write authorization denial test - PUT",
"eventHandlerIds": [
"auth_test_handler"
],
"properties": {
"collection": {
"id": "um-mod-dev:topology",
"type": "NakshaCollection"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"id": "auth_test_storage",
"type": "Storage",
"title": "Test PSQL storage (updated)",
"description": "PSQL storage instance for testing purpose - PUT auth denial",
"className": "naksha.psql.PsqlStorage",
"master": {
"host": "${dataDb.host}",
"db": "${dataDb.db}",
"user": "${dataDb.user}",
"password": "${dataDb.password}",
"port": ${dataDb.port}
},
"properties": {
"schema": "${dataDb.schema}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"appId": "web-client-custom-app-id",
"userId": "my-custom-user-id",
"urm": {
"naksha": {
"readFeatures": [
{
"storageId": "dev-*"
}
]
}
},
"iat": 1704063599,
"exp": 3000000000
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"appId": "web-client-custom-app-id",
"userId": "my-custom-user-id",
"urm": {
"xyz-hub": {
"readFeatures": [
{
"storageId": "dev-*"
}
]
}
},
"iat": 1704063599,
"exp": 3000000000
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ public void setActive(boolean active) {
getEventHandlerConstructor(getClassName(), EventHandlerConfig.class, eventTargetClass);
} else {
ClassLoader extClassLoader = naksha.getClassLoader(extensionId);
if (extClassLoader == null) {
throw new Exception("Extension " + extensionId + " is not loaded yet for event handler " + getId());
}
//noinspection unchecked
constructor = (Fe3<IEventHandler, INaksha, EventHandlerConfig, EventTarget<?>>) getEventHandlerConstructor(
getClassName(), EventHandlerConfig.class, eventTargetClass, extensionId, extClassLoader);
Expand Down
Loading
Loading