github-actions[bot] commented on code in PR #68220:
URL: https://github.com/apache/doris/pull/68220#discussion_r4068643753


##########
fe/fe-authorization/fe-authorization-plugins/fe-authorization-plugin-ranger-hive/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAccessController.java:
##########
@@ -343,6 +343,9 @@ private RangerAccessRequestImpl 
createRequest(AuthorizedSubject subject, Set<Str
             AccessContext context) {
         RangerAccessRequestImpl request = new RangerAccessRequestImpl();
         request.setUser(subject.getUser());
+        // Ranger's groups for the user, out of the user store this service's 
plugin downloads; a Hive
+        // service's policies are usually kept by group, and Doris has no 
groups of its own to send.
+        request.setUserGroups(groupsOf(subject));

Review Comment:
   [P1] Snapshot groups once per logical authorization, as this method already 
does for roles. `createRequest()` rereads and copies the asynchronously 
refreshed user store for every column. If a refresh moves the user from group A 
to B between two iterations, with A granting only c1 and B granting only c2, 
this one batch carries c1/A plus c2/B and grants a column set no single 
membership snapshot permits. This is distinct from the existing publication 
thread: both snapshots may be correctly published. Please capture one immutable 
group set before the loop, pass it to every request (and the analogous 
Doris/data-policy loops), and cover a refresh barrier during a multi-column 
check.



##########
fe/fe-authorization/fe-authorization-plugins/fe-authorization-plugin-ranger-common/src/main/java/org/apache/doris/catalog/authorizer/ranger/RangerUserStoreGroups.java:
##########
@@ -0,0 +1,142 @@
+// 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.doris.catalog.authorizer.ranger;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig;
+import org.apache.ranger.plugin.contextenricher.RangerAdminUserStoreRetriever;
+import org.apache.ranger.plugin.contextenricher.RangerUserStoreEnricher;
+import org.apache.ranger.plugin.policyengine.RangerPluginContext;
+import org.apache.ranger.plugin.service.RangerAuthContext;
+import org.apache.ranger.plugin.service.RangerBasePlugin;
+import org.apache.ranger.plugin.util.RangerUserStoreUtil;
+import org.apache.ranger.plugin.util.ServiceDefUtil;
+import org.apache.ranger.plugin.util.ServicePolicies;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Where a request built by a Doris source gets the groups a Ranger policy 
item may be written against.
+ *
+ * <p>A Ranger policy item names users, groups and roles, and Ranger's own 
plugins fill in the groups from
+ * the service they run in - the Hive plugin asks Hadoop's group mapping. 
Doris has nothing to ask: an
+ * account holds roles, and no part of the engine knows which groups a user is 
in. So a request that carries
+ * no groups matches no group item at all, and a service whose policies are 
kept by group - the normal way
+ * to run Ranger, so that nobody edits a policy each time somebody joins a 
team - grants nothing to a Doris
+ * user; and, worse, denies nothing either. An item that denies a group is 
exactly as silent about it.
+ *
+ * <p>What Ranger provides for a plugin in that position is its <b>user 
store</b>: the users and groups Ranger
+ * Admin itself holds, kept current by usersync from LDAP or the OS, which a 
plugin downloads next to its
+ * policies through a {@link RangerUserStoreEnricher}. Ranger 2.5 and later 
reads the groups of the request's
+ * user out of it when {@code ranger.plugin.<type>.use.rangerGroups} is set; 
the two sources Doris embeds do
+ * the same thing here, in the request builder, so that it neither depends on 
the Ranger version they were
+ * built against - branches still on 2.4 have no such setting - nor on an 
operator finding a Ranger property
+ * no Doris document names.
+ *
+ * <p>It is on by default, because a group item that is silently ignored is a 
bug in the deployment's eyes,
+ * not a behaviour anybody opted into; the property Ranger reads for this is 
the one that switches it off,
+ * so that a deployment which has already decided the question in Ranger's 
terms has decided it here too.
+ * Switched off, requests carry no groups and this source matches what it 
matched before.
+ */
+public final class RangerUserStoreGroups {
+    private static final Logger LOG = 
LogManager.getLogger(RangerUserStoreGroups.class);
+
+    /**
+     * Suffix of the property switching this off, {@code 
ranger.plugin.<type>.use.rangerGroups} in full: the
+     * name Ranger itself reads for the same thing, so that one setting 
decides both.
+     */
+    public static final String USE_RANGER_GROUPS = ".use.rangerGroups";
+
+    /** How often the plugin asks Ranger Admin for a newer user store, when 
nothing configures it. */
+    private static final String DEFAULT_REFRESH_INTERVAL_MS = 
Integer.toString(60 * 1000);
+
+    private RangerUserStoreGroups() {
+    }
+
+    /** Whether the plugin configured by {@code config} attaches user store 
groups; on unless switched off. */
+    public static boolean enabledFor(RangerPluginConfig config) {
+        // No configuration at all - a plugin stubbed out in a test - has not 
switched anything off.
+        return config == null || config.getBoolean(config.getPropertyPrefix() 
+ USE_RANGER_GROUPS, true);

Review Comment:
   [P1] Reject malformed values for this authorization opt-out. Hadoop's 
`Configuration.getBoolean()` returns its default for any value other than 
`true` or `false`, so `ranger.plugin.<type>.use.rangerGroups=flase` silently 
evaluates to `true` here and activates group allows/denies/masks/filters after 
an operator attempted the compatibility opt-out. Ranger itself cached the same 
malformed setting with default `false`, so the two consumers also disagree. 
Please parse the raw value strictly (missing means true; only case-insensitive 
true/false are accepted), fail construction otherwise, and add an invalid-value 
test.



##########
fe/fe-authorization/fe-authorization-plugins/fe-authorization-plugin-ranger-common/src/main/java/org/apache/doris/catalog/authorizer/ranger/RangerUserStoreGroups.java:
##########
@@ -0,0 +1,142 @@
+// 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.doris.catalog.authorizer.ranger;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig;
+import org.apache.ranger.plugin.contextenricher.RangerAdminUserStoreRetriever;
+import org.apache.ranger.plugin.contextenricher.RangerUserStoreEnricher;
+import org.apache.ranger.plugin.policyengine.RangerPluginContext;
+import org.apache.ranger.plugin.service.RangerAuthContext;
+import org.apache.ranger.plugin.service.RangerBasePlugin;
+import org.apache.ranger.plugin.util.RangerUserStoreUtil;
+import org.apache.ranger.plugin.util.ServiceDefUtil;
+import org.apache.ranger.plugin.util.ServicePolicies;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Where a request built by a Doris source gets the groups a Ranger policy 
item may be written against.
+ *
+ * <p>A Ranger policy item names users, groups and roles, and Ranger's own 
plugins fill in the groups from
+ * the service they run in - the Hive plugin asks Hadoop's group mapping. 
Doris has nothing to ask: an
+ * account holds roles, and no part of the engine knows which groups a user is 
in. So a request that carries
+ * no groups matches no group item at all, and a service whose policies are 
kept by group - the normal way
+ * to run Ranger, so that nobody edits a policy each time somebody joins a 
team - grants nothing to a Doris
+ * user; and, worse, denies nothing either. An item that denies a group is 
exactly as silent about it.
+ *
+ * <p>What Ranger provides for a plugin in that position is its <b>user 
store</b>: the users and groups Ranger
+ * Admin itself holds, kept current by usersync from LDAP or the OS, which a 
plugin downloads next to its
+ * policies through a {@link RangerUserStoreEnricher}. Ranger 2.5 and later 
reads the groups of the request's
+ * user out of it when {@code ranger.plugin.<type>.use.rangerGroups} is set; 
the two sources Doris embeds do
+ * the same thing here, in the request builder, so that it neither depends on 
the Ranger version they were
+ * built against - branches still on 2.4 have no such setting - nor on an 
operator finding a Ranger property
+ * no Doris document names.
+ *
+ * <p>It is on by default, because a group item that is silently ignored is a 
bug in the deployment's eyes,
+ * not a behaviour anybody opted into; the property Ranger reads for this is 
the one that switches it off,
+ * so that a deployment which has already decided the question in Ranger's 
terms has decided it here too.
+ * Switched off, requests carry no groups and this source matches what it 
matched before.
+ */
+public final class RangerUserStoreGroups {
+    private static final Logger LOG = 
LogManager.getLogger(RangerUserStoreGroups.class);
+
+    /**
+     * Suffix of the property switching this off, {@code 
ranger.plugin.<type>.use.rangerGroups} in full: the
+     * name Ranger itself reads for the same thing, so that one setting 
decides both.
+     */
+    public static final String USE_RANGER_GROUPS = ".use.rangerGroups";
+
+    /** How often the plugin asks Ranger Admin for a newer user store, when 
nothing configures it. */
+    private static final String DEFAULT_REFRESH_INTERVAL_MS = 
Integer.toString(60 * 1000);
+
+    private RangerUserStoreGroups() {
+    }
+
+    /** Whether the plugin configured by {@code config} attaches user store 
groups; on unless switched off. */
+    public static boolean enabledFor(RangerPluginConfig config) {
+        // No configuration at all - a plugin stubbed out in a test - has not 
switched anything off.
+        return config == null || config.getBoolean(config.getPropertyPrefix() 
+ USE_RANGER_GROUPS, true);
+    }
+
+    /**
+     * Puts a user store enricher on the service definition Ranger just 
downloaded, unless one is there or
+     * this is switched off, so that the plugin fetches and refreshes the user 
store {@link #groupsOf} reads.
+     *
+     * <p>Called by {@link LoadedRangerPlugin#setPolicies} before handing the 
policies on, on every
+     * call and not only the first: a download of policy deltas comes with its 
own copy of the service
+     * definition, which is why {@code RangerBasePlugin} re-adds the enricher 
on deltas too. The retriever
+     * class and the refresh interval are read under the option names Ranger 
itself reads them under, so that
+     * an operator who has tuned them for Ranger's own {@code 
use.rangerGroups} has tuned them here.
+     */
+    public static void addUserStoreEnricher(RangerPluginConfig config, 
ServicePolicies policies) {
+        if (policies == null || config == null || !enabledFor(config)) {
+            return;
+        }
+        String retriever = 
config.get(RangerUserStoreEnricher.USERSTORE_RETRIEVER_CLASSNAME_OPTION,
+                RangerAdminUserStoreRetriever.class.getCanonicalName());
+        String refreshIntervalMs = 
config.get(RangerUserStoreEnricher.USERSTORE_REFRESHER_POLLINGINTERVAL_OPTION,

Review Comment:
   [P2] Validate this interval as strictly positive before adding the enricher. 
Ranger 2.8 accepts `0`/negative through `Long.parseLong`, performs the initial 
download, starts `RangerUserStoreRefresher`, creates a `Timer`, and only then 
calls `Timer.schedule()`, which throws `IllegalArgumentException` for a 
non-positive period. `setPolicies()` catches that before publishing the engine, 
so `LoadedRangerPlugin.cleanup()` cannot reach the partially initialized 
enricher; its refresher remains blocked forever, and each failed ranger-hive 
bind retry leaks another daemon thread. Please reject non-positive values 
before Ranger init, ensure partial enricher construction is cleaned up, and add 
a `0` interval test that verifies the refresher terminates.



##########
fe/fe-authorization/fe-authorization-plugins/fe-authorization-plugin-ranger-common/src/main/java/org/apache/doris/catalog/authorizer/ranger/RangerUserStoreGroups.java:
##########
@@ -0,0 +1,142 @@
+// 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.doris.catalog.authorizer.ranger;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig;
+import org.apache.ranger.plugin.contextenricher.RangerAdminUserStoreRetriever;
+import org.apache.ranger.plugin.contextenricher.RangerUserStoreEnricher;
+import org.apache.ranger.plugin.policyengine.RangerPluginContext;
+import org.apache.ranger.plugin.service.RangerAuthContext;
+import org.apache.ranger.plugin.service.RangerBasePlugin;
+import org.apache.ranger.plugin.util.RangerUserStoreUtil;
+import org.apache.ranger.plugin.util.ServiceDefUtil;
+import org.apache.ranger.plugin.util.ServicePolicies;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Where a request built by a Doris source gets the groups a Ranger policy 
item may be written against.
+ *
+ * <p>A Ranger policy item names users, groups and roles, and Ranger's own 
plugins fill in the groups from
+ * the service they run in - the Hive plugin asks Hadoop's group mapping. 
Doris has nothing to ask: an
+ * account holds roles, and no part of the engine knows which groups a user is 
in. So a request that carries
+ * no groups matches no group item at all, and a service whose policies are 
kept by group - the normal way
+ * to run Ranger, so that nobody edits a policy each time somebody joins a 
team - grants nothing to a Doris
+ * user; and, worse, denies nothing either. An item that denies a group is 
exactly as silent about it.
+ *
+ * <p>What Ranger provides for a plugin in that position is its <b>user 
store</b>: the users and groups Ranger
+ * Admin itself holds, kept current by usersync from LDAP or the OS, which a 
plugin downloads next to its
+ * policies through a {@link RangerUserStoreEnricher}. Ranger 2.5 and later 
reads the groups of the request's
+ * user out of it when {@code ranger.plugin.<type>.use.rangerGroups} is set; 
the two sources Doris embeds do
+ * the same thing here, in the request builder, so that it neither depends on 
the Ranger version they were
+ * built against - branches still on 2.4 have no such setting - nor on an 
operator finding a Ranger property
+ * no Doris document names.
+ *
+ * <p>It is on by default, because a group item that is silently ignored is a 
bug in the deployment's eyes,

Review Comment:
   [P2] Add the promised operator documentation before making this default-on. 
This changes effective allows, denies, row filters, and masks on upgrade, adds 
opt-out/refresh settings for both plugins, and introduces a new 
FE-start/catalog-bind failure contract, but this PR contains no docs change. 
The linked #68203 is closed and was also code-only. Please update or link the 
actual `ranger.md` change, including both exact opt-out keys, refresh tuning, 
the no-policy/cache prerequisite, and mixed-version rolling-upgrade behavior.



##########
fe/fe-authorization/fe-authorization-plugins/fe-authorization-plugin-ranger-hive/src/main/java/org/apache/doris/catalog/authorizer/ranger/hive/RangerHiveAccessController.java:
##########
@@ -343,6 +343,9 @@ private RangerAccessRequestImpl 
createRequest(AuthorizedSubject subject, Set<Str
             AccessContext context) {
         RangerAccessRequestImpl request = new RangerAccessRequestImpl();
         request.setUser(subject.getUser());
+        // Ranger's groups for the user, out of the user store this service's 
plugin downloads; a Hive

Review Comment:
   [P1] Merge Ranger-derived roles instead of letting unrelated Doris roles 
suppress them. This request adds the Ranger groups here and then sets Doris 
roles on the next line, but Ranger 2.8 derives roles from `user/groups` only 
when the incoming role set is empty. A user holding Doris role `reporter` and 
Ranger group `analysts` therefore never receives Ranger role `data_reader` 
granted to that group; role-based denies can be omitted just like allows. The 
new Hive test masks this by forcing `rolesOf()` to return empty. After the same 
normalization required above, merge Ranger's `getRolesFromUserAndGroups()` 
result with the Doris roles and add a 
nonempty-Doris-role/group-granted-Ranger-role test.



##########
fe/fe-authorization/fe-authorization-plugins/fe-authorization-plugin-ranger-common/src/main/java/org/apache/doris/catalog/authorizer/ranger/RangerUserStoreGroups.java:
##########
@@ -0,0 +1,142 @@
+// 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.doris.catalog.authorizer.ranger;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig;
+import org.apache.ranger.plugin.contextenricher.RangerAdminUserStoreRetriever;
+import org.apache.ranger.plugin.contextenricher.RangerUserStoreEnricher;
+import org.apache.ranger.plugin.policyengine.RangerPluginContext;
+import org.apache.ranger.plugin.service.RangerAuthContext;
+import org.apache.ranger.plugin.service.RangerBasePlugin;
+import org.apache.ranger.plugin.util.RangerUserStoreUtil;
+import org.apache.ranger.plugin.util.ServiceDefUtil;
+import org.apache.ranger.plugin.util.ServicePolicies;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Where a request built by a Doris source gets the groups a Ranger policy 
item may be written against.
+ *
+ * <p>A Ranger policy item names users, groups and roles, and Ranger's own 
plugins fill in the groups from
+ * the service they run in - the Hive plugin asks Hadoop's group mapping. 
Doris has nothing to ask: an
+ * account holds roles, and no part of the engine knows which groups a user is 
in. So a request that carries
+ * no groups matches no group item at all, and a service whose policies are 
kept by group - the normal way
+ * to run Ranger, so that nobody edits a policy each time somebody joins a 
team - grants nothing to a Doris
+ * user; and, worse, denies nothing either. An item that denies a group is 
exactly as silent about it.
+ *
+ * <p>What Ranger provides for a plugin in that position is its <b>user 
store</b>: the users and groups Ranger
+ * Admin itself holds, kept current by usersync from LDAP or the OS, which a 
plugin downloads next to its
+ * policies through a {@link RangerUserStoreEnricher}. Ranger 2.5 and later 
reads the groups of the request's
+ * user out of it when {@code ranger.plugin.<type>.use.rangerGroups} is set; 
the two sources Doris embeds do
+ * the same thing here, in the request builder, so that it neither depends on 
the Ranger version they were
+ * built against - branches still on 2.4 have no such setting - nor on an 
operator finding a Ranger property
+ * no Doris document names.
+ *
+ * <p>It is on by default, because a group item that is silently ignored is a 
bug in the deployment's eyes,
+ * not a behaviour anybody opted into; the property Ranger reads for this is 
the one that switches it off,
+ * so that a deployment which has already decided the question in Ranger's 
terms has decided it here too.
+ * Switched off, requests carry no groups and this source matches what it 
matched before.
+ */
+public final class RangerUserStoreGroups {
+    private static final Logger LOG = 
LogManager.getLogger(RangerUserStoreGroups.class);
+
+    /**
+     * Suffix of the property switching this off, {@code 
ranger.plugin.<type>.use.rangerGroups} in full: the
+     * name Ranger itself reads for the same thing, so that one setting 
decides both.
+     */
+    public static final String USE_RANGER_GROUPS = ".use.rangerGroups";
+
+    /** How often the plugin asks Ranger Admin for a newer user store, when 
nothing configures it. */
+    private static final String DEFAULT_REFRESH_INTERVAL_MS = 
Integer.toString(60 * 1000);
+
+    private RangerUserStoreGroups() {
+    }
+
+    /** Whether the plugin configured by {@code config} attaches user store 
groups; on unless switched off. */
+    public static boolean enabledFor(RangerPluginConfig config) {
+        // No configuration at all - a plugin stubbed out in a test - has not 
switched anything off.
+        return config == null || config.getBoolean(config.getPropertyPrefix() 
+ USE_RANGER_GROUPS, true);
+    }
+
+    /**
+     * Puts a user store enricher on the service definition Ranger just 
downloaded, unless one is there or
+     * this is switched off, so that the plugin fetches and refreshes the user 
store {@link #groupsOf} reads.
+     *
+     * <p>Called by {@link LoadedRangerPlugin#setPolicies} before handing the 
policies on, on every
+     * call and not only the first: a download of policy deltas comes with its 
own copy of the service
+     * definition, which is why {@code RangerBasePlugin} re-adds the enricher 
on deltas too. The retriever
+     * class and the refresh interval are read under the option names Ranger 
itself reads them under, so that
+     * an operator who has tuned them for Ranger's own {@code 
use.rangerGroups} has tuned them here.
+     */
+    public static void addUserStoreEnricher(RangerPluginConfig config, 
ServicePolicies policies) {
+        if (policies == null || config == null || !enabledFor(config)) {
+            return;
+        }
+        String retriever = 
config.get(RangerUserStoreEnricher.USERSTORE_RETRIEVER_CLASSNAME_OPTION,
+                RangerAdminUserStoreRetriever.class.getCanonicalName());
+        String refreshIntervalMs = 
config.get(RangerUserStoreEnricher.USERSTORE_REFRESHER_POLLINGINTERVAL_OPTION,
+                DEFAULT_REFRESH_INTERVAL_MS);
+        // Ranger logs the addition itself, once per download that needed it; 
the operator-facing line about
+        // why the store is downloaded at all is the plugin's, written once 
when it starts (see describe).
+        if (ServiceDefUtil.addUserStoreEnricher(policies, retriever, 
refreshIntervalMs) && LOG.isDebugEnabled()) {
+            LOG.debug("Ranger service {} will download its user store every {} 
ms", policies.getServiceName(),
+                    refreshIntervalMs);
+        }
+    }
+
+    /** One line for the plugin's start-up log, saying what this does for it 
and how to switch it off. */
+    public static String describe(RangerPluginConfig config) {
+        String property = config.getPropertyPrefix() + USE_RANGER_GROUPS;
+        return enabledFor(config)
+                ? "Ranger service " + config.getServiceName() + ": requests 
carry the groups Ranger's user store"
+                        + " puts the user in, so that policy items written 
against a group apply; set "
+                        + property + "=false to switch that off"
+                : "Ranger service " + config.getServiceName() + ": " + 
property + "=false, so requests carry no"
+                        + " groups and policy items written against a group 
never apply";
+    }
+
+    /**
+     * The groups the user store {@code plugin} has downloaded puts {@code 
user} in.
+     *
+     * <p>Empty when this is switched off, when no store has arrived - Ranger 
Admin could not be reached for
+     * it and nothing was cached, which {@link LoadedRangerPlugin} says why it 
does not refuse - and when the
+     * store does not know the user, which is the case for every account that 
exists in Doris only. Empty
+     * and not null on purpose: a request with an empty group set matches 
items written against users and
+     * roles exactly as it did before.
+     */
+    public static Set<String> groupsOf(RangerBasePlugin plugin, String user) {
+        if (user == null || !enabledFor(plugin.getConfig())) {
+            return Collections.emptySet();
+        }
+        // The auth context the policy engine publishes is where Ranger's own 
request processing reads the
+        // store from; it is replaced together with the engine, and the store 
is carried over when it is.
+        RangerPluginContext pluginContext = plugin.getPluginContext();
+        RangerAuthContext authContext = pluginContext == null ? null : 
pluginContext.getAuthContext();
+        RangerUserStoreUtil userStore = authContext == null ? null : 
authContext.getUserStoreUtil();
+        Set<String> groups = userStore == null ? null : 
userStore.getUserGroups(user);

Review Comment:
   [P1] Preserve Ranger's user/group preprocessing order here. Ranger 2.8 
transforms the request username (including `convert.emailToUser`) before 
looking up store groups and does not transform those canonical groups 
afterward. This helper instead queries with the raw Doris username and supplies 
the result as caller groups. Thus `[email protected]` can miss a store keyed by 
`alice`; Ranger later converts the user, but with `use.rangerGroups` omitted 
its cached flag is false and the missing group deny is never repaired. Worse, 
Ranger 2.8's case-conversion-only path leaves `groupNameTransformer` null but 
calls it when caller groups are nonempty, so this newly nonempty set can make 
every request throw. This is distinct from the existing 
unavailable-store/publication threads because the store is healthy. Please 
populate groups at Ranger's supported post-transformation stage and cover email 
plus case-only name transformation.



##########
fe/fe-authorization/fe-authorization-plugins/fe-authorization-plugin-ranger-common/src/main/java/org/apache/doris/catalog/authorizer/ranger/LoadedRangerPlugin.java:
##########
@@ -0,0 +1,130 @@
+// 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.doris.catalog.authorizer.ranger;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.ranger.plugin.service.RangerBasePlugin;
+import org.apache.ranger.plugin.util.ServicePolicies;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * What the two Ranger plugins Doris embeds have in common: they are built 
with their service's policies or
+ * not at all, and the requests built over them carry the groups Ranger keeps 
for a user.
+ *
+ * <p><b>Built with the policies, or not at all.</b> {@code 
RangerBasePlugin.init()} downloads the service's
+ * roles and policies - and, with the enricher below on them, its user store - 
before it returns, and it
+ * survives a Ranger admin it cannot reach: it answers out of the local policy 
cache
+ * ({@code ranger.plugin.<type>.policy.cache.dir}) when there is one, and out 
of no policies at all when there
+ * is not, refusing every check until a later poll succeeds. That is the right 
thing for a plugin embedded in
+ * a service that has to keep running, and the wrong thing at the moment Doris 
binds one: an FE whose instance
+ * scope {@code access_controller_type=ranger-doris} governs, and which has no 
policies, is an FE nobody can
+ * use - no account bypasses the source - and a catalog bound to a {@code 
ranger-hive} source in that state
+ * refuses every statement against it, in both cases with nothing but a line 
in fe.log to say why. So
+ * {@link #init()} refuses that state instead: a load that ended with no 
policies, from the admin or the
+ * cache, stops what it started and throws, with the cause. That fails the FE 
start, the {@code CREATE
+ * CATALOG} (whose dry run builds the plugin), or the statement binding a 
catalog to its source again, which
+ * is where an operator sees it. Once built, an outage of the admin is 
Ranger's to survive, as it always
+ * was: the refresher keeps the last policies it downloaded and keeps polling.
+ *
+ * <p>The user store is not part of that. A load that found the policies but 
no user store is logged and
+ * accepted - policy items written against a group do not apply until the 
store arrives, which the enricher
+ * keeps asking for - because "no store has arrived" cannot be told apart from 
"this admin serves none", and
+ * an admin from before the user store download existed, or one that fails 
that download while serving the
+ * policies, ran every deployment before groups were attached at all; see 
{@link RangerUserStoreGroups}.
+ *
+ * <p><b>Requests carry Ranger's own groups.</b> Doris has none to offer, and 
a policy item written against a
+ * group matches nothing without them; the store they are read from is asked 
for with the policies, see
+ * {@link #setPolicies} and {@link RangerUserStoreGroups}.
+ */
+public abstract class LoadedRangerPlugin extends RangerBasePlugin {
+    private static final Logger LOG = 
LogManager.getLogger(LoadedRangerPlugin.class);
+
+    protected LoadedRangerPlugin(String serviceType, String serviceName, 
String appId) {
+        super(serviceType, serviceName, appId);
+    }
+
+    /**
+     * Loads the plugin - {@code RangerBasePlugin.init()}: roles, policies and 
user store, on this thread -
+     * and refuses to leave it without policies; see the class comment.
+     *
+     * @throws IllegalStateException when the load ended with no policies from 
either the admin or the cache
+     */
+    @Override
+    public void init() {
+        LOG.info(RangerUserStoreGroups.describe(getConfig()));
+        long startedAtNanos = System.nanoTime();
+        try {
+            super.init();
+            if (getPoliciesVersion() < 0) {

Review Comment:
   [P2] Do not diagnose every `-1` engine version as an Admin/cache miss. 
Ranger 2.8 catches policy-engine construction exceptions inside `setPolicies()` 
and returns with the engine null. A concrete new path is 
`userStoreRefresherPollingInterval=often`: this PR adds the user-store 
enricher, its `init()` throws while parsing the interval, Ranger logs and 
swallows that exception, and this branch then reports that Admin was 
unreachable/the service was missing and no cache existed. `CREATE CATALOG` 
therefore loses the actionable cause promised by the PR and directs the 
operator to the wrong repair. Track whether a non-null policy payload failed to 
produce an engine (and validate the new interval before init), preserve an 
accurate cause/message, and add this failure-path test.



##########
regression-test/suites/ranger_p2/test_ranger_group_policy.groovy:
##########
@@ -0,0 +1,212 @@
+// 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.
+
+import org.apache.ranger.RangerClient
+import org.apache.ranger.plugin.model.RangerPolicy
+
+// Policies written against a Ranger GROUP and never against the user: how a 
deployment runs Ranger so that
+// nobody edits a policy each time somebody joins a team. Doris has no groups 
of its own, so these apply
+// only because the ranger-doris source reads the user's groups out of 
Ranger's user store - the store the
+// plugin downloads next to its policies. Every other suite here writes its 
policy items against a user.
+suite("test_ranger_group_policy", "p2,ranger,external") {
+       String enabled = context.config.otherConfigs.get("enableRangerTest")
+       String rangerEndpoint = 
context.config.otherConfigs.get("rangerEndpoint")
+       String rangerUser = context.config.otherConfigs.get("rangerUser")
+       String rangerPassword = 
context.config.otherConfigs.get("rangerPassword")
+       String rangerServiceName = 
context.config.otherConfigs.get("rangerServiceName")
+
+       if (enabled == null || !enabled.equalsIgnoreCase("true")) {
+               return
+       }
+
+       String db = 'ranger_group_db_1'
+       String table = 'ranger_group_tbl_1'
+       // The same database, no policy of any kind: what the group's SELECT 
must not open.
+       String otherTable = 'ranger_group_tbl_2'
+       String user = 'ranger_group_user_1'
+       String pwd = 'C123_567p'
+       String group = 'ranger_group_readers_1'
+       String accessPolicy = 'ranger_test_group_access_policy'
+       String rowFilterPolicy = 'ranger_test_group_row_filter_policy'
+       String maskPolicy = 'ranger_test_group_mask_policy'
+       String denyPolicy = 'ranger_test_group_deny_policy'
+       // A table the user is allowed on by name while the group is denied on 
it; the deny has to win.
+       String deniedTable = 'ranger_group_tbl_3'
+
+       sql """CREATE DATABASE IF NOT EXISTS ${db}"""
+       [table, otherTable, deniedTable].each {
+               sql """DROP TABLE IF EXISTS ${db}.${it}"""
+               sql """CREATE TABLE ${db}.${it} (
+                       id BIGINT,
+                       c1 VARCHAR(20),
+                       c2 VARCHAR(20)
+               )
+               DISTRIBUTED BY HASH(id) BUCKETS 2
+               PROPERTIES (
+                       "replication_num" = "1"
+               )"""
+               sql """INSERT INTO ${db}.${it} VALUES
+               (1, 'DataOne01', 'SampleA1'),
+               (2, 'DataTwo02', 'SampleB2'),
+               (3, 'DataThr03', 'SampleC3'),
+               (4, 'DataFou04', 'SampleD4'),
+               (5, 'DataFiv05', 'SampleE5'),
+               (6, 'DataSix06', 'SampleF6'),
+               (7, 'DataSev07', 'SampleG7'),
+               (8, 'DataEig08', 'SampleH8'),
+               (9, 'DataNin09', 'SampleI9'),
+               (10, 'DataTen10', 'SampleJ0')"""
+       }
+       sql """DROP USER IF EXISTS ${user}"""
+       sql """CREATE USER '${user}' IDENTIFIED BY '${pwd}'"""
+
+       // Ranger's side of the same names: the group, the user, and the 
membership. Nothing syncs users between
+       // Doris and Ranger, so the names are simply kept equal, as the other 
suites do for users.
+       createRangerGroup(group)
+       createRangerUser(user, pwd, ["ROLE_USER"] as String[])
+       setRangerUserGroups(user, [group])
+
+       RangerClient rangerClient = new 
RangerClient("http://${rangerEndpoint}";, "simple", rangerUser, rangerPassword, 
null)
+       def dropPolicy = { String name ->
+               try {
+                       rangerClient.deletePolicy(rangerServiceName, name)
+               } catch (Exception e) {
+                       log.info("Policy not found: ${e.getMessage()}")
+               }
+       }
+       [accessPolicy, rowFilterPolicy, maskPolicy, denyPolicy].each { 
dropPolicy(it) }
+
+       def resourcesOf = { String tbl, String column ->
+               Map<String, RangerPolicy.RangerPolicyResource> resources = new 
HashMap<>()
+               resources.put("catalog", new 
RangerPolicy.RangerPolicyResource("internal"))
+               resources.put("database", new 
RangerPolicy.RangerPolicyResource(db))
+               resources.put("table", new 
RangerPolicy.RangerPolicyResource(tbl))
+               if (column != null) {
+                       resources.put("column", new 
RangerPolicy.RangerPolicyResource(column))
+               }
+               return resources
+       }
+       def select = [new RangerPolicy.RangerPolicyItemAccess("SELECT")]
+
+       // 1. Access: SELECT on the table for the group.
+       RangerPolicy policy = new RangerPolicy()
+       policy.setService(rangerServiceName)
+       policy.setName(accessPolicy)
+       policy.setResources(resourcesOf(table, null))
+       RangerPolicy.RangerPolicyItem accessItem = new 
RangerPolicy.RangerPolicyItem()
+       accessItem.setGroups([group])
+       accessItem.setAccesses(select)
+       policy.setPolicyItems([accessItem])
+       rangerClient.createPolicy(policy)
+
+       // 2. A row filter for the group.
+       policy = new RangerPolicy()
+       policy.setService(rangerServiceName)
+       policy.setName(rowFilterPolicy)
+       policy.setPolicyType(RangerPolicy.POLICY_TYPE_ROWFILTER)
+       policy.setResources(resourcesOf(table, null))
+       RangerPolicy.RangerRowFilterPolicyItem rowFilterItem = new 
RangerPolicy.RangerRowFilterPolicyItem()
+       rowFilterItem.setGroups([group])
+       rowFilterItem.setAccesses(select)
+       rowFilterItem.setRowFilterInfo(new 
RangerPolicy.RangerPolicyItemRowFilterInfo("id >= 5"))
+       policy.setRowFilterPolicyItems([rowFilterItem])
+       rangerClient.createPolicy(policy)
+
+       // 3. A column mask for the group.
+       policy = new RangerPolicy()
+       policy.setService(rangerServiceName)
+       policy.setName(maskPolicy)
+       policy.setPolicyType(RangerPolicy.POLICY_TYPE_DATAMASK)
+       policy.setResources(resourcesOf(table, "c1"))
+       RangerPolicy.RangerDataMaskPolicyItem maskItem = new 
RangerPolicy.RangerDataMaskPolicyItem()
+       maskItem.setGroups([group])
+       maskItem.setAccesses(select)
+       maskItem.setDataMaskInfo(new 
RangerPolicy.RangerPolicyItemDataMaskInfo("MASK_SHOW_LAST_4", null, null))
+       policy.setDataMaskPolicyItems([maskItem])
+       rangerClient.createPolicy(policy)
+
+       // 4. A deny written against the group, on a table the user is allowed 
on by name. Before groups were
+       // attached this deny was silently ignored and the user read the table 
- the dangerous half of the bug.
+       policy = new RangerPolicy()
+       policy.setService(rangerServiceName)
+       policy.setName(denyPolicy)
+       policy.setResources(resourcesOf(deniedTable, null))
+       RangerPolicy.RangerPolicyItem allowUserItem = new 
RangerPolicy.RangerPolicyItem()
+       allowUserItem.setUsers([user])
+       allowUserItem.setAccesses(select)
+       policy.setPolicyItems([allowUserItem])
+       RangerPolicy.RangerPolicyItem denyGroupItem = new 
RangerPolicy.RangerPolicyItem()
+       denyGroupItem.setGroups([group])
+       denyGroupItem.setAccesses(select)
+       policy.setDenyPolicyItems([denyGroupItem])
+       rangerClient.createPolicy(policy)
+
+       def tokens = context.config.jdbcUrl.split('/')
+       def defaultJdbcUrl = tokens[0] + "//" + tokens[2] + "/?"
+       def readable = { String tbl ->
+               return connect("${user}", "${pwd}", "${defaultJdbcUrl}") {
+                       try {
+                               sql """SELECT * FROM internal.${db}.${tbl}"""
+                               return true
+                       } catch (Exception e) {
+                               log.info("not readable yet: ${e.getMessage()}")
+                               return false
+                       }
+               }
+       }
+
+       // The policies reach the FE within its policy poll interval; the 
membership reaches it with the next
+       // user store download, which the plugin makes every 60 seconds unless 
userStoreRefresherPollingInterval
+       // in ranger-doris-security.xml says otherwise. Hence waiting on the 
effect rather than a fixed sleep.
+       logger.info("waiting for the group's SELECT to reach ${user}")
+       awaitUntil(180, 3) { readable(table) }

Review Comment:
   [P2] Wait for the complete policy generation, not only the first access 
policy. These four policies are created by separate Ranger Admin calls, so the 
FE refresher can poll after the access policy but before the filter, mask, or 
deny exists. `readable(table)` then passes, while the one-shot filtered/masked 
query and group-deny assertion below fail until the next poll. Please make this 
readiness loop prove the final filtered/masked result and denied-table refusal 
(or wait on the FE consuming the final policy version) before running the 
assertions.



-- 
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]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to