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

roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 8c2d91cebb [#13510] feat(common): add AccessControlContentDTO (#13901)
8c2d91cebb is described below

commit 8c2d91cebb3cbdf550e2e40d4a1ab0659f4f52ba
Author: Bharath Krishna <[email protected]>
AuthorDate: Fri Oct 16 21:18:50 2026 -0700

    [#13510] feat(common): add AccessControlContentDTO (#13901)
    
    ### What changes were proposed in this pull request?
    
    Adds the DTO layer for the `system_access_control` policy type
    introduced in #13511, so it can travel over REST.
    
    - `PolicyContentDTO.AccessControlContentDTO` — a new `@JsonSubTypes`
    entry registered under `system_access_control`, carrying `privileges`
    and `applicableRoles`. Validation and rule/property derivation delegate
    to the `AccessControlContent` domain object.
    - `DTOConverters` — conversion in both directions between
    `AccessControlContent` and the DTO.
    - `docs/open-api/policies.yaml` — an `AccessControlContent` schema plus
    `AccessControlPolicy`, `AccessControlPolicyCreateRequest`, and
    `UpdateAccessControlPolicyContentRequest`, wired into the three
    `oneOf`/discriminator blocks and excluded from the three `custom` `not:
    enum` lists.
    - `docs/policies.md` — table row and the "not enforced yet" note.
    - `AccessControlContent` — a Javadoc note on `PERMITTED_PRIVILEGES`,
    since the spec enum mirrors that list.
    
    ### Why are the changes needed?
    
    #13511 added `ACCESS_CONTROL` to `Policy.BuiltInType`, but no Jackson
    subtype is registered for `system_access_control`. A request carrying
    that policy type fails deserialization before it reaches a handler, so
    the type is currently unreachable over REST. This change makes it decode
    and round-trip.
    
    The OpenAPI spec is updated here because it is the published contract
    for that wire format. The `not: enum` lists matter in particular: they
    define `custom` as "any type not in this list", so omitting
    `system_access_control` would let a spec-driven client treat it as a
    valid custom type, which the Java rejects.
    
    Fix: #13510
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. `system_access_control` becomes a valid `policyType` in the policy
    create, get, and update-content payloads, with `privileges` and
    `applicableRoles` fields. The type is still not enforced — this adds
    only the wire representation.
    
    ### How was this patch tested?
    
    New unit tests:
    
    - `TestPolicyContentDTO` — rules, properties and supported object types;
    absent fields; and delegation of validation (unpermitted privilege,
    missing roles, missing privileges).
    - `TestPolicyDTO` — full `PolicyDTO` round-trip, plus a hand-written
    JSON case pinning the wire encoding of privilege names.
    - `TestDTOConverters` — both conversion directions and equality after a
    round-trip.
    
    `./gradlew :common:spotlessJavaCheck :common:test` and
    `:api:spotlessJavaCheck :api:javadoc` pass.
    
    The spec change was validated with the same linter `:docs:build` runs:
    
    ```
    npx @redocly/[email protected] lint --extends=recommended-strict 
docs/open-api/openapi.yaml
    ```
---
 .../gravitino/policy/AccessControlContent.java     |  3 +
 .../gravitino/dto/policy/PolicyContentDTO.java     | 67 ++++++++++++++++++
 .../org/apache/gravitino/dto/policy/PolicyDTO.java |  5 +-
 .../dto/requests/PolicyCreateRequest.java          |  5 +-
 .../dto/requests/PolicyUpdateRequest.java          |  5 +-
 .../apache/gravitino/dto/util/DTOConverters.java   | 16 +++++
 .../gravitino/dto/policy/TestPolicyContentDTO.java | 58 ++++++++++++++++
 .../apache/gravitino/dto/policy/TestPolicyDTO.java | 62 +++++++++++++++++
 .../gravitino/dto/util/TestDTOConverters.java      | 28 ++++++++
 docs/open-api/policies.yaml                        | 81 +++++++++++++++++++++-
 docs/policies.md                                   |  7 +-
 11 files changed, 328 insertions(+), 9 deletions(-)

diff --git 
a/api/src/main/java/org/apache/gravitino/policy/AccessControlContent.java 
b/api/src/main/java/org/apache/gravitino/policy/AccessControlContent.java
index 63c254a571..8ee5f75762 100644
--- a/api/src/main/java/org/apache/gravitino/policy/AccessControlContent.java
+++ b/api/src/main/java/org/apache/gravitino/policy/AccessControlContent.java
@@ -56,6 +56,9 @@ public final class AccessControlContent implements 
PolicyContent {
    * It holds only privileges that act on the tagged object itself. Privileges 
that hand out access,
    * create entities, change what other principals execute, or open a path to 
untagged objects are
    * excluded.
+   *
+   * <p>The {@code AccessControlContent} schema in {@code 
docs/open-api/policies.yaml} mirrors this
+   * list as an enum; change both together.
    */
   public static final Set<Privilege.Name> PERMITTED_PRIVILEGES =
       ImmutableSet.of(
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/policy/PolicyContentDTO.java 
b/common/src/main/java/org/apache/gravitino/dto/policy/PolicyContentDTO.java
index 4ac19d7be4..f2417fc4d7 100644
--- a/common/src/main/java/org/apache/gravitino/dto/policy/PolicyContentDTO.java
+++ b/common/src/main/java/org/apache/gravitino/dto/policy/PolicyContentDTO.java
@@ -19,8 +19,10 @@
 package org.apache.gravitino.dto.policy;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import javax.annotation.Nullable;
@@ -29,6 +31,7 @@ import lombok.Builder;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.authorization.Privilege;
 import org.apache.gravitino.policy.ColumnMaskContent;
 import org.apache.gravitino.policy.IcebergDataCompactionContent;
 import org.apache.gravitino.policy.IcebergOrphanFileRemovalContent;
@@ -481,4 +484,68 @@ public interface PolicyContentDTO extends PolicyContent {
       return PolicyContents.columnMask(expression);
     }
   }
+
+  /** Represents typed access-control policy content. */
+  @EqualsAndHashCode
+  @ToString
+  @Builder(setterPrefix = "with")
+  @AllArgsConstructor(access = lombok.AccessLevel.PRIVATE)
+  class AccessControlContentDTO implements PolicyContentDTO {
+
+    @JsonProperty("privileges")
+    private List<Privilege.Name> privileges;
+
+    @JsonProperty("applicableRoles")
+    private List<String> applicableRoles;
+
+    // Default constructor for Jackson deserialization only.
+    private AccessControlContentDTO() {}
+
+    /**
+     * Returns the privileges the rule confers on the tagged object.
+     *
+     * @return the conferred privileges
+     */
+    public List<Privilege.Name> privileges() {
+      return privileges == null
+          ? Collections.emptyList()
+          : Collections.unmodifiableList(new ArrayList<>(privileges));
+    }
+
+    /**
+     * Returns the roles that satisfy the rule's condition.
+     *
+     * @return the applicable role names
+     */
+    public List<String> applicableRoles() {
+      return applicableRoles == null
+          ? Collections.emptyList()
+          : Collections.unmodifiableList(new ArrayList<>(applicableRoles));
+    }
+
+    @Override
+    public Set<MetadataObject.Type> supportedObjectTypes() {
+      return toDomainContent().supportedObjectTypes();
+    }
+
+    @Override
+    public Map<String, String> properties() {
+      return toDomainContent().properties();
+    }
+
+    @Override
+    public Map<String, Object> rules() {
+      return toDomainContent().rules();
+    }
+
+    @Override
+    public void validate() throws IllegalArgumentException {
+      PolicyContentDTO.super.validate();
+      toDomainContent().validate();
+    }
+
+    private PolicyContent toDomainContent() {
+      return PolicyContents.accessControl(privileges(), applicableRoles());
+    }
+  }
 }
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/policy/PolicyDTO.java 
b/common/src/main/java/org/apache/gravitino/dto/policy/PolicyDTO.java
index 8ceff3a327..0d7d5ef635 100644
--- a/common/src/main/java/org/apache/gravitino/dto/policy/PolicyDTO.java
+++ b/common/src/main/java/org/apache/gravitino/dto/policy/PolicyDTO.java
@@ -72,7 +72,10 @@ public class PolicyDTO implements Policy {
         name = "system_row_filter"),
     @JsonSubTypes.Type(
         value = PolicyContentDTO.ColumnMaskContentDTO.class,
-        name = "system_column_mask")
+        name = "system_column_mask"),
+    @JsonSubTypes.Type(
+        value = PolicyContentDTO.AccessControlContentDTO.class,
+        name = "system_access_control")
   })
   private PolicyContentDTO content;
 
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/requests/PolicyCreateRequest.java
 
