You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by sm...@apache.org on 2026/09/08 09:32:43 UTC

(knox) branch master updated: KNOX-3438: Address review comments from delegation policy admin API (#1381)

This is an automated email from the ASF dual-hosted git repository.

smolnar82 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new 8872f980e KNOX-3438: Address review comments from delegation policy admin API (#1381)
8872f980e is described below

commit 8872f980e8ac326d498b74f910e50f60a2f8de76
Author: Sandor Molnar <sm...@apache.org>
AuthorDate: Tue Sep 8 11:32:37 2026 +0200

    KNOX-3438: Address review comments from delegation policy admin API (#1381)
    
    * KNOX-3438: read delegation policy TTL bounds from topology init-params with fail-fast validation instead of gateway config
    
    * KNOX-3438: use app clock for delegation policy updated_at to match created_at
    
    * KNOX-3438: audit delegation policy read endpoints (list, getOne) under Action.ACCESS
    
    * KNOX-3438: validate configured default token TTL against admin API bounds at startup
---
 .../gateway/config/impl/GatewayConfigImpl.java     | 10 ---
 .../delegation/DelegationPolicyDatabase.java       | 20 +++--
 .../delegation/EmptyDelegationPolicyService.java   |  8 ++
 .../delegation/JdbcDelegationPolicyService.java    |  5 ++
 .../service/knoxidf/DelegationPolicyResource.java  | 64 ++++++++++++++--
 .../knoxidf/DelegationPolicyResourceTest.java      | 87 ++++++++++++++++------
 .../org/apache/knox/gateway/GatewayTestConfig.java | 10 ---
 .../apache/knox/gateway/config/GatewayConfig.java  |  8 --
 .../delegation/DelegationPolicyService.java        |  9 +++
 9 files changed, 155 insertions(+), 66 deletions(-)

diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
index fc065ad78..21b4ae150 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
@@ -1982,14 +1982,4 @@ public class GatewayConfigImpl extends Configuration implements GatewayConfig {
   public int getDelegationServiceListMaxPerAuthority() {
     return getInt(DELEGATION_SERVICE_LIST_MAX_PER_AUTHORITY, DELEGATION_SERVICE_LIST_MAX_PER_AUTHORITY_DEFAULT);
   }
-
-  @Override
-  public int getDelegationServiceMinTokenTtlSec() {
-    return getInt(DELEGATION_SERVICE_MIN_TOKEN_TTL_SEC, DELEGATION_SERVICE_MIN_TOKEN_TTL_SEC_DEFAULT);
-  }
-
-  @Override
-  public int getDelegationServiceMaxTokenTtlSec() {
-    return getInt(DELEGATION_SERVICE_MAX_TOKEN_TTL_SEC, DELEGATION_SERVICE_MAX_TOKEN_TTL_SEC_DEFAULT);
-  }
 }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/DelegationPolicyDatabase.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/DelegationPolicyDatabase.java
index dbd5fd31c..baa157d59 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/DelegationPolicyDatabase.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/DelegationPolicyDatabase.java
@@ -25,6 +25,7 @@ import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -51,14 +52,16 @@ class DelegationPolicyDatabase extends KnoxDatabase {
           + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
 
   // actor_authority/actor_id/created_by/created_at are deliberately absent from SET: identity and
-  // creation metadata are immutable after registration. updated_at is computed by the database,
-  // not supplied by the caller. The WHERE clause requires the identity fields to still match, so
-  // an update attempting to change them affects 0 rows -- indistinguishable from (and reported the
-  // same as) registrationId not existing at all.
+  // creation metadata are immutable after registration. updated_at is computed by the storage layer
+  // and bound below (param 6) from the app clock -- the same source INSERT uses for created_at, so
+  // both columns share one clock/zone and stay comparable (a DB-side CURRENT_TIMESTAMP could read
+  // earlier than the JVM-written created_at). The WHERE clause requires the identity fields to still
+  // match, so an update attempting to change them affects 0 rows -- indistinguishable from (and
+  // reported the same as) registrationId not existing at all.
   private static final String UPDATE_CORE_SQL =
       "UPDATE " + CORE_TABLE + " SET "
           + "name = ?, status = ?, token_ttl_sec = ?, description = ?, allow_headless_exchange = ?, "
-          + "updated_at = CURRENT_TIMESTAMP "
+          + "updated_at = ? "
           + "WHERE registration_id = ? AND actor_authority = ? AND actor_id = ?";
 
   private static final String DELETE_REGISTRATION_SQL =
@@ -310,9 +313,10 @@ class DelegationPolicyDatabase extends KnoxDatabase {
       }
       ps.setString(4, policy.getDescription());
       ps.setBoolean(5, policy.isAllowHeadlessExchange());
-      ps.setString(6, registrationId);
-      ps.setString(7, policy.getActorAuthority());
-      ps.setString(8, policy.getActorId());
+      ps.setTimestamp(6, Timestamp.from(Instant.now()));
+      ps.setString(7, registrationId);
+      ps.setString(8, policy.getActorAuthority());
+      ps.setString(9, policy.getActorId());
       return ps.executeUpdate();
     }
   }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/EmptyDelegationPolicyService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/EmptyDelegationPolicyService.java
index 4edc21d59..561e917a3 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/EmptyDelegationPolicyService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/EmptyDelegationPolicyService.java
@@ -85,4 +85,12 @@ public class EmptyDelegationPolicyService implements DelegationPolicyService {
   public PolicyDecision evaluate(PolicyCheckRequest request) {
     return new PolicyDecision("service_not_configured", 0);
   }
+
+  @Override
+  public int getConfiguredTokenTtlSec() {
+    // This stub is only in play when the service role is not deployed (so the admin API is not
+    // deployed either). Return the gateway-config default so any bounds check still sees an
+    // in-range value rather than 0.
+    return GatewayConfig.DELEGATION_SERVICE_TOKEN_TTL_SEC_DEFAULT;
+  }
 }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/JdbcDelegationPolicyService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/JdbcDelegationPolicyService.java
index 8685436c3..b09cf9d67 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/JdbcDelegationPolicyService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/JdbcDelegationPolicyService.java
@@ -204,6 +204,11 @@ public class JdbcDelegationPolicyService implements DelegationPolicyService {
     }
   }
 
+  @Override
+  public int getConfiguredTokenTtlSec() {
+    return configuredKnoxTokenTtlSec;
+  }
+
   @Override
   public PolicyDecision evaluate(PolicyCheckRequest request) {
     // Step 1: look up registration
diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DelegationPolicyResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DelegationPolicyResource.java
index 968984556..9480e276a 100644
--- a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DelegationPolicyResource.java
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DelegationPolicyResource.java
@@ -27,7 +27,6 @@ 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.config.GatewayConfig;
 import org.apache.knox.gateway.services.GatewayServices;
 import org.apache.knox.gateway.services.ServiceType;
 import org.apache.knox.gateway.services.knoxidf.delegation.DelegationPolicy;
@@ -67,6 +66,11 @@ public class DelegationPolicyResource {
 
   static final String RESOURCE_PATH = "knoxidf/admin/v1/delegation-policies";
 
+  static final String MIN_TOKEN_TTL_SEC_PARAM = "knox.delegation.min.token.ttl.sec";
+  static final String MAX_TOKEN_TTL_SEC_PARAM = "knox.delegation.max.token.ttl.sec";
+  static final int DEFAULT_MIN_TOKEN_TTL_SEC = 60;      // 1 minute
+  static final int DEFAULT_MAX_TOKEN_TTL_SEC = 86400;   // 24 hours
+
   private static final ObjectMapper MAPPER = new ObjectMapper()
       .registerModule(new JavaTimeModule())
       .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
@@ -84,20 +88,52 @@ public class DelegationPolicyResource {
 
   private DelegationPolicyService policyService;
 
-  private int minTokenTtlSec = GatewayConfig.DELEGATION_SERVICE_MIN_TOKEN_TTL_SEC_DEFAULT;
-  private int maxTokenTtlSec = GatewayConfig.DELEGATION_SERVICE_MAX_TOKEN_TTL_SEC_DEFAULT;
+  private int minTokenTtlSec =  DEFAULT_MIN_TOKEN_TTL_SEC;
+  private int maxTokenTtlSec =   DEFAULT_MAX_TOKEN_TTL_SEC;
 
   @PostConstruct
   public void init() {
     final GatewayServices services = (GatewayServices)
         servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
     policyService = services.getService(ServiceType.DELEGATION_POLICY_SERVICE);
+    configureTtlBounds();
+  }
+
+  private void configureTtlBounds() {
+    minTokenTtlSec = readPositiveIntParam(MIN_TOKEN_TTL_SEC_PARAM, DEFAULT_MIN_TOKEN_TTL_SEC);
+    maxTokenTtlSec = readPositiveIntParam(MAX_TOKEN_TTL_SEC_PARAM, DEFAULT_MAX_TOKEN_TTL_SEC);
+    if (minTokenTtlSec > maxTokenTtlSec) {
+      throw new IllegalStateException("Invalid delegation policy TTL bounds: "
+              + MIN_TOKEN_TTL_SEC_PARAM + " (" + minTokenTtlSec + ") must not exceed "
+              + MAX_TOKEN_TTL_SEC_PARAM + " (" + maxTokenTtlSec + ")");
+    }
 
-    final GatewayConfig config = (GatewayConfig) servletContext.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
-    if (config != null) {
-      minTokenTtlSec = config.getDelegationServiceMinTokenTtlSec();
-      maxTokenTtlSec = config.getDelegationServiceMaxTokenTtlSec();
+    final int configuredDefaultTtlSec = policyService.getConfiguredTokenTtlSec();
+    if (configuredDefaultTtlSec < minTokenTtlSec || configuredDefaultTtlSec > maxTokenTtlSec) {
+      throw new IllegalStateException("Configured default delegation token TTL ("
+              + configuredDefaultTtlSec + "s) is outside the enforced bounds [" + minTokenTtlSec + ", "
+              + maxTokenTtlSec + "]; policies without an explicit tokenTtlSec would receive an "
+              + "out-of-range effective TTL");
+    }
+  }
+
+  private int readPositiveIntParam(String paramName, int defaultValue) {
+    final String raw = servletContext.getInitParameter(paramName);
+    if (StringUtils.isBlank(raw)) {
+      return defaultValue;
+    }
+    final int value;
+    try {
+      value = Integer.parseInt(raw.trim());
+    } catch (NumberFormatException e) {
+      throw new IllegalStateException("Invalid value for " + paramName + ": \"" + raw
+          + "\" is not an integer");
+    }
+    if (value <= 0) {
+      throw new IllegalStateException("Invalid value for " + paramName + ": " + value
+          + " must be a positive number of seconds");
     }
+    return value;
   }
 
   @POST
@@ -181,29 +217,41 @@ public class DelegationPolicyResource {
 
   @GET
   public Response list(@QueryParam("actorAuthority") String actorAuthority) {
+    final String filter = StringUtils.isBlank(actorAuthority) ? null : actorAuthority;
+    String outcome = ActionOutcome.FAILURE;
     try {
-      final String filter = StringUtils.isBlank(actorAuthority) ? null : actorAuthority;
       final DelegationPolicyList result = policyService.list(filter);
       final DelegationPolicyListResponse body = new DelegationPolicyListResponse();
       body.setPolicies(result.getPolicies().stream().map(DelegationPolicyResource::toResponse).collect(Collectors.toList()));
       body.setHasMore(result.hasMore());
+      outcome = ActionOutcome.SUCCESS;
       return Response.ok(writeJson(body)).build();
     } catch (RuntimeException e) {
       return errorResponse(Response.Status.INTERNAL_SERVER_ERROR, "storage_error", "Failed to list delegation policies");
+    } finally {
+      final String operatorId = getOperatorId();
+      auditor.audit(Action.ACCESS, filter != null ? filter : "ALL", ResourceType.DELEGATION_POLICY,
+          outcome, "event_type=policy_listed performed_by=" + auditLabel(operatorId));
     }
   }
 
   @GET
   @Path("/{registrationId}")
   public Response getOne(@PathParam("registrationId") String registrationId) {
+    String outcome = ActionOutcome.FAILURE;
     try {
       final Optional<DelegationPolicy> found = policyService.get(registrationId);
       if (!found.isPresent()) {
         return errorResponse(Response.Status.NOT_FOUND, "policy_not_found", "Delegation policy not found: " + registrationId);
       }
+      outcome = ActionOutcome.SUCCESS;
       return Response.ok(writeJson(toResponse(found.get()))).build();
     } catch (RuntimeException e) {
       return errorResponse(Response.Status.INTERNAL_SERVER_ERROR, "storage_error", "Failed to read delegation policy");
+    } finally {
+      final String operatorId = getOperatorId();
+      auditor.audit(Action.ACCESS, registrationId, ResourceType.DELEGATION_POLICY,
+          outcome, "event_type=policy_read performed_by=" + auditLabel(operatorId));
     }
   }
 
diff --git a/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/DelegationPolicyResourceTest.java b/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/DelegationPolicyResourceTest.java
index f5363f00a..01a551038 100644
--- a/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/DelegationPolicyResourceTest.java
+++ b/gateway-service-knoxidf/src/test/java/org/apache/knox/gateway/service/knoxidf/DelegationPolicyResourceTest.java
@@ -22,7 +22,6 @@ 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.config.GatewayConfig;
 import org.apache.knox.gateway.services.GatewayServices;
 import org.apache.knox.gateway.services.ServiceType;
 import org.apache.knox.gateway.services.knoxidf.delegation.DelegationPolicy;
@@ -63,8 +62,8 @@ public class DelegationPolicyResourceTest {
   private static final String ACTOR_ID = "svc-1";
   private static final String REGISTRATION_ID = "reg-123";
   private static final String OPERATOR = "admin";
-  private static final int MIN_TTL = GatewayConfig.DELEGATION_SERVICE_MIN_TOKEN_TTL_SEC_DEFAULT;
-  private static final int MAX_TTL = GatewayConfig.DELEGATION_SERVICE_MAX_TOKEN_TTL_SEC_DEFAULT;
+  private static final int MIN_TTL = DelegationPolicyResource.DEFAULT_MIN_TOKEN_TTL_SEC;
+  private static final int MAX_TTL = DelegationPolicyResource.DEFAULT_MAX_TOKEN_TTL_SEC;
 
   private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new JavaTimeModule());
   private static final String ISO_INSTANT_PATTERN =
@@ -775,6 +774,7 @@ public class DelegationPolicyResourceTest {
     EasyMock.expect(mockService.list(null))
         .andReturn(new DelegationPolicyList(Collections.singletonList(existingPolicy(REGISTRATION_ID, Instant.now())), false))
         .once();
+    expectReadAudit("ALL", ActionOutcome.SUCCESS, "policy_listed");
     EasyMock.replay(mockService, mockAuditor);
 
     final Response response = resource.list(null);
@@ -791,6 +791,7 @@ public class DelegationPolicyResourceTest {
     EasyMock.expect(mockService.list(ACTOR_AUTHORITY))
         .andReturn(new DelegationPolicyList(Collections.emptyList(), false))
         .once();
+    expectReadAudit(ACTOR_AUTHORITY, ActionOutcome.SUCCESS, "policy_listed");
     EasyMock.replay(mockService, mockAuditor);
 
     resource.list(ACTOR_AUTHORITY);
@@ -803,6 +804,7 @@ public class DelegationPolicyResourceTest {
     EasyMock.expect(mockService.list(null))
         .andReturn(new DelegationPolicyList(Collections.emptyList(), false))
         .once();
+    expectReadAudit("ALL", ActionOutcome.SUCCESS, "policy_listed");
     EasyMock.replay(mockService, mockAuditor);
 
     resource.list("");
@@ -815,6 +817,7 @@ public class DelegationPolicyResourceTest {
     EasyMock.expect(mockService.list(null))
         .andReturn(new DelegationPolicyList(Collections.emptyList(), false))
         .once();
+    expectReadAudit("ALL", ActionOutcome.SUCCESS, "policy_listed");
     EasyMock.replay(mockService, mockAuditor);
 
     final Response response = resource.list(null);
@@ -831,6 +834,7 @@ public class DelegationPolicyResourceTest {
     EasyMock.expect(mockService.list(null))
         .andReturn(new DelegationPolicyList(Collections.emptyList(), true))
         .once();
+    expectReadAudit("ALL", ActionOutcome.SUCCESS, "policy_listed");
     EasyMock.replay(mockService, mockAuditor);
 
     final DelegationPolicyListResponse body = parseListResponse(resource.list(null));
@@ -842,6 +846,7 @@ public class DelegationPolicyResourceTest {
   @Test
   public void testListStorageFailure() {
     EasyMock.expect(mockService.list(null)).andThrow(new RuntimeException("DB error")).once();
+    expectReadAudit("ALL", ActionOutcome.FAILURE, "policy_listed");
     EasyMock.replay(mockService, mockAuditor);
 
     final Response response = resource.list(null);
@@ -859,6 +864,7 @@ public class DelegationPolicyResourceTest {
   public void testGetOneFound() throws Exception {
     final DelegationPolicy stored = existingPolicy(REGISTRATION_ID, Instant.now());
     EasyMock.expect(mockService.get(REGISTRATION_ID)).andReturn(Optional.of(stored)).once();
+    expectReadAudit(REGISTRATION_ID, ActionOutcome.SUCCESS, "policy_read");
     EasyMock.replay(mockService, mockAuditor);
 
     final Response response = resource.getOne(REGISTRATION_ID);
@@ -871,6 +877,7 @@ public class DelegationPolicyResourceTest {
   @Test
   public void testGetOneNotFound() {
     EasyMock.expect(mockService.get(REGISTRATION_ID)).andReturn(Optional.empty()).once();
+    expectReadAudit(REGISTRATION_ID, ActionOutcome.FAILURE, "policy_read");
     EasyMock.replay(mockService, mockAuditor);
 
     final Response response = resource.getOne(REGISTRATION_ID);
@@ -883,6 +890,7 @@ public class DelegationPolicyResourceTest {
   @Test
   public void testGetOneStorageFailure() {
     EasyMock.expect(mockService.get(REGISTRATION_ID)).andThrow(new RuntimeException("DB error")).once();
+    expectReadAudit(REGISTRATION_ID, ActionOutcome.FAILURE, "policy_read");
     EasyMock.replay(mockService, mockAuditor);
 
     final Response response = resource.getOne(REGISTRATION_ID);
@@ -1178,6 +1186,7 @@ public class DelegationPolicyResourceTest {
   @Test
   public void testInitWiresServiceFromGatewayServices() throws Exception {
     final DelegationPolicyService svc = EasyMock.createNiceMock(DelegationPolicyService.class);
+    EasyMock.expect(svc.getConfiguredTokenTtlSec()).andReturn(3600).anyTimes();
     EasyMock.replay(svc);
 
     final GatewayServices gws = EasyMock.createNiceMock(GatewayServices.class);
@@ -1186,7 +1195,6 @@ public class DelegationPolicyResourceTest {
 
     final ServletContext ctx = EasyMock.createNiceMock(ServletContext.class);
     EasyMock.expect(ctx.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(gws).once();
-    EasyMock.expect(ctx.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(null).once();
     EasyMock.replay(ctx);
 
     final DelegationPolicyResource res = new DelegationPolicyResource();
@@ -1194,38 +1202,65 @@ public class DelegationPolicyResourceTest {
     injectField(res, "request", buildRequest(buildPrincipal(OPERATOR)));
     res.init();
 
+    // No init-params configured -> code-constant defaults apply.
     assertEquals(MIN_TTL, ((Number) readField(res, "minTokenTtlSec")).intValue());
     assertEquals(MAX_TTL, ((Number) readField(res, "maxTokenTtlSec")).intValue());
     EasyMock.verify(gws, ctx);
   }
 
   @Test
-  public void testInitWiresTokenTtlBoundsFromGatewayConfig() throws Exception {
+  public void testInitReadsTokenTtlBoundsFromInitParams() throws Exception {
+    final DelegationPolicyResource res = initResourceWithBounds("120", "7200");
+    assertEquals(120, ((Number) readField(res, "minTokenTtlSec")).intValue());
+    assertEquals(7200, ((Number) readField(res, "maxTokenTtlSec")).intValue());
+  }
+
+  @Test(expected = IllegalStateException.class)
+  public void testInitRejectsNonNumericBound() throws Exception {
+      initResourceWithBounds("not-a-number", null);
+  }
+
+  @Test(expected = IllegalStateException.class)
+  public void testInitRejectsNonPositiveBound() throws Exception {
+      initResourceWithBounds("0", null);
+  }
+
+  @Test(expected = IllegalStateException.class)
+  public void testInitRejectsMinGreaterThanMax() throws Exception {
+      initResourceWithBounds("7200", "120");
+  }
+
+  @Test(expected = IllegalStateException.class)
+  public void testInitRejectsConfiguredDefaultOutsideBounds() throws Exception {
+    // Default bounds are [60, 86400]; a gateway-wide default above max would let policies without
+    // an explicit tokenTtlSec yield an out-of-range effective TTL, so init() must fail fast.
+    initResource(null, null, 100000);
+  }
+
+  private DelegationPolicyResource initResourceWithBounds(String min, String max) throws Exception {
+    return initResource(min, max, 3600);
+  }
+
+  private DelegationPolicyResource initResource(String min, String max, int configuredDefault) throws Exception {
     final DelegationPolicyService svc = EasyMock.createNiceMock(DelegationPolicyService.class);
+    EasyMock.expect(svc.getConfiguredTokenTtlSec()).andReturn(configuredDefault).anyTimes();
     EasyMock.replay(svc);
 
     final GatewayServices gws = EasyMock.createNiceMock(GatewayServices.class);
-    EasyMock.expect(gws.getService(ServiceType.DELEGATION_POLICY_SERVICE)).andReturn(svc).once();
+    EasyMock.expect(gws.getService(ServiceType.DELEGATION_POLICY_SERVICE)).andReturn(svc).anyTimes();
     EasyMock.replay(gws);
 
-    final GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
-    EasyMock.expect(config.getDelegationServiceMinTokenTtlSec()).andReturn(120).once();
-    EasyMock.expect(config.getDelegationServiceMaxTokenTtlSec()).andReturn(7200).once();
-    EasyMock.replay(config);
-
     final ServletContext ctx = EasyMock.createNiceMock(ServletContext.class);
-    EasyMock.expect(ctx.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(gws).once();
-    EasyMock.expect(ctx.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(config).once();
+    EasyMock.expect(ctx.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(gws).anyTimes();
+    EasyMock.expect(ctx.getInitParameter(DelegationPolicyResource.MIN_TOKEN_TTL_SEC_PARAM)).andReturn(min).anyTimes();
+    EasyMock.expect(ctx.getInitParameter(DelegationPolicyResource.MAX_TOKEN_TTL_SEC_PARAM)).andReturn(max).anyTimes();
     EasyMock.replay(ctx);
 
     final DelegationPolicyResource res = new DelegationPolicyResource();
     injectField(res, "servletContext", ctx);
     injectField(res, "request", buildRequest(buildPrincipal(OPERATOR)));
     res.init();
-
-    assertEquals(120, ((Number) readField(res, "minTokenTtlSec")).intValue());
-    assertEquals(7200, ((Number) readField(res, "maxTokenTtlSec")).intValue());
-    EasyMock.verify(gws, ctx, config);
+    return res;
   }
 
   // ---------------------------------------------------------------------------
@@ -1257,12 +1292,20 @@ public class DelegationPolicyResourceTest {
   }
 
   private void expectAudit(String registrationId, String outcome, String eventType) {
+    expectAudit(Action.DELEGATION_LIFECYCLE, registrationId, outcome, eventType);
+  }
+
+  private void expectReadAudit(String id, String outcome, String eventType) {
+    expectAudit(Action.ACCESS, id, outcome, eventType);
+  }
+
+  private void expectAudit(String action, String registrationId, String outcome, String eventType) {
     mockAuditor.audit(
-        EasyMock.eq(Action.DELEGATION_LIFECYCLE),
-        EasyMock.eq(registrationId),
-        EasyMock.eq(ResourceType.DELEGATION_POLICY),
-        EasyMock.eq(outcome),
-        EasyMock.contains(eventType));
+            EasyMock.eq(action),
+            EasyMock.eq(registrationId),
+            EasyMock.eq(ResourceType.DELEGATION_POLICY),
+            EasyMock.eq(outcome),
+            EasyMock.contains(eventType));
     EasyMock.expectLastCall().once();
   }
 
diff --git a/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java b/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
index 5e892902a..e74df8b9d 100644
--- a/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
+++ b/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
@@ -1425,14 +1425,4 @@ public class GatewayTestConfig extends Configuration implements GatewayConfig {
     return DELEGATION_SERVICE_LIST_MAX_PER_AUTHORITY_DEFAULT;
   }
 
-  @Override
-  public int getDelegationServiceMinTokenTtlSec() {
-    return DELEGATION_SERVICE_MIN_TOKEN_TTL_SEC_DEFAULT;
-  }
-
-  @Override
-  public int getDelegationServiceMaxTokenTtlSec() {
-    return DELEGATION_SERVICE_MAX_TOKEN_TTL_SEC_DEFAULT;
-  }
-
 }
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
index 6e1e922f0..b2af39e6c 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
@@ -183,10 +183,6 @@ public interface GatewayConfig {
   int    DELEGATION_SERVICE_LIST_MAX_TOTAL_DEFAULT = 10_000;
   String DELEGATION_SERVICE_LIST_MAX_PER_AUTHORITY = DELEGATION_SERVICE_PREFIX + "list.max.per.authority";
   int    DELEGATION_SERVICE_LIST_MAX_PER_AUTHORITY_DEFAULT = 10_000;
-  String DELEGATION_SERVICE_MIN_TOKEN_TTL_SEC = DELEGATION_SERVICE_PREFIX + "min.token.ttl.sec";
-  int    DELEGATION_SERVICE_MIN_TOKEN_TTL_SEC_DEFAULT = 60;       // 1 minute
-  String DELEGATION_SERVICE_MAX_TOKEN_TTL_SEC = DELEGATION_SERVICE_PREFIX + "max.token.ttl.sec";
-  int    DELEGATION_SERVICE_MAX_TOKEN_TTL_SEC_DEFAULT = 86400;    // 24 hours
 
   // KnoxIDF federated-OP back-channel (token exchange) HTTP client timeouts. Without these an
   // unresponsive external OP token endpoint pins the calling request thread indefinitely.
@@ -1315,8 +1311,4 @@ public interface GatewayConfig {
 
   int getDelegationServiceListMaxPerAuthority();
 
-  int getDelegationServiceMinTokenTtlSec();
-
-  int getDelegationServiceMaxTokenTtlSec();
-
 }
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/DelegationPolicyService.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/DelegationPolicyService.java
index 02aa237cc..7b33631bc 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/DelegationPolicyService.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/DelegationPolicyService.java
@@ -80,4 +80,13 @@ public interface DelegationPolicyService extends Service {
   DelegationPolicyList list(String actorAuthorityFilter);
 
   PolicyDecision evaluate(PolicyCheckRequest request);
+
+  /**
+   * @return the gateway-wide default token TTL (seconds) that {@link #evaluate} applies when a
+   *     policy does not pin its own tokenTtlSec. Exposed so the admin API can validate this
+   *     fallback against its configured [min, max] bounds at startup and fail fast on an
+   *     out-of-range value -- otherwise policies stored without an explicit tokenTtlSec would
+   *     yield an effective TTL outside the range the API enforces for explicit values.
+   */
+  int getConfiguredTokenTtlSec();
 }