diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyContent.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyContent.java new file mode 100644 index 000000000..a1e82f7b6 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyContent.java @@ -0,0 +1,22 @@ +/* + * 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.polaris.core.policy; + +/** A marker interface for policy content */ +public interface PolicyContent {} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/InvalidPolicyException.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/InvalidPolicyException.java new file mode 100644 index 000000000..9672cfa61 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/InvalidPolicyException.java @@ -0,0 +1,36 @@ +/* + * 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.polaris.core.policy.validator; + +import org.apache.polaris.core.exceptions.PolarisException; + +/** Exception thrown when a policy is invalid or violates defined rules. */ +public class InvalidPolicyException extends PolarisException { + public InvalidPolicyException(String message) { + super(message); + } + + public InvalidPolicyException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidPolicyException(Throwable cause) { + super("Invalid policy", cause); + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidator.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidator.java new file mode 100644 index 000000000..9abd097ed --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidator.java @@ -0,0 +1,47 @@ +/* + * 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.polaris.core.policy.validator; + +import org.apache.polaris.core.entity.PolarisEntitySubType; +import org.apache.polaris.core.entity.PolarisEntityType; + +/** Validates and parses a given policy content string against its defined schema. */ +public interface PolicyValidator { + + /** + * Validates the provided policy content. + * + * @param content the policy content to parse and validate + * @throws InvalidPolicyException if the content is not valid + */ + void validate(String content) throws InvalidPolicyException; + + /** + * Determines whether the policy is attachable to a target entity. + * + *

This method examines the provided {@link PolarisEntityType} and {@link PolarisEntitySubType} + * to decide if a policy is applicable for attachment to the target entity. + * + * @param entityType the type of the target entity + * @param entitySubType the subtype of the target entity + * @return {@code true} if the policy can be attached to the target entity; {@code false} + * otherwise + */ + boolean canAttach(PolarisEntityType entityType, PolarisEntitySubType entitySubType); +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidatorUtil.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidatorUtil.java new file mode 100644 index 000000000..a08cdfd7e --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidatorUtil.java @@ -0,0 +1,35 @@ +/* + * 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.polaris.core.policy.validator; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class PolicyValidatorUtil { + public static final ObjectMapper MAPPER = configureMapper(); + + private static ObjectMapper configureMapper() { + ObjectMapper mapper = new ObjectMapper(); + // Fails if a required field (in the constructor) is missing + mapper.configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, true); + // Fails if a required field is present but explicitly null, e.g., {"enable": null} + mapper.configure(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES, true); + return mapper; + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidators.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidators.java new file mode 100644 index 000000000..ed37f1279 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidators.java @@ -0,0 +1,95 @@ +/* + * 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.polaris.core.policy.validator; + +import com.google.common.base.Preconditions; +import org.apache.polaris.core.entity.PolarisEntity; +import org.apache.polaris.core.policy.PolicyEntity; +import org.apache.polaris.core.policy.PredefinedPolicyTypes; +import org.apache.polaris.core.policy.validator.datacompaction.DataCompactionPolicyValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Validates a given {@link PolicyEntity} against its defined policy type. + * + *

This class maps the policy type code from the {@code PolicyEntity} to a predefined policy + * type, then delegates parsing/validation to a specific validator implementation. + */ +public class PolicyValidators { + private static final Logger LOGGER = LoggerFactory.getLogger(PolicyValidators.class); + + /** + * Validates the given policy. + * + * @param policy the policy entity to validate + * @throws InvalidPolicyException if the policy type is unknown or unsupported, or if the policy + * content is invalid + */ + public static void validate(PolicyEntity policy) { + Preconditions.checkNotNull(policy, "Policy must not be null"); + + var type = PredefinedPolicyTypes.fromCode(policy.getPolicyTypeCode()); + Preconditions.checkArgument(type != null, "Unknown policy type: " + policy.getPolicyTypeCode()); + + switch (type) { + case DATA_COMPACTION: + DataCompactionPolicyValidator.INSTANCE.validate(policy.getContent()); + break; + + // To support additional policy types in the future, add cases here. + case METADATA_COMPACTION: + case SNAPSHOT_RETENTION: + case ORPHAN_FILE_REMOVAL: + default: + throw new InvalidPolicyException("Unsupported policy type: " + type.getName()); + } + + LOGGER.info("Policy validated successfully: {}", type.getName()); + } + + /** + * Determines whether the given policy can be attached to the specified target entity. + * + * @param policy the policy entity to check + * @param targetEntity the target Polaris entity to attach the policy to + * @return {@code true} if the policy is attachable to the target entity; {@code false} otherwise + */ + public static boolean canAttach(PolicyEntity policy, PolarisEntity targetEntity) { + Preconditions.checkNotNull(policy, "Policy must not be null"); + Preconditions.checkNotNull(targetEntity, "Target entity must not be null"); + + var policyType = PredefinedPolicyTypes.fromCode(policy.getPolicyTypeCode()); + Preconditions.checkArgument( + policyType != null, "Unknown policy type: " + policy.getPolicyTypeCode()); + + switch (policyType) { + case DATA_COMPACTION: + return DataCompactionPolicyValidator.INSTANCE.canAttach( + targetEntity.getType(), targetEntity.getSubType()); + // To support additional policy types in the future, add cases here. + case METADATA_COMPACTION: + case SNAPSHOT_RETENTION: + case ORPHAN_FILE_REMOVAL: + default: + LOGGER.warn("Attachment not supported for policy type: {}", policyType.getName()); + return false; + } + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/StrictBooleanDeserializer.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/StrictBooleanDeserializer.java new file mode 100644 index 000000000..f6da87e70 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/StrictBooleanDeserializer.java @@ -0,0 +1,38 @@ +/* + * 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.polaris.core.policy.validator; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import java.io.IOException; + +public class StrictBooleanDeserializer extends JsonDeserializer { + @Override + public Boolean deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String text = p.getText(); + if ("true".equals(text)) { + return Boolean.TRUE; + } else if ("false".equals(text)) { + return Boolean.FALSE; + } else { + throw new InvalidPolicyException("Invalid boolean value: " + text); + } + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyContent.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyContent.java new file mode 100644 index 000000000..efdd158b5 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyContent.java @@ -0,0 +1,97 @@ +/* + * 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.polaris.core.policy.validator.datacompaction; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.common.base.Strings; +import java.util.Map; +import java.util.Set; +import org.apache.polaris.core.policy.PolicyContent; +import org.apache.polaris.core.policy.validator.InvalidPolicyException; +import org.apache.polaris.core.policy.validator.PolicyValidatorUtil; +import org.apache.polaris.core.policy.validator.StrictBooleanDeserializer; + +public class DataCompactionPolicyContent implements PolicyContent { + private static final String DEFAULT_POLICY_SCHEMA_VERSION = "2025-02-03"; + private static final Set POLICY_SCHEMA_VERSIONS = Set.of(DEFAULT_POLICY_SCHEMA_VERSION); + + @JsonDeserialize(using = StrictBooleanDeserializer.class) + private Boolean enable; + + private String version; + private Map config; + + @JsonCreator + public DataCompactionPolicyContent( + @JsonProperty(value = "enable", required = true) boolean enable) { + this.enable = enable; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public Boolean enabled() { + return enable; + } + + public void setEnabled(Boolean enable) { + this.enable = enable; + } + + public Map getConfig() { + return config; + } + + public void setConfig(Map config) { + this.config = config; + } + + public static DataCompactionPolicyContent fromString(String content) { + if (Strings.isNullOrEmpty(content)) { + throw new InvalidPolicyException("Policy is empty"); + } + + try { + DataCompactionPolicyContent policy = + PolicyValidatorUtil.MAPPER.readValue(content, DataCompactionPolicyContent.class); + if (policy == null) { + throw new InvalidPolicyException("Invalid policy"); + } + + if (Strings.isNullOrEmpty(policy.getVersion())) { + policy.setVersion(DEFAULT_POLICY_SCHEMA_VERSION); + } + + if (!POLICY_SCHEMA_VERSIONS.contains(policy.getVersion())) { + throw new InvalidPolicyException("Invalid policy version: " + policy.getVersion()); + } + + return policy; + } catch (Exception e) { + throw new InvalidPolicyException(e); + } + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyValidator.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyValidator.java new file mode 100644 index 000000000..8ef8b4dcf --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyValidator.java @@ -0,0 +1,58 @@ +/* + * 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.polaris.core.policy.validator.datacompaction; + +import static org.apache.polaris.core.entity.PolarisEntityType.CATALOG; +import static org.apache.polaris.core.entity.PolarisEntityType.ICEBERG_TABLE_LIKE; +import static org.apache.polaris.core.entity.PolarisEntityType.NAMESPACE; + +import java.util.Set; +import org.apache.polaris.core.entity.PolarisEntitySubType; +import org.apache.polaris.core.entity.PolarisEntityType; +import org.apache.polaris.core.policy.validator.InvalidPolicyException; +import org.apache.polaris.core.policy.validator.PolicyValidator; + +public class DataCompactionPolicyValidator implements PolicyValidator { + public static final DataCompactionPolicyValidator INSTANCE = new DataCompactionPolicyValidator(); + + private static final Set ATTACHABLE_ENTITY_TYPES = + Set.of(CATALOG, NAMESPACE, ICEBERG_TABLE_LIKE); + + @Override + public void validate(String content) throws InvalidPolicyException { + DataCompactionPolicyContent.fromString(content); + } + + @Override + public boolean canAttach(PolarisEntityType entityType, PolarisEntitySubType entitySubType) { + if (entityType == null) { + return false; + } + + if (!ATTACHABLE_ENTITY_TYPES.contains(entityType)) { + return false; + } + + if (entityType == ICEBERG_TABLE_LIKE && entitySubType != PolarisEntitySubType.TABLE) { + return false; + } + + return true; + } +} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyContentTest.java b/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyContentTest.java new file mode 100644 index 000000000..3d70fbeef --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyContentTest.java @@ -0,0 +1,97 @@ +/* + * 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.polaris.core.policy.validator; + +import static org.apache.polaris.core.policy.validator.datacompaction.DataCompactionPolicyContent.fromString; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +public class DataCompactionPolicyContentTest { + @Test + public void testValidPolicies() { + assertThat(fromString("{\"enable\": false}").enabled()).isFalse(); + assertThat(fromString("{\"enable\": true}").enabled()).isTrue(); + + var validJson = "{\"version\":\"2025-02-03\", \"enable\": true}"; + assertThat(fromString(validJson).getVersion()).isEqualTo("2025-02-03"); + + validJson = "{\"enable\": true, \"config\": {\"key1\": \"value1\", \"key2\": true}}"; + assertThat(fromString(validJson).getConfig().get("key1")).isEqualTo("value1"); + } + + @Test + void testIsValidEmptyString() { + assertThatThrownBy(() -> fromString("")) + .as("Validating empty string should throw InvalidPolicyException") + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Policy is empty"); + } + + @Test + void testIsValidEmptyJson() { + assertThatThrownBy(() -> fromString("{}")) + .as("Validating empty JSON '{}' should throw InvalidPolicyException") + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Invalid policy"); + } + + @Test + void testIsValidInvalidVersionFormat() { + String invalidPolicy1 = "{\"enable\": true, \"version\": \"fdafds\"}"; + assertThatThrownBy(() -> fromString(invalidPolicy1)) + .as("Validating policy with invalid version format should throw InvalidPolicyException") + .isInstanceOf(InvalidPolicyException.class); + } + + @Test + void testIsValidInvalidKeyInPolicy() { + String invalidPolicy2 = + "{\"version\":\"2025-02-03\", \"enable\": true, \"invalid_key\": 12342}"; + assertThatThrownBy(() -> fromString(invalidPolicy2)) + .as("Validating policy with an unknown key should throw InvalidPolicyException") + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Invalid policy"); + } + + @Test + void testIsValidUnrecognizedToken() { + var invalidPolicy = "{\"enable\": invalidToken}"; + assertThatThrownBy(() -> fromString(invalidPolicy)) + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Invalid policy"); + } + + @Test + void testIsValidNullValue() { + var invalidPolicy = "{\"enable\": null}"; + assertThatThrownBy(() -> fromString(invalidPolicy)) + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Invalid policy"); + } + + @Test + void testIsValidWrongString() { + var invalidPolicy = "{\"enable\": \"invalid\"}"; + assertThatThrownBy(() -> fromString(invalidPolicy)) + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Invalid policy"); + } +} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyValidatorTest.java b/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyValidatorTest.java new file mode 100644 index 000000000..e833015d6 --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyValidatorTest.java @@ -0,0 +1,88 @@ +/* + * 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.polaris.core.policy.validator; + +import static org.apache.polaris.core.entity.PolarisEntitySubType.ANY_SUBTYPE; +import static org.apache.polaris.core.entity.PolarisEntitySubType.TABLE; +import static org.apache.polaris.core.entity.PolarisEntitySubType.VIEW; +import static org.apache.polaris.core.entity.PolarisEntityType.CATALOG; +import static org.apache.polaris.core.entity.PolarisEntityType.ICEBERG_TABLE_LIKE; +import static org.apache.polaris.core.entity.PolarisEntityType.NAMESPACE; +import static org.apache.polaris.core.entity.PolarisEntityType.PRINCIPAL; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.apache.polaris.core.policy.validator.datacompaction.DataCompactionPolicyValidator; +import org.junit.jupiter.api.Test; + +public class DataCompactionPolicyValidatorTest { + private final DataCompactionPolicyValidator validator = new DataCompactionPolicyValidator(); + + @Test + public void testValidPolicies() { + var validJson = "{\"version\":\"2025-02-03\", \"enable\": true}"; + validator.validate(validJson); + + assertThatThrownBy(() -> validator.validate("")) + .as("Validating empty string should throw InvalidPolicyException") + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Policy is empty"); + } + + @Test + public void testCanAttachReturnsTrueForCatalogType() { + var result = validator.canAttach(CATALOG, ANY_SUBTYPE); // using any valid subtype + assertThat(result).isTrue().as("Expected canAttach() to return true for CATALOG type"); + } + + @Test + public void testCanAttachReturnsTrueForNamespaceType() { + var result = validator.canAttach(NAMESPACE, ANY_SUBTYPE); // using any valid subtype + assertThat(result).isTrue().as("Expected canAttach() to return true for CATALOG type"); + } + + @Test + public void testCanAttachReturnsTrueForIcebergTableLikeWithTableSubtype() { + var result = validator.canAttach(ICEBERG_TABLE_LIKE, TABLE); + assertThat(result) + .isTrue() + .as("Expected canAttach() to return true for ICEBERG_TABLE_LIKE with TABLE subtype"); + } + + @Test + public void testCanAttachReturnsFalseForIcebergTableLikeWithNonTableSubtype() { + // For ICEBERG_TABLE_LIKE, any subtype other than TABLE should return false. + boolean result = validator.canAttach(ICEBERG_TABLE_LIKE, VIEW); + assertThat(result) + .isFalse() + .as("Expected canAttach() to return false for ICEBERG_TABLE_LIKE with non-TABLE subtype"); + } + + @Test + public void testCanAttachReturnsFalseForNull() { + var result = validator.canAttach(null, null); // using any valid subtype + assertThat(result).isFalse().as("Expected canAttach() to return false for null"); + } + + @Test + public void testCanAttachReturnsFalseForUnattachableType() { + var result = validator.canAttach(PRINCIPAL, null); // using any valid subtype + assertThat(result).isFalse().as("Expected canAttach() to return false for null"); + } +} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/PolicyValidatorsTest.java b/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/PolicyValidatorsTest.java new file mode 100644 index 000000000..24d122a11 --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/PolicyValidatorsTest.java @@ -0,0 +1,65 @@ +/* + * 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.polaris.core.policy.validator; + +import static org.apache.polaris.core.policy.PredefinedPolicyTypes.DATA_COMPACTION; +import static org.apache.polaris.core.policy.PredefinedPolicyTypes.METADATA_COMPACTION; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.apache.iceberg.catalog.Namespace; +import org.apache.polaris.core.policy.PolicyEntity; +import org.junit.jupiter.api.Test; + +public class PolicyValidatorsTest { + @Test + public void testInvalidPolicy() { + var policyEntity = + new PolicyEntity.Builder(Namespace.of("NS1"), "testPolicy", DATA_COMPACTION) + .setContent("InvalidContent") + .setPolicyVersion(0) + .build(); + assertThatThrownBy(() -> PolicyValidators.validate(policyEntity)) + .as("Validating empty JSON '{}' should throw InvalidPolicyException") + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Invalid policy"); + } + + @Test + public void testUnsupportedPolicyType() { + var policyEntity = + new PolicyEntity.Builder(Namespace.of("NS1"), "testPolicy", METADATA_COMPACTION) + .setContent("InvalidContent") + .setPolicyVersion(0) + .build(); + + assertThatThrownBy(() -> PolicyValidators.validate(policyEntity)) + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Unsupported policy type"); + } + + @Test + public void testValidPolicy() { + var policyEntity = + new PolicyEntity.Builder(Namespace.of("NS1"), "testPolicy", DATA_COMPACTION) + .setContent("{\"enable\": false}") + .setPolicyVersion(0) + .build(); + PolicyValidators.validate(policyEntity); + } +} diff --git a/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java b/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java index 7686f9f75..0636f5b97 100644 --- a/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java +++ b/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java @@ -25,6 +25,7 @@ import org.apache.iceberg.rest.responses.ErrorResponse; import org.apache.polaris.core.exceptions.AlreadyExistsException; import org.apache.polaris.core.exceptions.PolarisException; +import org.apache.polaris.core.policy.validator.InvalidPolicyException; import org.apache.polaris.service.context.UnresolvableRealmContextException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,6 +45,8 @@ private Response.Status getStatus(PolarisException exception) { return Response.Status.CONFLICT; } else if (exception instanceof UnresolvableRealmContextException) { return Response.Status.NOT_FOUND; + } else if (exception instanceof InvalidPolicyException) { + return Response.Status.BAD_REQUEST; } else { return Response.Status.INTERNAL_SERVER_ERROR; }