b/common/src/main/java/org/apache/gravitino/dto/requests/PolicyCreateRequest.java
index 96a2357289..5a9e830ccf 100644
--- 
a/common/src/main/java/org/apache/gravitino/dto/requests/PolicyCreateRequest.java
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/PolicyCreateRequest.java
@@ -76,7 +76,10 @@ public class PolicyCreateRequest implements RESTRequest {
         name = "system_row_filter"),
     @JsonSubTypes.Type(
         value = PolicyContentDTO.ColumnMaskContentDTO.class,
-        name = "system_column_mask")
+        name = "system_column_mask"),
+    @JsonSubTypes.Type(
+        value = PolicyContentDTO.AccessControlContentDTO.class,
+        name = "system_access_control")
   })
   private final PolicyContentDTO policyContent;
 
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/requests/PolicyUpdateRequest.java
 
b/common/src/main/java/org/apache/gravitino/dto/requests/PolicyUpdateRequest.java
index 265296110b..49d4a3b547 100644
--- 
a/common/src/main/java/org/apache/gravitino/dto/requests/PolicyUpdateRequest.java
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/PolicyUpdateRequest.java
@@ -157,7 +157,10 @@ public interface PolicyUpdateRequest extends RESTRequest {
           name = "system_row_filter"),
       @JsonSubTypes.Type(
           value = PolicyContentDTO.ColumnMaskContentDTO.class,
-          name = "system_column_mask")
+          name = "system_column_mask"),
+      @JsonSubTypes.Type(
+          value = PolicyContentDTO.AccessControlContentDTO.class,
+          name = "system_access_control")
     })
     private final PolicyContentDTO newContent;
 
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java 
b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
index ebf54be030..0544512fd9 100644
--- a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
+++ b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
@@ -104,6 +104,7 @@ import org.apache.gravitino.json.JsonUtils;
 import org.apache.gravitino.messaging.Topic;
 import org.apache.gravitino.model.Model;
 import org.apache.gravitino.model.ModelVersion;
