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

jshao pushed a commit to branch branch-lance-namepspace-dev
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/branch-lance-namepspace-dev by 
this push:
     new eb8ace8ae6 feat(lance): add Lance REST server packaging (#8825)
eb8ace8ae6 is described below

commit eb8ace8ae69e8bdda4bce207f09ab221bf29f48c
Author: Beinan <[email protected]>
AuthorDate: Mon Oct 20 23:41:59 2025 -0700

    feat(lance): add Lance REST server packaging (#8825)
    
    ### What changes were proposed in this pull request?
    
    - add the new lance-common and lance-rest-server modules that bootstrap
    a minimal Lance catalog REST facade backed by in-memory state
    - introduce REST resources (LanceCatalogOperations,
    LanceMetadataOperations) plus a GravitinoLanceRESTServer entry point and
    auxiliary service wiring
    - package assets for the Lance REST server (config + startup script) and
    hook the module into Gradle distribution/standalone tarball tasks with
    checksums
    - extend build dependencies (SLF4J, Dropwizard metrics, Prometheus
    bridge) so the service runs using existing Gravitino telemetry
    components
    - surface scaffolding build.gradle.kts under lance/ to disable blanket
    task execution and ensure module resolution
    
      ### Why are the changes needed?
    
    - the Lance integration needs a thin REST façade to iterate on metadata
    APIs before the backend is available; this scaffolding mirrors the
    Iceberg REST
        flow
    - distribution packaging must include the new service so it can be
    assembled, tested, and deployed alongside other Gravitino auxiliaries
    
      Fix: #N/A
    
      ### Does this PR introduce any user-facing change?
    
    - new Lance REST server tarball and config/script templates shipped with
    the distribution
    - new REST endpoints under /lance/v1/... exposing catalog metadata
    (namespaces, tables)
      - no breaking changes to existing services
    
      ### How was this patch tested?
    
      - ./gradlew :lance:lance-rest-server:build
    - started the service locally with java -cp
    lance/lance-rest-server/build/libs/* ...GravitinoLanceRESTServer
    /tmp/gravitino-lance-rest-server.conf
      - curl http://127.0.0.1:9101/lance/v1/catalog
      - curl http://127.0.0.1:9101/lance/v1/catalog/namespaces
    - curl
    
"http://127.0.0.1:9101/lance/v1/metadata/table?namespace=default&name=sample_table";
    - curl -X POST http://127.0.0.1:9101/lance/v1/catalog/namespaces -H
    'Content-Type: application/json' -d '{"namespace":"demo"}'
    - curl -X DELETE http://127.0.0.1:9101/lance/v1/catalog/namespaces/demo
    
    ---------
    
    Co-authored-by: mchades <[email protected]>
    Co-authored-by: Mini Yu <[email protected]>
    Co-authored-by: Jerry Shao <[email protected]>
---
 bin/gravitino-lance-rest-server.sh.template        | 206 ++++++++++++
 build.gradle.kts                                   |  84 ++++-
 conf/gravitino-lance-rest-server.conf.template     |  45 +++
 .../apache/gravitino/cache/EntityCacheWeigher.java |  26 +-
 .../apache/gravitino/cache/TestCacheConfig.java    | 127 +++++++-
 lance/build.gradle.kts                             |  22 ++
 lance/lance-common/build.gradle.kts                |  43 +++
 .../gravitino/lance/common/config/LanceConfig.java |  65 ++++
 .../lance/common/ops/LanceCatalogService.java      | 352 +++++++++++++++++++++
 lance/lance-rest-server/build.gradle.kts           |  90 ++++++
 .../apache/gravitino/lance/LanceRESTService.java   |  97 ++++++
 .../lance/server/GravitinoLanceRESTServer.java     | 107 +++++++
 .../service/rest/LanceListNamespacesResponse.java  |  63 ++++
 .../service/rest/LanceListTablesResponse.java      |  63 ++++
 .../service/rest/LanceNamespaceOperations.java     |  92 ++++++
 .../server/authorization/MetadataFilterHelper.java |  32 +-
 .../AuthorizationExpressionEvaluator.java          |  53 +++-
 .../TestAuthorizationExpressionEvaluator.java      |  17 +-
 settings.gradle.kts                                |   2 +
 19 files changed, 1560 insertions(+), 26 deletions(-)

diff --git a/bin/gravitino-lance-rest-server.sh.template 
b/bin/gravitino-lance-rest-server.sh.template
new file mode 100644
index 0000000000..17f098903e
--- /dev/null
+++ b/bin/gravitino-lance-rest-server.sh.template
@@ -0,0 +1,206 @@
+#!/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 -ex
+USAGE="-e Usage: bin/gravitino-lance-rest-server.sh [--config <conf-dir>]\n\t
+        {start|run|stop|restart|status}"
+
+if [[ "$1" == "--config" ]]; then
+  shift
+  conf_dir="$1"
+  if [[ ! -d "${conf_dir}" ]]; then
+    echo "ERROR : ${conf_dir} is not a directory"
+    echo ${USAGE}
+    exit 1
+  else
+    export GRAVITINO_CONF_DIR="${conf_dir}"
+  fi
+  shift
+fi
+
+bin="$(dirname "${BASH_SOURCE-$0}")"
+bin="$(cd "${bin}">/dev/null; pwd)"
+
+. "${bin}/common.sh"
+
+check_java_version
+
+function check_process_status() {
+  local pid=$(found_lance_rest_server_pid)
+
+  if [[ -z "${pid}" ]]; then
+    echo "GravitinoLanceRESTServer is not running"
+  else
+    printArt
+    echo "GravitinoLanceRESTServer is running[PID:$pid]"
+  fi
+}
+
+function found_lance_rest_server_pid() {
+  process_name='GravitinoLanceRESTServer';
+  RUNNING_PIDS=$(ps x | grep ${process_name} | grep -v grep | awk '{print 
$1}');
+
+  if [[ -z "${RUNNING_PIDS}" ]]; then
+    return
+  fi
+
+  if ! kill -0 ${RUNNING_PIDS} > /dev/null 2>&1; then
+    echo "GravitinoLanceRESTServer running but process is dead"
+  fi
+
+  echo "${RUNNING_PIDS}"
+}
+
+function wait_for_lance_rest_server_to_die() {
+  timeout=10
+  timeoutTime=$(date "+%s")
+  let "timeoutTime+=$timeout"
+  currentTime=$(date "+%s")
+  forceKill=1
+
+  while [[ $currentTime -lt $timeoutTime ]]; do
+    local pid=$(found_lance_rest_server_pid)
+    if [[ -z "${pid}" ]]; then
+      forceKill=0
+      break
+    fi
+
+    kill ${pid} > /dev/null 2> /dev/null
+    if kill -0 ${pid} > /dev/null 2>&1; then
+      sleep 3
+    else
+      forceKill=0
+      break
+    fi
+    currentTime=$(date "+%s")
+  done
+
+  if [[ $forceKill -ne 0 ]]; then
+    kill -9 ${pid} > /dev/null 2> /dev/null
+  fi
+}
+
+function start() {
+  local pid=$(found_lance_rest_server_pid)
+
+  if [[ ! -z "${pid}" ]]; then
+    if kill -0 ${pid} >/dev/null 2>&1; then
+      echo "GravitinoLanceRESTServer is already running"
+      return 0;
+    fi
+  fi
+
+  if [[ ! -d "${GRAVITINO_LOG_DIR}" ]]; then
+    echo "Log dir doesn't exist, create ${GRAVITINO_LOG_DIR}"
+    mkdir -p "${GRAVITINO_LOG_DIR}"
+  fi
+
+  nohup ${JAVA_RUNNER} ${JAVA_OPTS} ${GRAVITINO_DEBUG_OPTS} -cp 
${GRAVITINO_CLASSPATH} ${GRAVITINO_SERVER_NAME} >> "${GRAVITINO_OUTFILE}" 2>&1 &
+
+  pid=$!
+  if [[ -z "${pid}" ]]; then
+    echo "GravitinoLanceRESTServer start error!"
+    return 1;
+  fi
+
+  sleep 2
+  check_process_status
+}
+
+function run() {
+  ${JAVA_RUNNER} ${JAVA_OPTS} ${GRAVITINO_DEBUG_OPTS} -cp 
${GRAVITINO_CLASSPATH} ${GRAVITINO_SERVER_NAME}
+}
+
+function stop() {
+  local pid
+
+  pid=$(found_lance_rest_server_pid)
+
+  if [[ -z "${pid}" ]]; then
+    echo "GravitinoLanceRESTServer is not running"
+  else
+    wait_for_lance_rest_server_to_die
+    echo "GravitinoLanceRESTServer stop"
+  fi
+}
+
+HOSTNAME=$(hostname)
+GRAVITINO_OUTFILE="${GRAVITINO_LOG_DIR}/gravitino-lance-rest-server.out"
+GRAVITINO_SERVER_NAME=org.apache.gravitino.lance.server.GravitinoLanceRESTServer
+GRAVITINO_SIMPLE_SERVER_NAME=gravitino-lance-rest-server
+
+JAVA_OPTS+=" -Dfile.encoding=UTF-8"
+JAVA_OPTS+=" 
-Dlog4j2.configurationFile=file://${GRAVITINO_CONF_DIR}/log4j2.properties"
+JAVA_OPTS+=" -Dgravitino.log.path=${GRAVITINO_LOG_DIR} ${GRAVITINO_MEM}"
+JAVA_OPTS+=" -Dgravitino.server.name=${GRAVITINO_SIMPLE_SERVER_NAME}"
+if [ "$JVM_VERSION" -eq 17 ]; then
+  JAVA_OPTS+=" -XX:+IgnoreUnrecognizedVMOptions"
+  JAVA_OPTS+=" --add-opens java.base/java.io=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/java.lang.invoke=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/java.lang.reflect=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/java.lang=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/java.math=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/java.net=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/java.nio=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/java.text=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/java.time=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/java.util.concurrent=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/java.util.regex=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/java.util=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/jdk.internal.ref=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.sql/java.sql=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/sun.util.calendar=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/sun.nio.ch=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/sun.nio.cs=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/sun.security.action=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.base/sun.util.calendar=ALL-UNNAMED"
+  JAVA_OPTS+=" --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED"
+fi
+
+#JAVA_OPTS+=" -Djava.security.krb5.conf=/etc/krb5.conf"
+
+if [ -d "${GRAVITINO_HOME}/lance-rest-server/libs" ]; then
+  addJarInDir "${GRAVITINO_HOME}/lance-rest-server/libs"
+  addDirToClasspath "${GRAVITINO_HOME}/lance-rest-server/conf"
+else
+  addJarInDir "${GRAVITINO_HOME}/libs"
+fi
+
+case "${1}" in
+  start)
+    start
+    ;;
+  run)
+    run
+    ;;
+  stop)
+    stop
+    ;;
+  restart)
+    stop
+    start
+    ;;
+  status)
+    check_process_status
+    ;;
+  *)
+    echo ${USAGE}
+esac
diff --git a/build.gradle.kts b/build.gradle.kts
index 0a98b6779b..ee419f23bd 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -683,6 +683,7 @@ tasks {
       "copyCliLib",
       ":authorizations:copyLibAndConfig",
       ":iceberg:iceberg-rest-server:copyLibAndConfigs",
+      ":lance:lance-rest-server:copyLibAndConfigs",
       ":web:web:build"
     )
 
@@ -772,6 +773,51 @@ tasks {
     }
   }
 
+  val compileLanceRESTServer by registering {
+    dependsOn("lance:lance-rest-server:copyLibAndConfigsToStandalonePackage")
+    group = "gravitino distribution"
+    
outputs.dir(projectDir.dir("distribution/${rootProject.name}-lance-rest-server"))
+    doLast {
+      copy {
+        from(projectDir.dir("conf")) {
+          include(
+            "${rootProject.name}-lance-rest-server.conf.template",
+            "${rootProject.name}-env.sh.template",
+            "log4j2.properties.template"
+          )
+          into("${rootProject.name}-lance-rest-server/conf")
+        }
+        from(projectDir.dir("bin")) {
+          include("common.sh.template", 
"${rootProject.name}-lance-rest-server.sh.template")
+          into("${rootProject.name}-lance-rest-server/bin")
+        }
+        into(outputDir)
+        rename { fileName ->
+          fileName.replace(".template", "")
+        }
+        eachFile {
+          if (name == "gravitino-env.sh") {
+            filter { line ->
+              line.replace("GRAVITINO_VERSION_PLACEHOLDER", "$version")
+            }
+          }
+        }
+        fileMode = 0b111101101
+      }
+
+      copy {
+        from(projectDir.dir("licenses")) { 
into("${rootProject.name}-lance-rest-server/licenses") }
+        from(projectDir.file("LICENSE.rest")) { 
into("${rootProject.name}-lance-rest-server") }
+        from(projectDir.file("NOTICE.rest")) { 
into("${rootProject.name}-lance-rest-server") }
+        from(projectDir.file("README.md")) { 
into("${rootProject.name}-lance-rest-server") }
+        into(outputDir)
+        rename { fileName ->
+          fileName.replace(".rest", "")
+        }
+      }
+    }
+  }
+
   val compileTrinoConnector by registering {
     dependsOn("trino-connector:trino-connector:copyLibs")
     group = "gravitino distribution"
@@ -791,7 +837,7 @@ tasks {
   }
 
   val assembleDistribution by registering(Tar::class) {
-    dependsOn("assembleTrinoConnector", "assembleIcebergRESTServer")
+    dependsOn("assembleTrinoConnector", "assembleIcebergRESTServer", 
"assembleLanceRESTServer")
     group = "gravitino distribution"
     finalizedBy("checksumDistribution")
     into("${rootProject.name}-$version-bin")
@@ -823,6 +869,17 @@ tasks {
     destinationDirectory.set(projectDir.dir("distribution"))
   }
 
+  val assembleLanceRESTServer by registering(Tar::class) {
+    dependsOn("compileLanceRESTServer")
+    group = "gravitino distribution"
+    finalizedBy("checksumLanceRESTServerDistribution")
+    into("${rootProject.name}-lance-rest-server-$version-bin")
+    from(compileLanceRESTServer.map { it.outputs.files.single() })
+    compression = Compression.GZIP
+    
archiveFileName.set("${rootProject.name}-lance-rest-server-$version-bin.tar.gz")
+    destinationDirectory.set(projectDir.dir("distribution"))
+  }
+
   register("checksumIcebergRESTServerDistribution") {
     group = "gravitino distribution"
     dependsOn(assembleIcebergRESTServer)
@@ -839,9 +896,30 @@ tasks {
     }
   }
 
+  register("checksumLanceRESTServerDistribution") {
+    group = "gravitino distribution"
+    dependsOn(assembleLanceRESTServer)
+    val archiveFile = assembleLanceRESTServer.flatMap { it.archiveFile }
+    val checksumFile = archiveFile.map { archive ->
+      archive.asFile.let { it.resolveSibling("${it.name}.sha256") }
+    }
+    inputs.file(archiveFile)
+    outputs.file(checksumFile)
+    doLast {
+      checksumFile.get().writeText(
+        
serviceOf<ChecksumService>().sha256(archiveFile.get().asFile).toString()
+      )
+    }
+  }
+
   register("checksumDistribution") {
     group = "gravitino distribution"
-    dependsOn(assembleDistribution, "checksumTrinoConnector", 
"checksumIcebergRESTServerDistribution")
+    dependsOn(
+      assembleDistribution,
+      "checksumTrinoConnector",
+      "checksumIcebergRESTServerDistribution",
+      "checksumLanceRESTServerDistribution"
+    )
     val archiveFile = assembleDistribution.flatMap { it.archiveFile }
     val checksumFile = archiveFile.map { archive ->
       archive.asFile.let { it.resolveSibling("${it.name}.sha256") }
@@ -885,6 +963,7 @@ tasks {
         !it.name.startsWith("filesystem") &&
         !it.name.startsWith("flink") &&
         !it.name.startsWith("iceberg") &&
+        !it.name.startsWith("lance") &&
         !it.name.startsWith("spark") &&
         it.name != "hadoop-common" &&
         it.name != "hive-metastore-common" &&
@@ -916,6 +995,7 @@ tasks {
         !it.name.startsWith("filesystem") &&
         !it.name.startsWith("flink") &&
         !it.name.startsWith("iceberg") &&
+        !it.name.startsWith("lance") &&
         !it.name.startsWith("integration-test") &&
         !it.name.startsWith("spark") &&
         !it.name.startsWith("trino-connector") &&
diff --git a/conf/gravitino-lance-rest-server.conf.template 
b/conf/gravitino-lance-rest-server.conf.template
new file mode 100644
index 0000000000..32609bffca
--- /dev/null
+++ b/conf/gravitino-lance-rest-server.conf.template
@@ -0,0 +1,45 @@
+#
+# 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.
+#
+
+# THE CONFIGURATION FOR Lance REST SERVER
+gravitino.lance-rest.shutdown.timeout = 3000
+
+# THE CONFIGURATION FOR Lance REST WEB SERVER
+# The host name of the built-in web server
+gravitino.lance-rest.host = 0.0.0.0
+# The http port number of the built-in web server
+gravitino.lance-rest.httpPort = 9101
+# The min thread size of the built-in web server
+gravitino.lance-rest.minThreads = 24
+# The max thread size of the built-in web server
+gravitino.lance-rest.maxThreads = 200
+# The stop timeout of the built-in web server
+gravitino.lance-rest.stopTimeout = 30000
+# The timeout of idle connections
+gravitino.lance-rest.idleTimeout = 30000
+# The executor thread pool work queue size of the built-in web server
+gravitino.lance-rest.threadPoolWorkQueueSize = 100
+# The request header size of the built-in web server
+gravitino.lance-rest.requestHeaderSize = 131072
+# The response header size of the built-in web server
+gravitino.lance-rest.responseHeaderSize = 131072
+
+# THE CONFIGURATION FOR Lance CATALOG
+# The logical Lance catalog served by this REST endpoint
+gravitino.lance-rest.catalog-name = default
diff --git 
a/core/src/main/java/org/apache/gravitino/cache/EntityCacheWeigher.java 
b/core/src/main/java/org/apache/gravitino/cache/EntityCacheWeigher.java
index 768f60eb64..edc3ca6b9b 100644
--- a/core/src/main/java/org/apache/gravitino/cache/EntityCacheWeigher.java
+++ b/core/src/main/java/org/apache/gravitino/cache/EntityCacheWeigher.java
@@ -40,24 +40,36 @@ import org.slf4j.LoggerFactory;
  *       or manually cleared.
  *   <li>Catalog: 0, which means that it will never be evicted from the cache 
unless timeout occurs
  *       or manually cleared.
- *   <li>Schema: 10
- *   <li>Other: 100
+ *   <li>Schema: 500
+ *   <li>Tag: 100
+ *   <li>Policy: 100
+ *   <li>Other: 200
  * </ul>
  */
 public class EntityCacheWeigher implements Weigher<EntityCacheKey, 
List<Entity>> {
-  public static final int METALAKE_WEIGHT = 0;
+  public static final int METALAKE_WEIGHT = 0; // 0 means never evict
   public static final int CATALOG_WEIGHT = 0;
-  public static final int SCHEMA_WEIGHT = 10;
-  public static final int OTHER_WEIGHT = 100;
+  public static final int SCHEMA_WEIGHT = 500; // higher weight means it will 
less likely be evicted
+  public static final int OTHER_WEIGHT = 200;
+  public static final int TAG_WEIGHT = 100;
+  public static final int POLICY_WEIGHT = 100;
   private static final Logger LOG = 
LoggerFactory.getLogger(EntityCacheWeigher.class.getName());
   private static final EntityCacheWeigher INSTANCE = new EntityCacheWeigher();
   private static final Map<Entity.EntityType, Integer> ENTITY_WEIGHTS =
       ImmutableMap.of(
           Entity.EntityType.METALAKE, METALAKE_WEIGHT,
           Entity.EntityType.CATALOG, CATALOG_WEIGHT,
-          Entity.EntityType.SCHEMA, SCHEMA_WEIGHT);
+          Entity.EntityType.SCHEMA, SCHEMA_WEIGHT,
+          Entity.EntityType.TAG, TAG_WEIGHT,
+          Entity.EntityType.POLICY, POLICY_WEIGHT);
   private static final long MAX_WEIGHT =
-      2 * (METALAKE_WEIGHT * 10 + CATALOG_WEIGHT * (10 * 200) + SCHEMA_WEIGHT 
* (10 * 200 * 1000));
+      2
+          * (METALAKE_WEIGHT * 10
+              + CATALOG_WEIGHT * 100
+              + SCHEMA_WEIGHT * 1000
+              + OTHER_WEIGHT * 10000
+              + TAG_WEIGHT * 10000
+              + POLICY_WEIGHT * 10000);
 
   @VisibleForTesting
   protected EntityCacheWeigher() {}
diff --git a/core/src/test/java/org/apache/gravitino/cache/TestCacheConfig.java 
b/core/src/test/java/org/apache/gravitino/cache/TestCacheConfig.java
index 55c62a1ae9..3944d9a282 100644
--- a/core/src/test/java/org/apache/gravitino/cache/TestCacheConfig.java
+++ b/core/src/test/java/org/apache/gravitino/cache/TestCacheConfig.java
@@ -21,19 +21,25 @@ package org.apache.gravitino.cache;
 
 import com.github.benmanes.caffeine.cache.Cache;
 import com.github.benmanes.caffeine.cache.Caffeine;
+import com.google.common.collect.ImmutableMap;
 import java.time.Duration;
 import java.util.List;
+import java.util.stream.IntStream;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.Config;
 import org.apache.gravitino.Configs;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
+import org.apache.gravitino.file.Fileset;
 import org.apache.gravitino.meta.AuditInfo;
 import org.apache.gravitino.meta.BaseMetalake;
 import org.apache.gravitino.meta.CatalogEntity;
+import org.apache.gravitino.meta.FilesetEntity;
 import org.apache.gravitino.meta.SchemaEntity;
 import org.apache.gravitino.meta.SchemaVersion;
+import org.apache.gravitino.meta.TagEntity;
+import org.apache.gravitino.utils.NameIdentifierUtil;
 import org.awaitility.Awaitility;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -47,14 +53,127 @@ public class TestCacheConfig {
     Assertions.assertTrue(config.get(Configs.CACHE_WEIGHER_ENABLED));
     Assertions.assertEquals(10_000, config.get(Configs.CACHE_MAX_ENTRIES));
     Assertions.assertEquals(3_600_000L, 
config.get(Configs.CACHE_EXPIRATION_TIME));
-    Assertions.assertEquals(40_000_000L, EntityCacheWeigher.getMaxWeight());
+    Assertions.assertEquals(9_000_000L, EntityCacheWeigher.getMaxWeight());
     Assertions.assertEquals("caffeine", 
config.get(Configs.CACHE_IMPLEMENTATION));
   }
 
+  @Test
+  void testPolicyAndTagCacheWeigher() throws InterruptedException {
+    Caffeine<Object, Object> builder = Caffeine.newBuilder();
+    builder.maximumWeight(2000);
+    builder.weigher(EntityCacheWeigher.getInstance());
+    Cache<EntityCacheRelationKey, List<Entity>> cache = builder.build();
+
+    BaseMetalake baseMetalake =
+        BaseMetalake.builder()
+            .withName("metalake1")
+            .withId(1L)
+            .withVersion(SchemaVersion.V_0_1)
+            .withAuditInfo(AuditInfo.EMPTY)
+            .build();
+    cache.put(
+        EntityCacheRelationKey.of(NameIdentifier.of("metalake1"), 
Entity.EntityType.METALAKE),
+        List.of(baseMetalake));
+    CatalogEntity catalogEntity =
+        CatalogEntity.builder()
+            .withNamespace(Namespace.of("metalake1"))
+            .withName("catalog1")
+            .withProvider("provider")
+            .withAuditInfo(AuditInfo.EMPTY)
+            .withId(100L)
+            .withType(Catalog.Type.RELATIONAL)
+            .build();
+    cache.put(
+        EntityCacheRelationKey.of(
+            NameIdentifier.of(new String[] {"metalake1", "catalog1"}), 
Entity.EntityType.CATALOG),
+        List.of(catalogEntity));
+
+    SchemaEntity schemaEntity =
+        SchemaEntity.builder()
+            .withNamespace(Namespace.of("metalake1", "catalog1"))
+            .withName("schema1")
+            .withAuditInfo(AuditInfo.EMPTY)
+            .withId(1000L)
+            .build();
+    cache.put(
+        EntityCacheRelationKey.of(
+            NameIdentifier.of(new String[] {"metalake1", "catalog1", 
"schema1"}),
+            Entity.EntityType.SCHEMA),
+        List.of(schemaEntity));
+
+    for (int i = 0; i < 5; i++) {
+      String filesetName = "fileset" + i;
+      FilesetEntity fileset =
+          FilesetEntity.builder()
+              .withNamespace(Namespace.of("metalake1", "catalog1", "schema1"))
+              .withName(filesetName)
+              .withAuditInfo(AuditInfo.EMPTY)
+              .withStorageLocations(ImmutableMap.of("default", 
"s3://bucket/path"))
+              .withId((long) (i + 1) * 10_000)
+              .withFilesetType(Fileset.Type.MANAGED)
+              .build();
+      cache.put(
+          EntityCacheRelationKey.of(
+              NameIdentifier.of(new String[] {"metalake1", "catalog1", 
"schema1", filesetName}),
+              Entity.EntityType.FILESET),
+          List.of(fileset));
+    }
+
+    for (int i = 0; i < 10; i++) {
+      String tagName = "tag" + i;
+      NameIdentifier tagNameIdent = NameIdentifierUtil.ofTag("metalake", 
tagName);
+      TagEntity tagEntity =
+          TagEntity.builder()
+              .withNamespace(tagNameIdent.namespace())
+              .withName(tagName)
+              .withAuditInfo(AuditInfo.EMPTY)
+              .withId((long) (i + 1) * 100_000)
+              .build();
+      cache.put(EntityCacheRelationKey.of(tagNameIdent, 
Entity.EntityType.TAG), List.of(tagEntity));
+    }
+
+    // The weight of the cache has exceeded 2000, some entities will be 
evicted if we continue to
+    // add fileset entities.
+    for (int i = 5; i < 15; i++) {
+      String filesetName = "fileset" + i;
+      FilesetEntity fileset =
+          FilesetEntity.builder()
+              .withNamespace(Namespace.of("metalake1", "catalog1", "schema1"))
+              .withName(filesetName)
+              .withAuditInfo(AuditInfo.EMPTY)
+              .withStorageLocations(ImmutableMap.of("default", 
"s3://bucket/path"))
+              .withId((long) (i + 1) * 10_000)
+              .withFilesetType(Fileset.Type.MANAGED)
+              .build();
+      cache.put(
+          EntityCacheRelationKey.of(
+              NameIdentifier.of(new String[] {"metalake1", "catalog1", 
"schema1", filesetName}),
+              Entity.EntityType.FILESET),
+          List.of(fileset));
+    }
+
+    Thread.sleep(1000);
+
+    // There should no tag entities in the cache, because the weight of each 
tag entity is 100 that
+    // is higher than the maximum weight of the fileset entity which is 200.
+    Awaitility.await()
+        .atMost(Duration.ofSeconds(5))
+        .pollInterval(Duration.ofMillis(10))
+        .until(
+            () ->
+                IntStream.of(0, 1, 2, 3)
+                    .mapToObj(i -> NameIdentifierUtil.ofTag("metalake", "tag" 
+ i))
+                    .allMatch(
+                        tagNameIdent ->
+                            cache.getIfPresent(
+                                    EntityCacheRelationKey.of(tagNameIdent, 
Entity.EntityType.TAG))
+                                == null));
+  }
+
   @Test
   void testCaffeineCacheWithWeight() throws Exception {
     Caffeine<Object, Object> builder = Caffeine.newBuilder();
-    builder.maximumWeight(500);
+    builder.maximumWeight(5000);
     builder.weigher(EntityCacheWeigher.getInstance());
     Cache<EntityCacheRelationKey, List<Entity>> cache = builder.build();
 
@@ -121,11 +240,11 @@ public class TestCacheConfig {
                   NameIdentifier.of("metalake1.catalog" + i), 
Entity.EntityType.CATALOG)));
     }
 
