Skip to content

Commit decd92b

Browse files
committed
Replace context
1 parent 3beebab commit decd92b

File tree

6 files changed

+288
-6
lines changed

6 files changed

+288
-6
lines changed

polaris-core/src/main/java/org/apache/polaris/core/policy/content/AccessControlPolicyContent.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919

2020
package org.apache.polaris.core.policy.content;
2121

22+
import com.fasterxml.jackson.core.JsonProcessingException;
2223
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
24+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
2325
import com.google.common.base.Strings;
2426
import java.util.List;
2527
import java.util.Set;
@@ -37,6 +39,7 @@ public class AccessControlPolicyContent implements PolicyContent {
3739
// Iceberg expressions without context functions for now.
3840
// Use a custom deserializer for the list of Iceberg Expressions
3941
@JsonDeserialize(using = IcebergExpressionListDeserializer.class)
42+
@JsonSerialize(using = IcebergExpressionListSerializer.class)
4043
private List<Expression> rowFilters;
4144

4245
private static final String DEFAULT_POLICY_SCHEMA_VERSION = "2025-02-03";
@@ -64,6 +67,21 @@ public static AccessControlPolicyContent fromString(String content) {
6467
return policy;
6568
}
6669

70+
public static String toString(AccessControlPolicyContent content) {
71+
if (content == null) {
72+
// Return null or an empty string, or throw an IllegalArgumentException
73+
// based on how you want to handle null input.
74+
return null;
75+
}
76+
try {
77+
// PolicyContentUtil.MAPPER is assumed to be an ObjectMapper instance
78+
// that is properly configured (e.g., with your custom serializers).
79+
return PolicyContentUtil.MAPPER.writeValueAsString(content);
80+
} catch (JsonProcessingException e) {
81+
throw new InvalidPolicyException("Failed to convert policy content to JSON string", e);
82+
}
83+
}
84+
6785
// Constructors, getters, and setters
6886
public AccessControlPolicyContent() {}
6987

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.polaris.core.policy.content;
21+
22+
import com.fasterxml.jackson.core.JsonGenerator;
23+
import com.fasterxml.jackson.databind.JsonSerializer;
24+
import com.fasterxml.jackson.databind.SerializerProvider;
25+
import java.io.IOException;
26+
import java.util.List;
27+
import org.apache.iceberg.expressions.Expression;
28+
import org.apache.iceberg.expressions.ExpressionParser;
29+
30+
/**
31+
* Custom Jackson JsonSerializer for a List of Iceberg Expression objects. This serializer converts
32+
* each Iceberg Expression into its string representation.
33+
*/
34+
public class IcebergExpressionListSerializer extends JsonSerializer<List<Expression>> {
35+
36+
@Override
37+
public void serialize(
38+
List<Expression> expressions,
39+
JsonGenerator jsonGenerator,
40+
SerializerProvider serializerProvider)
41+
throws IOException {
42+
jsonGenerator.writeStartArray();
43+
if (expressions != null) {
44+
for (Expression expression : expressions) {
45+
jsonGenerator.writeString(ExpressionParser.toJson(expression));
46+
}
47+
}
48+
jsonGenerator.writeEndArray();
49+
}
50+
}

polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidators.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ public static boolean canAttach(PolicyEntity policy, PolarisEntity targetEntity)
102102
case ORPHAN_FILE_REMOVAL:
103103
return BaseMaintenancePolicyValidator.INSTANCE.canAttach(entityType, entitySubType);
104104

105+
case ACCESS_CONTROL:
106+
// TODO: Add validator for attaching this only to table
107+
return true;
108+
105109
default:
106110
LOGGER.warn("Attachment not supported for policy type: {}", policyType.getName());
107111
return false;

polaris-core/src/test/java/org/apache/polaris/core/policy/AccessControlPolicyContentTest.java

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@
2525
import static org.junit.jupiter.api.Assertions.assertTrue;
2626

2727
import java.util.Arrays;
28+
import java.util.List;
2829
import org.apache.iceberg.expressions.Expression;
30+
import org.apache.iceberg.expressions.Expressions;
2931
import org.apache.polaris.core.policy.content.AccessControlPolicyContent;
3032
import org.apache.polaris.core.policy.validator.InvalidPolicyException;
33+
import org.junit.jupiter.api.Assertions;
3134
import org.junit.jupiter.api.DisplayName;
3235
import org.junit.jupiter.api.Test;
3336

@@ -69,6 +72,44 @@ void testFromString_fullPolicy() {
6972
assertEquals("ref(name=\"name\") == \"PK\"", filter2.toString());
7073
}
7174

75+
@Test
76+
@DisplayName(
77+
"Should deserialize a full policy with all fields correctly - with context variables")
78+
void testFromString_fullPolicy_withContextVariable() {
79+
String jsonContent =
80+
"{\n"
81+
+ " \"principalRole\": \"ANALYST\",\n"
82+
+ " \"columnProjections\": [\"name\", \"location\"],\n"
83+
+ " \"rowFilters\": [\n"
84+
+ " {\n"
85+
+ " \"type\": \"eq\",\n"
86+
+ " \"term\": \"country\",\n"
87+
+ " \"value\": \"USA\"\n"
88+
+ " },\n"
89+
+ " {\n"
90+
+ " \"type\": \"eq\",\n"
91+
+ " \"term\": \"$current_principal_role\",\n"
92+
+ " \"value\": \"PK\"\n"
93+
+ " }\n"
94+
+ " ]\n"
95+
+ "}";
96+
97+
AccessControlPolicyContent policy = AccessControlPolicyContent.fromString(jsonContent);
98+
99+
assertNotNull(policy);
100+
assertEquals("ANALYST", policy.getPrincipalRole());
101+
assertEquals(Arrays.asList("name", "location"), policy.getColumnProjections());
102+
103+
// Validate rowFilters
104+
assertNotNull(policy.getRowFilters());
105+
assertEquals(2, policy.getRowFilters().size());
106+
107+
Expression filter1 = policy.getRowFilters().get(0);
108+
Expression filter2 = policy.getRowFilters().get(1);
109+
assertEquals("ref(name=\"country\") == \"USA\"", filter1.toString());
110+
assertEquals("ref(name=\"$current_principal_role\") == \"PK\"", filter2.toString());
111+
}
112+
72113
@Test
73114
@DisplayName(
74115
"Should fail deserialize policy with only required fields (principalRole empty, lists empty/null)")
@@ -189,6 +230,64 @@ void testFromString_emptyListsInJson() {
189230
});
190231
}
191232

