bitflicker64 commented on code in PR #3273:
URL: https://github.com/apache/hugegraph/pull/3273#discussion_r4334092458


##########
hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/StandardAuthenticator.java:
##########
@@ -219,8 +202,9 @@ public static void initAdminUserIfNeeded(String confFile) 
throws Exception {
         config.addProperty(INITING_STORE, true);
         auth.setup(config);
         if (auth.graph().backendStoreFeatures().supportsPersistence()) {
-            auth.initAdminUser();
+            auth.initAdminUser(config.get(ServerOptions.ADMIN_PA));

Review Comment:
   Important: Blocking: yes. Summary: `init-store.sh` no longer asks for the 
admin password. Before this change the line called the private 
`initAdminUser()`, which reads the password from the console or stdin. It now 
passes `auth.admin_pa`, whose default is the public value `pa`. A bare-metal 
install that enables the built-in authenticator and does not set 
`auth.admin_pa` now gets an admin account with password `pa`, with no prompt 
and no warning. This affects every `StandardAuthenticator` user, not only 
Ranger users. Evidence: at `0a3e4ae5f` (master's #3218, the base this PR 
replays) this line is `auth.initAdminUser();`, and that method calls 
`this.initAdminUser(this.inputPassword())`. `ServerOptions.ADMIN_PA` defaults 
to `"pa"`, and the shipped `conf/rest-server.properties` leaves 
`#auth.admin_pa=pa` commented out. `hugegraph-dist/docker/docker-entrypoint.sh` 
(line 222) still documents that init-store reads the password from stdin. With 
this change, `initAdminUser()` and `inputPas
 sword()` are dead code. Fix: use `auth.admin_pa` only when the file sets it 
explicitly (`config.containsKey(ServerOptions.ADMIN_PA.name())` with a 
non-empty value, the same test `InitStore.checkAdminBootstrapReachable` uses), 
and otherwise keep the prompt. The Ranger stack sets `auth.admin_pa=admin`, so 
it would still work.



##########
hugegraph-server/hugegraph-ranger-plugin/src/main/java/org/apache/hugegraph/ranger/RangerAuthManager.java:
##########
@@ -0,0 +1,767 @@
+/*
+ * 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.hugegraph.ranger;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.security.sasl.AuthenticationException;
+
+import org.apache.hugegraph.auth.AuthManager;
+import org.apache.hugegraph.auth.HugeAccess;
+import org.apache.hugegraph.auth.HugeAuthenticator;
+import org.apache.hugegraph.auth.HugeBelong;
+import org.apache.hugegraph.auth.HugeDefaultRole;
+import org.apache.hugegraph.auth.HugeGroup;
+import org.apache.hugegraph.auth.HugePermission;
+import org.apache.hugegraph.auth.HugeProject;
+import org.apache.hugegraph.auth.HugeTarget;
+import org.apache.hugegraph.auth.HugeUser;
+import org.apache.hugegraph.auth.ResourceType;
+import org.apache.hugegraph.auth.RolePermission;
+import org.apache.hugegraph.auth.SchemaDefine.AuthElement;
+import org.apache.hugegraph.auth.UserWithRole;
+import org.apache.hugegraph.backend.id.Id;
+import org.apache.hugegraph.util.JsonUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * AuthManager implementation that:
+ * <ul>
+ *   <li>Delegates all identity-management operations (users, groups, tokens) 
to
+ *       the underlying {@link AuthManager} (StandardAuthManager /
+ *       StandardAuthManagerV2).</li>
+ *   <li>Intercepts {@link #validateUser} / {@link #rolePermission} to merge
+ *       Ranger policy decisions into the returned {@link RolePermission}.</li>
+ * </ul>
+ *
+ * <h3>Ranger → RolePermission mapping</h3>
+ * <pre>
+ *   Ranger access type  │  HugePermission
+ *   ────────────────────┼────────────────
+ *   read                │  READ
+ *   write               │  WRITE
+ *   delete              │  DELETE
+ *   execute             │  EXECUTE
+ *   admin               │  ADMIN
+ * </pre>
+ */
+public class RangerAuthManager implements AuthManager {
+
+    private static final Logger LOG =
+            LoggerFactory.getLogger(RangerAuthManager.class);
+
+    private final AuthManager delegate;
+    private final RangerHugeGraphPlugin rangerPlugin;
+
+    public RangerAuthManager(AuthManager delegate,
+                             String serviceName,
+                             String rangerConfigFile) {
+        this.delegate = delegate;
+        this.rangerPlugin = new RangerHugeGraphPlugin(serviceName, 
rangerConfigFile);
+        this.rangerPlugin.init();
+    }
+
+    /**
+     * The underlying Ranger plugin instance, used by
+     * {@link RangerHugeGraphAuthenticator#setup} to register real per-request
+     * enforcement via {@code HugeGraphAuthProxy.setResourceAuthorizer}.
+     */
+    public RangerHugeGraphPlugin rangerPlugin() {
+        return this.rangerPlugin;
+    }
+
+    // ------------------------------------------------------------------
+    // Authentication helpers
+    // ------------------------------------------------------------------
+
+    /**
+     * Authenticate by username+password or token, then enrich the returned
+     * role with Ranger policy grants.  Called from
+     * {@link RangerHugeGraphAuthenticator#authenticate}.
+     */
+    public UserWithRole authenticate(String username, String password,
+                                     String token) {
+        UserWithRole base = (token != null && !token.isEmpty()) ?
+                            delegate.validateUser(token) :
+                            delegate.validateUser(username, password);
+        return enrichWithRanger(base);
+    }
+
+    @Override
+    public UserWithRole validateUser(String username, String password) {
+        return enrichWithRanger(delegate.validateUser(username, password));
+    }
+
+    @Override
+    public UserWithRole validateUser(String token) {
+        return enrichWithRanger(delegate.validateUser(token));
+    }
+
+    @Override
+    public RolePermission rolePermission(AuthElement element) {
+        RolePermission base = delegate.rolePermission(element);
+        if (base == null) {
+            base = RolePermission.none();
+        }
+        if (RolePermission.isAdmin(base)) {
+            return base;
+        }
+        String name = (element instanceof HugeUser) ?
+                      ((HugeUser) element).name() :
+                      element.id().asString();
+        // admin is a trusted bootstrap identity (same bypass as
+        // HugeGraphAuthProxy.verifyResPermission() and
+        // RangerHugeGraphAuthenticator.authenticate()) — no Ranger policy is
+        // expected to exist for it, so never probe Ranger on its behalf.
+        if (HugeAuthenticator.USER_ADMIN.equals(name)) {
+            return RolePermission.admin();
+        }
+        return mergeRangerRole(base, name);
+    }
+
+    // ------------------------------------------------------------------
+    // Private: Ranger enrichment
+    // ------------------------------------------------------------------
+
+    private UserWithRole enrichWithRanger(UserWithRole base) {
+        if (base == null || base.username() == null) {
+            return base;
+        }
+        // userId() is null when authentication failed (wrong password or 
unknown user).
+        // Do not grant Ranger permissions to unauthenticated identities.
+        if (base.userId() == null) {
+            return base;
+        }
+        RolePermission role = base.role();
+        if (role != null && RolePermission.isAdmin(role)) {
+            return base;
+        }
+        // admin is a trusted bootstrap identity — never probe Ranger for it,
+        // otherwise every login logs a spurious Denied admin/*/*/all/* audit
+        // entry even though RangerHugeGraphAuthenticator.authenticate()
+        // overrides the role with ROLE_ADMIN right after this call returns.
+        if (HugeAuthenticator.USER_ADMIN.equals(base.username())) {
+            return new UserWithRole(base.userId(), base.username(),
+                                    RolePermission.admin());
+        }
+        RolePermission enriched = mergeRangerRole(
+                role != null ? role : RolePermission.none(),
+                base.username());
+        // No grants from either HugeGraph's own role or Ranger policy: reject 
the
+        // login outright rather than admitting a role with no permissions.
+        //
+        // This MUST be the HugeAuthenticator.ROLE_NONE instance, not a fresh
+        // RolePermission.none(). HugeAuthenticator.verifyRole() tests 
identity:
+        //     return role != ROLE_NONE && role != null;
+        // so a newly built none() is not that reference, passes verifyRole() 
and
+        // logs the user in with an empty role.
+        if (isEffectivelyNone(enriched)) {
+            enriched = HugeAuthenticator.ROLE_NONE;
+        }
+        return new UserWithRole(base.userId(), base.username(), enriched);
+    }
+
+    /**
+     * Query Ranger for {@code username} and merge any grants into {@code 
base}.
+     *
+     * <p>We check one broad wildcard request (graphspace=*, graph=*, type=ALL)
+     * for each permission level.  Fine-grained resource checks are applied
+     * per-request inside {@link org.apache.hugegraph.auth.HugeGraphAuthProxy}
+     * via the normal {@link RolePermission} matching logic, so what we build
+     * here must satisfy that matching algorithm.
+     *
+     * <p>We can't call the protected {@code RolePermission.add()} from outside
+     * the package, so we build a JSON role string and deserialise it instead —
+     * this goes through the same public factory path used everywhere else.
+     */
+    private RolePermission mergeRangerRole(RolePermission base,
+                                           String username) {
+        if (base == null) {
+            base = RolePermission.none();
+        }
+        // Admin check first (cheapest)
+        if (rangerPlugin.isAllowed(username, "*", "*",
+                                   ResourceType.ALL, "*",
+                                   HugePermission.ADMIN)) {
+            return RolePermission.admin();
+        }
+
+        // Build a minimal JSON roles document from the Ranger grants, then
+        // deserialise into a RolePermission and merge with base via its JSON
+        // round-trip (the only public merge path available outside the pkg).
+        // Use "DEFAULT" as graphspace key — RolePerm.matchResource() does an
+        // exact key lookup for requiredResource.graphSpace(), which is 
"DEFAULT".
+        StringBuilder sb = new 
StringBuilder("{\"roles\":{\"DEFAULT\":{\"*\":{");

Review Comment:
   Important: Blocking: yes. Summary: The role built from the Ranger grants is 
keyed only under graph space `DEFAULT`. `RolePerm.matchResource()` looks up 
`roles.get(requiredResource.graphSpace())` by exact key, so a user whose access 
comes only from Ranger is refused in every other graph space, even under a 
`graphspace=*` policy. The per-request authorizer never gets a say, because 
`verifyResPermission` consults it only after the local match has already 
passed. In a PD/HStore deployment with several graph spaces, Ranger cannot 
grant anything outside `DEFAULT`. Evidence: this line hard-codes 
`{"roles":{"DEFAULT":{"*":{...}}}}`. HugeAuthenticator.java line 438 is 
`this.roles.get(requiredResource.graphSpace())`, which returns false when the 
result is null. In `HugeGraphAuthProxy.verifyResPermission` the authorizer 
branch is guarded by `if (matched && RESOURCE_AUTHORIZER != null)`. Fix: build 
the role for the graph space being accessed (or for every graph space the user 
can reach), and 
 add a test where a Ranger-only user reads a graph in a graph space other than 
`DEFAULT`.



##########
hugegraph-server/hugegraph-ranger-plugin/pom.xml:
##########
@@ -0,0 +1,212 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.hugegraph</groupId>
+        <artifactId>hugegraph-server</artifactId>
+        <version>${revision}</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>hugegraph-ranger-plugin</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>Apache Ranger authorization plugin for HugeGraph</description>
+
+    <properties>
+        <ranger.version>2.4.0</ranger.version>

Review Comment:
   Important: Blocking: yes. Summary: `ranger-plugins-common` 2.4.0 has a 
critical code injection advisory, and the `dependency-review` check on this 
head fails because of it. The shaded `plugin` jar bundles this artifact into 
the server's `lib/`. Evidence: the log of check run 111854595312 reports 
`org.apache.ranger:[email protected] - Apache Ranger has a Code 
Injection vulnerability (critical severity)`. That is GHSA-c87w-642h-m97h / 
CVE-2025-59059: affected `< 2.8.0`, first patched in 2.8.0. Fix: raise 
`ranger.version` to 2.8.0 or later, update the `apache/ranger:2.4.0` base image 
in `Dockerfile.ranger` to match, and check whether the Java 8 target is still 
needed for that Ranger Admin version.



##########
hugegraph-server/hugegraph-ranger-plugin/src/main/java/org/apache/hugegraph/ranger/RangerHugeGraphService.java:
##########
@@ -0,0 +1,383 @@
+/*
+ * 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.hugegraph.ranger;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeSet;
+
+import org.apache.ranger.plugin.service.RangerBaseService;
+import org.apache.ranger.plugin.service.ResourceLookupContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+/**
+ * Ranger Admin-side service class for HugeGraph.
+ * <p>
+ * Loaded by Ranger Admin via {@code Class.newInstance()} to validate 
connection
+ * configuration and provide resource lookup for the policy editor UI.
+ * Must have a public no-arg constructor and must extend {@link 
RangerBaseService}.
+ * <p>
+ * This class is loaded by <b>Ranger Admin</b>, not by HugeGraph, so it must 
not
+ * reference any {@code org.apache.hugegraph.*} class: the shaded plugin JAR
+ * deliberately excludes hugegraph-core and hugegraph-api, and Ranger Admin's
+ * classpath has neither. Resource names and resource-type values are therefore
+ * duplicated here as literals rather than read from the HugeGraph enums.
+ * <p>
+ * For the policy editor's autocomplete to call {@link #lookupResource}, the
+ * service definition's {@code implClass} must name this class <i>and</i> the
+ * shaded plugin JAR must sit on Ranger Admin's classpath under
+ * {@code ews/webapp/WEB-INF/classes/ranger-plugins/hugegraph/}.
+ */
+public class RangerHugeGraphService extends RangerBaseService {
+
+    private static final Logger LOG =
+            LoggerFactory.getLogger(RangerHugeGraphService.class);
+
+    private static final String CONFIG_URL      = "hugegraph.url";
+    private static final String CONFIG_USERNAME = "username";
+    private static final String CONFIG_PASSWORD = "password";
+
+    private static final String DEFAULT_URL        = "http://localhost:8080";;
+    private static final String DEFAULT_GRAPHSPACE = "DEFAULT";
+
+    // Mirrors RangerHugeGraphPlugin.RES_* — kept as local literals so this
+    // Admin-side class stays free of any dependency on the plugin class.
+    private static final String RES_GRAPHSPACE    = "graphspace";
+    private static final String RES_GRAPH         = "graph";
+    private static final String RES_RESOURCE_TYPE = "resource-type";
+    private static final String RES_LABEL         = "label";
+
+    private static final int CONNECT_TIMEOUT = 5000;
+
+    /*
+     * Mirrors org.apache.hugegraph.auth.ResourceType, lowercased exactly the 
way
+     * RangerHugeGraphPlugin.buildRequest() lowercases it when populating the
+     * "resource-type" element of a RangerAccessResource. Keep in sync with 
that
+     * enum; it cannot be referenced directly (see the class javadoc).
+     */
+    private static final List<String> RESOURCE_TYPES = 
Collections.unmodifiableList(
+            Arrays.asList("none", "status", "vertex", "edge", "vertex_aggr", 
"edge_aggr",
+                          "var", "gremlin", "task", "property_key", 
"vertex_label",
+                          "edge_label", "index_label", "schema", "meta", 
"all", "grant",
+                          "user_group", "project", "target", "metrics", 
"root"));
+
+    public RangerHugeGraphService() {
+        // Required: no-arg constructor for Ranger Admin Class.newInstance()
+    }
+
+    /**
+     * Tests connectivity to HugeGraph by making a GET request to /versions.
+     * Called by Ranger Admin's "Test Connection" button.
+     */
+    @Override
+    public HashMap<String, Object> validateConfig() throws Exception {
+        HashMap<String, Object> result = new HashMap<>();
+
+        String testUrl = baseUrl() + "/versions";
+        LOG.info("Testing connection to HugeGraph at {}", testUrl);
+
+        try {
+            HttpURLConnection conn = openConnection(testUrl);
+            int code;
+            try {
+                code = conn.getResponseCode();
+            } finally {
+                conn.disconnect();
+            }
+
+            // 401/403 mean HugeGraph is up but rejected the configured
+            // credentials — the endpoint is still reachable.
+            if (code == 200 || code == 401 || code == 403) {

Review Comment:
   Minor: Test Connection reports success on HTTP 401 and 403. A wrong username 
or password in the service config therefore looks fine in Ranger Admin, and 
then policy-editor autocomplete quietly returns empty lists, because 
`getJson()` throws on any non-200 and `lookupResource()` swallows the error. 
Fix: report 401 and 403 as a failed check, with a message saying the 
credentials were rejected.



##########
hugegraph-server/hugegraph-ranger-plugin/src/main/java/org/apache/hugegraph/ranger/RangerAuthManager.java:
##########
@@ -0,0 +1,767 @@
+/*
+ * 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.hugegraph.ranger;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.security.sasl.AuthenticationException;
+
+import org.apache.hugegraph.auth.AuthManager;
+import org.apache.hugegraph.auth.HugeAccess;
+import org.apache.hugegraph.auth.HugeAuthenticator;
+import org.apache.hugegraph.auth.HugeBelong;
+import org.apache.hugegraph.auth.HugeDefaultRole;
+import org.apache.hugegraph.auth.HugeGroup;
+import org.apache.hugegraph.auth.HugePermission;
+import org.apache.hugegraph.auth.HugeProject;
+import org.apache.hugegraph.auth.HugeTarget;
+import org.apache.hugegraph.auth.HugeUser;
+import org.apache.hugegraph.auth.ResourceType;
+import org.apache.hugegraph.auth.RolePermission;
+import org.apache.hugegraph.auth.SchemaDefine.AuthElement;
+import org.apache.hugegraph.auth.UserWithRole;
+import org.apache.hugegraph.backend.id.Id;
+import org.apache.hugegraph.util.JsonUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * AuthManager implementation that:
+ * <ul>
+ *   <li>Delegates all identity-management operations (users, groups, tokens) 
to
+ *       the underlying {@link AuthManager} (StandardAuthManager /
+ *       StandardAuthManagerV2).</li>
+ *   <li>Intercepts {@link #validateUser} / {@link #rolePermission} to merge
+ *       Ranger policy decisions into the returned {@link RolePermission}.</li>
+ * </ul>
+ *
+ * <h3>Ranger → RolePermission mapping</h3>
+ * <pre>
+ *   Ranger access type  │  HugePermission
+ *   ────────────────────┼────────────────
+ *   read                │  READ
+ *   write               │  WRITE
+ *   delete              │  DELETE
+ *   execute             │  EXECUTE
+ *   admin               │  ADMIN
+ * </pre>
+ */
+public class RangerAuthManager implements AuthManager {
+
+    private static final Logger LOG =
+            LoggerFactory.getLogger(RangerAuthManager.class);
+
+    private final AuthManager delegate;
+    private final RangerHugeGraphPlugin rangerPlugin;
+
+    public RangerAuthManager(AuthManager delegate,
+                             String serviceName,
+                             String rangerConfigFile) {
+        this.delegate = delegate;
+        this.rangerPlugin = new RangerHugeGraphPlugin(serviceName, 
rangerConfigFile);
+        this.rangerPlugin.init();
+    }
+
+    /**
+     * The underlying Ranger plugin instance, used by
+     * {@link RangerHugeGraphAuthenticator#setup} to register real per-request
+     * enforcement via {@code HugeGraphAuthProxy.setResourceAuthorizer}.
+     */
+    public RangerHugeGraphPlugin rangerPlugin() {
+        return this.rangerPlugin;
+    }
+
+    // ------------------------------------------------------------------
+    // Authentication helpers
+    // ------------------------------------------------------------------
+
+    /**
+     * Authenticate by username+password or token, then enrich the returned
+     * role with Ranger policy grants.  Called from
+     * {@link RangerHugeGraphAuthenticator#authenticate}.
+     */
+    public UserWithRole authenticate(String username, String password,
+                                     String token) {
+        UserWithRole base = (token != null && !token.isEmpty()) ?
+                            delegate.validateUser(token) :
+                            delegate.validateUser(username, password);
+        return enrichWithRanger(base);
+    }
+
+    @Override
+    public UserWithRole validateUser(String username, String password) {
+        return enrichWithRanger(delegate.validateUser(username, password));
+    }
+
+    @Override
+    public UserWithRole validateUser(String token) {
+        return enrichWithRanger(delegate.validateUser(token));
+    }
+
+    @Override
+    public RolePermission rolePermission(AuthElement element) {
+        RolePermission base = delegate.rolePermission(element);
+        if (base == null) {
+            base = RolePermission.none();
+        }
+        if (RolePermission.isAdmin(base)) {
+            return base;
+        }
+        String name = (element instanceof HugeUser) ?
+                      ((HugeUser) element).name() :
+                      element.id().asString();
+        // admin is a trusted bootstrap identity (same bypass as
+        // HugeGraphAuthProxy.verifyResPermission() and
+        // RangerHugeGraphAuthenticator.authenticate()) — no Ranger policy is
+        // expected to exist for it, so never probe Ranger on its behalf.
+        if (HugeAuthenticator.USER_ADMIN.equals(name)) {
+            return RolePermission.admin();
+        }
+        return mergeRangerRole(base, name);
+    }
+
+    // ------------------------------------------------------------------
+    // Private: Ranger enrichment
+    // ------------------------------------------------------------------
+
+    private UserWithRole enrichWithRanger(UserWithRole base) {
+        if (base == null || base.username() == null) {
+            return base;
+        }
+        // userId() is null when authentication failed (wrong password or 
unknown user).
+        // Do not grant Ranger permissions to unauthenticated identities.
+        if (base.userId() == null) {
+            return base;
+        }
+        RolePermission role = base.role();
+        if (role != null && RolePermission.isAdmin(role)) {
+            return base;
+        }
+        // admin is a trusted bootstrap identity — never probe Ranger for it,
+        // otherwise every login logs a spurious Denied admin/*/*/all/* audit
+        // entry even though RangerHugeGraphAuthenticator.authenticate()
+        // overrides the role with ROLE_ADMIN right after this call returns.
+        if (HugeAuthenticator.USER_ADMIN.equals(base.username())) {
+            return new UserWithRole(base.userId(), base.username(),
+                                    RolePermission.admin());
+        }
+        RolePermission enriched = mergeRangerRole(
+                role != null ? role : RolePermission.none(),
+                base.username());
+        // No grants from either HugeGraph's own role or Ranger policy: reject 
the
+        // login outright rather than admitting a role with no permissions.
+        //
+        // This MUST be the HugeAuthenticator.ROLE_NONE instance, not a fresh
+        // RolePermission.none(). HugeAuthenticator.verifyRole() tests 
identity:
+        //     return role != ROLE_NONE && role != null;
+        // so a newly built none() is not that reference, passes verifyRole() 
and
+        // logs the user in with an empty role.
+        if (isEffectivelyNone(enriched)) {
+            enriched = HugeAuthenticator.ROLE_NONE;
+        }
+        return new UserWithRole(base.userId(), base.username(), enriched);
+    }
+
+    /**
+     * Query Ranger for {@code username} and merge any grants into {@code 
base}.
+     *
+     * <p>We check one broad wildcard request (graphspace=*, graph=*, type=ALL)
+     * for each permission level.  Fine-grained resource checks are applied
+     * per-request inside {@link org.apache.hugegraph.auth.HugeGraphAuthProxy}
+     * via the normal {@link RolePermission} matching logic, so what we build
+     * here must satisfy that matching algorithm.
+     *
+     * <p>We can't call the protected {@code RolePermission.add()} from outside
+     * the package, so we build a JSON role string and deserialise it instead —
+     * this goes through the same public factory path used everywhere else.
+     */
+    private RolePermission mergeRangerRole(RolePermission base,
+                                           String username) {
+        if (base == null) {
+            base = RolePermission.none();
+        }
+        // Admin check first (cheapest)
+        if (rangerPlugin.isAllowed(username, "*", "*",
+                                   ResourceType.ALL, "*",
+                                   HugePermission.ADMIN)) {
+            return RolePermission.admin();
+        }
+
+        // Build a minimal JSON roles document from the Ranger grants, then
+        // deserialise into a RolePermission and merge with base via its JSON
+        // round-trip (the only public merge path available outside the pkg).
+        // Use "DEFAULT" as graphspace key — RolePerm.matchResource() does an
+        // exact key lookup for requiredResource.graphSpace(), which is 
"DEFAULT".
+        StringBuilder sb = new 
StringBuilder("{\"roles\":{\"DEFAULT\":{\"*\":{");
+        boolean first = true;
+        HugePermission[] probedPerms = {HugePermission.READ, 
HugePermission.WRITE,
+                                        HugePermission.DELETE, 
HugePermission.EXECUTE};
+        for (HugePermission perm : probedPerms) {
+            if (rangerPlugin.isAllowed(username, "*", "*",
+                                       ResourceType.ALL, "*", perm)) {
+                if (!first) {
+                    sb.append(',');
+                }
+                // JSON format: 
"READ":[{"type":"ALL","label":"*","properties":null}]
+                sb.append('"').append(perm.name()).append('"')
+                  .append(":{\"ALL\":[{\"type\":\"ALL\",\"label\":\"*\"}]}");
+                first = false;
+            }
+        }
+        sb.append("}}}}");
+
+        if (first) {
+            // Ranger denied everything — return base unchanged
+            return base;
+        }
+
+        try {
+            RolePermission rangerRole =
+                    RolePermission.fromJson(sb.toString());
+            // Merge: rangerRole entries win, existing base entries are kept
+            return mergeViaJson(base, rangerRole);
+        } catch (Exception e) {
+            LOG.warn("Failed to parse Ranger-derived role JSON, " +
+                     "falling back to local permissions: {}", e.getMessage());
+            return base;
+        }
+    }
+
+    /**
+     * Merge two {@link RolePermission} objects using their JSON 
representation.
+     * This avoids calling the package-private {@code add()} method.
+     */
+    private static RolePermission mergeViaJson(RolePermission a,
+                                               RolePermission b) {
+        if (a == null) {
+            a = RolePermission.none();
+        }
+        if (b == null) {
+            b = RolePermission.none();
+        }
+        if (RolePermission.isAdmin(a) || RolePermission.isAdmin(b)) {
+            return RolePermission.admin();
+        }
+        // Deserialise both, combine role maps at the JSON level
+        String jsonA = a.toJson();
+        String jsonB = b.toJson();
+
+        // If either is empty / NONE just return the other
+        if (isEffectivelyNone(a)) {
+            return b;
+        }
+        if (isEffectivelyNone(b)) {
+            return a;
+        }
+
+        // Both are non-trivial: combine by building a merged JSON manually.
+        // We rely on the fact that toJson() produces {"roles":{...}} and both
+        // have the same outer structure.
+        // Simple approach: the role whose JSON is a superset wins.
+        // For a production deployment you'd use a richer merge strategy; this
+        // is sufficient for the common case where Ranger extends local grants.
+        try {
+            // Re-serialise 'a' roles map merged with 'b'
+            java.util.Map<String, Object> mapA =
+                    JsonUtil.fromJson(jsonA, new 
org.apache.tinkerpop.shaded.jackson.core.type.TypeReference<
+                            java.util.Map<String, Object>>() {});
+            java.util.Map<String, Object> mapB =
+                    JsonUtil.fromJson(jsonB, new 
org.apache.tinkerpop.shaded.jackson.core.type.TypeReference<
+                            java.util.Map<String, Object>>() {});
+            deepMerge(mapA, mapB);
+            return RolePermission.fromJson(JsonUtil.toJson(mapA));
+        } catch (Exception e) {
+            LOG.warn("JSON merge failed, returning base role: {}", 
e.getMessage());
+            return a;
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static void deepMerge(java.util.Map<String, Object> target,
+                                  java.util.Map<String, Object> source) {
+        for (Map.Entry<String, Object> e : source.entrySet()) {
+            String key = e.getKey();
+            Object srcVal = e.getValue();
+            Object tgtVal = target.get(key);
+            if (tgtVal instanceof java.util.Map && srcVal instanceof 
java.util.Map) {
+                deepMerge((java.util.Map<String, Object>) tgtVal,
+                          (java.util.Map<String, Object>) srcVal);
+            } else {
+                target.put(key, srcVal);
+            }
+        }
+    }
+
+    private static boolean isEffectivelyNone(RolePermission role) {
+        return role == null ||
+               role.equals(RolePermission.none()) ||
+               role.roles().isEmpty();
+    }
+
+    // ------------------------------------------------------------------
+    // Pure delegation — identity management
+    // ------------------------------------------------------------------
+
+    @Override
+    public void init() {
+        // The delegate (StandardAuthManager) was already initialised by
+        // GraphFactory.open() before RangerAuthManager was wired in via
+        // HugeGraph.switchAuthManager().  Calling delegate.init() here would
+        // create a cycle:
+        //   RangerAuthManager.init()
+        //     → AuthManagerProxy.init()         (proxy wraps 
RangerAuthManager)
+        //       → RangerAuthManager.init()       (infinite recursion)
+        // So we intentionally skip the delegation.
+    }
+
+    @Override
+    public boolean close() {
+        return delegate.close();
+    }
+
+    @Override
+    public Id createUser(HugeUser user) {
+        // The HugeGraphAuthProxy.AuthManagerProxy.updateCreator() path is not
+        // reachable here (this manager is not wrapped by that proxy), so we
+        // mirror what StandardAuthManagerV2 does: pull the creator from the
+        // current auth context, falling back to "system" for bootstrap calls.
+        if (user.creator() == null) {
+            org.apache.hugegraph.auth.HugeGraphAuthProxy.Context ctx =
+                    org.apache.hugegraph.auth.HugeGraphAuthProxy.getContext();
+            String username = (ctx != null) ? ctx.user().username() : null;
+            user.creator(username != null ? username : "system");
+        }
+        return delegate.createUser(user);
+    }
+
+    @Override
+    public Id updateUser(HugeUser user) {
+        return delegate.updateUser(user);
+    }
+
+    @Override
+    public HugeUser deleteUser(Id id) {
+        return delegate.deleteUser(id);
+    }
+
+    @Override
+    public HugeUser findUser(String name) {
+        return delegate.findUser(name);
+    }
+
+    @Override
+    public HugeUser getUser(Id id) {
+        return delegate.getUser(id);
+    }
+
+    @Override
+    public List<HugeUser> listUsers(List<Id> ids) {
+        return delegate.listUsers(ids);
+    }
+
+    @Override
+    public List<HugeUser> listAllUsers(long limit) {
+        return delegate.listAllUsers(limit);
+    }
+
+    @Override
+    public Id createGroup(HugeGroup group) {
+        return delegate.createGroup(group);
+    }
+
+    @Override
+    public Id updateGroup(HugeGroup group) {
+        return delegate.updateGroup(group);
+    }
+
+    @Override
+    public HugeGroup deleteGroup(Id id) {
+        return delegate.deleteGroup(id);
+    }
+
+    @Override
+    public HugeGroup getGroup(Id id) {
+        return delegate.getGroup(id);
+    }
+
+    @Override
+    public HugeGroup findGroup(String name) {
+        return delegate.findGroup(name);
+    }
+
+    @Override
+    public List<HugeGroup> listGroups(List<Id> ids) {
+        return delegate.listGroups(ids);
+    }
+
+    @Override
+    public List<HugeGroup> listAllGroups(long limit) {
+        return delegate.listAllGroups(limit);
+    }
+
+    @Override
+    public Id createTarget(HugeTarget target) {
+        return delegate.createTarget(target);
+    }
+
+    @Override
+    public Id updateTarget(HugeTarget target) {
+        return delegate.updateTarget(target);
+    }
+
+    @Override
+    public HugeTarget deleteTarget(Id id) {
+        return delegate.deleteTarget(id);
+    }
+
+    @Override
+    public HugeTarget getTarget(Id id) {
+        return delegate.getTarget(id);
+    }
+
+    @Override
+    public List<HugeTarget> listTargets(List<Id> ids) {
+        return delegate.listTargets(ids);
+    }
+
+    @Override
+    public List<HugeTarget> listAllTargets(long limit) {
+        return delegate.listAllTargets(limit);
+    }
+
+    @Override
+    public Id createBelong(HugeBelong belong) {
+        return delegate.createBelong(belong);
+    }
+
+    @Override
+    public Id updateBelong(HugeBelong belong) {
+        return delegate.updateBelong(belong);
+    }
+
+    @Override
+    public HugeBelong deleteBelong(Id id) {
+        return delegate.deleteBelong(id);
+    }
+
+    @Override
+    public HugeBelong getBelong(Id id) {
+        return delegate.getBelong(id);
+    }
+
+    @Override
+    public List<HugeBelong> listBelong(List<Id> ids) {
+        return delegate.listBelong(ids);
+    }
+
+    @Override
+    public List<HugeBelong> listAllBelong(long limit) {
+        return delegate.listAllBelong(limit);
+    }
+
+    @Override
+    public List<HugeBelong> listBelongByUser(Id user, long limit) {
+        return delegate.listBelongByUser(user, limit);
+    }
+
+    @Override
+    public List<HugeBelong> listBelongByGroup(Id group, long limit) {
+        return delegate.listBelongByGroup(group, limit);
+    }
+
+    @Override
+    public Id createAccess(HugeAccess access) {
+        return delegate.createAccess(access);
+    }
+
+    @Override
+    public Id updateAccess(HugeAccess access) {
+        return delegate.updateAccess(access);
+    }
+
+    @Override
+    public HugeAccess deleteAccess(Id id) {
+        return delegate.deleteAccess(id);
+    }
+
+    @Override
+    public HugeAccess getAccess(Id id) {
+        return delegate.getAccess(id);
+    }
+
+    @Override
+    public List<HugeAccess> listAccess(List<Id> ids) {
+        return delegate.listAccess(ids);
+    }
+
+    @Override
+    public List<HugeAccess> listAllAccess(long limit) {
+        return delegate.listAllAccess(limit);
+    }
+
+    @Override
+    public List<HugeAccess> listAccessByGroup(Id group, long limit) {
+        return delegate.listAccessByGroup(group, limit);
+    }
+
+    @Override
+    public List<HugeAccess> listAccessByTarget(Id target, long limit) {
+        return delegate.listAccessByTarget(target, limit);
+    }
+
+    @Override
+    public Id createProject(HugeProject project) {
+        return delegate.createProject(project);
+    }
+
+    @Override
+    public HugeProject deleteProject(Id id) {
+        return delegate.deleteProject(id);
+    }
+
+    @Override
+    public Id updateProject(HugeProject project) {
+        return delegate.updateProject(project);
+    }
+
+    @Override
+    public Id projectAddGraphs(Id id, Set<String> graphs) {
+        return delegate.projectAddGraphs(id, graphs);
+    }
+
+    @Override
+    public Id projectRemoveGraphs(Id id, Set<String> graphs) {
+        return delegate.projectRemoveGraphs(id, graphs);
+    }
+
+    @Override
+    public HugeProject getProject(Id id) {
+        return delegate.getProject(id);
+    }
+
+    @Override
+    public List<HugeProject> listAllProject(long limit) {
+        return delegate.listAllProject(limit);
+    }
+
+    @Override
+    public HugeUser matchUser(String name, String password) {
+        return delegate.matchUser(name, password);
+    }
+
+    @Override
+    public String loginUser(String username, String password)
+            throws AuthenticationException {
+        return delegate.loginUser(username, password);
+    }
+
+    @Override
+    public String loginUser(String username, String password, long expire)
+            throws AuthenticationException {
+        return delegate.loginUser(username, password, expire);
+    }
+
+    @Override
+    public void logoutUser(String token) {
+        delegate.logoutUser(token);
+    }
+
+    @Override
+    public Set<String> listWhiteIPs() {
+        return delegate.listWhiteIPs();
+    }
+
+    @Override
+    public void setWhiteIPs(Set<String> whiteIpList) {
+        delegate.setWhiteIPs(whiteIpList);
+    }
+
+    @Override
+    public boolean getWhiteIpStatus() {
+        return delegate.getWhiteIpStatus();
+    }
+
+    @Override
+    public void enabledWhiteIpList(boolean status) {
+        delegate.enabledWhiteIpList(status);
+    }
+
+    @Override
+    public Id createSpaceManager(String graphSpace, String owner) {
+        return delegate.createSpaceManager(graphSpace, owner);
+    }
+
+    @Override
+    public void deleteSpaceManager(String graphSpace, String owner) {
+        delegate.deleteSpaceManager(graphSpace, owner);
+    }
+
+    @Override
+    public List<String> listSpaceManager(String graphSpace) {
+        return delegate.listSpaceManager(graphSpace);
+    }
+
+    @Override
+    public boolean isSpaceManager(String owner) {
+        return delegate.isSpaceManager(owner);
+    }
+
+    @Override
+    public boolean isSpaceManager(String graphSpace, String owner) {
+        return delegate.isSpaceManager(graphSpace, owner);
+    }
+
+    @Override
+    public Id createSpaceMember(String graphSpace, String user) {
+        return delegate.createSpaceMember(graphSpace, user);
+    }
+
+    @Override
+    public void deleteSpaceMember(String graphSpace, String user) {
+        delegate.deleteSpaceMember(graphSpace, user);
+    }
+
+    @Override
+    public List<String> listSpaceMember(String graphSpace) {
+        return delegate.listSpaceMember(graphSpace);
+    }
+
+    @Override
+    public boolean isSpaceMember(String graphSpace, String user) {
+        return delegate.isSpaceMember(graphSpace, user);
+    }
+
+    @Override
+    public Id createAdminManager(String user) {
+        return delegate.createAdminManager(user);
+    }
+
+    @Override
+    public void deleteAdminManager(String user) {
+        delegate.deleteAdminManager(user);
+    }
+
+    @Override
+    public List<String> listAdminManager() {
+        return delegate.listAdminManager();
+    }
+
+    @Override
+    public boolean isAdminManager(String user) {
+        return delegate.isAdminManager(user);
+    }
+
+    // The methods below were added to the AuthManager interface in a source
+    // version newer than the currently installed hugegraph-core jar.
+    // We omit @Override and use reflection so this module compiles against
+    // both old and new installed jars.
+
+    public void setDefaultGraph(String graphSpace, String graph, String user) {
+        callDelegate("setDefaultGraph", Void.class,
+                     new Class[]{String.class, String.class, String.class},
+                     graphSpace, graph, user);
+    }
+
+    public void unsetDefaultGraph(String graphSpace, String graph,
+                                  String user) {
+        callDelegate("unsetDefaultGraph", Void.class,
+                     new Class[]{String.class, String.class, String.class},
+                     graphSpace, graph, user);
+    }
+
+    @SuppressWarnings("unchecked")
+    public Map<String, Date> getDefaultGraph(String graphSpace, String user) {
+        return (Map<String, Date>) callDelegate("getDefaultGraph", 
Object.class,
+                new Class[]{String.class, String.class},
+                graphSpace, user);
+    }
+
+    public Id createDefaultRole(String graphSpace, String owner,
+                                HugeDefaultRole role, String graph) {
+        return callDelegate("createDefaultRole", Id.class,
+                            new Class[]{String.class, String.class,
+                                        HugeDefaultRole.class, String.class},
+                            graphSpace, owner, role, graph);
+    }
+
+    public Id createSpaceDefaultRole(String graphSpace, String owner,
+                                     HugeDefaultRole role) {
+        return callDelegate("createSpaceDefaultRole", Id.class,
+                            new Class[]{String.class, String.class, 
HugeDefaultRole.class},
+                            graphSpace, owner, role);
+    }
+
+    public boolean isDefaultRole(String graphSpace, String owner,
+                                 HugeDefaultRole role) {
+        return Boolean.TRUE.equals(callDelegate("isDefaultRole", Boolean.class,
+                                                new Class[]{String.class, 
String.class,
+                                                            
HugeDefaultRole.class},
+                                                graphSpace, owner, role));
+    }
+
+    public boolean isDefaultRole(String graphSpace, String graph,
+                                 String owner, HugeDefaultRole role) {
+        return Boolean.TRUE.equals(callDelegate("isDefaultRole", Boolean.class,
+                                                new Class[]{String.class, 
String.class,
+                                                            String.class, 
HugeDefaultRole.class},
+                                                graphSpace, graph, owner, 
role));
+    }
+
+    public void deleteDefaultRole(String graphSpace, String owner,
+                                  HugeDefaultRole role) {
+        callDelegate("deleteDefaultRole", Void.class,
+                     new Class[]{String.class, String.class, 
HugeDefaultRole.class},
+                     graphSpace, owner, role);
+    }
+
+    public void deleteDefaultRole(String graphSpace, String owner,
+                                  HugeDefaultRole role, String graph) {
+        callDelegate("deleteDefaultRole", Void.class,
+                     new Class[]{String.class, String.class,
+                                 HugeDefaultRole.class, String.class},
+                     graphSpace, owner, role, graph);
+    }
+
+    @SuppressWarnings("unchecked")
+    private <T> T callDelegate(String methodName, Class<T> returnType,
+                                Class<?>[] paramTypes, Object... args) {
+        try {
+            java.lang.reflect.Method m =
+                    delegate.getClass().getMethod(methodName, paramTypes);

Review Comment:
   Important: Blocking: yes. Summary: With the Ranger authenticator enabled, 
every default-graph and default-role call fails with `IllegalAccessException`, 
so those REST endpoints return 500. The delegate is 
`HugeGraphAuthProxy.AuthManagerProxy`, a package-private inner class in 
`org.apache.hugegraph.auth`. `getClass().getMethod(...)` returns the method 
declared on that class, and `Method.invoke` from `org.apache.hugegraph.ranger` 
is refused. The affected callers reach this class through 
`GraphManager.authManager()`: `GraphsAPI` (`setDefaultGraph`, 
`getDefaultGraph`), `GraphSpaceAPI` and `ManagerAPI` (`createDefaultRole`, 
`createSpaceDefaultRole`, `isDefaultRole`, `deleteDefaultRole`). Evidence: 
`RangerHugeGraphAuthenticator.setup()` passes `this.graph.authManager()` as the 
delegate. With `gremlin.graph=org.apache.hugegraph.auth.HugeFactoryAuthProxy`, 
that is `AuthManagerProxy`, declared as `class AuthManagerProxy implements 
AuthManager` (HugeGraphAuthProxy.java line 1592). A two-pac
 kage repro of the same shape on JDK 17 throws `IllegalAccessException: class 
b.Main cannot access a member of class a.Outer$AuthManagerProxy with modifiers 
"public"`, while the direct interface call works. All of these methods are 
already declared on `AuthManager` at this head (AuthManager.java lines 
259-281), so the reflection is not needed. Fix: call 
`delegate.setDefaultGraph(...)` and the others directly, mark them `@Override`, 
and remove `callDelegate`.



##########
hugegraph-server/pom.xml:
##########
@@ -350,6 +349,31 @@
     </build>
 
     <profiles>
+        <!--
+          The Ranger authorization plugin is opt-in. It is built only with
+          -Dwith-ranger-plugin, the same property that makes hugegraph-dist
+          bundle the shaded jar into the distribution's lib/.
+
+          Keeping the module out of the default reactor also keeps its
+          dependency tree out of 
install-dist/scripts/dependency/known-dependencies.txt
+          (and therefore out of release-docs/LICENSE), since that manifest is
+          generated by resolving every module in the reactor.
+
+          Trade-off: the module is not compiled by a default `mvn install`, so
+          API drift in hugegraph-core/hugegraph-api will not surface until
+          someone builds with the flag.
+        -->
+        <profile>
+            <id>with-ranger-plugin</id>
+            <activation>
+                <property>
+                    <name>with-ranger-plugin</name>
+                </property>
+            </activation>
+            <modules>
+                <module>hugegraph-ranger-plugin</module>

Review Comment:
   Important: Blocking: yes. Summary: The new module is built only with 
`-Dwith-ranger-plugin`, and no workflow passes that flag. CI never compiles the 
module, and `RangerHugeGraphPluginTest` never runs. The comment above names the 
trade-off, but it means the next change to `AuthManager` or 
`HugeGraphAuthProxy` can break `RangerAuthManager` without anyone noticing. The 
authorizer branch added to `HugeGraphAuthProxy.verifyResPermission` also has no 
test: no test registers a `ResourceAuthorizer`. A test that did would have 
caught the rate-limiter allow above. Evidence: `git grep ranger -- .github/` at 
this head returns nothing, and every check run on this head builds only the 
default reactor. Fix: add a CI job (or a server-ci step) that builds and tests 
the module with `-Dwith-ranger-plugin`. Also add a `HugeGraphAuthProxyTest` 
case that registers an authorizer denying one label and checks that a burst of 
element reads filters out every element with that label.



##########
pom.xml:
##########
@@ -218,6 +221,10 @@
                             <!-- sources generated by gRPC -->
                             
<exclude>**/src/main/java/org/apache/hugegraph/pd/grpc/**</exclude>
                             
<exclude>**/src/main/java/org/apache/hugegraph/store/grpc/**</exclude>
+                            <!-- Docker local data and notebook -->
+                            <exclude>docker/data/**</exclude>
+                            <exclude>docker/.ipynb_checkpoints/**</exclude>
+                            
<exclude>docker/aml_sql_showcase_hugegraph.ipynb</exclude>

Review Comment:
   Minor: The 3713-line AML Gremlin notebook (`docker/aml_sql_*.ipynb`) has 
nothing to do with the Ranger plugin, and neither do the RAT excludes for it, 
for `docker/data/**` and for `docker/.ipynb_checkpoints/**`. The notebook also 
points at a `docker/cloud-storage` stack that is not in the repository. Fix: 
drop the notebook and these three excludes from this PR, and send them 
separately if they are wanted.



##########
hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java:
##########
@@ -1220,6 +1293,30 @@ else if (ro.type().isGrantOrUser()) {
         return result;
     }
 
+    /**
+     * Consult the registered external ResourceAuthorizer for a single
+     * ResourceObject. Repeated calls with the same
+     * (username, graphSpace, graph, resourceType) shape — as happens while
+     * filtering a large traversal result one element at a time — are
+     * rate-limited so audit volume stays bounded; once the limiter is
+     * exhausted for a shape, further calls in that window are allowed
+     * through without re-consulting the authorizer (the earlier calls in
+     * the same window already established the decision).
+     */
+    private boolean checkResourceAuthorizer(String username, HugePermission 
actionPerm,
+                                             ResourceObject<?> ro) {
+        String key = username + "/" + ro.graphSpace() + "/" + ro.graph() + "/" 
+ ro.type();
+        Id limiterId = IdGenerator.of(key);
+        RateLimiter limiter = this.resourceAuthLimiters.getOrFetch(limiterId, 
id -> {
+            return RateLimiter.create(this.auditLogMaxRate);
+        });
+        if (!limiter.tryAcquire()) {
+            return true;

Review Comment:
   Critical: Blocking: yes. Summary: When the per-shape rate limiter has no 
permit left, `checkResourceAuthorizer()` returns `true` without asking Ranger, 
so a Ranger deny is skipped for almost every check in a burst. The limiter key 
is `username/graphSpace/graph/type`, with no label or action in it. One allowed 
read of `person` uses up the permit, and a read of `software` in the same 
millisecond is then allowed even when a Ranger deny policy covers it. That is 
the setup the integration guide uses to narrow access (a wildcard allow plus a 
per-label deny). Listing vertices or edges checks every element back to back, 
so a plain `GET .../graph/vertices` returns the denied labels. Evidence: 
`auth.audit_log_rate` defaults to 1000/s, so `RateLimiter.create(1000)` hands 
out one permit per millisecond. A local run with Guava 32.1.2 that called 
`tryAcquire()` 10000 times in a loop on `RateLimiter.create(1000.0)` got a 
permit 2 times and `false` 9998 times. This method turns every one of those
  `false` results into an allow. Fix: always ask the authorizer for the 
decision. If audit volume is the concern, rate-limit the audit output, or cache 
the decision per user, graph space, graph, type, label and action. Never fall 
back to allow.



##########
hugegraph-server/hugegraph-ranger-plugin/src/main/java/org/apache/hugegraph/ranger/RangerAuthManager.java:
##########
@@ -0,0 +1,767 @@
+/*
+ * 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.hugegraph.ranger;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.security.sasl.AuthenticationException;
+
+import org.apache.hugegraph.auth.AuthManager;
+import org.apache.hugegraph.auth.HugeAccess;
+import org.apache.hugegraph.auth.HugeAuthenticator;
+import org.apache.hugegraph.auth.HugeBelong;
+import org.apache.hugegraph.auth.HugeDefaultRole;
+import org.apache.hugegraph.auth.HugeGroup;
+import org.apache.hugegraph.auth.HugePermission;
+import org.apache.hugegraph.auth.HugeProject;
+import org.apache.hugegraph.auth.HugeTarget;
+import org.apache.hugegraph.auth.HugeUser;
+import org.apache.hugegraph.auth.ResourceType;
+import org.apache.hugegraph.auth.RolePermission;
+import org.apache.hugegraph.auth.SchemaDefine.AuthElement;
+import org.apache.hugegraph.auth.UserWithRole;
+import org.apache.hugegraph.backend.id.Id;
+import org.apache.hugegraph.util.JsonUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * AuthManager implementation that:
+ * <ul>
+ *   <li>Delegates all identity-management operations (users, groups, tokens) 
to
+ *       the underlying {@link AuthManager} (StandardAuthManager /
+ *       StandardAuthManagerV2).</li>
+ *   <li>Intercepts {@link #validateUser} / {@link #rolePermission} to merge
+ *       Ranger policy decisions into the returned {@link RolePermission}.</li>
+ * </ul>
+ *
+ * <h3>Ranger → RolePermission mapping</h3>
+ * <pre>
+ *   Ranger access type  │  HugePermission
+ *   ────────────────────┼────────────────
+ *   read                │  READ
+ *   write               │  WRITE
+ *   delete              │  DELETE
+ *   execute             │  EXECUTE
+ *   admin               │  ADMIN
+ * </pre>
+ */
+public class RangerAuthManager implements AuthManager {
+
+    private static final Logger LOG =
+            LoggerFactory.getLogger(RangerAuthManager.class);
+
+    private final AuthManager delegate;
+    private final RangerHugeGraphPlugin rangerPlugin;
+
+    public RangerAuthManager(AuthManager delegate,
+                             String serviceName,
+                             String rangerConfigFile) {
+        this.delegate = delegate;
+        this.rangerPlugin = new RangerHugeGraphPlugin(serviceName, 
rangerConfigFile);
+        this.rangerPlugin.init();
+    }
+
+    /**
+     * The underlying Ranger plugin instance, used by
+     * {@link RangerHugeGraphAuthenticator#setup} to register real per-request
+     * enforcement via {@code HugeGraphAuthProxy.setResourceAuthorizer}.
+     */
+    public RangerHugeGraphPlugin rangerPlugin() {
+        return this.rangerPlugin;
+    }
+
+    // ------------------------------------------------------------------
+    // Authentication helpers
+    // ------------------------------------------------------------------
+
+    /**
+     * Authenticate by username+password or token, then enrich the returned
+     * role with Ranger policy grants.  Called from
+     * {@link RangerHugeGraphAuthenticator#authenticate}.
+     */
+    public UserWithRole authenticate(String username, String password,
+                                     String token) {
+        UserWithRole base = (token != null && !token.isEmpty()) ?
+                            delegate.validateUser(token) :
+                            delegate.validateUser(username, password);
+        return enrichWithRanger(base);

Review Comment:
   Minor: Every authenticated REST request goes through this method, and 
`enrichWithRanger()` then makes up to five `isAccessAllowed` calls (ADMIN, 
READ, WRITE, DELETE and EXECUTE on `*/*/all/*`). `RangerHugeGraphPlugin.init()` 
sets `RangerDefaultAuditHandler`, so each of these calls is audited. Ranger 
therefore gets up to five synthetic audit records per request, on top of the 
real per-resource checks. Evidence: `AuthenticationFilter.filter()` calls 
`authenticate()` on every non-whitelisted request, which reaches 
`GraphManager.authenticate()`, then 
`RangerHugeGraphAuthenticator.authenticate()`, then this method. The probes are 
in `mergeRangerRole()` (lines 202-228). Fix: cache the merged role per user for 
a short TTL, the way `usersRoleCache` does for the local role, or turn off 
auditing for these probe requests.



##########
docker/hgRangerIntegration/Dockerfile.ranger:
##########
@@ -0,0 +1,72 @@
+# syntax=docker/dockerfile:1
+#
+# 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.
+
+# Stage 1: build the thin hugegraph-ranger-plugin jar
+FROM --platform=$BUILDPLATFORM maven:3.9.0-eclipse-temurin-11 AS build
+
+WORKDIR /pkg
+
+COPY . .
+
+# This also produces the shaded "-plugin" artifact (~520 MB, mostly the audit
+# backends ranger-plugins-audit requires: Solr, Elasticsearch, Kafka and the 
AWS
+# SDK). That one is for HugeGraph's own lib/ and is NOT copied below: Ranger
+# Admin already has ranger-plugins-common, gson and slf4j on its classpath, so
+# only the thin jar is needed here.
+#
+# -Dwith-ranger-plugin is required, not optional: the plugin is an opt-in 
module,
+# declared under that profile in hugegraph-server/pom.xml. Without the flag the
+# module is not in the reactor and -pl fails with
+# "Could not find the selected project in the reactor".
+RUN --mount=type=cache,target=/root/.m2 \
+    mvn package -pl hugegraph-server/hugegraph-ranger-plugin -am -e -B -ntp \
+        -Dwith-ranger-plugin \
+        -Dmaven.test.skip=true \
+        -Dmaven.javadoc.skip=true \
+        -Drat.skip=true \
+        -Dcheckstyle.skip=true
+
+# Stage 2: bake the service class into the stock Ranger Admin image
+#
+# Must stay in step with the ranger.version property in
+# hugegraph-server/hugegraph-ranger-plugin/pom.xml, which decides the
+# ranger-plugins-common version the jar is compiled against.
+FROM apache/ranger:2.4.0
+
+LABEL maintainer="HugeGraph Docker Maintainers <[email protected]>"
+
+# Ranger Admin loads the service definition's implClass
+# (org.apache.hugegraph.ranger.RangerHugeGraphService, see
+# ranger-servicedef-hugegraph.json) from ranger-plugins/<service-type>/, where
+# <service-type> is the service def name "hugegraph". That directory is scanned
+# once at JVM startup, so baking the jar in is what makes "Test Connection" and
+# the policy editor's resource autocomplete work on a plain `docker compose up`
+# — no copy-into-the-container-and-restart dance.
+#
+# The /opt/ranger/admin symlink is used instead of the versioned
+# ranger-<version>-admin directory so this path survives a base image bump.
+#
+# The base image's bundled Java 8 is sufficient: hugegraph-ranger-plugin sets
+# maven.compiler.target=8 in its POM, so the jar is class file version 52.
+ARG HUGEGRAPH_VERSION=1.7.0

Review Comment:
   Minor: `HUGEGRAPH_VERSION=1.7.0` is hard-coded, but `<revision>` on master 
is now 1.8.0. After the rebase this PR needs, the build stage produces 
`hugegraph-ranger-plugin-1.8.0.jar` and this `COPY` fails. Fix: in the build 
stage, copy the thin jar to a fixed name (or read the version with `mvn 
help:evaluate -Dexpression=project.version`), then `COPY` that path.



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