Copilot commented on code in PR #14539:
URL: https://github.com/apache/gravitino/pull/14539#discussion_r4442231137


##########
design-docs/gravitino-trusted-proxy.md:
##########
@@ -0,0 +1,401 @@
+<!--
+  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.
+-->
+
+# Design: Trusted Proxy Users for Apache Gravitino
+
+---
+
+## Background
+
+Gravitino authenticates each REST request with one of the authenticators in 
`gravitino.authenticators` (`simple`, `basic`, `oauth`, `kerberos`), and 
authorization, ownership and audit use the authenticated principal. This breaks 
when a service sits between the end user and Gravitino and the end user holds 
no credential that Gravitino can verify:
+
+| Deployment | Who authenticates the end user | What Gravitino sees today |
+|------------|--------------------------------|---------------------------|
+| Trino serving BI, JDBC and ODBC clients | Trino | With `authType=simple` and 
`gravitino.client.session.forwardUser=true`, an unverified session user name. 
With `oauth2`, the user's token if the session has one, otherwise the 
connector's identity. With `basic` or `kerberos`, only the connector's 
identity, because a catalog that sets `forwardUser=true` fails to load 
([#13472](https://github.com/apache/gravitino/issues/13472)). |
+| Standalone Lance REST server | Lance REST | One configured service identity, 
so the service account owns every table Lance REST creates and audit never 
records the caller 
([#13090](https://github.com/apache/gravitino/issues/13090)). |
+
+Knox in front of the Gravitino UI has the same problem 
([#8876](https://github.com/apache/gravitino/issues/8876)); see Non-Goal 7.
+
+Hadoop (`hadoop.proxyuser.*`), HiveServer2 and Kyuubi let an authenticated 
service act for a user that server-side rules allow. Gravitino has no such 
mechanism, and `SimpleAuthenticator` accepts any user name the client sends.
+
+---
+
+## Goals
+
+1. **Trusted proxy**: A request carrying `X-Gravitino-Proxy-User: alice` runs 
as `alice` for authorization, ownership and audit if and only if proxying is 
enabled, its proxy headers pass the checks in [Headers](#headers), and one 
proxy rule matches the authenticated identity and allows `alice`, every 
declared group and the TCP peer address. The request must be authenticated by 
Kerberos, by OAuth 2 with `JwksTokenValidator` or `StaticSignKeyValidator`, by 
Basic, or by a custom authenticator that supports proxying. A request that 
fails authentication is rejected as today; among authenticated requests, a 
malformed proxy header gets 400 and any other failed condition gets 403.
+2. **Real user in audit**: For every proxied request, post events and audit 
logs in the `SimpleAuditLogV2` and JSON formats record the authenticated 
identity as `realUser`.
+3. **Per-user authorization behind Trino**: With 
`gravitino.client.session.forwardUser=true`, the Trino connector's Gravitino 
calls run as the session user under `authType=basic` and `kerberos`, and under 
`oauth2` for sessions without a user token when 
`gravitino.client.session.forwardUser.useProxy=true`.
+4. **No silent fallback**: A client configured to proxy a user fails at 
`build()` when the server does not apply the proxy header, and rejects any 
later successful response that the server did not run as the proxied user.
+5. **Per-caller identity behind Lance REST**: With 
`gravitino.lance-rest.gravitino-proxy-caller=true`, objects that a caller 
creates through the standalone Lance REST server are owned and audited as that 
caller.
+6. **Off by default**: Without the new settings, requests without the new 
headers behave as today, apart from the [fileset fix](#fileset-fix), and 
requests with a proxy header are rejected.
+
+---
+
+## Non-Goals
+
+1. **A Trino `SystemAccessControl` plugin or an `/authorize` decision API**: 
Running the connector's own calls as the end user already gives per-user 
decisions. A decision API can be added later; see [Solution 
Investigations](#solution-investigations).
+2. **Server-side group resolution**: Gravitino does not look up a proxied 
user's groups. The proxy declares them, limited to its rule's `groups`. A group 
mapping SPI belongs with LDAP support 
([#6006](https://github.com/apache/gravitino/issues/6006)).
+3. **Credentials for processes that run user code**: User code in such a 
process, for example a Spark engine that Kyuubi launches, could read a proxy's 
credential and act for every user the proxy's rule allows. Such processes need 
a per-user credential, which is out of scope.
+4. **Per-user identity on Iceberg REST paths that bypass the Gravitino client 
or forward the caller's credential**: Trino catalogs routed to Gravitino's 
Iceberg REST service use Trino's native Iceberg REST client, which never sends 
the Gravitino client's headers, so for proxied sessions they keep the identity 
configured for that route. Iceberg catalogs with a `rest` backend forward the 
caller's own `Authorization` header through 
`UserPrincipalForwardingAuthManager`; proxied principals carry none, so their 
requests to such catalogs fail.
+5. **Python client**: No deployment in [Background](#background) uses the 
Python client. A Python `with_proxy_user` needs no server change and can follow 
separately.
+6. **Replacing token exchange**: Sites whose IdP supports RFC 8693 token 
exchange can keep using it 
([#13092](https://github.com/apache/gravitino/issues/13092)); Gravitino sees an 
exchanged token as an ordinary OAuth token.
+7. **Knox and the MCP server**: Knox passes the end user in a `doAs` query 
parameter. Using this mechanism needs either a Knox dispatch that sends 
`X-Gravitino-Proxy-User` instead, or Gravitino accepting `doAs` under the same 
proxy rules; [#8877](https://github.com/apache/gravitino/pull/8877) proposes 
accepting `doAs` from any Kerberos caller. That choice is left to a follow-up. 
The MCP server forwards the `Authorization` header of each HTTP request to 
Gravitino, by default uses its own configured identity when there is none, and 
does not authenticate its callers, so a proxy mode would first need caller 
authentication in the MCP server.
+
+---
+
+## Solution Investigations
+
+| Approach | Pros | Cons | Decision |
+|----------|------|------|----------|
+| Decision API `POST /api/metalakes/{metalake}/authorize` plus a Trino 
`SystemAccessControl` plugin (the sketch in #11307) | Trino enforces before 
planning and covers every Trino check hook | Answers allow or deny only. The 
connector's metadata calls still run as the service account, so owners and 
audit stay wrong. Each engine needs its own plugin. Does nothing for Lance 
REST. | Rejected |
+| RFC 8693 token exchange at the IdP | Standard; Gravitino sees an ordinary 
OAuth token and needs no proxy rules | Requires an IdP that supports the 
exchange grant; Kerberos-only sites have none | Compatible, not the base 
mechanism |
+| Replay the end user's credential (Trino `oauth2` forwarding; 
[#12984](https://github.com/apache/gravitino/pull/12984) for Lance REST) | No 
new trust configuration | Trino's SPI does not pass passwords to connectors 
([#13473](https://github.com/apache/gravitino/pull/13473)). A Kerberos ticket 
cannot be replayed to another service. The relay holds the user's credential. | 
Rejected |
+| Trust `simple` user names from a trusted network | Works today | Nothing is 
authenticated; any client can claim any name | Rejected |
+| Trusted proxy header checked against server rules | Same model as Hadoop 
proxy users, which Trino, Kyuubi and HiveServer2 operators already run. Works 
with any strong authenticator. One server change serves every such deployment. 
| New trust configuration to manage | **Chosen** |
+
+---
+
+## Proposal
+
+### Overview
+
+```
+ BI client ──► Trino ──(Kerberos as trino/[email protected] + 
X-Gravitino-Proxy-User: alice)──► Gravitino
+ caller ────► Lance REST ──(OAuth as lance-rest + X-Gravitino-Proxy-User: 
bob)─────────────────► Gravitino
+```
+
+### Headers
+
+| Header | Direction | Value | Notes |
+|--------|-----------|-------|-------|
+| `X-Gravitino-Proxy-User` | Request | One user name | The effective user of 
the request |
+| `X-Gravitino-Proxy-Groups` | Request | Comma-separated group names | Valid 
only with `X-Gravitino-Proxy-User` |
+| `X-Gravitino-Effective-User` | Response | One user name | Set on every 
response to a request whose proxy header the server applied |
+
+Names are UTF-8 and percent-encoded as in RFC 3986. The server encodes 
`X-Gravitino-Effective-User` the same way, and the client decodes it strictly 
before comparing. The server splits `X-Gravitino-Proxy-Groups` on raw commas 
before decoding, so a comma inside a group name is sent as `%2C`.
+
+The server rejects with 400 a repeated proxy header, a name with an invalid 
percent escape or invalid UTF-8 (`+` is a literal `+`), and any decoded name 
that:
+
+- is empty or has a leading or trailing space;
+- contains a space separator (category Zs) other than U+0020, a character of 
category Cc, Cf, Zl, Zp, Co, Cs or Cn, or a `Default_Ignorable_Code_Point` 
character such as a zero-width space or a variation selector;
+- changes under NFKC normalization, which covers non-NFC forms and 
compatibility characters such as full-width letters and the long s `ſ`.
+
+Every name the server compares is therefore in one form and has no invisible 
characters. Names may contain `/` and `@`, as e-mail addresses and Kerberos 
principals do.
+
+### Configuration
+
+Key names follow the existing `gravitino.authenticator.oauth.*` style. `<id>` 
is a rule name chosen by the administrator.
+
+| Key | Default | Description |
+|-----|---------|-------------|
+| `gravitino.authenticator.proxy.enable` | `false` | Whether the server 
accepts proxy headers. When `false`, a proxy header gets 403, or 400 when 
malformed. |
+| `gravitino.authenticator.proxy.rules` | empty | Comma-separated rule names |
+| `gravitino.authenticator.proxy.rule.<id>.authenticator` | none, required | 
The authenticator that must have accepted the request: `kerberos`, `oauth`, 
`basic`, or a custom authenticator's class name as written in 
`gravitino.authenticators` |
+| `gravitino.authenticator.proxy.rule.<id>.issuer` | none, required for 
`oauth` | The `iss` claim the proxy's token must carry |
+| `gravitino.authenticator.proxy.rule.<id>.principals` | none, required | 
Comma-separated proxy identities the rule applies to (see below) |
+| `gravitino.authenticator.proxy.rule.<id>.users` | none, required | 
Comma-separated users the rule allows, or `*` |
+| `gravitino.authenticator.proxy.rule.<id>.excludeUsers` | empty | 
Comma-separated users the rule never allows, even when `users` names them |
+| `gravitino.authenticator.proxy.rule.<id>.groups` | empty | Comma-separated 
groups the proxy may declare, or `*`. When empty, `X-Gravitino-Proxy-Groups` is 
rejected. |
+| `gravitino.authenticator.proxy.rule.<id>.hosts` | `*` | Comma-separated IP 
addresses or CIDR ranges the TCP peer address must be in, or `*` |
+
+A rule matches when the authenticator that accepted the request is the rule's 
`authenticator` and the proxy identity is in `principals`. The proxy identity 
is the name the authenticator verified, before principal mapping:
+
+| Authenticator | Proxy identity | Accepted forms in `principals` | Example |
+|---------------|----------------|--------------------------------|---------|
+| `kerberos` | The full principal, with instance and realm | `primary@REALM`, 
`primary/instance@REALM`, or `primary/*@REALM`, where `*` matches one non-empty 
instance; primary and realm are literal | `trino/*@EXAMPLE.COM` |
+| `oauth` | The token's `sub`, only when its `iss` equals the rule's `issuer` 
| Literal values without `*` | `lance-rest` |
+| `basic` | The user name | Literal values without `*` | `etl-service` |
+| A custom authenticator, by class name | The proxy identity the authenticator 
sets on its `UserPrincipal` | Literal values without `*` | `svc-gateway` |
+
+The OAuth proxy identity is always `sub`, whatever 
`gravitino.authenticator.oauth.principalFields` selects, because users can 
choose claims such as `preferred_username` in many IdPs.
+
+Rule semantics:
+
+- One rule must allow everything in a request: the authenticator, the 
identity, the user, every declared group and the peer address. Rules do not 
combine.
+- `*` in `users` matches no user in `gravitino.authorization.serviceAdmins`, 
so a service admin can be proxied only when named in `users`. `anonymous` 
cannot be proxied.
+- `hosts` is compared with `HttpServletRequest.getRemoteAddr()`, never with 
`X-Forwarded-For`. Behind a load balancer the peer is the balancer, so `hosts` 
stays `*` there.
+- The standalone Iceberg REST and Lance REST servers read the same keys, and 
`gravitino.authorization.serviceAdmins`, from their own configuration files.
+- Rules are read at startup, so a change takes effect on a server, standalone 
ones included, only after that server restarts.
+- The server fails to start when a listed rule lacks a required key, when 
`authenticator` is a built-in authenticator's class name instead of its short 
name, when a `principals` entry does not have an accepted form, when a `users`, 
`excludeUsers` or `groups` entry fails the decoded-name checks in 
[Headers](#headers), or when a `hosts` entry is not `*`, an IP address or a 
CIDR range. A rule whose `authenticator` is any other name not registered on a 
server loads there and matches no request.
+
+The names in `excludeUsers` and `gravitino.authorization.serviceAdmins`, and 
`anonymous`, match a proxied name by `String.equalsIgnoreCase` against both the 
full name and its part before the first `/` or `@`. So `excludeUsers=hdfs` also 
excludes `HDFS`, `[email protected]` and `hdfs/[email protected]`, and `hive` 
also excludes `hıve` with a dotless `ı`. Exclusions ignore case because Hadoop 
group mapping often does. Entries in `users` and `groups` match only the exact 
name.
+
+### Changes to `Authenticator` and `AuthenticationFilter`
+
+`Authenticator` gains a method that marks the authenticators whose principals 
may act as a proxy. `KerberosAuthenticator`, `OAuth2TokenAuthenticator` and the 
idp-basic `BasicAuthenticator` return `true`; `SimpleAuthenticator` and 
existing custom authenticators keep the default.
+
+```java
+/**
+ * Returns whether a principal accepted by this authenticator may act as a 
trusted proxy.
+ *
+ * @return true if requests accepted by this authenticator may carry a proxy 
header
+ */
+default boolean supportsProxy() {
+  return false;
+}
+```
+
+An authenticator that supports proxying sets the proxy identity on the 
`UserPrincipal` it returns. Principal mapping happens inside 
`KerberosAuthenticator` and the OAuth validators (`JwksTokenValidator`, 
`StaticSignKeyValidator`), so they set it, and the validators also set the 
token's issuer. `OAuth2TokenAuthenticator` keeps both only when the configured 
validator's class is exactly one of those two, and drops them for any other 
`tokenValidatorClass`, including a subclass. A principal without a proxy 
identity cannot proxy.
+
+`AuthenticatorFactory` names each authenticator after its entry in 
`gravitino.authenticators`, with built-in class names normalized to `kerberos`, 
`oauth`, `basic` and `simple`, so an authenticator cannot claim another's name. 
`ServerAuthenticator` keeps an ordered name-to-instance map. For a duplicate 
name the factory keeps the first entry and logs a warning, which matches 
today's first-match behavior. The `BasicAuthenticator` of `plugins/idp-basic` 
is named `basic`, including the one that 
`IdpRESTFeature.registerBasicAuthenticator` adds. `resolvePrincipal` stores the 
name of the accepting authenticator as a request attribute and clears any real 
user on the principal, so only `applyProxyUser` sets it.
+
+`authenticate` gains a step between principal resolution and active roles:
+
+```java
+private Principal authenticate(HttpServletRequest request, HttpServletResponse 
response) {
+  return applyActiveRoles(request, applyProxyUser(request, response, 
resolvePrincipal(request)));
+}

Review Comment:
   If a valid proxy request carries malformed active roles, such as 
`ALL,analyst`, `applyProxyUser` succeeds but `applyActiveRoles` throws before 
`runAsPrincipal` publishes the authenticated-principal request attribute. 
`HttpAuditFilter` then records an unknown user and cannot obtain `realUser`, 
contrary to the audit guarantee for proxied requests. Publish the accepted 
proxied principal before parsing active roles; `runAsPrincipal` can still 
replace it with the finalized principal before invoking the downstream chain.



##########
design-docs/gravitino-trusted-proxy.md:
##########
@@ -0,0 +1,401 @@
+<!--
+  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.
+-->
+
+# Design: Trusted Proxy Users for Apache Gravitino
+
+---
+
+## Background
+
+Gravitino authenticates each REST request with one of the authenticators in 
`gravitino.authenticators` (`simple`, `basic`, `oauth`, `kerberos`), and 
authorization, ownership and audit use the authenticated principal. This breaks 
when a service sits between the end user and Gravitino and the end user holds 
no credential that Gravitino can verify:
+
+| Deployment | Who authenticates the end user | What Gravitino sees today |
+|------------|--------------------------------|---------------------------|
+| Trino serving BI, JDBC and ODBC clients | Trino | With `authType=simple` and 
`gravitino.client.session.forwardUser=true`, an unverified session user name. 
With `oauth2`, the user's token if the session has one, otherwise the 
connector's identity. With `basic` or `kerberos`, only the connector's 
identity, because a catalog that sets `forwardUser=true` fails to load 
([#13472](https://github.com/apache/gravitino/issues/13472)). |
+| Standalone Lance REST server | Lance REST | One configured service identity, 
so the service account owns every table Lance REST creates and audit never 
records the caller 
([#13090](https://github.com/apache/gravitino/issues/13090)). |
+
+Knox in front of the Gravitino UI has the same problem 
([#8876](https://github.com/apache/gravitino/issues/8876)); see Non-Goal 7.
+
+Hadoop (`hadoop.proxyuser.*`), HiveServer2 and Kyuubi let an authenticated 
service act for a user that server-side rules allow. Gravitino has no such 
mechanism, and `SimpleAuthenticator` accepts any user name the client sends.
+
+---
+
+## Goals
+
+1. **Trusted proxy**: A request carrying `X-Gravitino-Proxy-User: alice` runs 
as `alice` for authorization, ownership and audit if and only if proxying is 
enabled, its proxy headers pass the checks in [Headers](#headers), and one 
proxy rule matches the authenticated identity and allows `alice`, every 
declared group and the TCP peer address. The request must be authenticated by 
Kerberos, by OAuth 2 with `JwksTokenValidator` or `StaticSignKeyValidator`, by 
Basic, or by a custom authenticator that supports proxying. A request that 
fails authentication is rejected as today; among authenticated requests, a 
malformed proxy header gets 400 and any other failed condition gets 403.
+2. **Real user in audit**: For every proxied request, post events and audit 
logs in the `SimpleAuditLogV2` and JSON formats record the authenticated 
identity as `realUser`.
+3. **Per-user authorization behind Trino**: With 
`gravitino.client.session.forwardUser=true`, the Trino connector's Gravitino 
calls run as the session user under `authType=basic` and `kerberos`, and under 
`oauth2` for sessions without a user token when 
`gravitino.client.session.forwardUser.useProxy=true`.
+4. **No silent fallback**: A client configured to proxy a user fails at 
`build()` when the server does not apply the proxy header, and rejects any 
later successful response that the server did not run as the proxied user.
+5. **Per-caller identity behind Lance REST**: With 
`gravitino.lance-rest.gravitino-proxy-caller=true`, objects that a caller 
creates through the standalone Lance REST server are owned and audited as that 
caller.
+6. **Off by default**: Without the new settings, requests without the new 
headers behave as today, apart from the [fileset fix](#fileset-fix), and 
requests with a proxy header are rejected.
+
+---
+
+## Non-Goals
+
+1. **A Trino `SystemAccessControl` plugin or an `/authorize` decision API**: 
Running the connector's own calls as the end user already gives per-user 
decisions. A decision API can be added later; see [Solution 
Investigations](#solution-investigations).
+2. **Server-side group resolution**: Gravitino does not look up a proxied 
user's groups. The proxy declares them, limited to its rule's `groups`. A group 
mapping SPI belongs with LDAP support 
([#6006](https://github.com/apache/gravitino/issues/6006)).
+3. **Credentials for processes that run user code**: User code in such a 
process, for example a Spark engine that Kyuubi launches, could read a proxy's 
credential and act for every user the proxy's rule allows. Such processes need 
a per-user credential, which is out of scope.
+4. **Per-user identity on Iceberg REST paths that bypass the Gravitino client 
or forward the caller's credential**: Trino catalogs routed to Gravitino's 
Iceberg REST service use Trino's native Iceberg REST client, which never sends 
the Gravitino client's headers, so for proxied sessions they keep the identity 
configured for that route. Iceberg catalogs with a `rest` backend forward the 
caller's own `Authorization` header through 
`UserPrincipalForwardingAuthManager`; proxied principals carry none, so their 
requests to such catalogs fail.
+5. **Python client**: No deployment in [Background](#background) uses the 
Python client. A Python `with_proxy_user` needs no server change and can follow 
separately.
+6. **Replacing token exchange**: Sites whose IdP supports RFC 8693 token 
exchange can keep using it 
([#13092](https://github.com/apache/gravitino/issues/13092)); Gravitino sees an 
exchanged token as an ordinary OAuth token.
+7. **Knox and the MCP server**: Knox passes the end user in a `doAs` query 
parameter. Using this mechanism needs either a Knox dispatch that sends 
`X-Gravitino-Proxy-User` instead, or Gravitino accepting `doAs` under the same 
proxy rules; [#8877](https://github.com/apache/gravitino/pull/8877) proposes 
accepting `doAs` from any Kerberos caller. That choice is left to a follow-up. 
The MCP server forwards the `Authorization` header of each HTTP request to 
Gravitino, by default uses its own configured identity when there is none, and 
does not authenticate its callers, so a proxy mode would first need caller 
authentication in the MCP server.
+
+---
+
+## Solution Investigations
+
+| Approach | Pros | Cons | Decision |
+|----------|------|------|----------|
+| Decision API `POST /api/metalakes/{metalake}/authorize` plus a Trino 
`SystemAccessControl` plugin (the sketch in #11307) | Trino enforces before 
planning and covers every Trino check hook | Answers allow or deny only. The 
connector's metadata calls still run as the service account, so owners and 
audit stay wrong. Each engine needs its own plugin. Does nothing for Lance 
REST. | Rejected |
+| RFC 8693 token exchange at the IdP | Standard; Gravitino sees an ordinary 
OAuth token and needs no proxy rules | Requires an IdP that supports the 
exchange grant; Kerberos-only sites have none | Compatible, not the base 
mechanism |
+| Replay the end user's credential (Trino `oauth2` forwarding; 
[#12984](https://github.com/apache/gravitino/pull/12984) for Lance REST) | No 
new trust configuration | Trino's SPI does not pass passwords to connectors 
([#13473](https://github.com/apache/gravitino/pull/13473)). A Kerberos ticket 
cannot be replayed to another service. The relay holds the user's credential. | 
Rejected |
+| Trust `simple` user names from a trusted network | Works today | Nothing is 
authenticated; any client can claim any name | Rejected |
+| Trusted proxy header checked against server rules | Same model as Hadoop 
proxy users, which Trino, Kyuubi and HiveServer2 operators already run. Works 
with any strong authenticator. One server change serves every such deployment. 
| New trust configuration to manage | **Chosen** |
+
+---
+
+## Proposal
+
+### Overview
+
+```
+ BI client ──► Trino ──(Kerberos as trino/[email protected] + 
X-Gravitino-Proxy-User: alice)──► Gravitino
+ caller ────► Lance REST ──(OAuth as lance-rest + X-Gravitino-Proxy-User: 
bob)─────────────────► Gravitino
+```
+
+### Headers
+
+| Header | Direction | Value | Notes |
+|--------|-----------|-------|-------|
+| `X-Gravitino-Proxy-User` | Request | One user name | The effective user of 
the request |
+| `X-Gravitino-Proxy-Groups` | Request | Comma-separated group names | Valid 
only with `X-Gravitino-Proxy-User` |
+| `X-Gravitino-Effective-User` | Response | One user name | Set on every 
response to a request whose proxy header the server applied |
+
+Names are UTF-8 and percent-encoded as in RFC 3986. The server encodes 
`X-Gravitino-Effective-User` the same way, and the client decodes it strictly 
before comparing. The server splits `X-Gravitino-Proxy-Groups` on raw commas 
before decoding, so a comma inside a group name is sent as `%2C`.
+
+The server rejects with 400 a repeated proxy header, a name with an invalid 
percent escape or invalid UTF-8 (`+` is a literal `+`), and any decoded name 
that:
+
+- is empty or has a leading or trailing space;
+- contains a space separator (category Zs) other than U+0020, a character of 
category Cc, Cf, Zl, Zp, Co, Cs or Cn, or a `Default_Ignorable_Code_Point` 
character such as a zero-width space or a variation selector;
+- changes under NFKC normalization, which covers non-NFC forms and 
compatibility characters such as full-width letters and the long s `ſ`.
+
+Every name the server compares is therefore in one form and has no invisible 
characters. Names may contain `/` and `@`, as e-mail addresses and Kerberos 
principals do.
+
+### Configuration
+
+Key names follow the existing `gravitino.authenticator.oauth.*` style. `<id>` 
is a rule name chosen by the administrator.
+
+| Key | Default | Description |
+|-----|---------|-------------|
+| `gravitino.authenticator.proxy.enable` | `false` | Whether the server 
accepts proxy headers. When `false`, a proxy header gets 403, or 400 when 
malformed. |
+| `gravitino.authenticator.proxy.rules` | empty | Comma-separated rule names |
+| `gravitino.authenticator.proxy.rule.<id>.authenticator` | none, required | 
The authenticator that must have accepted the request: `kerberos`, `oauth`, 
`basic`, or a custom authenticator's class name as written in 
`gravitino.authenticators` |
+| `gravitino.authenticator.proxy.rule.<id>.issuer` | none, required for 
`oauth` | The `iss` claim the proxy's token must carry |
+| `gravitino.authenticator.proxy.rule.<id>.principals` | none, required | 
Comma-separated proxy identities the rule applies to (see below) |
+| `gravitino.authenticator.proxy.rule.<id>.users` | none, required | 
Comma-separated users the rule allows, or `*` |
+| `gravitino.authenticator.proxy.rule.<id>.excludeUsers` | empty | 
Comma-separated users the rule never allows, even when `users` names them |
+| `gravitino.authenticator.proxy.rule.<id>.groups` | empty | Comma-separated 
groups the proxy may declare, or `*`. When empty, `X-Gravitino-Proxy-Groups` is 
rejected. |
+| `gravitino.authenticator.proxy.rule.<id>.hosts` | `*` | Comma-separated IP 
addresses or CIDR ranges the TCP peer address must be in, or `*` |
+
+A rule matches when the authenticator that accepted the request is the rule's 
`authenticator` and the proxy identity is in `principals`. The proxy identity 
is the name the authenticator verified, before principal mapping:
+
+| Authenticator | Proxy identity | Accepted forms in `principals` | Example |
+|---------------|----------------|--------------------------------|---------|
+| `kerberos` | The full principal, with instance and realm | `primary@REALM`, 
`primary/instance@REALM`, or `primary/*@REALM`, where `*` matches one non-empty 
instance; primary and realm are literal | `trino/*@EXAMPLE.COM` |
+| `oauth` | The token's `sub`, only when its `iss` equals the rule's `issuer` 
| Literal values without `*` | `lance-rest` |
+| `basic` | The user name | Literal values without `*` | `etl-service` |
+| A custom authenticator, by class name | The proxy identity the authenticator 
sets on its `UserPrincipal` | Literal values without `*` | `svc-gateway` |
+
+The OAuth proxy identity is always `sub`, whatever 
`gravitino.authenticator.oauth.principalFields` selects, because users can 
choose claims such as `preferred_username` in many IdPs.
+
+Rule semantics:
+
+- One rule must allow everything in a request: the authenticator, the 
identity, the user, every declared group and the peer address. Rules do not 
combine.
+- `*` in `users` matches no user in `gravitino.authorization.serviceAdmins`, 
so a service admin can be proxied only when named in `users`. `anonymous` 
cannot be proxied.
+- `hosts` is compared with `HttpServletRequest.getRemoteAddr()`, never with 
`X-Forwarded-For`. Behind a load balancer the peer is the balancer, so `hosts` 
stays `*` there.
+- The standalone Iceberg REST and Lance REST servers read the same keys, and 
`gravitino.authorization.serviceAdmins`, from their own configuration files.
+- Rules are read at startup, so a change takes effect on a server, standalone 
ones included, only after that server restarts.
+- The server fails to start when a listed rule lacks a required key, when 
`authenticator` is a built-in authenticator's class name instead of its short 
name, when a `principals` entry does not have an accepted form, when a `users`, 
`excludeUsers` or `groups` entry fails the decoded-name checks in 
[Headers](#headers), or when a `hosts` entry is not `*`, an IP address or a 
CIDR range. A rule whose `authenticator` is any other name not registered on a 
server loads there and matches no request.
+
+The names in `excludeUsers` and `gravitino.authorization.serviceAdmins`, and 
`anonymous`, match a proxied name by `String.equalsIgnoreCase` against both the 
full name and its part before the first `/` or `@`. So `excludeUsers=hdfs` also 
excludes `HDFS`, `[email protected]` and `hdfs/[email protected]`, and `hive` 
also excludes `hıve` with a dotless `ı`. Exclusions ignore case because Hadoop 
group mapping often does. Entries in `users` and `groups` match only the exact 
name.
+
+### Changes to `Authenticator` and `AuthenticationFilter`
+
+`Authenticator` gains a method that marks the authenticators whose principals 
may act as a proxy. `KerberosAuthenticator`, `OAuth2TokenAuthenticator` and the 
idp-basic `BasicAuthenticator` return `true`; `SimpleAuthenticator` and 
existing custom authenticators keep the default.
+
+```java
+/**
+ * Returns whether a principal accepted by this authenticator may act as a 
trusted proxy.
+ *
+ * @return true if requests accepted by this authenticator may carry a proxy 
header
+ */
+default boolean supportsProxy() {
+  return false;
+}
+```
+
+An authenticator that supports proxying sets the proxy identity on the 
`UserPrincipal` it returns. Principal mapping happens inside 
`KerberosAuthenticator` and the OAuth validators (`JwksTokenValidator`, 
`StaticSignKeyValidator`), so they set it, and the validators also set the 
token's issuer. `OAuth2TokenAuthenticator` keeps both only when the configured 
validator's class is exactly one of those two, and drops them for any other 
`tokenValidatorClass`, including a subclass. A principal without a proxy 
identity cannot proxy.
+
+`AuthenticatorFactory` names each authenticator after its entry in 
`gravitino.authenticators`, with built-in class names normalized to `kerberos`, 
`oauth`, `basic` and `simple`, so an authenticator cannot claim another's name. 
`ServerAuthenticator` keeps an ordered name-to-instance map. For a duplicate 
name the factory keeps the first entry and logs a warning, which matches 
today's first-match behavior. The `BasicAuthenticator` of `plugins/idp-basic` 
is named `basic`, including the one that 
`IdpRESTFeature.registerBasicAuthenticator` adds. `resolvePrincipal` stores the 
name of the accepting authenticator as a request attribute and clears any real 
user on the principal, so only `applyProxyUser` sets it.
+
+`authenticate` gains a step between principal resolution and active roles:
+
+```java
+private Principal authenticate(HttpServletRequest request, HttpServletResponse 
response) {
+  return applyActiveRoles(request, applyProxyUser(request, response, 
resolvePrincipal(request)));
+}
+```
+
+`applyProxyUser` does the following, in order:
+
+1. Neither proxy header is present: return the principal unchanged.
+2. A proxy header fails the checks in [Headers](#headers), or 
`X-Gravitino-Proxy-Groups` comes without `X-Gravitino-Proxy-User`: throw 
`IllegalProxyHeaderException` (400).
+3. Proxying is disabled, the accepting authenticator does not support 
proxying, or the principal has no proxy identity: throw `ForbiddenException` 
(403).
+4. The user is `anonymous`, or no single rule allows the authenticator, the 
identity, the user, the peer address and every declared group: throw 
`ForbiddenException` (403).
+5. Otherwise set `X-Gravitino-Effective-User` on the response and return a 
`UserPrincipal` for the proxied user with the declared groups, no access token, 
and the proxy identity as real user.
+
+`IllegalProxyHeaderException` extends `IllegalArgumentException`. 
`AuthenticationFilter.doFilter` catches it next to 
`IllegalActiveRolesException`, and `sendAuthErrorResponse` in 
`AuthenticationFilter` and `LanceAuthenticationFilter` maps it to 400. 
`IcebergAuthenticationFilter` already maps every `IllegalArgumentException` to 
400. All three map `ForbiddenException` to 403, and the Iceberg and Lance 
filters, which override only `sendAuthErrorResponse`, inherit the new step. 
Active roles are applied after it, so `X-Gravitino-Active-Roles` narrows the 
proxied user's roles.
+
+### Changes to `UserPrincipal`
+
+`UserPrincipal` gets three optional fields:
+
+| Accessor | Value |
+|----------|-------|
+| `getProxyIdentity()` | The name the authenticator verified, before principal 
mapping: the full Kerberos principal, the OAuth `sub`, the Basic user name, or 
what a custom authenticator sets. |
+| `getProxyIdentityIssuer()` | The `iss` of the caller's OAuth token, set by 
the OAuth validators. Only `oauth` rules read it. |
+| `getRealUser()` | The proxy identity of the service that sent 
`X-Gravitino-Proxy-User` for this user, for example `trino/[email protected]`. 
Audit and events record this form. |

Review Comment:
   Recording only the OAuth `sub` loses the issuer binding used to authenticate 
the proxy. Two rules with different issuers but the same `sub` produce 
indistinguishable `realUser` values when acting for the same user; different 
authenticator types can collide too. Preserve the accepting authenticator and, 
for OAuth, the issuer alongside the subject in the proxied principal and 
audit/events so the authenticated service can be identified unambiguously.



##########
design-docs/gravitino-trusted-proxy.md:
##########
@@ -0,0 +1,401 @@
+<!--
+  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.
+-->
+
+# Design: Trusted Proxy Users for Apache Gravitino
+
+---
+
+## Background
+
+Gravitino authenticates each REST request with one of the authenticators in 
`gravitino.authenticators` (`simple`, `basic`, `oauth`, `kerberos`), and 
authorization, ownership and audit use the authenticated principal. This breaks 
when a service sits between the end user and Gravitino and the end user holds 
no credential that Gravitino can verify:
+
+| Deployment | Who authenticates the end user | What Gravitino sees today |
+|------------|--------------------------------|---------------------------|
+| Trino serving BI, JDBC and ODBC clients | Trino | With `authType=simple` and 
`gravitino.client.session.forwardUser=true`, an unverified session user name. 
With `oauth2`, the user's token if the session has one, otherwise the 
connector's identity. With `basic` or `kerberos`, only the connector's 
identity, because a catalog that sets `forwardUser=true` fails to load 
([#13472](https://github.com/apache/gravitino/issues/13472)). |
+| Standalone Lance REST server | Lance REST | One configured service identity, 
so the service account owns every table Lance REST creates and audit never 
records the caller 
([#13090](https://github.com/apache/gravitino/issues/13090)). |
+
+Knox in front of the Gravitino UI has the same problem 
([#8876](https://github.com/apache/gravitino/issues/8876)); see Non-Goal 7.
+
+Hadoop (`hadoop.proxyuser.*`), HiveServer2 and Kyuubi let an authenticated 
service act for a user that server-side rules allow. Gravitino has no such 
mechanism, and `SimpleAuthenticator` accepts any user name the client sends.
+
+---
+
+## Goals
+
+1. **Trusted proxy**: A request carrying `X-Gravitino-Proxy-User: alice` runs 
as `alice` for authorization, ownership and audit if and only if proxying is 
enabled, its proxy headers pass the checks in [Headers](#headers), and one 
proxy rule matches the authenticated identity and allows `alice`, every 
declared group and the TCP peer address. The request must be authenticated by 
Kerberos, by OAuth 2 with `JwksTokenValidator` or `StaticSignKeyValidator`, by 
Basic, or by a custom authenticator that supports proxying. A request that 
fails authentication is rejected as today; among authenticated requests, a 
malformed proxy header gets 400 and any other failed condition gets 403.
+2. **Real user in audit**: For every proxied request, post events and audit 
logs in the `SimpleAuditLogV2` and JSON formats record the authenticated 
identity as `realUser`.
+3. **Per-user authorization behind Trino**: With 
`gravitino.client.session.forwardUser=true`, the Trino connector's Gravitino 
calls run as the session user under `authType=basic` and `kerberos`, and under 
`oauth2` for sessions without a user token when 
`gravitino.client.session.forwardUser.useProxy=true`.
+4. **No silent fallback**: A client configured to proxy a user fails at 
`build()` when the server does not apply the proxy header, and rejects any 
later successful response that the server did not run as the proxied user.
+5. **Per-caller identity behind Lance REST**: With 
`gravitino.lance-rest.gravitino-proxy-caller=true`, objects that a caller 
creates through the standalone Lance REST server are owned and audited as that 
caller.
+6. **Off by default**: Without the new settings, requests without the new 
headers behave as today, apart from the [fileset fix](#fileset-fix), and 
requests with a proxy header are rejected.
+
+---
+
+## Non-Goals
+
+1. **A Trino `SystemAccessControl` plugin or an `/authorize` decision API**: 
Running the connector's own calls as the end user already gives per-user 
decisions. A decision API can be added later; see [Solution 
Investigations](#solution-investigations).
+2. **Server-side group resolution**: Gravitino does not look up a proxied 
user's groups. The proxy declares them, limited to its rule's `groups`. A group 
mapping SPI belongs with LDAP support 
([#6006](https://github.com/apache/gravitino/issues/6006)).
+3. **Credentials for processes that run user code**: User code in such a 
process, for example a Spark engine that Kyuubi launches, could read a proxy's 
credential and act for every user the proxy's rule allows. Such processes need 
a per-user credential, which is out of scope.
+4. **Per-user identity on Iceberg REST paths that bypass the Gravitino client 
or forward the caller's credential**: Trino catalogs routed to Gravitino's 
Iceberg REST service use Trino's native Iceberg REST client, which never sends 
the Gravitino client's headers, so for proxied sessions they keep the identity 
configured for that route. Iceberg catalogs with a `rest` backend forward the 
caller's own `Authorization` header through 
`UserPrincipalForwardingAuthManager`; proxied principals carry none, so their 
requests to such catalogs fail.
+5. **Python client**: No deployment in [Background](#background) uses the 
Python client. A Python `with_proxy_user` needs no server change and can follow 
separately.
+6. **Replacing token exchange**: Sites whose IdP supports RFC 8693 token 
exchange can keep using it 
([#13092](https://github.com/apache/gravitino/issues/13092)); Gravitino sees an 
exchanged token as an ordinary OAuth token.
+7. **Knox and the MCP server**: Knox passes the end user in a `doAs` query 
parameter. Using this mechanism needs either a Knox dispatch that sends 
`X-Gravitino-Proxy-User` instead, or Gravitino accepting `doAs` under the same 
proxy rules; [#8877](https://github.com/apache/gravitino/pull/8877) proposes 
accepting `doAs` from any Kerberos caller. That choice is left to a follow-up. 
The MCP server forwards the `Authorization` header of each HTTP request to 
Gravitino, by default uses its own configured identity when there is none, and 
does not authenticate its callers, so a proxy mode would first need caller 
authentication in the MCP server.
+
+---
+
+## Solution Investigations
+
+| Approach | Pros | Cons | Decision |
+|----------|------|------|----------|
+| Decision API `POST /api/metalakes/{metalake}/authorize` plus a Trino 
`SystemAccessControl` plugin (the sketch in #11307) | Trino enforces before 
planning and covers every Trino check hook | Answers allow or deny only. The 
connector's metadata calls still run as the service account, so owners and 
audit stay wrong. Each engine needs its own plugin. Does nothing for Lance 
REST. | Rejected |
+| RFC 8693 token exchange at the IdP | Standard; Gravitino sees an ordinary 
OAuth token and needs no proxy rules | Requires an IdP that supports the 
exchange grant; Kerberos-only sites have none | Compatible, not the base 
mechanism |
+| Replay the end user's credential (Trino `oauth2` forwarding; 
[#12984](https://github.com/apache/gravitino/pull/12984) for Lance REST) | No 
new trust configuration | Trino's SPI does not pass passwords to connectors 
([#13473](https://github.com/apache/gravitino/pull/13473)). A Kerberos ticket 
cannot be replayed to another service. The relay holds the user's credential. | 
Rejected |
+| Trust `simple` user names from a trusted network | Works today | Nothing is 
authenticated; any client can claim any name | Rejected |
+| Trusted proxy header checked against server rules | Same model as Hadoop 
proxy users, which Trino, Kyuubi and HiveServer2 operators already run. Works 
with any strong authenticator. One server change serves every such deployment. 
| New trust configuration to manage | **Chosen** |
+
+---
+
+## Proposal
+
+### Overview
+
+```
+ BI client ──► Trino ──(Kerberos as trino/[email protected] + 
X-Gravitino-Proxy-User: alice)──► Gravitino
+ caller ────► Lance REST ──(OAuth as lance-rest + X-Gravitino-Proxy-User: 
bob)─────────────────► Gravitino
+```
+
+### Headers
+
+| Header | Direction | Value | Notes |
+|--------|-----------|-------|-------|
+| `X-Gravitino-Proxy-User` | Request | One user name | The effective user of 
the request |
+| `X-Gravitino-Proxy-Groups` | Request | Comma-separated group names | Valid 
only with `X-Gravitino-Proxy-User` |
+| `X-Gravitino-Effective-User` | Response | One user name | Set on every 
response to a request whose proxy header the server applied |
+
+Names are UTF-8 and percent-encoded as in RFC 3986. The server encodes 
`X-Gravitino-Effective-User` the same way, and the client decodes it strictly 
before comparing. The server splits `X-Gravitino-Proxy-Groups` on raw commas 
before decoding, so a comma inside a group name is sent as `%2C`.
+
+The server rejects with 400 a repeated proxy header, a name with an invalid 
percent escape or invalid UTF-8 (`+` is a literal `+`), and any decoded name 
that:
+
+- is empty or has a leading or trailing space;
+- contains a space separator (category Zs) other than U+0020, a character of 
category Cc, Cf, Zl, Zp, Co, Cs or Cn, or a `Default_Ignorable_Code_Point` 
character such as a zero-width space or a variation selector;
+- changes under NFKC normalization, which covers non-NFC forms and 
compatibility characters such as full-width letters and the long s `ſ`.
+
+Every name the server compares is therefore in one form and has no invisible 
characters. Names may contain `/` and `@`, as e-mail addresses and Kerberos 
principals do.
+
+### Configuration
+
+Key names follow the existing `gravitino.authenticator.oauth.*` style. `<id>` 
is a rule name chosen by the administrator.
+
+| Key | Default | Description |
+|-----|---------|-------------|
+| `gravitino.authenticator.proxy.enable` | `false` | Whether the server 
accepts proxy headers. When `false`, a proxy header gets 403, or 400 when 
malformed. |
+| `gravitino.authenticator.proxy.rules` | empty | Comma-separated rule names |
+| `gravitino.authenticator.proxy.rule.<id>.authenticator` | none, required | 
The authenticator that must have accepted the request: `kerberos`, `oauth`, 
`basic`, or a custom authenticator's class name as written in 
`gravitino.authenticators` |
+| `gravitino.authenticator.proxy.rule.<id>.issuer` | none, required for 
`oauth` | The `iss` claim the proxy's token must carry |
+| `gravitino.authenticator.proxy.rule.<id>.principals` | none, required | 
Comma-separated proxy identities the rule applies to (see below) |
+| `gravitino.authenticator.proxy.rule.<id>.users` | none, required | 
Comma-separated users the rule allows, or `*` |
+| `gravitino.authenticator.proxy.rule.<id>.excludeUsers` | empty | 
Comma-separated users the rule never allows, even when `users` names them |
+| `gravitino.authenticator.proxy.rule.<id>.groups` | empty | Comma-separated 
groups the proxy may declare, or `*`. When empty, `X-Gravitino-Proxy-Groups` is 
rejected. |
+| `gravitino.authenticator.proxy.rule.<id>.hosts` | `*` | Comma-separated IP 
addresses or CIDR ranges the TCP peer address must be in, or `*` |
+
+A rule matches when the authenticator that accepted the request is the rule's 
`authenticator` and the proxy identity is in `principals`. The proxy identity 
is the name the authenticator verified, before principal mapping:
+
+| Authenticator | Proxy identity | Accepted forms in `principals` | Example |
+|---------------|----------------|--------------------------------|---------|
+| `kerberos` | The full principal, with instance and realm | `primary@REALM`, 
`primary/instance@REALM`, or `primary/*@REALM`, where `*` matches one non-empty 
instance; primary and realm are literal | `trino/*@EXAMPLE.COM` |
+| `oauth` | The token's `sub`, only when its `iss` equals the rule's `issuer` 
| Literal values without `*` | `lance-rest` |
+| `basic` | The user name | Literal values without `*` | `etl-service` |
+| A custom authenticator, by class name | The proxy identity the authenticator 
sets on its `UserPrincipal` | Literal values without `*` | `svc-gateway` |
+
+The OAuth proxy identity is always `sub`, whatever 
`gravitino.authenticator.oauth.principalFields` selects, because users can 
choose claims such as `preferred_username` in many IdPs.
+
+Rule semantics:
+
+- One rule must allow everything in a request: the authenticator, the 
identity, the user, every declared group and the peer address. Rules do not 
combine.
+- `*` in `users` matches no user in `gravitino.authorization.serviceAdmins`, 
so a service admin can be proxied only when named in `users`. `anonymous` 
cannot be proxied.
+- `hosts` is compared with `HttpServletRequest.getRemoteAddr()`, never with 
`X-Forwarded-For`. Behind a load balancer the peer is the balancer, so `hosts` 
stays `*` there.
+- The standalone Iceberg REST and Lance REST servers read the same keys, and 
`gravitino.authorization.serviceAdmins`, from their own configuration files.
+- Rules are read at startup, so a change takes effect on a server, standalone 
ones included, only after that server restarts.
+- The server fails to start when a listed rule lacks a required key, when 
`authenticator` is a built-in authenticator's class name instead of its short 
name, when a `principals` entry does not have an accepted form, when a `users`, 
`excludeUsers` or `groups` entry fails the decoded-name checks in 
[Headers](#headers), or when a `hosts` entry is not `*`, an IP address or a 
CIDR range. A rule whose `authenticator` is any other name not registered on a 
server loads there and matches no request.
+
+The names in `excludeUsers` and `gravitino.authorization.serviceAdmins`, and 
`anonymous`, match a proxied name by `String.equalsIgnoreCase` against both the 
full name and its part before the first `/` or `@`. So `excludeUsers=hdfs` also 
excludes `HDFS`, `[email protected]` and `hdfs/[email protected]`, and `hive` 
also excludes `hıve` with a dotless `ı`. Exclusions ignore case because Hadoop 
group mapping often does. Entries in `users` and `groups` match only the exact 
name.
+
+### Changes to `Authenticator` and `AuthenticationFilter`
+
+`Authenticator` gains a method that marks the authenticators whose principals 
may act as a proxy. `KerberosAuthenticator`, `OAuth2TokenAuthenticator` and the 
idp-basic `BasicAuthenticator` return `true`; `SimpleAuthenticator` and 
existing custom authenticators keep the default.
+
+```java
+/**
+ * Returns whether a principal accepted by this authenticator may act as a 
trusted proxy.
+ *
+ * @return true if requests accepted by this authenticator may carry a proxy 
header
+ */
+default boolean supportsProxy() {
+  return false;
+}
+```
+
+An authenticator that supports proxying sets the proxy identity on the 
`UserPrincipal` it returns. Principal mapping happens inside 
`KerberosAuthenticator` and the OAuth validators (`JwksTokenValidator`, 
`StaticSignKeyValidator`), so they set it, and the validators also set the 
token's issuer. `OAuth2TokenAuthenticator` keeps both only when the configured 
validator's class is exactly one of those two, and drops them for any other 
`tokenValidatorClass`, including a subclass. A principal without a proxy 
identity cannot proxy.
+
+`AuthenticatorFactory` names each authenticator after its entry in 
`gravitino.authenticators`, with built-in class names normalized to `kerberos`, 
`oauth`, `basic` and `simple`, so an authenticator cannot claim another's name. 
`ServerAuthenticator` keeps an ordered name-to-instance map. For a duplicate 
name the factory keeps the first entry and logs a warning, which matches 
today's first-match behavior. The `BasicAuthenticator` of `plugins/idp-basic` 
is named `basic`, including the one that 
`IdpRESTFeature.registerBasicAuthenticator` adds. `resolvePrincipal` stores the 
name of the accepting authenticator as a request attribute and clears any real 
user on the principal, so only `applyProxyUser` sets it.
+
+`authenticate` gains a step between principal resolution and active roles:
+
+```java
+private Principal authenticate(HttpServletRequest request, HttpServletResponse 
response) {
+  return applyActiveRoles(request, applyProxyUser(request, response, 
resolvePrincipal(request)));
+}
+```
+
+`applyProxyUser` does the following, in order:
+
+1. Neither proxy header is present: return the principal unchanged.
+2. A proxy header fails the checks in [Headers](#headers), or 
`X-Gravitino-Proxy-Groups` comes without `X-Gravitino-Proxy-User`: throw 
`IllegalProxyHeaderException` (400).
+3. Proxying is disabled, the accepting authenticator does not support 
proxying, or the principal has no proxy identity: throw `ForbiddenException` 
(403).
+4. The user is `anonymous`, or no single rule allows the authenticator, the 
identity, the user, the peer address and every declared group: throw 
`ForbiddenException` (403).
+5. Otherwise set `X-Gravitino-Effective-User` on the response and return a 
`UserPrincipal` for the proxied user with the declared groups, no access token, 
and the proxy identity as real user.
+
+`IllegalProxyHeaderException` extends `IllegalArgumentException`. 
`AuthenticationFilter.doFilter` catches it next to 
`IllegalActiveRolesException`, and `sendAuthErrorResponse` in 
`AuthenticationFilter` and `LanceAuthenticationFilter` maps it to 400. 
`IcebergAuthenticationFilter` already maps every `IllegalArgumentException` to 
400. All three map `ForbiddenException` to 403, and the Iceberg and Lance 
filters, which override only `sendAuthErrorResponse`, inherit the new step. 
Active roles are applied after it, so `X-Gravitino-Active-Roles` narrows the 
proxied user's roles.
+
+### Changes to `UserPrincipal`
+
+`UserPrincipal` gets three optional fields:
+
+| Accessor | Value |
+|----------|-------|
+| `getProxyIdentity()` | The name the authenticator verified, before principal 
mapping: the full Kerberos principal, the OAuth `sub`, the Basic user name, or 
what a custom authenticator sets. |
+| `getProxyIdentityIssuer()` | The `iss` of the caller's OAuth token, set by 
the OAuth validators. Only `oauth` rules read it. |
+| `getRealUser()` | The proxy identity of the service that sent 
`X-Gravitino-Proxy-User` for this user, for example `trino/[email protected]`. 
Audit and events record this form. |
+
+Two new public constructors take either the proxy identity and its issuer, 
used by the authenticators and validators above, or the real user, used by 
`applyProxyUser`. `withActiveRoles` and every other rebuild for the same user 
copy all three fields. Only `OAuth2TokenAuthenticator` and `resolvePrincipal` 
drop any, as described above.
+
+A proxied principal carries no access token, so no component can forward the 
proxy's credential as the user's. `UserPrincipalForwardingAuthManager`, the one 
server-side reader of the access token, fails with its existing `UserPrincipal 
has no authorization header value` error (Non-Goal 4).
+
+### Audit and events
+
+Owners, the `creator` field of audit info, and privilege checks read the 
current principal's name, so they see the proxied user with no change. Two 
additions record the real user:
+
+- `AuditLog` gets `default @Nullable String realUser() { return null; }`. 
`SimpleAuditLogV2` and the `JsonAuditLog` of `JsonAuditFormatter` write it only 
when it is set. `SimpleAuditLogV2` writes it as a `realUser=` field between 
`remoteAddress` and `customInfo`, because `customInfo` holds raw query 
parameters and is not escaped, so a field after it could be forged. The v1 
`SimpleFormatter` does not write it.
+- The `Event` constructor captures `realUser` from the current principal, as 
it captures `remoteAddress` from `RequestContext`, and `BaseEvent.realUser()` 
exposes it. `HttpAuditFilter` builds `HttpRequestEvent` and 
`HttpRequestFailureEvent` outside `Subject.doAs`, so it passes them `realUser` 
from the authenticated principal request attribute.
+
+### Client changes
+
+The Java client builder gets one method, which combines with any 
authentication method:
+
+```java
+/**
+ * Makes every request act for the given user, which the server accepts only 
from a trusted proxy.
+ *
+ * @param user the user to act for
+ * @param groups the groups to declare for the user, or an empty list
+ * @return this builder
+ */
+public Builder<T> withProxyUser(String user, List<String> groups)
+```
+
+The builder keeps the user and groups in their own fields and adds the headers 
at `build()`, so a later `withHeaders`, which replaces the whole header map, 
cannot drop them. It percent-encodes names as in [Headers](#headers) and sends 
no groups header for an empty list.
+
+A client built with `withProxyUser` checks that the proxy takes effect:
+
+- `build()` of `GravitinoClient` and `GravitinoAdminClient` calls `GET 
/api/authn/me`, which returns the effective user in `principal`, and fails 
unless it equals the proxied user. A server without the endpoint fails the 
build with an error that names the endpoint.
+- Every successful response must carry an `X-Gravitino-Effective-User` equal 
to the proxied user.
+
+A server that ignores the proxy header therefore fails `build()`. The 
per-response check fires only after a request has run, so every server behind 
one address must have this feature before clients proxy through it.
+
+Because only `withProxyUser` clients make these checks, `withHeaders` rejects 
both proxy headers, in any letter case, with `IllegalArgumentException`; GVFS's 
`fs.gravitino.client.request.header.*` keys go through `withHeaders` and are 
rejected too. A service that acts for many users builds one client per user, as 
the Trino connector already does for `forwardUser`.
+
+### Client configuration
+
+| Component | Key | Default | Description |
+|-----------|-----|---------|-------------|
+| Trino connector | `gravitino.client.session.forwardUser.useProxy` | `false` 
| Under `oauth2`, use the proxy header for sessions without a user token |
+| Trino connector | `gravitino.client.session.forwardGroups` | `false` | Send 
the session's groups in `X-Gravitino-Proxy-Groups` on calls that use the proxy 
header; ignored otherwise |
+| Lance REST | `gravitino.lance-rest.gravitino-proxy-caller` | `false` | Call 
Gravitino as the service, with the caller in proxy headers |
+| Lance REST | `gravitino.lance-rest.gravitino-proxy-client-cache-size` | 
`1000` | Maximum number of per-caller clients in `gravitino-proxy-caller` mode |
+
+### Trino connector
+
+With `gravitino.client.session.forwardUser=true`:
+
+| `authType` | Current behavior | Behavior with this design |
+|------------|------------------|---------------------------|
+| `simple` | Sends the session user name, unverified | Unchanged |
+| `basic`, `kerberos` | `GravitinoConnector.buildSessionCache` rejects the 
setting, so the catalog fails to load | The connector's own credentials plus 
`X-Gravitino-Proxy-User: <session user>` |
+| `oauth2`, session has a user token | Forwards the user's token | Unchanged |
+| `oauth2`, no user token | `resolveSessionMetadata` returns the shared 
metadata, so calls run as the connector | Proxy header when 
`forwardUser.useProxy=true`; unchanged otherwise |
+
+- `buildSessionCache` accepts `basic` and `kerberos`, `resolveSessionMetadata` 
honors `forwardUser.useProxy`, and `GravitinoAuthProvider.buildForSession` 
builds each per-user client that uses the proxy header with `withProxyUser`.
+- Under `basic` and `kerberos`, and under `oauth2` with 
`forwardUser.useProxy`, `GravitinoMetadata` resolves the per-user client from 
the `ConnectorSession` of each call, not once in `getMetadata`, because Trino 
creates one `ConnectorMetadata` per catalog and transaction, and the session of 
a `SECURITY DEFINER` view shares the transaction of the query that reads it. 
Under `simple`, and under `oauth2` without `forwardUser.useProxy`, 
`GravitinoMetadata` keeps one client per transaction, as today.
+- With `forwardGroups=true` the connector declares 
`ConnectorSession.getIdentity().getGroups()`, which is empty unless Trino has a 
group provider. A session with a group that the rule does not list fails every 
query with 403.
+- Under `kerberos`, each per-user client gets its own `KerberosTokenProvider` 
built from the connector's keytab, so evicting one client logs out only that 
client. The cost is one Kerberos login per cache miss.
+- Under `oauth2` with `forwardUser.useProxy`, the proxied per-user clients 
share the connector's OAuth token provider through a `CustomTokenProvider` that 
delegates `getTokenData()` and whose `close()` does nothing, as in [Lance REST 
proxy-caller mode](#lance-rest-proxy-caller-mode), because `HTTPClient.close()` 
closes its provider when the cache evicts a client.
+- `sessionCacheKey` becomes a structured key of the auth type, the session 
user, the sorted set of declared groups, whether the proxy header is used and, 
for `oauth2` sessions with a user token, a SHA-256 hash of the token. No 
separator character can make two sessions share a client, and a refreshed user 
token gets a new client.
+
+Catalogs routed to Gravitino's Iceberg REST service are out of scope (Non-Goal 
4).
+
+### Lance REST proxy-caller mode
+
+With `gravitino.lance-rest.gravitino-proxy-caller=true`, Lance REST calls 
Gravitino with its own OAuth client credentials 
(`gravitino.lance-rest.gravitino-auth-type=oauth2`):
+
+- For each caller it builds a client with `withProxyUser(caller, groups)` and 
passes the caller's `X-Gravitino-Active-Roles` through `withHeaders`, so the 
checks in [Client changes](#client-changes) apply to every call.
+- It caches at most `gravitino-proxy-client-cache-size` clients, under a 
structured key of the caller, the sorted set of groups and the sorted, 
deduplicated set of trimmed role names. Spellings of one role list share a 
client, no separator character makes two callers collide, and a caller cannot 
grow the cache without bound.
+- All clients share the one OAuth token provider that Lance REST closes at 
shutdown. Each client gets it through a `CustomTokenProvider` that delegates 
`getTokenData()` and whose `close()` does nothing, because `HTTPClient.close()` 
closes its provider when the cache evicts the client.

Review Comment:
   A no-op token-provider `close()` protects the shared token, but eviction 
still closes the client's HTTP transport. With cache size 1, caller A can 
obtain a catalog, caller B can evict and close A's client, and A's subsequent 
table operation uses that closed client. Please specify request-scoped 
leases/reference counting (or another safe retirement mechanism), so eviction 
removes an entry from the cache but closes it only after its active users 
finish, and include this interleaving in the planned tests.



##########
design-docs/gravitino-trusted-proxy.md:
##########
@@ -0,0 +1,401 @@
+<!--
+  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.
+-->
+
+# Design: Trusted Proxy Users for Apache Gravitino
+
+---
+
+## Background
+
+Gravitino authenticates each REST request with one of the authenticators in 
`gravitino.authenticators` (`simple`, `basic`, `oauth`, `kerberos`), and 
authorization, ownership and audit use the authenticated principal. This breaks 
when a service sits between the end user and Gravitino and the end user holds 
no credential that Gravitino can verify:
+
+| Deployment | Who authenticates the end user | What Gravitino sees today |
+|------------|--------------------------------|---------------------------|
+| Trino serving BI, JDBC and ODBC clients | Trino | With `authType=simple` and 
`gravitino.client.session.forwardUser=true`, an unverified session user name. 
With `oauth2`, the user's token if the session has one, otherwise the 
connector's identity. With `basic` or `kerberos`, only the connector's 
identity, because a catalog that sets `forwardUser=true` fails to load 
([#13472](https://github.com/apache/gravitino/issues/13472)). |
+| Standalone Lance REST server | Lance REST | One configured service identity, 
so the service account owns every table Lance REST creates and audit never 
records the caller 
([#13090](https://github.com/apache/gravitino/issues/13090)). |
+
+Knox in front of the Gravitino UI has the same problem 
([#8876](https://github.com/apache/gravitino/issues/8876)); see Non-Goal 7.
+
+Hadoop (`hadoop.proxyuser.*`), HiveServer2 and Kyuubi let an authenticated 
service act for a user that server-side rules allow. Gravitino has no such 
mechanism, and `SimpleAuthenticator` accepts any user name the client sends.
+
+---
+
+## Goals
+
+1. **Trusted proxy**: A request carrying `X-Gravitino-Proxy-User: alice` runs 
as `alice` for authorization, ownership and audit if and only if proxying is 
enabled, its proxy headers pass the checks in [Headers](#headers), and one 
proxy rule matches the authenticated identity and allows `alice`, every 
declared group and the TCP peer address. The request must be authenticated by 
Kerberos, by OAuth 2 with `JwksTokenValidator` or `StaticSignKeyValidator`, by 
Basic, or by a custom authenticator that supports proxying. A request that 
fails authentication is rejected as today; among authenticated requests, a 
malformed proxy header gets 400 and any other failed condition gets 403.
+2. **Real user in audit**: For every proxied request, post events and audit 
logs in the `SimpleAuditLogV2` and JSON formats record the authenticated 
identity as `realUser`.
+3. **Per-user authorization behind Trino**: With 
`gravitino.client.session.forwardUser=true`, the Trino connector's Gravitino 
calls run as the session user under `authType=basic` and `kerberos`, and under 
`oauth2` for sessions without a user token when 
`gravitino.client.session.forwardUser.useProxy=true`.
+4. **No silent fallback**: A client configured to proxy a user fails at 
`build()` when the server does not apply the proxy header, and rejects any 
later successful response that the server did not run as the proxied user.
+5. **Per-caller identity behind Lance REST**: With 
`gravitino.lance-rest.gravitino-proxy-caller=true`, objects that a caller 
creates through the standalone Lance REST server are owned and audited as that 
caller.
+6. **Off by default**: Without the new settings, requests without the new 
headers behave as today, apart from the [fileset fix](#fileset-fix), and 
requests with a proxy header are rejected.
+
+---
+
+## Non-Goals
+
+1. **A Trino `SystemAccessControl` plugin or an `/authorize` decision API**: 
Running the connector's own calls as the end user already gives per-user 
decisions. A decision API can be added later; see [Solution 
Investigations](#solution-investigations).

Review Comment:
   Forwarding the user authorizes Gravitino metadata calls, but does not 
enforce operation-specific privileges for Trino data reads and writes. 
`CatalogConnectorMetadata.getTable` loads metadata with either `SELECT_TABLE` 
or `MODIFY_TABLE`, while `GravitinoMetadata.beginInsert`, `executeUpdate` and 
`executeDelete` delegate to the native connector without a Gravitino modify 
check. Qualify this statement and document that deployments still need separate 
Trino or backend authorization for data operations.



##########
design-docs/gravitino-trusted-proxy.md:
##########
@@ -0,0 +1,401 @@
+<!--
+  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.
+-->
+
+# Design: Trusted Proxy Users for Apache Gravitino
+
+---
+
+## Background
+
+Gravitino authenticates each REST request with one of the authenticators in 
`gravitino.authenticators` (`simple`, `basic`, `oauth`, `kerberos`), and 
authorization, ownership and audit use the authenticated principal. This breaks 
when a service sits between the end user and Gravitino and the end user holds 
no credential that Gravitino can verify:
+
+| Deployment | Who authenticates the end user | What Gravitino sees today |
+|------------|--------------------------------|---------------------------|
+| Trino serving BI, JDBC and ODBC clients | Trino | With `authType=simple` and 
`gravitino.client.session.forwardUser=true`, an unverified session user name. 
With `oauth2`, the user's token if the session has one, otherwise the 
connector's identity. With `basic` or `kerberos`, only the connector's 
identity, because a catalog that sets `forwardUser=true` fails to load 
([#13472](https://github.com/apache/gravitino/issues/13472)). |
+| Standalone Lance REST server | Lance REST | One configured service identity, 
so the service account owns every table Lance REST creates and audit never 
records the caller 
([#13090](https://github.com/apache/gravitino/issues/13090)). |
+
+Knox in front of the Gravitino UI has the same problem 
([#8876](https://github.com/apache/gravitino/issues/8876)); see Non-Goal 7.
+
+Hadoop (`hadoop.proxyuser.*`), HiveServer2 and Kyuubi let an authenticated 
service act for a user that server-side rules allow. Gravitino has no such 
mechanism, and `SimpleAuthenticator` accepts any user name the client sends.
+
+---
+
+## Goals
+
+1. **Trusted proxy**: A request carrying `X-Gravitino-Proxy-User: alice` runs 
as `alice` for authorization, ownership and audit if and only if proxying is 
enabled, its proxy headers pass the checks in [Headers](#headers), and one 
proxy rule matches the authenticated identity and allows `alice`, every 
declared group and the TCP peer address. The request must be authenticated by 
Kerberos, by OAuth 2 with `JwksTokenValidator` or `StaticSignKeyValidator`, by 
Basic, or by a custom authenticator that supports proxying. A request that 
fails authentication is rejected as today; among authenticated requests, a 
malformed proxy header gets 400 and any other failed condition gets 403.
+2. **Real user in audit**: For every proxied request, post events and audit 
logs in the `SimpleAuditLogV2` and JSON formats record the authenticated 
identity as `realUser`.
+3. **Per-user authorization behind Trino**: With 
`gravitino.client.session.forwardUser=true`, the Trino connector's Gravitino 
calls run as the session user under `authType=basic` and `kerberos`, and under 
`oauth2` for sessions without a user token when 
`gravitino.client.session.forwardUser.useProxy=true`.
+4. **No silent fallback**: A client configured to proxy a user fails at 
`build()` when the server does not apply the proxy header, and rejects any 
later successful response that the server did not run as the proxied user.
+5. **Per-caller identity behind Lance REST**: With 
`gravitino.lance-rest.gravitino-proxy-caller=true`, objects that a caller 
creates through the standalone Lance REST server are owned and audited as that 
caller.
+6. **Off by default**: Without the new settings, requests without the new 
headers behave as today, apart from the [fileset fix](#fileset-fix), and 
requests with a proxy header are rejected.
+
+---
+
+## Non-Goals
+
+1. **A Trino `SystemAccessControl` plugin or an `/authorize` decision API**: 
Running the connector's own calls as the end user already gives per-user 
decisions. A decision API can be added later; see [Solution 
Investigations](#solution-investigations).
+2. **Server-side group resolution**: Gravitino does not look up a proxied 
user's groups. The proxy declares them, limited to its rule's `groups`. A group 
mapping SPI belongs with LDAP support 
([#6006](https://github.com/apache/gravitino/issues/6006)).
+3. **Credentials for processes that run user code**: User code in such a 
process, for example a Spark engine that Kyuubi launches, could read a proxy's 
credential and act for every user the proxy's rule allows. Such processes need 
a per-user credential, which is out of scope.
+4. **Per-user identity on Iceberg REST paths that bypass the Gravitino client 
or forward the caller's credential**: Trino catalogs routed to Gravitino's 
Iceberg REST service use Trino's native Iceberg REST client, which never sends 
the Gravitino client's headers, so for proxied sessions they keep the identity 
configured for that route. Iceberg catalogs with a `rest` backend forward the 
caller's own `Authorization` header through 
`UserPrincipalForwardingAuthManager`; proxied principals carry none, so their 
requests to such catalogs fail.
+5. **Python client**: No deployment in [Background](#background) uses the 
Python client. A Python `with_proxy_user` needs no server change and can follow 
separately.
+6. **Replacing token exchange**: Sites whose IdP supports RFC 8693 token 
exchange can keep using it 
([#13092](https://github.com/apache/gravitino/issues/13092)); Gravitino sees an 
exchanged token as an ordinary OAuth token.
+7. **Knox and the MCP server**: Knox passes the end user in a `doAs` query 
parameter. Using this mechanism needs either a Knox dispatch that sends 
`X-Gravitino-Proxy-User` instead, or Gravitino accepting `doAs` under the same 
proxy rules; [#8877](https://github.com/apache/gravitino/pull/8877) proposes 
accepting `doAs` from any Kerberos caller. That choice is left to a follow-up. 
The MCP server forwards the `Authorization` header of each HTTP request to 
Gravitino, by default uses its own configured identity when there is none, and 
does not authenticate its callers, so a proxy mode would first need caller 
authentication in the MCP server.
+
+---
+
+## Solution Investigations
+
+| Approach | Pros | Cons | Decision |
+|----------|------|------|----------|
+| Decision API `POST /api/metalakes/{metalake}/authorize` plus a Trino 
`SystemAccessControl` plugin (the sketch in #11307) | Trino enforces before 
planning and covers every Trino check hook | Answers allow or deny only. The 
connector's metadata calls still run as the service account, so owners and 
audit stay wrong. Each engine needs its own plugin. Does nothing for Lance 
REST. | Rejected |
+| RFC 8693 token exchange at the IdP | Standard; Gravitino sees an ordinary 
OAuth token and needs no proxy rules | Requires an IdP that supports the 
exchange grant; Kerberos-only sites have none | Compatible, not the base 
mechanism |
+| Replay the end user's credential (Trino `oauth2` forwarding; 
[#12984](https://github.com/apache/gravitino/pull/12984) for Lance REST) | No 
new trust configuration | Trino's SPI does not pass passwords to connectors 
([#13473](https://github.com/apache/gravitino/pull/13473)). A Kerberos ticket 
cannot be replayed to another service. The relay holds the user's credential. | 
Rejected |
+| Trust `simple` user names from a trusted network | Works today | Nothing is 
authenticated; any client can claim any name | Rejected |
+| Trusted proxy header checked against server rules | Same model as Hadoop 
proxy users, which Trino, Kyuubi and HiveServer2 operators already run. Works 
with any strong authenticator. One server change serves every such deployment. 
| New trust configuration to manage | **Chosen** |
+
+---
+
+## Proposal
+
+### Overview
+
+```
+ BI client ──► Trino ──(Kerberos as trino/[email protected] + 
X-Gravitino-Proxy-User: alice)──► Gravitino
+ caller ────► Lance REST ──(OAuth as lance-rest + X-Gravitino-Proxy-User: 
bob)─────────────────► Gravitino
+```
+
+### Headers
+
+| Header | Direction | Value | Notes |
+|--------|-----------|-------|-------|
+| `X-Gravitino-Proxy-User` | Request | One user name | The effective user of 
the request |
+| `X-Gravitino-Proxy-Groups` | Request | Comma-separated group names | Valid 
only with `X-Gravitino-Proxy-User` |
+| `X-Gravitino-Effective-User` | Response | One user name | Set on every 
response to a request whose proxy header the server applied |
+
+Names are UTF-8 and percent-encoded as in RFC 3986. The server encodes 
`X-Gravitino-Effective-User` the same way, and the client decodes it strictly 
before comparing. The server splits `X-Gravitino-Proxy-Groups` on raw commas 
before decoding, so a comma inside a group name is sent as `%2C`.
+
+The server rejects with 400 a repeated proxy header, a name with an invalid 
percent escape or invalid UTF-8 (`+` is a literal `+`), and any decoded name 
that:
+
+- is empty or has a leading or trailing space;
+- contains a space separator (category Zs) other than U+0020, a character of 
category Cc, Cf, Zl, Zp, Co, Cs or Cn, or a `Default_Ignorable_Code_Point` 
character such as a zero-width space or a variation selector;
+- changes under NFKC normalization, which covers non-NFC forms and 
compatibility characters such as full-width letters and the long s `ſ`.
+
+Every name the server compares is therefore in one form and has no invisible 
characters. Names may contain `/` and `@`, as e-mail addresses and Kerberos 
principals do.
+
+### Configuration
+
+Key names follow the existing `gravitino.authenticator.oauth.*` style. `<id>` 
is a rule name chosen by the administrator.
+
+| Key | Default | Description |
+|-----|---------|-------------|
+| `gravitino.authenticator.proxy.enable` | `false` | Whether the server 
accepts proxy headers. When `false`, a proxy header gets 403, or 400 when 
malformed. |
+| `gravitino.authenticator.proxy.rules` | empty | Comma-separated rule names |
+| `gravitino.authenticator.proxy.rule.<id>.authenticator` | none, required | 
The authenticator that must have accepted the request: `kerberos`, `oauth`, 
`basic`, or a custom authenticator's class name as written in 
`gravitino.authenticators` |
+| `gravitino.authenticator.proxy.rule.<id>.issuer` | none, required for 
`oauth` | The `iss` claim the proxy's token must carry |
+| `gravitino.authenticator.proxy.rule.<id>.principals` | none, required | 
Comma-separated proxy identities the rule applies to (see below) |
+| `gravitino.authenticator.proxy.rule.<id>.users` | none, required | 
Comma-separated users the rule allows, or `*` |
+| `gravitino.authenticator.proxy.rule.<id>.excludeUsers` | empty | 
Comma-separated users the rule never allows, even when `users` names them |
+| `gravitino.authenticator.proxy.rule.<id>.groups` | empty | Comma-separated 
groups the proxy may declare, or `*`. When empty, `X-Gravitino-Proxy-Groups` is 
rejected. |
+| `gravitino.authenticator.proxy.rule.<id>.hosts` | `*` | Comma-separated IP 
addresses or CIDR ranges the TCP peer address must be in, or `*` |
+
+A rule matches when the authenticator that accepted the request is the rule's 
`authenticator` and the proxy identity is in `principals`. The proxy identity 
is the name the authenticator verified, before principal mapping:
+
+| Authenticator | Proxy identity | Accepted forms in `principals` | Example |
+|---------------|----------------|--------------------------------|---------|
+| `kerberos` | The full principal, with instance and realm | `primary@REALM`, 
`primary/instance@REALM`, or `primary/*@REALM`, where `*` matches one non-empty 
instance; primary and realm are literal | `trino/*@EXAMPLE.COM` |
+| `oauth` | The token's `sub`, only when its `iss` equals the rule's `issuer` 
| Literal values without `*` | `lance-rest` |
+| `basic` | The user name | Literal values without `*` | `etl-service` |
+| A custom authenticator, by class name | The proxy identity the authenticator 
sets on its `UserPrincipal` | Literal values without `*` | `svc-gateway` |
+
+The OAuth proxy identity is always `sub`, whatever 
`gravitino.authenticator.oauth.principalFields` selects, because users can 
choose claims such as `preferred_username` in many IdPs.
+
+Rule semantics:
+
+- One rule must allow everything in a request: the authenticator, the 
identity, the user, every declared group and the peer address. Rules do not 
combine.
+- `*` in `users` matches no user in `gravitino.authorization.serviceAdmins`, 
so a service admin can be proxied only when named in `users`. `anonymous` 
cannot be proxied.
+- `hosts` is compared with `HttpServletRequest.getRemoteAddr()`, never with 
`X-Forwarded-For`. Behind a load balancer the peer is the balancer, so `hosts` 
stays `*` there.
+- The standalone Iceberg REST and Lance REST servers read the same keys, and 
`gravitino.authorization.serviceAdmins`, from their own configuration files.
+- Rules are read at startup, so a change takes effect on a server, standalone 
ones included, only after that server restarts.
+- The server fails to start when a listed rule lacks a required key, when 
`authenticator` is a built-in authenticator's class name instead of its short 
name, when a `principals` entry does not have an accepted form, when a `users`, 
`excludeUsers` or `groups` entry fails the decoded-name checks in 
[Headers](#headers), or when a `hosts` entry is not `*`, an IP address or a 
CIDR range. A rule whose `authenticator` is any other name not registered on a 
server loads there and matches no request.
+
+The names in `excludeUsers` and `gravitino.authorization.serviceAdmins`, and 
`anonymous`, match a proxied name by `String.equalsIgnoreCase` against both the 
full name and its part before the first `/` or `@`. So `excludeUsers=hdfs` also 
excludes `HDFS`, `[email protected]` and `hdfs/[email protected]`, and `hive` 
also excludes `hıve` with a dotless `ı`. Exclusions ignore case because Hadoop 
group mapping often does. Entries in `users` and `groups` match only the exact 
name.
+
+### Changes to `Authenticator` and `AuthenticationFilter`
+
+`Authenticator` gains a method that marks the authenticators whose principals 
may act as a proxy. `KerberosAuthenticator`, `OAuth2TokenAuthenticator` and the 
idp-basic `BasicAuthenticator` return `true`; `SimpleAuthenticator` and 
existing custom authenticators keep the default.
+
+```java
+/**
+ * Returns whether a principal accepted by this authenticator may act as a 
trusted proxy.
+ *
+ * @return true if requests accepted by this authenticator may carry a proxy 
header
+ */
+default boolean supportsProxy() {
+  return false;
+}
+```
+
+An authenticator that supports proxying sets the proxy identity on the 
`UserPrincipal` it returns. Principal mapping happens inside 
`KerberosAuthenticator` and the OAuth validators (`JwksTokenValidator`, 
`StaticSignKeyValidator`), so they set it, and the validators also set the 
token's issuer. `OAuth2TokenAuthenticator` keeps both only when the configured 
validator's class is exactly one of those two, and drops them for any other 
`tokenValidatorClass`, including a subclass. A principal without a proxy 
identity cannot proxy.
+
+`AuthenticatorFactory` names each authenticator after its entry in 
`gravitino.authenticators`, with built-in class names normalized to `kerberos`, 
`oauth`, `basic` and `simple`, so an authenticator cannot claim another's name. 
`ServerAuthenticator` keeps an ordered name-to-instance map. For a duplicate 
name the factory keeps the first entry and logs a warning, which matches 
today's first-match behavior. The `BasicAuthenticator` of `plugins/idp-basic` 
is named `basic`, including the one that 
`IdpRESTFeature.registerBasicAuthenticator` adds. `resolvePrincipal` stores the 
name of the accepting authenticator as a request attribute and clears any real 
user on the principal, so only `applyProxyUser` sets it.
+
+`authenticate` gains a step between principal resolution and active roles:
+
+```java
+private Principal authenticate(HttpServletRequest request, HttpServletResponse 
response) {
+  return applyActiveRoles(request, applyProxyUser(request, response, 
resolvePrincipal(request)));
+}
+```
+
+`applyProxyUser` does the following, in order:
+
+1. Neither proxy header is present: return the principal unchanged.
+2. A proxy header fails the checks in [Headers](#headers), or 
`X-Gravitino-Proxy-Groups` comes without `X-Gravitino-Proxy-User`: throw 
`IllegalProxyHeaderException` (400).
+3. Proxying is disabled, the accepting authenticator does not support 
proxying, or the principal has no proxy identity: throw `ForbiddenException` 
(403).
+4. The user is `anonymous`, or no single rule allows the authenticator, the 
identity, the user, the peer address and every declared group: throw 
`ForbiddenException` (403).
+5. Otherwise set `X-Gravitino-Effective-User` on the response and return a 
`UserPrincipal` for the proxied user with the declared groups, no access token, 
and the proxy identity as real user.
+
+`IllegalProxyHeaderException` extends `IllegalArgumentException`. 
`AuthenticationFilter.doFilter` catches it next to 
`IllegalActiveRolesException`, and `sendAuthErrorResponse` in 
`AuthenticationFilter` and `LanceAuthenticationFilter` maps it to 400. 
`IcebergAuthenticationFilter` already maps every `IllegalArgumentException` to 
400. All three map `ForbiddenException` to 403, and the Iceberg and Lance 
filters, which override only `sendAuthErrorResponse`, inherit the new step. 
Active roles are applied after it, so `X-Gravitino-Active-Roles` narrows the 
proxied user's roles.
+
+### Changes to `UserPrincipal`
+
+`UserPrincipal` gets three optional fields:
+
+| Accessor | Value |
+|----------|-------|
+| `getProxyIdentity()` | The name the authenticator verified, before principal 
mapping: the full Kerberos principal, the OAuth `sub`, the Basic user name, or 
what a custom authenticator sets. |
+| `getProxyIdentityIssuer()` | The `iss` of the caller's OAuth token, set by 
the OAuth validators. Only `oauth` rules read it. |
+| `getRealUser()` | The proxy identity of the service that sent 
`X-Gravitino-Proxy-User` for this user, for example `trino/[email protected]`. 
Audit and events record this form. |
+
+Two new public constructors take either the proxy identity and its issuer, 
used by the authenticators and validators above, or the real user, used by 
`applyProxyUser`. `withActiveRoles` and every other rebuild for the same user 
copy all three fields. Only `OAuth2TokenAuthenticator` and `resolvePrincipal` 
drop any, as described above.
+
+A proxied principal carries no access token, so no component can forward the 
proxy's credential as the user's. `UserPrincipalForwardingAuthManager`, the one 
server-side reader of the access token, fails with its existing `UserPrincipal 
has no authorization header value` error (Non-Goal 4).
+
+### Audit and events
+
+Owners, the `creator` field of audit info, and privilege checks read the 
current principal's name, so they see the proxied user with no change. Two 
additions record the real user:
+
+- `AuditLog` gets `default @Nullable String realUser() { return null; }`. 
`SimpleAuditLogV2` and the `JsonAuditLog` of `JsonAuditFormatter` write it only 
when it is set. `SimpleAuditLogV2` writes it as a `realUser=` field between 
`remoteAddress` and `customInfo`, because `customInfo` holds raw query 
parameters and is not escaped, so a field after it could be forged. The v1 
`SimpleFormatter` does not write it.
+- The `Event` constructor captures `realUser` from the current principal, as 
it captures `remoteAddress` from `RequestContext`, and `BaseEvent.realUser()` 
exposes it. `HttpAuditFilter` builds `HttpRequestEvent` and 
`HttpRequestFailureEvent` outside `Subject.doAs`, so it passes them `realUser` 
from the authenticated principal request attribute.
+
+### Client changes
+
+The Java client builder gets one method, which combines with any 
authentication method:
+
+```java
+/**
+ * Makes every request act for the given user, which the server accepts only 
from a trusted proxy.
+ *
+ * @param user the user to act for
+ * @param groups the groups to declare for the user, or an empty list
+ * @return this builder
+ */
+public Builder<T> withProxyUser(String user, List<String> groups)
+```
+
+The builder keeps the user and groups in their own fields and adds the headers 
at `build()`, so a later `withHeaders`, which replaces the whole header map, 
cannot drop them. It percent-encodes names as in [Headers](#headers) and sends 
no groups header for an empty list.
+
+A client built with `withProxyUser` checks that the proxy takes effect:
+
+- `build()` of `GravitinoClient` and `GravitinoAdminClient` calls `GET 
/api/authn/me`, which returns the effective user in `principal`, and fails 
unless it equals the proxied user. A server without the endpoint fails the 
build with an error that names the endpoint.
+- Every successful response must carry an `X-Gravitino-Effective-User` equal 
to the proxied user.
+
+A server that ignores the proxy header therefore fails `build()`. The 
per-response check fires only after a request has run, so every server behind 
one address must have this feature before clients proxy through it.
+
+Because only `withProxyUser` clients make these checks, `withHeaders` rejects 
both proxy headers, in any letter case, with `IllegalArgumentException`; GVFS's 
`fs.gravitino.client.request.header.*` keys go through `withHeaders` and are 
rejected too. A service that acts for many users builds one client per user, as 
the Trino connector already does for `forwardUser`.
+
+### Client configuration
+
+| Component | Key | Default | Description |
+|-----------|-----|---------|-------------|
+| Trino connector | `gravitino.client.session.forwardUser.useProxy` | `false` 
| Under `oauth2`, use the proxy header for sessions without a user token |
+| Trino connector | `gravitino.client.session.forwardGroups` | `false` | Send 
the session's groups in `X-Gravitino-Proxy-Groups` on calls that use the proxy 
header; ignored otherwise |
+| Lance REST | `gravitino.lance-rest.gravitino-proxy-caller` | `false` | Call 
Gravitino as the service, with the caller in proxy headers |
+| Lance REST | `gravitino.lance-rest.gravitino-proxy-client-cache-size` | 
`1000` | Maximum number of per-caller clients in `gravitino-proxy-caller` mode |
+
+### Trino connector
+
+With `gravitino.client.session.forwardUser=true`:
+
+| `authType` | Current behavior | Behavior with this design |
+|------------|------------------|---------------------------|
+| `simple` | Sends the session user name, unverified | Unchanged |
+| `basic`, `kerberos` | `GravitinoConnector.buildSessionCache` rejects the 
setting, so the catalog fails to load | The connector's own credentials plus 
`X-Gravitino-Proxy-User: <session user>` |
+| `oauth2`, session has a user token | Forwards the user's token | Unchanged |
+| `oauth2`, no user token | `resolveSessionMetadata` returns the shared 
metadata, so calls run as the connector | Proxy header when 
`forwardUser.useProxy=true`; unchanged otherwise |
+
+- `buildSessionCache` accepts `basic` and `kerberos`, `resolveSessionMetadata` 
honors `forwardUser.useProxy`, and `GravitinoAuthProvider.buildForSession` 
builds each per-user client that uses the proxy header with `withProxyUser`.
+- Under `basic` and `kerberos`, and under `oauth2` with 
`forwardUser.useProxy`, `GravitinoMetadata` resolves the per-user client from 
the `ConnectorSession` of each call, not once in `getMetadata`, because Trino 
creates one `ConnectorMetadata` per catalog and transaction, and the session of 
a `SECURITY DEFINER` view shares the transaction of the query that reads it. 
Under `simple`, and under `oauth2` without `forwardUser.useProxy`, 
`GravitinoMetadata` keeps one client per transaction, as today.
+- With `forwardGroups=true` the connector declares 
`ConnectorSession.getIdentity().getGroups()`, which is empty unless Trino has a 
group provider. A session with a group that the rule does not list fails every 
query with 403.
+- Under `kerberos`, each per-user client gets its own `KerberosTokenProvider` 
built from the connector's keytab, so evicting one client logs out only that 
client. The cost is one Kerberos login per cache miss.
+- Under `oauth2` with `forwardUser.useProxy`, the proxied per-user clients 
share the connector's OAuth token provider through a `CustomTokenProvider` that 
delegates `getTokenData()` and whose `close()` does nothing, as in [Lance REST 
proxy-caller mode](#lance-rest-proxy-caller-mode), because `HTTPClient.close()` 
closes its provider when the cache evicts a client.
+- `sessionCacheKey` becomes a structured key of the auth type, the session 
user, the sorted set of declared groups, whether the proxy header is used and, 
for `oauth2` sessions with a user token, a SHA-256 hash of the token. No 
separator character can make two sessions share a client, and a refreshed user 
token gets a new client.
+
+Catalogs routed to Gravitino's Iceberg REST service are out of scope (Non-Goal 
4).
+
+### Lance REST proxy-caller mode
+
+With `gravitino.lance-rest.gravitino-proxy-caller=true`, Lance REST calls 
Gravitino with its own OAuth client credentials 
(`gravitino.lance-rest.gravitino-auth-type=oauth2`):
+
+- For each caller it builds a client with `withProxyUser(caller, groups)` and 
passes the caller's `X-Gravitino-Active-Roles` through `withHeaders`, so the 
checks in [Client changes](#client-changes) apply to every call.
+- It caches at most `gravitino-proxy-client-cache-size` clients, under a 
structured key of the caller, the sorted set of groups and the sorted, 
deduplicated set of trimmed role names. Spellings of one role list share a 
client, no separator character makes two callers collide, and a caller cannot 
grow the cache without bound.
+- All clients share the one OAuth token provider that Lance REST closes at 
shutdown. Each client gets it through a `CustomTokenProvider` that delegates 
`getTokenData()` and whose `close()` does nothing, because `HTTPClient.close()` 
closes its provider when the cache evicts the client.
+- Lance REST fails to start in this mode unless the outbound auth type is 
`oauth2` and its own `gravitino.authenticators`, whose default is `simple`, 
does not include `simple`, so it never forwards a name from `simple`. It also 
fails to start when the cache size is not positive.
+
+### Catalog impersonation
+
+Catalogs, schemas and filesets with `authentication.impersonation-enable` act 
on HMS and HDFS as the current user. Today the Kerberos call sites and the 
fileset proxy append a realm to a name without `@` and pass it to 
`UserGroupInformation.createProxyUser`. HMS and the NameNode then map that 
principal to a short name with their own `hadoop.security.auth_to_local` rules, 
which Gravitino cannot see and which may map, for example, 
`[email protected]` to `hdfs`. A proxied user must not reach storage 
through such a mapping, so for a principal that has a real user, every 
impersonation call site first does the following:
+
+1. **Reject service names.** Throw `ForbiddenException` (403) when the name 
contains `/` or `@`, or equals, by `String.equalsIgnoreCase`, the login user 
name of the call site or its part before the first `/` or `@`. Under Kerberos 
the login user name is the login principal, for a fileset the one in the merged 
catalog, schema and fileset properties; under simple authentication it is the 
name of the process user from `UserGroupInformation.getCurrentUser()`. No 
`auth_to_local` rule is applied, because the JVM's rules may differ from those 
of HMS and the NameNode.
+2. **Pass the bare name.** Call `createProxyUser` with the name and no realm, 
and use the same bare name as the owner of any HMS delegation token that the 
call site requests. `KerberosName.getShortName` returns a one-component name 
without a realm unchanged, so no `auth_to_local` rule applies to it anywhere.
+
+| Call site | Covers | Change |
+|-----------|--------|--------|
+| `KerberosCatalogUtils` | Iceberg catalogs, the Iceberg REST server, 
`ClosableHiveCatalog` | `resolveProxyPrincipal` returns the bare name; the 
impersonation branch of `createRealUser` runs the check; `ClosableHiveCatalog` 
requests an HMS token only when impersonation is enabled 
([#14408](https://github.com/apache/gravitino/pull/14408)), and for a principal 
with a real user uses the bare owner |
+| `HmsKerberosClient.loginProxyUser` | Hive | The check runs before the 
existing shortcut `currentUser.equals(realLoginUgi.getUserName()) || hiveClient 
== null`, which returns the login UGI. For a principal with a real user, only 
the `hiveClient == null` branch stays; it builds the client that 
`HiveClientFactory` uses for token requests. The HMS token owner is the bare 
name. |
+| `HiveClientFactory`, simple-authentication branch | Hive | The check runs 
before the shortcut that reuses the process UGI when the names match |
+| `FilesetCatalogOperations` with `ImpersonationHDFSFileSystemProxy` | 
Filesets | The check runs in the request thread, where the [fileset 
fix](#fileset-fix) reads the user. For a principal with a real user, 
`FilesetCatalogOperations` tells `ImpersonationHDFSFileSystemProxy` not to 
append a realm, so `createProxyUser` gets the bare name. |
+
+Each call site throws the `ForbiddenException` before its own error handling. 
All code between the call site and the REST layer rethrows that exception 
unchanged, so the client gets 403. Several layers wrap exceptions today, among 
them `HiveClientFactory.createHiveClient`, `HiveClientPool.newClient`, 
`HiveCatalogOperations`, `HiveViewCatalogOperations` and 
`IcebergCatalogOperations`. The Hive client pool key 
(`CachedClientPool.extractKey`) and 
`FilesetCatalogOperations.FileSystemCacheKey` gain the real user, so proxied 
and non-proxied requests for the same name never share a client or a file 
system. The Iceberg Hive client pool needs no change: under Kerberos it is 
keyed by the UGI user name, which is the bare name only for a principal that 
has a real user.
+
+**Short names.** Step 1 compares only with the login user name of the call 
site and its first component. HMS and the NameNode may give a principal a short 
name that differs from its first component, for example `gravitino` for 
`[email protected]`, and HMS issues a delegation token without a 
proxy-user check when the owner's short name equals the caller's. A proxied 
name equal to the login principal's short name, or to the short name of another 
principal, such as one that a schema sets with impersonation off, therefore 
passes step 1 and reaches storage as that principal.
+
+So when any catalog, schema or fileset enables impersonation, the 
`excludeUsers` of every rule lists `hdfs`, `hive`, and the short name that HMS 
and HDFS give each principal that a catalog, schema or fileset sets, whether or 
not it enables impersonation. Administrators add a name before a catalog, 
schema or fileset is created with that principal or altered to set it.

Review Comment:
   The exclusion list misses storage superusers whose names are neither 
`hdfs`/`hive` nor a configured catalog principal, such as a NameNode process 
owner named `nn`. With `users=*`, bare `nn` passes step 1 when the catalog logs 
in as `gravitino`, and an otherwise permitted Hadoop proxy request gains HDFS 
superuser access regardless of superuser-group membership. Require exclusion of 
the actual target storage superuser names as well as the conventional names and 
catalog credentials.



##########
design-docs/gravitino-trusted-proxy.md:
##########
@@ -0,0 +1,401 @@
+<!--
+  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.
+-->
+
+# Design: Trusted Proxy Users for Apache Gravitino
+
+---
+
+## Background
+
+Gravitino authenticates each REST request with one of the authenticators in 
`gravitino.authenticators` (`simple`, `basic`, `oauth`, `kerberos`), and 
authorization, ownership and audit use the authenticated principal. This breaks 
when a service sits between the end user and Gravitino and the end user holds 
no credential that Gravitino can verify:
+
+| Deployment | Who authenticates the end user | What Gravitino sees today |
+|------------|--------------------------------|---------------------------|
+| Trino serving BI, JDBC and ODBC clients | Trino | With `authType=simple` and 
`gravitino.client.session.forwardUser=true`, an unverified session user name. 
With `oauth2`, the user's token if the session has one, otherwise the 
connector's identity. With `basic` or `kerberos`, only the connector's 
identity, because a catalog that sets `forwardUser=true` fails to load 
([#13472](https://github.com/apache/gravitino/issues/13472)). |
+| Standalone Lance REST server | Lance REST | One configured service identity, 
so the service account owns every table Lance REST creates and audit never 
records the caller 
([#13090](https://github.com/apache/gravitino/issues/13090)). |
+
+Knox in front of the Gravitino UI has the same problem 
([#8876](https://github.com/apache/gravitino/issues/8876)); see Non-Goal 7.
+
+Hadoop (`hadoop.proxyuser.*`), HiveServer2 and Kyuubi let an authenticated 
service act for a user that server-side rules allow. Gravitino has no such 
mechanism, and `SimpleAuthenticator` accepts any user name the client sends.
+
+---
+
+## Goals
+
+1. **Trusted proxy**: A request carrying `X-Gravitino-Proxy-User: alice` runs 
as `alice` for authorization, ownership and audit if and only if proxying is 
enabled, its proxy headers pass the checks in [Headers](#headers), and one 
proxy rule matches the authenticated identity and allows `alice`, every 
declared group and the TCP peer address. The request must be authenticated by 
Kerberos, by OAuth 2 with `JwksTokenValidator` or `StaticSignKeyValidator`, by 
Basic, or by a custom authenticator that supports proxying. A request that 
fails authentication is rejected as today; among authenticated requests, a 
malformed proxy header gets 400 and any other failed condition gets 403.
+2. **Real user in audit**: For every proxied request, post events and audit 
logs in the `SimpleAuditLogV2` and JSON formats record the authenticated 
identity as `realUser`.
+3. **Per-user authorization behind Trino**: With 
`gravitino.client.session.forwardUser=true`, the Trino connector's Gravitino 
calls run as the session user under `authType=basic` and `kerberos`, and under 
`oauth2` for sessions without a user token when 
`gravitino.client.session.forwardUser.useProxy=true`.
+4. **No silent fallback**: A client configured to proxy a user fails at 
`build()` when the server does not apply the proxy header, and rejects any 
later successful response that the server did not run as the proxied user.
+5. **Per-caller identity behind Lance REST**: With 
`gravitino.lance-rest.gravitino-proxy-caller=true`, objects that a caller 
creates through the standalone Lance REST server are owned and audited as that 
caller.
+6. **Off by default**: Without the new settings, requests without the new 
headers behave as today, apart from the [fileset fix](#fileset-fix), and 
requests with a proxy header are rejected.
+
+---
+
+## Non-Goals
+
+1. **A Trino `SystemAccessControl` plugin or an `/authorize` decision API**: 
Running the connector's own calls as the end user already gives per-user 
decisions. A decision API can be added later; see [Solution 
Investigations](#solution-investigations).
+2. **Server-side group resolution**: Gravitino does not look up a proxied 
user's groups. The proxy declares them, limited to its rule's `groups`. A group 
mapping SPI belongs with LDAP support 
([#6006](https://github.com/apache/gravitino/issues/6006)).
+3. **Credentials for processes that run user code**: User code in such a 
process, for example a Spark engine that Kyuubi launches, could read a proxy's 
credential and act for every user the proxy's rule allows. Such processes need 
a per-user credential, which is out of scope.
+4. **Per-user identity on Iceberg REST paths that bypass the Gravitino client 
or forward the caller's credential**: Trino catalogs routed to Gravitino's 
Iceberg REST service use Trino's native Iceberg REST client, which never sends 
the Gravitino client's headers, so for proxied sessions they keep the identity 
configured for that route. Iceberg catalogs with a `rest` backend forward the 
caller's own `Authorization` header through 
`UserPrincipalForwardingAuthManager`; proxied principals carry none, so their 
requests to such catalogs fail.
+5. **Python client**: No deployment in [Background](#background) uses the 
Python client. A Python `with_proxy_user` needs no server change and can follow 
separately.
+6. **Replacing token exchange**: Sites whose IdP supports RFC 8693 token 
exchange can keep using it 
([#13092](https://github.com/apache/gravitino/issues/13092)); Gravitino sees an 
exchanged token as an ordinary OAuth token.
+7. **Knox and the MCP server**: Knox passes the end user in a `doAs` query 
parameter. Using this mechanism needs either a Knox dispatch that sends 
`X-Gravitino-Proxy-User` instead, or Gravitino accepting `doAs` under the same 
proxy rules; [#8877](https://github.com/apache/gravitino/pull/8877) proposes 
accepting `doAs` from any Kerberos caller. That choice is left to a follow-up. 
The MCP server forwards the `Authorization` header of each HTTP request to 
Gravitino, by default uses its own configured identity when there is none, and 
does not authenticate its callers, so a proxy mode would first need caller 
authentication in the MCP server.
+
+---
+
+## Solution Investigations
+
+| Approach | Pros | Cons | Decision |
+|----------|------|------|----------|
+| Decision API `POST /api/metalakes/{metalake}/authorize` plus a Trino 
`SystemAccessControl` plugin (the sketch in #11307) | Trino enforces before 
planning and covers every Trino check hook | Answers allow or deny only. The 
connector's metadata calls still run as the service account, so owners and 
audit stay wrong. Each engine needs its own plugin. Does nothing for Lance 
REST. | Rejected |
+| RFC 8693 token exchange at the IdP | Standard; Gravitino sees an ordinary 
OAuth token and needs no proxy rules | Requires an IdP that supports the 
exchange grant; Kerberos-only sites have none | Compatible, not the base 
mechanism |
+| Replay the end user's credential (Trino `oauth2` forwarding; 
[#12984](https://github.com/apache/gravitino/pull/12984) for Lance REST) | No 
new trust configuration | Trino's SPI does not pass passwords to connectors 
([#13473](https://github.com/apache/gravitino/pull/13473)). A Kerberos ticket 
cannot be replayed to another service. The relay holds the user's credential. | 
Rejected |
+| Trust `simple` user names from a trusted network | Works today | Nothing is 
authenticated; any client can claim any name | Rejected |
+| Trusted proxy header checked against server rules | Same model as Hadoop 
proxy users, which Trino, Kyuubi and HiveServer2 operators already run. Works 
with any strong authenticator. One server change serves every such deployment. 
| New trust configuration to manage | **Chosen** |
+
+---
+
+## Proposal
+
+### Overview
+
+```
+ BI client ──► Trino ──(Kerberos as trino/[email protected] + 
X-Gravitino-Proxy-User: alice)──► Gravitino
+ caller ────► Lance REST ──(OAuth as lance-rest + X-Gravitino-Proxy-User: 
bob)─────────────────► Gravitino
+```
+
+### Headers
+
+| Header | Direction | Value | Notes |
+|--------|-----------|-------|-------|
+| `X-Gravitino-Proxy-User` | Request | One user name | The effective user of 
the request |
+| `X-Gravitino-Proxy-Groups` | Request | Comma-separated group names | Valid 
only with `X-Gravitino-Proxy-User` |
+| `X-Gravitino-Effective-User` | Response | One user name | Set on every 
response to a request whose proxy header the server applied |
+
+Names are UTF-8 and percent-encoded as in RFC 3986. The server encodes 
`X-Gravitino-Effective-User` the same way, and the client decodes it strictly 
before comparing. The server splits `X-Gravitino-Proxy-Groups` on raw commas 
before decoding, so a comma inside a group name is sent as `%2C`.
+
+The server rejects with 400 a repeated proxy header, a name with an invalid 
percent escape or invalid UTF-8 (`+` is a literal `+`), and any decoded name 
that:
+
+- is empty or has a leading or trailing space;
+- contains a space separator (category Zs) other than U+0020, a character of 
category Cc, Cf, Zl, Zp, Co, Cs or Cn, or a `Default_Ignorable_Code_Point` 
character such as a zero-width space or a variation selector;
+- changes under NFKC normalization, which covers non-NFC forms and 
compatibility characters such as full-width letters and the long s `ſ`.
+
+Every name the server compares is therefore in one form and has no invisible 
characters. Names may contain `/` and `@`, as e-mail addresses and Kerberos 
principals do.
+
+### Configuration
+
+Key names follow the existing `gravitino.authenticator.oauth.*` style. `<id>` 
is a rule name chosen by the administrator.
+
+| Key | Default | Description |
+|-----|---------|-------------|
+| `gravitino.authenticator.proxy.enable` | `false` | Whether the server 
accepts proxy headers. When `false`, a proxy header gets 403, or 400 when 
malformed. |
+| `gravitino.authenticator.proxy.rules` | empty | Comma-separated rule names |
+| `gravitino.authenticator.proxy.rule.<id>.authenticator` | none, required | 
The authenticator that must have accepted the request: `kerberos`, `oauth`, 
`basic`, or a custom authenticator's class name as written in 
`gravitino.authenticators` |
+| `gravitino.authenticator.proxy.rule.<id>.issuer` | none, required for 
`oauth` | The `iss` claim the proxy's token must carry |
+| `gravitino.authenticator.proxy.rule.<id>.principals` | none, required | 
Comma-separated proxy identities the rule applies to (see below) |
+| `gravitino.authenticator.proxy.rule.<id>.users` | none, required | 
Comma-separated users the rule allows, or `*` |
+| `gravitino.authenticator.proxy.rule.<id>.excludeUsers` | empty | 
Comma-separated users the rule never allows, even when `users` names them |
+| `gravitino.authenticator.proxy.rule.<id>.groups` | empty | Comma-separated 
groups the proxy may declare, or `*`. When empty, `X-Gravitino-Proxy-Groups` is 
rejected. |
+| `gravitino.authenticator.proxy.rule.<id>.hosts` | `*` | Comma-separated IP 
addresses or CIDR ranges the TCP peer address must be in, or `*` |
+
+A rule matches when the authenticator that accepted the request is the rule's 
`authenticator` and the proxy identity is in `principals`. The proxy identity 
is the name the authenticator verified, before principal mapping:
+
+| Authenticator | Proxy identity | Accepted forms in `principals` | Example |
+|---------------|----------------|--------------------------------|---------|
+| `kerberos` | The full principal, with instance and realm | `primary@REALM`, 
`primary/instance@REALM`, or `primary/*@REALM`, where `*` matches one non-empty 
instance; primary and realm are literal | `trino/*@EXAMPLE.COM` |
+| `oauth` | The token's `sub`, only when its `iss` equals the rule's `issuer` 
| Literal values without `*` | `lance-rest` |
+| `basic` | The user name | Literal values without `*` | `etl-service` |
+| A custom authenticator, by class name | The proxy identity the authenticator 
sets on its `UserPrincipal` | Literal values without `*` | `svc-gateway` |
+
+The OAuth proxy identity is always `sub`, whatever 
`gravitino.authenticator.oauth.principalFields` selects, because users can 
choose claims such as `preferred_username` in many IdPs.
+
+Rule semantics:
+
+- One rule must allow everything in a request: the authenticator, the 
identity, the user, every declared group and the peer address. Rules do not 
combine.
+- `*` in `users` matches no user in `gravitino.authorization.serviceAdmins`, 
so a service admin can be proxied only when named in `users`. `anonymous` 
cannot be proxied.
+- `hosts` is compared with `HttpServletRequest.getRemoteAddr()`, never with 
`X-Forwarded-For`. Behind a load balancer the peer is the balancer, so `hosts` 
stays `*` there.

Review Comment:
   A load balancer does not require `hosts=*` under the proposed TCP-peer 
semantics: the rule can allow the balancer's IP addresses or CIDRs. 
Recommending a wildcard removes that restriction and also allows requests 
arriving directly from other addresses with the proxy credential. Please 
explain that an allowlist still constrains the immediate peer, although it 
cannot constrain the original client behind the balancer.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to