flyingImer commented on code in PR #3928: URL: https://github.com/apache/polaris/pull/3928#discussion_r2891599429
########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizerFactory.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.extension.auth.ranger; + +import io.smallrye.common.annotation.Identifier; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.apache.polaris.core.auth.PolarisAuthorizerFactory; +import org.apache.polaris.core.config.RealmConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ApplicationScoped +@Identifier("ranger") +public class RangerPolarisAuthorizerFactory implements PolarisAuthorizerFactory { + private static final Logger LOG = LoggerFactory.getLogger(RangerPolarisAuthorizerFactory.class); + + private final RangerPolarisAuthorizerConfig config; + + @Inject + RangerPolarisAuthorizerFactory(RangerPolarisAuthorizerConfig config) { + this.config = config; + + LOG.debug("RangerPolarisAuthorizerFactory has been activated."); + } + + @PostConstruct + public void initialize() { + config.validate(); + } + + @PreDestroy + public void cleanup() {} + + @Override + public RangerPolarisAuthorizer create(RealmConfig realmConfig) { + LOG.debug("Creating RangerPolarisAuthorizer"); + + try { + return new RangerPolarisAuthorizer(config, realmConfig); + } catch (Throwable t) { + LOG.error("Failed to create RangerPolarisAuthorizer", t); + } + + return null; Review Comment: IMO should throw some SPI exception rather than bubbling up null causing NPE later ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java: ########## @@ -0,0 +1,449 @@ +/* + * 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.extension.auth.ranger; + +import static org.apache.polaris.core.entity.PolarisEntityConstants.getRootPrincipalName; + +import com.google.common.base.Preconditions; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.polaris.core.auth.AuthorizationDecision; +import org.apache.polaris.core.auth.AuthorizationRequest; +import org.apache.polaris.core.auth.AuthorizationState; +import org.apache.polaris.core.auth.PolarisAuthorizableOperation; +import org.apache.polaris.core.auth.PolarisAuthorizer; +import org.apache.polaris.core.auth.PolarisPrincipal; +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.persistence.PolarisResolvedPathWrapper; +import org.apache.polaris.extension.auth.ranger.utils.RangerUtils; +import org.apache.ranger.authz.api.RangerAuthorizer; +import org.apache.ranger.authz.api.RangerAuthzException; +import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer; +import org.apache.ranger.authz.model.RangerAccessContext; +import org.apache.ranger.authz.model.RangerAccessInfo; +import org.apache.ranger.authz.model.RangerAuthzResult; +import org.apache.ranger.authz.model.RangerMultiAuthzRequest; +import org.apache.ranger.authz.model.RangerMultiAuthzResult; +import org.apache.ranger.authz.model.RangerUserInfo; +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Authorizes operations based on policies defined in Apache Ranger. */ +public class RangerPolarisAuthorizer implements PolarisAuthorizer { + private static final Logger LOG = LoggerFactory.getLogger(RangerPolarisAuthorizer.class); + + public static final String SERVICE_TYPE = "polaris"; + public static final String SERVICE_NAME_PROPERTY = "ranger.plugin.polaris.service.name"; + + 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 ROOT_PRINCIPLE_NEEDED_ERROR = + "Principal '%s' is not authorized for op %s as only root principal can perform this operation"; + private static final String RANGER_AUTH_FAILED_ERROR = + "Principal '%s' is not authorized for op '%s'"; + private static final String RANGER_UNSUPPORTED_OPERATION = + "Operation %s is not supported by Ranger authorizer"; + + private static final Set<PolarisAuthorizableOperation> AUTHORIZED_OPERATIONS = + initAuthorizedOperations(); + + private final RealmConfig realmConfig; + private final RangerAuthorizer authorizer; + private final String serviceName; + + public RangerPolarisAuthorizer(RangerPolarisAuthorizerConfig config, RealmConfig realmConfig) { + LOG.info("Initializing RangerPolarisAuthorizer"); + + Properties rangerProp = RangerUtils.loadProperties(config.configFileName().get()); + + this.realmConfig = realmConfig; + this.authorizer = new RangerEmbeddedAuthorizer(rangerProp); + this.serviceName = rangerProp.getProperty(SERVICE_NAME_PROPERTY); + + try { + authorizer.init(); + } catch (RangerAuthzException t) { + LOG.error("Failed to initialize RangerPolarisAuthorizer", t); + throw new RuntimeException(t); + } + + LOG.info("RangerPolarisAuthorizer initialized successfully"); + } + + @Override + public void resolveAuthorizationInputs( + @NonNull AuthorizationState authzState, @NonNull AuthorizationRequest request) { + throw new UnsupportedOperationException( + "resolveAuthorizationInputs is not implemented yet for RangerPolarisAuthorizer"); + } + + @Override + public @NonNull AuthorizationDecision authorize( + @NonNull AuthorizationState authzState, @NonNull AuthorizationRequest request) { + throw new UnsupportedOperationException( + "authorize is not implemented yet for RangerPolarisAuthorizer"); + } Review Comment: are these ready for a review? ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java: ########## @@ -0,0 +1,449 @@ +/* + * 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.extension.auth.ranger; + +import static org.apache.polaris.core.entity.PolarisEntityConstants.getRootPrincipalName; + +import com.google.common.base.Preconditions; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.polaris.core.auth.AuthorizationDecision; +import org.apache.polaris.core.auth.AuthorizationRequest; +import org.apache.polaris.core.auth.AuthorizationState; +import org.apache.polaris.core.auth.PolarisAuthorizableOperation; +import org.apache.polaris.core.auth.PolarisAuthorizer; +import org.apache.polaris.core.auth.PolarisPrincipal; +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.persistence.PolarisResolvedPathWrapper; +import org.apache.polaris.extension.auth.ranger.utils.RangerUtils; +import org.apache.ranger.authz.api.RangerAuthorizer; +import org.apache.ranger.authz.api.RangerAuthzException; +import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer; +import org.apache.ranger.authz.model.RangerAccessContext; +import org.apache.ranger.authz.model.RangerAccessInfo; +import org.apache.ranger.authz.model.RangerAuthzResult; +import org.apache.ranger.authz.model.RangerMultiAuthzRequest; +import org.apache.ranger.authz.model.RangerMultiAuthzResult; +import org.apache.ranger.authz.model.RangerUserInfo; +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Authorizes operations based on policies defined in Apache Ranger. */ +public class RangerPolarisAuthorizer implements PolarisAuthorizer { + private static final Logger LOG = LoggerFactory.getLogger(RangerPolarisAuthorizer.class); + + public static final String SERVICE_TYPE = "polaris"; + public static final String SERVICE_NAME_PROPERTY = "ranger.plugin.polaris.service.name"; + + 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 ROOT_PRINCIPLE_NEEDED_ERROR = + "Principal '%s' is not authorized for op %s as only root principal can perform this operation"; + private static final String RANGER_AUTH_FAILED_ERROR = + "Principal '%s' is not authorized for op '%s'"; + private static final String RANGER_UNSUPPORTED_OPERATION = + "Operation %s is not supported by Ranger authorizer"; + + private static final Set<PolarisAuthorizableOperation> AUTHORIZED_OPERATIONS = + initAuthorizedOperations(); + + private final RealmConfig realmConfig; + private final RangerAuthorizer authorizer; + private final String serviceName; + + public RangerPolarisAuthorizer(RangerPolarisAuthorizerConfig config, RealmConfig realmConfig) { + LOG.info("Initializing RangerPolarisAuthorizer"); + + Properties rangerProp = RangerUtils.loadProperties(config.configFileName().get()); + + this.realmConfig = realmConfig; + this.authorizer = new RangerEmbeddedAuthorizer(rangerProp); + this.serviceName = rangerProp.getProperty(SERVICE_NAME_PROPERTY); + + try { + authorizer.init(); + } catch (RangerAuthzException t) { + LOG.error("Failed to initialize RangerPolarisAuthorizer", t); + throw new RuntimeException(t); + } + + LOG.info("RangerPolarisAuthorizer initialized successfully"); + } + + @Override + public void resolveAuthorizationInputs( + @NonNull AuthorizationState authzState, @NonNull AuthorizationRequest request) { + throw new UnsupportedOperationException( + "resolveAuthorizationInputs is not implemented yet for RangerPolarisAuthorizer"); + } + + @Override + public @NonNull AuthorizationDecision authorize( + @NonNull AuthorizationState authzState, @NonNull AuthorizationRequest request) { + throw new UnsupportedOperationException( + "authorize is not implemented yet for RangerPolarisAuthorizer"); + } Review Comment: are they review ready? ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java: ########## @@ -0,0 +1,449 @@ +/* + * 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.extension.auth.ranger; + +import static org.apache.polaris.core.entity.PolarisEntityConstants.getRootPrincipalName; + +import com.google.common.base.Preconditions; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.polaris.core.auth.AuthorizationDecision; +import org.apache.polaris.core.auth.AuthorizationRequest; +import org.apache.polaris.core.auth.AuthorizationState; +import org.apache.polaris.core.auth.PolarisAuthorizableOperation; +import org.apache.polaris.core.auth.PolarisAuthorizer; +import org.apache.polaris.core.auth.PolarisPrincipal; +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.persistence.PolarisResolvedPathWrapper; +import org.apache.polaris.extension.auth.ranger.utils.RangerUtils; +import org.apache.ranger.authz.api.RangerAuthorizer; +import org.apache.ranger.authz.api.RangerAuthzException; +import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer; +import org.apache.ranger.authz.model.RangerAccessContext; +import org.apache.ranger.authz.model.RangerAccessInfo; +import org.apache.ranger.authz.model.RangerAuthzResult; +import org.apache.ranger.authz.model.RangerMultiAuthzRequest; +import org.apache.ranger.authz.model.RangerMultiAuthzResult; +import org.apache.ranger.authz.model.RangerUserInfo; +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Authorizes operations based on policies defined in Apache Ranger. */ +public class RangerPolarisAuthorizer implements PolarisAuthorizer { + private static final Logger LOG = LoggerFactory.getLogger(RangerPolarisAuthorizer.class); + + public static final String SERVICE_TYPE = "polaris"; + public static final String SERVICE_NAME_PROPERTY = "ranger.plugin.polaris.service.name"; + + 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 ROOT_PRINCIPLE_NEEDED_ERROR = + "Principal '%s' is not authorized for op %s as only root principal can perform this operation"; + private static final String RANGER_AUTH_FAILED_ERROR = + "Principal '%s' is not authorized for op '%s'"; + private static final String RANGER_UNSUPPORTED_OPERATION = + "Operation %s is not supported by Ranger authorizer"; + + private static final Set<PolarisAuthorizableOperation> AUTHORIZED_OPERATIONS = + initAuthorizedOperations(); + + private final RealmConfig realmConfig; + private final RangerAuthorizer authorizer; + private final String serviceName; + + public RangerPolarisAuthorizer(RangerPolarisAuthorizerConfig config, RealmConfig realmConfig) { + LOG.info("Initializing RangerPolarisAuthorizer"); + + Properties rangerProp = RangerUtils.loadProperties(config.configFileName().get()); + + this.realmConfig = realmConfig; + this.authorizer = new RangerEmbeddedAuthorizer(rangerProp); + this.serviceName = rangerProp.getProperty(SERVICE_NAME_PROPERTY); + + try { + authorizer.init(); + } catch (RangerAuthzException t) { + LOG.error("Failed to initialize RangerPolarisAuthorizer", t); + throw new RuntimeException(t); + } + + LOG.info("RangerPolarisAuthorizer initialized successfully"); + } + + @Override + public void resolveAuthorizationInputs( + @NonNull AuthorizationState authzState, @NonNull AuthorizationRequest request) { + throw new UnsupportedOperationException( + "resolveAuthorizationInputs is not implemented yet for RangerPolarisAuthorizer"); + } + + @Override + public @NonNull AuthorizationDecision authorize( + @NonNull AuthorizationState authzState, @NonNull AuthorizationRequest request) { + throw new UnsupportedOperationException( + "authorize is not implemented yet for RangerPolarisAuthorizer"); + } + + @Override + public void authorizeOrThrow( + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull Set<PolarisBaseEntity> activatedEntities, + @Nonnull PolarisAuthorizableOperation authzOp, + @Nullable PolarisResolvedPathWrapper target, + @Nullable PolarisResolvedPathWrapper secondary) { + authorizeOrThrow( + polarisPrincipal, + activatedEntities, + authzOp, + target == null ? null : List.of(target), + secondary == null ? null : List.of(secondary)); + } + + @Override + public void authorizeOrThrow( + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull Set<PolarisBaseEntity> activatedEntities, + @Nonnull PolarisAuthorizableOperation authzOp, + @Nullable List<PolarisResolvedPathWrapper> targets, + @Nullable List<PolarisResolvedPathWrapper> secondaries) { + try { + if (authzOp == PolarisAuthorizableOperation.ROTATE_CREDENTIALS) { + boolean enforceCredentialRotationRequiredState = + realmConfig.getConfig( + FeatureConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING); + + if (enforceCredentialRotationRequiredState + && !polarisPrincipal + .getProperties() + .containsKey(PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE)) { + // TODO: enable ranger audit from here to ensure that the request denied captured. + throw new ForbiddenException( + OPERATION_NOT_ALLOWED_FOR_USER_ERROR, polarisPrincipal.getName(), authzOp.name()); + } Review Comment: This logic seems to be inverted from https://github.com/apache/polaris/blob/main/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java#L802-L813, is this right? ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/utils/RangerUtils.java: ########## @@ -0,0 +1,249 @@ +/* + * 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.extension.auth.ranger.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.polaris.core.auth.PolarisAuthorizableOperation; +import org.apache.polaris.core.auth.PolarisPrincipal; +import org.apache.polaris.core.entity.PolarisEntity; +import org.apache.polaris.core.entity.PolarisEntityType; +import org.apache.polaris.core.entity.PolarisPrivilege; +import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; +import org.apache.polaris.core.persistence.ResolvedPolarisEntity; +import org.apache.polaris.extension.auth.ranger.RangerPolarisAuthorizer; +import org.apache.ranger.authz.model.RangerAccessInfo; +import org.apache.ranger.authz.model.RangerResourceInfo; +import org.apache.ranger.authz.model.RangerUserInfo; +import org.apache.ranger.authz.util.RangerResourceNameParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RangerUtils { + private static final Logger LOG = LoggerFactory.getLogger(RangerUtils.class); + + public static Properties loadProperties(String resourcePath) { + Properties prop = new Properties(); + + if (resourcePath != null) { + resourcePath = resourcePath.trim(); + + if (!resourcePath.startsWith("/")) { + LOG.info("Adding / to the configFileName [{}]", resourcePath); + + resourcePath = "/" + resourcePath; + } + + try (InputStream in = RangerPolarisAuthorizer.class.getResourceAsStream(resourcePath)) { Review Comment: do you intend to load by classpath or filesystem? they might be different. Your README implies filesystem access. we should do better in README expectation as well as aligning with the implementation ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java: ########## @@ -0,0 +1,449 @@ +/* + * 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.extension.auth.ranger; + +import static org.apache.polaris.core.entity.PolarisEntityConstants.getRootPrincipalName; + +import com.google.common.base.Preconditions; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.polaris.core.auth.AuthorizationDecision; +import org.apache.polaris.core.auth.AuthorizationRequest; +import org.apache.polaris.core.auth.AuthorizationState; +import org.apache.polaris.core.auth.PolarisAuthorizableOperation; +import org.apache.polaris.core.auth.PolarisAuthorizer; +import org.apache.polaris.core.auth.PolarisPrincipal; +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.persistence.PolarisResolvedPathWrapper; +import org.apache.polaris.extension.auth.ranger.utils.RangerUtils; +import org.apache.ranger.authz.api.RangerAuthorizer; +import org.apache.ranger.authz.api.RangerAuthzException; +import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer; +import org.apache.ranger.authz.model.RangerAccessContext; +import org.apache.ranger.authz.model.RangerAccessInfo; +import org.apache.ranger.authz.model.RangerAuthzResult; +import org.apache.ranger.authz.model.RangerMultiAuthzRequest; +import org.apache.ranger.authz.model.RangerMultiAuthzResult; +import org.apache.ranger.authz.model.RangerUserInfo; +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Authorizes operations based on policies defined in Apache Ranger. */ +public class RangerPolarisAuthorizer implements PolarisAuthorizer { + private static final Logger LOG = LoggerFactory.getLogger(RangerPolarisAuthorizer.class); + + public static final String SERVICE_TYPE = "polaris"; + public static final String SERVICE_NAME_PROPERTY = "ranger.plugin.polaris.service.name"; + + 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 ROOT_PRINCIPLE_NEEDED_ERROR = + "Principal '%s' is not authorized for op %s as only root principal can perform this operation"; + private static final String RANGER_AUTH_FAILED_ERROR = + "Principal '%s' is not authorized for op '%s'"; + private static final String RANGER_UNSUPPORTED_OPERATION = + "Operation %s is not supported by Ranger authorizer"; + + private static final Set<PolarisAuthorizableOperation> AUTHORIZED_OPERATIONS = + initAuthorizedOperations(); + + private final RealmConfig realmConfig; + private final RangerAuthorizer authorizer; + private final String serviceName; + + public RangerPolarisAuthorizer(RangerPolarisAuthorizerConfig config, RealmConfig realmConfig) { + LOG.info("Initializing RangerPolarisAuthorizer"); + + Properties rangerProp = RangerUtils.loadProperties(config.configFileName().get()); + + this.realmConfig = realmConfig; + this.authorizer = new RangerEmbeddedAuthorizer(rangerProp); + this.serviceName = rangerProp.getProperty(SERVICE_NAME_PROPERTY); + + try { + authorizer.init(); + } catch (RangerAuthzException t) { + LOG.error("Failed to initialize RangerPolarisAuthorizer", t); + throw new RuntimeException(t); + } + + LOG.info("RangerPolarisAuthorizer initialized successfully"); + } + + @Override + public void resolveAuthorizationInputs( + @NonNull AuthorizationState authzState, @NonNull AuthorizationRequest request) { + throw new UnsupportedOperationException( + "resolveAuthorizationInputs is not implemented yet for RangerPolarisAuthorizer"); + } + + @Override + public @NonNull AuthorizationDecision authorize( + @NonNull AuthorizationState authzState, @NonNull AuthorizationRequest request) { + throw new UnsupportedOperationException( + "authorize is not implemented yet for RangerPolarisAuthorizer"); + } + + @Override + public void authorizeOrThrow( + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull Set<PolarisBaseEntity> activatedEntities, + @Nonnull PolarisAuthorizableOperation authzOp, + @Nullable PolarisResolvedPathWrapper target, + @Nullable PolarisResolvedPathWrapper secondary) { + authorizeOrThrow( + polarisPrincipal, + activatedEntities, + authzOp, + target == null ? null : List.of(target), + secondary == null ? null : List.of(secondary)); + } + + @Override + public void authorizeOrThrow( + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull Set<PolarisBaseEntity> activatedEntities, + @Nonnull PolarisAuthorizableOperation authzOp, + @Nullable List<PolarisResolvedPathWrapper> targets, + @Nullable List<PolarisResolvedPathWrapper> secondaries) { + try { + if (authzOp == PolarisAuthorizableOperation.ROTATE_CREDENTIALS) { + boolean enforceCredentialRotationRequiredState = + realmConfig.getConfig( + FeatureConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING); + + if (enforceCredentialRotationRequiredState + && !polarisPrincipal + .getProperties() + .containsKey(PolarisEntityConstants.PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE)) { + // TODO: enable ranger audit from here to ensure that the request denied captured. + throw new ForbiddenException( + OPERATION_NOT_ALLOWED_FOR_USER_ERROR, polarisPrincipal.getName(), authzOp.name()); + } + } else if (authzOp == PolarisAuthorizableOperation.RESET_CREDENTIALS) { + boolean isRootPrincipal = getRootPrincipalName().equals(polarisPrincipal.getName()); + + if (!isRootPrincipal) { + // TODO: enable ranger audit from here to ensure that the request denied captured. + throw new ForbiddenException( + ROOT_PRINCIPLE_NEEDED_ERROR, polarisPrincipal.getName(), authzOp.name()); + } + } else if (!AUTHORIZED_OPERATIONS.contains(authzOp)) { + throw new ForbiddenException(RANGER_UNSUPPORTED_OPERATION, authzOp.name()); + } else if (!isAccessAuthorized( + polarisPrincipal, activatedEntities, authzOp, targets, secondaries)) { + throw new ForbiddenException( + RANGER_AUTH_FAILED_ERROR, polarisPrincipal.getName(), authzOp.name()); + } + } catch (RangerAuthzException excp) { + LOG.error("Failed to authorize principal {} for op {}", polarisPrincipal, authzOp, excp); + throw new IllegalStateException(excp); + } catch (IllegalStateException ise) { + LOG.error("Failed to authorize principal {} for op {}", polarisPrincipal, authzOp, ise); + throw ise; + } + } + + private boolean isAccessAuthorized( + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull Set<PolarisBaseEntity> activatedEntities, + @Nonnull PolarisAuthorizableOperation authzOp, + @Nullable List<PolarisResolvedPathWrapper> targets, + @Nullable List<PolarisResolvedPathWrapper> secondaries) + throws RangerAuthzException { + if (LOG.isDebugEnabled()) { + LOG.debug( + "isAuthorized: users={}, groups={}", + polarisPrincipal.getName(), + String.join(",", polarisPrincipal.getRoles())); + + LOG.debug( + "isAuthorized: activatedEntities={}", + activatedEntities.stream() Review Comment: info: I may miss some context here: why Ranger AuthZ doesn't need `activatedEntities`? and how is it different from `PolarisPrincipal`? current SPI does not have too much doc around it ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java: ########## @@ -0,0 +1,449 @@ +/* + * 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.extension.auth.ranger; + +import static org.apache.polaris.core.entity.PolarisEntityConstants.getRootPrincipalName; + +import com.google.common.base.Preconditions; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.polaris.core.auth.AuthorizationDecision; +import org.apache.polaris.core.auth.AuthorizationRequest; +import org.apache.polaris.core.auth.AuthorizationState; +import org.apache.polaris.core.auth.PolarisAuthorizableOperation; +import org.apache.polaris.core.auth.PolarisAuthorizer; +import org.apache.polaris.core.auth.PolarisPrincipal; +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.persistence.PolarisResolvedPathWrapper; +import org.apache.polaris.extension.auth.ranger.utils.RangerUtils; +import org.apache.ranger.authz.api.RangerAuthorizer; +import org.apache.ranger.authz.api.RangerAuthzException; +import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer; +import org.apache.ranger.authz.model.RangerAccessContext; +import org.apache.ranger.authz.model.RangerAccessInfo; +import org.apache.ranger.authz.model.RangerAuthzResult; +import org.apache.ranger.authz.model.RangerMultiAuthzRequest; +import org.apache.ranger.authz.model.RangerMultiAuthzResult; +import org.apache.ranger.authz.model.RangerUserInfo; +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Authorizes operations based on policies defined in Apache Ranger. */ +public class RangerPolarisAuthorizer implements PolarisAuthorizer { + private static final Logger LOG = LoggerFactory.getLogger(RangerPolarisAuthorizer.class); + + public static final String SERVICE_TYPE = "polaris"; + public static final String SERVICE_NAME_PROPERTY = "ranger.plugin.polaris.service.name"; + + 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 ROOT_PRINCIPLE_NEEDED_ERROR = Review Comment: typo: `PRINCIPAL` ########## extensions/auth/ranger/build.gradle.kts: ########## @@ -0,0 +1,55 @@ +/* + * 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. + */ + +plugins { + id("polaris-server") + id("org.kordamp.gradle.jandex") +} + +dependencies { + implementation(project(":polaris-core")) + + implementation(fileTree("override-libs") { include("*.jar") }) Review Comment: what is this? ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java: ########## @@ -0,0 +1,449 @@ +/* + * 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.extension.auth.ranger; + +import static org.apache.polaris.core.entity.PolarisEntityConstants.getRootPrincipalName; + +import com.google.common.base.Preconditions; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.polaris.core.auth.AuthorizationDecision; +import org.apache.polaris.core.auth.AuthorizationRequest; +import org.apache.polaris.core.auth.AuthorizationState; +import org.apache.polaris.core.auth.PolarisAuthorizableOperation; +import org.apache.polaris.core.auth.PolarisAuthorizer; +import org.apache.polaris.core.auth.PolarisPrincipal; +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.persistence.PolarisResolvedPathWrapper; +import org.apache.polaris.extension.auth.ranger.utils.RangerUtils; +import org.apache.ranger.authz.api.RangerAuthorizer; +import org.apache.ranger.authz.api.RangerAuthzException; +import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer; +import org.apache.ranger.authz.model.RangerAccessContext; +import org.apache.ranger.authz.model.RangerAccessInfo; +import org.apache.ranger.authz.model.RangerAuthzResult; +import org.apache.ranger.authz.model.RangerMultiAuthzRequest; +import org.apache.ranger.authz.model.RangerMultiAuthzResult; +import org.apache.ranger.authz.model.RangerUserInfo; +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Authorizes operations based on policies defined in Apache Ranger. */ +public class RangerPolarisAuthorizer implements PolarisAuthorizer { + private static final Logger LOG = LoggerFactory.getLogger(RangerPolarisAuthorizer.class); + + public static final String SERVICE_TYPE = "polaris"; + public static final String SERVICE_NAME_PROPERTY = "ranger.plugin.polaris.service.name"; + + 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 ROOT_PRINCIPLE_NEEDED_ERROR = + "Principal '%s' is not authorized for op %s as only root principal can perform this operation"; + private static final String RANGER_AUTH_FAILED_ERROR = + "Principal '%s' is not authorized for op '%s'"; + private static final String RANGER_UNSUPPORTED_OPERATION = + "Operation %s is not supported by Ranger authorizer"; + + private static final Set<PolarisAuthorizableOperation> AUTHORIZED_OPERATIONS = + initAuthorizedOperations(); + + private final RealmConfig realmConfig; + private final RangerAuthorizer authorizer; + private final String serviceName; + + public RangerPolarisAuthorizer(RangerPolarisAuthorizerConfig config, RealmConfig realmConfig) { + LOG.info("Initializing RangerPolarisAuthorizer"); + + Properties rangerProp = RangerUtils.loadProperties(config.configFileName().get()); + + this.realmConfig = realmConfig; + this.authorizer = new RangerEmbeddedAuthorizer(rangerProp); + this.serviceName = rangerProp.getProperty(SERVICE_NAME_PROPERTY); + + try { + authorizer.init(); + } catch (RangerAuthzException t) { + LOG.error("Failed to initialize RangerPolarisAuthorizer", t); + throw new RuntimeException(t); + } + + LOG.info("RangerPolarisAuthorizer initialized successfully"); + } + + @Override + public void resolveAuthorizationInputs( + @NonNull AuthorizationState authzState, @NonNull AuthorizationRequest request) { + throw new UnsupportedOperationException( + "resolveAuthorizationInputs is not implemented yet for RangerPolarisAuthorizer"); + } + + @Override + public @NonNull AuthorizationDecision authorize( + @NonNull AuthorizationState authzState, @NonNull AuthorizationRequest request) { + throw new UnsupportedOperationException( + "authorize is not implemented yet for RangerPolarisAuthorizer"); + } + + @Override + public void authorizeOrThrow( + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull Set<PolarisBaseEntity> activatedEntities, + @Nonnull PolarisAuthorizableOperation authzOp, + @Nullable PolarisResolvedPathWrapper target, + @Nullable PolarisResolvedPathWrapper secondary) { + authorizeOrThrow( + polarisPrincipal, + activatedEntities, + authzOp, + target == null ? null : List.of(target), + secondary == null ? null : List.of(secondary)); + } + + @Override + public void authorizeOrThrow( + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull Set<PolarisBaseEntity> activatedEntities, + @Nonnull PolarisAuthorizableOperation authzOp, + @Nullable List<PolarisResolvedPathWrapper> targets, + @Nullable List<PolarisResolvedPathWrapper> secondaries) { + try { + if (authzOp == PolarisAuthorizableOperation.ROTATE_CREDENTIALS) { Review Comment: should this branch explicitly calls isAccessAuthorized? otherwise `ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING` is false, which is the default, any principal can rotate credentials regardless of Ranger policy I assume this is not a desired behavior - allowing credentials rotation w/o any Ranger policy check ########## extensions/auth/ranger/src/test/java/org/apache/polaris/extension/auth/ranger/TestRangerPolarisAuthorizer.java: ########## @@ -0,0 +1,278 @@ +/* + * 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.extension.auth.ranger; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.polaris.extension.auth.ranger.RangerTestUtils.createConfig; +import static org.apache.polaris.extension.auth.ranger.RangerTestUtils.createRealmConfig; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.polaris.core.auth.PolarisAuthorizableOperation; +import org.apache.polaris.core.auth.PolarisAuthorizer; +import org.apache.polaris.core.auth.PolarisPrincipal; +import org.apache.polaris.core.entity.PolarisEntity; +import org.apache.polaris.core.entity.PolarisEntityType; +import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; +import org.apache.polaris.core.persistence.ResolvedPolarisEntity; +import org.junit.jupiter.api.Test; + +public class TestRangerPolarisAuthorizer { + private static final String RESOURCE_TYPE_NAME_SEP = ":"; + private static final String RESOURCE_ELEMENTS_SEP = "/"; + + private final Gson gsonBuilder; Review Comment: 2 cents: using conventional Jackson would be helpful to minimize dep footprint ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java: ########## @@ -0,0 +1,449 @@ +/* + * 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.extension.auth.ranger; + +import static org.apache.polaris.core.entity.PolarisEntityConstants.getRootPrincipalName; + +import com.google.common.base.Preconditions; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.polaris.core.auth.AuthorizationDecision; +import org.apache.polaris.core.auth.AuthorizationRequest; +import org.apache.polaris.core.auth.AuthorizationState; +import org.apache.polaris.core.auth.PolarisAuthorizableOperation; +import org.apache.polaris.core.auth.PolarisAuthorizer; +import org.apache.polaris.core.auth.PolarisPrincipal; +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.persistence.PolarisResolvedPathWrapper; +import org.apache.polaris.extension.auth.ranger.utils.RangerUtils; +import org.apache.ranger.authz.api.RangerAuthorizer; +import org.apache.ranger.authz.api.RangerAuthzException; +import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer; +import org.apache.ranger.authz.model.RangerAccessContext; +import org.apache.ranger.authz.model.RangerAccessInfo; +import org.apache.ranger.authz.model.RangerAuthzResult; +import org.apache.ranger.authz.model.RangerMultiAuthzRequest; +import org.apache.ranger.authz.model.RangerMultiAuthzResult; +import org.apache.ranger.authz.model.RangerUserInfo; +import org.jspecify.annotations.NonNull; Review Comment: do we need both `import jakarta.annotation.Nonnull;` and `import org.jspecify.annotations.NonNull;`? ########## extensions/auth/ranger/src/test/java/org/apache/polaris/extension/auth/ranger/TestRangerPolarisAuthorizer.java: ########## @@ -0,0 +1,278 @@ +/* + * 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.extension.auth.ranger; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.polaris.extension.auth.ranger.RangerTestUtils.createConfig; +import static org.apache.polaris.extension.auth.ranger.RangerTestUtils.createRealmConfig; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.polaris.core.auth.PolarisAuthorizableOperation; +import org.apache.polaris.core.auth.PolarisAuthorizer; +import org.apache.polaris.core.auth.PolarisPrincipal; +import org.apache.polaris.core.entity.PolarisEntity; +import org.apache.polaris.core.entity.PolarisEntityType; +import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; +import org.apache.polaris.core.persistence.ResolvedPolarisEntity; +import org.junit.jupiter.api.Test; + +public class TestRangerPolarisAuthorizer { + private static final String RESOURCE_TYPE_NAME_SEP = ":"; + private static final String RESOURCE_ELEMENTS_SEP = "/"; + + private final Gson gsonBuilder; + private final PolarisAuthorizer authorizer; + + public TestRangerPolarisAuthorizer() throws Exception { + gsonBuilder = + new GsonBuilder() + .setDateFormat("yyyyMMdd-HH:mm:ss.SSSZ") + .setPrettyPrinting() + .registerTypeAdapter(PolarisPrincipal.class, new PolarisPrincipalDeserializer()) + .registerTypeAdapter( + PolarisResolvedPathWrapper.class, new PolarisResolvedPathWrapperDeserializer()) + .create(); + + RangerPolarisAuthorizerFactory factory = + new RangerPolarisAuthorizerFactory(createConfig("authz_tests/ranger-plugin.properties")); + + authorizer = factory.create(createRealmConfig()); + + assertNotNull(authorizer); + } + + @Test + public void testAuthzRoot() { + runTests(authorizer, "/authz_tests/tests_authz_root.json"); + } + + @Test + public void testAuthzCatalog() { + runTests(authorizer, "/authz_tests/tests_authz_catalog.json"); + } + + @Test + public void testAuthzPrincipal() { + runTests(authorizer, "/authz_tests/tests_authz_principal.json"); + } + + @Test + public void testAuthzNamespace() { + runTests(authorizer, "/authz_tests/tests_authz_namespace.json"); + } + + @Test + public void testAuthzTable() { + runTests(authorizer, "/authz_tests/tests_authz_table.json"); + } + + @Test + public void testAuthzPolicy() { + runTests(authorizer, "/authz_tests/tests_authz_policy.json"); + } + + @Test + public void testAuthzUnsupported() { + runTests(authorizer, "/authz_tests/tests_authz_unsupported.json"); + } + + private void runTests(PolarisAuthorizer authorizer, String testFilename) { + InputStream inStream = this.getClass().getResourceAsStream(testFilename); + InputStreamReader reader = new InputStreamReader(inStream, UTF_8); + + TestSuite testSuite = gsonBuilder.fromJson(reader, TestSuite.class); + + for (TestData test : testSuite.tests) { + try { + authorizer.authorizeOrThrow( + test.request.principal, + Collections.emptySet(), + test.request.authzOp, + test.request.target, + test.request.secondary); + + assertEquals( + test.result.isAllowed, + Boolean.TRUE, + () -> + test.request.principal + + " performed " + + test.request.authzOp + + " on (target: " + + test.request.target + + ", secondary: " + + test.request.secondary + + ")"); + } catch (Throwable t) { Review Comment: can we narrow it to something like `ForbiddenException` or `AssertionError` specifically? ########## extensions/auth/ranger/src/test/java/org/apache/polaris/extension/auth/ranger/TestRangerPolarisAuthorizerFactory.java: ########## @@ -0,0 +1,37 @@ +/* + * 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.extension.auth.ranger; + +import static org.apache.polaris.extension.auth.ranger.RangerTestUtils.createConfig; +import static org.apache.polaris.extension.auth.ranger.RangerTestUtils.createRealmConfig; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +public class TestRangerPolarisAuthorizerFactory { Review Comment: we should exercise @PostConstruct in TestRangerPolarisAuthorizerFactory by calling `initialize()` ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/utils/RangerUtils.java: ########## @@ -0,0 +1,249 @@ +/* + * 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.extension.auth.ranger.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.polaris.core.auth.PolarisAuthorizableOperation; +import org.apache.polaris.core.auth.PolarisPrincipal; +import org.apache.polaris.core.entity.PolarisEntity; +import org.apache.polaris.core.entity.PolarisEntityType; +import org.apache.polaris.core.entity.PolarisPrivilege; +import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; +import org.apache.polaris.core.persistence.ResolvedPolarisEntity; +import org.apache.polaris.extension.auth.ranger.RangerPolarisAuthorizer; +import org.apache.ranger.authz.model.RangerAccessInfo; +import org.apache.ranger.authz.model.RangerResourceInfo; +import org.apache.ranger.authz.model.RangerUserInfo; +import org.apache.ranger.authz.util.RangerResourceNameParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RangerUtils { + private static final Logger LOG = LoggerFactory.getLogger(RangerUtils.class); + + public static Properties loadProperties(String resourcePath) { + Properties prop = new Properties(); + + if (resourcePath != null) { + resourcePath = resourcePath.trim(); + + if (!resourcePath.startsWith("/")) { + LOG.info("Adding / to the configFileName [{}]", resourcePath); + + resourcePath = "/" + resourcePath; + } + + try (InputStream in = RangerPolarisAuthorizer.class.getResourceAsStream(resourcePath)) { + if (in != null) { + prop.load(in); + } else { + LOG.error("Unable to find ranger config file in the classpath : [{}]", resourcePath); + } + } catch (IOException e) { + LOG.error("Unable to load config file: [{}]", resourcePath, e); + } + } + + return prop; + } + + public static String toResourceType(PolarisEntityType entityType) { + return switch (entityType) { + case ROOT -> "root"; + case PRINCIPAL -> "principal"; + case CATALOG -> "catalog"; + case NAMESPACE -> "namespace"; + case TABLE_LIKE -> "table"; + case POLICY -> "policy"; + default -> entityType.name(); // NULL_TYPE, PRINCIPAL_ROLE, CATALOG_ROLE, TASK, FILE + }; + } + + public static String toAccessType(PolarisPrivilege privilege) { + return switch (privilege) { + case SERVICE_MANAGE_ACCESS -> "service-access-manage"; + + case PRINCIPAL_CREATE -> "principal-create"; + case PRINCIPAL_DROP -> "principal-drop"; + case PRINCIPAL_LIST -> "principal-list"; + case PRINCIPAL_READ_PROPERTIES -> "principal-properties-read"; + case PRINCIPAL_WRITE_PROPERTIES -> "principal-properties-write"; + case PRINCIPAL_FULL_METADATA -> "principal-metadata-full"; + case PRINCIPAL_ROTATE_CREDENTIALS -> "principal-credentials-rotate"; + case PRINCIPAL_RESET_CREDENTIALS -> "principal-credentials-reset"; + + case CATALOG_CREATE -> "catalog-create"; + case CATALOG_DROP -> "catalog-drop"; + case CATALOG_LIST -> "catalog-list"; + case CATALOG_READ_PROPERTIES -> "catalog-properties-read"; + case CATALOG_WRITE_PROPERTIES -> "catalog-properties-write"; + case CATALOG_FULL_METADATA -> "catalog-metadata-full"; + case CATALOG_MANAGE_METADATA -> "catalog-metadata-manage"; + case CATALOG_MANAGE_CONTENT -> "catalog-content-manage"; + case CATALOG_ATTACH_POLICY -> "catalog-policy-attach"; + case CATALOG_DETACH_POLICY -> "catalog-policy-detach"; + + case NAMESPACE_CREATE -> "namespace-create"; + case NAMESPACE_DROP -> "namespace-drop"; + case NAMESPACE_LIST -> "namespace-list"; + case NAMESPACE_READ_PROPERTIES -> "namespace-properties-read"; + case NAMESPACE_WRITE_PROPERTIES -> "namespace-properties-write"; + case NAMESPACE_FULL_METADATA -> "namespace-metadata-full"; + case NAMESPACE_ATTACH_POLICY -> "namespace-policy-attach"; + case NAMESPACE_DETACH_POLICY -> "namespace-policy-detach"; + + case TABLE_CREATE -> "table-create"; + case TABLE_DROP -> "table-drop"; + case TABLE_LIST -> "table-list"; + case TABLE_READ_PROPERTIES -> "table-properties-read"; + case TABLE_WRITE_PROPERTIES -> "table-properties-write"; + case TABLE_READ_DATA -> "table-data-read"; + case TABLE_WRITE_DATA -> "table-data-write"; + case TABLE_FULL_METADATA -> "table-metadata-full"; + case TABLE_ATTACH_POLICY -> "table-policy-attach"; + case TABLE_DETACH_POLICY -> "table-policy-detach"; + case TABLE_ASSIGN_UUID -> "table-uuid-assign"; + case TABLE_UPGRADE_FORMAT_VERSION -> "table-format-version-upgrade"; + case TABLE_ADD_SCHEMA -> "table-schema-add"; + case TABLE_SET_CURRENT_SCHEMA -> "table-schema-set-current"; + case TABLE_ADD_PARTITION_SPEC -> "table-partition-spec-add"; + case TABLE_ADD_SORT_ORDER -> "table-sort-order-add"; + case TABLE_SET_DEFAULT_SORT_ORDER -> "table-sort-order-set-default"; + case TABLE_ADD_SNAPSHOT -> "table-snapshot-add"; + case TABLE_SET_SNAPSHOT_REF -> "table-snapshot-ref-set"; + case TABLE_REMOVE_SNAPSHOTS -> "table-snapshots-remove"; + case TABLE_REMOVE_SNAPSHOT_REF -> "table-snapshot-ref-remove"; + case TABLE_SET_LOCATION -> "table-location-set"; + case TABLE_SET_PROPERTIES -> "table-properties-set"; + case TABLE_REMOVE_PROPERTIES -> "table-properties-remove"; + case TABLE_SET_STATISTICS -> "table-statistics-set"; + case TABLE_REMOVE_STATISTICS -> "table-statistics-remove"; + case TABLE_REMOVE_PARTITION_SPECS -> "table-partition-specs-remove"; + case TABLE_MANAGE_STRUCTURE -> "table-structure-manage"; + + case VIEW_CREATE -> "view-create"; + case VIEW_DROP -> "view-drop"; + case VIEW_LIST -> "view-list"; + case VIEW_READ_PROPERTIES -> "view-properties-read"; + case VIEW_WRITE_PROPERTIES -> "view-properties-write"; + case VIEW_FULL_METADATA -> "view-metadata-full"; + + case POLICY_CREATE -> "policy-create"; + case POLICY_READ -> "policy-read"; + case POLICY_DROP -> "policy-drop"; + case POLICY_WRITE -> "policy-write"; + case POLICY_LIST -> "policy-list"; + case POLICY_FULL_METADATA -> "policy-metadata-full"; + case POLICY_ATTACH -> "policy-attach"; + case POLICY_DETACH -> "policy-detach"; + + default -> privilege.name(); + }; + } + + public static RangerUserInfo toUserInfo(PolarisPrincipal principal) { + return new RangerUserInfo( + principal.getName(), getUserAttributes(principal), null, principal.getRoles()); + } + + public static RangerAccessInfo toAccessInfo( + PolarisResolvedPathWrapper entity, + PolarisAuthorizableOperation authzOp, + EnumSet<PolarisPrivilege> privileges) { + return new RangerAccessInfo( + RangerUtils.toResourceInfo(entity), authzOp.name(), RangerUtils.toPermissions(privileges)); + } + + public static String toResourcePath(List<PolarisResolvedPathWrapper> resolvedPaths) { + return resolvedPaths.stream().map(RangerUtils::toResourcePath).collect(Collectors.joining(",")); + } + + public static String toResourcePath(PolarisResolvedPathWrapper resolvedPath) { + StringBuilder sb = new StringBuilder(); + String resourceType = + toResourceType(resolvedPath.getResolvedLeafEntity().getEntity().getType()); + + sb.append(resourceType).append(RangerResourceNameParser.RRN_RESOURCE_TYPE_SEP); + + boolean isFirst = true; + + for (ResolvedPolarisEntity entity : resolvedPath.getResolvedFullPath()) { + if (!isFirst) { + sb.append(RangerResourceNameParser.DEFAULT_RRN_RESOURCE_SEP); + } else { + isFirst = false; + } + + sb.append(entity.getEntity().getName()); + } + + return sb.toString(); + } + + private static RangerResourceInfo toResourceInfo(PolarisResolvedPathWrapper resourcePath) { + RangerResourceInfo ret = new RangerResourceInfo(); + + ret.setName(toResourcePath(resourcePath)); + ret.setAttributes(getResourceAttributes(resourcePath)); + + return ret; + } + + private static Set<String> toPermissions(EnumSet<PolarisPrivilege> privileges) { + return privileges.stream().map(RangerUtils::toAccessType).collect(Collectors.toSet()); + } + + private static Map<String, Object> getResourceAttributes( + PolarisResolvedPathWrapper resourcePath) { + Map<String, Object> ret = null; + + for (ResolvedPolarisEntity resolvedEntity : resourcePath.getResolvedFullPath()) { + PolarisEntity entity = resolvedEntity.getEntity(); + + if (StringUtils.isNotBlank(entity.getProperties())) { + if (ret == null) { + ret = new HashMap<>(entity.getPropertiesAsMap()); + } else { + ret.putAll(entity.getPropertiesAsMap()); Review Comment: curious - if silently overriding parent properties is intentional or not? could you elaborate the rationale? ########## extensions/auth/ranger/build.gradle.kts: ########## @@ -0,0 +1,55 @@ +/* + * 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. + */ + +plugins { + id("polaris-server") + id("org.kordamp.gradle.jandex") +} + +dependencies { + implementation(project(":polaris-core")) + + implementation(fileTree("override-libs") { include("*.jar") }) + + implementation("org.apache.ranger:ranger-plugins-common:2.8.0") + + implementation("org.apache.ranger:authz-embedded:2.8.0") + + // Iceberg dependency for ForbiddenException + implementation(platform(libs.iceberg.bom)) + implementation("org.apache.iceberg:iceberg-api") + implementation(project(":polaris-async-api")) + implementation(libs.guava) + + compileOnly(project(":polaris-immutables")) + compileOnly(libs.jakarta.annotation.api) + compileOnly(libs.jakarta.enterprise.cdi.api) + compileOnly(libs.jakarta.inject.api) + compileOnly(libs.smallrye.config.core) + + implementation("org.apache.commons:commons-lang3:3.19.0") + runtimeOnly("org.apache.commons:commons-configuration2:2.10.1") + runtimeOnly("org.apache.commons:commons-text") + runtimeOnly("commons-collections:commons-collections:3.2.2") Review Comment: pinned version is not consistent with the project conventions -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