+import org.apache.gravitino.policy.AccessControlContent;
 import org.apache.gravitino.policy.ColumnMaskContent;
 import org.apache.gravitino.policy.IcebergDataCompactionContent;
 import org.apache.gravitino.policy.IcebergOrphanFileRemovalContent;
@@ -723,6 +724,14 @@ public class DTOConverters {
           .build();
     }
 
+    if (policyContent instanceof AccessControlContent) {
+      AccessControlContent accessControlContent = (AccessControlContent) 
policyContent;
+      return PolicyContentDTO.AccessControlContentDTO.builder()
+          .withPrivileges(accessControlContent.privileges())
+          .withApplicableRoles(accessControlContent.applicableRoles())
+          .build();
+    }
+
     throw new IllegalArgumentException("Unsupported policy content: " + 
policyContent);
   }
 
@@ -1605,6 +1614,13 @@ public class DTOConverters {
       return PolicyContents.columnMask(contentDTO.expression());
     }
 
+    if (policyContentDTO instanceof PolicyContentDTO.AccessControlContentDTO) {
+      PolicyContentDTO.AccessControlContentDTO accessControlContentDTO =
+          (PolicyContentDTO.AccessControlContentDTO) policyContentDTO;
+      return PolicyContents.accessControl(
+          accessControlContentDTO.privileges(), 
accessControlContentDTO.applicableRoles());
+    }
+
     throw new IllegalArgumentException(
         "Unsupported policy content type: " + policyContentDTO.getClass());
   }
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/policy/TestPolicyContentDTO.java
 