-    // Only some of the 100 schemas are still in the cache, to be exact, 500 / 
10 = 50 schemas.
+    // Only some of the 100 schemas are still in the cache, to be exact, 5000 
/ 500 = 10 schemas.
     Awaitility.await()
         .atMost(Duration.ofSeconds(5))
         .pollInterval(Duration.ofMillis(10))
-        .until(() -> cache.asMap().size() == 10 + 3 + 500 / 10);
+        .until(() -> cache.asMap().size() == 10 + 3 + 5000 / 500);
   }
 
   @Test
diff --git a/lance/build.gradle.kts b/lance/build.gradle.kts
new file mode 100644
index 0000000000..fa6eb7d5ef
--- /dev/null
+++ b/lance/build.gradle.kts
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+tasks.all {
+    enabled = false
+}
diff --git a/lance/lance-common/build.gradle.kts 
b/lance/lance-common/build.gradle.kts
new file mode 100644
index 0000000000..5048d274f6
--- /dev/null
+++ b/lance/lance-common/build.gradle.kts
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+description = "lance-common"
+
+plugins {
+  `maven-publish`
+  id("java")
+  id("idea")
+}
+
+dependencies {
+  implementation(project(":api"))
+  implementation(project(":catalogs:catalog-common"))
+  implementation(project(":common")) {
+    exclude("*")
+  }
+  implementation(project(":core")) {
+    exclude("*")
+  }
+
+  implementation(libs.guava)
+  implementation(libs.commons.lang3)
+  implementation(libs.slf4j.api)
+
+  testImplementation(libs.junit.jupiter.api)
+  testRuntimeOnly(libs.junit.jupiter.engine)
+}
diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/config/LanceConfig.java
 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/config/LanceConfig.java
