This is an automated email from the ASF dual-hosted git repository.

imbajin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hugegraph.git


The following commit(s) were added to refs/heads/master by this push:
     new 4f1a8b341 feat(server): integrate Hubble with PD mode (#3143)
4f1a8b341 is described below

commit 4f1a8b34169e021717906167444c75c4c22d426b
Author: imbajin <[email protected]>
AuthorDate: Fri Aug 7 23:07:38 2026 +0800

    feat(server): integrate Hubble with PD mode (#3143)
    
    - Add Hubble to both release and development PD Compose files.
    - Configure Hubble to discover the `hg` cluster through PD and reach Server 
through the Docker network.
    - Let embedded Gremlin authenticate Bearer JWTs while preserving existing 
Basic authentication.
    - Require a non-default administrator password and validate explicit JWT 
secrets at 32 bytes or longer.
    - Wait for real Server and Hubble health responses instead of treating an 
open port as ready.
---
 .dockerignore                                      |   1 +
 .github/workflows/server-ci.yml                    |  59 ++++++
 .gitignore                                         |   1 +
 docker/README.md                                   | 210 ++++++++++++++++++---
 docker/docker-compose.dev.yml                      |  32 +++-
 docker/docker-compose.yml                          |  34 +++-
 .../hugegraph-hubble.properties                    |  54 ++----
 .../hugegraph/auth/WsAndHttpBasicAuthHandler.java  |  16 ++
 .../docker/docker-entrypoint-test.sh               | 205 ++++++++++++++++++++
 .../hugegraph-dist/docker/docker-entrypoint.sh     | 104 +++++++++-
 .../docker/test/test-docker-entrypoint.sh          |   4 +-
 .../auth/WsAndHttpBasicAuthHandlerTest.java        |  34 +++-
 12 files changed, 673 insertions(+), 81 deletions(-)

diff --git a/.dockerignore b/.dockerignore
index ee61478a1..28d48eb93 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -46,6 +46,7 @@ apache-hugegraph-*/
 # Env files
 .env.local
 .env.*.local
+/docker/.env
 
 # Git internals
 .git
diff --git a/.github/workflows/server-ci.yml b/.github/workflows/server-ci.yml
index 6d5ab2f19..9c4e577d8 100644
--- a/.github/workflows/server-ci.yml
+++ b/.github/workflows/server-ci.yml
@@ -10,10 +10,14 @@ on:
 
 jobs:
   wait-storage-shell-test:
+    permissions:
+      contents: read
     runs-on: ubuntu-22.04
     steps:
       - name: Checkout
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
 
       - name: Run wait-storage.sh peer failover tests
         run: 
hugegraph-server/hugegraph-dist/src/assembly/travis/test-wait-storage.sh
@@ -79,6 +83,61 @@ jobs:
         run: |
           mvn clean compile -U -Dmaven.javadoc.skip=true -ntp
 
+      - name: Validate Docker integration
+        if: ${{ env.BACKEND == 'rocksdb' }}
+        run: |
+          bash hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh
+          check_compose() {
+            local file="$1"
+            local server_policy="$2"
+            local hubble_policy="$3"
+            local rendered
+            rendered="$(mktemp)"
+
+            if env -u HUGEGRAPH_ADMIN_PASSWORD \
+                 docker compose -f "$file" config -q >/dev/null 2>&1; then
+              echo "$file accepted an unset admin password" >&2
+              return 1
+            fi
+            if HUGEGRAPH_ADMIN_PASSWORD= \
+                 docker compose -f "$file" config -q >/dev/null 2>&1; then
+              echo "$file accepted an empty admin password" >&2
+              return 1
+            fi
+
+            HUGEGRAPH_ADMIN_PASSWORD=ci-test-password \
+              docker compose -f "$file" config --format json > "$rendered"
+            jq -e \
+              --arg server_policy "$server_policy" \
+              --arg hubble_policy "$hubble_policy" '
+                .services.server.pull_policy == $server_policy and
+                .services.hubble.pull_policy == $hubble_policy and
+                .services.server.environment.HG_SERVER_USE_PD == "true" and
+                .services.server.environment.HG_SERVER_CLUSTER == "hg" and
+                .services.server.environment.HG_SERVER_REST_URL ==
+                  "http://server:8080"; and
+                .services.server.environment.HG_SERVER_INIT_STORE_ENABLED ==
+                  "false" and
+                .services.server.environment.PASSWORD ==
+                  "ci-test-password" and
+                (.services.server.environment.HG_SERVER_AUTH_TOKEN_SECRET !=
+                  null) and
+                .services.hubble.depends_on.server.condition ==
+                  "service_healthy" and
+                (.services.hubble.healthcheck.test[1] |
+                  contains("http://127.0.0.1:8088/about";) and
+                  contains("\"status\":200") and
+                  contains("\"name\":\"hugegraph-hubble\"")) and
+                any(.services.hubble.ports[];
+                    .target == 8088 and .published == "8088" and
+                    .host_ip == "127.0.0.1")
+              ' "$rendered" >/dev/null
+            rm -f "$rendered"
+          }
+
+          check_compose docker/docker-compose.yml always always
+          check_compose docker/docker-compose.dev.yml build missing
+
       - name: Run check_port unit tests
         if: ${{ env.BACKEND == 'rocksdb' }}
         run: |
diff --git a/.gitignore b/.gitignore
index e27322b73..972d89d35 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,6 +45,7 @@ build/
 .env.development.local
 .env.test.local
 .env.production.local
+docker/.env
 
 *.orig
 *.rej
diff --git a/docker/README.md b/docker/README.md
index e55879874..0ee1f586b 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -4,14 +4,15 @@ This directory contains Docker Compose files for running 
HugeGraph:
 
 | File | Description |
 |------|-------------|
-| `docker-compose.yml` | Single-node cluster using pre-built images from 
Docker Hub |
-| `docker-compose.dev.yml` | Single-node cluster built from source (for 
developers) |
+| `docker-compose.yml` | PD, Store, Server, and Hubble using pre-built images |
+| `docker-compose.dev.yml` | PD, Store, and Server built from source, plus 
Hubble |
 | `docker-compose-3pd-3store-3server.yml` | 3-node distributed cluster (PD + 
Store + Server) |
 
 ## Prerequisites
 
 - **Docker Engine** 20.10+ (or Docker Desktop 4.x+)
 - **Docker Compose** v2 (included in Docker Desktop)
+- **OpenSSL CLI** (used to generate the initial administrator password)
 - **Memory**: Allocate at least **12 GB** to Docker Desktop (Settings → 
Resources → Memory). The 3-node cluster runs 9 JVM processes (3 PD + 3 Store + 
3 Server) which are memory-intensive. Insufficient memory causes OOM kills that 
appear as silent Raft failures.
 
 > [!IMPORTANT]
@@ -20,51 +21,147 @@ This directory contains Docker Compose files for running 
HugeGraph:
 
 ## Single-Node Setup
 
-Two compose files are available for running a single-node cluster (1 PD + 1 
Store + 1 Server):
+Two compose files run one PD, one Store, one Server, and one Hubble instance:
+
+Create a Compose environment file once so every lifecycle command can resolve
+the required administrator password:
+
+```bash
+(
+  set -eu
+  cd docker
+  if [ -e .env ]; then
+    echo "docker/.env already exists; reusing it"
+  else
+    command -v openssl >/dev/null 2>&1
+    admin_password="$(openssl rand -base64 12)"
+    if [ "${#admin_password}" -ne 16 ]; then
+      echo "Failed to generate a 16-character password" >&2
+      exit 1
+    fi
+    install -m 600 /dev/null .env
+    {
+      printf "HUGEGRAPH_ADMIN_PASSWORD='%s'\n" "${admin_password}"
+    } >> .env
+    unset admin_password
+  fi
+  chmod 600 .env
+  if ! env -u HUGEGRAPH_ADMIN_PASSWORD \
+       docker compose -f docker-compose.yml config --quiet ||
+     ! env -u HUGEGRAPH_ADMIN_PASSWORD \
+       docker compose -f docker-compose.dev.yml config --quiet; then
+    echo "docker/.env is incomplete; repair or move it, then retry" >&2
+    exit 1
+  fi
+)
+```
+
+Compose automatically reads `docker/.env` for `up`, `ps`, `stop`, and `down`.
+The generated password is a 16-character, Compose-safe random value. The file
+is excluded from Git and Docker build contexts; keep its permissions restricted
+and source production credentials from your secret manager instead of
+committing them.
 
 ### Option A: Quick Start (pre-built images)
 
 Uses pre-built images from Docker Hub. Best for **end users** who want to run 
HugeGraph quickly.
+Set `HUGEGRAPH_VERSION` to the same published release for PD, Store, Server,
+and Hubble. The authenticated PD/Hubble integration is not present in `1.7.x`;
+if no later compatible release is available, use Option B.
 
 ```bash
-cd docker
-HUGEGRAPH_VERSION=1.7.0 docker compose up -d
+(
+  cd docker
+  HUGEGRAPH_VERSION='<compatible-release-after-1.7.x>' \
+  docker compose up -d
+)
 ```
 
-- Images: `hugegraph/pd:1.7.0`, `hugegraph/store:1.7.0`, 
`hugegraph/server:1.7.0`
+- Images: matching `hugegraph/pd`, `hugegraph/store`, `hugegraph/server`, and
+  `hugegraph/hubble` tags from the selected compatible release
 - `pull_policy: always` — always pulls the specified image tag
 
-> **Note**: Use release tags (e.g., `1.7.0`) for stable deployments. The 
`latest` tag is intended for testing or development only.
+> **Note**: Do not use `latest` to claim a reproducible deployment. Pin a
+> compatible release tag and keep it unchanged for later lifecycle commands.
 - PD healthcheck endpoint: `/v1/health`
-- Single PD, single Store (`HG_PD_INITIAL_STORE_LIST: store:8500`), single 
Server
+- Hubble is available at `http://localhost:8088`; sign in as `admin` with the
+  required `HUGEGRAPH_ADMIN_PASSWORD`
+- Hubble binds to host loopback by default. Set `HUBBLE_PUBLISH_HOST`
+  explicitly only behind an HTTPS reverse proxy and trusted network controls.
+- Hubble uses PD discovery and the Docker-network Server address
 - Server healthcheck endpoint: `/versions`
 
 ### Option B: Development Build (build from source)
 
 Builds images locally from source Dockerfiles. Best for **developers** who 
want to test local changes.
+Build the matching `hugegraph-toolchain` Hubble source as
+`local/hugegraph-hubble:dev` before starting this stack.
 
 ```bash
-cd docker
-docker compose -f docker-compose.dev.yml up -d
+(
+  cd docker
+  HUBBLE_IMAGE=local/hugegraph-hubble:dev \
+  HUBBLE_PULL_POLICY=never \
+  docker compose -f docker-compose.dev.yml up -d
+)
 ```
 
-- Images: built from source via `build: context: ..` with Dockerfiles
-- No `pull_policy` — builds locally, doesn't pull
-- Entrypoint scripts are baked into the built image (no volume mounts)
+- PD, Store, and Server images are built from this repository
+- Hubble uses `HUBBLE_IMAGE` because its source is in `hugegraph-toolchain`
+- Server entrypoint scripts are baked into the built image; Hubble mounts the
+  Docker-local PD configuration
 - PD healthcheck endpoint: `/v1/health`
 - Otherwise identical env vars and structure to the quickstart file
 
+Use the same release tag for Option A lifecycle commands:
+
+```bash
+(
+  cd docker
+  export HUGEGRAPH_VERSION='<same-compatible-release>'
+  docker compose ps
+  docker compose stop
+  docker compose down
+)
+```
+
+Use the development Compose file for every Option B lifecycle command:
+
+```bash
+(
+  cd docker
+  docker compose -f docker-compose.dev.yml ps
+  docker compose -f docker-compose.dev.yml stop
+  docker compose -f docker-compose.dev.yml down
+)
+```
+
 ### Key Differences
 
 | | `docker-compose.yml` (quickstart) | `docker-compose.dev.yml` (dev build) |
 |---|---|---|
 | **Images** | Pull from Docker Hub | Build from source |
 | **Who it's for** | End users | Developers |
-| **pull_policy** | `always` | not set (build) |
+| **Server pull_policy** | `always` | `build` |
+| **Hubble pull_policy** | `always` | `never` in the workflow above (`missing` 
in the Compose file by default) |
 
 **Verify** (both options):
 ```bash
 curl http://localhost:8080/versions
+curl -fsS http://localhost:8088/about
+```
+
+To validate local images without Compose replacing them with remote `latest`:
+
+```bash
+(
+  cd docker
+  HUGEGRAPH_SERVER_IMAGE=local/hugegraph-server:test \
+  HUGEGRAPH_SERVER_PULL_POLICY=never \
+  HUBBLE_IMAGE=local/hugegraph-hubble:test \
+  HUBBLE_PULL_POLICY=never \
+  docker compose up -d --wait
+)
 ```
 
 ---
@@ -159,8 +256,13 @@ Configuration is injected via environment variables. The 
old `docker/configs/app
 |----------|----------|---------|-----------------------------|-------------|
 | `HG_SERVER_BACKEND` | Yes | — | `backend` in `hugegraph.properties` | 
Storage backend (e.g. `hstore`) |
 | `HG_SERVER_PD_PEERS` | Yes | — | `pd.peers` | PD cluster addresses (e.g. 
`pd0:8686,pd1:8686,pd2:8686`) |
+| `HG_SERVER_CLUSTER` | No | — | `cluster` in `rest-server.properties` | PD 
discovery application name; single-node Compose uses `hg` to match Hubble |
+| `HG_SERVER_USE_PD` | No | — | `usePD` in `rest-server.properties` | Enables 
Server PD registration and discovery |
+| `HG_SERVER_REST_URL` | No | — | `restserver.url` | Address registered with 
PD and used by clients |
+| `HG_SERVER_MIN_FREE_MEMORY` | No | — | `restserver.min_free_memory` | 
Minimum free-memory guard in MB; local Compose uses `0` |
+| `HG_SERVER_AUTH_TOKEN_SECRET` | No | generated in auth mode | 
`auth.token_secret` | Shared JWT secret for REST and embedded Gremlin 
authentication; explicit values must be at least 32 bytes |
 | `STORE_REST` | No | — | Used by `wait-partition.sh` | Store REST endpoint 
for partition verification (e.g. `store0:8520`) |
-| `PASSWORD` | No | — | Enables auth mode | Optional authentication password; 
ignored when `HG_SERVER_INIT_STORE_ENABLED` is `false` (see below) |
+| `PASSWORD` | No | — | Enables auth and sets `auth.admin_pa` | Initial 
administrator password; disabled init-store does not read it from stdin, but 
the entrypoint still applies it to the PD bootstrap path |
 | `HG_SERVER_INIT_STORE_ENABLED` | No | `true` | `init_store.enabled` in 
`rest-server.properties` | Set `false` in PD/HStore deployments so init-store 
skips local backend and admin initialization |
 
 > **The built-in authenticator with `HG_SERVER_INIT_STORE_ENABLED=false`
@@ -180,12 +282,74 @@ Configuration is injected via environment variables. The 
old `docker/configs/app
 > one performs the fail-closed check above first, so a marker left by an
 > earlier release or an earlier enabled run cannot bypass it.
 >
-> **`PASSWORD` does not reach that path.** init-store reads it from standard
-> input, and a disabled one returns before doing so. The admin is instead
-> created from `auth.admin_pa`, whose `pa` default is public, so init-store
-> refuses to skip unless it is explicitly set to a non-empty value in a mounted
-> `rest-server.properties`. It applies only when the account is first created,
-> so changing it later does not rotate an existing password.
+> The entrypoint maps **`PASSWORD` to `auth.admin_pa`** before init-store runs.
+> A disabled init-store does not read the password from standard input, but the
+> PD startup path uses the explicit `auth.admin_pa` value when it first creates
+> the administrator. Changing it later does not rotate an existing password.
+
+The single-node Compose files also accept these deployment-level overrides:
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `HUGEGRAPH_SERVER_IMAGE` | `hugegraph/server:<version>` | Complete Server 
image reference |
+| `HUGEGRAPH_SERVER_PULL_POLICY` | `always` (`build` for dev) | Server pull 
policy |
+| `HUBBLE_IMAGE` | `hugegraph/hubble:<version>` | Complete Hubble image 
reference |
+| `HUBBLE_PULL_POLICY` | `always` (`missing` for dev) | Hubble pull policy |
+| `HUBBLE_PUBLISH_HOST` | `127.0.0.1` | Hubble host bind address; remote 
access requires an HTTPS reverse proxy |
+| `HUGEGRAPH_ADMIN_PASSWORD` | required (`docker/.env`) | Initial admin 
password; no public default is provided |
+| `HUGEGRAPH_AUTH_TOKEN_SECRET` | generated | JWT signing secret; explicit 
values must be at least 32 bytes |
+
+When authentication is enabled and no token secret is supplied, the Server
+entrypoint generates a random secret and writes it to both authentication
+configurations. The value is reused on container restart while the container
+filesystem is preserved. To preserve tokens across container recreation,
+generate a compatible secret once and add it to the mode-600 `docker/.env`:
+
+```bash
+(
+  set -euo pipefail
+  cd docker
+  
secret_pattern='^[[:space:]]*(export[[:space:]]+)?HUGEGRAPH_AUTH_TOKEN_SECRET[[:space:]]*='
+  secret_count="$(grep -Ec "${secret_pattern}" .env || true)"
+  case "${secret_count}" in
+    0)
+      command -v openssl >/dev/null 2>&1
+      token_secret="$(openssl rand -hex 32)"
+      LC_ALL=C
+      if (( ${#token_secret} != 64 )); then
+        echo "Failed to generate a 64-character token secret" >&2
+        exit 1
+      fi
+      printf "HUGEGRAPH_AUTH_TOKEN_SECRET='%s'\n" \
+        "${token_secret}" >> .env
+      unset token_secret
+      echo "Generated HUGEGRAPH_AUTH_TOKEN_SECRET"
+      ;;
+    1)
+      token_secret="$(
+        sed -nE \
+          "s/${secret_pattern}'([^']*)'[[:space:]]*$/\\2/p" .env
+      )"
+      LC_ALL=C
+      if (( ${#token_secret} < 32 )); then
+        echo "Existing token secret must use the documented single-quoted" \
+             "format and contain at least 32 bytes; .env was not changed" >&2
+        exit 1
+      fi
+      unset token_secret
+      echo "HUGEGRAPH_AUTH_TOKEN_SECRET already exists; reusing it"
+      ;;
+    *)
+      echo "Duplicate HUGEGRAPH_AUTH_TOKEN_SECRET entries; repair .env" >&2
+      exit 1
+      ;;
+  esac
+  chmod 600 .env
+)
+```
+
+The entrypoint rejects shorter explicit values before changing either Server
+configuration file.
 
 **Deprecated aliases** (still work but log a warning):
 
@@ -199,7 +363,8 @@ Configuration is injected via environment variables. The 
old `docker/configs/app
 ## Port Reference
 
 The table below reflects the published host ports in 
`docker-compose-3pd-3store-3server.yml`.
-The single-node compose file (`docker-compose.yml`) only publishes the 
REST/API ports (`8620`, `8520`, `8080`) by default.
+The single-node Compose file publishes `8620`, `8520`, `8080`, and Hubble
+`8088`; Hubble defaults to host loopback.
 
 | Service | Container Port | Host Port | Protocol | Purpose |
 |---------|---------------|-----------|----------|---------|
@@ -232,6 +397,7 @@ The single-node compose file (`docker-compose.yml`) only 
publishes the REST/API
 | PD | `GET /v1/health` | `200 OK` |
 | Store | `GET /v1/health` | `200 OK` |
 | Server | `GET /versions` | `200 OK` with version JSON |
+| Hubble | `GET /about` | `200` JSON with Hubble name and version |
 
 ---
 
diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml
index aa0736a38..70089f488 100644
--- a/docker/docker-compose.dev.yml
+++ b/docker/docker-compose.dev.yml
@@ -83,6 +83,8 @@ services:
       start_period: 30s
 
   server:
+    image: ${HUGEGRAPH_SERVER_IMAGE:-hugegraph/server:dev}
+    pull_policy: ${HUGEGRAPH_SERVER_PULL_POLICY:-build}
     build:
       context: ..
       dockerfile: hugegraph-server/Dockerfile-hstore
@@ -96,10 +98,38 @@ services:
     environment:
       HG_SERVER_BACKEND: hstore
       HG_SERVER_PD_PEERS: pd:8686
+      HG_SERVER_CLUSTER: hg
+      HG_SERVER_USE_PD: "true"
+      HG_SERVER_REST_URL: http://server:8080
+      HG_SERVER_MIN_FREE_MEMORY: "0"
+      HG_SERVER_INIT_STORE_ENABLED: "false"
+      HG_SERVER_AUTH_TOKEN_SECRET: ${HUGEGRAPH_AUTH_TOKEN_SECRET:-}
+      PASSWORD: ${HUGEGRAPH_ADMIN_PASSWORD:?Set a non-default admin password}
     ports:
       - "8080:8080"
     healthcheck:
-      test: ["CMD-SHELL", "curl -fsS http://localhost:8080/versions >/dev/null 
|| exit 1"]
+      test: ["CMD-SHELL", "curl -fsS http://server:8080/versions >/dev/null || 
exit 1"]
+      interval: 10s
+      timeout: 5s
+      retries: 30
+      start_period: 60s
+
+  hubble:
+    image: ${HUBBLE_IMAGE:-hugegraph/hubble:latest}
+    pull_policy: ${HUBBLE_PULL_POLICY:-missing}
+    container_name: hg-hubble
+    hostname: hubble
+    restart: unless-stopped
+    networks: [hg-net]
+    depends_on:
+      server:
+        condition: service_healthy
+    ports:
+      - "${HUBBLE_PUBLISH_HOST:-127.0.0.1}:8088:8088"
+    volumes:
+      - 
./hugegraph-hubble.properties:/hubble/conf/hugegraph-hubble.properties:ro
+    healthcheck:
+      test: ["CMD-SHELL", "body=$$(curl -fsS http://127.0.0.1:8088/about) && 
printf '%s' \"$$body\" | grep -q '\"status\":200' && printf '%s' \"$$body\" | 
grep -q '\"name\":\"hugegraph-hubble\"'"]
       interval: 10s
       timeout: 5s
       retries: 30
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 953c438f5..0a7a7ea29 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -81,8 +81,8 @@ services:
       start_period: 60s
 
   server:
-    image: hugegraph/server:${HUGEGRAPH_VERSION:-latest}
-    pull_policy: always
+    image: 
${HUGEGRAPH_SERVER_IMAGE:-hugegraph/server:${HUGEGRAPH_VERSION:-latest}}
+    pull_policy: ${HUGEGRAPH_SERVER_PULL_POLICY:-always}
     container_name: hg-server
     hostname: server
     restart: unless-stopped
@@ -93,10 +93,38 @@ services:
     environment:
       HG_SERVER_BACKEND: hstore
       HG_SERVER_PD_PEERS: pd:8686
+      HG_SERVER_CLUSTER: hg
+      HG_SERVER_USE_PD: "true"
+      HG_SERVER_REST_URL: http://server:8080
+      HG_SERVER_MIN_FREE_MEMORY: "0"
+      HG_SERVER_INIT_STORE_ENABLED: "false"
+      HG_SERVER_AUTH_TOKEN_SECRET: ${HUGEGRAPH_AUTH_TOKEN_SECRET:-}
+      PASSWORD: ${HUGEGRAPH_ADMIN_PASSWORD:?Set a non-default admin password}
     ports:
       - "8080:8080"
     healthcheck:
-      test: ["CMD-SHELL", "curl -fsS http://localhost:8080/versions >/dev/null 
|| exit 1"]
+      test: ["CMD-SHELL", "curl -fsS http://server:8080/versions >/dev/null || 
exit 1"]
+      interval: 10s
+      timeout: 5s
+      retries: 30
+      start_period: 60s
+
+  hubble:
+    image: ${HUBBLE_IMAGE:-hugegraph/hubble:${HUGEGRAPH_VERSION:-latest}}
+    pull_policy: ${HUBBLE_PULL_POLICY:-always}
+    container_name: hg-hubble
+    hostname: hubble
+    restart: unless-stopped
+    networks: [hg-net]
+    depends_on:
+      server:
+        condition: service_healthy
+    ports:
+      - "${HUBBLE_PUBLISH_HOST:-127.0.0.1}:8088:8088"
+    volumes:
+      - 
./hugegraph-hubble.properties:/hubble/conf/hugegraph-hubble.properties:ro
+    healthcheck:
+      test: ["CMD-SHELL", "body=$$(curl -fsS http://127.0.0.1:8088/about) && 
printf '%s' \"$$body\" | grep -q '\"status\":200' && printf '%s' \"$$body\" | 
grep -q '\"name\":\"hugegraph-hubble\"'"]
       interval: 10s
       timeout: 5s
       retries: 30
diff --git a/.dockerignore b/docker/hugegraph-hubble.properties
similarity index 53%
copy from .dockerignore
copy to docker/hugegraph-hubble.properties
index ee61478a1..b44255cfc 100644
--- a/.dockerignore
+++ b/docker/hugegraph-hubble.properties
@@ -1,4 +1,3 @@
-#
 # 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.
@@ -6,55 +5,26 @@
 # (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
+#     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.
-#
-
-# IMPORTANT: .dockerignore does NOT inherit .gitignore — patterns must be 
restated.
-
-# Build output
-**/target/
-
-# Pre-extracted release dirs / archives
-apache-hugegraph-*/
-**/*.tar
-**/*.tar.gz
-**/*.zip
-**/*.war
 
-# IDE / OS
-.idea/
-.vscode/
-**/*.iml
-**/*.iws
-**/.DS_Store
+server.host=0.0.0.0
+server.port=8088
 
-# Build / runtime artifacts
-**/logs/
-**/*.log
-**/*.class
-**/gen-java/
-**/upload-files/
-**/build/
-**/node_modules/
+cluster=hg
+idc=docker
 
-# Env files
-.env.local
-.env.*.local
+pd.enabled=true
+server.direct_url=http://server:8080
+pd.peers=pd:8686
+pd.server=pd:8620
 
-# Git internals
-.git
-.gitignore
-.gitattributes
-.github
+operations.store.allowed_targets=[http://store:8520]
 
-# Compose / docs not needed in build context
-**/docker-compose*.yml
-**/docker-compose*.yaml
-**/*.md
-docs/
+# Dashboard is not part of this Compose stack.
+dashboard.address=
diff --git 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java
 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java
index 304e712fd..f18171d55 100644
--- 
a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java
+++ 
b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandler.java
@@ -118,6 +118,13 @@ public class WsAndHttpBasicAuthHandler extends 
SaslAuthenticationHandler {
                         sendError(ctx, msg);
                         return;
                     }
+                } else if ("Bearer".equalsIgnoreCase(scheme)) {
+                    String token = payload.trim();
+                    if (token.isEmpty() || containsWhitespace(token)) {
+                        sendError(ctx, msg);
+                        return;
+                    }
+                    credentials.put(HugeAuthenticator.KEY_TOKEN, token);
                 } else {
                     sendError(ctx, msg);
                     return;
@@ -141,6 +148,15 @@ public class WsAndHttpBasicAuthHandler extends 
SaslAuthenticationHandler {
             }
         }
 
+        private static boolean containsWhitespace(String value) {
+            for (int i = 0; i < value.length(); i++) {
+                if (Character.isWhitespace(value.charAt(i))) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
         private boolean parseBasicCredentials(String encoded,
                                               Map<String, String> credentials) 
{
             byte[] userPass;
diff --git a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh 
b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh
new file mode 100755
index 000000000..4b5dcf5ba
--- /dev/null
+++ b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh
@@ -0,0 +1,205 @@
+#!/bin/bash
+#
+# 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.
+#
+set -euo pipefail
+
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+TEST_HOME=$(mktemp -d "${TMPDIR:-/tmp}/hugegraph-entrypoint-test.XXXXXX")
+trap 'rm -rf "${TEST_HOME}"' EXIT
+
+mkdir -p "${TEST_HOME}/bin" "${TEST_HOME}/conf/graphs" "${TEST_HOME}/docker"
+cp "${SCRIPT_DIR}/docker-entrypoint.sh" "${TEST_HOME}/docker-entrypoint.sh"
+touch "${TEST_HOME}/docker/init_complete"
+
+cat > "${TEST_HOME}/conf/rest-server.properties" <<'EOF'
+restserver.url=http://127.0.0.1:8080
+# usePD=true
+EOF
+cat > "${TEST_HOME}/conf/graphs/hugegraph.properties" <<'EOF'
+backend=rocksdb
+#pd.peers=127.0.0.1:8686
+EOF
+cat > "${TEST_HOME}/bin/start-hugegraph.sh" <<'EOF'
+#!/usr/bin/env bash
+exit 0
+EOF
+cat > "${TEST_HOME}/bin/init-store.sh" <<'EOF'
+#!/usr/bin/env bash
+printf 'called\n' >> ./docker/init-store-calls
+if IFS= read -r password; then
+    printf '%s' "${password}" > ./docker/init-store-password
+fi
+EOF
+cat > "${TEST_HOME}/bin/enable-auth.sh" <<'EOF'
+#!/usr/bin/env bash
+printf 'called\n' >> ./docker/enable-auth-calls
+EOF
+cat > "${TEST_HOME}/bin/wait-partition.sh" <<'EOF'
+#!/usr/bin/env bash
+exit 0
+EOF
+cat > "${TEST_HOME}/bin/wait-storage.sh" <<'EOF'
+#!/usr/bin/env bash
+exit 0
+EOF
+chmod +x "${TEST_HOME}/bin/"*.sh
+
+(
+    cd "${TEST_HOME}"
+    HG_SERVER_BACKEND=hstore \
+    HG_SERVER_PD_PEERS=pd:8686 \
+    HG_SERVER_CLUSTER=hg \
+    HG_SERVER_USE_PD=true \
+    HG_SERVER_REST_URL=http://server:8080 \
+    HG_SERVER_MIN_FREE_MEMORY=0 \
+    HG_SERVER_AUTH_TOKEN_SECRET=12345678901234567890123456789012 \
+        bash ./docker-entrypoint.sh
+)
+[[ "$(wc -l < "${TEST_HOME}/docker/init-store-calls")" -eq 1 ]]
+
+grep -qx 'backend=hstore' "${TEST_HOME}/conf/graphs/hugegraph.properties"
+grep -qx 'pd.peers=pd:8686' "${TEST_HOME}/conf/graphs/hugegraph.properties"
+grep -qx 'usePD=true' "${TEST_HOME}/conf/rest-server.properties"
+grep -qx 'pd.peers=pd:8686' "${TEST_HOME}/conf/rest-server.properties"
+grep -qx 'cluster=hg' "${TEST_HOME}/conf/rest-server.properties"
+grep -qx 'restserver.url=http://server:8080' \
+    "${TEST_HOME}/conf/rest-server.properties"
+grep -qx 'restserver.min_free_memory=0' \
+    "${TEST_HOME}/conf/rest-server.properties"
+grep -qx 'auth.token_secret=12345678901234567890123456789012' \
+    "${TEST_HOME}/conf/rest-server.properties"
+grep -qx 'auth.token_secret=12345678901234567890123456789012' \
+    "${TEST_HOME}/conf/graphs/hugegraph.properties"
+
+cp "${TEST_HOME}/conf/rest-server.properties" \
+    "${TEST_HOME}/conf/rest-server.properties.before-short-secret"
+cp "${TEST_HOME}/conf/graphs/hugegraph.properties" \
+    "${TEST_HOME}/conf/graphs/hugegraph.properties.before-short-secret"
+if (
+    cd "${TEST_HOME}"
+    PASSWORD=pa \
+    HG_SERVER_AUTH_TOKEN_SECRET=1234567890123456789012345678901 \
+        bash ./docker-entrypoint.sh
+); then
+    echo "short authentication token secret unexpectedly succeeded" >&2
+    exit 1
+fi
+cmp "${TEST_HOME}/conf/rest-server.properties.before-short-secret" \
+    "${TEST_HOME}/conf/rest-server.properties"
+cmp "${TEST_HOME}/conf/graphs/hugegraph.properties.before-short-secret" \
+    "${TEST_HOME}/conf/graphs/hugegraph.properties"
+[[ ! -e "${TEST_HOME}/docker/enable-auth-calls" ]]
+
+sed -i '/^auth\.token_secret=/d' "${TEST_HOME}/conf/rest-server.properties"
+sed -i '/^auth\.token_secret=/d' 
"${TEST_HOME}/conf/graphs/hugegraph.properties"
+(
+    cd "${TEST_HOME}"
+    PASSWORD=pa bash ./docker-entrypoint.sh
+)
+rest_secret=$(sed -n 's/^auth\.token_secret=//p' \
+    "${TEST_HOME}/conf/rest-server.properties")
+graph_secret=$(sed -n 's/^auth\.token_secret=//p' \
+    "${TEST_HOME}/conf/graphs/hugegraph.properties")
+[[ ${#rest_secret} -ge 43 ]]
+[[ "${rest_secret}" == "${graph_secret}" ]]
+grep -qx 'auth.admin_pa=pa' "${TEST_HOME}/conf/rest-server.properties"
+(
+    cd "${TEST_HOME}"
+    PASSWORD=pa bash ./docker-entrypoint.sh
+)
+reused_secret=$(sed -n 's/^auth\.token_secret=//p' \
+    "${TEST_HOME}/conf/rest-server.properties")
+[[ "${reused_secret}" == "${rest_secret}" ]]
+
+sed -i '/^auth\.token_secret=/d' \
+    "${TEST_HOME}/conf/graphs/hugegraph.properties"
+sed -i "s|^auth\\.token_secret=.*|auth.token_secret:  ${rest_secret}|" \
+    "${TEST_HOME}/conf/rest-server.properties"
+(
+    cd "${TEST_HOME}"
+    PASSWORD=pa bash ./docker-entrypoint.sh
+)
+grep -qx "auth.token_secret=${rest_secret}" \
+    "${TEST_HOME}/conf/graphs/hugegraph.properties"
+
+sed -i '/^auth\.token_secret=/d' \
+    "${TEST_HOME}/conf/rest-server.properties"
+sed -i "s|^auth\\.token_secret=.*|auth.token_secret  ${rest_secret}|" \
+    "${TEST_HOME}/conf/graphs/hugegraph.properties"
+(
+    cd "${TEST_HOME}"
+    PASSWORD=pa bash ./docker-entrypoint.sh
+)
+grep -qx "auth.token_secret=${rest_secret}" \
+    "${TEST_HOME}/conf/rest-server.properties"
+
+[[ "$(wc -l < "${TEST_HOME}/docker/init-store-calls")" -eq 5 ]]
+[[ "$(wc -l < "${TEST_HOME}/docker/enable-auth-calls")" -eq 4 ]]
+
+(
+    cd "${TEST_HOME}"
+    PASSWORD=pa \
+    HG_SERVER_AUTH_TOKEN_SECRET='Strong\Secret 9!0123456789abcdef' \
+        bash ./docker-entrypoint.sh
+)
+complex_secret=$(sed -n 's/^auth\.token_secret=//p' \
+    "${TEST_HOME}/conf/rest-server.properties")
+(
+    cd "${TEST_HOME}"
+    PASSWORD=pa bash ./docker-entrypoint.sh
+)
+reused_complex_secret=$(sed -n 's/^auth\.token_secret=//p' \
+    "${TEST_HOME}/conf/rest-server.properties")
+[[ "${reused_complex_secret}" == "${complex_secret}" ]]
+[[ "${reused_complex_secret}" == \
+   'Strong\\Secret\ 9!0123456789abcdef' ]]
+
+(
+    cd "${TEST_HOME}"
+    PASSWORD=pa \
+    HG_SERVER_AUTH_TOKEN_SECRET='SecretEnds 0123456789abcdefABCDE ' \
+        bash ./docker-entrypoint.sh
+)
+trailing_space_secret=$(sed -n 's/^auth\.token_secret=//p' \
+    "${TEST_HOME}/conf/rest-server.properties")
+(
+    cd "${TEST_HOME}"
+    PASSWORD=pa bash ./docker-entrypoint.sh
+)
+reused_trailing_space_secret=$(sed -n 's/^auth\.token_secret=//p' \
+    "${TEST_HOME}/conf/rest-server.properties")
+[[ "${trailing_space_secret}" == \
+   'SecretEnds\ 0123456789abcdefABCDE\ ' ]]
+[[ "${reused_trailing_space_secret}" == "${trailing_space_secret}" ]]
+grep -Fqx 'auth.admin_pa=pa' \
+    "${TEST_HOME}/conf/rest-server.properties"
+
+(
+    cd "${TEST_HOME}"
+    PASSWORD='Strong\Pass 9!' bash ./docker-entrypoint.sh
+)
+grep -Fqx 'auth.admin_pa=Strong\\Pass\ 9!' \
+    "${TEST_HOME}/conf/rest-server.properties"
+
+rm -f "${TEST_HOME}/docker/init_complete"
+(
+    cd "${TEST_HOME}"
+    PASSWORD=-n bash ./docker-entrypoint.sh
+)
+grep -Fqx -- '-n' "${TEST_HOME}/docker/init-store-password"
+
+echo "PASS: Docker entrypoint configures HStore discovery and authentication"
diff --git a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh 
b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh
index 11d2c460e..295fe1e2e 100755
--- a/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh
+++ b/hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh
@@ -26,22 +26,58 @@ mkdir -p "${DOCKER_FOLDER}"
 
 log() { echo "[hugegraph-server-entrypoint] $*"; }
 
-set_prop() {
-    local key="$1" val="$2" file="$3"
+encode_prop_value() {
+    local value="$1" encoded="" char
+    local i
+
+    LC_ALL=C
+    for ((i = 0; i < ${#value}; i++)); do
+        char="${value:i:1}"
+        case "${char}" in
+            "\\") encoded+="\\\\" ;;
+            " ") encoded+="\\ " ;;
+            $'\t') encoded+="\\t" ;;
+            $'\n') encoded+="\\n" ;;
+            $'\r') encoded+="\\r" ;;
+            $'\f') encoded+="\\f" ;;
+            *) encoded+="${char}" ;;
+        esac
+    done
+    printf '%s' "${encoded}"
+}
+
+set_prop_encoded() {
+    local key="$1" encoded_val="$2" file="$3"
     local esc_key esc_val key_re
 
     esc_key=$(printf '%s' "$key" | sed -e 's/[][(){}.^$*+?|\\/]/\\&/g')
-    esc_val=$(printf '%s' "$val" | sed -e 's/[&|\\~]/\\&/g')
+    esc_val=$(printf '%s' "$encoded_val" | sed -e 's/[&|\\~]/\\&/g')
     
key_re="^[[:space:]]*${esc_key}([[:space:]]*[:=]|[[:space:]]+|[[:space:]]*$)"
 
     if grep -qE "${key_re}" "${file}"; then
         sed -ri "0,/${key_re}/!{/${key_re}/d;}" "${file}"
         sed -ri "0,/${key_re}/s~${key_re}.*~${key}=${esc_val}~" "${file}"
     else
-        printf '%s=%s\n' "$key" "$val" >> "${file}"
+        printf '%s=%s\n' "$key" "$encoded_val" >> "${file}"
     fi
 }
 
+set_prop() {
+    local key="$1" val="$2" file="$3"
+
+    set_prop_encoded "$key" "$(encode_prop_value "$val")" "$file"
+}
+
+get_prop_encoded() {
+    local key="$1" file="$2"
+    local esc_key
+
+    esc_key=$(printf '%s' "$key" | sed -e 's/[][(){}.^$*+?|\\/]/\\&/g')
+    sed -nE \
+        
"s~^[[:space:]]*${esc_key}([[:space:]]*[:=][[:space:]]*|[[:space:]]+)(.*)$~\\2~p"
 \
+        "${file}" | head -n 1
+}
+
 migrate_env() {
     local old_name="$1" new_name="$2"
 
@@ -54,9 +90,60 @@ migrate_env() {
 migrate_env "BACKEND"  "HG_SERVER_BACKEND"
 migrate_env "PD_PEERS" "HG_SERVER_PD_PEERS"
 
+if [[ -n "${HG_SERVER_AUTH_TOKEN_SECRET:-}" ]]; then
+    LC_ALL=C
+    if (( ${#HG_SERVER_AUTH_TOKEN_SECRET} < 32 )); then
+        log "ERROR: HG_SERVER_AUTH_TOKEN_SECRET must be at least 32 bytes"
+        exit 1
+    fi
+fi
+
+AUTH_TOKEN_SECRET_ENCODED=""
+if [[ -n "${PASSWORD:-}" && -z "${HG_SERVER_AUTH_TOKEN_SECRET:-}" ]]; then
+    rest_secret=$(get_prop_encoded "auth.token_secret" "${REST_SERVER_CONF}")
+    graph_secret=$(get_prop_encoded "auth.token_secret" "${GRAPH_CONF}")
+    if [[ -n "${rest_secret}" ]]; then
+        AUTH_TOKEN_SECRET_ENCODED="${rest_secret}"
+        if [[ -n "${graph_secret}" && "${graph_secret}" != "${rest_secret}" 
]]; then
+            log "WARN: authentication token secrets differ; using REST secret"
+        fi
+    elif [[ -n "${graph_secret}" ]]; then
+        AUTH_TOKEN_SECRET_ENCODED="${graph_secret}"
+    else
+        HG_SERVER_AUTH_TOKEN_SECRET=$(head -c 32 /dev/urandom | base64 | tr -d 
'\n')
+        log "generated a shared authentication token secret"
+    fi
+fi
+
 # ── Map env → properties file ─────────────────────────────────────────
 [[ -n "${HG_SERVER_BACKEND:-}"  ]] && set_prop "backend"  
"${HG_SERVER_BACKEND}"  "${GRAPH_CONF}"
 [[ -n "${HG_SERVER_PD_PEERS:-}" ]] && set_prop "pd.peers" 
"${HG_SERVER_PD_PEERS}" "${GRAPH_CONF}"
+[[ -n "${HG_SERVER_USE_PD:-}" ]] && \
+    set_prop "usePD" "${HG_SERVER_USE_PD}" "${REST_SERVER_CONF}"
+[[ -n "${HG_SERVER_PD_PEERS:-}" ]] && \
+    set_prop "pd.peers" "${HG_SERVER_PD_PEERS}" "${REST_SERVER_CONF}"
+[[ -n "${HG_SERVER_CLUSTER:-}" ]] && \
+    set_prop "cluster" "${HG_SERVER_CLUSTER}" "${REST_SERVER_CONF}"
+[[ -n "${HG_SERVER_REST_URL:-}" ]] && set_prop "restserver.url" \
+    "${HG_SERVER_REST_URL}" "${REST_SERVER_CONF}"
+[[ -n "${HG_SERVER_MIN_FREE_MEMORY:-}" ]] && set_prop 
"restserver.min_free_memory" \
+    "${HG_SERVER_MIN_FREE_MEMORY}" "${REST_SERVER_CONF}"
+if [[ -n "${HG_SERVER_AUTH_TOKEN_SECRET:-}" ]]; then
+    set_prop "auth.token_secret" "${HG_SERVER_AUTH_TOKEN_SECRET}" \
+        "${REST_SERVER_CONF}"
+    set_prop "auth.token_secret" "${HG_SERVER_AUTH_TOKEN_SECRET}" 
"${GRAPH_CONF}"
+elif [[ -n "${AUTH_TOKEN_SECRET_ENCODED}" ]]; then
+    set_prop_encoded "auth.token_secret" "${AUTH_TOKEN_SECRET_ENCODED}" \
+        "${REST_SERVER_CONF}"
+    set_prop_encoded "auth.token_secret" "${AUTH_TOKEN_SECRET_ENCODED}" \
+        "${GRAPH_CONF}"
+fi
+if [[ -n "${PASSWORD:-}" ]]; then
+    set_prop "auth.admin_pa" "${PASSWORD}" "${REST_SERVER_CONF}"
+    # This script is idempotent and must run outside the initialization guard:
+    # an upgrade can preserve the marker from an unauthenticated deployment.
+    ./bin/enable-auth.sh
+fi
 
 # Normalized once here and reused by the init-flag guard below. The accepted
 # spellings are the ones HugeConfig accepts, case-insensitive: commons-lang 2.x
@@ -102,16 +189,15 @@ if [[ ! -f "${INIT_MARKER_PATH}" ]]; then
         ./bin/init-store.sh
     else
         log "init hugegraph with auth mode"
-        ./bin/enable-auth.sh
         # init-store reads the password from stdin, and a disabled one returns
         # before it gets there, so say plainly that PASSWORD is being dropped
         case "${INIT_STORE_ENABLED}" in
             n | f | no | off | false)
-                log "WARN: PASSWORD is ignored while init-store is disabled;" \
-                    "the admin is created on the PD startup path from" \
-                    "'auth.admin_pa', which defaults to the public value 'pa'" 
;;
+                log "init-store does not read PASSWORD while disabled;" \
+                    "the entrypoint applies it through 'auth.admin_pa' for" \
+                    "the PD startup path" ;;
         esac
-        echo "${PASSWORD}" | ./bin/init-store.sh
+        printf '%s\n' "${PASSWORD}" | ./bin/init-store.sh
     fi
 else
     log "HugeGraph initialization already done. Revalidating the config..."
diff --git 
a/hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint.sh 
b/hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint.sh
index 1279da555..d5e11c502 100644
--- a/hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint.sh
+++ b/hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint.sh
@@ -24,9 +24,9 @@ test_dir="$(mktemp -d)"
 trap 'rm -rf "${test_dir}"' EXIT
 
 eval "$(awk '
-    /^set_prop\(\) \{/ { capture = 1 }
+    /^encode_prop_value\(\) \{/ { capture = 1 }
     capture { print }
-    capture && /^\}$/ { exit }
+    capture && /^\}$/ && ++function_ends == 3 { exit }
 ' "${entrypoint}")"
 
 assert_replaced() {
diff --git 
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java
 
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java
index bc0c83299..2e486adf5 100644
--- 
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java
+++ 
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/auth/WsAndHttpBasicAuthHandlerTest.java
@@ -132,9 +132,39 @@ public class WsAndHttpBasicAuthHandlerTest {
         channel.finishAndReleaseAll();
     }
 
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testBearerTokenAuthenticatesHttpGremlinRequest()
+            throws Exception {
+        Authenticator authenticator = Mockito.mock(Authenticator.class);
+        AuthenticatedUser user = new AuthenticatedUser("admin");
+        Mockito.when(authenticator.authenticate(Mockito.anyMap()))
+               .thenReturn(user);
+        EmbeddedChannel channel = channel(authenticator);
+
+        channel.writeInbound(request("Bearer server-token"));
+
+        ArgumentCaptor<Map<String, String>> credentials =
+                ArgumentCaptor.forClass(Map.class);
+        Mockito.verify(authenticator, Mockito.times(1))
+               .authenticate(credentials.capture());
+        Assert.assertEquals("server-token", credentials.getValue().get(
+                HugeAuthenticator.KEY_TOKEN));
+        Assert.assertFalse(credentials.getValue().containsKey("username"));
+        Assert.assertFalse(credentials.getValue().containsKey("password"));
+        Assert.assertSame(user,
+                          channel.attr(StateKey.AUTHENTICATED_USER).get());
+        channel.finishAndReleaseAll();
+    }
+
+    @Test
+    public void testWhitespaceOnlyBearerTokenIsRejected() throws Exception {
+        assertUnauthorized("Bearer   ");
+    }
+
     @Test
-    public void testBearerTokenIsRejected() throws Exception {
-        assertUnauthorized("Bearer server-token");
+    public void testBearerTokenWithWhitespaceIsRejected() throws Exception {
+        assertUnauthorized("Bearer token with-space");
     }
 
     @Test

Reply via email to