b/common/src/test/java/org/apache/gravitino/dto/policy/TestPolicyContentDTO.java
index 1f37c74c72..18b48e4ecc 100644
--- 
a/common/src/test/java/org/apache/gravitino/dto/policy/TestPolicyContentDTO.java
+++ 
b/common/src/test/java/org/apache/gravitino/dto/policy/TestPolicyContentDTO.java
@@ -18,10 +18,13 @@
  */
 package org.apache.gravitino.dto.policy;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import java.util.Map;
 import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.policy.AccessControlContent;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -42,4 +45,59 @@ public class TestPolicyContentDTO {
     // customRules from rules().
     Assertions.assertEquals(customRules, content.rules());
   }
+
+  @Test
+  public void testAccessControlContentDtoRules() {
+    PolicyContentDTO.AccessControlContentDTO content =
+        PolicyContentDTO.AccessControlContentDTO.builder()
+            .withPrivileges(ImmutableList.of(Privilege.Name.SELECT_TABLE))
+            .withApplicableRoles(ImmutableList.of("analyst"))
+            .build();
+
+    Assertions.assertEquals(
+        ImmutableList.of(Privilege.Name.SELECT_TABLE.name()),
+        content.rules().get(AccessControlContent.PRIVILEGES_KEY));
+    Assertions.assertEquals(
+        ImmutableList.of("analyst"),
+        content.rules().get(AccessControlContent.APPLICABLE_ROLES_KEY));
+    Assertions.assertTrue(content.properties().isEmpty());
+    
Assertions.assertFalse(content.supportedObjectTypes().contains(MetadataObject.Type.METALAKE));
+    Assertions.assertDoesNotThrow(content::validate);
+  }
+
+  @Test
+  public void testAccessControlContentDtoAbsentFields() {
+    PolicyContentDTO.AccessControlContentDTO content =
+        PolicyContentDTO.AccessControlContentDTO.builder().build();
+
+    Assertions.assertTrue(content.privileges().isEmpty());
+    Assertions.assertTrue(content.applicableRoles().isEmpty());
+  }
+
+  @Test
+  public void testAccessControlContentDtoDelegatesValidation() {
+    // A DTO that skips validate() would let a privilege outside the allowlist 
reach the server, so
+    // the delegation to the domain content is the contract under test here, 
not the rule itself.
+    PolicyContentDTO.AccessControlContentDTO unpermittedPrivilege =
+        PolicyContentDTO.AccessControlContentDTO.builder()
+            .withPrivileges(ImmutableList.of(Privilege.Name.CREATE_TABLE))
+            .withApplicableRoles(ImmutableList.of("analyst"))
+            .build();
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        unpermittedPrivilege::validate,
+        "expected a privilege outside PERMITTED_PRIVILEGES to be rejected");
+
+    PolicyContentDTO.AccessControlContentDTO noRoles =
+        PolicyContentDTO.AccessControlContentDTO.builder()
+            .withPrivileges(ImmutableList.of(Privilege.Name.SELECT_TABLE))
+            .build();
+    Assertions.assertThrows(IllegalArgumentException.class, noRoles::validate);
+
+    PolicyContentDTO.AccessControlContentDTO noPrivileges =
+        PolicyContentDTO.AccessControlContentDTO.builder()
+            .withApplicableRoles(ImmutableList.of("analyst"))
+            .build();
+    Assertions.assertThrows(IllegalArgumentException.class, 
noPrivileges::validate);
+  }
 }
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/policy/TestPolicyDTO.java 
b/common/src/test/java/org/apache/gravitino/dto/policy/TestPolicyDTO.java
index 8dc0292c10..1f8530f3e0 100644
--- a/common/src/test/java/org/apache/gravitino/dto/policy/TestPolicyDTO.java
+++ b/common/src/test/java/org/apache/gravitino/dto/policy/TestPolicyDTO.java
@@ -19,11 +19,13 @@
 package org.apache.gravitino.dto.policy;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import java.time.Instant;
 import java.util.Optional;
 import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.authorization.Privilege;
 import org.apache.gravitino.dto.AuditDTO;
 import org.apache.gravitino.json.JsonUtils;
 import org.apache.gravitino.policy.IcebergDataCompactionContent;