233+
@Test
234+
void testToString_basicPolicy() {
235+
AccessControlPolicyContent policy = new AccessControlPolicyContent();
236+
policy.setPrincipalRole("analyst");
237+
policy.setAllowedColumns(Arrays.asList("col1", "col2"));
238+
policy.setRowFilters(null);
239+
240+
String expectedJson =
241+
"{\"principalRole\":\"analyst\",\"columnProjections\":[\"col1\",\"col2\"],\"rowFilters\":null}";
242+
String actualJson = AccessControlPolicyContent.toString(policy);
243+
244+
assertEquals(expectedJson, actualJson);
245+
}
246+
247+
@Test
248+
void testToString_policyWithRowFilters() {
249+
AccessControlPolicyContent policy = new AccessControlPolicyContent();
250+
policy.setPrincipalRole("data_engineer");
251+
policy.setAllowedColumns(List.of());
252+
253+
Expression filter1 = Expressions.equal("name", "Alice");
254+
Expression filter2 = Expressions.greaterThan("age", 30);
255+
policy.setRowFilters(Arrays.asList(filter1, filter2));
256+
257+
String expectedJson =
258+
"{\"principalRole\":\"data_engineer\",\"columnProjections\":[],\"rowFilters\":[\"{\\\"type\\\":\\\"eq\\\",\\\"term\\\":\\\"name\\\",\\\"value\\\":\\\"Alice\\\"}\",\"{\\\"type\\\":\\\"gt\\\",\\\"term\\\":\\\"age\\\",\\\"value\\\":30}\"]}";
259+
260+
String actualJson = AccessControlPolicyContent.toString(policy);
261+
262+
assertEquals(expectedJson, actualJson);
263+
}
264+
265+
@Test
266+
void testToString_emptyPolicy() {
267+
AccessControlPolicyContent policy = new AccessControlPolicyContent();
268+
policy.setPrincipalRole(null);
269+
policy.setAllowedColumns(List.of());
270+
policy.setRowFilters(List.of());
271+
272+
String expectedJson = "{\"principalRole\":null,\"columnProjections\":[],\"rowFilters\":[]}";
273+
String actualJson = AccessControlPolicyContent.toString(policy);
274+
275+
assertEquals(expectedJson, actualJson);
276+
}
277+
278+
@Test
279+
void testToString_nullInput() {
280+
String actualJson = AccessControlPolicyContent.toString(null);
281+
Assertions.assertNull(actualJson);
282+
}
283+
284+
@Test
285+
void testToString_exceptionHandling() {
286+
AccessControlPolicyContent policy = new AccessControlPolicyContent();
287+
policy.setAllowedColumns(Arrays.asList("id"));
288+
Assertions.assertDoesNotThrow(() -> AccessControlPolicyContent.toString(policy));
289+
}
290+
192291
@Test
193292
@DisplayName(
194293
"Should handle unmapped properties if FAIL_ON_UNKNOWN_PROPERTIES is false (default for this setup)")

