smolnar82 commented on code in PR #1379:
URL: https://github.com/apache/knox/pull/1379#discussion_r3947783156
##########
gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/delegation/DelegationPolicyDatabase.java:
##########
@@ -50,12 +50,16 @@ class DelegationPolicyDatabase extends KnoxDatabase {
+ "description, created_by, created_at, updated_at,
allow_headless_exchange) "
+ "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.
private static final String UPDATE_CORE_SQL =
"UPDATE " + CORE_TABLE + " SET "
- + "actor_authority = ?, actor_id = ?, name = ?, status = ?,
token_ttl_sec = ?, "
- + "description = ?, created_by = ?, created_at = ?, updated_at = ?, "
- + "allow_headless_exchange = ? "
- + "WHERE registration_id = ?";
+ + "name = ?, status = ?, token_ttl_sec = ?, description = ?,
allow_headless_exchange = ?, "
+ + "updated_at = CURRENT_TIMESTAMP "
Review Comment:
`INSERT` writes `created_at/updated_at` from the app clock
(`Timestamp.from(Instant.now())`), but `UPDATE` sets `updated_at =
CURRENT_TIMESTAMP` (DB clock). After an update, `createdAt` (app clock) and
`updatedAt` (DB clock) come from different sources; if the DB host clock lags
the app host, `updatedAt` could read earlier than `createdAt`. Same-host
deployments make this negligible, but it's an avoidable inconsistency: either
use the DB clock for both, or the app clock for both. (the `assertNotEquals` at
`JdbcDelegationPolicyServiceTest.java:362` is safe from flakiness since two
distinct clock sources will essentially never produce an identical instant; no
change needed there.)
##########
gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DelegationPolicyResource.java:
##########
@@ -0,0 +1,368 @@
+/*
+ * 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.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.apache.commons.lang3.StringUtils;
+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.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;
+import
org.apache.knox.gateway.services.knoxidf.delegation.DelegationPolicyAlreadyExistsException;
+import
org.apache.knox.gateway.services.knoxidf.delegation.DelegationPolicyList;
+import
org.apache.knox.gateway.services.knoxidf.delegation.DelegationPolicyNotFoundException;
+import
org.apache.knox.gateway.services.knoxidf.delegation.DelegationPolicyService;
+import
org.apache.knox.gateway.services.knoxidf.delegation.RegisterOrUpdateResult;
+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.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+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.security.Principal;
+import java.time.Instant;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Path(DelegationPolicyResource.RESOURCE_PATH)
+@Produces(MediaType.APPLICATION_JSON)
+public class DelegationPolicyResource {
+
+ static final String RESOURCE_PATH = "knoxidf/admin/v1/delegation-policies";
+
+ private static final ObjectMapper MAPPER = new ObjectMapper()
+ .registerModule(new JavaTimeModule())
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+
+ // 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 DelegationPolicyService policyService;
+
+ private int minTokenTtlSec =
GatewayConfig.DELEGATION_SERVICE_MIN_TOKEN_TTL_SEC_DEFAULT;
+ private int maxTokenTtlSec =
GatewayConfig.DELEGATION_SERVICE_MAX_TOKEN_TTL_SEC_DEFAULT;
+
+ @PostConstruct
+ public void init() {
+ final GatewayServices services = (GatewayServices)
+
servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+ policyService = services.getService(ServiceType.DELEGATION_POLICY_SERVICE);
+
+ final GatewayConfig config = (GatewayConfig)
servletContext.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
+ if (config != null) {
+ minTokenTtlSec = config.getDelegationServiceMinTokenTtlSec();
+ maxTokenTtlSec = config.getDelegationServiceMaxTokenTtlSec();
+ }
Review Comment:
`min/maxTokenTtlSec` are read only from GatewayConfig here. The convention
for per-service knobs like this is topology `<param>` via
`context.getInitParameter(...)`, with a code-constant default fallback. For
instance `TokenResource` (KNOXTOKEN) does for `token.ttl`.
I suggest:
- Drop the `DELEGATION_SERVICE_MIN/MAX_TOKEN_TTL_SEC` keys + getters from
`GatewayConfig/GatewayConfigImpl`; keep the two default values as constants in
the resource.
- In `init()`, read them using `getInitParameter`, fall back to the constant
when absent.
- Fail fast when a supplied value is
- non-numeric,
- ≤ 0,
- or min > max
In this PR: if an operator sets `min.token.ttl.sec > max.token.ttl.sec`,
every request carrying a TTL is rejected with a confusing message, with no
startup warning.
With my proposal this goes away and shrinks the gateway-site surface.
##########
gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DelegationPolicyResource.java:
##########
@@ -0,0 +1,368 @@
+/*
+ * 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.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.apache.commons.lang3.StringUtils;
+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.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;
+import
org.apache.knox.gateway.services.knoxidf.delegation.DelegationPolicyAlreadyExistsException;
+import
org.apache.knox.gateway.services.knoxidf.delegation.DelegationPolicyList;
+import
org.apache.knox.gateway.services.knoxidf.delegation.DelegationPolicyNotFoundException;
+import
org.apache.knox.gateway.services.knoxidf.delegation.DelegationPolicyService;
+import
org.apache.knox.gateway.services.knoxidf.delegation.RegisterOrUpdateResult;
+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.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+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.security.Principal;
+import java.time.Instant;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Path(DelegationPolicyResource.RESOURCE_PATH)
+@Produces(MediaType.APPLICATION_JSON)
+public class DelegationPolicyResource {
+
+ static final String RESOURCE_PATH = "knoxidf/admin/v1/delegation-policies";
+
+ private static final ObjectMapper MAPPER = new ObjectMapper()
+ .registerModule(new JavaTimeModule())
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+
+ // 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 DelegationPolicyService policyService;
+
+ private int minTokenTtlSec =
GatewayConfig.DELEGATION_SERVICE_MIN_TOKEN_TTL_SEC_DEFAULT;
+ private int maxTokenTtlSec =
GatewayConfig.DELEGATION_SERVICE_MAX_TOKEN_TTL_SEC_DEFAULT;
+
+ @PostConstruct
+ public void init() {
+ final GatewayServices services = (GatewayServices)
+
servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+ policyService = services.getService(ServiceType.DELEGATION_POLICY_SERVICE);
+
+ final GatewayConfig config = (GatewayConfig)
servletContext.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
+ if (config != null) {
+ minTokenTtlSec = config.getDelegationServiceMinTokenTtlSec();
+ maxTokenTtlSec = config.getDelegationServiceMaxTokenTtlSec();
+ }
+ }
+
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response register(String body) {
+ final String operatorId = getOperatorId();
+ String auditId = "INVALID_REQUEST";
+ String outcome = ActionOutcome.FAILURE;
+
+ try {
+ final DelegationPolicyRequest parsed;
+ try {
+ parsed = MAPPER.readValue(body, DelegationPolicyRequest.class);
+ } catch (IOException e) {
+ return errorResponse(Response.Status.BAD_REQUEST, "invalid_request",
"Malformed or invalid JSON body");
+ }
+ auditId = bestEffortActorIdForAudit(parsed);
+
+ final Response validationError = validateRequest(parsed);
+ if (validationError != null) {
+ return validationError;
+ }
+
+ final Instant now = Instant.now();
+ final DelegationPolicy toStore = toDomain(null, parsed, operatorId, now,
now);
+ final DelegationPolicy stored = policyService.register(toStore);
+ auditId = stored.getRegistrationId();
+ outcome = ActionOutcome.SUCCESS;
+ return
Response.status(Response.Status.CREATED).entity(writeJson(toResponse(stored))).build();
+ } catch (DelegationPolicyAlreadyExistsException e) {
+ return errorResponse(Response.Status.CONFLICT, "actor_exists",
e.getMessage());
+ } catch (RuntimeException e) {
+ return errorResponse(Response.Status.INTERNAL_SERVER_ERROR,
"storage_error", "Failed to register delegation policy");
+ } finally {
+ auditor.audit(Action.DELEGATION_LIFECYCLE, auditId,
ResourceType.DELEGATION_POLICY,
+ outcome, "event_type=policy_registered performed_by=" +
auditLabel(operatorId));
+ }
+ }
+
+ @PUT
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response registerOrUpdate(String body) {
+ final String operatorId = getOperatorId();
+ String auditId = "INVALID_REQUEST";
+ String outcome = ActionOutcome.FAILURE;
+ String eventType = "policy_registered";
+
+ try {
+ final DelegationPolicyRequest parsed;
+ try {
+ parsed = MAPPER.readValue(body, DelegationPolicyRequest.class);
+ } catch (IOException e) {
+ return errorResponse(Response.Status.BAD_REQUEST, "invalid_request",
"Malformed or invalid JSON body");
+ }
+ auditId = bestEffortActorIdForAudit(parsed);
+
+ final Response validationError = validateRequest(parsed);
+ if (validationError != null) {
+ return validationError;
+ }
+
+ final Instant now = Instant.now();
+ final DelegationPolicy toStore = toDomain(null, parsed, operatorId, now,
now);
+ final RegisterOrUpdateResult result =
policyService.registerOrUpdate(toStore);
+ auditId = result.getPolicy().getRegistrationId();
+ eventType = result.isCreated() ? "policy_registered" : "policy_updated";
+ outcome = ActionOutcome.SUCCESS;
+ final Response.Status status = result.isCreated() ?
Response.Status.CREATED : Response.Status.OK;
+ return
Response.status(status).entity(writeJson(toResponse(result.getPolicy()))).build();
+ } catch (DelegationPolicyAlreadyExistsException e) {
+ return errorResponse(Response.Status.CONFLICT, "actor_exists",
e.getMessage());
+ } catch (DelegationPolicyNotFoundException e) {
+ return errorResponse(Response.Status.NOT_FOUND, "policy_not_found",
e.getMessage());
+ } catch (RuntimeException e) {
+ return errorResponse(Response.Status.INTERNAL_SERVER_ERROR,
"storage_error", "Failed to register or update delegation policy");
+ } finally {
+ auditor.audit(Action.DELEGATION_LIFECYCLE, auditId,
ResourceType.DELEGATION_POLICY,
+ outcome, "event_type=" + eventType + " performed_by=" +
auditLabel(operatorId));
+ }
+ }
+
+ @GET
+ public Response list(@QueryParam("actorAuthority") String actorAuthority) {
Review Comment:
Only mutating endpoints emit audit records; `list/getOne` do not. Reading
who-can-impersonate-whom is arguably sensitive. I'd ad auditing here too.
##########
gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DelegationPolicyResource.java:
##########
@@ -0,0 +1,368 @@
+/*
+ * 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.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.apache.commons.lang3.StringUtils;
+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.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;
+import
org.apache.knox.gateway.services.knoxidf.delegation.DelegationPolicyAlreadyExistsException;
+import
org.apache.knox.gateway.services.knoxidf.delegation.DelegationPolicyList;
+import
org.apache.knox.gateway.services.knoxidf.delegation.DelegationPolicyNotFoundException;
+import
org.apache.knox.gateway.services.knoxidf.delegation.DelegationPolicyService;
+import
org.apache.knox.gateway.services.knoxidf.delegation.RegisterOrUpdateResult;
+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.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+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.security.Principal;
+import java.time.Instant;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Path(DelegationPolicyResource.RESOURCE_PATH)
+@Produces(MediaType.APPLICATION_JSON)
+public class DelegationPolicyResource {
+
+ static final String RESOURCE_PATH = "knoxidf/admin/v1/delegation-policies";
+
+ private static final ObjectMapper MAPPER = new ObjectMapper()
+ .registerModule(new JavaTimeModule())
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+
+ // 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 DelegationPolicyService policyService;
+
+ private int minTokenTtlSec =
GatewayConfig.DELEGATION_SERVICE_MIN_TOKEN_TTL_SEC_DEFAULT;
+ private int maxTokenTtlSec =
GatewayConfig.DELEGATION_SERVICE_MAX_TOKEN_TTL_SEC_DEFAULT;
+
+ @PostConstruct
+ public void init() {
+ final GatewayServices services = (GatewayServices)
+
servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+ policyService = services.getService(ServiceType.DELEGATION_POLICY_SERVICE);
+
+ final GatewayConfig config = (GatewayConfig)
servletContext.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
+ if (config != null) {
+ minTokenTtlSec = config.getDelegationServiceMinTokenTtlSec();
+ maxTokenTtlSec = config.getDelegationServiceMaxTokenTtlSec();
+ }
+ }
+
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response register(String body) {
+ final String operatorId = getOperatorId();
+ String auditId = "INVALID_REQUEST";
+ String outcome = ActionOutcome.FAILURE;
+
+ try {
+ final DelegationPolicyRequest parsed;
+ try {
+ parsed = MAPPER.readValue(body, DelegationPolicyRequest.class);
+ } catch (IOException e) {
+ return errorResponse(Response.Status.BAD_REQUEST, "invalid_request",
"Malformed or invalid JSON body");
+ }
+ auditId = bestEffortActorIdForAudit(parsed);
+
+ final Response validationError = validateRequest(parsed);
+ if (validationError != null) {
+ return validationError;
+ }
+
+ final Instant now = Instant.now();
+ final DelegationPolicy toStore = toDomain(null, parsed, operatorId, now,
now);
+ final DelegationPolicy stored = policyService.register(toStore);
+ auditId = stored.getRegistrationId();
+ outcome = ActionOutcome.SUCCESS;
+ return
Response.status(Response.Status.CREATED).entity(writeJson(toResponse(stored))).build();
+ } catch (DelegationPolicyAlreadyExistsException e) {
+ return errorResponse(Response.Status.CONFLICT, "actor_exists",
e.getMessage());
+ } catch (RuntimeException e) {
+ return errorResponse(Response.Status.INTERNAL_SERVER_ERROR,
"storage_error", "Failed to register delegation policy");
+ } finally {
+ auditor.audit(Action.DELEGATION_LIFECYCLE, auditId,
ResourceType.DELEGATION_POLICY,
+ outcome, "event_type=policy_registered performed_by=" +
auditLabel(operatorId));
+ }
+ }
+
+ @PUT
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response registerOrUpdate(String body) {
+ final String operatorId = getOperatorId();
+ String auditId = "INVALID_REQUEST";
+ String outcome = ActionOutcome.FAILURE;
+ String eventType = "policy_registered";
+
+ try {
+ final DelegationPolicyRequest parsed;
+ try {
+ parsed = MAPPER.readValue(body, DelegationPolicyRequest.class);
+ } catch (IOException e) {
+ return errorResponse(Response.Status.BAD_REQUEST, "invalid_request",
"Malformed or invalid JSON body");
+ }
+ auditId = bestEffortActorIdForAudit(parsed);
+
+ final Response validationError = validateRequest(parsed);
+ if (validationError != null) {
+ return validationError;
+ }
+
+ final Instant now = Instant.now();
+ final DelegationPolicy toStore = toDomain(null, parsed, operatorId, now,
now);
+ final RegisterOrUpdateResult result =
policyService.registerOrUpdate(toStore);
+ auditId = result.getPolicy().getRegistrationId();
+ eventType = result.isCreated() ? "policy_registered" : "policy_updated";
+ outcome = ActionOutcome.SUCCESS;
+ final Response.Status status = result.isCreated() ?
Response.Status.CREATED : Response.Status.OK;
+ return
Response.status(status).entity(writeJson(toResponse(result.getPolicy()))).build();
+ } catch (DelegationPolicyAlreadyExistsException e) {
+ return errorResponse(Response.Status.CONFLICT, "actor_exists",
e.getMessage());
+ } catch (DelegationPolicyNotFoundException e) {
+ return errorResponse(Response.Status.NOT_FOUND, "policy_not_found",
e.getMessage());
+ } catch (RuntimeException e) {
+ return errorResponse(Response.Status.INTERNAL_SERVER_ERROR,
"storage_error", "Failed to register or update delegation policy");
+ } finally {
+ auditor.audit(Action.DELEGATION_LIFECYCLE, auditId,
ResourceType.DELEGATION_POLICY,
+ outcome, "event_type=" + eventType + " performed_by=" +
auditLabel(operatorId));
+ }
+ }
+
+ @GET
+ public Response list(@QueryParam("actorAuthority") String actorAuthority) {
+ 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());
+ return Response.ok(writeJson(body)).build();
+ } catch (RuntimeException e) {
+ return errorResponse(Response.Status.INTERNAL_SERVER_ERROR,
"storage_error", "Failed to list delegation policies");
+ }
+ }
+
+ @GET
+ @Path("/{registrationId}")
+ public Response getOne(@PathParam("registrationId") String registrationId) {
+ 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);
+ }
+ 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");
+ }
+ }
+
+ @PUT
+ @Path("/{registrationId}")
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response update(@PathParam("registrationId") String registrationId,
String body) {
+ final String operatorId = getOperatorId();
+ String outcome = ActionOutcome.FAILURE;
+
+ try {
+ final DelegationPolicyRequest parsed;
+ try {
+ parsed = MAPPER.readValue(body, DelegationPolicyRequest.class);
+ } catch (IOException e) {
+ return errorResponse(Response.Status.BAD_REQUEST, "invalid_request",
"Malformed or invalid JSON body");
+ }
+
+ final Response validationError = validateRequest(parsed);
+ if (validationError != null) {
+ return validationError;
+ }
+
+ // PUT is full-replace, but actorAuthority/actorId/createdBy/createdAt
are immutable after
+ // registration and are never written by update() regardless of what
toStore carries for
+ // them; updatedAt is computed by the storage layer, not the caller. The
values passed here
+ // are inert placeholders -- the response is built from the policy
update() actually
+ // persisted and returned, not from toStore.
+ final Instant now = Instant.now();
+ final DelegationPolicy toStore = toDomain(registrationId, parsed,
operatorId, now, now);
+ final DelegationPolicy stored = policyService.update(registrationId,
toStore);
+ outcome = ActionOutcome.SUCCESS;
+ return Response.ok(writeJson(toResponse(stored))).build();
+ } catch (DelegationPolicyNotFoundException e) {
+ // registrationId may not exist at all, or it may exist with a different
+ // actorAuthority/actorId -- identity is immutable, so a mismatch is
rejected the same way as
+ // not-found (see DelegationPolicyService.update() javadoc). The two
causes are deliberately
+ // not distinguished here; GET the registrationId separately to tell
them apart.
+ return errorResponse(Response.Status.NOT_FOUND, "policy_not_found",
+ "Delegation policy not found, or actorAuthority/actorId does not
match the existing record: " + registrationId);
+ } catch (RuntimeException e) {
+ return errorResponse(Response.Status.INTERNAL_SERVER_ERROR,
"storage_error", "Failed to update delegation policy");
+ } finally {
+ auditor.audit(Action.DELEGATION_LIFECYCLE, registrationId,
ResourceType.DELEGATION_POLICY,
+ outcome, "event_type=policy_updated performed_by=" +
auditLabel(operatorId));
+ }
+ }
+
+ @DELETE
+ @Path("/{registrationId}")
+ public Response delete(@PathParam("registrationId") String registrationId) {
+ final String operatorId = getOperatorId();
+ String outcome = ActionOutcome.FAILURE;
+
+ try {
+ policyService.delete(registrationId);
+ outcome = ActionOutcome.SUCCESS;
+ return Response.noContent().build();
+ } catch (DelegationPolicyNotFoundException e) {
+ return errorResponse(Response.Status.NOT_FOUND, "policy_not_found",
e.getMessage());
+ } catch (RuntimeException e) {
+ return errorResponse(Response.Status.INTERNAL_SERVER_ERROR,
"storage_error", "Failed to delete delegation policy");
+ } finally {
+ auditor.audit(Action.DELEGATION_LIFECYCLE, registrationId,
ResourceType.DELEGATION_POLICY,
+ outcome, "event_type=policy_deleted performed_by=" +
auditLabel(operatorId));
+ }
+ }
+
+ private Response validateRequest(DelegationPolicyRequest req) {
+ if (StringUtils.isBlank(req.getActorAuthority())) {
+ return errorResponse(Response.Status.BAD_REQUEST, "invalid_request",
"actorAuthority is required");
+ }
+ if (StringUtils.isBlank(req.getActorId())) {
+ return errorResponse(Response.Status.BAD_REQUEST, "invalid_request",
"actorId is required");
+ }
+ final Integer tokenTtlSec = req.getTokenTtlSec();
+ if (tokenTtlSec != null && (tokenTtlSec < minTokenTtlSec || tokenTtlSec >
maxTokenTtlSec)) {
+ return errorResponse(Response.Status.BAD_REQUEST, "invalid_request",
+ "tokenTtlSec must be between " + minTokenTtlSec + " and " +
maxTokenTtlSec + " seconds (inclusive)");
+ }
Review Comment:
`tokenTtlSec` is optional on register/update: omit it and the policy is
stored with `token_ttl_sec = NULL`, so this fallback is the default path, not
an edge case. The admin API bounds-checks an explicit `tokenTtlSec` against
`[minTokenTtlSec, maxTokenTtlSec]`, but the fallback `configuredKnoxTokenTtlSec
` (in `JdbcDelegationPolicyService`) is never checked against those bounds, so
every policy that doesn't pin its own TTL can yield an effective TTL outside
the range the API enforces for explicit values.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]