new file mode 100644
index 0000000000..f2d7e748cf
--- /dev/null
+++ 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/config/LanceConfig.java
@@ -0,0 +1,65 @@
+/*
+ * 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.gravitino.lance.common.config;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.OverwriteDefaultConfig;
+import org.apache.gravitino.config.ConfigBuilder;
+import org.apache.gravitino.config.ConfigConstants;
+import org.apache.gravitino.config.ConfigEntry;
+
+/** Base Lance REST configuration. */
+public class LanceConfig extends Config implements OverwriteDefaultConfig {
+
+  public static final String LANCE_CONFIG_PREFIX = "gravitino.lance-rest.";
+
+  public static final int DEFAULT_LANCE_REST_SERVICE_HTTP_PORT = 9101;
+  public static final int DEFAULT_LANCE_REST_SERVICE_HTTPS_PORT = 9533;
+
+  public static final ConfigEntry<String> CATALOG_NAME =
+      new ConfigBuilder(LANCE_CONFIG_PREFIX + "catalog-name")
+          .doc("Logical Lance catalog served by the REST endpoint")
+          .version(ConfigConstants.VERSION_0_1_0)
+          .stringConf()
+          .createWithDefault("default");
+
+  public LanceConfig(Map<String, String> properties) {
+    super(false);
+    loadFromMap(properties, key -> true);
+  }
+
+  public LanceConfig() {
+    super(false);
+  }
+
+  public String getCatalogName() {
+    return get(CATALOG_NAME);
+  }
+
+  @Override
+  public Map<String, String> getOverwriteDefaultConfig() {
+    return ImmutableMap.of(
+        ConfigConstants.WEBSERVER_HTTP_PORT,
+        String.valueOf(DEFAULT_LANCE_REST_SERVICE_HTTP_PORT),
+        ConfigConstants.WEBSERVER_HTTPS_PORT,
+        String.valueOf(DEFAULT_LANCE_REST_SERVICE_HTTPS_PORT));
+  }
+}
diff --git 
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceCatalogService.java
 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceCatalogService.java
