This is an automated email from the ASF dual-hosted git repository.
smolnar82 pushed a commit to branch knox_idf
in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/knox_idf by this push:
new 3c30bfbb5 KNOX-3368 - Trusted OIDC Issuer admin API for Knox IDF
(#1327)
3c30bfbb5 is described below
commit 3c30bfbb5af9aef446e484c3bd85687bf6f7ae72
Author: hsheinblatt <[email protected]>
AuthorDate: Fri Jul 24 01:43:07 2026 -0700
KNOX-3368 - Trusted OIDC Issuer admin API for Knox IDF (#1327)
---
.../knoxidf/TrustedOidcIssuersResource.java | 227 +++++++++
.../KnoxIDFAdminServiceDeploymentContributor.java | 51 ++
...nox.gateway.deploy.ServiceDeploymentContributor | 1 +
.../knoxidf/TrustedOidcIssuersResourceTest.java | 551 +++++++++++++++++++++
...oxIDFAdminServiceDeploymentContributorTest.java | 145 ++++++
.../org/apache/knox/gateway/audit/api/Action.java | 1 +
.../knox/gateway/audit/api/ResourceType.java | 1 +
7 files changed, 977 insertions(+)
diff --git
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TrustedOidcIssuersResource.java
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TrustedOidcIssuersResource.java
new file mode 100644
index 000000000..b8e8b393e
--- /dev/null
+++
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TrustedOidcIssuersResource.java
@@ -0,0 +1,227 @@
+/*
+ * 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.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;
+import org.apache.knox.gateway.audit.api.AuditServiceFactory;
+import org.apache.knox.gateway.audit.api.Auditor;
+import org.apache.knox.gateway.audit.api.ResourceType;
+import org.apache.knox.gateway.audit.log4j.audit.AuditConstants;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.commons.lang3.StringUtils;
+import
org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.TrustedOidcIssuer;
+import
org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.TrustedOidcIssuerService;
+import org.apache.knox.gateway.util.JsonUtils;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.Principal;
+import java.time.Instant;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Path(TrustedOidcIssuersResource.RESOURCE_PATH)
+@Produces(MediaType.APPLICATION_JSON)
+public class TrustedOidcIssuersResource {
+
+ static final String RESOURCE_PATH =
"knoxidf/issuers-admin/v1/trusted-oidc-issuers";
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ // Non-final and package-private to allow test injection of a mock Auditor.
+ static Auditor auditor = AuditServiceFactory.getAuditService()
+ .getAuditor(AuditConstants.DEFAULT_AUDITOR_NAME,
+ AuditConstants.KNOX_SERVICE_NAME,
AuditConstants.KNOX_COMPONENT_NAME);
+
+ @Context
+ private ServletContext servletContext;
+
+ @Context
+ private HttpServletRequest request;
+
+ private TrustedOidcIssuerService trustedIssuers;
+
+ @PostConstruct
+ public void init() {
+ final GatewayServices services = (GatewayServices)
+
servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+ trustedIssuers =
services.getService(ServiceType.TRUSTED_OIDC_ISSUER_SERVICE);
+ }
+
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response registerIssuer(String body) {
+ String issuerUrl = "INVALID_REQUEST";
+ final String operatorId = getOperatorId();
+ String outcome = ActionOutcome.FAILURE;
+
+ try {
+ final Map<String, Object> parsed;
+ try {
+ parsed = MAPPER.readValue(body, new TypeReference<Map<String,
Object>>() {});
+ } catch (IOException e) {
+ return errorResponse(Response.Status.BAD_REQUEST, "invalid_request",
"Malformed JSON body");
+ }
+
+ final String rawUrl = (String) parsed.get("issuerUrl");
+ issuerUrl = (rawUrl != null && !rawUrl.isEmpty()) ? rawUrl :
"UNKNOWN_ISSUER";
+
+ if (rawUrl == null || rawUrl.isEmpty()) {
+ return errorResponse(Response.Status.BAD_REQUEST, "invalid_request",
"issuerUrl is required");
+ }
+ if (!isHttpsUrl(rawUrl)) {
+ return errorResponse(Response.Status.BAD_REQUEST, "invalid_request",
+ "issuerUrl must use HTTPS scheme");
+ }
+ if (trustedIssuers.isTrusted(rawUrl)) {
+ return errorResponse(Response.Status.CONFLICT, "issuer_exists",
+ "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));
+ outcome = ActionOutcome.SUCCESS;
+ return Response.status(Response.Status.CREATED).build();
+ } catch (RuntimeException e) {
+ return errorResponse(Response.Status.INTERNAL_SERVER_ERROR,
"storage_error",
+ "Failed to register issuer");
+ } finally {
+ auditor.audit(Action.DELEGATION_LIFECYCLE, issuerUrl,
ResourceType.TRUSTED_ISSUER,
+ outcome, "event_type=issuer_registered performed_by=" +
auditLabel(operatorId));
+ }
+ }
+
+ @DELETE
+ public Response removeIssuer(@QueryParam("issuerUrl") String issuerUrl) {
+ final String operatorId = getOperatorId();
+ final String auditIssuerUrl = StringUtils.isBlank(issuerUrl) ?
"UNKNOWN_ISSUER" : issuerUrl;
+ String outcome = ActionOutcome.FAILURE;
+
+ try {
+ if (StringUtils.isBlank(issuerUrl)) {
+ return errorResponse(Response.Status.BAD_REQUEST, "invalid_request",
+ "issuerUrl query parameter is required");
+ }
+
+ // deregister is idempotent at the service layer: it returns silently if
the issuer is
+ // not registered. Admins deleting a non-existent issuer receive the
same 204 and audit
+ // event as a successful delete — there is no separate 404 path at this
layer.
+ trustedIssuers.deregister(issuerUrl);
+ outcome = ActionOutcome.SUCCESS;
+ return Response.noContent().build();
+ } catch (RuntimeException e) {
+ return errorResponse(Response.Status.INTERNAL_SERVER_ERROR,
"storage_error",
+ "Failed to remove issuer");
+ } finally {
+ auditor.audit(Action.DELEGATION_LIFECYCLE, auditIssuerUrl,
ResourceType.TRUSTED_ISSUER,
+ outcome, "event_type=issuer_removed performed_by=" +
auditLabel(operatorId));
+ }
+ }
+
+ @GET
+ public Response listIssuers() {
+ final List<Map<String, Object>> result = trustedIssuers.list().stream()
+ .map(this::issuerToMap)
+ .collect(Collectors.toList());
+ return Response.ok(JsonUtils.renderAsJsonString(result)).build();
+ }
+
+ @POST
+ @Path("/refresh-jwks")
+ public Response refreshJwksUri(@QueryParam("issuerUrl") String issuerUrl) {
+ final String operatorId = getOperatorId();
+ final String auditIssuerUrl = StringUtils.isBlank(issuerUrl) ?
"UNKNOWN_ISSUER" : issuerUrl;
+ String outcome = ActionOutcome.FAILURE;
+
+ try {
+ if (StringUtils.isBlank(issuerUrl)) {
+ return errorResponse(Response.Status.BAD_REQUEST, "invalid_request",
+ "issuerUrl query parameter is required");
+ }
+
+ // No-op at the service layer if the issuer is not registered or not
configured for
+ // dynamic JWKS; still returns 204 so the caller does not need to check
existence first.
+ trustedIssuers.refreshJwksUri(issuerUrl);
+ outcome = ActionOutcome.SUCCESS;
+ return Response.noContent().build();
+ } catch (RuntimeException e) {
+ return errorResponse(Response.Status.INTERNAL_SERVER_ERROR,
"storage_error",
+ "Failed to refresh JWKS URI");
+ } finally {
+ auditor.audit(Action.DELEGATION_LIFECYCLE, auditIssuerUrl,
ResourceType.TRUSTED_ISSUER,
+ outcome, "event_type=issuer_jwks_refreshed performed_by=" +
auditLabel(operatorId));
+ }
+ }
+
+ private String getOperatorId() {
+ final Principal principal = request.getUserPrincipal();
+ return principal != null ? principal.getName() : null;
+ }
+
+ private static String auditLabel(String operatorId) {
+ return operatorId != null ? operatorId : "ANONYMOUS";
+ }
+
+ private Map<String, Object> issuerToMap(TrustedOidcIssuer issuer) {
+ final Map<String, Object> map = new LinkedHashMap<>();
+ map.put("issuerUrl", issuer.getIssuerUrl());
+ map.put("dynamicJwks", issuer.isDynamicJwks());
+ map.put("clusterName", issuer.getClusterName());
+ map.put("registeredAt",
+ issuer.getRegisteredAt() != null ? issuer.getRegisteredAt().toString()
: null);
+ map.put("registeredBy", issuer.getRegisteredBy());
+ return map;
+ }
+
+ private static boolean isHttpsUrl(String url) {
+ try {
+ return "https".equalsIgnoreCase(new URI(url).getScheme());
+ } catch (URISyntaxException e) {
+ return false;
+ }
+ }
+
+ private static Response errorResponse(Response.Status status, String error,
String description) {
+ final Map<String, String> body = new LinkedHashMap<>();
+ body.put("error", error);
+ body.put("error_description", description);
+ return
Response.status(status).entity(JsonUtils.renderAsJsonString(body)).build();
+ }
+}
diff --git
a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/deploy/KnoxIDFAdminServiceDeploymentContributor.java
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/deploy/KnoxIDFAdminServiceDeploymentContributor.java
new file mode 100644
index 000000000..060774a93
--- /dev/null
+++
b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/deploy/KnoxIDFAdminServiceDeploymentContributor.java
@@ -0,0 +1,51 @@
+/*
+ * 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.deploy;
+
+import org.apache.knox.gateway.jersey.JerseyServiceDeploymentContributorBase;
+
+/**
+ * Deployment contributor for the KNOXIDF_ADMIN service role, which hosts the
+ * trusted OIDC issuer admin REST API. This contributor registers
+ * {@link org.apache.knox.gateway.service.knoxidf.TrustedOidcIssuersResource}
+ * under the {@code knoxidf/issuers-admin/**?**} pattern, which is disjoint
from
+ * the KNOXIDF role's {@code knoxidf/api/**?**} pattern. This ensures the
KNOXIDF
+ * role cannot serve admin endpoints, and that per-role AclsAuthz authorization
+ * ({@code KNOXIDF_ADMIN.acl}) applies only to trusted-issuer admin requests.
+ */
+public class KnoxIDFAdminServiceDeploymentContributor extends
JerseyServiceDeploymentContributorBase {
+
+ @Override
+ public String getRole() {
+ return "KNOXIDF_ADMIN";
+ }
+
+ @Override
+ public String getName() {
+ return "KNOXIDF_ADMIN";
+ }
+
+ @Override
+ protected String[] getPackages() {
+ return new String[] { "org.apache.knox.gateway.service.knoxidf" };
+ }
+
+ @Override
+ protected String[] getPatterns() {
+ return new String[] { "knoxidf/issuers-admin/**?**" };
+ }
+}
diff --git
a/gateway-service-knoxidf/src/main/resources/META-INF/services/org.apache.knox.gateway.deploy.ServiceDeploymentContributor
b/gateway-service-knoxidf/src/main/resources/META-INF/services/org.apache.knox.gateway.deploy.ServiceDeploymentContributor
index 49fc687f1..1fca75abb 100644
---
a/gateway-service-knoxidf/src/main/resources/META-INF/services/org.apache.knox.gateway.deploy.ServiceDeploymentContributor
+++
b/gateway-service-knoxidf/src/main/resources/META-INF/services/org.apache.knox.gateway.deploy.ServiceDeploymentContributor
@@ -16,3 +16,4 @@
# limitations under the License.
##########################################################################
org.apache.knox.gateway.service.knoxidf.deploy.KnoxIDFServiceDeploymentContributor
+org.apache.knox.gateway.service.knoxidf.deploy.KnoxIDFAdminServiceDeploymentContributor
diff --git
a/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/TrustedOidcIssuersResourceTest.java
b/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/TrustedOidcIssuersResourceTest.java
new file mode 100644
index 000000000..e082d3255
--- /dev/null
+++
b/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/TrustedOidcIssuersResourceTest.java
@@ -0,0 +1,551 @@
+/*
+ * 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.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;
+import org.apache.knox.gateway.audit.api.Auditor;
+import org.apache.knox.gateway.audit.api.ResourceType;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import
org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.TrustedOidcIssuer;
+import
org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.TrustedOidcIssuerService;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.Response;
+import java.lang.reflect.Field;
+import java.security.Principal;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class TrustedOidcIssuersResourceTest {
+
+ private static final String ISSUER_A = "https://issuer-a.example.com";
+ private static final String ISSUER_B = "https://issuer-b.example.com";
+ private static final String OPERATOR = "admin";
+
+ // Capture the real static Auditor so @After can restore it.
+ private static final Auditor ORIGINAL_AUDITOR =
TrustedOidcIssuersResource.auditor;
+
+ private TrustedOidcIssuersResource resource;
+ private TrustedOidcIssuerService mockService;
+ private Auditor mockAuditor;
+
+ @Before
+ public void setUp() throws Exception {
+ mockService = EasyMock.createMock(TrustedOidcIssuerService.class);
+ mockAuditor = EasyMock.createMock(Auditor.class);
+ TrustedOidcIssuersResource.auditor = mockAuditor;
+ resource = buildResource(buildPrincipal(OPERATOR));
+ }
+
+ @After
+ public void tearDown() {
+ TrustedOidcIssuersResource.auditor = ORIGINAL_AUDITOR;
+ }
+
+ //
---------------------------------------------------------------------------
+ // POST /register
+ //
---------------------------------------------------------------------------
+
+ @Test
+ public void testRegisterIssuer() {
+ EasyMock.expect(mockService.isTrusted(ISSUER_A)).andReturn(false).once();
+ final Capture<TrustedOidcIssuer> capturedIssuer = EasyMock.newCapture();
+ mockService.register(EasyMock.capture(capturedIssuer));
+ EasyMock.expectLastCall().once();
+ final Capture<String> auditMsg = EasyMock.newCapture();
+ mockAuditor.audit(
+ EasyMock.eq(Action.DELEGATION_LIFECYCLE), EasyMock.eq(ISSUER_A),
+ EasyMock.eq(ResourceType.TRUSTED_ISSUER),
EasyMock.eq(ActionOutcome.SUCCESS),
+ EasyMock.capture(auditMsg));
+ EasyMock.expectLastCall().once();
+ EasyMock.replay(mockService, mockAuditor);
+
+ final Response response =
resource.registerIssuer(buildRegisterBody(ISSUER_A, false, null));
+
+ assertEquals(Response.Status.CREATED.getStatusCode(),
response.getStatus());
+ assertEquals(ISSUER_A, capturedIssuer.getValue().getIssuerUrl());
+ assertFalse(capturedIssuer.getValue().isDynamicJwks());
+ assertNull(capturedIssuer.getValue().getClusterName());
+ assertEquals(OPERATOR, capturedIssuer.getValue().getRegisteredBy());
+ assertNotNull(capturedIssuer.getValue().getRegisteredAt());
+ assertTrue(auditMsg.getValue().contains("event_type=issuer_registered"));
+ assertTrue(auditMsg.getValue().contains("performed_by=" + OPERATOR));
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testRegisterWithClusterNameAndDynamicJwks() {
+ EasyMock.expect(mockService.isTrusted(ISSUER_A)).andReturn(false).once();
+ final Capture<TrustedOidcIssuer> capturedIssuer = EasyMock.newCapture();
+ mockService.register(EasyMock.capture(capturedIssuer));
+ EasyMock.expectLastCall().once();
+ expectAudit(ISSUER_A, ActionOutcome.SUCCESS, "issuer_registered");
+ EasyMock.replay(mockService, mockAuditor);
+
+ final Response response = resource.registerIssuer(
+ buildRegisterBody(ISSUER_A, true, "production-cluster"));
+
+ assertEquals(Response.Status.CREATED.getStatusCode(),
response.getStatus());
+ assertTrue(capturedIssuer.getValue().isDynamicJwks());
+ assertEquals("production-cluster",
capturedIssuer.getValue().getClusterName());
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testRegisterNonHttpsUrl() {
+ final String httpUrl = "http://insecure.example.com";
+ // No service calls expected; audit fires with the non-HTTPS URL as
resource name.
+ expectAudit(httpUrl, ActionOutcome.FAILURE, "issuer_registered");
+ EasyMock.replay(mockService, mockAuditor);
+
+ final Response response =
resource.registerIssuer(buildRegisterBody(httpUrl, false, null));
+
+ assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
response.getStatus());
+ assertErrorField(response, "invalid_request");
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testRegisterMissingIssuerUrl() {
+ // issuerUrl field absent from JSON → sentinel UNKNOWN_ISSUER used as
audit resource name.
+ expectAudit("UNKNOWN_ISSUER", ActionOutcome.FAILURE, "issuer_registered");
+ EasyMock.replay(mockService, mockAuditor);
+
+ assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
+ resource.registerIssuer("{\"dynamicJwks\":false}").getStatus());
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testRegisterEmptyIssuerUrl() {
+ // Empty string issuerUrl → same sentinel UNKNOWN_ISSUER as null case.
+ expectAudit("UNKNOWN_ISSUER", ActionOutcome.FAILURE, "issuer_registered");
+ EasyMock.replay(mockService, mockAuditor);
+
+ assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
+
resource.registerIssuer("{\"issuerUrl\":\"\",\"dynamicJwks\":false}").getStatus());
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testRegisterInvalidJson() {
+ // JSON parse failure before URL is known → sentinel INVALID_REQUEST as
audit resource name.
+ expectAudit("INVALID_REQUEST", ActionOutcome.FAILURE, "issuer_registered");
+ EasyMock.replay(mockService, mockAuditor);
+
+ assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
+ resource.registerIssuer("{ not valid json }").getStatus());
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testDuplicateIssuer() {
+ EasyMock.expect(mockService.isTrusted(ISSUER_A)).andReturn(true).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_exists");
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testRegisterNullPrincipal() throws Exception {
+ final TrustedOidcIssuersResource res = buildResource(null);
+ EasyMock.expect(mockService.isTrusted(ISSUER_A)).andReturn(false).once();
+ final Capture<TrustedOidcIssuer> capturedIssuer = EasyMock.newCapture();
+ mockService.register(EasyMock.capture(capturedIssuer));
+ EasyMock.expectLastCall().once();
+ mockAuditor.audit(
+ EasyMock.eq(Action.DELEGATION_LIFECYCLE), EasyMock.eq(ISSUER_A),
+ EasyMock.eq(ResourceType.TRUSTED_ISSUER),
EasyMock.eq(ActionOutcome.SUCCESS),
+ EasyMock.contains("performed_by=ANONYMOUS"));
+ EasyMock.expectLastCall().once();
+ EasyMock.replay(mockService, mockAuditor);
+
+ assertEquals(Response.Status.CREATED.getStatusCode(),
+ res.registerIssuer(buildRegisterBody(ISSUER_A, false,
null)).getStatus());
+ assertNull(capturedIssuer.getValue().getRegisteredBy());
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testRemoveNullPrincipalAuditsAnonymous() throws Exception {
+ final TrustedOidcIssuersResource res = buildResource(null);
+ mockService.deregister(ISSUER_A);
+ EasyMock.expectLastCall().once();
+ mockAuditor.audit(
+ EasyMock.eq(Action.DELEGATION_LIFECYCLE), EasyMock.eq(ISSUER_A),
+ EasyMock.eq(ResourceType.TRUSTED_ISSUER),
EasyMock.eq(ActionOutcome.SUCCESS),
+ EasyMock.contains("performed_by=ANONYMOUS"));
+ EasyMock.expectLastCall().once();
+ EasyMock.replay(mockService, mockAuditor);
+
+ assertEquals(Response.Status.NO_CONTENT.getStatusCode(),
+ res.removeIssuer(ISSUER_A).getStatus());
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testRefreshJwksNullPrincipalAuditsAnonymous() throws Exception {
+ final TrustedOidcIssuersResource res = buildResource(null);
+ mockService.refreshJwksUri(ISSUER_A);
+ EasyMock.expectLastCall().once();
+ mockAuditor.audit(
+ EasyMock.eq(Action.DELEGATION_LIFECYCLE), EasyMock.eq(ISSUER_A),
+ EasyMock.eq(ResourceType.TRUSTED_ISSUER),
EasyMock.eq(ActionOutcome.SUCCESS),
+ EasyMock.contains("performed_by=ANONYMOUS"));
+ EasyMock.expectLastCall().once();
+ EasyMock.replay(mockService, mockAuditor);
+
+ assertEquals(Response.Status.NO_CONTENT.getStatusCode(),
+ res.refreshJwksUri(ISSUER_A).getStatus());
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testAuditRegisterStorageFailure() {
+ EasyMock.expect(mockService.isTrusted(ISSUER_A)).andReturn(false).once();
+ mockService.register(EasyMock.anyObject(TrustedOidcIssuer.class));
+ EasyMock.expectLastCall().andThrow(new RuntimeException("DB
error")).once();
+ expectAudit(ISSUER_A, ActionOutcome.FAILURE, "issuer_registered");
+ EasyMock.replay(mockService, mockAuditor);
+
+ assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
+ resource.registerIssuer(buildRegisterBody(ISSUER_A, false,
null)).getStatus());
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ //
---------------------------------------------------------------------------
+ // DELETE /
+ //
---------------------------------------------------------------------------
+
+ @Test
+ public void testRemoveRegisteredIssuer() {
+ mockService.deregister(ISSUER_A);
+ EasyMock.expectLastCall().once();
+ final Capture<String> auditMsg = EasyMock.newCapture();
+ mockAuditor.audit(
+ EasyMock.eq(Action.DELEGATION_LIFECYCLE), EasyMock.eq(ISSUER_A),
+ EasyMock.eq(ResourceType.TRUSTED_ISSUER),
EasyMock.eq(ActionOutcome.SUCCESS),
+ EasyMock.capture(auditMsg));
+ EasyMock.expectLastCall().once();
+ EasyMock.replay(mockService, mockAuditor);
+
+ assertEquals(Response.Status.NO_CONTENT.getStatusCode(),
+ resource.removeIssuer(ISSUER_A).getStatus());
+ assertTrue(auditMsg.getValue().contains("event_type=issuer_removed"));
+ assertTrue(auditMsg.getValue().contains("performed_by=" + OPERATOR));
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testRemoveMissingIssuerUrl() {
+ // Null models a request where ?issuerUrl= was omitted entirely (JAX-RS
injects null).
+ expectAudit("UNKNOWN_ISSUER", ActionOutcome.FAILURE, "issuer_removed");
+ EasyMock.replay(mockService, mockAuditor);
+
+ final Response response = resource.removeIssuer(null);
+
+ assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
response.getStatus());
+ assertErrorField(response, "invalid_request");
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testRemoveEmptyIssuerUrl() {
+ // Empty string models ?issuerUrl= with no value.
+ expectAudit("UNKNOWN_ISSUER", ActionOutcome.FAILURE, "issuer_removed");
+ EasyMock.replay(mockService, mockAuditor);
+
+ final Response response = resource.removeIssuer("");
+
+ assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
response.getStatus());
+ assertErrorField(response, "invalid_request");
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testRemoveWhitespaceIssuerUrl() {
+ // Whitespace-only is not a valid HTTPS URL; fail fast rather than
propagating to the service.
+ expectAudit("UNKNOWN_ISSUER", ActionOutcome.FAILURE, "issuer_removed");
+ EasyMock.replay(mockService, mockAuditor);
+
+ final Response response = resource.removeIssuer(" ");
+
+ assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
response.getStatus());
+ assertErrorField(response, "invalid_request");
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testAuditRemoveStorageFailure() {
+ mockService.deregister(ISSUER_A);
+ EasyMock.expectLastCall().andThrow(new RuntimeException("DB
error")).once();
+ expectAudit(ISSUER_A, ActionOutcome.FAILURE, "issuer_removed");
+ EasyMock.replay(mockService, mockAuditor);
+
+ assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
+ resource.removeIssuer(ISSUER_A).getStatus());
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+
+ //
---------------------------------------------------------------------------
+ // POST /refresh-jwks
+ //
---------------------------------------------------------------------------
+
+ @Test
+ public void testRefreshJwksUri() {
+ mockService.refreshJwksUri(ISSUER_A);
+ EasyMock.expectLastCall().once();
+ final Capture<String> auditMsg = EasyMock.newCapture();
+ mockAuditor.audit(
+ EasyMock.eq(Action.DELEGATION_LIFECYCLE), EasyMock.eq(ISSUER_A),
+ EasyMock.eq(ResourceType.TRUSTED_ISSUER),
EasyMock.eq(ActionOutcome.SUCCESS),
+ EasyMock.capture(auditMsg));
+ EasyMock.expectLastCall().once();
+ EasyMock.replay(mockService, mockAuditor);
+
+ assertEquals(Response.Status.NO_CONTENT.getStatusCode(),
+ resource.refreshJwksUri(ISSUER_A).getStatus());
+
assertTrue(auditMsg.getValue().contains("event_type=issuer_jwks_refreshed"));
+ assertTrue(auditMsg.getValue().contains("performed_by=" + OPERATOR));
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testRefreshMissingIssuerUrl() {
+ expectAudit("UNKNOWN_ISSUER", ActionOutcome.FAILURE,
"issuer_jwks_refreshed");
+ EasyMock.replay(mockService, mockAuditor);
+
+ final Response response = resource.refreshJwksUri(null);
+
+ assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
response.getStatus());
+ assertErrorField(response, "invalid_request");
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testRefreshEmptyIssuerUrl() {
+ expectAudit("UNKNOWN_ISSUER", ActionOutcome.FAILURE,
"issuer_jwks_refreshed");
+ EasyMock.replay(mockService, mockAuditor);
+
+ final Response response = resource.refreshJwksUri("");
+
+ assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
response.getStatus());
+ assertErrorField(response, "invalid_request");
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testRefreshWhitespaceIssuerUrl() {
+ expectAudit("UNKNOWN_ISSUER", ActionOutcome.FAILURE,
"issuer_jwks_refreshed");
+ EasyMock.replay(mockService, mockAuditor);
+
+ final Response response = resource.refreshJwksUri(" ");
+
+ assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
response.getStatus());
+ assertErrorField(response, "invalid_request");
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testRefreshJwksUriAuditsStorageFailure() {
+ mockService.refreshJwksUri(ISSUER_A);
+ EasyMock.expectLastCall().andThrow(new RuntimeException("Cache
error")).once();
+ expectAudit(ISSUER_A, ActionOutcome.FAILURE, "issuer_jwks_refreshed");
+ EasyMock.replay(mockService, mockAuditor);
+
+ assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
+ resource.refreshJwksUri(ISSUER_A).getStatus());
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+
+ //
---------------------------------------------------------------------------
+ // GET /
+ //
---------------------------------------------------------------------------
+
+ @Test
+ public void testListIssuers() throws Exception {
+ final Instant now = Instant.now();
+ final TrustedOidcIssuer issuerA = new TrustedOidcIssuer(ISSUER_A, true,
"cluster-a",
+ now, OPERATOR);
+ final TrustedOidcIssuer issuerB = new TrustedOidcIssuer(ISSUER_B, false,
null,
+ now, null);
+ EasyMock.expect(mockService.list()).andReturn(Arrays.asList(issuerA,
issuerB)).once();
+ // listIssuers does not audit; no expectations set on mockAuditor.
+ EasyMock.replay(mockService, mockAuditor);
+
+ final Response response = resource.listIssuers();
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+ final List<Map<String, Object>> body =
parseJsonList(response.getEntity().toString());
+ assertEquals(2, body.size());
+
+ final Map<String, Object> a = findByIssuerUrl(body, ISSUER_A);
+ assertEquals(true, a.get("dynamicJwks"));
+ assertEquals("cluster-a", a.get("clusterName"));
+ assertNotNull(a.get("registeredAt"));
+ assertEquals(OPERATOR, a.get("registeredBy"));
+
+ final Map<String, Object> b = findByIssuerUrl(body, ISSUER_B);
+ assertEquals(false, b.get("dynamicJwks"));
+ assertNull(b.get("clusterName"));
+ assertNull(b.get("registeredBy"));
+ assertNotNull(b.get("registeredAt"));
+
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ @Test
+ public void testListReturnsEmptyArray() throws Exception {
+
EasyMock.expect(mockService.list()).andReturn(Collections.emptyList()).once();
+ EasyMock.replay(mockService, mockAuditor);
+
+ final Response response = resource.listIssuers();
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+ assertTrue(parseJsonList(response.getEntity().toString()).isEmpty());
+ EasyMock.verify(mockService, mockAuditor);
+ }
+
+ //
---------------------------------------------------------------------------
+ // @PostConstruct wiring
+ //
---------------------------------------------------------------------------
+
+ @Test
+ public void testInitWiresServiceFromGatewayServices() throws Exception {
+ final TrustedOidcIssuerService svc =
EasyMock.createNiceMock(TrustedOidcIssuerService.class);
+ EasyMock.replay(svc);
+
+ final GatewayServices gws = EasyMock.createNiceMock(GatewayServices.class);
+
EasyMock.expect(gws.getService(ServiceType.TRUSTED_OIDC_ISSUER_SERVICE)).andReturn(svc).once();
+ EasyMock.replay(gws);
+
+ final ServletContext ctx = EasyMock.createNiceMock(ServletContext.class);
+
EasyMock.expect(ctx.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(gws).once();
+ EasyMock.replay(ctx);
+
+ final TrustedOidcIssuersResource res = new TrustedOidcIssuersResource();
+ injectField(res, "servletContext", ctx);
+ injectField(res, "request", buildRequest(buildPrincipal(OPERATOR)));
+ res.init();
+
+ EasyMock.verify(gws, ctx);
+ }
+
+ //
---------------------------------------------------------------------------
+ // Helpers
+ //
---------------------------------------------------------------------------
+
+ private TrustedOidcIssuersResource buildResource(Principal principal) throws
Exception {
+ final TrustedOidcIssuersResource res = new TrustedOidcIssuersResource();
+ injectField(res, "request", buildRequest(principal));
+ injectField(res, "trustedIssuers", mockService);
+ return res;
+ }
+
+ private HttpServletRequest buildRequest(Principal principal) {
+ final HttpServletRequest req =
EasyMock.createNiceMock(HttpServletRequest.class);
+ EasyMock.expect(req.getUserPrincipal()).andReturn(principal).anyTimes();
+ EasyMock.replay(req);
+ return req;
+ }
+
+ private Principal buildPrincipal(String name) {
+ if (name == null) {
+ return null;
+ }
+ final Principal p = EasyMock.createNiceMock(Principal.class);
+ EasyMock.expect(p.getName()).andReturn(name).anyTimes();
+ EasyMock.replay(p);
+ return p;
+ }
+
+ private void expectAudit(String issuerUrl, String outcome, String eventType)
{
+ mockAuditor.audit(
+ EasyMock.eq(Action.DELEGATION_LIFECYCLE),
+ EasyMock.eq(issuerUrl),
+ EasyMock.eq(ResourceType.TRUSTED_ISSUER),
+ EasyMock.eq(outcome),
+ EasyMock.contains(eventType));
+ EasyMock.expectLastCall().once();
+ }
+
+ private static String buildRegisterBody(String issuerUrl, boolean
dynamicJwks,
+ String clusterName) {
+ final StringBuilder sb = new StringBuilder("{");
+ if (issuerUrl != null) {
+ sb.append("\"issuerUrl\":\"").append(issuerUrl).append("\",");
+ }
+ sb.append("\"dynamicJwks\":").append(dynamicJwks);
+ if (clusterName != null) {
+ sb.append(",\"clusterName\":\"").append(clusterName).append("\"");
+ }
+ sb.append("}");
+ return sb.toString();
+ }
+
+ private static void assertErrorField(Response response, String
expectedError) {
+ assertNotNull(response.getEntity());
+ final String body = response.getEntity().toString();
+ assertFalse("Error body must not be empty", body.isEmpty());
+ assertTrue("Expected error field '" + expectedError + "' in: " + body,
+ body.contains(expectedError));
+ }
+
+ private static List<Map<String, Object>> parseJsonList(String json) throws
Exception {
+ return new ObjectMapper().readValue(json, new
TypeReference<List<Map<String, Object>>>() {});
+ }
+
+ private static Map<String, Object> findByIssuerUrl(List<Map<String, Object>>
list,
+ String issuerUrl) {
+ return list.stream()
+ .filter(m -> issuerUrl.equals(m.get("issuerUrl")))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("Issuer not found: " +
issuerUrl));
+ }
+
+ private static void injectField(Object target, String fieldName, Object
value) throws Exception {
+ final Field field = target.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(target, value);
+ }
+}
diff --git
a/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/deploy/KnoxIDFAdminServiceDeploymentContributorTest.java
b/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/deploy/KnoxIDFAdminServiceDeploymentContributorTest.java
new file mode 100644
index 000000000..cd3494b4c
--- /dev/null
+++
b/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/deploy/KnoxIDFAdminServiceDeploymentContributorTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.deploy;
+
+import org.apache.knox.gateway.deploy.DeploymentContext;
+import org.apache.knox.gateway.deploy.ServiceDeploymentContributor;
+import org.apache.knox.gateway.descriptor.FilterParamDescriptor;
+import org.apache.knox.gateway.descriptor.GatewayDescriptor;
+import org.apache.knox.gateway.descriptor.ResourceDescriptor;
+import org.apache.knox.gateway.topology.Service;
+import org.apache.knox.gateway.topology.Topology;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.ServiceLoader;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Unit tests for {@link KnoxIDFAdminServiceDeploymentContributor}.
+ *
+ * Tests verify: (1) the Service SPI registration (ServiceLoader discovery);
+ * (2) the values returned by the contributor's property methods (role, name,
+ * packages, patterns); and (3) that {@code contributeService()} correctly
wires
+ * the gateway descriptor resource with the right role and pattern — verifies
+ * that a deployed KNOXIDF_ADMIN service role produces a resource descriptor
+ * with the correct role and URL pattern, as required for Knox gateway routing.
+ */
+public class KnoxIDFAdminServiceDeploymentContributorTest {
+
+ @Test
+ public void testRoleAndName() {
+ final KnoxIDFAdminServiceDeploymentContributor c =
+ new KnoxIDFAdminServiceDeploymentContributor();
+ assertEquals("KNOXIDF_ADMIN", c.getRole());
+ assertEquals("KNOXIDF_ADMIN", c.getName());
+ }
+
+ @Test
+ public void testPackages() {
+ final KnoxIDFAdminServiceDeploymentContributor c =
+ new KnoxIDFAdminServiceDeploymentContributor();
+ final String[] packages = c.getPackages();
+ assertNotNull(packages);
+ assertTrue("Expected org.apache.knox.gateway.service.knoxidf in packages",
+
Arrays.asList(packages).contains("org.apache.knox.gateway.service.knoxidf"));
+ }
+
+ @Test
+ public void testPatterns() {
+ final KnoxIDFAdminServiceDeploymentContributor c =
+ new KnoxIDFAdminServiceDeploymentContributor();
+ final String[] patterns = c.getPatterns();
+ assertNotNull(patterns);
+ // Distinct from KnoxIDFServiceDeploymentContributor's "knoxidf/api/**?**"
so that the
+ // KNOXIDF role cannot accidentally serve admin endpoints, and so that
per-role AclsAuthz
+ // params (KNOXIDF_ADMIN.acl) apply only to trusted-issuer admin requests.
+ assertTrue("Expected knoxidf/issuers-admin/**?** in patterns",
+ Arrays.asList(patterns).contains("knoxidf/issuers-admin/**?**"));
+ }
+
+ @Test
+ public void testServiceLoaderDiscovery() {
+ for (ServiceDeploymentContributor c :
+ ServiceLoader.load(ServiceDeploymentContributor.class)) {
+ if (c instanceof KnoxIDFAdminServiceDeploymentContributor) {
+ assertEquals("KNOXIDF_ADMIN", c.getRole());
+ assertEquals("KNOXIDF_ADMIN", c.getName());
+ return;
+ }
+ }
+ fail("KnoxIDFAdminServiceDeploymentContributor not discoverable via
ServiceLoader");
+ }
+
+ /**
+ * Verifies that {@code contributeService()} sets the correct service role
and URL pattern
+ * on the gateway resource descriptor. This exercises the inherited
+ * {@code JerseyServiceDeploymentContributorBase.contributeService()} with
the concrete
+ * values from {@code getPackages()} and {@code getPatterns()}.
+ */
+ @Test
+ public void testContributeService() throws Exception {
+ final KnoxIDFAdminServiceDeploymentContributor contributor =
+ new KnoxIDFAdminServiceDeploymentContributor();
+
+ // Mock FilterParamDescriptor for the
jersey.config.server.provider.packages param.
+ final FilterParamDescriptor param =
EasyMock.createNiceMock(FilterParamDescriptor.class);
+
EasyMock.expect(param.name(EasyMock.anyString())).andReturn(param).anyTimes();
+
EasyMock.expect(param.value(EasyMock.anyString())).andReturn(param).anyTimes();
+ EasyMock.replay(param);
+
+ // Capture role and pattern set on the resource descriptor.
+ final Capture<String> capturedRole = EasyMock.newCapture();
+ final Capture<String> capturedPattern = EasyMock.newCapture();
+ final ResourceDescriptor resource =
EasyMock.createNiceMock(ResourceDescriptor.class);
+
EasyMock.expect(resource.role(EasyMock.capture(capturedRole))).andReturn(resource).anyTimes();
+ EasyMock.expect(resource.pattern(EasyMock.capture(capturedPattern)))
+ .andReturn(resource).anyTimes();
+ EasyMock.expect(resource.createFilterParam()).andReturn(param).anyTimes();
+
EasyMock.expect(resource.filters()).andReturn(Collections.emptyList()).anyTimes();
+ EasyMock.replay(resource);
+
+ final GatewayDescriptor descriptor =
EasyMock.createNiceMock(GatewayDescriptor.class);
+ EasyMock.expect(descriptor.addResource()).andReturn(resource).anyTimes();
+ EasyMock.replay(descriptor);
+
+ // Use an empty topology so the base-class addXxxFilter calls are no-ops,
isolating
+ // the test to this contributor's specific contributions (role and
pattern)"
+ final Topology topology = new Topology();
+
+ final DeploymentContext context =
EasyMock.createNiceMock(DeploymentContext.class);
+
EasyMock.expect(context.getGatewayDescriptor()).andReturn(descriptor).anyTimes();
+ EasyMock.expect(context.getTopology()).andReturn(topology).anyTimes();
+ EasyMock.replay(context);
+
+ final Service service = new Service();
+ service.setRole("KNOXIDF_ADMIN");
+ service.setName("KNOXIDF_ADMIN");
+
+ contributor.contributeService(context, service);
+
+ assertEquals("KNOXIDF_ADMIN", capturedRole.getValue());
+ assertEquals("knoxidf/issuers-admin/**?**", capturedPattern.getValue());
+ }
+}
diff --git
a/gateway-util-common/src/main/java/org/apache/knox/gateway/audit/api/Action.java
b/gateway-util-common/src/main/java/org/apache/knox/gateway/audit/api/Action.java
index 7057d56b0..3a77f60bb 100644
---
a/gateway-util-common/src/main/java/org/apache/knox/gateway/audit/api/Action.java
+++
b/gateway-util-common/src/main/java/org/apache/knox/gateway/audit/api/Action.java
@@ -31,5 +31,6 @@ public abstract class Action {
public static final String DISPATCH = "dispatch";
public static final String ACCESS = "access";
public static final String WEBSHELL = "webshell";
+ public static final String DELEGATION_LIFECYCLE = "delegation-lifecycle";
}
diff --git
a/gateway-util-common/src/main/java/org/apache/knox/gateway/audit/api/ResourceType.java
b/gateway-util-common/src/main/java/org/apache/knox/gateway/audit/api/ResourceType.java
index a9eb21186..7c0624056 100644
---
a/gateway-util-common/src/main/java/org/apache/knox/gateway/audit/api/ResourceType.java
+++
b/gateway-util-common/src/main/java/org/apache/knox/gateway/audit/api/ResourceType.java
@@ -25,5 +25,6 @@ public abstract class ResourceType {
public static final String TOPOLOGY = "topology";
public static final String PRINCIPAL = "principal";
public static final String PROCESS = "process";
+ public static final String TRUSTED_ISSUER = "trusted-issuer";
}