adutra commented on code in PR #3928: URL: https://github.com/apache/polaris/pull/3928#discussion_r2930945415
########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizerConfig.java: ########## @@ -0,0 +1,72 @@ +/* + * 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.config.ConfigMapping; +import java.util.Optional; +import java.util.Properties; +import org.apache.commons.lang3.StringUtils; +import org.apache.polaris.extension.auth.ranger.utils.RangerUtils; +import org.apache.polaris.immutables.PolarisImmutable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@PolarisImmutable Review Comment: ```suggestion ``` ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizerConfig.java: ########## @@ -0,0 +1,72 @@ +/* + * 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.config.ConfigMapping; +import java.util.Optional; +import java.util.Properties; +import org.apache.commons.lang3.StringUtils; +import org.apache.polaris.extension.auth.ranger.utils.RangerUtils; +import org.apache.polaris.immutables.PolarisImmutable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@PolarisImmutable +@ConfigMapping(prefix = "polaris.authorization.ranger") +public interface RangerPolarisAuthorizerConfig { + Logger LOG = LoggerFactory.getLogger(RangerPolarisAuthorizerConfig.class); + + String PROP_RANGER_CONFIG_FILE_NAME = "polaris.authorization.ranger.config-file-name"; + String PROP_POLARIS_SERVICE_NAME = "ranger.plugin.polaris.service.name"; + + String ERR_INVALID_AUTHORIZER_CONFIG_MISSING_CONFIG_FILE_NAME = + "Invalid Ranger authorizer configuration: missing configuration " + + PROP_RANGER_CONFIG_FILE_NAME; + String ERR_INVALID_CONFIG_FILE_MISSING_CONFIGURATION = + "Invalid Ranger authorizer configuration file %s: missing mandatory configuration %s"; + + Optional<String> configFileName(); + + default void validate() { + boolean isConfigFileNamePresent = configFileName().isPresent(); + + if (!isConfigFileNamePresent) { + LOG.error(ERR_INVALID_AUTHORIZER_CONFIG_MISSING_CONFIG_FILE_NAME); + + throw new IllegalStateException(ERR_INVALID_AUTHORIZER_CONFIG_MISSING_CONFIG_FILE_NAME); + } + + String fileName = configFileName().get(); + + Properties prop = RangerUtils.loadProperties(fileName); Review Comment: I think this has been voiced already, but this doesn't look very "Quarkus-friendly". Why not include all the required configuration in Quarkus default configuration files? There are a few ways of achieving this: ### Using pure Quarkus Just expose the relevant settings in Quarkus: ```java @ConfigMapping(prefix = "polaris.authorization.ranger") public interface RangerPolarisAuthorizerConfig { String serviceName(); // ranger.plugin.polaris.service.name Optional<String> clusterName(); // ranger.authz.default.access.cluster.name // etc. // You can also create inner interfaces to group things together: RestClientConfig restClient(); AuthenticationConfig auth(); AuditConfig audit(); // Convert to Properties for RangerEmbeddedAuthorizer default Properties toProperties() { ... } } ``` Then you can configure the authorizer very simply: ```properties polaris.authorization.ranger.service-name=dev_polaris polaris.authorization.ranger.cluster-name=dev polaris.authorization.ranger.rest-client.url=http://ranger-admin:6080 polaris.authorization.ranger.rest-client.connection-timeout=PT2M polaris.authorization.ranger.rest-client.read-timeout=PT30S polaris.authorization.ranger.auth.type=basic polaris.authorization.ranger.auth.basic.username=admin polaris.authorization.ranger.auth.basic.password=rangerR0cks! polaris.authorization.ranger.audit.destination=solr polaris.authorization.ranger.audit.solr.urls=http://solr-service:8983/solr/ranger_audits ``` This option has several advantages: * Environment variables automatic support * Type safety: Duration, URI, Path types are validated at startup * Secrets management: Can use Quarkus secret stores * Unified configuration: All config in one place ### Embed Ranger configuration in Quarkus If you prefer to expose Ranger config options directly in Quarkus: ```java @ConfigMapping(prefix = "polaris.authorization.ranger") public interface RangerPolarisAuthorizerConfig { // Capture ALL ranger.* properties as-is @WithParentName Map<String, String> properties(); default Properties toRangerProperties() { Properties props = new Properties(); props.putAll(properties()); return props; } } ``` Example config: ```properties polaris.authorization.ranger."ranger.plugin.polaris.service.name"=dev_polaris polaris.authorization.ranger."ranger.authz.default.policy.rest.url"=http://ranger-admin:6080 polaris.authorization.ranger."ranger.authz.default.policy.rest.client.username"=admin polaris.authorization.ranger."ranger.authz.default.policy.rest.client.password"=rangerR0cks! polaris.authorization.ranger."xasecure.audit.destination.solr"=enabled polaris.authorization.ranger."xasecure.audit.destination.solr.urls"=http://solr:8983/solr/ranger_audits ``` This approach is more extensible, but we lose a few things like rich type system. ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizerConfig.java: ########## @@ -0,0 +1,72 @@ +/* + * 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.config.ConfigMapping; +import java.util.Optional; +import java.util.Properties; +import org.apache.commons.lang3.StringUtils; +import org.apache.polaris.extension.auth.ranger.utils.RangerUtils; +import org.apache.polaris.immutables.PolarisImmutable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@PolarisImmutable +@ConfigMapping(prefix = "polaris.authorization.ranger") +public interface RangerPolarisAuthorizerConfig { + Logger LOG = LoggerFactory.getLogger(RangerPolarisAuthorizerConfig.class); + + String PROP_RANGER_CONFIG_FILE_NAME = "polaris.authorization.ranger.config-file-name"; + String PROP_POLARIS_SERVICE_NAME = "ranger.plugin.polaris.service.name"; + + String ERR_INVALID_AUTHORIZER_CONFIG_MISSING_CONFIG_FILE_NAME = + "Invalid Ranger authorizer configuration: missing configuration " + + PROP_RANGER_CONFIG_FILE_NAME; + String ERR_INVALID_CONFIG_FILE_MISSING_CONFIGURATION = Review Comment: Nit: These constants are exposed publicly, but they shouldn't. I'd prefer to have them inlined. Especially having a `LOG` constant exposed in an interface feels odd. ########## extensions/auth/ranger/build.gradle.kts: ########## @@ -0,0 +1,44 @@ +/* + * 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(libs.ranger.authz.embedded) Review Comment: It seems that this dependency brings in a LOT of transitive deps that we may not want/need, e.g.: ``` com.sun.jersey:jersey-bundle:1.19.4 javax.servlet:javax.servlet-api:4.0.0 org.apache.hadoop:hadoop-client-api:3.4.2 (*) org.apache.hadoop:hadoop-client-runtime:3.4.2 (*) ``` Could you try to evaluate which ones are really needed? I would like to exclude at least the Hadoop ones. Also FYI my IDE is flagging two CVEs here: * CVE-2024-29131 * CVE-2024-29133 ########## 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 Review Comment: Why do certain entity types have special names and others don't? ########## extensions/auth/ranger/build.gradle.kts: ########## @@ -0,0 +1,50 @@ +/* + * 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(libs.ranger.authz.embedded) + implementation(libs.commons.lang3) + implementation(libs.guava) + + // Iceberg dependency for ForbiddenException + implementation(platform(libs.iceberg.bom)) + implementation("org.apache.iceberg:iceberg-api") + + compileOnly(libs.jakarta.annotation.api) + compileOnly(libs.jakarta.enterprise.cdi.api) + compileOnly(libs.jakarta.inject.api) + compileOnly(libs.smallrye.config.core) + compileOnly(project(":polaris-immutables")) + + runtimeOnly(libs.graalvm.graal.sdk) + runtimeOnly(libs.graalvm.inspect.community) + runtimeOnly(libs.graalvm.js.community) + runtimeOnly(libs.graalvm.js.scriptengine) + runtimeOnly(libs.graalvm.nativeimage) Review Comment: In fact it seems that `nativeimage` remains a transitive dep: ``` +--- org.graalvm.js:js-scriptengine:25.0.2 | \--- org.graalvm.polyglot:polyglot:25.0.2 | +--- org.graalvm.sdk:collections:25.0.2 | \--- org.graalvm.sdk:nativeimage:25.0.2 | \--- org.graalvm.sdk:word:25.0.2 +--- org.graalvm.polyglot:js:25.0.2 | \--- org.graalvm.js:js:25.0.2 | +--- org.graalvm.js:js-language:25.0.2 | | +--- org.graalvm.regex:regex:25.0.2 | | | +--- org.graalvm.truffle:truffle-api:25.0.2 | | | | +--- org.graalvm.sdk:collections:25.0.2 | | | | +--- org.graalvm.sdk:nativeimage:25.0.2 (*) | | | | \--- org.graalvm.polyglot:polyglot:25.0.2 (*) | | | \--- org.graalvm.shadowed:icu4j:25.0.2 | | | +--- org.graalvm.sdk:nativeimage:25.0.2 (*) | | | \--- org.graalvm.shadowed:xz:25.0.2 | | +--- org.graalvm.truffle:truffle-api:25.0.2 (*) | | +--- org.graalvm.polyglot:polyglot:25.0.2 (*) | | \--- org.graalvm.shadowed:icu4j:25.0.2 (*) | \--- org.graalvm.truffle:truffle-runtime:25.0.2 | +--- org.graalvm.sdk:jniutils:25.0.2 | | +--- org.graalvm.sdk:collections:25.0.2 | | \--- org.graalvm.sdk:nativeimage:25.0.2 (*) | +--- org.graalvm.truffle:truffle-api:25.0.2 (*) | \--- org.graalvm.truffle:truffle-compiler:25.0.2 ``` @mneethiraj I'm a bit worried about the final jar size overhead that this extension could bring. Do you have any numbers to share? ########## 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: I agree with @flyrain, my recollection from the OPA authorizer is that the common ground that authorizers should rely on is `PolarisAuthorizableOperation`. `PolarisPrivilege` is specific to Polaris built-in RBAC model. ########## extensions/auth/ranger/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizerFactory.java: ########## @@ -0,0 +1,92 @@ +/* + * 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()); Review Comment: ```suggestion Properties rangerProp = RangerUtils.loadProperties(config.configFileName().orElseThrow()); ``` ########## 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: Although I like the idea of operation types. Here we just need to remove the operations that are only relevant for the built-in RBAC system, an operation type could solve this elegantly. ########## 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) { + 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(); Review Comment: Why can't you just use the enum name for all privileges? -- 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]
