This is an automated email from the ASF dual-hosted git repository.
yuqi1129 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 0d195cf4f5 [#12574] improvement(lance): document REST auth and add ITs
(#12954)
0d195cf4f5 is described below
commit 0d195cf4f5173335f15ce1adf61306697b8c74cc
Author: Qi Yu <[email protected]>
AuthorDate: Mon Sep 14 11:28:00 2026 +0800
[#12574] improvement(lance): document REST auth and add ITs (#12954)
### What changes were proposed in this pull request?
Document Lance REST authentication, authorization, active roles, and
deployment differences, with examples and regression coverage.
Preserve backend authentication/authorization errors as HTTP 401/403,
remove stack traces from those errors and auxiliary authorization
denials, and return a generic response for unexpected HTTP 500 failures
while retaining server-side logging.
### Why are the changes needed?
Users need clear authentication guidance and correct, sanitized error
responses. Backend access denials currently become HTTP 500.
Fix: #12574
The independent Arrow CreateTable fix is tracked by #12988 and
implemented in #12989. Authentication-related fixes stay here because
the authentication work has not been merged into branch-1.3.
### Does this PR introduce _any_ user-facing change?
Improved authentication documentation; backend
authentication/authorization errors retain HTTP 401/403; authorization
denials omit stack traces; unexpected HTTP 500 responses omit internal
exception details. No identity propagation or configuration changes.
### How was this patch tested?
86 tests passed: 75 service unit tests and 11 integration tests across
LanceNamespaceAuthorizationIT and LanceRESTServiceAuthIT. No failures or
skips.
Coverage includes error mapping and sanitization, auxiliary
authentication and active roles, and standalone backend access denial
through a separate production server JVM. Relevant Spotless formatting
passed. Tests used the embedded backend; Docker and the full deployment
matrix were not run.
---
docs/lance-rest-integration.md | 52 ++++-
docs/lance-rest-service.md | 172 ++++++++++++--
lance/lance-rest-server/build.gradle.kts | 1 +
.../lance/service/LanceExceptionMapper.java | 12 +-
...anceMetadataAuthorizationMethodInterceptor.java | 6 +-
.../LanceRESTAuthInterceptionService.java | 3 +-
.../test/LanceNamespaceAuthorizationIT.java | 258 +++++++++++++++++++++
.../lance/service/TestLanceExceptionMapper.java | 58 ++++-
.../service/rest/TestLanceNamespaceOperations.java | 29 ++-
9 files changed, 547 insertions(+), 44 deletions(-)
diff --git a/docs/lance-rest-integration.md b/docs/lance-rest-integration.md
index 6942b01df4..18f581f522 100644
--- a/docs/lance-rest-integration.md
+++ b/docs/lance-rest-integration.md
@@ -21,7 +21,7 @@ This documentation assumes familiarity with the Lance REST
service setup as desc
The following table outlines the tested compatibility between Gravitino
versions and Lance connector versions:
| Gravitino Version (Lance REST) | Supported lance-spark Versions | Supported
lance-ray Versions |
-|--------------------------------|--------------------------------|-----------------------------------------------|
+| ------------------------------ | ------------------------------ |
--------------------------------------------- |
| 1.1.1 - 1.2.1 | 0.0.10 - 0.0.15 | 0.0.6 -
0.0.8 |
| 1.3.0 | 0.2.0, 0.4.0, 0.5.1 | 0.3.0 -
0.4.2 (0.2.0 conditionally supported) |
@@ -109,6 +109,56 @@ Before proceeding, ensure the following requirements are
met:
- For Spark integration: `pyspark`
- For Ray integration: `ray`, `lance-namespace`, `lance-ray`
+## Authentication and authorization
+
+For per-user metadata authorization, connect engines to the auxiliary Lance
REST service with
+`gravitino.authorization.enable=true`. Configure each engine's REST client to
send the caller's
+`Authorization` header on every namespace and table request. If supported by
that client version,
+`X-Gravitino-Active-Roles` can restrict the active roles. See the
+[Lance REST authentication and privilege
matrix](./lance-rest-service.md#authentication-and-authorization).
+
+For example, with development-only `simple` authentication, this request lists
only tables that
+`user1` may access (the password is not validated):
+
+```shell
+curl --user 'user1:unused' \
+ -H 'X-Gravitino-Active-Roles: ALL' \
+
'http://localhost:9101/lance/v1/namespace/lance_catalog.sales/table/list?delimiter=.'
+```
+
+Connector header configuration depends on the connector version. The Spark and
Ray examples
+below omit credentials and assume the default simple-authentication setup; in
auxiliary mode
+such requests use the configured Lance service identity. They do not
demonstrate per-user
+access control. In standalone mode, all metadata requests to Gravitino use the
backend service
+identity even when an engine supplies its own incoming credentials.
+
+Engines that probe before creating need the corresponding creation privileges.
Reading table
+metadata requires `SELECT_TABLE` or `MODIFY_TABLE` with parent access, while
overwriting requires
+`MODIFY_TABLE` and dropping requires ownership. Metadata authorization does
not authorize direct
+reads or writes to object storage: configure storage access independently.
Lance REST responses
+can return shared storage credentials configured on the catalog or table; it
does not issue
+per-user, scoped storage credentials.
+
+### Verify authentication and authorization locally
+
+The HTTP integration suites start Gravitino with the Lance auxiliary service
and exercise
+caller identity, service identity fallback, active roles, namespace and table
privileges,
+filtered listings, denied mutations, and rejection of non-empty Arrow creates
without side effects.
+They also start standalone Lance REST through its production entry point in a
separate JVM to verify
+its outbound service identity and propagation of backend authorization denials
through the Gravitino
+HTTP API.
+
+```shell
+./gradlew :lance:lance-rest-server:test \
+ --tests '*LanceRESTServiceAuthIT' \
+ --tests '*LanceNamespaceAuthorizationIT' \
+ --tests '*LanceTableAuthorizationIT' \
+ -PskipDockerTests=true
+```
+
+These suites use `simple` authentication and local storage. They do not
validate an external
+OAuth2/Kerberos provider or object-store access policies.
+
## Spark Integration
### Configuration
diff --git a/docs/lance-rest-service.md b/docs/lance-rest-service.md
index 7186243bb2..14b9c61a01 100644
--- a/docs/lance-rest-service.md
+++ b/docs/lance-rest-service.md
@@ -57,7 +57,7 @@ The Lance REST service acts as a bridge between Lance
datasets and applications:
```
**Key Features:**
-- Full compliance with Lance REST API specification
+- Support for the Lance REST operations listed below
- Can run standalone or integrated with Gravitino server
- Support for namespace and table management
- Metadata stored in Gravitino for unified governance
@@ -67,7 +67,7 @@ The Lance REST service acts as a bridge between Lance
datasets and applications:
The Lance REST service provides comprehensive support for namespace management
and table management. Index operations are not supported yet. The table below
lists all supported operations:
| Operation | Description
| HTTP Method | Endpoint Pattern |
-|-------------------|---------------------------------------------------------------------------|-------------|---------------------------------------|
+| ----------------- |
------------------------------------------------------------------------- |
----------- | ------------------------------------- |
| CreateNamespace | Create a new Lance namespace
| POST | `/lance/v1/namespace/{id}/create` |
| ListNamespaces | List all namespaces under a parent namespace
| GET | `/lance/v1/namespace/{parent}/list` |
| DescribeNamespace | Retrieve detailed information about a specific namespace
| POST | `/lance/v1/namespace/{id}/describe` |
@@ -99,17 +99,24 @@ REST-style canonical form.
- `overwrite`: Replaces existing namespace
**DropNamespace** behavior:
-- Recursively deletes all child namespaces and tables
-- Deletes both metadata and Lance data files
-- Operation is irreversible
+- Defaults to `behavior=restrict`; non-empty namespaces cannot be dropped in
this mode
+- `behavior=cascade` removes child metadata recursively
+- Cascading a schema drop uses table drop semantics: external Lance datasets
are preserved
+- Use `DropTable` explicitly to delete a table's data; namespace deletion is
not a storage purge
#### Table Operations
**RegisterTable vs CreateTable**:
- **RegisterTable**: Links existing Lance datasets into Gravitino catalog
without data movement
-- **CreateTable**: Creates new Lance table with schema and write metadata files
+- **CreateTable**: Creates an empty Lance dataset using the schema from the
Arrow IPC stream
:::note
-The `version` field of `CreateTable` response is always null, which stands for
the latest version.
+The current `CreateTable` implementation accepts schema-only Arrow streams,
including zero-row
+batches. A stream containing rows returns HTTP `406` before any metadata or
dataset changes,
+including for `overwrite`. Write records through a Lance client or engine
after creation.
+
+The `version` field of `CreateTable` reports the stored Lance dataset version
when available.
+`DescribeTable` currently returns the latest metadata even when a historical
`version` is requested;
+versioned metadata reads are not implemented.
:::
**DropTable vs DeregisterTable**:
@@ -124,7 +131,7 @@ The `version` field of `CreateTable` response is always
null, which stands for t
To enable the Lance REST service within Gravitino server, configure the
following properties in your Gravitino configuration file
`${GRAVITINO_HOME}/conf/gravitino.conf`:
| Configuration Property | Description
| Default Value |
Required |
-|-------------------------------------------|------------------------------------------------------------------------------|-------------------------|----------|
+| ----------------------------------------- |
---------------------------------------------------------------------------- |
----------------------- | -------- |
| `gravitino.auxService.names` | Auxiliary services to run.
Include `lance-rest` to enable Lance REST service | iceberg-rest,lance-rest |
Yes |
| `gravitino.lance-rest.classpath` | Classpath for Lance REST
service, relative to Gravitino home directory | lance-rest-server/libs |
Yes |
| `gravitino.lance-rest.httpPort` | Port number for Lance REST
service | 9101 |
No |
@@ -135,13 +142,13 @@ To enable the Lance REST service within Gravitino server,
configure the followin
**Authentication to the Gravitino Server**
-The Lance REST service makes its own requests to the Gravitino server. Those
requests must carry
-credentials, otherwise a Gravitino server configured with an authenticator
other than `simple`
-rejects them and every Lance operation fails. Configure the auth type to match
the Gravitino
-server:
+In standalone mode, the Lance REST service makes HTTP requests to the
Gravitino server using
+its configured service credentials. Configure the auth type to match the
Gravitino server.
+Auxiliary mode uses internal APIs and preserves the authenticated caller
instead; the simple
+user name below is only the fallback for requests accepted as anonymous.
| Configuration Property | Description
| Default Value
| Required |
-|----------------------------------------------------|------------------------------------------------------------------------------------|---------------------|-------------------|
+| -------------------------------------------------- |
----------------------------------------------------------------------------------
| ------------------- | ----------------- |
| `gravitino.lance-rest.gravitino-auth-type` | Auth type used to reach
the Gravitino server. Supported values: `simple`, `oauth2` | `simple`
| No |
| `gravitino.lance-rest.gravitino-simple.user-name` | User name presented
when the auth type is `simple` |
`lance-rest-server` | No |
| `gravitino.lance-rest.gravitino-oauth2.server-uri` | OAuth2 server URI
| (none)
| Yes, for `oauth2` |
@@ -149,8 +156,8 @@ server:
| `gravitino.lance-rest.gravitino-oauth2.token-path` | Path on the OAuth2
server used to request the token | (none)
| Yes, for `oauth2` |
| `gravitino.lance-rest.gravitino-oauth2.scope` | Scope of the requested
OAuth2 token | (none)
| Yes, for `oauth2` |
-This setting controls how the service authenticates to the Gravitino server.
It does not change how
-callers authenticate to the Lance REST service itself.
+These settings control outbound authentication in standalone mode. They do not
configure inbound
+authentication to Lance REST. See [Authentication and
authorization](#authentication-and-authorization).
**Example Configuration:**
@@ -176,7 +183,7 @@ To run Lance REST service independently without Gravitino
server (You need to st
Configure the service by editing
`{GRAVITINO_HOME}/conf/gravitino-lance-rest-server.conf` or passing
command-line arguments:
| Configuration Property | Description |
Default Value | Required |
-|-------------------------------------------|----------------------------|-----------------------|----------|
+| ----------------------------------------- | -------------------------- |
--------------------- | -------- |
| `gravitino.lance-rest.namespace-backend` | Namespace metadata backend |
gravitino | Yes |
| `gravitino.lance-rest.gravitino-uri` | Gravitino server URI |
http://localhost:8090 | Yes |
| `gravitino.lance-rest.gravitino-metalake` | Gravitino metalake name |
(none) | Yes |
@@ -205,7 +212,7 @@ Access the service at `http://localhost:9101`.
**Environment Variables:**
| Environment Variable | Configuration Property
| Required | Default Value |
-|--------------------------------------|-------------------------------------------|----------|-------------------------|
+| ------------------------------------ |
----------------------------------------- | -------- | ----------------------- |
| `LANCE_REST_NAMESPACE_BACKEND` |
`gravitino.lance-rest.namespace-backend` | Yes | `gravitino` |
| `LANCE_REST_GRAVITINO_METALAKE_NAME` |
`gravitino.lance-rest.gravitino-metalake` | Yes | (none) |
| `LANCE_REST_GRAVITINO_URI` | `gravitino.lance-rest.gravitino-uri`
| Yes | `http://localhost:8090` |
@@ -271,9 +278,138 @@ URL encoded: lance_catalog%24schema%24table01
- Supports only **two levels of namespaces** before tables
- Tables **cannot** be nested deeper than schema level
- Parent catalog must be created in Gravitino before using Lance REST API
-- Namespace deletion is recursive and irreversible
+- Namespace deletion defaults to `restrict`; use `cascade` to remove child
metadata
:::
+## Authentication and authorization
+
+### Authentication and deployment modes
+
+Lance REST uses Gravitino's `gravitino.authenticators` configuration for
incoming requests in
+both auxiliary and standalone mode. See
[Authentication](./security/how-to-authenticate.md) for
+configuring the authenticators and their credentials. Health check endpoints
bypass authentication.
+Authentication errors use the Lance JSON error format; unsupported credentials
return HTTP `401`.
+In standalone mode, backend authentication and authorization failures retain
HTTP `401` and `403`
+respectively. Authentication/authorization failures do not include internal
stack traces in `detail`.
+Unexpected failures return HTTP `500` with a generic message; the server logs
retain the exception
+for diagnosis.
+
+| Mode | Identity used for Gravitino metadata
operations
| Metadata authorization
|
+| ---------------------------------- |
-------------------------------------------------------------------------------------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------------------------------------------------------------
|
+| Auxiliary (running with Gravitino) | Authenticated caller, including active
roles; anonymous requests fall back to
`gravitino.lance-rest.gravitino-simple.user-name` (default `lance-rest-server`)
| Enabled by `gravitino.authorization.enable=true` with a configured metalake
|
+| Standalone | Configured service credentials
(`gravitino.lance-rest.gravitino-auth-type` and its simple/OAuth2 settings)
| No Lance REST per-user
metadata authorization; the remote Gravitino server checks the service identity
if its authorization is enabled |
+
+The auxiliary fallback applies only after authentication accepts an anonymous
request. It does
+not recover a rejected authentication attempt. Authenticated callers keep
their own privileges,
+active roles, ownership and audit identity; they do not inherit the service
user's privileges.
+The fallback service user itself needs the privileges required by the
requested operation.
+Setting `gravitino.lance-rest.gravitino-simple.user-name` explicitly is
optional in auxiliary mode;
+configure it only to override the default anonymous fallback identity,
`lance-rest-server`.
+
+With `simple` authentication, a Basic header supplies a user name without
validating a password,
+and a request without credentials is accepted as anonymous. Some malformed
Basic credentials
+also resolve to anonymous. Use an authenticator that validates credentials
when caller identity
+must be verified; `simple` is not password authentication.
+
+Standalone authenticates incoming requests, but does not forward their
identities or active roles
+to its Gravitino backend. All callers use the configured backend service
identity. Standalone
+per-user authorization and scoped temporary credential vending are outside the
supported scope. The
+backend service identity needs privileges for all underlying Gravitino calls,
including existence
+checks performed before mutations (for example, catalog access before creating
a namespace).
+
+### Enable auxiliary metadata authorization
+
+Configure `${GRAVITINO_HOME}/conf/gravitino.conf`:
+
+```properties
+gravitino.auxService.names = lance-rest
+gravitino.lance-rest.gravitino-metalake = my_metalake
+gravitino.authorization.enable = true
+gravitino.authorization.serviceAdmins = adminUser
+# Development example: simple accepts the supplied user name without password
validation.
+gravitino.authenticators = simple
+```
+
+Create the metalake, add users, and grant roles through the Gravitino API as
described in
+[Access Control](./security/access-control.md). The Lance service exposes this
configured metalake:
+a one-level namespace identifies a catalog, a two-level namespace identifies a
schema, and a
+three-level table identifier identifies a table.
+
+Requests may set `X-Gravitino-Active-Roles` to `ALL` (also the default when
omitted), `NONE`, or
+a comma-separated list of assigned role names. This selection reaches both
operation checks
+and listing filters. Malformed selections return `400`; selecting an
unassigned role is forbidden.
+Ownership is independent of role selection, so `NONE` does not remove
ownership privileges.
+
+### Required privileges
+
+The following rules use the same Gravitino privileges and ownership rules as
+[Iceberg REST authorization](./iceberg-rest-service.md). Privileges can be
inherited from
+ancestor scopes as described in Access Control. Service administrators and
metalake owners
+can operate throughout the metalake; catalog owners can operate within their
catalogs.
+Schema owners additionally need `USE_CATALOG`, and table owners need
`USE_CATALOG` and
+`USE_SCHEMA`. The ownership alternatives below include these ancestor owners.
+
+| Namespace operation
| Required privileges or ownership
|
+|
----------------------------------------------------------------------------------------------------
| ---------------------------------------------------------------------- |
+| `ListNamespaces` at root
| Membership in the metalake; returns only accessible
catalogs |
+| `ListNamespaces` under a catalog; `DescribeNamespace` for a catalog;
`NamespaceExists` for a catalog | `USE_CATALOG`, or ownership
|
+| `ListNamespaces` under a schema; `DescribeNamespace` for a schema;
`ListTables` | `USE_CATALOG` and `USE_SCHEMA`, or
ownership |
+| `NamespaceExists` for a schema
| `USE_CATALOG` and either `USE_SCHEMA` or
`CREATE_SCHEMA`, or ownership |
+| `CreateNamespace` for a catalog (`create`, `exist_ok`)
| `CREATE_CATALOG` on the metalake, or metalake
ownership |
+| `CreateNamespace` for a schema (`create`, `exist_ok`)
| `USE_CATALOG` and `CREATE_SCHEMA`, or
catalog/metalake ownership |
+| `CreateNamespace` (`overwrite`); `DropNamespace`
| Ownership of the namespace or an ancestor
|
+
+| Table operation
| Required privileges or ownership
|
+|
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------------------------------
|
+| `DescribeTable`
| `USE_CATALOG`, `USE_SCHEMA`, and either `SELECT_TABLE` or `MODIFY_TABLE`,
or ownership |
+| `TableExists`
| Same as `DescribeTable`, or `USE_CATALOG`, `USE_SCHEMA`, and either
`PROBE_TABLE_LIKE` or `CREATE_TABLE` |
+| `CreateTable` (`create`, `exist_ok`); `RegisterTable` (`create`);
`DeclareTable` | `USE_CATALOG`, `USE_SCHEMA`, and `CREATE_TABLE`, or
schema/ancestor ownership |
+| `CreateTable` or `RegisterTable` (`overwrite`); `AlterColumns`;
`DropColumns` | `USE_CATALOG`, `USE_SCHEMA`, and `MODIFY_TABLE`, or
ownership |
+| `DropTable`; `DeregisterTable`
| Ownership of the table or an ancestor
|
+
+`CREATE_TABLE` and `PROBE_TABLE_LIKE` authorize `TableExists` without
authorizing
+`DescribeTable`. `CREATE_TABLE` alone does not authorize overwrite requests.
The `DropTable`
+and `DeregisterTable` endpoints require ownership rather than `MODIFY_TABLE`.
Similarly, namespace creation privileges do not authorize
+overwriting or dropping another owner's namespace. Successful creation assigns
ownership to
+the effective caller.
+
+### Listings and concealed objects
+
+Namespace listings omit inaccessible catalogs and schemas. Table listings omit
tables for which
+the caller has neither ownership nor `SELECT_TABLE`/`MODIFY_TABLE`. Filtering
happens before
+pagination; hidden entries do not consume page slots. Access to the parent is
checked separately.
+
+When a caller lacks an endpoint's required privileges, the request returns
`403`, whether or not
+the target exists, without returning its stored metadata or location. An
authorized caller can
+distinguish an existing object from a missing one (`404`). Concealment
therefore does not mean that every
+inaccessible object returns `404`.
+
+Authorization governs metadata requests; engines access Lance data files
directly. Responses can
+include configured storage credentials: namespace descriptions resolve secret
properties, and
+table descriptions, creation and declaration responses return effective
`storage_options` from
+catalog defaults and table overrides. These are shared configured credentials,
not temporary
+credentials restricted to the caller's table privileges. Access to data files
depends on the
+permissions of those credentials. Per-user, scoped credential vending is not
implemented.
+
+### Authorization differences between deployment modes
+
+:::warning
+Lance REST metadata authorization is currently supported only in **auxiliary
mode**.
+**Standalone mode is not recommended**: its authorization decisions can differ
from auxiliary
+mode and may produce unexpected results, even for the same user and
privileges. Use auxiliary
+mode for deployments that require Lance REST authorization.
+:::
+
+Auxiliary mode applies the Lance endpoint authorization rules and listing
filters described above.
+Standalone mode does not apply this authorization pipeline. Instead, the
remote Gravitino server
+checks each underlying REST call using its own rules and the backend identity
described above.
+These checks do not provide equivalent Lance REST authorization: an operation
allowed in one mode
+may be denied in the other, and ownership requirements and metadata visibility
can also differ.
+Forwarding the caller's identity alone does not eliminate these differences.
+
+Alignment of the authorization behavior is tracked in
+[#13089](https://github.com/apache/gravitino/issues/13089).
+
## Examples
The following examples demonstrate how to interact with Lance REST service
using different programming languages and tools.
diff --git a/lance/lance-rest-server/build.gradle.kts
b/lance/lance-rest-server/build.gradle.kts
index 7899bad829..d55180fa15 100644
--- a/lance/lance-rest-server/build.gradle.kts
+++ b/lance/lance-rest-server/build.gradle.kts
@@ -194,6 +194,7 @@ tasks {
val primaryBundleDir =
lanceSparkBundleDirFor(primaryLanceSparkBundleVersion)
doFirst {
+ systemProperty("lance.test.runtimeClasspath",
sourceSets["main"].runtimeClasspath.asPath)
val bundleJar =
primaryBundleDir.get().asFile.listFiles()?.singleOrNull { it.extension
== "jar" }
?: throw GradleException(
diff --git
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceExceptionMapper.java
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceExceptionMapper.java
index a3de0c46d6..a56ee9bc3b 100644
---
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceExceptionMapper.java
+++
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceExceptionMapper.java
@@ -23,8 +23,10 @@ import static
org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
+import org.apache.gravitino.exceptions.ForbiddenException;
import org.apache.gravitino.exceptions.NoSuchTableException;
import org.apache.gravitino.exceptions.NotFoundException;
+import org.apache.gravitino.exceptions.UnauthorizedException;
import org.apache.gravitino.server.web.ServerHealth;
import org.lance.namespace.errors.ConcurrentModificationException;
import org.lance.namespace.errors.InternalException;
@@ -68,7 +70,13 @@ public class LanceExceptionMapper implements
ExceptionMapper<Throwable> {
}
private static LanceNamespaceException toLanceNamespaceException(String
instance, Throwable ex) {
- if (ex instanceof NoSuchTableException) {
+ if (ex instanceof ForbiddenException) {
+ return new PermissionDeniedException(ex.getMessage(), "", instance);
+
+ } else if (ex instanceof UnauthorizedException) {
+ return new UnauthenticatedException(ex.getMessage(), "", instance);
+
+ } else if (ex instanceof NoSuchTableException) {
return new TableNotFoundException(ex.getMessage(), getStackTrace(ex),
instance);
} else if (ex instanceof NotFoundException) {
@@ -86,7 +94,7 @@ public class LanceExceptionMapper implements
ExceptionMapper<Throwable> {
} else {
LOG.warn("Lance REST server unexpected exception:", ex);
- return new InternalException(ex.getMessage(), getStackTrace(ex),
instance);
+ return new InternalException("Internal server error", "", instance);
}
}
diff --git
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceMetadataAuthorizationMethodInterceptor.java
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceMetadataAuthorizationMethodInterceptor.java
index 88b3d645ec..3ec6e9c903 100644
---
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceMetadataAuthorizationMethodInterceptor.java
+++
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceMetadataAuthorizationMethodInterceptor.java
@@ -18,8 +18,6 @@
*/
package org.apache.gravitino.lance.service.authorization;
-import static org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace;
-
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
@@ -188,9 +186,7 @@ public class LanceMetadataAuthorizationMethodInterceptor
String namespaceId = pathArgument(method.getParameters(), args,
"id").orElse("");
Exception exception;
if (throwable instanceof ForbiddenException) {
- exception =
- new PermissionDeniedException(
- throwable.getMessage(), getStackTrace(throwable), namespaceId);
+ exception = new PermissionDeniedException(throwable.getMessage(), "",
namespaceId);
} else if (throwable instanceof Exception) {
exception = (Exception) throwable;
} else {
diff --git
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceRESTAuthInterceptionService.java
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceRESTAuthInterceptionService.java
index ae10436f69..4a717aefc7 100644
---
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceRESTAuthInterceptionService.java
+++
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceRESTAuthInterceptionService.java
@@ -42,8 +42,7 @@ public class LanceRESTAuthInterceptionService implements
InterceptionService {
public static final String METALAKE_BINDING = "lanceAuthorizationMetalake";
// Membership here only routes a class through the interceptor; each method
still opts in with
- // @AuthorizationExpression, and a method without one runs unauthorized. The
table writes
- // (create, register, drop, alter) are still to be annotated.
+ // @AuthorizationExpression. Endpoint coverage tests ensure no REST
operation omits it.
private static final Set<String> INTERCEPTED_CLASSES =
ImmutableSet.of(
LanceNamespaceOperations.class.getName(),
LanceTableOperations.class.getName());
diff --git
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceNamespaceAuthorizationIT.java
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceNamespaceAuthorizationIT.java
index 5226a20beb..ae4bffe98a 100644
---
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceNamespaceAuthorizationIT.java
+++
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceNamespaceAuthorizationIT.java
@@ -18,16 +18,21 @@
*/
package org.apache.gravitino.lance.integration.test;
+import java.io.Writer;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
import org.apache.gravitino.Configs;
import org.apache.gravitino.auth.AuthConstants;
import org.apache.gravitino.authorization.Privileges;
@@ -35,14 +40,20 @@ import org.apache.gravitino.authorization.SecurableObject;
import org.apache.gravitino.authorization.SecurableObjects;
import org.apache.gravitino.client.GravitinoMetalake;
import org.apache.gravitino.integration.test.util.BaseIT;
+import org.apache.gravitino.integration.test.util.HttpUtils;
+import org.apache.gravitino.lance.server.GravitinoLanceRESTServer;
+import org.apache.gravitino.rest.RESTUtils;
import org.apache.gravitino.server.web.ObjectMapperProvider;
+import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
import org.lance.namespace.model.CreateNamespaceRequest;
import org.lance.namespace.model.DescribeNamespaceResponse;
import org.lance.namespace.model.DropNamespaceRequest;
+import org.lance.namespace.model.ErrorResponse;
import org.lance.namespace.model.ListNamespacesResponse;
/** Verifies namespace authorization and list filtering through auxiliary-mode
Lance REST. */
@@ -70,6 +81,7 @@ public class LanceNamespaceAuthorizationIT extends BaseIT {
customConfigs.put(Configs.SERVICE_ADMINS.getKey(), ADMIN);
customConfigs.put(Configs.AUTHENTICATORS.getKey(), "simple");
customConfigs.put("SimpleAuthUserName", ADMIN);
+ customConfigs.put("gravitino.lance-rest.gravitino-simple.user-name",
WRITER);
super.startIntegrationTest();
String metalakeName = getLanceRESTServerMetalakeName();
@@ -181,6 +193,252 @@ public class LanceNamespaceAuthorizationIT extends BaseIT
{
assertStatus(403, drop(USER, HIDDEN_CATALOG, "skip", null));
}
+ /** Verifies that active roles survive authentication and reach namespace
authorization. */
+ @Test
+ public void testActiveRolesReachAuthorizationAndListingFilters() throws
Exception {
+ String schemaPath = "/v1/namespace/" + id(VISIBLE_CATALOG, VISIBLE_SCHEMA)
+ "/describe";
+ assertStatus(200, sendWithRoles(USER, schemaPath, "ALL"));
+ assertStatus(403, sendWithRoles(USER, schemaPath, "NONE"));
+ assertStatus(403, sendWithRoles(USER, schemaPath,
"lance_authz_catalog_role"));
+ assertStatus(
+ 200, sendWithRoles(USER, schemaPath,
"lance_authz_catalog_role,lance_authz_schema_role"));
+ assertStatus(403, sendWithRoles(USER, schemaPath,
"lance_authz_writer_role"));
+
+ HttpResponse<String> response =
+ httpClient.send(
+ request(USER, "/v1/namespace/" + VISIBLE_CATALOG + "/list")
+ .setHeader(
+ AuthConstants.X_GRAVITINO_ACTIVE_ROLES_HEADER,
"lance_authz_catalog_role")
+ .GET()
+ .build(),
+ HttpResponse.BodyHandlers.ofString());
+ assertStatus(200, response);
+ Assertions.assertTrue(
+ ObjectMapperProvider.objectMapper()
+ .readValue(response.body(), ListNamespacesResponse.class)
+ .getNamespaces()
+ .isEmpty());
+ // Narrowing one request must not affect subsequent requests on the same
server.
+ Assertions.assertEquals(List.of(VISIBLE_SCHEMA), list(USER,
VISIBLE_CATALOG));
+ }
+
+ /** Verifies authentication failures stop before metadata writes or service
identity fallback. */
+ @Test
+ public void testAuthenticationErrorsUseLanceJsonAndDoNotCreateMetadata()
throws Exception {
+ String catalog = "lance_authz_rejected_auth_catalog";
+ CreateNamespaceRequest body = new CreateNamespaceRequest();
+ body.addIdItem(catalog);
+ String json = ObjectMapperProvider.objectMapper().writeValueAsString(body);
+ HttpResponse<String> unauthorized =
+ httpClient.send(
+ request(ADMIN, "/v1/namespace/" + catalog + "/create")
+ .setHeader(AuthConstants.HTTP_HEADER_AUTHORIZATION, "Bearer
unsupported-token")
+ .POST(HttpRequest.BodyPublishers.ofString(json))
+ .build(),
+ HttpResponse.BodyHandlers.ofString());
+ assertError(401, unauthorized);
+ HttpResponse<String> malformedRoles =
+ httpClient.send(
+ request(ADMIN, "/v1/namespace/" + catalog + "/create")
+ .setHeader(AuthConstants.X_GRAVITINO_ACTIVE_ROLES_HEADER,
"ALL,NONE")
+ .POST(HttpRequest.BodyPublishers.ofString(json))
+ .build(),
+ HttpResponse.BodyHandlers.ofString());
+ assertError(400, malformedRoles);
+ assertStatus(404, post(ADMIN, catalog, "exists"));
+ }
+
+ /** Verifies anonymous fallback uses the service user's privileges and
records its ownership. */
+ @Test
+ public void testServiceIdentityFallbackIsAuthorized() throws Exception {
+ String catalog = "lance_authz_fallback_catalog";
+ CreateNamespaceRequest body = new CreateNamespaceRequest();
+ body.addIdItem(catalog);
+ HttpRequest.Builder anonymous =
+ HttpRequest.newBuilder()
+ .uri(
+ URI.create(
+ String.format("http://localhost:%d/lance",
getLanceRESTServerPort())
+ + "/v1/namespace/"
+ + catalog
+ + "/create?delimiter=."))
+ .header("Content-Type", "application/json");
+ assertStatus(
+ 200,
+ httpClient.send(
+ anonymous
+ .POST(
+ HttpRequest.BodyPublishers.ofString(
+
ObjectMapperProvider.objectMapper().writeValueAsString(body)))
+ .build(),
+ HttpResponse.BodyHandlers.ofString()));
+ GravitinoMetalake metalake =
client.loadMetalake(getLanceRESTServerMetalakeName());
+ Assertions.assertEquals(WRITER,
metalake.loadCatalog(catalog).auditInfo().creator());
+ // Ownership is usable by the real service user after creation through
anonymous fallback.
+ assertStatus(200, drop(WRITER, catalog, null, "cascade"));
+
+ HttpRequest denied =
+ HttpRequest.newBuilder()
+ .uri(
+ URI.create(
+ String.format("http://localhost:%d/lance",
getLanceRESTServerPort())
+ + "/v1/namespace/"
+ + HIDDEN_CATALOG
+ + "/describe?delimiter=."))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString("{}"))
+ .build();
+ assertStatus(403, httpClient.send(denied,
HttpResponse.BodyHandlers.ofString()));
+ // An authenticated reader cannot borrow the fallback user's
CREATE_CATALOG privilege.
+ assertStatus(403, create(USER, catalog, null, Map.of()));
+ }
+
+ /** Verifies standalone HTTP backend calls use service credentials rather
than caller roles. */
+ @Test
+ public void testStandaloneUsesBackendServiceIdentity(@TempDir Path
directory) throws Exception {
+ int port = RESTUtils.findAvailablePort(10000, 11000);
+ String catalog = "lance_authz_standalone_catalog";
+ String serviceUser = "lance_authz_standalone_user";
+ GravitinoMetalake metalake =
client.loadMetalake(getLanceRESTServerMetalakeName());
+ metalake.addUser(serviceUser);
+ metalake.createRole(
+ "lance_authz_standalone_role",
+ new HashMap<>(),
+ List.of(
+ SecurableObjects.ofMetalake(
+ metalake.name(),
+ new ArrayList<>(
+ List.of(Privileges.UseCatalog.allow(),
Privileges.CreateCatalog.allow())))));
+ metalake.grantRolesToUser(List.of("lance_authz_standalone_role"),
serviceUser);
+ Properties config = new Properties();
+ config.setProperty(Configs.AUTHENTICATORS.getKey(), "simple");
+ config.setProperty("gravitino.lance-rest.httpPort", String.valueOf(port));
+ config.setProperty(
+ "gravitino.lance-rest.gravitino-uri", "http://localhost:" +
getGravitinoServerPort());
+ config.setProperty("gravitino.lance-rest.gravitino-metalake",
getLanceRESTServerMetalakeName());
+ config.setProperty("gravitino.lance-rest.gravitino-auth-type", "simple");
+ config.setProperty("gravitino.lance-rest.gravitino-simple.user-name",
serviceUser);
+ Path configFile = directory.resolve("standalone.conf");
+ try (Writer writer = Files.newBufferedWriter(configFile)) {
+ config.store(writer, "Standalone Lance REST integration test");
+ }
+ Path logFile = directory.resolve("standalone.log");
+ // Use the production bootstrap in its own JVM: deploy mode has no local
GravitinoEnv,
+ // while embedded mode must not share its backend environment with the
standalone service.
+ ProcessBuilder builder =
+ new ProcessBuilder(
+ Path.of(System.getProperty("java.home"), "bin",
"java").toString(),
+ "--add-opens=java.base/java.nio=ALL-UNNAMED",
+ "-cp",
+ System.getProperty("lance.test.runtimeClasspath"),
+ GravitinoLanceRESTServer.class.getName(),
+ configFile.toString())
+ .redirectErrorStream(true)
+ .redirectOutput(logFile.toFile());
+ builder.environment().put("GRAVITINO_TEST", "true");
+ Process standalone = builder.start();
+ try {
+ try {
+ Awaitility.await()
+ .atMost(60, TimeUnit.SECONDS)
+ .until(
+ () -> {
+ Assertions.assertTrue(standalone.isAlive(), "Standalone
process exited");
+ // Namespace initialization is lazy and occurs on the first
metadata request.
+ return HttpUtils.isHttpServerUp(
+ "http://localhost:" + port + "/lance/health/live");
+ });
+ } catch (Exception | AssertionError e) {
+ throw new AssertionError("Standalone startup failed:\n" +
Files.readString(logFile), e);
+ }
+ CreateNamespaceRequest body = new CreateNamespaceRequest();
+ body.addIdItem(catalog);
+ HttpRequest request =
+ request(USER, "/v1/namespace/" + catalog + "/create")
+ .uri(
+ URI.create(
+ "http://localhost:"
+ + port
+ + "/lance/v1/namespace/"
+ + catalog
+ + "/create?delimiter=."))
+ .setHeader(AuthConstants.X_GRAVITINO_ACTIVE_ROLES_HEADER, "NONE")
+ .POST(
+ HttpRequest.BodyPublishers.ofString(
+
ObjectMapperProvider.objectMapper().writeValueAsString(body)))
+ .build();
+ // USER cannot create catalogs in auxiliary mode. The backend receives
the service user's
+ // credentials and roles, despite USER selecting NONE on this incoming
request.
+ assertStatus(200, httpClient.send(request,
HttpResponse.BodyHandlers.ofString()));
+ Assertions.assertEquals(
+ serviceUser,
+ client
+ .loadMetalake(getLanceRESTServerMetalakeName())
+ .loadCatalog(catalog)
+ .auditInfo()
+ .creator());
+ // The backend service user cannot read this admin-owned schema. Even an
incoming admin
+ // must receive the backend's 403, rather than 500 or the incoming
caller's privileges.
+ HttpRequest deniedRequest =
+ request(ADMIN, "/v1/namespace/" + id(VISIBLE_CATALOG,
VISIBLE_SCHEMA) + "/describe")
+ .uri(
+ URI.create(
+ "http://localhost:"
+ + port
+ + "/lance/v1/namespace/"
+ + id(VISIBLE_CATALOG, VISIBLE_SCHEMA)
+ + "/describe?delimiter=."))
+ .POST(HttpRequest.BodyPublishers.ofString("{}"))
+ .build();
+ HttpResponse<String> deniedResponse =
+ httpClient.send(deniedRequest, HttpResponse.BodyHandlers.ofString());
+ assertStatus(403, deniedResponse);
+ ErrorResponse error =
+ ObjectMapperProvider.objectMapper().readValue(deniedResponse.body(),
ErrorResponse.class);
+ Assertions.assertEquals("", error.getDetail());
+ Assertions.assertTrue(error.getError().contains(serviceUser),
error.getError());
+ assertStatus(200, drop(serviceUser, catalog, null, "cascade"));
+ } finally {
+ standalone.destroy();
+ if (!standalone.waitFor(10, TimeUnit.SECONDS)) {
+ standalone.destroyForcibly();
+ Assertions.assertTrue(
+ standalone.waitFor(10, TimeUnit.SECONDS), "Standalone process did
not stop");
+ }
+ }
+ }
+
+ /** Verifies health endpoints remain reachable even when credentials would
be rejected. */
+ @Test
+ public void testHealthBypassesAuthentication() throws Exception {
+ HttpRequest health =
+ request(USER, "/health")
+ .setHeader(AuthConstants.HTTP_HEADER_AUTHORIZATION, "Bearer
unsupported-token")
+ .GET()
+ .build();
+ assertStatus(200, httpClient.send(health,
HttpResponse.BodyHandlers.ofString()));
+ }
+
+ private HttpResponse<String> sendWithRoles(String user, String path, String
roles)
+ throws Exception {
+ return httpClient.send(
+ request(user, path)
+ .setHeader(AuthConstants.X_GRAVITINO_ACTIVE_ROLES_HEADER, roles)
+ .POST(HttpRequest.BodyPublishers.ofString("{}"))
+ .build(),
+ HttpResponse.BodyHandlers.ofString());
+ }
+
+ private void assertError(int status, HttpResponse<String> response) throws
Exception {
+ assertStatus(status, response);
+ Assertions.assertTrue(
+
response.headers().firstValue("Content-Type").orElse("").startsWith("application/json"));
+ ErrorResponse error =
+ ObjectMapperProvider.objectMapper().readValue(response.body(),
ErrorResponse.class);
+ Assertions.assertEquals(status, error.getCode());
+ Assertions.assertFalse(error.getError().isEmpty());
+ }
+
private void grant(GravitinoMetalake metalake, String role, SecurableObject
object) {
metalake.createRole(role, new HashMap<>(), List.of(object));
metalake.grantRolesToUser(List.of(role), USER);
diff --git
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/TestLanceExceptionMapper.java
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/TestLanceExceptionMapper.java
index fd3d98ded6..8f4513fdf6 100644
---
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/TestLanceExceptionMapper.java
+++
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/TestLanceExceptionMapper.java
@@ -24,6 +24,8 @@ import javax.ws.rs.Path;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.gravitino.exceptions.UnauthorizedException;
import org.apache.gravitino.rest.RESTUtils;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;
@@ -31,6 +33,7 @@ import org.glassfish.jersey.test.JerseyTest;
import org.glassfish.jersey.test.TestProperties;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.lance.namespace.errors.InvalidInputException;
import org.lance.namespace.model.ErrorResponse;
/** Tests for {@link LanceExceptionMapper}. */
@@ -79,12 +82,57 @@ public class TestLanceExceptionMapper extends JerseyTest {
Assertions.assertEquals(
Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
response.getStatus());
ErrorResponse entity = response.readEntity(ErrorResponse.class);
- Assertions.assertEquals("assertion failure", entity.getError());
+ Assertions.assertEquals("Internal server error", entity.getError());
Assertions.assertEquals("", entity.getInstance());
- Assertions.assertTrue(
- entity.getDetail().contains("java.lang.AssertionError: assertion
failure"));
- Assertions.assertTrue(
- entity.getDetail().contains("Caused by:
java.lang.IllegalStateException: root cause"));
+ Assertions.assertEquals("", entity.getDetail());
+ }
+ }
+
+ /** Verifies backend authorization failures use the Lance forbidden
response. */
+ @Test
+ public void testBackendForbidden() {
+ assertAuthenticationError(new ForbiddenException("Access denied"), 403);
+ }
+
+ /** Verifies backend authentication failures use the Lance unauthenticated
response. */
+ @Test
+ public void testBackendUnauthorized() {
+ assertAuthenticationError(new UnauthorizedException("Invalid
credentials"), 401);
+ }
+
+ /** Verifies unexpected exceptions do not expose internal details in the
response. */
+ @Test
+ public void testInternalFailureDoesNotExposeException() {
+ try (Response response =
+ LanceExceptionMapper.toRESTResponse(
+ "catalog.schema.table", new
RuntimeException("private-backend-detail"))) {
+ Assertions.assertEquals(500, response.getStatus());
+ ErrorResponse error = (ErrorResponse) response.getEntity();
+ Assertions.assertEquals("Internal server error", error.getError());
+ Assertions.assertEquals("", error.getDetail());
+ }
+ }
+
+ /** Verifies intentional protocol validation details remain available to
callers. */
+ @Test
+ public void testProtocolValidationDetailsArePreserved() {
+ try (Response response =
+ LanceExceptionMapper.toRESTResponse(
+ "table",
+ new InvalidInputException("Invalid field", "field must be
positive", "table"))) {
+ Assertions.assertEquals(400, response.getStatus());
+ Assertions.assertEquals(
+ "field must be positive", ((ErrorResponse)
response.getEntity()).getDetail());
+ }
+ }
+
+ private void assertAuthenticationError(Exception exception, int status) {
+ try (Response response = LanceExceptionMapper.toRESTResponse("catalog",
exception)) {
+ Assertions.assertEquals(status, response.getStatus());
+ ErrorResponse error = (ErrorResponse) response.getEntity();
+ Assertions.assertEquals(exception.getMessage(), error.getError());
+ Assertions.assertEquals("", error.getDetail());
+ Assertions.assertEquals("catalog", error.getInstance());
}
}
}
diff --git
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceNamespaceOperations.java
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceNamespaceOperations.java
index a80c70a06b..87069562bd 100644
---
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceNamespaceOperations.java
+++
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceNamespaceOperations.java
@@ -208,10 +208,9 @@ public class TestLanceNamespaceOperations extends
JerseyTest {
ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
Assertions.assertEquals(18, errorResp.getCode());
- Assertions.assertEquals("Test exception", errorResp.getError());
+ Assertions.assertEquals("Internal server error", errorResp.getError());
+ Assertions.assertEquals("", errorResp.getDetail());
Assertions.assertEquals("ns1.ns2", errorResp.getInstance());
- Assertions.assertNotNull(errorResp.getDetail());
- Assertions.assertTrue(errorResp.getDetail().contains("Test exception"));
// root endpoint should use explicit root identifier instead of delimiter
in error instance
resp =
@@ -262,7 +261,8 @@ public class TestLanceNamespaceOperations extends
JerseyTest {
ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
Assertions.assertEquals(18, errorResp.getCode());
- Assertions.assertEquals("Test exception", errorResp.getError());
+ Assertions.assertEquals("Internal server error", errorResp.getError());
+ Assertions.assertEquals("", errorResp.getDetail());
}
@Test
@@ -321,7 +321,8 @@ public class TestLanceNamespaceOperations extends
JerseyTest {
ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
Assertions.assertEquals(18, errorResp.getCode());
- Assertions.assertEquals("Test exception", errorResp.getError());
+ Assertions.assertEquals("Internal server error", errorResp.getError());
+ Assertions.assertEquals("", errorResp.getDetail());
}
@Test
@@ -393,7 +394,8 @@ public class TestLanceNamespaceOperations extends
JerseyTest {
ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
Assertions.assertEquals(18, errorResp.getCode());
- Assertions.assertEquals("Test exception", errorResp.getError());
+ Assertions.assertEquals("Internal server error", errorResp.getError());
+ Assertions.assertEquals("", errorResp.getDetail());
}
@Test
@@ -457,7 +459,8 @@ public class TestLanceNamespaceOperations extends
JerseyTest {
Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp.getStatus());
Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
- Assertions.assertEquals("Runtime exception", errorResp.getError());
+ Assertions.assertEquals("Internal server error", errorResp.getError());
+ Assertions.assertEquals("", errorResp.getDetail());
}
@Test
@@ -513,7 +516,8 @@ public class TestLanceNamespaceOperations extends
JerseyTest {
Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp.getStatus());
Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
- Assertions.assertEquals("Runtime exception", errorResp.getError());
+ Assertions.assertEquals("Internal server error", errorResp.getError());
+ Assertions.assertEquals("", errorResp.getDetail());
}
@Test
@@ -620,7 +624,8 @@ public class TestLanceNamespaceOperations extends
JerseyTest {
Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp.getStatus());
Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
- Assertions.assertEquals("Runtime exception", errorResp.getError());
+ Assertions.assertEquals("Internal server error", errorResp.getError());
+ Assertions.assertEquals("", errorResp.getDetail());
}
@Test
@@ -677,7 +682,8 @@ public class TestLanceNamespaceOperations extends
JerseyTest {
Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp.getStatus());
Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
- Assertions.assertEquals("Runtime exception", errorResp.getError());
+ Assertions.assertEquals("Internal server error", errorResp.getError());
+ Assertions.assertEquals("", errorResp.getDetail());
}
@Test
@@ -999,6 +1005,7 @@ public class TestLanceNamespaceOperations extends
JerseyTest {
Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp.getStatus());
Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
ErrorResponse errorResp = resp.readEntity(ErrorResponse.class);
- Assertions.assertEquals("Runtime exception", errorResp.getError());
+ Assertions.assertEquals("Internal server error", errorResp.getError());
+ Assertions.assertEquals("", errorResp.getDetail());
}
}