new file mode 100644
index 0000000000..67dd4c2d22
--- /dev/null
+++ 
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/LanceCatalogService.java
@@ -0,0 +1,352 @@
+/*
+ * 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.gravitino.lance.common.ops;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.lance.common.config.LanceConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Thin placeholder that will later bridge Lance catalog metadata into 
Gravitino.
+ *
+ * <p>The current implementation keeps an in-memory catalog view so the REST 
surface mirrors the
+ * Iceberg catalog experience while the Lance integration is built out for 
real.
+ */
+public class LanceCatalogService implements AutoCloseable {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(LanceCatalogService.class);
+
+  private final LanceConfig config;
+  private final ConcurrentMap<String, NamespaceState> namespaces;
+
+  public LanceCatalogService(LanceConfig config) {
+    this.config = config;
+    this.namespaces = new ConcurrentHashMap<>();
+    seedSampleMetadata();
+  }
+
+  public String catalogName() {
+    return config.getCatalogName();
+  }
+
+  public boolean namespaceExists(String namespace) {
+    return namespaces.containsKey(namespace);
+  }
+
+  public Map<String, Map<String, String>> listNamespaces() {
+    Map<String, Map<String, String>> result = new ConcurrentHashMap<>();
+    namespaces.forEach(
+        (name, state) ->
+            result.put(
+                name, Collections.unmodifiableMap(new 
ConcurrentHashMap<>(state.properties))));
+    return Map.copyOf(result);
+  }
+
+  public List<String> listNamespaceNames() {
+    return namespaces.keySet().stream()
+        .sorted(Comparator.naturalOrder())
+        .collect(Collectors.toUnmodifiableList());
+  }
+
+  public NamespaceListingResult listChildNamespaces(
+      String parentId, String delimiter, String pageToken, Integer limit) {
+    String normalizedParent = StringUtils.trimToEmpty(parentId);
+    String effectiveDelimiter = StringUtils.isBlank(delimiter) ? "$" : 
delimiter;
+
+    List<String> sortedNamespaces = listNamespaceNames();
+    List<String> filtered = filterChildren(sortedNamespaces, normalizedParent, 
effectiveDelimiter);
+
+    int startingOffset = parsePageToken(pageToken, filtered.size());
+    int pageLimit = limit == null ? filtered.size() : 
validatePositiveLimit(limit, filtered.size());
+    int endIndex = Math.min(filtered.size(), startingOffset + pageLimit);
+
+    List<String> page = filtered.subList(startingOffset, endIndex);
+    String nextToken = endIndex < filtered.size() ? String.valueOf(endIndex) : 
null;
+    return new NamespaceListingResult(normalizedParent, effectiveDelimiter, 
page, nextToken);
+  }
+
+  public boolean createNamespace(String namespace) {
+    if (StringUtils.isBlank(namespace)) {
+      throw new IllegalArgumentException("Namespace must be non-empty");
+    }
+    NamespaceState state = new NamespaceState(Collections.emptyMap());
+    NamespaceState existing = namespaces.putIfAbsent(namespace, state);
+    if (existing == null) {
+      LOG.info("Created Lance namespace {}", namespace);
+      return true;
+    }
+    return false;
+  }
+
+  public boolean dropNamespace(String namespace) {
+    NamespaceState state = namespaces.get(namespace);
+    if (state == null) {
+      return false;
+    }
+    if (!state.tables.isEmpty()) {
+      LOG.info("Refusing to drop Lance namespace {} because it still owns 
tables", namespace);
+      return false;
+    }
+    boolean removed = namespaces.remove(namespace, state);
+    if (removed) {
+      LOG.info("Dropped Lance namespace {}", namespace);
+    }
+    return removed;
+  }
+
+  public List<String> listTables(String namespace) {
+    NamespaceState state = namespaces.get(namespace);
+    if (state == null) {
+      throw new IllegalArgumentException("Unknown namespace: " + namespace);
+    }
+    return state.tables.keySet().stream()
+        .sorted(Comparator.naturalOrder())
+        .collect(Collectors.toUnmodifiableList());
+  }
+
+  public Optional<Map<String, Object>> loadTable(String namespace, String 
table) {
+    NamespaceState state = namespaces.get(namespace);
+    if (state == null) {
+      return Optional.empty();
+    }
+    LanceTableEntry tableEntry = state.tables.get(table);
+    if (tableEntry == null) {
+      return Optional.empty();
+    }
+    return Optional.of(tableEntry.describe());
+  }
+
+  public TableListingResult listTables(
+      String namespaceId, String delimiter, String pageToken, Integer limit) {
+    String normalizedNamespace = StringUtils.trimToEmpty(namespaceId);
+    if (StringUtils.isBlank(normalizedNamespace)) {
+      throw new IllegalArgumentException("Namespace id must be provided");
+    }
+
+    String effectiveDelimiter = StringUtils.isBlank(delimiter) ? "$" : 
delimiter;
+
+    NamespaceState state = namespaces.get(normalizedNamespace);
+    if (state == null) {
+      throw new NoSuchElementException("Unknown namespace: " + 
normalizedNamespace);
+    }
+
+    List<String> sortedTables =
+        state.tables.keySet().stream()
+            .sorted(Comparator.naturalOrder())
+            .collect(Collectors.toList());
+
+    int startingOffset = parsePageToken(pageToken, sortedTables.size());
+    int pageLimit =
+        limit == null ? sortedTables.size() : validatePositiveLimit(limit, 
sortedTables.size());
+    int endIndex = Math.min(sortedTables.size(), startingOffset + pageLimit);
+
+    List<String> page = sortedTables.subList(startingOffset, endIndex);
+    String nextToken = endIndex < sortedTables.size() ? 
String.valueOf(endIndex) : null;
+
+    return new TableListingResult(normalizedNamespace, effectiveDelimiter, 
page, nextToken);
+  }
+
+  @Override
+  public void close() {
+    namespaces.clear();
+  }
+
+  private void seedSampleMetadata() {
+    NamespaceState defaultNamespace =
+        namespaces.computeIfAbsent("default", key -> new 
NamespaceState(Collections.emptyMap()));
+    defaultNamespace.tables.put(
+        "sample_table",
+        new LanceTableEntry(
+            "sample_table",
+            "default",
+            ImmutableMap.of(
+                "format", "lance",
+                "uri", "file:///tmp/sample_table.lance",
+                "summary", "Placeholder Lance table metadata")));
+  }
+
+  private static final class NamespaceState {
+    private final Map<String, String> properties;
+    private final ConcurrentMap<String, LanceTableEntry> tables;
+
+    NamespaceState(Map<String, String> properties) {
+      this.properties = new ConcurrentHashMap<>(properties);
+      this.tables = new ConcurrentHashMap<>();
+    }
+  }
+
+  private static final class LanceTableEntry {
+    private final String name;
+    private final String namespace;
+    private final Map<String, Object> metadata;
+
+    LanceTableEntry(String name, String namespace, Map<String, Object> 
metadata) {
+      this.name = name;
+      this.namespace = namespace;
+      this.metadata = new ConcurrentHashMap<>(metadata);
+    }
+
+    Map<String, Object> describe() {
+      Map<String, Object> result = new ConcurrentHashMap<>(metadata);
+      result.put("name", name);
+      result.put("namespace", namespace);
+      return Collections.unmodifiableMap(result);
+    }
+  }
+
+  private List<String> filterChildren(List<String> namespaces, String 
parentId, String delimiter) {
+    boolean rootRequest = StringUtils.isBlank(parentId) || 
"root".equalsIgnoreCase(parentId);
+    if (rootRequest) {
+      return namespaces;
+    }
+
+    String parentPrefix = parentId + delimiter;
+    return namespaces.stream()
+        .filter(ns -> ns.startsWith(parentPrefix))
+        .map(
+            ns -> {
+              String remainder = ns.substring(parentPrefix.length());
+              int nextDelimiter = remainder.indexOf(delimiter);
+              if (nextDelimiter >= 0) {
+                return remainder.substring(0, nextDelimiter);
+              }
+              return remainder;
+            })
+        .filter(child -> !child.isEmpty())
+        .distinct()
+        .sorted(Comparator.naturalOrder())
+        .collect(Collectors.toUnmodifiableList());
+  }
+
+  private int parsePageToken(String pageToken, int size) {
+    if (StringUtils.isBlank(pageToken)) {
+      return 0;
+    }
+    try {
+      int parsed = Integer.parseInt(pageToken);
+      if (parsed < 0 || parsed > size) {
+        throw new IllegalArgumentException("Invalid page_token value");
+      }
+      return parsed;
+    } catch (NumberFormatException nfe) {
+      throw new IllegalArgumentException("Invalid page_token value", nfe);
+    }
+  }
+
+  private int validatePositiveLimit(int limit, int size) {
+    if (limit <= 0) {
+      throw new IllegalArgumentException("limit must be greater than 0");
+    }
+    return Math.min(limit, Math.max(size, 0));
+  }
+
+  public static final class NamespaceListingResult {
+    private final String parentId;
+    private final String delimiter;
+    private final List<String> namespaces;
+    private final String nextPageToken;
+
+    NamespaceListingResult(
+        String parentId, String delimiter, List<String> namespaces, String 
nextPageToken) {
+      this.parentId = parentId;
+      this.delimiter = delimiter;
+      this.namespaces = List.copyOf(namespaces);
+      this.nextPageToken = nextPageToken;
+    }
+
+    public String getParentId() {
+      return parentId;
+    }
+
+    public String getDelimiter() {
+      return delimiter;
+    }
+
+    public List<String> getNamespaces() {
+      return namespaces;
+    }
+
+    public Optional<String> getNextPageToken() {
+      return Optional.ofNullable(nextPageToken);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof NamespaceListingResult)) {
+        return false;
+      }
+      NamespaceListingResult that = (NamespaceListingResult) o;
+      return Objects.equals(parentId, that.parentId)
+          && Objects.equals(delimiter, that.delimiter)
+          && Objects.equals(namespaces, that.namespaces)
+          && Objects.equals(nextPageToken, that.nextPageToken);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(parentId, delimiter, namespaces, nextPageToken);
+    }
+  }
+
+  public static final class TableListingResult {
+    private final String namespaceId;
+    private final String delimiter;
+    private final List<String> tables;
+    private final String nextPageToken;
+
+    TableListingResult(
+        String namespaceId, String delimiter, List<String> tables, String 
nextPageToken) {
+      this.namespaceId = namespaceId;
+      this.delimiter = delimiter;
+      this.tables = List.copyOf(tables);
+      this.nextPageToken = nextPageToken;
+    }
+
+    public String getNamespaceId() {
+      return namespaceId;
+    }
+
+    public String getDelimiter() {
+      return delimiter;
+    }
+
+    public List<String> getTables() {
+      return tables;
+    }
+
+    public Optional<String> getNextPageToken() {
+      return Optional.ofNullable(nextPageToken);
+    }
+  }
+}
diff --git a/lance/lance-rest-server/build.gradle.kts 
b/lance/lance-rest-server/build.gradle.kts
new file mode 100644
index 0000000000..0337609593
--- /dev/null
+++ b/lance/lance-rest-server/build.gradle.kts
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+description = "lance-rest-server"
+
+plugins {
+  `maven-publish`
+  id("java")
+  id("idea")
+}
+
+dependencies {
+  implementation(project(":api"))
+  implementation(project(":catalogs:catalog-common"))
+  implementation(project(":common")) {
+    exclude("*")
+  }
+  implementation(project(":core")) {
+    exclude("*")
+  }
+  implementation(project(":server-common")) {
+    exclude("*")
+  }
+  implementation(project(":lance:lance-common"))
+
+  implementation(libs.bundles.jetty)
+  implementation(libs.bundles.jersey)
+  implementation(libs.bundles.log4j)
+  implementation(libs.bundles.metrics)
+  implementation(libs.bundles.prometheus)
+  implementation(libs.metrics.jersey2)
+  implementation(libs.guava)
+  implementation(libs.jackson.annotations)
+  implementation(libs.jackson.databind)
+  implementation(libs.jackson.datatype.jdk8)
+  implementation(libs.jackson.datatype.jsr310)
+
+  testImplementation(libs.junit.jupiter.api)
+  testRuntimeOnly(libs.junit.jupiter.engine)
+}
+
+tasks {
+  val copyDepends by registering(Copy::class) {
+    from(configurations.runtimeClasspath)
+    into("build/libs")
+  }
+
+  jar {
+    finalizedBy(copyDepends)
+  }
+
+  register("copyLibs", Copy::class) {
+    dependsOn(copyDepends, "build")
+    from("build/libs")
+    into("$rootDir/distribution/package/lance-rest-server/libs")
+  }
+
+  register("copyLibsToStandalonePackage", Copy::class) {
+    dependsOn(copyDepends, "build")
+    from("build/libs")
+    into("$rootDir/distribution/gravitino-lance-rest-server/libs")
+  }
+
+  register("copyLibAndConfigs", Copy::class) {
+    dependsOn("copyLibs")
+  }
+
+  register("copyLibAndConfigsToStandalonePackage", Copy::class) {
+    dependsOn("copyLibsToStandalonePackage")
+  }
+
+  named("generateMetadataFileForMavenJavaPublication") {
+    dependsOn(copyDepends)
+  }
+}
diff --git 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceRESTService.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceRESTService.java
new file mode 100644
index 0000000000..e85dc37b4a
--- /dev/null
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceRESTService.java
@@ -0,0 +1,97 @@
+/*
+ * 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.gravitino.lance;
+
+import java.util.Map;
+import javax.servlet.Servlet;
+import org.apache.gravitino.auxiliary.GravitinoAuxiliaryService;
+import org.apache.gravitino.lance.common.config.LanceConfig;
+import org.apache.gravitino.lance.common.ops.LanceCatalogService;
+import org.apache.gravitino.lance.service.rest.LanceNamespaceOperations;
+import org.apache.gravitino.server.web.JettyServer;
+import org.apache.gravitino.server.web.JettyServerConfig;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Thin REST service shell for Lance metadata. */
+public class LanceRESTService implements GravitinoAuxiliaryService {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(LanceRESTService.class);
+
+  public static final String SERVICE_NAME = "lance-rest";
+  public static final String LANCE_SPEC = "/lance/*";
+
+  private JettyServer server;
+  private LanceCatalogService catalogService;
+
+  @Override
+  public String shortName() {
+    return SERVICE_NAME;
+  }
+
+  @Override
+  public void serviceInit(Map<String, String> properties) {
+    LanceConfig lanceConfig = new LanceConfig(properties);
+    JettyServerConfig serverConfig = JettyServerConfig.fromConfig(lanceConfig);
+
+    server = new JettyServer();
+    server.initialize(serverConfig, SERVICE_NAME, false);
+
+    catalogService = new LanceCatalogService(lanceConfig);
+
+    ResourceConfig resourceConfig = new ResourceConfig();
+    resourceConfig.register(JacksonFeature.class);
+    resourceConfig.register(new LanceNamespaceOperations(catalogService));
+
+    Servlet container = new ServletContainer(resourceConfig);
+    server.addServlet(container, LANCE_SPEC);
+    server.addCustomFilters(LANCE_SPEC);
+    server.addSystemFilters(LANCE_SPEC);
+
+    LOG.info("Initialized Lance REST service for catalog {}", 
lanceConfig.getCatalogName());
+  }
+
+  @Override
+  public void serviceStart() {
+    if (server != null) {
+      server.start();
+      LOG.info("Lance REST service started");
+    }
+  }
+
+  @Override
+  public void serviceStop() throws Exception {
+    if (server != null) {
+      server.stop();
+      LOG.info("Lance REST service stopped");
+    }
+    if (catalogService != null) {
+      catalogService.close();
+    }
+  }
+
+  public void join() {
+    if (server != null) {
+      server.join();
+    }
+  }
+}
diff --git 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/server/GravitinoLanceRESTServer.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/server/GravitinoLanceRESTServer.java
new file mode 100644
index 0000000000..e28bdd5c17
--- /dev/null
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/server/GravitinoLanceRESTServer.java
@@ -0,0 +1,107 @@
+/*
+ * 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.gravitino.lance.server;
+
+import org.apache.gravitino.Config;
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.lance.LanceRESTService;
+import org.apache.gravitino.lance.common.config.LanceConfig;
+import org.apache.gravitino.server.ServerConfig;
+import org.apache.gravitino.server.authentication.ServerAuthenticator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Bootstrap entry point for the Lance REST facade. */
+public class GravitinoLanceRESTServer {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(GravitinoLanceRESTServer.class);
+
+  public static final String CONF_FILE = "gravitino-lance-rest-server.conf";
+
+  private final Config serverConfig;
+
+  private LanceRESTService lanceRESTService;
+  private GravitinoEnv gravitinoEnv;
+
+  public GravitinoLanceRESTServer(Config config) {
+    this.serverConfig = config;
+    this.gravitinoEnv = GravitinoEnv.getInstance();
+    this.lanceRESTService = new LanceRESTService();
+  }
+
+  private void initialize() {
+    gravitinoEnv.initializeBaseComponents(serverConfig);
+    lanceRESTService.serviceInit(
+        serverConfig.getConfigsWithPrefix(LanceConfig.LANCE_CONFIG_PREFIX));
+    ServerAuthenticator.getInstance().initialize(serverConfig);
+  }
+
+  private void start() {
+    gravitinoEnv.start();
+    lanceRESTService.serviceStart();
+  }
+
+  private void join() {
+    lanceRESTService.join();
+  }
+
+  private void stop() throws Exception {
+    lanceRESTService.serviceStop();
+    LOG.info("Gravitino Lance REST service stopped");
+  }
+
+  public static void main(String[] args) {
+    LOG.info("Starting Gravitino Lance REST Server");
+    String confPath = System.getenv("GRAVITINO_TEST") == null ? "" : args[0];
+    ServerConfig serverConfig = ServerConfig.loadConfig(confPath, CONF_FILE);
+    GravitinoLanceRESTServer lanceRESTServer = new 
GravitinoLanceRESTServer(serverConfig);
+    lanceRESTServer.initialize();
+
+    try {
+      lanceRESTServer.start();
+    } catch (Exception e) {
+      LOG.error("Error while running lance REST server", e);
+      System.exit(-1);
+    }
+    LOG.info("Done, Gravitino Lance REST server started.");
+
+    Runtime.getRuntime()
+        .addShutdownHook(
+            new Thread(
+                () -> {
+                  try {
+                    
Thread.sleep(serverConfig.get(ServerConfig.SERVER_SHUTDOWN_TIMEOUT));
+                  } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    LOG.error("Interrupted exception:", e);
+                  } catch (Exception e) {
+                    LOG.error("Error while running clean-up tasks in shutdown 
hook", e);
+                  }
+                }));
+    lanceRESTServer.join();
+
+    LOG.info("Shutting down Gravitino Lance REST Server ... ");
+    try {
+      lanceRESTServer.stop();
+      LOG.info("Gravitino Lance REST Server has shut down.");
+    } catch (Exception e) {
+      LOG.error("Error while stopping Gravitino Lance REST Server", e);
+    }
+  }
+}
diff --git 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceListNamespacesResponse.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceListNamespacesResponse.java
new file mode 100644
index 0000000000..11ec7d3c3c
--- /dev/null
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceListNamespacesResponse.java
@@ -0,0 +1,63 @@
+/*
+ * 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.gravitino.lance.service.rest;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class LanceListNamespacesResponse {
+
+  @JsonProperty("id")
+  private final String id;
+
+  @JsonProperty("delimiter")
+  private final String delimiter;
+
+  @JsonProperty("namespaces")
+  private final List<String> namespaces;
+
+  @JsonProperty("next_page_token")
+  private final String nextPageToken;
+
+  public LanceListNamespacesResponse(
+      String id, String delimiter, List<String> namespaces, String 
nextPageToken) {
+    this.id = id;
+    this.delimiter = delimiter;
+    this.namespaces = List.copyOf(namespaces);
+    this.nextPageToken = nextPageToken;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public String getDelimiter() {
+    return delimiter;
+  }
+
+  public List<String> getNamespaces() {
+    return namespaces;
+  }
+
+  public String getNextPageToken() {
+    return nextPageToken;
+  }
+}
diff --git 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceListTablesResponse.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceListTablesResponse.java
new file mode 100644
index 0000000000..82e2a90978
--- /dev/null
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceListTablesResponse.java
@@ -0,0 +1,63 @@
+/*
+ * 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.gravitino.lance.service.rest;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class LanceListTablesResponse {
+
+  @JsonProperty("id")
+  private final String namespaceId;
+
+  @JsonProperty("delimiter")
+  private final String delimiter;
+
+  @JsonProperty("tables")
+  private final List<String> tables;
+
+  @JsonProperty("next_page_token")
+  private final String nextPageToken;
+
+  public LanceListTablesResponse(
+      String namespaceId, String delimiter, List<String> tables, String 
nextPageToken) {
+    this.namespaceId = namespaceId;
+    this.delimiter = delimiter;
+    this.tables = List.copyOf(tables);
+    this.nextPageToken = nextPageToken;
+  }
+
+  public String getNamespaceId() {
+    return namespaceId;
+  }
+
+  public String getDelimiter() {
+    return delimiter;
+  }
+
+  public List<String> getTables() {
+    return tables;
+  }
+
+  public String getNextPageToken() {
+    return nextPageToken;
+  }
+}
diff --git 
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceNamespaceOperations.java
 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceNamespaceOperations.java
new file mode 100644
index 0000000000..0ac9457eff
--- /dev/null
+++ 
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceNamespaceOperations.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.lance.service.rest;
+
+import java.util.NoSuchElementException;
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.Encoded;
+import javax.ws.rs.GET;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.lance.common.ops.LanceCatalogService;
+
+@Path("/v1/namespace")
+@Produces(MediaType.APPLICATION_JSON)
+public class LanceNamespaceOperations {
+
+  private final LanceCatalogService catalogService;
+
+  public LanceNamespaceOperations(LanceCatalogService catalogService) {
+    this.catalogService = catalogService;
+  }
+
+  @GET
+  @Path("/{id}/list")
+  public Response listNamespaces(
+      @Encoded @PathParam("id") String namespaceId,
+      @DefaultValue("$") @QueryParam("delimiter") String delimiter,
+      @QueryParam("page_token") String pageToken,
+      @QueryParam("limit") Integer limit) {
+    try {
+      LanceCatalogService.NamespaceListingResult result =
+          catalogService.listChildNamespaces(namespaceId, delimiter, 
pageToken, limit);
+      LanceListNamespacesResponse payload =
+          new LanceListNamespacesResponse(
+              result.getParentId(),
+              result.getDelimiter(),
+              result.getNamespaces(),
+              result.getNextPageToken().orElse(null));
+      return Response.ok(payload).build();
+    } catch (NoSuchElementException nse) {
+      throw new NotFoundException(nse.getMessage(), nse);
+    } catch (IllegalArgumentException iae) {
+      throw new BadRequestException(iae.getMessage(), iae);
+    }
+  }
+
+  @GET
+  @Path("/{id}/table/list")
+  public Response listTables(
+      @Encoded @PathParam("id") String namespaceId,
+      @DefaultValue("$") @QueryParam("delimiter") String delimiter,
+      @QueryParam("page_token") String pageToken,
+      @QueryParam("limit") Integer limit) {
+    try {
+      LanceCatalogService.TableListingResult result =
+          catalogService.listTables(namespaceId, delimiter, pageToken, limit);
+      LanceListTablesResponse payload =
+          new LanceListTablesResponse(
+              result.getNamespaceId(),
+              result.getDelimiter(),
+              result.getTables(),
+              result.getNextPageToken().orElse(null));
+      return Response.ok(payload).build();
+    } catch (NoSuchElementException nse) {
+      throw new NotFoundException(nse.getMessage(), nse);
+    } catch (IllegalArgumentException iae) {
+      throw new BadRequestException(iae.getMessage(), iae);
+    }
+  }
+}
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/MetadataFilterHelper.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/MetadataFilterHelper.java
index e435bac4a1..874424feb2 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/MetadataFilterHelper.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/MetadataFilterHelper.java
@@ -156,6 +156,35 @@ public class MetadataFilterHelper {
       Entity.EntityType entityType,
       E[] entities,
       Function<E, NameIdentifier> toNameIdentifier) {
+    GravitinoAuthorizer authorizer =
+        GravitinoAuthorizerProvider.getInstance().getGravitinoAuthorizer();
+    Principal currentPrincipal = PrincipalUtils.getCurrentPrincipal();
+    return filterByExpression(
+        metalake, expression, entityType, entities, toNameIdentifier, 
currentPrincipal, authorizer);
+  }
+
+  /**
+   * Call {@link AuthorizationExpressionEvaluator} and use specified Principal 
and
+   * GravitinoAuthorizer to filter the metadata list
+   *
+   * @param metalake metalake name
+   * @param expression authorization expression
+   * @param entityType entity type
+   * @param entities metadata entities
+   * @param toNameIdentifier function to convert entity to NameIdentifier
+   * @param currentPrincipal current principal
+   * @param authorizer authorizer to filter metadata
+   * @return Filtered Metadata Entity
+   * @param <E> Entity class
+   */
+  public static <E> E[] filterByExpression(
+      String metalake,
+      String expression,
+      Entity.EntityType entityType,
+      E[] entities,
+      Function<E, NameIdentifier> toNameIdentifier,
+      Principal currentPrincipal,
+      GravitinoAuthorizer authorizer) {
     if (!enableAuthorization()) {
       return entities;
     }
@@ -163,7 +192,6 @@ public class MetadataFilterHelper {
     AuthorizationRequestContext authorizationRequestContext = new 
AuthorizationRequestContext();
     List<CompletableFuture<E>> futures = new ArrayList<>();
     for (E entity : entities) {
-      Principal currentPrincipal = PrincipalUtils.getCurrentPrincipal();
       futures.add(
           CompletableFuture.supplyAsync(
               () -> {
@@ -172,7 +200,7 @@ public class MetadataFilterHelper {
                       currentPrincipal,
                       () -> {
                         AuthorizationExpressionEvaluator 
authorizationExpressionEvaluator =
-                            new AuthorizationExpressionEvaluator(expression);
+                            new AuthorizationExpressionEvaluator(expression, 
authorizer);
                         NameIdentifier nameIdentifier = 
toNameIdentifier.apply(entity);
                         Map<Entity.EntityType, NameIdentifier> 
nameIdentifierMap =
                             spiltMetadataNames(metalake, entityType, 
nameIdentifier);
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionEvaluator.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionEvaluator.java
index 78ddf58d44..02c455e887 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionEvaluator.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionEvaluator.java
@@ -39,6 +39,7 @@ import org.apache.gravitino.utils.PrincipalUtils;
 public class AuthorizationExpressionEvaluator {
 
   private final String ognlAuthorizationExpression;
+  private final GravitinoAuthorizer authorizer;
 
   /**
    * Use {@link AuthorizationExpressionConverter} to convert the authorization 
expression into an
@@ -47,8 +48,19 @@ public class AuthorizationExpressionEvaluator {
    * @param expression authorization expression
    */
   public AuthorizationExpressionEvaluator(String expression) {
+    this(expression, 
GravitinoAuthorizerProvider.getInstance().getGravitinoAuthorizer());
+  }
+
+  /**
+   * Constructor of AuthorizationExpressionEvaluator
+   *
+   * @param expression authorization expression
+   * @param authorizer GravitinoAuthorizer instance
+   */
+  public AuthorizationExpressionEvaluator(String expression, 
GravitinoAuthorizer authorizer) {
     this.ognlAuthorizationExpression =
         AuthorizationExpressionConverter.convertToOgnlExpression(expression);
+    this.authorizer = authorizer;
   }
 
   /**
@@ -61,7 +73,24 @@ public class AuthorizationExpressionEvaluator {
   public boolean evaluate(
       Map<Entity.EntityType, NameIdentifier> metadataNames,
       AuthorizationRequestContext requestContext) {
-    return evaluate(metadataNames, new HashMap<>(), requestContext);
+    Principal currentPrincipal = PrincipalUtils.getCurrentPrincipal();
+    return evaluate(metadataNames, new HashMap<>(), requestContext, 
currentPrincipal);
+  }
+
+  /**
+   * Use OGNL expressions to invoke GravitinoAuthorizer for authorizing 
multiple types of metadata
+   * IDs.
+   *
+   * @param metadataNames key-metadata type, value-metadata NameIdentifier
+   * @param requestContext authorization request context
+   * @param principal current principal
+   * @return authorization result
+   */
+  public boolean evaluate(
+      Map<Entity.EntityType, NameIdentifier> metadataNames,
+      AuthorizationRequestContext requestContext,
+      Principal principal) {
+    return evaluate(metadataNames, new HashMap<>(), requestContext, principal);
   }
 
   /**
@@ -77,11 +106,27 @@ public class AuthorizationExpressionEvaluator {
       Map<String, Object> pathParams,
       AuthorizationRequestContext requestContext) {
     Principal currentPrincipal = PrincipalUtils.getCurrentPrincipal();
-    GravitinoAuthorizer gravitinoAuthorizer =
-        GravitinoAuthorizerProvider.getInstance().getGravitinoAuthorizer();
+    return evaluate(metadataNames, pathParams, requestContext, 
currentPrincipal);
+  }
+
+  /**
+   * Use OGNL expressions to invoke GravitinoAuthorizer for authorizing 
multiple types of metadata
+   * IDs.
+   *
+   * @param metadataNames key-metadata type, value-metadata NameIdentifier
+   * @param pathParams params from request path
+   * @param requestContext authorization request context
+   * @param currentPrincipal current principal
+   * @return authorization result
+   */
+  private boolean evaluate(
+      Map<Entity.EntityType, NameIdentifier> metadataNames,
+      Map<String, Object> pathParams,
+      AuthorizationRequestContext requestContext,
+      Principal currentPrincipal) {
     OgnlContext ognlContext = Ognl.createDefaultContext(null);
     ognlContext.put("principal", currentPrincipal);
-    ognlContext.put("authorizer", gravitinoAuthorizer);
+    ognlContext.put("authorizer", authorizer);
     ognlContext.put("authorizationContext", requestContext);
     ognlContext.putAll(pathParams);
     metadataNames.forEach(
diff --git 
a/server-common/src/test/java/org/apache/gravitino/server/authorization/expression/TestAuthorizationExpressionEvaluator.java
 
b/server-common/src/test/java/org/apache/gravitino/server/authorization/expression/TestAuthorizationExpressionEvaluator.java
index 59def1e536..ed3f291dc8 100644
--- 
a/server-common/src/test/java/org/apache/gravitino/server/authorization/expression/TestAuthorizationExpressionEvaluator.java
+++ 
b/server-common/src/test/java/org/apache/gravitino/server/authorization/expression/TestAuthorizationExpressionEvaluator.java
@@ -42,8 +42,6 @@ public class TestAuthorizationExpressionEvaluator {
   public void testEvaluator() {
     String expression =
         "CATALOG::USE_CATALOG && SCHEMA::USE_SCHEMA && (TABLE::SELECT_TABLE || 
TABLE::MODIFY_TABLE)";
-    AuthorizationExpressionEvaluator authorizationExpressionEvaluator =
-        new AuthorizationExpressionEvaluator(expression);
     try (MockedStatic<PrincipalUtils> principalUtilsMocked = 
mockStatic(PrincipalUtils.class);
         MockedStatic<GravitinoAuthorizerProvider> mockStatic =
             mockStatic(GravitinoAuthorizerProvider.class)) {
@@ -53,6 +51,9 @@ public class TestAuthorizationExpressionEvaluator {
       GravitinoAuthorizerProvider mockedProvider = 
mock(GravitinoAuthorizerProvider.class);
       
mockStatic.when(GravitinoAuthorizerProvider::getInstance).thenReturn(mockedProvider);
       when(mockedProvider.getGravitinoAuthorizer()).thenReturn(new 
MockGravitinoAuthorizer());
+      AuthorizationExpressionEvaluator authorizationExpressionEvaluator =
+          new AuthorizationExpressionEvaluator(expression);
+
       Map<Entity.EntityType, NameIdentifier> metadataNames = new HashMap<>();
       metadataNames.put(Entity.EntityType.METALAKE, 
NameIdentifierUtil.ofMetalake("testMetalake"));
       metadataNames.put(
@@ -79,17 +80,19 @@ public class TestAuthorizationExpressionEvaluator {
   @Test
   public void testEvaluatorWithOwner() {
     String expression = "METALAKE::OWNER || CATALOG::CREATE_CATALOG";
-    AuthorizationExpressionEvaluator authorizationExpressionEvaluator =
-        new AuthorizationExpressionEvaluator(expression);
     try (MockedStatic<PrincipalUtils> principalUtilsMocked = 
mockStatic(PrincipalUtils.class);
         MockedStatic<GravitinoAuthorizerProvider> mockStatic =
             mockStatic(GravitinoAuthorizerProvider.class)) {
-      principalUtilsMocked
-          .when(PrincipalUtils::getCurrentPrincipal)
-          .thenReturn(new UserPrincipal("tester"));
       GravitinoAuthorizerProvider mockedProvider = 
mock(GravitinoAuthorizerProvider.class);
       
mockStatic.when(GravitinoAuthorizerProvider::getInstance).thenReturn(mockedProvider);
       when(mockedProvider.getGravitinoAuthorizer()).thenReturn(new 
MockGravitinoAuthorizer());
+
+      AuthorizationExpressionEvaluator authorizationExpressionEvaluator =
+          new AuthorizationExpressionEvaluator(expression);
+      principalUtilsMocked
+          .when(PrincipalUtils::getCurrentPrincipal)
+          .thenReturn(new UserPrincipal("tester"));
+
       Map<Entity.EntityType, NameIdentifier> metadataNames = new HashMap<>();
       metadataNames.put(
           Entity.EntityType.METALAKE, 
NameIdentifierUtil.ofMetalake("metalakeWithOutOwner"));
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 5355fe7bc5..4a3a5d468a 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -59,6 +59,8 @@ if 
(gradle.startParameter.projectProperties["enableFuse"]?.toBoolean() == true)
 }
 include("iceberg:iceberg-common")
 include("iceberg:iceberg-rest-server")
+include("lance:lance-common")
+include("lance:lance-rest-server")
 include("authorizations:authorization-ranger", 
"authorizations:authorization-common", "authorizations:authorization-chain")
 include("trino-connector:trino-connector", "trino-connector:integration-test")
 include("spark-connector:spark-common")

Reply via email to