Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion docs/lance-rest-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |

Expand Down Expand Up @@ -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
Expand Down
172 changes: 154 additions & 18 deletions docs/lance-rest-service.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions lance/lance-rest-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
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;
Expand Down Expand Up @@ -68,7 +70,13 @@ public Response toResponse(Throwable ex) {
}

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) {
Expand All @@ -86,7 +94,7 @@ private static LanceNamespaceException toLanceNamespaceException(String instance

} else {
LOG.warn("Lance REST server unexpected exception:", ex);
return new InternalException(ex.getMessage(), getStackTrace(ex), instance);
return new InternalException("Internal server error", "", instance);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -188,9 +186,7 @@ protected Object toErrorResponse(Method method, Object[] args, Throwable throwab
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Loading
Loading