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 34272ba4fa [#13692] feat(policy): define read restriction policy
(#13693)
34272ba4fa is described below
commit 34272ba4fa954c671a927619a72850ffdb5b47b5
Author: roryqi <[email protected]>
AuthorDate: Fri Oct 9 11:37:45 2026 +0800
[#13692] feat(policy): define read restriction policy (#13693)
### What changes were proposed in this pull request?
Add the first policy-model implementation slice for tag-based Iceberg
read restrictions:
- Add the built-in `system_row_filter` and `system_column_mask` policy
types.
- Define distinct typed content for row filters and column masks.
- Store exactly one restricted Rego `expression` in each policy, aligned
with #13546.
- Share non-blank and 16 KiB UTF-8 source validation.
- Support create, update, REST serialization, DTO conversion, and
persisted-content round trips.
- Reject legacy `rules` content and content that does not match its
built-in policy type.
Expression parsing, subject and schema binding, policy resolution, and
Iceberg REST enforcement remain follow-up work.
### Why are the changes needed?
The tag-based read-restriction design needs typed policy content before
expressions can be parsed, resolved, and returned through Iceberg REST.
Fix: #13692
### Does this PR introduce _any_ user-facing change?
Yes. It adds the `system_row_filter` and `system_column_mask` built-in
policy types and their Java and REST content shape:
```json
{
"expression": "..."
}
```
### How was this patch tested?
- `./gradlew :api:spotlessApply :common:spotlessApply
:core:spotlessApply`
- `./gradlew :api:javadoc :api:test --tests
org.apache.gravitino.policy.TestReadRestrictionContent --tests
org.apache.gravitino.policy.TestPolicyBuiltInType`
- `./gradlew :common:test --tests
org.apache.gravitino.dto.policy.TestReadRestrictionPolicyDTO`
- `./gradlew :core:test --tests
org.apache.gravitino.meta.TestPolicyEntity`
- `git diff upstream/main...HEAD --check`
---
.../apache/gravitino/policy/ColumnMaskContent.java | 91 +++++++++
.../java/org/apache/gravitino/policy/Policy.java | 6 +
.../apache/gravitino/policy/PolicyContents.java | 20 ++
.../gravitino/policy/ReadRestrictionContent.java | 52 +++++
.../apache/gravitino/policy/RowFilterContent.java | 91 +++++++++
.../gravitino/policy/TestPolicyBuiltInType.java | 15 ++
.../policy/TestReadRestrictionContent.java | 89 +++++++++
.../gravitino/dto/policy/PolicyContentDTO.java | 102 ++++++++++
.../org/apache/gravitino/dto/policy/PolicyDTO.java | 8 +-
.../dto/requests/PolicyCreateRequest.java | 8 +-
.../dto/requests/PolicyUpdateRequest.java | 8 +-
.../apache/gravitino/dto/util/DTOConverters.java | 28 +++
.../dto/policy/TestReadRestrictionPolicyDTO.java | 214 +++++++++++++++++++++
.../apache/gravitino/meta/TestPolicyEntity.java | 31 +++
.../apache/gravitino/policy/TestPolicyManager.java | 25 +++
docs/open-api/policies.yaml | 116 ++++++++++-
docs/policies.md | 15 +-
17 files changed, 911 insertions(+), 8 deletions(-)
diff --git
a/api/src/main/java/org/apache/gravitino/policy/ColumnMaskContent.java
b/api/src/main/java/org/apache/gravitino/policy/ColumnMaskContent.java
new file mode 100644
index 0000000000..a5120a6607
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/policy/ColumnMaskContent.java
@@ -0,0 +1,91 @@
+/*
+ * 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.policy;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.apache.gravitino.MetadataObject;
+
+/** Built-in policy content for masking columns of tagged tables or tagged
columns. */
+public final class ColumnMaskContent extends ReadRestrictionContent {
+
+ private static final Set<MetadataObject.Type> SUPPORTED_OBJECT_TYPES =
+ ImmutableSet.of(MetadataObject.Type.TABLE, MetadataObject.Type.COLUMN);
+
+ @Nullable private final String expression;
+
+ /** Default constructor for Jackson deserialization only. */
+ private ColumnMaskContent() {
+ this.expression = null;
+ }
+
+ ColumnMaskContent(String expression) {
+ this.expression = expression;
+ validate();
+ }
+
+ /**
+ * Returns the authored column-mask expression.
+ *
+ * @return column-mask expression
+ */
+ public String expression() {
+ return expression;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<MetadataObject.Type> supportedObjectTypes() {
+ return SUPPORTED_OBJECT_TYPES;
+ }
+
+ @Override
+ public Map<String, Object> rules() {
+ return Collections.singletonMap("expression", expression);
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ super.validate();
+ validateSource(expression, "column-mask expression");
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof ColumnMaskContent)) {
+ return false;
+ }
+ ColumnMaskContent that = (ColumnMaskContent) other;
+ return Objects.equals(expression, that.expression);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(expression);
+ }
+
+ @Override
+ public String toString() {
+ return "ColumnMaskContent{" + "expression='" + expression + '\'' + '}';
+ }
+}
diff --git a/api/src/main/java/org/apache/gravitino/policy/Policy.java
b/api/src/main/java/org/apache/gravitino/policy/Policy.java
index 04d30b45c1..80519d1157 100644
--- a/api/src/main/java/org/apache/gravitino/policy/Policy.java
+++ b/api/src/main/java/org/apache/gravitino/policy/Policy.java
@@ -47,6 +47,12 @@ public interface Policy extends Auditable {
BUILT_IN_TYPE_PREFIX + "iceberg_orphan_file_removal",
IcebergOrphanFileRemovalContent.class),
+ /** Built-in policy type for filtering rows of tagged tables. */
+ ROW_FILTER(BUILT_IN_TYPE_PREFIX + "row_filter", RowFilterContent.class),
+
+ /** Built-in policy type for masking columns of tagged tables or tagged
columns. */
+ COLUMN_MASK(BUILT_IN_TYPE_PREFIX + "column_mask", ColumnMaskContent.class),
+
/** Non-built-in policies use the fixed wire value {@code custom}. */
CUSTOM("custom", PolicyContents.CustomContent.class);
diff --git a/api/src/main/java/org/apache/gravitino/policy/PolicyContents.java
b/api/src/main/java/org/apache/gravitino/policy/PolicyContents.java
index f767d98f75..50e16ac5fe 100644
--- a/api/src/main/java/org/apache/gravitino/policy/PolicyContents.java
+++ b/api/src/main/java/org/apache/gravitino/policy/PolicyContents.java
@@ -127,6 +127,26 @@ public class PolicyContents {
return new IcebergOrphanFileRemovalContent(olderThanDays, location,
dryRun);
}
+ /**
+ * Creates a row-filter policy for tagged tables.
+ *
+ * @param expression row-filter expression
+ * @return validated read-restriction policy content
+ */
+ public static RowFilterContent rowFilter(String expression) {
+ return new RowFilterContent(expression);
+ }
+
+ /**
+ * Creates a column-mask policy for tagged tables or columns.
+ *
+ * @param expression column-mask expression
+ * @return validated read-restriction policy content
+ */
+ public static ColumnMaskContent columnMask(String expression) {
+ return new ColumnMaskContent(expression);
+ }
+
private PolicyContents() {}
/**
diff --git
a/api/src/main/java/org/apache/gravitino/policy/ReadRestrictionContent.java
b/api/src/main/java/org/apache/gravitino/policy/ReadRestrictionContent.java
new file mode 100644
index 0000000000..e1bb820df6
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/policy/ReadRestrictionContent.java
@@ -0,0 +1,52 @@
+/*
+ * 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.policy;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+
+/** Shared behavior for built-in policies that restrict data reads. */
+public abstract class ReadRestrictionContent implements PolicyContent {
+
+ /** Maximum UTF-8 size of an authored expression. */
+ public static final int MAX_SOURCE_LENGTH_BYTES = 16 * 1024;
+
+ @Override
+ public Map<String, String> properties() {
+ return ImmutableMap.of();
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ PolicyContent.super.validate();
+ }
+
+ static void validateSource(@Nullable String source, String fieldName) {
+ Preconditions.checkArgument(StringUtils.isNotBlank(source), "%s cannot be
blank", fieldName);
+ Preconditions.checkArgument(
+ source.getBytes(StandardCharsets.UTF_8).length <=
MAX_SOURCE_LENGTH_BYTES,
+ "%s cannot exceed %s UTF-8 bytes",
+ fieldName,
+ MAX_SOURCE_LENGTH_BYTES);
+ }
+}
diff --git
a/api/src/main/java/org/apache/gravitino/policy/RowFilterContent.java
b/api/src/main/java/org/apache/gravitino/policy/RowFilterContent.java
new file mode 100644
index 0000000000..9a78e44fd3
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/policy/RowFilterContent.java
@@ -0,0 +1,91 @@
+/*
+ * 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.policy;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.apache.gravitino.MetadataObject;
+
+/** Built-in policy content for filtering rows of tagged tables. */
+public final class RowFilterContent extends ReadRestrictionContent {
+
+ private static final Set<MetadataObject.Type> SUPPORTED_OBJECT_TYPES =
+ ImmutableSet.of(MetadataObject.Type.TABLE);
+
+ @Nullable private final String expression;
+
+ /** Default constructor for Jackson deserialization only. */
+ private RowFilterContent() {
+ this.expression = null;
+ }
+
+ RowFilterContent(String expression) {
+ this.expression = expression;
+ validate();
+ }
+
+ /**
+ * Returns the authored row-filter expression.
+ *
+ * @return row-filter expression
+ */
+ public String expression() {
+ return expression;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<MetadataObject.Type> supportedObjectTypes() {
+ return SUPPORTED_OBJECT_TYPES;
+ }
+
+ @Override
+ public Map<String, Object> rules() {
+ return Collections.singletonMap("expression", expression);
+ }
+
+ @Override
+ public void validate() throws IllegalArgumentException {
+ super.validate();
+ validateSource(expression, "row-filter expression");
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof RowFilterContent)) {
+ return false;
+ }
+ RowFilterContent that = (RowFilterContent) other;
+ return Objects.equals(expression, that.expression);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(expression);
+ }
+
+ @Override
+ public String toString() {
+ return "RowFilterContent{" + "expression='" + expression + '\'' + '}';
+ }
+}
diff --git
a/api/src/test/java/org/apache/gravitino/policy/TestPolicyBuiltInType.java
b/api/src/test/java/org/apache/gravitino/policy/TestPolicyBuiltInType.java
index 68d2065cf5..9e49f61326 100644
--- a/api/src/test/java/org/apache/gravitino/policy/TestPolicyBuiltInType.java
+++ b/api/src/test/java/org/apache/gravitino/policy/TestPolicyBuiltInType.java
@@ -29,6 +29,10 @@ public class TestPolicyBuiltInType {
Assertions.assertEquals(
Policy.BuiltInType.ICEBERG_COMPACTION,
Policy.BuiltInType.fromPolicyType("system_iceberg_compaction"));
+ Assertions.assertEquals(
+ Policy.BuiltInType.ROW_FILTER,
Policy.BuiltInType.fromPolicyType("system_row_filter"));
+ Assertions.assertEquals(
+ Policy.BuiltInType.COLUMN_MASK,
Policy.BuiltInType.fromPolicyType("system_column_mask"));
}
@Test
@@ -40,15 +44,26 @@ public class TestPolicyBuiltInType {
Assertions.assertTrue(exception.getMessage().contains("Unknown policy
type"));
}
+ @Test
+ void testSingleReadRestrictionTypeIsUnsupported() {
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> Policy.BuiltInType.fromPolicyType("system_read_restriction"));
+ }
+
@Test
void testBuiltInTypePolicyTypeValue() {
Assertions.assertEquals(
"system_iceberg_compaction",
Policy.BuiltInType.ICEBERG_COMPACTION.policyType());
+ Assertions.assertEquals("system_row_filter",
Policy.BuiltInType.ROW_FILTER.policyType());
+ Assertions.assertEquals("system_column_mask",
Policy.BuiltInType.COLUMN_MASK.policyType());
}
@Test
void testBuiltInTypeContentClass() {
Assertions.assertEquals(
IcebergDataCompactionContent.class,
Policy.BuiltInType.ICEBERG_COMPACTION.contentClass());
+ Assertions.assertEquals(RowFilterContent.class,
Policy.BuiltInType.ROW_FILTER.contentClass());
+ Assertions.assertEquals(ColumnMaskContent.class,
Policy.BuiltInType.COLUMN_MASK.contentClass());
}
}
diff --git
a/api/src/test/java/org/apache/gravitino/policy/TestReadRestrictionContent.java
b/api/src/test/java/org/apache/gravitino/policy/TestReadRestrictionContent.java
new file mode 100644
index 0000000000..4939291249
--- /dev/null
+++
b/api/src/test/java/org/apache/gravitino/policy/TestReadRestrictionContent.java
@@ -0,0 +1,89 @@
+/*
+ * 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.policy;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Map;
+import org.apache.gravitino.MetadataObject;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestReadRestrictionContent {
+
+ @Test
+ void testCreatesReadRestrictionContent() {
+ String rowFilterExpression =
+ "filter := col(\"region\") == \"US\" if is_group_member(\"analysts\")
else := false";
+ String columnMaskExpression =
+ "mask := action(\"show-last-4\") if is_group_member(\"analysts\") "
+ + "else := action(\"replace-with-null\")";
+
+ RowFilterContent rowFilter = PolicyContents.rowFilter(rowFilterExpression);
+ ColumnMaskContent columnMask =
PolicyContents.columnMask(columnMaskExpression);
+
+ Assertions.assertEquals(rowFilterExpression, rowFilter.expression());
+ Assertions.assertEquals(columnMaskExpression, columnMask.expression());
+ Assertions.assertEquals(Map.of("expression", rowFilterExpression),
rowFilter.rules());
+ Assertions.assertEquals(Map.of("expression", columnMaskExpression),
columnMask.rules());
+ Assertions.assertEquals(
+ ImmutableSet.of(MetadataObject.Type.TABLE),
rowFilter.supportedObjectTypes());
+ Assertions.assertEquals(
+ ImmutableSet.of(MetadataObject.Type.TABLE, MetadataObject.Type.COLUMN),
+ columnMask.supportedObjectTypes());
+ Assertions.assertTrue(rowFilter.properties().isEmpty());
+ Assertions.assertTrue(columnMask.properties().isEmpty());
+ Assertions.assertDoesNotThrow(rowFilter::validate);
+ Assertions.assertDoesNotThrow(columnMask::validate);
+ }
+
+ @Test
+ void testEqualityUsesPolicyTypeAndExpression() {
+ String expression = "filter := col(\"region\") == \"US\"";
+
+ Assertions.assertEquals(
+ PolicyContents.rowFilter(expression),
PolicyContents.rowFilter(expression));
+ Assertions.assertNotEquals(
+ PolicyContents.rowFilter(expression), PolicyContents.rowFilter("filter
:= true"));
+ Assertions.assertNotEquals(
+ PolicyContents.rowFilter(expression),
PolicyContents.columnMask(expression));
+ }
+
+ @Test
+ void testRejectsMissingOrBlankExpressions() {
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
PolicyContents.rowFilter(null));
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
PolicyContents.columnMask(null));
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
PolicyContents.rowFilter(" \t"));
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
PolicyContents.columnMask("\n"));
+ }
+
+ @Test
+ void testValidatesUtf8ExpressionLength() {
+ Assertions.assertEquals(16 * 1024,
ReadRestrictionContent.MAX_SOURCE_LENGTH_BYTES);
+
+ String maximumLength =
"é".repeat(ReadRestrictionContent.MAX_SOURCE_LENGTH_BYTES / 2);
+ String tooLong = maximumLength + "a";
+
+ Assertions.assertDoesNotThrow(() ->
PolicyContents.rowFilter(maximumLength));
+ Assertions.assertDoesNotThrow(() ->
PolicyContents.columnMask(maximumLength));
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
PolicyContents.rowFilter(tooLong));
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
PolicyContents.columnMask(tooLong));
+ }
+}
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 a0c2b71616..e3f0f77583 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
@@ -29,10 +29,12 @@ import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.policy.ColumnMaskContent;
import org.apache.gravitino.policy.IcebergDataCompactionContent;
import org.apache.gravitino.policy.IcebergOrphanFileRemovalContent;
import org.apache.gravitino.policy.PolicyContent;
import org.apache.gravitino.policy.PolicyContents;
+import org.apache.gravitino.policy.RowFilterContent;
/** Represents a Policy Content Data Transfer Object (DTO). */
public interface PolicyContentDTO extends PolicyContent {
@@ -273,4 +275,104 @@ public interface PolicyContentDTO extends PolicyContent {
return PolicyContents.icebergOrphanFileRemoval(olderThanDays(),
location(), dryRun());
}
}
+
+ /** Represents typed row-filter policy content. */
+ @EqualsAndHashCode
+ @ToString
+ @Builder(setterPrefix = "with")
+ @AllArgsConstructor(access = lombok.AccessLevel.PRIVATE)
+ class RowFilterContentDTO implements PolicyContentDTO {
+
+ @JsonProperty("expression")
+ @Nullable
+ private String expression;
+
+ // Default constructor for Jackson deserialization only.
+ private RowFilterContentDTO() {}
+
+ /**
+ * Returns the authored row-filter expression.
+ *
+ * @return row-filter expression
+ */
+ @Nullable
+ public String expression() {
+ return expression;
+ }
+
+ @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 RowFilterContent toDomainContent() {
+ return PolicyContents.rowFilter(expression);
+ }
+ }
+
+ /** Represents typed column-mask policy content. */
+ @EqualsAndHashCode
+ @ToString
+ @Builder(setterPrefix = "with")
+ @AllArgsConstructor(access = lombok.AccessLevel.PRIVATE)
+ class ColumnMaskContentDTO implements PolicyContentDTO {
+
+ @JsonProperty("expression")
+ @Nullable
+ private String expression;
+
+ // Default constructor for Jackson deserialization only.
+ private ColumnMaskContentDTO() {}
+
+ /**
+ * Returns the authored column-mask expression.
+ *
+ * @return column-mask expression
+ */
+ @Nullable
+ public String expression() {
+ return expression;
+ }
+
+ @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 ColumnMaskContent toDomainContent() {
+ return PolicyContents.columnMask(expression);
+ }
+ }
}
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 ce65479c25..2f75784868 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
@@ -60,7 +60,13 @@ public class PolicyDTO implements Policy {
name = "system_iceberg_compaction"),
@JsonSubTypes.Type(
value = PolicyContentDTO.IcebergOrphanFileRemovalContentDTO.class,
- name = "system_iceberg_orphan_file_removal")
+ name = "system_iceberg_orphan_file_removal"),
+ @JsonSubTypes.Type(
+ value = PolicyContentDTO.RowFilterContentDTO.class,
+ name = "system_row_filter"),
+ @JsonSubTypes.Type(
+ value = PolicyContentDTO.ColumnMaskContentDTO.class,
+ name = "system_column_mask")
})
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 9ec2e0ccf9..d5ef0430a7 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
@@ -64,7 +64,13 @@ public class PolicyCreateRequest implements RESTRequest {
name = "system_iceberg_compaction"),
@JsonSubTypes.Type(
value = PolicyContentDTO.IcebergOrphanFileRemovalContentDTO.class,
- name = "system_iceberg_orphan_file_removal")
+ name = "system_iceberg_orphan_file_removal"),
+ @JsonSubTypes.Type(
+ value = PolicyContentDTO.RowFilterContentDTO.class,
+ name = "system_row_filter"),
+ @JsonSubTypes.Type(
+ value = PolicyContentDTO.ColumnMaskContentDTO.class,
+ name = "system_column_mask")
})
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 4e362466be..28a37aa25f 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
@@ -145,7 +145,13 @@ public interface PolicyUpdateRequest extends RESTRequest {
name = "system_iceberg_compaction"),
@JsonSubTypes.Type(
value = PolicyContentDTO.IcebergOrphanFileRemovalContentDTO.class,
- name = "system_iceberg_orphan_file_removal")
+ name = "system_iceberg_orphan_file_removal"),
+ @JsonSubTypes.Type(
+ value = PolicyContentDTO.RowFilterContentDTO.class,
+ name = "system_row_filter"),
+ @JsonSubTypes.Type(
+ value = PolicyContentDTO.ColumnMaskContentDTO.class,
+ name = "system_column_mask")
})
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 14f6bc6d64..9851e8e4cd 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,10 +104,12 @@ 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.ColumnMaskContent;
import org.apache.gravitino.policy.IcebergDataCompactionContent;
import org.apache.gravitino.policy.IcebergOrphanFileRemovalContent;
import org.apache.gravitino.policy.PolicyContent;
import org.apache.gravitino.policy.PolicyContents;
+import org.apache.gravitino.policy.RowFilterContent;
import org.apache.gravitino.rel.Column;
import org.apache.gravitino.rel.Representation;
import org.apache.gravitino.rel.SQLRepresentation;
@@ -697,6 +699,20 @@ public class DTOConverters {
.build();
}
+ if (policyContent instanceof RowFilterContent) {
+ RowFilterContent content = (RowFilterContent) policyContent;
+ return PolicyContentDTO.RowFilterContentDTO.builder()
+ .withExpression(content.expression())
+ .build();
+ }
+
+ if (policyContent instanceof ColumnMaskContent) {
+ ColumnMaskContent content = (ColumnMaskContent) policyContent;
+ return PolicyContentDTO.ColumnMaskContentDTO.builder()
+ .withExpression(content.expression())
+ .build();
+ }
+
throw new IllegalArgumentException("Unsupported policy content: " +
policyContent);
}
@@ -1559,6 +1575,18 @@ public class DTOConverters {
icebergCompactionContentDTO.rewriteOptions());
}
+ if (policyContentDTO instanceof PolicyContentDTO.RowFilterContentDTO) {
+ PolicyContentDTO.RowFilterContentDTO contentDTO =
+ (PolicyContentDTO.RowFilterContentDTO) policyContentDTO;
+ return PolicyContents.rowFilter(contentDTO.expression());
+ }
+
+ if (policyContentDTO instanceof PolicyContentDTO.ColumnMaskContentDTO) {
+ PolicyContentDTO.ColumnMaskContentDTO contentDTO =
+ (PolicyContentDTO.ColumnMaskContentDTO) policyContentDTO;
+ return PolicyContents.columnMask(contentDTO.expression());
+ }
+
throw new IllegalArgumentException(
"Unsupported policy content type: " + policyContentDTO.getClass());
}
diff --git
a/common/src/test/java/org/apache/gravitino/dto/policy/TestReadRestrictionPolicyDTO.java
b/common/src/test/java/org/apache/gravitino/dto/policy/TestReadRestrictionPolicyDTO.java
new file mode 100644
index 0000000000..9c1dd80bde
--- /dev/null
+++
b/common/src/test/java/org/apache/gravitino/dto/policy/TestReadRestrictionPolicyDTO.java
@@ -0,0 +1,214 @@
+/*
+ * 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.dto.policy;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
+import org.apache.gravitino.dto.requests.PolicyCreateRequest;
+import org.apache.gravitino.dto.requests.PolicyUpdateRequest;
+import org.apache.gravitino.dto.util.DTOConverters;
+import org.apache.gravitino.json.JsonUtils;
+import org.apache.gravitino.policy.ColumnMaskContent;
+import org.apache.gravitino.policy.Policy;
+import org.apache.gravitino.policy.PolicyContent;
+import org.apache.gravitino.policy.PolicyContents;
+import org.apache.gravitino.policy.RowFilterContent;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestReadRestrictionPolicyDTO {
+
+ @Test
+ void testRowFilterPolicyRoundTrip() throws JsonProcessingException {
+ String expression =
+ "filter := col(\"region\") == \"US\" if is_group_member(\"auditors\") "
+ + "else := col(\"owner\") == session_user()";
+ RowFilterContent content = PolicyContents.rowFilter(expression);
+ PolicyDTO policy =
+ PolicyDTO.builder()
+ .withName("owner-filter")
+ .withPolicyType("system_row_filter")
+ .withEnabled(true)
+ .withContent(DTOConverters.toDTO(content))
+ .build();
+
+ String json = JsonUtils.objectMapper().writeValueAsString(policy);
+ PolicyDTO restored = JsonUtils.objectMapper().readValue(json,
PolicyDTO.class);
+
+ Assertions.assertEquals(policy, restored);
+ Assertions.assertInstanceOf(PolicyContentDTO.RowFilterContentDTO.class,
restored.content());
+ Assertions.assertTrue(json.contains("\"expression\":\"filter :="));
+ Assertions.assertFalse(json.contains("\"rules\""));
+ Assertions.assertEquals(content,
DTOConverters.fromDTO(restored.content()));
+ Assertions.assertDoesNotThrow(restored.content()::validate);
+ }
+
+ @Test
+ void testRowFilterCreateRequestRoundTrip() throws JsonProcessingException {
+ String json =
+ "{"
+ + "\"name\":\"region-filter\","
+ + "\"policyType\":\"system_row_filter\","
+ + "\"enabled\":true,"
+ + "\"content\":{\"expression\":"
+ + "\"filter := col(\\\"region\\\") == \\\"US\\\" if "
+ + "is_group_member(\\\"auditors\\\") else := false\"}}";
+
+ PolicyCreateRequest request =
+ JsonUtils.objectMapper().readValue(json, PolicyCreateRequest.class);
+ Assertions.assertInstanceOf(
+ PolicyContentDTO.RowFilterContentDTO.class,
request.getPolicyContent());
+ Assertions.assertDoesNotThrow(request::validate);
+
+ RowFilterContent content = (RowFilterContent)
DTOConverters.fromDTO(request.getPolicyContent());
+ Assertions.assertEquals(
+ "filter := col(\"region\") == \"US\" if is_group_member(\"auditors\")
else := false",
+ content.expression());
+ }
+
+ @Test
+ void testColumnMaskCreateRequestRoundTrip() throws JsonProcessingException {
+ String json =
+ "{"
+ + "\"name\":\"pii-mask\","
+ + "\"policyType\":\"system_column_mask\","
+ + "\"enabled\":true,"
+ + "\"content\":{\"expression\":"
+ + "\"mask := action(\\\"show-last-4\\\") if "
+ + "is_group_member(\\\"pii_unmasked\\\") "
+ + "else := action(\\\"mask-alphanum\\\")\"}}";
+
+ PolicyCreateRequest request =
+ JsonUtils.objectMapper().readValue(json, PolicyCreateRequest.class);
+ Assertions.assertInstanceOf(
+ PolicyContentDTO.ColumnMaskContentDTO.class,
request.getPolicyContent());
+ Assertions.assertDoesNotThrow(request::validate);
+
+ ColumnMaskContent content =
+ (ColumnMaskContent) DTOConverters.fromDTO(request.getPolicyContent());
+ Assertions.assertEquals(
+ "mask := action(\"show-last-4\") if is_group_member(\"pii_unmasked\") "
+ + "else := action(\"mask-alphanum\")",
+ content.expression());
+ }
+
+ @Test
+ void testUpdateRequestsUseEachPolicyType() throws JsonProcessingException {
+ assertUpdateRoundTrip(
+ "system_row_filter",
+ PolicyContents.rowFilter(
+ "filter := col(\"region\") == \"US\" if session_user() ==
\"alice\" "
+ + "else := col(\"owner\") == session_user()"));
+ assertUpdateRoundTrip(
+ "system_column_mask",
+ PolicyContents.columnMask(
+ "mask := action(\"show-last-4\") if session_user() == \"alice\" "
+ + "else := action(\"replace-with-null\")"));
+ }
+
+ @Test
+ void testStoredContentRoundTripUsesEachBuiltInContentClass() throws
JsonProcessingException {
+ RowFilterContent rowFilter =
+ PolicyContents.rowFilter(
+ "filter := col(\"region\") == \"US\" if
is_group_member(\"analysts\") "
+ + "else := col(\"owner\") == session_user()");
+ ColumnMaskContent columnMask =
+ PolicyContents.columnMask(
+ "mask := action(\"show-last-4\") if is_group_member(\"analysts\") "
+ + "else := action(\"replace-with-null\")");
+
+ PolicyContent restoredRowFilter =
+ JsonUtils.anyFieldMapper()
+ .readValue(
+ JsonUtils.anyFieldMapper().writeValueAsString(rowFilter),
+ Policy.BuiltInType.ROW_FILTER.contentClass());
+ PolicyContent restoredColumnMask =
+ JsonUtils.anyFieldMapper()
+ .readValue(
+ JsonUtils.anyFieldMapper().writeValueAsString(columnMask),
+ Policy.BuiltInType.COLUMN_MASK.contentClass());
+
+ Assertions.assertEquals(rowFilter, restoredRowFilter);
+ Assertions.assertEquals(columnMask, restoredColumnMask);
+ Assertions.assertDoesNotThrow(restoredRowFilter::validate);
+ Assertions.assertDoesNotThrow(restoredColumnMask::validate);
+ }
+
+ @Test
+ void testRejectsLegacyRuleShape() {
+ String rowFilterWithRules =
+ "{"
+ + "\"name\":\"invalid-filter\","
+ + "\"policyType\":\"system_row_filter\","
+ + "\"enabled\":true,"
+ + "\"content\":{\"rules\":[{\"expression\":\"filter := true\"}]}"
+ + "}";
+ String columnMaskWithRules =
+ "{"
+ + "\"name\":\"invalid-mask\","
+ + "\"policyType\":\"system_column_mask\","
+ + "\"enabled\":true,"
+ + "\"content\":{\"rules\":[{\"action\":\"show-first-4\"}]}"
+ + "}";
+
+ assertLegacyRuleShapeRejected(rowFilterWithRules);
+ assertLegacyRuleShapeRejected(columnMaskWithRules);
+ }
+
+ @Test
+ void testRejectsMissingOrBlankExpressions() {
+ PolicyContentDTO.RowFilterContentDTO missingRowFilter =
+ PolicyContentDTO.RowFilterContentDTO.builder().build();
+ PolicyContentDTO.ColumnMaskContentDTO missingColumnMask =
+ PolicyContentDTO.ColumnMaskContentDTO.builder().build();
+ PolicyContentDTO.RowFilterContentDTO blankRowFilter =
+ PolicyContentDTO.RowFilterContentDTO.builder().withExpression("
").build();
+ PolicyContentDTO.ColumnMaskContentDTO blankColumnMask =
+
PolicyContentDTO.ColumnMaskContentDTO.builder().withExpression("\n").build();
+
+ Assertions.assertThrows(IllegalArgumentException.class,
missingRowFilter::validate);
+ Assertions.assertThrows(IllegalArgumentException.class,
missingColumnMask::validate);
+ Assertions.assertThrows(IllegalArgumentException.class,
blankRowFilter::validate);
+ Assertions.assertThrows(IllegalArgumentException.class,
blankColumnMask::validate);
+ }
+
+ private static void assertUpdateRoundTrip(String policyType, PolicyContent
content)
+ throws JsonProcessingException {
+ PolicyUpdateRequest.UpdatePolicyContentRequest request =
+ new PolicyUpdateRequest.UpdatePolicyContentRequest(
+ policyType, DTOConverters.toDTO(content));
+ String json = JsonUtils.objectMapper().writeValueAsString(request);
+ PolicyUpdateRequest.UpdatePolicyContentRequest restored =
+ JsonUtils.objectMapper()
+ .readValue(json,
PolicyUpdateRequest.UpdatePolicyContentRequest.class);
+
+ Assertions.assertEquals(request, restored);
+ Assertions.assertEquals(policyType, restored.getPolicyType());
+ Assertions.assertEquals(content,
DTOConverters.fromDTO(restored.getNewContent()));
+ }
+
+ private static void assertLegacyRuleShapeRejected(String json) {
+ UnrecognizedPropertyException exception =
+ Assertions.assertThrows(
+ UnrecognizedPropertyException.class,
+ () -> JsonUtils.objectMapper().readValue(json,
PolicyCreateRequest.class));
+ Assertions.assertEquals("rules", exception.getPropertyName());
+ Assertions.assertTrue(exception.getMessage().contains("rules"));
+ }
+}
diff --git a/core/src/test/java/org/apache/gravitino/meta/TestPolicyEntity.java
b/core/src/test/java/org/apache/gravitino/meta/TestPolicyEntity.java
index 5152d14429..3ee2fa2ed2 100644
--- a/core/src/test/java/org/apache/gravitino/meta/TestPolicyEntity.java
+++ b/core/src/test/java/org/apache/gravitino/meta/TestPolicyEntity.java
@@ -155,4 +155,35 @@ public class TestPolicyEntity {
.withAuditInfo(auditInfo)
.build());
}
+
+ @Test
+ public void testReadRestrictionPolicyContentTypes() {
+ PolicyContent rowFilter = PolicyContents.rowFilter("filter := true");
+ PolicyContent columnMask = PolicyContents.columnMask("mask :=
action(\"replace-with-null\")");
+
+ Assertions.assertDoesNotThrow(
+ () -> readRestrictionPolicy(Policy.BuiltInType.ROW_FILTER, rowFilter));
+ Assertions.assertDoesNotThrow(
+ () -> readRestrictionPolicy(Policy.BuiltInType.COLUMN_MASK,
columnMask));
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> readRestrictionPolicy(Policy.BuiltInType.ROW_FILTER,
columnMask));
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> readRestrictionPolicy(Policy.BuiltInType.COLUMN_MASK,
rowFilter));
+ }
+
+ private static PolicyEntity readRestrictionPolicy(
+ Policy.BuiltInType type, PolicyContent content) {
+ return PolicyEntity.builder()
+ .withId(1L)
+ .withName("read-restriction")
+ .withNamespace(Namespace.of("m1", "c1", "s1"))
+ .withPolicyType(type)
+ .withEnabled(true)
+ .withContent(content)
+ .withAuditInfo(
+
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build())
+ .build();
+ }
}
diff --git
a/core/src/test/java/org/apache/gravitino/policy/TestPolicyManager.java
b/core/src/test/java/org/apache/gravitino/policy/TestPolicyManager.java
index 3ec76afae6..dedb7e164e 100644
--- a/core/src/test/java/org/apache/gravitino/policy/TestPolicyManager.java
+++ b/core/src/test/java/org/apache/gravitino/policy/TestPolicyManager.java
@@ -460,6 +460,31 @@ public class TestPolicyManager {
"format args were not substituted: " + removeEx.getMessage());
}
+ @Test
+ public void testCreateUpdateAndGetRowFilterPolicy() {
+ String policyName = "row_filter_" +
UUID.randomUUID().toString().replace("-", "");
+ PolicyContent originalContent = PolicyContents.rowFilter("filter :=
col(\"region\") == \"US\"");
+
+ PolicyEntity created =
+ policyManager.createPolicy(
+ METALAKE, policyName, Policy.BuiltInType.ROW_FILTER, null, true,
originalContent);
+
+ Assertions.assertEquals(originalContent, created.content());
+ Assertions.assertEquals(
+ ImmutableSet.of(MetadataObject.Type.TABLE),
created.content().supportedObjectTypes());
+ Assertions.assertEquals(
+ originalContent, policyManager.getPolicy(METALAKE,
policyName).content());
+
+ PolicyContent updatedContent = PolicyContents.rowFilter("filter :=
col(\"region\") == \"EU\"");
+ PolicyEntity updated =
+ policyManager.alterPolicy(
+ METALAKE, policyName,
PolicyChange.updateContent("system_row_filter", updatedContent));
+
+ Assertions.assertEquals(updatedContent, updated.content());
+ Assertions.assertEquals(
+ updatedContent, policyManager.getPolicy(METALAKE,
policyName).content());
+ }
+
@Test
public void testDeletePolicy() {
String policyName = "policy1" + UUID.randomUUID().toString().replace("-",
"");
diff --git a/docs/open-api/policies.yaml b/docs/open-api/policies.yaml
index 2e0d953020..ce5adcac28 100644
--- a/docs/open-api/policies.yaml
+++ b/docs/open-api/policies.yaml
@@ -439,6 +439,78 @@ components:
content:
$ref: '#/components/schemas/IcebergOrphanFileRemovalContent'
+ RowFilterContent:
+ type: object
+ description: Expression content for a built-in row-filter policy.
+ required:
+ - expression
+ properties:
+ expression:
+ type: string
+ minLength: 1
+ pattern: '[\s\S]*\S[\s\S]*'
+ description: A nonblank expression whose UTF-8 encoding must not
exceed 16 KiB.
+
+ ColumnMaskContent:
+ type: object
+ description: Expression content for a built-in column-mask policy.
+ required:
+ - expression
+ properties:
+ expression:
+ type: string
+ minLength: 1
+ pattern: '[\s\S]*\S[\s\S]*'
+ description: A nonblank expression whose UTF-8 encoding must not
exceed 16 KiB.
+
+ RowFilterPolicy:
+ allOf:
+ - $ref: '#/components/schemas/PolicyBase'
+ - type: object
+ required: [content]
+ properties:
+ policyType:
+ type: string
+ enum: [system_row_filter]
+ content:
+ $ref: '#/components/schemas/RowFilterContent'
+
+ RowFilterPolicyCreateRequest:
+ allOf:
+ - $ref: '#/components/schemas/PolicyCreateRequestBase'
+ - type: object
+ required: [content]
+ properties:
+ policyType:
+ type: string
+ enum: [system_row_filter]
+ content:
+ $ref: '#/components/schemas/RowFilterContent'
+
+ ColumnMaskPolicy:
+ allOf:
+ - $ref: '#/components/schemas/PolicyBase'
+ - type: object
+ required: [content]
+ properties:
+ policyType:
+ type: string
+ enum: [system_column_mask]
+ content:
+ $ref: '#/components/schemas/ColumnMaskContent'
+
+ ColumnMaskPolicyCreateRequest:
+ allOf:
+ - $ref: '#/components/schemas/PolicyCreateRequestBase'
+ - type: object
+ required: [content]
+ properties:
+ policyType:
+ type: string
+ enum: [system_column_mask]
+ content:
+ $ref: '#/components/schemas/ColumnMaskContent'
+
PolicyContentBase:
type: object
description: A base content structure for policies
@@ -514,6 +586,10 @@ components:
required:
- content
properties:
+ policyType:
+ type: string
+ not:
+ enum: [system_iceberg_compaction,
system_iceberg_orphan_file_removal, system_row_filter, system_column_mask]
content:
$ref: '#/components/schemas/CustomPolicyContent'
@@ -524,12 +600,16 @@ components:
- $ref: '#/components/schemas/CustomPolicy'
- $ref: '#/components/schemas/IcebergOrphanFileRemovalPolicy'
- $ref: '#/components/schemas/IcebergCompactionPolicy'
+ - $ref: '#/components/schemas/RowFilterPolicy'
+ - $ref: '#/components/schemas/ColumnMaskPolicy'
discriminator:
propertyName: policyType
mapping:
custom: '#/components/schemas/CustomPolicy'
system_iceberg_orphan_file_removal:
'#/components/schemas/IcebergOrphanFileRemovalPolicy'
system_iceberg_compaction:
'#/components/schemas/IcebergCompactionPolicy'
+ system_row_filter: '#/components/schemas/RowFilterPolicy'
+ system_column_mask: '#/components/schemas/ColumnMaskPolicy'
PolicyListResponse:
type: object
@@ -597,6 +677,10 @@ components:
required:
- content
properties:
+ policyType:
+ type: string
+ not:
+ enum: [system_iceberg_compaction,
system_iceberg_orphan_file_removal, system_row_filter, system_column_mask]
content:
$ref: '#/components/schemas/CustomPolicyContent'
@@ -607,12 +691,16 @@ components:
- $ref: '#/components/schemas/CustomPolicyCreateRequest'
- $ref:
'#/components/schemas/IcebergOrphanFileRemovalPolicyCreateRequest'
- $ref: '#/components/schemas/IcebergCompactionPolicyCreateRequest'
+ - $ref: '#/components/schemas/RowFilterPolicyCreateRequest'
+ - $ref: '#/components/schemas/ColumnMaskPolicyCreateRequest'
discriminator:
propertyName: policyType
mapping:
custom: '#/components/schemas/CustomPolicyCreateRequest'
system_iceberg_orphan_file_removal:
'#/components/schemas/IcebergOrphanFileRemovalPolicyCreateRequest'
system_iceberg_compaction:
'#/components/schemas/IcebergCompactionPolicyCreateRequest'
+ system_row_filter:
'#/components/schemas/RowFilterPolicyCreateRequest'
+ system_column_mask:
'#/components/schemas/ColumnMaskPolicyCreateRequest'
PolicyUpdatesRequest:
@@ -702,7 +790,7 @@ components:
policyType:
type: string
not:
- enum: [system_iceberg_compaction,
system_iceberg_orphan_file_removal]
+ enum: [system_iceberg_compaction,
system_iceberg_orphan_file_removal, system_row_filter, system_column_mask]
newContent:
$ref: '#/components/schemas/CustomPolicyContent'
@@ -728,18 +816,44 @@ components:
newContent:
$ref: '#/components/schemas/IcebergOrphanFileRemovalContent'
+ UpdateRowFilterPolicyContentRequest:
+ allOf:
+ - $ref: '#/components/schemas/UpdatePolicyContentRequestBase'
+ - type: object
+ properties:
+ policyType:
+ type: string
+ enum: [system_row_filter]
+ newContent:
+ $ref: '#/components/schemas/RowFilterContent'
+
+ UpdateColumnMaskPolicyContentRequest:
+ allOf:
+ - $ref: '#/components/schemas/UpdatePolicyContentRequestBase'
+ - type: object
+ properties:
+ policyType:
+ type: string
+ enum: [system_column_mask]
+ newContent:
+ $ref: '#/components/schemas/ColumnMaskContent'
+
UpdatePolicyContentRequest:
description: Selects typed content using the sibling policyType field,
matching REST deserialization.
oneOf:
- $ref: '#/components/schemas/UpdateCustomPolicyContentRequest'
- $ref:
'#/components/schemas/UpdateIcebergCompactionPolicyContentRequest'
- $ref:
'#/components/schemas/UpdateIcebergOrphanFileRemovalPolicyContentRequest'
+ - $ref: '#/components/schemas/UpdateRowFilterPolicyContentRequest'
+ - $ref: '#/components/schemas/UpdateColumnMaskPolicyContentRequest'
discriminator:
propertyName: policyType
mapping:
custom: '#/components/schemas/UpdateCustomPolicyContentRequest'
system_iceberg_compaction:
'#/components/schemas/UpdateIcebergCompactionPolicyContentRequest'
system_iceberg_orphan_file_removal:
'#/components/schemas/UpdateIcebergOrphanFileRemovalPolicyContentRequest'
+ system_row_filter:
'#/components/schemas/UpdateRowFilterPolicyContentRequest'
+ system_column_mask:
'#/components/schemas/UpdateColumnMaskPolicyContentRequest'
example: {
"@type": "updateContent",
"policyType": "custom",
diff --git a/docs/policies.md b/docs/policies.md
index fa87ebfaed..f39c8b2578 100644
--- a/docs/policies.md
+++ b/docs/policies.md
@@ -35,10 +35,17 @@ policy. A table with only `data_domain=risk` does not.
### Policy Types and Content
-| Type | Rules |
Consumer |
-|-----------------------------|--------------------------------------|---------------------------|
-| `system_iceberg_compaction` | Compaction thresholds and scheduling | Table
maintenance service |
-| `custom` | A free-form map that you define | A
system that you provide |
+| Type | Rules
| Consumer |
+|--------------------------------------|--------------------------------------|---------------------------|
+| `system_iceberg_compaction` | Compaction thresholds and scheduling
| Table maintenance service |
+| `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 |
+| `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.
A custom policy's rules live in `customRules`. Gravitino stores them and
returns them to clients;
it does not interpret their names or values. Built-in types have a defined
content shape. See