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

commit 740c6960c97d354f5e4ad7a327cdf539568cbc9f
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";
 
 }

Reply via email to