mchades commented on code in PR #7842: URL: https://github.com/apache/gravitino/pull/7842#discussion_r2269290725
########## server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectPolicyOperations.java: ########## @@ -0,0 +1,375 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.gravitino.server.web.rest; + +import com.codahale.metrics.annotation.ResponseMetered; +import com.codahale.metrics.annotation.Timed; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +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.Response; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.gravitino.MetadataObject; +import org.apache.gravitino.MetadataObjects; +import org.apache.gravitino.dto.policy.PolicyDTO; +import org.apache.gravitino.dto.requests.PoliciesAssociateRequest; +import org.apache.gravitino.dto.responses.NameListResponse; +import org.apache.gravitino.dto.responses.PolicyListResponse; +import org.apache.gravitino.dto.responses.PolicyResponse; +import org.apache.gravitino.dto.util.DTOConverters; +import org.apache.gravitino.exceptions.NoSuchPolicyException; +import org.apache.gravitino.metrics.MetricNames; +import org.apache.gravitino.policy.Policy; +import org.apache.gravitino.policy.PolicyDispatcher; +import org.apache.gravitino.server.web.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Path("/metalakes/{metalake}/objects/{type}/{fullName}/policies") +public class MetadataObjectPolicyOperations { + private static final Logger LOG = LoggerFactory.getLogger(MetadataObjectPolicyOperations.class); + + private final PolicyDispatcher policyDispatcher; + + @Context private HttpServletRequest httpRequest; + + @Inject + public MetadataObjectPolicyOperations(PolicyDispatcher policyDispatcher) { + this.policyDispatcher = policyDispatcher; + } + + @GET + @Path("{policy}") + @Produces("application/vnd.gravitino.v1+json") + @Timed(name = "get-object-policy." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) + @ResponseMetered(name = "get-object-policy", absolute = true) + public Response getPolicyForObject( + @PathParam("metalake") String metalake, + @PathParam("type") String type, + @PathParam("fullName") String fullName, + @PathParam("policy") String policyName) { + LOG.info( + "Received get policy {} request for object type: {}, full name: {} under metalake: {}", + policyName, + type, + fullName, + metalake); + + try { + return Utils.doAs( + httpRequest, + () -> { + MetadataObject object = + MetadataObjects.parse( + fullName, MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT))); + + // 1. Check if the policy is directly attached to the object. + Optional<Policy> policy = getPolicyForObject(metalake, object, policyName); + if (policy.isPresent()) { + LOG.info( + "Get policy: {} for object type: {}, full name: {} under metalake: {}", + policyName, + type, + fullName, + metalake); + return Utils.ok( + new PolicyResponse(DTOConverters.toDTO(policy.get(), Optional.of(false)))); + } + + // 2. If not found, try to find an inheritable policy from its parents. + // First, ensure the target policy exists and is inheritable for this object type. + Policy targetPolicy = policyDispatcher.getPolicy(metalake, policyName); + if (!targetPolicy.inheritable() + || !targetPolicy.supportedObjectTypes().contains(object.type())) { + return getPolicyNotFoundResponse(metalake, policyName, type, fullName); + } + + // 3. Check for exclusive policy conflicts on the current object. + // If an exclusive policy of the same type already exists on this object, the target + // policy cannot be inherited. + Policy[] currentObjectPolicies = + policyDispatcher.listPolicyInfosForMetadataObject(metalake, object); + if (hasConflictExclusivePolicy(currentObjectPolicies, targetPolicy)) { + return getPolicyNotFoundResponse(metalake, policyName, type, fullName); + } + + // 4. Traverse up the hierarchy to find the policy from a parent. + MetadataObject parentObject = MetadataObjects.parent(object); + while (parentObject != null) { + if (!targetPolicy.supportedObjectTypes().contains(parentObject.type())) { + // If the parent object type is not supported by the target policy, skip it. + parentObject = MetadataObjects.parent(parentObject); + continue; + } + + Policy[] parentPolicies = + policyDispatcher.listPolicyInfosForMetadataObject(metalake, parentObject); + + // If another exclusive policy of the same type is found on a parent, it blocks + // inheritance from ancestors further up. + if (hasConflictExclusivePolicy(parentPolicies, targetPolicy)) { + return getPolicyNotFoundResponse(metalake, policyName, type, fullName); + } + + // Check if the target policy is attached to this parent. + Optional<Policy> inheritedPolicy = + ArrayUtils.isEmpty(parentPolicies) + ? Optional.empty() + : Arrays.stream(parentPolicies) + .filter(p -> p.name().equals(policyName)) + .findFirst(); + if (inheritedPolicy.isPresent()) { + // Policy found. Return it as an inherited policy. + LOG.info( + "Found policy: {} for object type: {}, full name: {} under metalake: {}", + policyName, + type, + fullName, + metalake); + return Utils.ok( + new PolicyResponse( + DTOConverters.toDTO(inheritedPolicy.get(), Optional.of(true)))); + } + + parentObject = MetadataObjects.parent(parentObject); + } + + // 5. If no policy is found after checking all parents, return not found. + return getPolicyNotFoundResponse(metalake, policyName, type, fullName); + }); + + } catch (Exception e) { + return ExceptionHandlers.handlePolicyException(OperationType.GET, policyName, fullName, e); + } + } + + @GET + @Produces("application/vnd.gravitino.v1+json") + @Timed(name = "list-object-policies." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) + @ResponseMetered(name = "list-object-policies", absolute = true) + public Response listPoliciesForMetadataObject( + @PathParam("metalake") String metalake, + @PathParam("type") String type, + @PathParam("fullName") String fullName, + @QueryParam("details") @DefaultValue("false") boolean verbose) { + LOG.info( + "Received list policy {} request for object type: {}, full name: {} under metalake: {}", + verbose ? "infos" : "names", + type, + fullName, + metalake); + + try { + return Utils.doAs( + httpRequest, + () -> { + MetadataObject object = + MetadataObjects.parse( + fullName, MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT))); + + Set<PolicyDTO> policies = new HashSet<>(); + Set<String> exclusivePolicyTypes = new HashSet<>(); + + // 1. Get policies directly attached to the object. + Policy[] nonInheritedPolicies = + policyDispatcher.listPolicyInfosForMetadataObject(metalake, object); + for (Policy policy : nonInheritedPolicies) { + if (policy.exclusive()) { + // Record the types of exclusive policies to handle inheritance blocking. + exclusivePolicyTypes.add(policy.policyType()); + } + policies.add(DTOConverters.toDTO(policy, Optional.of(false))); + } + + // 2. Traverse up the hierarchy to collect all inheritable policies. + MetadataObject parentObject = MetadataObjects.parent(object); + while (parentObject != null) { + Policy[] inheritedPolicies = + policyDispatcher.listPolicyInfosForMetadataObject(metalake, parentObject); + for (Policy policy : inheritedPolicies) { + // An inherited policy is collected only if: + // a. It is marked as inheritable. + // b. It supports the current object's type. + if (!policy.supportedObjectTypes().contains(object.type()) + || !policy.inheritable()) { + continue; + } + + // c. If it's an exclusive policy, its type must not have been added by a + // closer-level policy (from the child or a lower-level parent). This + // ensures that child's exclusive policies override parent's. + if (policy.exclusive()) { + if (exclusivePolicyTypes.contains(policy.policyType())) { + continue; + } + // Record the exclusive policy type to block any further policies of the same + // type from higher-level ancestors. + exclusivePolicyTypes.add(policy.policyType()); + } + policies.add(DTOConverters.toDTO(policy, Optional.of(true))); + } + parentObject = MetadataObjects.parent(parentObject); + } + + // 3. Format and return the response + if (verbose) { + LOG.info( + "List {} policies info for object type: {}, full name: {} under metalake: {}", + policies.size(), + type, + fullName, + metalake); + return Utils.ok(new PolicyListResponse(policies.toArray(new PolicyDTO[0]))); + + } else { + String[] policyNames = policies.stream().map(PolicyDTO::name).toArray(String[]::new); + + LOG.info( + "List {} policies for object type: {}, full name: {} under metalake: {}", + policyNames.length, + type, + fullName, + metalake); + return Utils.ok(new NameListResponse(policyNames)); + } + }); + + } catch (Exception e) { + return ExceptionHandlers.handlePolicyException(OperationType.LIST, "", fullName, e); + } + } + + @POST + @Produces("application/vnd.gravitino.v1+json") + @Timed(name = "associate-object-policies." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) + @ResponseMetered(name = "associate-object-policies", absolute = true) + public Response associatePoliciesForObject( + @PathParam("metalake") String metalake, + @PathParam("type") String type, + @PathParam("fullName") String fullName, + PoliciesAssociateRequest request) { + LOG.info( + "Received associate policies request for object type: {}, full name: {} under metalake: {}", + type, + fullName, + metalake); + + try { + return Utils.doAs( + httpRequest, + () -> { + request.validate(); + MetadataObject object = + MetadataObjects.parse( + fullName, MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT))); + String[] policyNames = + policyDispatcher.associatePoliciesForMetadataObject( + metalake, object, request.getPoliciesToAdd(), request.getPoliciesToRemove()); + policyNames = policyNames == null ? new String[0] : policyNames; Review Comment: The association operation validates the exclusive policy to ensure that the same type of exclusive policy cannot be associated with one metadata object (the logic is in `associatePoliciesWithMetadataObject.associatePoliciesForObject`), while the list and get operations perform the calculation (determine the inheritance relationship), not validation. -- 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]