@@ -177,4 +179,64 @@ public class TestPolicyDTO {
         IcebergDataCompactionContent.DEFAULT_SORT_ORDER, 
contentDTO.sortOrder());
     Assertions.assertDoesNotThrow(contentDTO::validate);
   }
+
+  @Test
+  public void testAccessControlPolicySerDe() throws JsonProcessingException {
+    AuditDTO audit = 
AuditDTO.builder().withCreator("user1").withCreateTime(Instant.now()).build();
+    PolicyContentDTO.AccessControlContentDTO typedContent =
+        PolicyContentDTO.AccessControlContentDTO.builder()
+            .withPrivileges(
+                ImmutableList.of(Privilege.Name.SELECT_TABLE, 
Privilege.Name.PROBE_TABLE_LIKE))
+            .withApplicableRoles(ImmutableList.of("analyst", "auditor"))
+            .build();
+
+    PolicyDTO policyDTO =
+        PolicyDTO.builder()
+            .withName("pii-read")
+            .withComment("typed policy")
+            .withPolicyType("system_access_control")
+            .withEnabled(true)
+            .withContent(typedContent)
+            .withAudit(audit)
+            .build();
+
+    String serJson = JsonUtils.objectMapper().writeValueAsString(policyDTO);
+    PolicyDTO deserPolicyDTO = JsonUtils.objectMapper().readValue(serJson, 
PolicyDTO.class);
+
+    Assertions.assertEquals(policyDTO, deserPolicyDTO);
+    Assertions.assertInstanceOf(
+        PolicyContentDTO.AccessControlContentDTO.class, 
deserPolicyDTO.content());
+
+    PolicyContentDTO.AccessControlContentDTO deserContent =
+        (PolicyContentDTO.AccessControlContentDTO) deserPolicyDTO.content();
+    Assertions.assertEquals(
+        ImmutableList.of(Privilege.Name.SELECT_TABLE, 
Privilege.Name.PROBE_TABLE_LIKE),
+        deserContent.privileges());
+    Assertions.assertEquals(ImmutableList.of("analyst", "auditor"), 
deserContent.applicableRoles());
+  }
+
+  @Test
+  public void testAccessControlPolicyDeserializesPrivilegeNames() throws 
JsonProcessingException {
+    // Privileges travel as enum names on the wire; a change to that encoding 
would break every
+    // existing policy stored against this type.
+    String json =
+        "{"
+            + "\"name\":\"pii-read\","
+            + "\"comment\":\"typed policy\","
+            + "\"policyType\":\"system_access_control\","
+            + "\"enabled\":true,"
+            + "\"content\":{"
+            + "\"privileges\":[\"SELECT_TABLE\"],"
+            + "\"applicableRoles\":[\"analyst\"]"
+            + "}"
+            + "}";
+
+    PolicyDTO policyDTO = JsonUtils.objectMapper().readValue(json, 
PolicyDTO.class);
+    PolicyContentDTO.AccessControlContentDTO contentDTO =
+        (PolicyContentDTO.AccessControlContentDTO) policyDTO.content();
+
+    Assertions.assertEquals(ImmutableList.of(Privilege.Name.SELECT_TABLE), 
contentDTO.privileges());
+    Assertions.assertEquals(ImmutableList.of("analyst"), 
contentDTO.applicableRoles());
+    Assertions.assertDoesNotThrow(contentDTO::validate);
+  }
 }
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/util/TestDTOConverters.java 
b/common/src/test/java/org/apache/gravitino/dto/util/TestDTOConverters.java
index 479bc3a340..4ae1e7a654 100644
--- a/common/src/test/java/org/apache/gravitino/dto/util/TestDTOConverters.java
+++ b/common/src/test/java/org/apache/gravitino/dto/util/TestDTOConverters.java
@@ -19,16 +19,19 @@
 
 package org.apache.gravitino.dto.util;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
