jerryshao commented on code in PR #12249: URL: https://github.com/apache/gravitino/pull/12249#discussion_r3689223450
########## design-docs/gravitino-entity-secrets.md: ########## @@ -0,0 +1,946 @@ +<!-- + 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 of Entity Secrets Management in Apache Gravitino + +## 1. Background + +Gravitino entities often require **connection secrets** in properties, for example: + +| Category | Where | Example keys | +| ----------------- | ----------------------------------------- | -------------------------------------------------- | +| JDBC | Catalog | `jdbc-password` | +| Static cloud keys | Catalog / schema / fileset (esp. Fileset) | `s3-secret-access-key`, `aws-secret-access-key`, … | +| Kerberos | Schema / fileset (Fileset catalog) | keytab / principal related properties | + +Today these values are commonly stored as **plaintext strings** in entity properties JSON +(`catalog_meta` / `schema_meta` / `fileset_version_info`). API responses may redact them, but +persistence is not a secrets manager. That creates: + +1. **Security risk** — DB / backup / support dumps may expose long-lived secrets. +2. **Governance gap** — enterprises already operate a central secret store and want Gravitino to + **reference** it, not fork a second password silo. + +Peer systems solve the same problem with an abstraction between **metadata** and **secret +material**: + +- **Apache Polaris** exposes a `UserSecretsManager` SPI and persists a typed `SecretReference` + object on the entity (not an ambiguous plaintext string). +- **Databricks** provides a **Secrets** service (scopes); Unity Catalog connections / foreign + catalogs reference secrets via `secret(scope, key)` instead of embedding passwords as bare + strings in connection options. + +Gravitino should define a pluggable **secrets-provider SPI**, persist durable **URN references** +instead of plaintext for marked keys, expose a clear **REST create/alter contract**, and ship an +**in-memory provider** for tests and local use. + +--- + +## 2. Goals + +1. **Secrets provider interface**: define a pluggable `GravitinoSecretProvider` for write / + read / delete of secret material behind durable **references**. + +2. **Follow existing `KmsClient` patterns when implementing `GravitinoSecretProvider`**: only a + minority of products can be connected the same way for both table-encryption KMS and entity + secrets. What can be reused is the Factory / registry style and, where applicable, the same + connection setup — not the `KmsClient` interface itself. + +3. **REST + persistence split**: HTTP **`properties`** stays **`map<string, string>`**; optional + **`secretReferences`** (key → locator object; server **builds** the URN) and/or + **`secretBindings`** (key → provider name for write-through) mark secrets on **create**; **alter** + adds `@type`s **`setSecretBinding`** / **`setSecretReference`** (§5.9.4) for **catalog, schema, and + fileset** (see §5.9). **Persistence** stays an all-string JSON map on each entity's properties + column. Secret property values are stored as **URN strings**. A property is treated as a secret + when its value matches the **URN recognition rule** (§5.1): starts with `urn:gravitino-secret` + and ends with that property's key. Whether to `deleteSecret` on entity drop or alter + `removeProperty` is decided from the **URN shape** (write-through embeds + `entityType`/`entityId`/`propertyKey` — §5.5.2 C). + +4. **Backward compatible reads**: existing all-string entity properties continue to + work as plaintext with no migration required. + +5. **Omit secrets on GET/list and audit**: GET/list **omit** any property whose value matches the + URN recognition rule (§5.1) (same strip behavior as today's `PropertiesMetadata.hidden`). + +6. **In-memory provider**: ship a process-local `InMemorySecretsProvider` for UT / IT / local + quick-start (not for production). + +7. **Server-side resolution only**: resolve references on the Gravitino server when loading + catalogs / schemas / filesets or connecting; call `readSecret` **on each use**. + +## 3. Non-Goals + +1. **Fixed sensitive-key allowlists as the resolution gate**: Polaris-style fixed property-name + allowlists are out of scope. Secrets are identified by **URN-shaped values** (§5.1), not by a + hardcoded or reserved list of property names. REST **`secretReferences` / `secretBindings`** + declare which keys become secrets on create. + +2. **Plaintext provider credentials in configuration**: if a future provider needs credentials, + long-lived credential **values** must not appear in `gravitino.properties`. The in-memory + provider needs none. Configuration stores only non-secret settings (and env var **names** when + a provider requires them). + +3. **Additional provider implementations**: this design ships only **`InMemorySecretsProvider`**. + Other backends are out of scope here. + +## 4. Industry Approaches (Polaris and Databricks) + +This section compares **Apache Polaris** and **Databricks Secrets**. + +### 4.1 Apache Polaris — typed `SecretReference`, not string sniffing + +On create, inline plaintext is **write-through** via `UserSecretsManager.writeSecret`; only the +`SecretReference` object is stored. Reads call `readSecret(reference)`. + +**Takeaway:** typed persistence + SPI; secret material lives in the secrets manager. URN shell follows +[RFC 8141](https://www.rfc-editor.org/rfc/rfc8141.html) `urn:<NID>:<NSS>` with NID `polaris-secret` +(`urn:polaris-secret:<type>:<type-specific-identifier>`); identifier **semantics** stay per-provider. + +### 4.2 Databricks — Secrets service + references from connections + +Databricks stores secret material in the **Secrets** platform service. Connections recommend +`secret('scope', 'key')` instead of password literals. Runtime resolves from the Secrets service; +displays redact as `[REDACTED]`. + +**Takeaway:** password material in a secrets service; catalog/connection config holds references. + +### 4.3 Cross-product summary + +| Topic | Apache Polaris | Databricks | +| --------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| Where secret material lives | `UserSecretsManager` backend | Databricks Secrets service | +| What is persisted on catalog/connection | Typed `SecretReference` object | `secret(scope,key)` | +| Secret binding model | **Fixed allowlist** (`clientSecret`, `bearerToken`, …); always write-through | **Same property** may be plaintext **or** `secret(scope,key)` | +| Official backend kinds | SPI — any implementation | **Databricks-backed** + Azure Key Vault (peer product; not Gravitino scope) | + +### 4.4 Why Gravitino follows Polaris (not Databricks) for the reference shape + +Both peers share one idea we keep: **secret material lives outside catalog metadata**; catalogs hold +**references**, and does not expose secret material on read. **How** that idea is expressed differs +— and Gravitino’s product shape matches **Polaris** more closely than **Databricks Secrets**. + +| Dimension | Databricks | Polaris | Gravitino choice | +| ------------------------------ | ------------------------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Where secrets are stored | First-party **Secrets** service (scopes) | Pluggable **`UserSecretsManager`** (BYO / impl) | Pluggable SPI + in-memory provider — not a new Gravitino “scopes” product | +| How catalogs reference secrets | Platform DSL `secret(scope, key)` in SQL / connection options | Typed **`SecretReference`** object on the entity | REST: create **`secretReferences` / `secretBindings`**; alter **`setSecretBinding` / `setSecretReference`**; persistence: **URN string**; recognize secrets by URN shape; no SQL/`secret()` runtime | +| Secret binding model | **Same property** may be plaintext **or** `secret(scope,key)` | **Fixed allowlist** only; always write-through | **`secretReferences` / `secretBindings`** on create; alter via **`setSecretBinding` / `setSecretReference`**; omit URN-shaped keys on GET | +| Multi-backend / multi-instance | Scoped under the Databricks Secrets service | SPI type + URN `type-specific-identifier` | Named entries in server conf; URN embeds **`provider_name`** only (`className` selects implementation at factory time) | +| Official backend kinds | Databricks-backed + Azure Key Vault (peer) | SPI — implementer’s choice | **In-memory** provider shipped; SPI remains pluggable | + +--- + +## 5. Proposal + +### 5.1 Value model (REST vs persistence) + +| Layer | Shape | Role | +| ----------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| **REST** `properties` | `map<string, string>` | Unchanged from today — create HTTP values are strings | +| **REST** `secretReferences` | `map<string, object>` | Optional on **create** — **property key → locator** (external ref; server builds URN — §5.9.2) | +| **REST** alter secret `@type`s | in `updates` | **`setSecretBinding`** / **`setSecretReference`** (§5.9.4) — same `{ "updates": [...] }` body; `setProperty` unchanged | +| **REST** `secretBindings` | `map<string, string>` | Optional on **create** — **property key → provider name** (write-through; plaintext in `properties`) | +| **Persistence** entity `properties` | JSON **string map** | `catalog_meta` / `schema_meta` / `fileset_version_info` — secret keys store URN strings (§5.1 recognition rule) | + +**Secret recognition rule** (server-side; no reserved metadata key): + +A property `(key, value)` is treated as a **secret property** when **both** hold: + +1. `value` **starts with** `urn:gravitino-secret` +2. `value` **ends with** `key` (the property key) + +Server-built URNs always place the property key as the **last segment**, so create/alter paths +satisfy this rule by construction. Plaintext values never match. + +**Server-side resolve path** (entity load / connect — URN shape, not a key list): + +| Condition | Runtime behavior | +| ---------------------------------- | ----------------------------------------------------------------- | +| Value matches the recognition rule | Value is a URN string → parse `provider_name` → `readSecret(urn)` | +| Value does **not** match | Use value as plaintext; **do not** call secrets provider | + +Drop / `removeProperty` `deleteSecret` uses URN shape (§5.5.2 C). + +#### 5.1.1 URN shape + +```text +urn:gravitino-secret:<provider_name>:<type-specific-identifier> +``` + +| Part | Unified? | Rule | +| ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `urn:gravitino-secret` | Yes | Fixed scheme + namespace | +| `<provider_name>` | Yes | Config key `gravitino.secret.provider.<name>.*`; **authoritative** for registry lookup (provider settings live here — **not** in the URN) | +| `<type-specific-identifier>` | No | Address of the secret **inside** the provider selected by `provider_name`. Colon-separated `[a-zA-Z0-9_-]+` segments. Layout is defined by the provider implementation (`className`). | + +Secret URNs follow [RFC 8141](https://www.rfc-editor.org/rfc/rfc8141.html) `urn:<NID>:<NSS>` — a +persistent, location-independent name (not a fetch URL). Gravitino uses informal NID +`gravitino-secret`. + +| RFC 8141 term | Role | Gravitino mapping | +| ----------------------------------- | ------------------------------------------------- | -------------------------------------------- | +| **Scheme** | Always `urn` | `urn` | +| **NID** (Namespace Identifier) | Which naming system; unique across all `urn:` IDs | `gravitino-secret` | +| **NSS** (Namespace Specific String) | Concrete ID within that namespace | `<provider_name>:<type-specific-identifier>` | + +We use `urn:gravitino-secret:` so a secret property value is unambiguously a **Gravitino secret +handle**. Resolve by **`provider_name`** only; the SPI parses `<type-specific-identifier>`. +For recognition (§5.1), the full URN must also **end with the property key**. + +##### Why catalog **and** schema / fileset + +Fileset catalogs may attach Kerberos / cloud credentials on **schema** and **fileset** properties +(not only on the catalog). The same secrets SPI and URN envelope therefore apply to all three +entity levels. Write-through identifiers must include an **entity type** plus a **stable entity id** +so that (1) secrets for different levels never collide, and (2) renames of metalake/catalog/schema/ +fileset **names** do not invalidate stored URNs. + +##### Write-through / `in-memory`: `type-specific-identifier` naming + +```text +<type-specific-identifier> ::= <entityType>:<entityId>:<propertyKey> +``` + +| Segment | Values | Notes | +| --------------- | --------------------------------- | ----------------------------------------------------------- | +| `<entityType>` | `catalog`, `schema`, or `fileset` | Discriminator for property-bag owner | +| `<entityId>` | Stable numeric id | From `SecretWriteContext`; survives rename | +| `<propertyKey>` | Entity property key | e.g. `jdbc-password`, `s3-secret-access-key` (last segment) | + +Examples: + +```text +urn:gravitino-secret:memory:catalog:10042:jdbc-password +urn:gravitino-secret:memory:schema:20007:authentication-type +urn:gravitino-secret:memory:fileset:30019:s3-secret-access-key +``` + +The property key from `SecretWriteContext` is the last segment (not an opaque ordinal). +Re-writing the same property key overwrites that URN / map entry (no ordinal allocation). + +Write-through URNs (including in-memory) embed `<entityType>:<entityId>:<propertyKey>` so drop +can decide whether to call `deleteSecret` (§5.5.2 C). External-ref identifier layouts are defined +by each provider implementation, but the built URN **must still end with the property key** so the +recognition rule applies. This design only specifies the in-memory write-through form. + +Legacy persistence (no `secretReferences` / `secretBindings` on create): + +```json +{ + "jdbc-url": "jdbc:postgresql://db.example.com:5432/inventory", + "jdbc-user": "app_reader", + "jdbc-password": "S3cret!Passw0rd" +} +``` + +Reference persistence (after external ref or write-through): + +```json +{ + "jdbc-url": "jdbc:postgresql://db.example.com:5432/inventory", + "jdbc-user": "app_reader", + "jdbc-password": "urn:gravitino-secret:memory:catalog:10042:jdbc-password" +} +``` + +(Write-through stores the URN under the secret key; ownership is visible in the URN shape.) + +### 5.2 Secrets-provider instance registry + +Register named instances in +**server configuration** (`gravitino.conf` / `gravitino.properties` and included files), not in a +database table. + +**Authentication model:** the in-memory provider needs no credentials. Configuration stores +`className` and any non-secret settings — never plaintext credential values. + +```properties +gravitino.secret.providers=memory + +# In-memory (default for tests / local) +gravitino.secret.provider.memory.className=org.apache.gravitino.secrets.memory.InMemorySecretsProvider +``` + +Same shape as `gravitino.eventListener.names` + `gravitino.eventListener.{name}.class`: the +list holds **instance names**; `className` selects the implementation; remaining keys are +instance settings. + +| Key pattern | Meaning | +| -------------------------------------------- | ----------------------------------------------------------------------------- | +| `gravitino.secret.providers` | Comma-separated **instance names** (cluster scope) | +| `gravitino.secret.provider.<name>.className` | Fully qualified `GravitinoSecretProvider` implementation class (**required**) | +| `gravitino.secret.provider.<name>.*` | Implementation-specific settings (none for in-memory beyond `className`) | + +**Startup sequence (v1):** + +1. Operator starts Gravitino. +2. Provider factory loads each instance's **`className`**, passes the remaining + `gravitino.secret.provider.<name>.*` keys, and constructs live `GravitinoSecretProvider` + instances. + +Example entry summary: + +| provider_name | className (short) | settings (excerpt) | +| ------------- | -------------------------- | ------------------------- | +| `memory` | `…InMemorySecretsProvider` | (none beyond `className`) | + +Operators register or change these entries by **editing configuration and restarting** the Gravitino +server (v1). See §8 for the full configuration reference. + +### 5.3 Architecture + +``` + gravitino.conf (cluster) + gravitino.secret.providers=memory + gravitino.secret.provider.memory.className=… + + Catalog load / create + │ + ▼ + for each property (key, value): + value starts with urn:gravitino-secret + AND value ends with key + → parse provider_name → SPI.readSecret(urn) + else → plaintext as stored + │ + ▼ + catalog_meta.properties: all-string JSON map (secret values = URN strings) + GET/list: omit keys whose values match the URN recognition rule +``` + +### 5.4 Scope of this design + +| In scope | Out of scope | +| ---------------------------------------------- | ------------------------------------------------- | +| SPI + URN recognition + resolve / omit-on-read | Additional provider implementations beyond memory | +| Load providers from server conf | | +| **In-memory** secrets provider (UT/IT / local) | | + +Missing / unloadable `className` ⇒ startup or resolve fails with a clear error. + +### 5.5 `GravitinoSecretProvider` + +#### 5.5.1 One provider instance per configured name + +Core loads each named conf entry into **one** live `GravitinoSecretProvider` (via `className`) +and passes the remaining instance properties. Catalog resolve does: + +```text +value matches URN recognition rule (§5.1) + → parse provider_name from URN + → lookup live instance by name + → instance.readSecret(urn) +``` + +Illustrative Java (names TBD): + +```java +/** + * Backend client for a single configured secrets provider. + * Not a cluster-wide facade — core routes by provider_name parsed from the URN. + * Instance name / className are bound at factory time; each impl must implement type(). + */ +public interface GravitinoSecretProvider { + + String type(); + + /** + * Write-through: store plaintext in this backend and return a durable reference URN. + * Read-only / external-ref-only implementations throw UnsupportedOperationException. + * Core may wrap a provider-returned type-specific identifier into the full URN using the + * factory-bound provider name — or the impl returns the full URN. + */ + String writeSecret(String plaintext, SecretWriteContext context); + + /** Fetch secret material. Caller must not log or return this to HTTP GET/list. */ + String readSecret(String urn); + + /** + * Best-effort delete. Used on entity drop, alter removeProperty, and create + * rollback for <b>Gravitino-managed</b> (write-through) secrets only — see §5.5.2 C. + */ + void deleteSecret(String urn); +} + +/** + * Context for write-through path generation / GC metadata (Polaris forEntity analogue). + * External-ref path never calls writeSecret — this type is unused there. + * + * <p>Do <b>not</b> put renameable names (metalake / catalog / schema / fileset <b>names</b>) into + * generated URNs. Prefer stable ids so renames do not orphan or collide paths. + */ +public final class SecretWriteContext { + private final String entityType; // catalog | schema | fileset (URN segment; lowercase) + private final long entityId; // stable id for that entity + private final String propertyKey; // e.g. jdbc-password, s3-secret-access-key +} +``` + +**Mapping to entity property JSON** after write-through: + +```text +writeSecret → urn string + (URN envelope provider_name from factory binding; identifier includes entityType + entityId + propertyKey) +Core persists: + "jdbc-password": "<returned-urn>" +``` + +URN / write-through paths must use **stable entity ids** + **entity type** + **property key**, not +display names. + +#### 5.5.2 Concrete examples + +Create request shapes: §5.9.2 / §5.9.5. Below is the provider / URN outcome only. + +**A. Write-through (in-memory)** + +Client create uses `secretBindings` (`jdbc-password` → `memory`) plus plaintext in +`properties`. Core calls `writeSecret(plaintext, SecretWriteContext("catalog", entityId, +"jdbc-password"))`, persists e.g.: + +```text +jdbc-password = urn:gravitino-secret:memory:catalog:10042:jdbc-password +``` + +Trailing `catalog:<id>:jdbc-password` marks write-through for drop (§5.5.2 C). The value starts +with `urn:gravitino-secret` and ends with `jdbc-password`, so resolve / omit treat it as a secret. + +**B. External reference (`secretReferences`)** + +The REST contract accepts a locator (`provider` + `attributes`). Core **builds** the URN and +persists it without calling `writeSecret`. Required `attributes` keys are defined by the selected +provider. The built URN **must end with the property key**. **`InMemorySecretsProvider` rejects +external-ref binds** in this design (it is write-through only); other providers may accept them +via the same interface. + +**C. Drop entity / `removeProperty` — URN shape decides delete** + +No new drop query param (no `purgeSecrets`). The same ownership check applies when: + +- dropping a catalog, schema, or fileset, or +- alter `removeProperty` removes a secret key + +For each candidate property whose value matches the URN recognition rule (§5.1), parse the stored +URN: + +| URN type-specific identifier (after `provider_name`) | Behavior | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| **Write-through shaped for this entity** — for in-memory, the identifier is exactly `<entityType>:<entityId>:<propertyKey>` where `entityType` ∈ {`catalog`,`schema`,`fileset`}, `entityId` equals this entity's id, and `propertyKey` equals the property key | Best-effort `deleteSecret(urn)`, then drop / remove the property metadata | +| **Anything else** (including external-ref URNs) | Drop / remove property metadata only — **do not** call `deleteSecret` | + +Rationale: write-through secrets are **Gravitino-managed**; once the property (or entity) is gone, +leaving material in the provider would orphan it. External refs are owned outside Gravitino. + +### 5.6 In-memory secrets provider (OSS default) + +OSS ships a default `GravitinoSecretProvider` for **tests / local / quick-start**. + +| Aspect | Rule | +| ---------------- | ---------------------------------------------------------------------------- | +| Config | `className=…InMemorySecretsProvider` | +| Intended use | UT / IT / local only — **not** production | +| Durability | Process-local `ConcurrentHashMap`; **lost on restart** | +| Multi-node | Each JVM has its own map — do not share write-through secrets across servers | +| Auth / conf keys | None beyond `className`; SPI `type()` returns `memory` | + +**Config example:** + +```properties +gravitino.secret.providers=memory +gravitino.secret.provider.memory.className=org.apache.gravitino.secrets.memory.InMemorySecretsProvider +``` + +**Storage:** map value is the secret material as a **Base64 string** (encoding only — not +cryptographic protection). Entity metadata still stores only the URN. + +```text +map[URN] = Base64(plaintext) +``` + +| SPI method | Behavior | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `writeSecret` | Base64-encode plaintext. Allocate URN `urn:gravitino-secret:<provider_name>:<entityType>:<entityId>:<propertyKey>` from `SecretWriteContext`. Store Base64 under that URN (overwrite if same key). Return the URN. | +| `readSecret` | Look up map by URN; Base64-decode; return plaintext. Missing URN → treat as gone (impl-defined: null / error). Provider name in URN must match this instance. | +| `deleteSecret` | `map.remove(urn)` — best-effort; used by create rollback, entity drop, and alter `removeProperty` for **managed** (write-through) secrets (§5.5.2 C). | + +**Type-specific identifier:** `<entityType>:<entityId>:<propertyKey>` — see §5.1.1 (catalog / schema / fileset). + +**External `secretReferences`:** **`InMemorySecretsProvider` rejects them** (write-through only). +Use `secretBindings` / `setSecretBinding` with this provider. + +### 5.7 Backward compatibility + +- Empty registry (no `gravitino.secret.providers`) + all-string properties ⇒ today’s behavior. +- Migrate gradually: configure providers, set `secretReferences` / `secretBindings` on create so + selected keys store URN strings that match the recognition rule (§5.1). + +### 5.8 Credential refresh + +The in-memory provider has no login credentials and needs no refresh. + +### 5.9 Entity REST API (catalog / schema / fileset) + +The same secrets rules apply to **catalog**, **schema**, and **fileset** property bags. Today each +models `properties` as `map<string, string>`. Secrets management **keeps** those string maps, adds +optional **`secretReferences`** / **`secretBindings`** on **create**, and on **alter** adds +**`setSecretBinding`** / **`setSecretReference`** `@type`s (§5.9.4); persists URN strings, and +**omits** keys whose values match the URN recognition rule on GET/list (like today’s hidden +properties). + +Fileset catalogs may place Kerberos / cloud credentials on schema and fileset properties; JDBC and +other catalogs primarily use catalog properties. One REST contract covers all three so connectors +do not diverge. + +#### 5.9.1 Schema change + +| Layer | Today | v1 | +| -------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------ | +| REST `properties` | `map<string, string>` | **Unchanged** (create) | +| REST `secretReferences` | — | **New optional** on **create** (property key → **locator object**; server builds URN) | +| REST `secretBindings` | — | **New optional** on **create** (property key → provider name) | +| REST alter `updates` | existing `@type`s | **Add** `setSecretBinding` / `setSecretReference` (§5.9.4); `setProperty` stays plaintext string | +| REST list providers | — | **New** `GET /api/secrets/providers` — safe metadata only (§5.9.6) | +| Persistence | string map per entity | Same; secret values = **URN strings** (recognized by URN shape; drop delete via URN shape) | +| Tables | — | `catalog_meta.properties`, `schema_meta.properties`, `fileset_version_info.properties` | +| REST response `properties` | hidden keys stripped | Keys whose values match the URN recognition rule **omitted** | +| OpenAPI / DTOs | per-entity create/update DTOs | Create: add `secretReferences` + `secretBindings`; alter: new update `@type`s; + list-providers | + +Persisted example (write-through owned key): + +```json +{ + "jdbc-password": "urn:gravitino-secret:memory:catalog:10042:jdbc-password" +} +``` + +Write-through ownership is visible in the URN shape (`…:catalog:<id>:<propertyKey>`). Drop and +alter `removeProperty` call `deleteSecret` only for that shape (§5.5.2 C). + +**Affected endpoints** (entity paths unchanged; secrets behavior shared — §5.9.2–5.9.5, drop §5.5.2 C; +plus cluster list §5.9.6): + +| Method | Path | Entity / scope | +| -------- | ------------------------------------------------------------------------------ | -------------- | +| `GET` | `/api/secrets/providers` | Cluster | +| `POST` | `/metalakes/{metalake}/catalogs` | Catalog | +| `GET` | `/metalakes/{metalake}/catalogs/{catalog}` | Catalog | +| `GET` | `/metalakes/{metalake}/catalogs?details=true` | Catalog | +| `PUT` | `/metalakes/{metalake}/catalogs/{catalog}` | Catalog | +| `DELETE` | `/metalakes/{metalake}/catalogs/{catalog}` | Catalog | +| `POST` | `/metalakes/{metalake}/catalogs/{catalog}/schemas` | Schema | +| `GET` | `/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}` | Schema | +| `PUT` | `/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}` | Schema | +| `DELETE` | `/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}` | Schema | +| `POST` | `/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/filesets` | Fileset | +| `GET` | `/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/filesets/{fileset}` | Fileset | +| `PUT` | `/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/filesets/{fileset}` | Fileset | +| `DELETE` | `/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/filesets/{fileset}` | Fileset | + +List-with-details endpoints that return properties for schema/fileset follow the same omit-on-read +rules as catalog list. + +#### 5.9.2 Create (request) + +**`properties`** and **`secretBindings`** are `map<string, string>`. +**`secretReferences`** is `map<string, object>` (same for catalog, schema, and fileset create bodies). + +**Locator object** (each `secretReferences` value — shared by create and by alter +`setSecretReference`): + +| Field | Required | Meaning | +| ------------ | -------- | ------------------------------------------------------------------------------------------------------------ | +| `provider` | Yes | Registered `provider_name` | +| `attributes` | No | Provider-specific locator keys (`map<string, string>`). Empty / omitted ⇒ empty map. Never a raw URN string. | + +**`attributes`** are provider-specific. The REST schema stays the same for every provider; each +`GravitinoSecretProvider` documents required keys. **`InMemorySecretsProvider` does not use +`attributes`** (write-through only). + +The Gravitino **property key** (map key on create, or `property` on alter) is bound by the server +when building write-through URNs (and as the last segment of any built URN). + +Server builds: + +```text +urn:gravitino-secret:<provider>:<type-specific-identifier> +``` + +| Situation | Server behavior | +| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Key **not** in either secret map | Persist `properties` value as plain **string** (legacy / non-secret). | +| Key in **`secretReferences`** | Validate locator; **build URN** (must end with the property key); persist **URN string**; key **must not** be in `properties`; **do not** `readSecret` on write. | +| Key in **`secretBindings`** | `properties[key]` required; plaintext (not `******`) → `writeSecret` via named provider; persist **returned URN string**. | +| Key in **both** `secretReferences` and `secretBindings` | **Reject**. | +| Key in `secretReferences` **and** also present in `properties` | **Reject**. | +| Key in `secretBindings`, value in `properties` is `******` | **Reject** (no existing value to preserve). | +| Key in `secretBindings` but missing from `properties` | **Reject**. | +| `secretBindings` / locator `provider` not in `gravitino.secret.providers` | **Reject**. | +| Locator `attributes` value is a raw `urn:gravitino-secret:...` string | **Reject** — use locator attributes, not a client-built URN. | +| Client sends a raw `urn:gravitino-secret:...` string as `secretReferences` value | **Reject** in v1 — use the locator object (server builds the URN). | + +**Write-through example** (`secretBindings` + plaintext in `properties`): + +```json +{ + "name": "mysql_staging", + "type": "relational", + "provider": "jdbc-mysql", + "secretBindings": { + "jdbc-password": "memory" + }, + "properties": { + "jdbc-url": "jdbc:mysql://staging.example.com:3306/app", + "jdbc-driver": "com.mysql.cj.jdbc.Driver", + "jdbc-user": "app", + "jdbc-password": "S3cret!Passw0rd" + } +} +``` + +Only `jdbc-password` is write-through; `jdbc-url` and `jdbc-user` stay plain strings. + +**External reference:** `secretReferences` / `setSecretReference` are part of the REST contract +for providers that support locators. **`InMemorySecretsProvider` rejects them**; use +`secretBindings` / `setSecretBinding` instead (examples below and in §5.9.5). + +#### 5.9.3 GET and list (response) + +Responses **never** include resolved secret material or URNs. Entity DTOs keep +`properties` as `Map<String, String>` (catalog / schema / fileset). + +| Persisted value | GET / list response | +| ---------------------------------------------------------- | -------------------------------------------------------------------- | +| Value matches the URN recognition rule (§5.1) | **Omit** the key (same strip as today’s `PropertiesMetadata.hidden`) | +| Key is `PropertiesMetadata.hidden` (legacy plaintext path) | **Omit** the key (unchanged today) | +| Other non-secret string | Return the string unchanged | + +Legacy entities with plaintext secrets keep connector `PropertiesMetadata.hidden` strip behavior. +URN-shaped values are omitted by the recognition rule even when the key is not in a connector +hidden list. + +#### 5.9.4 Alter (request) + +Entity alter (`PUT` catalog / schema / fileset) keeps today’s **`updates`** array. Secrets on alter +use **two new `@type`s** (request body stays `{ "updates": [ ... ] }` only; **no** create-style +sibling maps). Existing **`setProperty`** stays a **string** `value` (plaintext only). + +**Common fields** (every update item has `@type` + `property`; other fields depend on `@type`): + +| Field | Required | Meaning | +| ---------- | -------- | ------------- | +| `@type` | Yes | Discriminator | +| `property` | Yes | Property key | + +| `@type` | Fields (flat; not nested under `value`) | Behavior | +| -------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `setProperty` | `value` (**string** plaintext) | Today’s plaintext set. If the **current** value matches the URN recognition rule, in-place `writeSecret` via provider in the current URN; persist new URN | +| `setSecretBinding` | `provider` (instance name) + `value` (plaintext string) | Write-through bind/re-bind (`writeSecret`); persist returned URN | +| `setSecretReference` | `provider` (instance name) + `attributes` (`map<string,string>`; same locator as §5.9.2) | External ref; server builds URN from locator (must end with the property key). Required `attributes` keys are provider-defined. | + +| Rule | Behavior | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `setSecretBinding` missing `provider` / string `value`, or `value` is `******` | **Reject** | +| `setSecretReference` missing `provider` | **Reject** | +| `setSecretReference` `attributes` value is a raw `urn:gravitino-secret:...` string | **Reject** — use locator attributes, not a client-built URN | +| `setSecretReference` missing attributes required by the selected provider | **Reject** | +| `provider` unknown | **Reject** | +| `setProperty` `value` is `******` | **Reject** | +| `setProperty` `value` is `urn:gravitino-secret:...` | **Reject** in v1 — use `setSecretReference` or `setSecretBinding` | +| `removeProperty` on a secret key | Remove from properties; if the **current** value is write-through-shaped for this entity (§5.5.2 C), best-effort `deleteSecret(urn)` (same rule as entity drop). External-ref / other URN shapes: remove property only — **do not** `deleteSecret` | +| Other `@type`s (`rename` / `updateComment` / …) | Unchanged | + +OpenAPI: add `SetSecretBindingRequest` / `SetSecretReferenceRequest` (and schema/fileset +equivalents) to the catalog-update oneOf. Example: TC-3 below. + +#### 5.9.5 API test cases (create + alter) + +All examples use metalake `prod`. HTTP **200** on success; response bodies omitted (URN-shaped +secret keys omitted on GET/list — §5.9.3). Each case shows **request** + persisted **DB `properties`**. + +- **TC-1–TC-2** — `POST …/catalogs` (create) +- **TC-3** — `PUT …/catalogs/{catalog}` (alter; `setSecretBinding` in `updates`) + +Reject cases follow §5.9.2 / §5.9.4 and existing catalog semantics — no separate fixtures. + +--- + +**TC-1 — Create: write-through via `secretBindings` (200)** + +Request: + +```json +{ + "name": "mysql_staging", + "type": "relational", + "provider": "jdbc-mysql", + "comment": "Staging MySQL catalog", + "secretBindings": { + "jdbc-password": "memory" + }, + "properties": { + "jdbc-url": "jdbc:mysql://staging.example.com:3306/app", + "jdbc-driver": "com.mysql.cj.jdbc.Driver", + "jdbc-user": "app", + "jdbc-password": "S3cret!Passw0rd" + } +} +``` + +DB `properties` (illustrative returned URN): + +```json +{ + "jdbc-url": "jdbc:mysql://staging.example.com:3306/app", + "jdbc-driver": "com.mysql.cj.jdbc.Driver", + "jdbc-user": "app", + "jdbc-password": "urn:gravitino-secret:memory:catalog:10042:jdbc-password" +} +``` + +--- + +**TC-2 — Create: legacy plaintext, no `secretReferences` / `secretBindings` (200)** + +Request: + +```json +{ + "name": "mysql_legacy", + "type": "relational", + "provider": "jdbc-mysql", + "comment": "Legacy plaintext catalog", + "properties": { + "jdbc-url": "jdbc:mysql://legacy.example.com:3306/app", + "jdbc-driver": "com.mysql.cj.jdbc.Driver", + "jdbc-user": "app", + "jdbc-password": "S3cret!Passw0rd" + } +} +``` + +DB `properties` (plaintext): + +```json +{ + "jdbc-url": "jdbc:mysql://legacy.example.com:3306/app", + "jdbc-driver": "com.mysql.cj.jdbc.Driver", + "jdbc-user": "app", + "jdbc-password": "S3cret!Passw0rd" +} +``` + +--- + +**TC-3 — Alter: write-through via `setSecretBinding` (200)** + +Request (flat `provider` + plaintext `value`): + +```json +{ + "updates": [ + { + "@type": "setSecretBinding", + "property": "jdbc-password", + "provider": "memory", + "value": "S3cret!Passw0rd" + } + ] +} +``` + +DB `properties` (illustrative write-through URN): + +```json +{ + "jdbc-url": "jdbc:mysql://staging.example.com:3306/app", + "jdbc-driver": "com.mysql.cj.jdbc.Driver", + "jdbc-user": "app", + "jdbc-password": "urn:gravitino-secret:memory:catalog:10042:jdbc-password" +} +``` + +#### 5.9.6 List secrets providers (request / response) + +Create/alter require clients to send a **registered instance name** (`secretBindings` values, +locator / binding-object `provider`). Unknown names are **Reject**. Web UI and API clients +therefore need discovery — without hardcoding names from ops docs. + +**Registration** stays file-based (§8): no REST create/update/delete of providers. +**Discovery** is a read-only cluster-scoped endpoint (not under a metalake path): + +```http +GET /api/secrets/providers Review Comment: What is the access control/privilege for this endpoint? -- 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]
