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