runtime/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogTest.java

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package org.apache.polaris.service.quarkus.catalog;
2020

2121
import static org.apache.iceberg.types.Types.NestedField.required;
22+
import static org.apache.polaris.core.policy.PredefinedPolicyTypes.ACCESS_CONTROL;
2223
import static org.apache.polaris.core.policy.PredefinedPolicyTypes.DATA_COMPACTION;
2324
import static org.apache.polaris.core.policy.PredefinedPolicyTypes.METADATA_COMPACTION;
2425
import static org.apache.polaris.core.policy.PredefinedPolicyTypes.ORPHAN_FILE_REMOVAL;
@@ -137,6 +138,24 @@ public Map<String, String> getConfigOverrides() {
137138
required(3, "id", Types.IntegerType.get(), "unique ID"),
138139
required(4, "data", Types.StringType.get()));
139140

141+
private static final String EXAMPLE_ACCESS_CONTROL_POLICY_CONTENT =
142+
"{\n"
143+
+ " \"principalRole\": \"ANALYST\",\n"
144+
+ " \"columnProjections\": [\"name\", \"location\"],\n"
145+
+ " \"rowFilters\": [\n"
146+
+ " {\n"
147+
+ " \"type\": \"eq\",\n"
148+
+ " \"term\": \"country\",\n"
149+
+ " \"value\": \"USA\"\n"
150+
+ " },\n"
151+
+ " {\n"
152+
+ " \"type\": \"eq\",\n"
153+
+ " \"term\": \"$current_principal\",\n"
154+
+ " \"value\": \"PK\"\n"
155+
+ " }\n"
156+
+ " ]\n"
157+
+ "}";
158+
140159
private static final PolicyIdentifier POLICY1 = new PolicyIdentifier(NS, "p1");
141160
private static final PolicyIdentifier POLICY2 = new PolicyIdentifier(NS, "p2");
142161
private static final PolicyIdentifier POLICY3 = new PolicyIdentifier(NS, "p3");
@@ -630,16 +649,47 @@ public void testGetApplicablePoliciesFilterOnType() {
630649
assertThat(applicablePolicies.contains(policyToApplicablePolicy(p2, false, NS))).isTrue();
631650
}
632651

652+
@Test
653+
public void testAttachAccessControlPolicyToTableAndCheckApplicablePolicy() {
654+
icebergCatalog.createNamespace(NS);
655+
icebergCatalog.createTable(TABLE, SCHEMA);
656+
policyCatalog.createPolicy(
657+
POLICY1, METADATA_COMPACTION.getName(), "test", "{\"enable\": false}");
658+
var p2 =
659+
policyCatalog.createPolicy(
660+
POLICY2, ACCESS_CONTROL.getName(), "FGAC", EXAMPLE_ACCESS_CONTROL_POLICY_CONTENT);
661+
662+
// attach a policy to table
663+
var target =
664+
new PolicyAttachmentTarget(
665+
PolicyAttachmentTarget.TypeEnum.TABLE_LIKE, List.of(NS.toString(), TABLE.name()));
666+
policyCatalog.attachPolicy(POLICY2, target, null);
667+
668+
// attach a different type of policy to namespace
669+
policyCatalog.attachPolicy(POLICY1, POLICY_ATTACH_TARGET_NS, null);
670+
var applicablePolicies = policyCatalog.getApplicablePolicies(NS, TABLE.name(), ACCESS_CONTROL);
671+
System.out.println(applicablePolicies);
672+
// only p2 is with the type fetched
673+
assertThat(applicablePolicies.contains(policyToApplicablePolicy(p2, false, NS))).isTrue();
674+
}
675+
633676
private static ApplicablePolicy policyToApplicablePolicy(
634677
Policy policy, boolean inherited, Namespace parent) {
635678
return new ApplicablePolicy(
636679
policy.getPolicyType(),
637680
policy.getInheritable(),
638681
policy.getName(),
639682
policy.getDescription(),
640-
policy.getContent(),
683+
replaceContextVariable(policy.getContent(), policy.getPolicyType()),
641684
policy.getVersion(),
642685
inherited,
643686
Arrays.asList(parent.levels()));
644687
}
688+
689+
private static String replaceContextVariable(String content, String policyType) {
690+
if (policyType.equals(ACCESS_CONTROL.getName())) {
691+
return "{\"principalRole\":\"ANALYST\",\"columnProjections\":[\"name\",\"location\"],\"rowFilters\":[\"{\\\"type\\\":\\\"eq\\\",\\\"term\\\":\\\"country\\\",\\\"value\\\":\\\"USA\\\"}\",\"false\"]}";
692+
}
693+
return content;
694+
}
645695
}

0 commit comments

Comments
 (0)