diff --git a/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java b/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java index 271282d0e1..76a6d54780 100644 --- a/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java +++ b/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java @@ -26,6 +26,7 @@ import java.util.Set; import org.apache.iceberg.exceptions.ForbiddenException; import org.apache.polaris.core.auth.AuthorizationDecision; +import org.apache.polaris.core.auth.AuthorizationPreConditions; import org.apache.polaris.core.auth.AuthorizationRequest; import org.apache.polaris.core.auth.AuthorizationState; import org.apache.polaris.core.auth.PolarisAuthorizableOperation; @@ -35,7 +36,6 @@ import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.context.RealmContext; import org.apache.polaris.core.entity.PolarisBaseEntity; -import org.apache.polaris.core.entity.PolarisEntityConstants; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; import org.apache.polaris.extension.auth.ranger.utils.RangerUtils; import org.apache.ranger.authz.api.RangerAuthzException; @@ -55,8 +55,6 @@ public class RangerPolarisAuthorizer implements PolarisAuthorizer { public static final String SERVICE_TYPE = "polaris"; - private static final String OPERATION_NOT_ALLOWED_FOR_USER_ERROR = - "Principal '%s' is not authorized for op %s due to PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE"; private static final String RANGER_AUTH_FAILED_ERROR = "Principal '%s' is not authorized for op '%s'"; @@ -136,14 +134,8 @@ public void authorizeOrThrow( } try { - if (enforceCredentialRotationRequiredState - && authzOp != PolarisAuthorizableOperation.ROTATE_CREDENTIALS - && polarisPrincipal - .getProperties() - .containsKey(PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE)) { - throw new ForbiddenException( - OPERATION_NOT_ALLOWED_FOR_USER_ERROR, polarisPrincipal.getName(), authzOp.name()); - } + AuthorizationPreConditions.checkCredentialRotationRequired( + polarisPrincipal, authzOp, enforceCredentialRotationRequiredState); if (!isAccessAuthorized(polarisPrincipal, authzOp, targets, secondaries)) { throw new ForbiddenException( diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationPreConditions.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationPreConditions.java new file mode 100644 index 0000000000..19c0558fb8 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationPreConditions.java @@ -0,0 +1,56 @@ +/* + * 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.auth; + +import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.polaris.core.entity.PolarisEntityConstants; + +/** + * Common pre-condition checks shared across authorizer implementations for credential-related + * operations. + */ +public final class AuthorizationPreConditions { + + private AuthorizationPreConditions() {} + + /** + * Checks whether the principal is required to rotate credentials before performing the requested + * operation. If the principal has the {@code PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE} + * property set, only {@link PolarisAuthorizableOperation#ROTATE_CREDENTIALS} is allowed. + * + * @param polarisPrincipal the principal attempting the operation + * @param authzOp the operation being attempted + * @param enforceCredentialRotationRequiredState whether the enforcement flag is enabled + * @throws ForbiddenException if the principal must rotate credentials first + */ + public static void checkCredentialRotationRequired( + PolarisPrincipal polarisPrincipal, + PolarisAuthorizableOperation authzOp, + boolean enforceCredentialRotationRequiredState) { + if (enforceCredentialRotationRequiredState + && authzOp != PolarisAuthorizableOperation.ROTATE_CREDENTIALS + && polarisPrincipal + .getProperties() + .containsKey(PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE)) { + throw new ForbiddenException( + "Principal '%s' is not authorized for op %s due to PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE", + polarisPrincipal.getName(), authzOp); + } + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java index bf84a2ad4c..00d594b3dc 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java @@ -137,7 +137,6 @@ import org.apache.polaris.core.config.FeatureConfiguration; import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.entity.PolarisBaseEntity; -import org.apache.polaris.core.entity.PolarisEntityConstants; import org.apache.polaris.core.entity.PolarisEntityCore; import org.apache.polaris.core.entity.PolarisGrantRecord; import org.apache.polaris.core.entity.PolarisPrivilege; @@ -885,16 +884,10 @@ public void authorizeOrThrow( boolean enforceCredentialRotationRequiredState = realmConfig.getConfig( FeatureConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING); + AuthorizationPreConditions.checkCredentialRotationRequired( + polarisPrincipal, authzOp, enforceCredentialRotationRequiredState); boolean isRoot = getRootPrincipalName().equals(polarisPrincipal.getName()); - if (enforceCredentialRotationRequiredState - && polarisPrincipal - .getProperties() - .containsKey(PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE) - && authzOp != PolarisAuthorizableOperation.ROTATE_CREDENTIALS) { - throw new ForbiddenException( - "Principal '%s' is not authorized for op %s due to PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE", - polarisPrincipal.getName(), authzOp); - } else if (authzOp == PolarisAuthorizableOperation.RESET_CREDENTIALS) { + if (authzOp == PolarisAuthorizableOperation.RESET_CREDENTIALS) { if (!isRoot) { throw new ForbiddenException("Only Root principal(service-admin) can perform %s", authzOp); } diff --git a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationPreConditionsTest.java b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationPreConditionsTest.java new file mode 100644 index 0000000000..1f69ddf1f2 --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationPreConditionsTest.java @@ -0,0 +1,82 @@ +/* + * 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.auth; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Map; +import java.util.Set; +import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.polaris.core.entity.PolarisEntityConstants; +import org.junit.jupiter.api.Test; + +public class AuthorizationPreConditionsTest { + + private static final String ROTATION_KEY = + PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE; + + @Test + public void testFlagOff_noException() { + PolarisPrincipal principal = + PolarisPrincipal.of("alice", Map.of(ROTATION_KEY, "true"), Set.of("role")); + + assertThatCode( + () -> + AuthorizationPreConditions.checkCredentialRotationRequired( + principal, PolarisAuthorizableOperation.LIST_CATALOGS, false)) + .doesNotThrowAnyException(); + } + + @Test + public void testFlagOn_rotateCredentialsOp_noException() { + PolarisPrincipal principal = + PolarisPrincipal.of("alice", Map.of(ROTATION_KEY, "true"), Set.of("role")); + + assertThatCode( + () -> + AuthorizationPreConditions.checkCredentialRotationRequired( + principal, PolarisAuthorizableOperation.ROTATE_CREDENTIALS, true)) + .doesNotThrowAnyException(); + } + + @Test + public void testFlagOn_nonRotationOp_propertySet_throwsForbidden() { + PolarisPrincipal principal = + PolarisPrincipal.of("alice", Map.of(ROTATION_KEY, "true"), Set.of("role")); + + assertThatThrownBy( + () -> + AuthorizationPreConditions.checkCredentialRotationRequired( + principal, PolarisAuthorizableOperation.LIST_CATALOGS, true)) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE"); + } + + @Test + public void testFlagOn_nonRotationOp_propertyNotSet_noException() { + PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role")); + + assertThatCode( + () -> + AuthorizationPreConditions.checkCredentialRotationRequired( + principal, PolarisAuthorizableOperation.LIST_CATALOGS, true)) + .doesNotThrowAnyException(); + } +}