+import org.apache.gravitino.authorization.Privilege;
 import org.apache.gravitino.dto.AuditDTO;
 import org.apache.gravitino.dto.job.JobTemplateDTO;
 import org.apache.gravitino.dto.job.ShellJobTemplateDTO;
 import org.apache.gravitino.dto.job.SparkJobTemplateDTO;
+import org.apache.gravitino.dto.policy.PolicyContentDTO;
 import org.apache.gravitino.dto.rel.expressions.LiteralDTO;
 import org.apache.gravitino.dto.rel.partitioning.ListPartitioningDTO;
 import org.apache.gravitino.dto.rel.partitioning.RangePartitioningDTO;
@@ -39,6 +42,9 @@ import 
org.apache.gravitino.dto.rel.partitions.RangePartitionDTO;
 import org.apache.gravitino.job.JobTemplate;
 import org.apache.gravitino.job.ShellJobTemplate;
 import org.apache.gravitino.job.SparkJobTemplate;
+import org.apache.gravitino.policy.AccessControlContent;
+import org.apache.gravitino.policy.PolicyContent;
+import org.apache.gravitino.policy.PolicyContents;
 import org.apache.gravitino.rel.expressions.literals.Literal;
 import org.apache.gravitino.rel.expressions.literals.Literals;
 import org.apache.gravitino.rel.expressions.transforms.Transform;
@@ -368,4 +374,26 @@ public class TestDTOConverters {
     Assertions.assertTrue(exception.getMessage().contains("job-1"));
     Assertions.assertFalse(exception.getMessage().contains(malformedJson));
   }
+
+  @Test
+  public void testAccessControlPolicyContentConvert() {
+    PolicyContent content =
+        PolicyContents.accessControl(
+            ImmutableList.of(Privilege.Name.SELECT_TABLE, 
Privilege.Name.READ_FILESET),
+            ImmutableList.of("analyst"));
+
+    PolicyContentDTO contentDTO = DTOConverters.toDTO(content);
+    
Assertions.assertInstanceOf(PolicyContentDTO.AccessControlContentDTO.class, 
contentDTO);
+
+    PolicyContentDTO.AccessControlContentDTO accessControlContentDTO =
+        (PolicyContentDTO.AccessControlContentDTO) contentDTO;
+    Assertions.assertEquals(
+        ImmutableList.of(Privilege.Name.SELECT_TABLE, 
Privilege.Name.READ_FILESET),
+        accessControlContentDTO.privileges());
+    Assertions.assertEquals(ImmutableList.of("analyst"), 
accessControlContentDTO.applicableRoles());
+
+    PolicyContent roundTripped = DTOConverters.fromDTO(contentDTO);
+    Assertions.assertInstanceOf(AccessControlContent.class, roundTripped);
+    Assertions.assertEquals(content, roundTripped);
+  }
 }
diff --git a/docs/open-api/policies.yaml b/docs/open-api/policies.yaml
index 659a3313d2..c576a2ba7b 100644
--- a/docs/open-api/policies.yaml
+++ b/docs/open-api/policies.yaml
@@ -540,6 +540,40 @@ components:
             `else`. The UTF-8 source must not exceed 16 KiB; see the Policies 
documentation for
             allowlisted actions, grammar, limits, and examples.
 
