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
2 changes: 1 addition & 1 deletion gateway-service-knoxidf/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with this
* work for additional information regarding copyright ownership. The ASF
* licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.apache.knox.gateway.service.knoxidf;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

/**
* Request body for registering a trusted OIDC issuer via {@link TrustedOidcIssuersResource#registerIssuer(String)}.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class RegisterIssuerRequest {

private String issuerUrl;
private boolean dynamicJwks;
private String clusterName;

public String getIssuerUrl() {
return issuerUrl;
}

public void setIssuerUrl(String issuerUrl) {
this.issuerUrl = issuerUrl;
}

public boolean isDynamicJwks() {
return dynamicJwks;
}

public void setDynamicJwks(boolean dynamicJwks) {
this.dynamicJwks = dynamicJwks;
}

public String getClusterName() {
return clusterName;
}

public void setClusterName(String clusterName) {
this.clusterName = clusterName;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
*/
package org.apache.knox.gateway.service.knoxidf;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.knox.gateway.audit.api.Action;
import org.apache.knox.gateway.audit.api.ActionOutcome;
Expand Down Expand Up @@ -90,14 +89,15 @@ public Response registerIssuer(String body) {
String outcome = ActionOutcome.FAILURE;

try {
final Map<String, Object> parsed;
final RegisterIssuerRequest parsed;
try {
parsed = MAPPER.readValue(body, new TypeReference<Map<String, Object>>() {});
parsed = MAPPER.readValue(body, RegisterIssuerRequest.class);
} catch (IOException e) {
return errorResponse(Response.Status.BAD_REQUEST, "invalid_request", "Malformed JSON body");
return errorResponse(Response.Status.BAD_REQUEST, "invalid_request",
"Malformed or invalid JSON body");
}

final String rawUrl = (String) parsed.get("issuerUrl");
final String rawUrl = parsed.getIssuerUrl();
issuerUrl = (rawUrl != null && !rawUrl.isEmpty()) ? rawUrl : "UNKNOWN_ISSUER";

if (rawUrl == null || rawUrl.isEmpty()) {
Expand All @@ -112,13 +112,17 @@ public Response registerIssuer(String body) {
"Issuer already registered: " + rawUrl);
}

final boolean dynamicJwks = Boolean.TRUE.equals(parsed.get("dynamicJwks"));
final String clusterName = (String) parsed.get("clusterName");

trustedIssuers.register(new TrustedOidcIssuer(rawUrl, dynamicJwks, clusterName,
Instant.now(), operatorId));
trustedIssuers.register(new TrustedOidcIssuer(rawUrl, parsed.isDynamicJwks(),
parsed.getClusterName(), Instant.now(), operatorId));
outcome = ActionOutcome.SUCCESS;
return Response.status(Response.Status.CREATED).build();
} catch (IllegalStateException e) {
// The service throws IllegalStateException when the configured maximum number of
// registered issuers (MAX_TRUSTED_ISSUERS) is reached. This is an operator-facing
// capacity condition, distinct from an internal storage failure, so report it as a
// 409 rather than lumping it into the generic 500 storage_error path below.
return errorResponse(Response.Status.CONFLICT, "issuer_limit_reached",
"Maximum number of registered trusted issuers reached");
} catch (RuntimeException e) {
return errorResponse(Response.Status.INTERNAL_SERVER_ERROR, "storage_error",
"Failed to register issuer");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
*/
package org.apache.knox.gateway.service.knoxidf;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.knox.gateway.audit.api.Action;
import org.apache.knox.gateway.audit.api.ActionOutcome;
Expand Down Expand Up @@ -251,6 +250,42 @@ public void testAuditRegisterStorageFailure() {
EasyMock.verify(mockService, mockAuditor);
}

@Test
public void testRegisterWrongTypeFieldReturnsBadRequest() {
// A syntactically valid JSON body with a type-mismatched field (clusterName as an
// array instead of a string). Binding to the typed RegisterIssuerRequest bean makes
// Jackson reject this during deserialization, so it is a 400 invalid_request rather
// than a ClassCastException surfacing as a 500. No service calls are expected; audit
// fires with the INVALID_REQUEST sentinel because parsing failed before the URL was read.
expectAudit("INVALID_REQUEST", ActionOutcome.FAILURE, "issuer_registered");
EasyMock.replay(mockService, mockAuditor);

final Response response = resource.registerIssuer(
"{\"issuerUrl\":\"" + ISSUER_A + "\",\"clusterName\":[1,2,3]}");

assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
assertErrorField(response, "invalid_request");
EasyMock.verify(mockService, mockAuditor);
}

@Test
public void testRegisterIssuerLimitReached() {
// The service throws IllegalStateException when MAX_TRUSTED_ISSUERS is reached. This is
// an operator-facing capacity condition and must map to 409 issuer_limit_reached, not
// the generic 500 storage_error used for genuine storage failures.
EasyMock.expect(mockService.isTrusted(ISSUER_A)).andReturn(false).once();
mockService.register(EasyMock.anyObject(TrustedOidcIssuer.class));
EasyMock.expectLastCall().andThrow(new IllegalStateException("MAX_TRUSTED_ISSUERS (100) reached")).once();
expectAudit(ISSUER_A, ActionOutcome.FAILURE, "issuer_registered");
EasyMock.replay(mockService, mockAuditor);

final Response response = resource.registerIssuer(buildRegisterBody(ISSUER_A, false, null));

assertEquals(Response.Status.CONFLICT.getStatusCode(), response.getStatus());
assertErrorField(response, "issuer_limit_reached");
EasyMock.verify(mockService, mockAuditor);
}

// ---------------------------------------------------------------------------
// DELETE /
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -531,8 +566,9 @@ private static void assertErrorField(Response response, String expectedError) {
body.contains(expectedError));
}

@SuppressWarnings("unchecked")
private static List<Map<String, Object>> parseJsonList(String json) throws Exception {
return new ObjectMapper().readValue(json, new TypeReference<List<Map<String, Object>>>() {});
return new ObjectMapper().readValue(json, List.class);
}

private static Map<String, Object> findByIssuerUrl(List<Map<String, Object>> list,
Expand Down
Loading