flyrain commented on code in PR #3928: URL: https://github.com/apache/polaris/pull/3928#discussion_r2921553905
########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java: ########## @@ -0,0 +1,432 @@ +/* + * 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.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.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.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"; + + 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_PRINCIPAL_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 RangerEmbeddedAuthorizer authorizer; + private final String serviceName; + private final boolean enforceCredentialRotationRequiredState; + + public RangerPolarisAuthorizer( + RangerEmbeddedAuthorizer authorizer, String serviceName, RealmConfig realmConfig) { + this.authorizer = authorizer; + this.serviceName = serviceName; + this.enforceCredentialRotationRequiredState = + realmConfig.getConfig( + FeatureConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING); + } + + @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 (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()); + } + + 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_PRINCIPAL_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; Review Comment: Same here ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/utils/RangerUtils.java: ########## @@ -0,0 +1,255 @@ +/* + * 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) { + if (resourcePath == null) { + throw new IllegalStateException("invalid resource path: null"); + } + + resourcePath = resourcePath.trim(); + + if (!resourcePath.startsWith("/")) { + LOG.info("Prefixing / to resource path: {}", resourcePath); + + resourcePath = "/" + resourcePath; + } + + try (InputStream in = RangerPolarisAuthorizer.class.getResourceAsStream(resourcePath)) { + if (in != null) { + Properties prop = new Properties(); + + prop.load(in); + + return prop; + } else { + LOG.error("Unable to find {} in the classpath", resourcePath); + + throw new IllegalStateException("failed to find " + resourcePath); + } + } catch (IOException e) { + LOG.error("Unable to load {}", resourcePath, e); + + throw new IllegalStateException("failed to load " + resourcePath); + } + } + + 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) { Review Comment: Do we need `PolarisPrivilege` as a bridge to route between operation and ranger privileges? Can we map `PolarisAuthorizableOperation` directly to ranger privileges? In that case, the ranger privileges are completely decoupled with `PolarisPrivilege`, which avoid any sync between them. Ranger should only care about the `PolarisAuthorizableOperation`. ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizerFactory.java: ########## @@ -0,0 +1,99 @@ +/* + * 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.RangerPolarisAuthorizerConfig.PROP_POLARIS_SERVICE_NAME; + +import io.smallrye.common.annotation.Identifier; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.util.Properties; +import org.apache.commons.lang3.StringUtils; +import org.apache.polaris.core.auth.PolarisAuthorizerFactory; +import org.apache.polaris.core.config.RealmConfig; +import org.apache.polaris.extension.auth.ranger.utils.RangerUtils; +import org.apache.ranger.authz.api.RangerAuthzException; +import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer; +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 static final String ERR_AUTHORIZER_FACTORY_NOT_INITIALIZED = + "Ranger authorizer factory was not initialized successfully"; + + private final RangerPolarisAuthorizerConfig config; + private RangerEmbeddedAuthorizer authorizer; + private String serviceName; + + @Inject + RangerPolarisAuthorizerFactory(RangerPolarisAuthorizerConfig config) { + this.config = config; + + LOG.debug("RangerPolarisAuthorizerFactory has been activated."); + } + + @PostConstruct + public void initialize() { + LOG.info("Initializing RangerAuthorizer"); + + config.validate(); + + try { + Properties rangerProp = RangerUtils.loadProperties(config.configFileName().get()); + + RangerEmbeddedAuthorizer authorizer = new RangerEmbeddedAuthorizer(rangerProp); + + authorizer.init(); + + this.authorizer = authorizer; + this.serviceName = rangerProp.getProperty(PROP_POLARIS_SERVICE_NAME); + } catch (RangerAuthzException t) { + LOG.error("Failed to initialize RangerPolarisAuthorizer", t); + throw new RuntimeException(t); Review Comment: Same here ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java: ########## @@ -0,0 +1,432 @@ +/* + * 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.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.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.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"; + + 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_PRINCIPAL_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 RangerEmbeddedAuthorizer authorizer; + private final String serviceName; + private final boolean enforceCredentialRotationRequiredState; + + public RangerPolarisAuthorizer( + RangerEmbeddedAuthorizer authorizer, String serviceName, RealmConfig realmConfig) { + this.authorizer = authorizer; + this.serviceName = serviceName; + this.enforceCredentialRotationRequiredState = + realmConfig.getConfig( + FeatureConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING); + } + + @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 (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()); + } + + 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_PRINCIPAL_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: user={}, properties={}, groups={}", + polarisPrincipal.getName(), + polarisPrincipal.getProperties(), + String.join(",", polarisPrincipal.getRoles())); + + LOG.debug( + "isAuthorized: activatedEntities={}", + activatedEntities.stream() + .map(e -> RangerUtils.toResourceType(e.getType()) + ":" + e.getName()) + .collect(Collectors.joining(","))); + + LOG.debug("isAuthorized: authzOp={}", authzOp.name()); + + LOG.debug( + "isAuthorized: permissions={}", + authzOp.getPrivilegesOnTarget().stream() + .map(RangerUtils::toAccessType) + .collect(Collectors.joining(","))); + + if (targets != null) { + LOG.debug( + "isAuthorized: targets={}", + targets.stream().map(RangerUtils::toResourcePath).collect(Collectors.joining(","))); + } + + if (secondaries != null) { + LOG.debug( + "isAuthorized: secondaries={}", + secondaries.stream().map(RangerUtils::toResourcePath).collect(Collectors.joining(","))); + } + } + + return isAccessAuthorized(polarisPrincipal, authzOp, targets, secondaries); + } + + private boolean isAccessAuthorized( + @Nonnull PolarisPrincipal principal, + @Nonnull PolarisAuthorizableOperation authzOp, + @Nullable List<PolarisResolvedPathWrapper> targets, + @Nullable List<PolarisResolvedPathWrapper> secondaries) + throws RangerAuthzException { + boolean isTargetSpecified = targets != null && !targets.isEmpty(); + boolean isSecondarySpecified = secondaries != null && !secondaries.isEmpty(); + List<RangerAccessInfo> accessInfos = new ArrayList<>(); + + if (!authzOp.getPrivilegesOnTarget().isEmpty()) { + Preconditions.checkState( + isTargetSpecified, + "No target provided to authorize %s for privileges %s", + authzOp, + authzOp.getPrivilegesOnTarget()); + + for (PolarisResolvedPathWrapper target : targets) { + accessInfos.add(RangerUtils.toAccessInfo(target, authzOp, authzOp.getPrivilegesOnTarget())); + } + } else if (isTargetSpecified) { + LOG.warn( + "No privileges specified for target authorization. Ignoring target {}, op: {}, user: {}", + RangerUtils.toResourcePath(targets), + authzOp.name(), + principal.getName()); + } + + if (!authzOp.getPrivilegesOnSecondary().isEmpty()) { + Preconditions.checkState( + isSecondarySpecified, + "No secondaries provided to authorize %s for privileges %s", + authzOp, + authzOp.getPrivilegesOnSecondary()); + + for (PolarisResolvedPathWrapper secondary : secondaries) { + accessInfos.add( + RangerUtils.toAccessInfo(secondary, authzOp, authzOp.getPrivilegesOnSecondary())); + } + } else if (isSecondarySpecified) { + LOG.warn( + "No privileges specified for secondary authorization. Ignoring secondaries {}, op: {}, user: {}", + RangerUtils.toResourcePath(secondaries), + authzOp.name(), + principal.getName()); + } + + RangerUserInfo userInfo = RangerUtils.toUserInfo(principal); + RangerAccessContext context = new RangerAccessContext(SERVICE_TYPE, serviceName); + RangerMultiAuthzRequest authzRequest = + new RangerMultiAuthzRequest(userInfo, accessInfos, context); + RangerMultiAuthzResult authzResult = authorizer.authorize(authzRequest); + boolean isAllowed = RangerAuthzResult.AccessDecision.ALLOW.equals(authzResult.getDecision()); + + if (!isAllowed && LOG.isDebugEnabled()) { + for (int i = 0; i < accessInfos.size(); i++) { + RangerAccessInfo accessInfo = accessInfos.get(i); + RangerAuthzResult accessResult = authzResult.getAccesses().get(i); + + if (!RangerAuthzResult.AccessDecision.ALLOW.equals(accessResult.getDecision())) { + LOG.debug( + "User {} is not authorized for {} on {}", + userInfo.getName(), + authzOp, + accessInfo.getResource()); + } + } + } + + return isAllowed; + } + + private static Set<PolarisAuthorizableOperation> initAuthorizedOperations() { + Set<PolarisAuthorizableOperation> ret = new HashSet<>(); + + for (PolarisAuthorizableOperation op : PolarisAuthorizableOperation.values()) { + if (isAuthorizable(op)) { + ret.add(op); + } + } + + return ret; + } + + private static boolean isAuthorizable(PolarisAuthorizableOperation op) { + switch (op) { + case CREATE_PRINCIPAL: + case DELETE_PRINCIPAL: + case UPDATE_PRINCIPAL: + case GET_PRINCIPAL: + case LIST_PRINCIPALS: + case ROTATE_CREDENTIALS: + case RESET_CREDENTIALS: + return true; + + case CREATE_CATALOG: + case DELETE_CATALOG: + case UPDATE_CATALOG: + case GET_CATALOG: + case LIST_CATALOGS: + case ATTACH_POLICY_TO_CATALOG: + case DETACH_POLICY_FROM_CATALOG: + case GET_APPLICABLE_POLICIES_ON_CATALOG: + return true; + + case CREATE_NAMESPACE: + case DROP_NAMESPACE: + case UPDATE_NAMESPACE_PROPERTIES: + case LIST_NAMESPACES: + case NAMESPACE_EXISTS: + case LOAD_NAMESPACE_METADATA: + case ATTACH_POLICY_TO_NAMESPACE: + case DETACH_POLICY_FROM_NAMESPACE: + case GET_APPLICABLE_POLICIES_ON_NAMESPACE: + return true; + + case CREATE_TABLE_DIRECT: + case CREATE_TABLE_DIRECT_WITH_WRITE_DELEGATION: + case CREATE_TABLE_STAGED: + case CREATE_TABLE_STAGED_WITH_WRITE_DELEGATION: + case REGISTER_TABLE: + case DROP_TABLE_WITHOUT_PURGE: + case DROP_TABLE_WITH_PURGE: + case UPDATE_TABLE: + case UPDATE_TABLE_FOR_STAGED_CREATE: + case RENAME_TABLE: + case LIST_TABLES: + case TABLE_EXISTS: + case LOAD_TABLE: + case LOAD_TABLE_WITH_READ_DELEGATION: + case LOAD_TABLE_WITH_WRITE_DELEGATION: + case COMMIT_TRANSACTION: + case ATTACH_POLICY_TO_TABLE: + case DETACH_POLICY_FROM_TABLE: + case GET_APPLICABLE_POLICIES_ON_TABLE: + case REPORT_READ_METRICS: + case REPORT_WRITE_METRICS: + case ASSIGN_TABLE_UUID: + case UPGRADE_TABLE_FORMAT_VERSION: + case ADD_TABLE_SCHEMA: + case SET_TABLE_CURRENT_SCHEMA: + case ADD_TABLE_PARTITION_SPEC: + case ADD_TABLE_SORT_ORDER: + case SET_TABLE_DEFAULT_SORT_ORDER: + case ADD_TABLE_SNAPSHOT: + case SET_TABLE_SNAPSHOT_REF: + case REMOVE_TABLE_SNAPSHOTS: + case REMOVE_TABLE_SNAPSHOT_REF: + case SET_TABLE_LOCATION: + case SET_TABLE_PROPERTIES: + case REMOVE_TABLE_PROPERTIES: + case SET_TABLE_STATISTICS: + case REMOVE_TABLE_STATISTICS: + case REMOVE_TABLE_PARTITION_SPECS: + return true; + + case CREATE_VIEW: + case DROP_VIEW: + case REPLACE_VIEW: + case RENAME_VIEW: + case LIST_VIEWS: + case VIEW_EXISTS: + case LOAD_VIEW: + return true; + + case CREATE_POLICY: + case DROP_POLICY: + case UPDATE_POLICY: + case LIST_POLICY: + case LOAD_POLICY: + return true; + + case SEND_NOTIFICATIONS: + return true; + + case CREATE_PRINCIPAL_ROLE: + case DELETE_PRINCIPAL_ROLE: + case UPDATE_PRINCIPAL_ROLE: + case GET_PRINCIPAL_ROLE: + case LIST_PRINCIPAL_ROLES: + case ASSIGN_PRINCIPAL_ROLE: + case REVOKE_PRINCIPAL_ROLE: + case LIST_PRINCIPAL_ROLES_ASSIGNED: + case LIST_ASSIGNEE_PRINCIPALS_FOR_PRINCIPAL_ROLE: + case ADD_PRINCIPAL_GRANT_TO_PRINCIPAL_ROLE: + case REVOKE_PRINCIPAL_GRANT_FROM_PRINCIPAL_ROLE: + case LIST_GRANTS_ON_PRINCIPAL: + case ADD_PRINCIPAL_ROLE_GRANT_TO_PRINCIPAL_ROLE: + case REVOKE_PRINCIPAL_ROLE_GRANT_FROM_PRINCIPAL_ROLE: + case LIST_GRANTS_ON_PRINCIPAL_ROLE: + case ADD_ROOT_GRANT_TO_PRINCIPAL_ROLE: + case REVOKE_ROOT_GRANT_FROM_PRINCIPAL_ROLE: + case LIST_GRANTS_ON_ROOT: + return false; + + case CREATE_CATALOG_ROLE: + case DELETE_CATALOG_ROLE: + case UPDATE_CATALOG_ROLE: + case GET_CATALOG_ROLE: + case LIST_CATALOG_ROLES: + case ASSIGN_CATALOG_ROLE_TO_PRINCIPAL_ROLE: + case REVOKE_CATALOG_ROLE_FROM_PRINCIPAL_ROLE: + case LIST_CATALOG_ROLES_FOR_PRINCIPAL_ROLE: + case LIST_ASSIGNEE_PRINCIPAL_ROLES_FOR_CATALOG_ROLE: + case LIST_GRANTS_FOR_CATALOG_ROLE: + case ADD_CATALOG_ROLE_GRANT_TO_CATALOG_ROLE: + case REVOKE_CATALOG_ROLE_GRANT_FROM_CATALOG_ROLE: + case ADD_NAMESPACE_GRANT_TO_CATALOG_ROLE: + case ADD_CATALOG_GRANT_TO_CATALOG_ROLE: + case ADD_TABLE_GRANT_TO_CATALOG_ROLE: + case ADD_VIEW_GRANT_TO_CATALOG_ROLE: + case ADD_POLICY_GRANT_TO_CATALOG_ROLE: + case REVOKE_NAMESPACE_GRANT_FROM_CATALOG_ROLE: + case REVOKE_CATALOG_GRANT_FROM_CATALOG_ROLE: + case REVOKE_TABLE_GRANT_FROM_CATALOG_ROLE: + case REVOKE_VIEW_GRANT_FROM_CATALOG_ROLE: + case REVOKE_POLICY_GRANT_FROM_CATALOG_ROLE: + case LIST_GRANTS_ON_CATALOG_ROLE: + return false; + + case LIST_GRANTS_ON_CATALOG: + case LIST_GRANTS_ON_NAMESPACE: + case LIST_GRANTS_ON_TABLE: + case LIST_GRANTS_ON_VIEW: + return false; Review Comment: I'm a bit concerned that every time we add/update/remove an op enum, we need to make changes here as well. What do we do here is basically to exclude all ops related to RBAC. Is it possible we introduced a op type like `rbac` or `native rbac` to indicate these ops are tied to the native RBAC only? cc @sungwy @collado-mike ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java: ########## @@ -0,0 +1,432 @@ +/* + * 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.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.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.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"; + + 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_PRINCIPAL_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 RangerEmbeddedAuthorizer authorizer; + private final String serviceName; + private final boolean enforceCredentialRotationRequiredState; + + public RangerPolarisAuthorizer( + RangerEmbeddedAuthorizer authorizer, String serviceName, RealmConfig realmConfig) { + this.authorizer = authorizer; + this.serviceName = serviceName; + this.enforceCredentialRotationRequiredState = + realmConfig.getConfig( + FeatureConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING); + } + + @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 (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()); + } + + 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_PRINCIPAL_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()) { Review Comment: It looks like there are about 6 debug level log statements here. Would it be possible to consolidate them into a single debug log message instead? That might make the logs easier to read and won't split when other logs happen at the same time. ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java: ########## @@ -0,0 +1,432 @@ +/* + * 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.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.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.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"; + + 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_PRINCIPAL_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 RangerEmbeddedAuthorizer authorizer; + private final String serviceName; + private final boolean enforceCredentialRotationRequiredState; + + public RangerPolarisAuthorizer( + RangerEmbeddedAuthorizer authorizer, String serviceName, RealmConfig realmConfig) { + this.authorizer = authorizer; + this.serviceName = serviceName; + this.enforceCredentialRotationRequiredState = + realmConfig.getConfig( + FeatureConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING); + } + + @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 (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()); + } + + 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_PRINCIPAL_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: user={}, properties={}, groups={}", + polarisPrincipal.getName(), + polarisPrincipal.getProperties(), + String.join(",", polarisPrincipal.getRoles())); + + LOG.debug( + "isAuthorized: activatedEntities={}", + activatedEntities.stream() + .map(e -> RangerUtils.toResourceType(e.getType()) + ":" + e.getName()) + .collect(Collectors.joining(","))); + + LOG.debug("isAuthorized: authzOp={}", authzOp.name()); + + LOG.debug( + "isAuthorized: permissions={}", + authzOp.getPrivilegesOnTarget().stream() + .map(RangerUtils::toAccessType) + .collect(Collectors.joining(","))); + + if (targets != null) { + LOG.debug( + "isAuthorized: targets={}", + targets.stream().map(RangerUtils::toResourcePath).collect(Collectors.joining(","))); + } + + if (secondaries != null) { + LOG.debug( + "isAuthorized: secondaries={}", + secondaries.stream().map(RangerUtils::toResourcePath).collect(Collectors.joining(","))); + } + } + + return isAccessAuthorized(polarisPrincipal, authzOp, targets, secondaries); + } + + private boolean isAccessAuthorized( + @Nonnull PolarisPrincipal principal, + @Nonnull PolarisAuthorizableOperation authzOp, + @Nullable List<PolarisResolvedPathWrapper> targets, + @Nullable List<PolarisResolvedPathWrapper> secondaries) + throws RangerAuthzException { + boolean isTargetSpecified = targets != null && !targets.isEmpty(); + boolean isSecondarySpecified = secondaries != null && !secondaries.isEmpty(); + List<RangerAccessInfo> accessInfos = new ArrayList<>(); + + if (!authzOp.getPrivilegesOnTarget().isEmpty()) { + Preconditions.checkState( + isTargetSpecified, + "No target provided to authorize %s for privileges %s", + authzOp, + authzOp.getPrivilegesOnTarget()); + + for (PolarisResolvedPathWrapper target : targets) { + accessInfos.add(RangerUtils.toAccessInfo(target, authzOp, authzOp.getPrivilegesOnTarget())); + } + } else if (isTargetSpecified) { + LOG.warn( + "No privileges specified for target authorization. Ignoring target {}, op: {}, user: {}", + RangerUtils.toResourcePath(targets), + authzOp.name(), + principal.getName()); + } + + if (!authzOp.getPrivilegesOnSecondary().isEmpty()) { + Preconditions.checkState( + isSecondarySpecified, + "No secondaries provided to authorize %s for privileges %s", + authzOp, + authzOp.getPrivilegesOnSecondary()); + + for (PolarisResolvedPathWrapper secondary : secondaries) { + accessInfos.add( + RangerUtils.toAccessInfo(secondary, authzOp, authzOp.getPrivilegesOnSecondary())); + } + } else if (isSecondarySpecified) { + LOG.warn( + "No privileges specified for secondary authorization. Ignoring secondaries {}, op: {}, user: {}", + RangerUtils.toResourcePath(secondaries), + authzOp.name(), + principal.getName()); + } + + RangerUserInfo userInfo = RangerUtils.toUserInfo(principal); + RangerAccessContext context = new RangerAccessContext(SERVICE_TYPE, serviceName); + RangerMultiAuthzRequest authzRequest = + new RangerMultiAuthzRequest(userInfo, accessInfos, context); + RangerMultiAuthzResult authzResult = authorizer.authorize(authzRequest); + boolean isAllowed = RangerAuthzResult.AccessDecision.ALLOW.equals(authzResult.getDecision()); + + if (!isAllowed && LOG.isDebugEnabled()) { + for (int i = 0; i < accessInfos.size(); i++) { + RangerAccessInfo accessInfo = accessInfos.get(i); + RangerAuthzResult accessResult = authzResult.getAccesses().get(i); + + if (!RangerAuthzResult.AccessDecision.ALLOW.equals(accessResult.getDecision())) { + LOG.debug( + "User {} is not authorized for {} on {}", + userInfo.getName(), + authzOp, + accessInfo.getResource()); Review Comment: I think we can group them in a single debug message instead of emitting multiple ones in case of multiple targets and secondaries. The split way is even more confusing as the operation will apply to both target and secondary, not either. ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java: ########## @@ -0,0 +1,432 @@ +/* + * 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.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.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.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"; + + 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_PRINCIPAL_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 RangerEmbeddedAuthorizer authorizer; + private final String serviceName; + private final boolean enforceCredentialRotationRequiredState; + + public RangerPolarisAuthorizer( + RangerEmbeddedAuthorizer authorizer, String serviceName, RealmConfig realmConfig) { + this.authorizer = authorizer; + this.serviceName = serviceName; + this.enforceCredentialRotationRequiredState = + realmConfig.getConfig( + FeatureConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING); + } + + @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 (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()); + } + + 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_PRINCIPAL_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); Review Comment: Do we need to log an error given we throw right after it? Can we put the error message into the throw statement? ```suggestion throw new IllegalStateException(errorMsg, excp); ``` ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java: ########## @@ -0,0 +1,432 @@ +/* + * 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.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.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.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"; + + 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_PRINCIPAL_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 RangerEmbeddedAuthorizer authorizer; + private final String serviceName; + private final boolean enforceCredentialRotationRequiredState; + + public RangerPolarisAuthorizer( + RangerEmbeddedAuthorizer authorizer, String serviceName, RealmConfig realmConfig) { + this.authorizer = authorizer; + this.serviceName = serviceName; + this.enforceCredentialRotationRequiredState = + realmConfig.getConfig( + FeatureConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING); + } + + @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 (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()); + } + + 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_PRINCIPAL_NEEDED_ERROR, polarisPrincipal.getName(), authzOp.name()); + } Review Comment: Looks like we copy the logic from the default authorizer, I think it is the right behavior. Can we move it to a util class so that both default authorizer and ranger authorizer can reuse it? I'm also curious how OPA remote server handle this? cc @sungwy ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizerFactory.java: ########## @@ -0,0 +1,99 @@ +/* + * 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.RangerPolarisAuthorizerConfig.PROP_POLARIS_SERVICE_NAME; + +import io.smallrye.common.annotation.Identifier; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.util.Properties; +import org.apache.commons.lang3.StringUtils; +import org.apache.polaris.core.auth.PolarisAuthorizerFactory; +import org.apache.polaris.core.config.RealmConfig; +import org.apache.polaris.extension.auth.ranger.utils.RangerUtils; +import org.apache.ranger.authz.api.RangerAuthzException; +import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer; +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 static final String ERR_AUTHORIZER_FACTORY_NOT_INITIALIZED = + "Ranger authorizer factory was not initialized successfully"; + + private final RangerPolarisAuthorizerConfig config; + private RangerEmbeddedAuthorizer authorizer; + private String serviceName; + + @Inject + RangerPolarisAuthorizerFactory(RangerPolarisAuthorizerConfig config) { + this.config = config; + + LOG.debug("RangerPolarisAuthorizerFactory has been activated."); + } + + @PostConstruct + public void initialize() { + LOG.info("Initializing RangerAuthorizer"); + + config.validate(); + + try { + Properties rangerProp = RangerUtils.loadProperties(config.configFileName().get()); + + RangerEmbeddedAuthorizer authorizer = new RangerEmbeddedAuthorizer(rangerProp); + + authorizer.init(); + + this.authorizer = authorizer; + this.serviceName = rangerProp.getProperty(PROP_POLARIS_SERVICE_NAME); + } catch (RangerAuthzException t) { + LOG.error("Failed to initialize RangerPolarisAuthorizer", t); + throw new RuntimeException(t); + } + + LOG.info("RangerAuthorizer initialized successfully"); + } + + @PreDestroy + public void cleanup() {} + + @Override + public RangerPolarisAuthorizer create(RealmConfig realmConfig) { + LOG.debug("Creating RangerPolarisAuthorizer"); + + if (authorizer == null || StringUtils.isBlank(serviceName)) { + throw new IllegalStateException(ERR_AUTHORIZER_FACTORY_NOT_INITIALIZED); + } + + try { + return new RangerPolarisAuthorizer(authorizer, serviceName, realmConfig); + } catch (Throwable t) { + LOG.error("Failed to create RangerPolarisAuthorizer", t); + + throw t; Review Comment: Same here -- 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]