+    AccessControlContent:
+      type: object
+      description: Rule content for a built-in access-control policy.
+      required:
+        - privileges
+        - applicableRoles
+      properties:
+        privileges:
+          type: array
+          minItems: 1
+          description: >-
+            The privileges granted when the rule matches. Limited to 
privileges that apply to a
+            whole securable object; column-scoped privileges are not 
permitted. This enum mirrors
+            AccessControlContent.PERMITTED_PRIVILEGES; change both together.
+          items:
+            type: string
+            enum:
+              - SELECT_TABLE
+              - MODIFY_TABLE
+              - PROBE_TABLE_LIKE
+              - SELECT_VIEW
+              - READ_FILESET
+              - WRITE_FILESET
+              - CONSUME_TOPIC
+              - PRODUCE_TOPIC
+              - USE_MODEL
+              - EXECUTE_FUNCTION
+        applicableRoles:
+          type: array
+          minItems: 1
+          description: The names of the roles the rule applies to.
+          items:
+            type: string
+
     RowFilterPolicy:
       allOf:
         - $ref: '#/components/schemas/PolicyBase'
@@ -588,6 +622,30 @@ components:
             content:
               $ref: '#/components/schemas/ColumnMaskContent'
 
+    AccessControlPolicy:
+      allOf:
+        - $ref: '#/components/schemas/PolicyBase'
+        - type: object
+          required: [content]
+          properties:
+            policyType:
+              type: string
+              enum: [system_access_control]
+            content:
+              $ref: '#/components/schemas/AccessControlContent'
+
+    AccessControlPolicyCreateRequest:
+      allOf:
+        - $ref: '#/components/schemas/PolicyCreateRequestBase'
+        - type: object
+          required: [content]
+          properties:
+            policyType:
+              type: string
+              enum: [system_access_control]
+            content:
+              $ref: '#/components/schemas/AccessControlContent'
+
     PolicyContentBase:
       type: object
       description: A base content structure for policies
@@ -670,7 +728,7 @@ components:
             policyType:
               type: string
               not:
-                enum: [system_iceberg_compaction, 
system_iceberg_orphan_file_removal, system_iceberg_rewrite_manifests, 
system_iceberg_snapshot_expiration, system_row_filter, system_column_mask]
+                enum: [system_iceberg_compaction, 
system_iceberg_orphan_file_removal, system_iceberg_rewrite_manifests, 
system_iceberg_snapshot_expiration, system_row_filter, system_column_mask, 
system_access_control]
             content:
               $ref: '#/components/schemas/CustomPolicyContent'
 
@@ -685,6 +743,7 @@ components:
         - $ref: '#/components/schemas/IcebergSnapshotExpirationPolicy'
         - $ref: '#/components/schemas/RowFilterPolicy'
         - $ref: '#/components/schemas/ColumnMaskPolicy'
+        - $ref: '#/components/schemas/AccessControlPolicy'
       discriminator:
         propertyName: policyType
         mapping:
@@ -695,6 +754,7 @@ components:
           system_iceberg_snapshot_expiration: 
'#/components/schemas/IcebergSnapshotExpirationPolicy'
           system_row_filter: '#/components/schemas/RowFilterPolicy'
           system_column_mask: '#/components/schemas/ColumnMaskPolicy'
+          system_access_control: '#/components/schemas/AccessControlPolicy'
 
     PolicyListResponse:
       type: object
@@ -789,7 +849,7 @@ components:
             policyType:
               type: string
               not:
-                enum: [system_iceberg_compaction, 
system_iceberg_orphan_file_removal, system_iceberg_rewrite_manifests, 
system_iceberg_snapshot_expiration, system_row_filter, system_column_mask]
+                enum: [system_iceberg_compaction, 
system_iceberg_orphan_file_removal, system_iceberg_rewrite_manifests, 
system_iceberg_snapshot_expiration, system_row_filter, system_column_mask, 
system_access_control]
             content:
               $ref: '#/components/schemas/CustomPolicyContent'
 
@@ -804,6 +864,7 @@ components:
         - $ref: '#/components/schemas/IcebergCompactionPolicyCreateRequest'
         - $ref: '#/components/schemas/RowFilterPolicyCreateRequest'
         - $ref: '#/components/schemas/ColumnMaskPolicyCreateRequest'
+        - $ref: '#/components/schemas/AccessControlPolicyCreateRequest'
       discriminator:
         propertyName: policyType
         mapping:
@@ -814,6 +875,7 @@ components:
           system_iceberg_compaction: 
'#/components/schemas/IcebergCompactionPolicyCreateRequest'
           system_row_filter: 
'#/components/schemas/RowFilterPolicyCreateRequest'
           system_column_mask: 
'#/components/schemas/ColumnMaskPolicyCreateRequest'
+          system_access_control: 
'#/components/schemas/AccessControlPolicyCreateRequest'
 
 
     PolicyUpdatesRequest:
@@ -903,7 +965,7 @@ components:
             policyType:
               type: string
               not:
-                enum: [system_iceberg_compaction, 
system_iceberg_orphan_file_removal, system_iceberg_rewrite_manifests, 
system_iceberg_snapshot_expiration, system_row_filter, system_column_mask]
+                enum: [system_iceberg_compaction, 
system_iceberg_orphan_file_removal, system_iceberg_rewrite_manifests, 
system_iceberg_snapshot_expiration, system_row_filter, system_column_mask, 
system_access_control]
             newContent:
               $ref: '#/components/schemas/CustomPolicyContent'
 
@@ -973,6 +1035,17 @@ components:
             newContent:
               $ref: '#/components/schemas/ColumnMaskContent'
 
+    UpdateAccessControlPolicyContentRequest:
+      allOf:
+        - $ref: '#/components/schemas/UpdatePolicyContentRequestBase'
+        - type: object
+          properties:
+            policyType:
+              type: string
+              enum: [system_access_control]
+            newContent:
+              $ref: '#/components/schemas/AccessControlContent'
+
     UpdatePolicyContentRequest:
       description: Selects typed content using the sibling policyType field, 
matching REST deserialization.
       oneOf:
@@ -983,6 +1056,7 @@ components:
         - $ref: 
'#/components/schemas/UpdateIcebergSnapshotExpirationPolicyContentRequest'
         - $ref: '#/components/schemas/UpdateRowFilterPolicyContentRequest'
         - $ref: '#/components/schemas/UpdateColumnMaskPolicyContentRequest'
+        - $ref: '#/components/schemas/UpdateAccessControlPolicyContentRequest'
       discriminator:
         propertyName: policyType
         mapping:
@@ -993,6 +1067,7 @@ components:
           system_iceberg_snapshot_expiration: 
'#/components/schemas/UpdateIcebergSnapshotExpirationPolicyContentRequest'
           system_row_filter: 
'#/components/schemas/UpdateRowFilterPolicyContentRequest'
           system_column_mask: 
'#/components/schemas/UpdateColumnMaskPolicyContentRequest'
+          system_access_control: 
'#/components/schemas/UpdateAccessControlPolicyContentRequest'
       example: {
         "@type": "updateContent",
         "policyType": "custom",
diff --git a/docs/policies.md b/docs/policies.md
index 832126444a..3e3889c075 100644
--- a/docs/policies.md
+++ b/docs/policies.md
@@ -41,11 +41,12 @@ policy. A table with only `data_domain=risk` does not.
 | `system_iceberg_orphan_file_removal` | Orphan-file cleanup options          
| Table maintenance service |
 | `system_row_filter`                  | Row-filter expression                
| Not enforced yet          |
 | `system_column_mask`                 | Column-mask expression               
| Not enforced yet          |
+| `system_access_control`              | Privileges and applicable roles      
| Not enforced yet          |
 | `custom`                             | A free-form map that you define      
| A system that you provide |
 
-The row-filter and column-mask policy types can be created and associated with 
tags, but Gravitino
-does not enforce them yet. Until enforcement is available, these policy types 
do not restrict data
-access.
+The row-filter, column-mask, and access-control policy types can be created 
and associated with
+tags, but Gravitino does not enforce them yet. Until enforcement is available, 
these policy types
+do not restrict data access.
 
 ### Read-Restriction Expressions
 

Reply via email to