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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 8cbd3847d6 [docs] Add REST management OpenAPI (#9406)
8cbd3847d6 is described below

commit 8cbd3847d646b1c40e380f53b68a0cd97880b02e
Author: Jingsong Lee <[email protected]>
AuthorDate: Wed Aug 26 16:50:38 2026 +0800

    [docs] Add REST management OpenAPI (#9406)
---
 docs/README.md                            |   2 +-
 docs/docs/concepts/rest/index.md          |   3 +-
 docs/docs/concepts/rest/management-api.md | 232 ++++++++
 docs/scripts/validate-rest-openapi.js     | 897 +++++++++++++++++++++++++-----
 docs/sidebars.js                          |   3 +-
 docs/static/rest-management-open-api.yaml | 852 ++++++++++++++++++++++++++++
 6 files changed, 1853 insertions(+), 136 deletions(-)

diff --git a/docs/README.md b/docs/README.md
index 67d65ae18e..5e7ba5586c 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -22,7 +22,7 @@ The site will be available at 
http://localhost:3000/docs/master/.
 ## Build
 
 ```bash
-# Validate the REST Catalog OpenAPI contract
+# Validate the REST OpenAPI contracts
 yarn test:rest-openapi
 
 # Production build
diff --git a/docs/docs/concepts/rest/index.md b/docs/docs/concepts/rest/index.md
index bd8ada4b67..57a3283831 100644
--- a/docs/docs/concepts/rest/index.md
+++ b/docs/docs/concepts/rest/index.md
@@ -63,4 +63,5 @@ RESTCatalog supports multiple access authentication methods, 
including the follo
 
 ## REST Open API
 
-See [REST API](./rest-api).
+See [REST Catalog API](./rest-api) for catalog operations and
+[REST Management API](./management-api) for permission and data-policy 
management.
diff --git a/docs/docs/concepts/rest/management-api.md 
b/docs/docs/concepts/rest/management-api.md
new file mode 100644
index 0000000000..95892fa8a2
--- /dev/null
+++ b/docs/docs/concepts/rest/management-api.md
@@ -0,0 +1,232 @@
+---
+title: "REST Management API"
+hide_table_of_contents: true
+---
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+The REST Management API is an experimental OpenAPI 3.1 control-plane extension 
for object
+privileges, row filters, and column masks in a Paimon REST Catalog. Its 
current contract version is
+`1.0` and may evolve incompatibly while the design is being validated.
+
+`RESTCatalog` exposes `permissionManagement()` and `policyManagement()` 
directly. These methods are
+intentionally not part of the generic `Catalog` interface. Other catalog 
implementations do not
+expose this management contract.
+
+## Catalog addressing
+
+All management endpoints use the opaque `prefix` returned by the REST Catalog 
config endpoint. It
+is not a catalog name in a payload and is independent of local engine catalog 
aliases.
+
+```
+GET  /v1/{prefix}/permissions
+POST /v1/{prefix}/permissions/grant
+POST /v1/{prefix}/permissions/revoke
+
+GET    /v1/{prefix}/databases/{database}/tables/{table}/policies
+POST   /v1/{prefix}/databases/{database}/tables/{table}/policies
+POST   /v1/{prefix}/databases/{database}/tables/{table}/policies/drop
+```
+
+Policies are currently attached only to tables. The path is the attachment 
identity, so policy
+request bodies do not repeat a catalog, database, table, or resource type. 
Catalog- and
+database-level matching can be added later with explicit matching semantics 
instead of implied
+path inheritance.
+
+The complete wire contract is available in
+[`rest-management-open-api.yaml`](/rest-management-open-api.yaml).
+
+## Privileges and policies are independent
+
+A permission grants one access on one resource to one principal. A data policy 
restricts rows or
+columns visible through an already-authorized read. Creating a policy never 
grants `SELECT`, and
+revoking `SELECT` does not delete policies.
+
+This separation also defines the expected query path:
+
+1. The server evaluates object privileges.
+2. The server resolves all row-filter and column-masking policies applicable 
to the caller.
+3. The existing REST Catalog table authorization endpoint returns the stored 
Paimon predicate and
+   column transforms to the engine.
+4. The engine applies those restrictions when planning the scan.
+
+Management payloads use the same serialized Paimon `Predicate` and `Transform` 
representation as
+the existing `AuthTableQueryResponse`. Policy conflict detection, schema 
validation, and principal
+resolution are server responsibilities.
+
+## Permission model
+
+Permission resources are structured objects:
+
+| Resource type | Required locator | Example |
+| --- | --- | --- |
+| `CATALOG` | none | `{"type":"CATALOG"}` |
+| `CATALOG_ALL` | none | `{"type":"CATALOG_ALL"}` |
+| `DATABASE` | `database` | `{"type":"DATABASE","database":"sales"}` |
+| `DATABASE_ALL` | `database` | `{"type":"DATABASE_ALL","database":"sales"}` |
+| `TABLE` | `database`, `table` | 
`{"type":"TABLE","database":"sales","table":"orders"}` |
+| `COLUMN` | `database`, `table` | 
`{"type":"COLUMN","database":"sales","table":"orders"}` |
+| `FUNCTION` | `database`, `function` | 
`{"type":"FUNCTION","database":"sales","function":"calculate_tax"}` |
+| `VIEW` | `database`, `view` | 
`{"type":"VIEW","database":"sales","view":"daily_orders"}` |
+
+Principals are opaque, canonical strings that are globally unique in the 
server namespace. Their
+format is server-defined and may encode a user, group, role, or service 
identity, for example
+`role:analyst` or an external identity-provider ARN. Principal type and 
membership resolution are
+server responsibilities. Access values are limited to 32 characters and 
principals to 128
+characters. An implementation may resolve wire locators and principals to 
different stable
+persistence identifiers; those internal ids are not exposed by this API.
+
+The built-in accesses use a common data-authorization vocabulary. Creation 
accesses intentionally
+use their persisted names without underscores:
+
+| Access | Meaning |
+| --- | --- |
+| `ALL` | All accesses applicable to the resource. |
+| `CREATEDATABASE` | Create a database in a catalog. |
+| `DESCRIBE` | Read database metadata or select the current database. |
+| `ALTER` | Modify resource metadata. |
+| `DROP` | Drop the resource. |
+| `CREATETABLE` | Create a table in a database. |
+| `CREATEFUNCTION` | Create a function in a database. |
+| `CREATEVIEW` | Create a view in a database. |
+| `LIST` | List resources in a database. |
+| `SELECT` | Read table or view data, or use a function. |
+| `UPDATE` | Write table data, including insert, update, and delete 
operations. |
+| `GRANT` | Grant or revoke assignments on the resource. |
+
+Java helpers accept access names case-insensitively and normalize them before 
sending. The REST
+wire format uses upper case. Built-in accesses are resource-specific:
+
+| Resource | Accesses |
+| --- | --- |
+| `CATALOG` | `ALL`, `ALTER`, `DROP`, `GRANT`, `CREATEDATABASE` |
+| `CATALOG_ALL` | `ALL`, `DESCRIBE`, `ALTER`, `DROP`, `GRANT`, `CREATETABLE`, 
`CREATEVIEW`, `CREATEFUNCTION`, `LIST`, `SELECT`, `UPDATE` |
+| `DATABASE` | `ALL`, `DESCRIBE`, `ALTER`, `DROP`, `GRANT`, `CREATETABLE`, 
`CREATEVIEW`, `CREATEFUNCTION`, `LIST` |
+| `DATABASE_ALL` | `ALL`, `SELECT`, `UPDATE`, `ALTER`, `DROP`, `GRANT` |
+| `TABLE` | `ALL`, `SELECT`, `UPDATE`, `ALTER`, `DROP`, `GRANT` |
+| `COLUMN` | `SELECT` |
+| `VIEW` | `ALL`, `SELECT`, `ALTER`, `DROP`, `GRANT` |
+| `FUNCTION` | `ALL`, `SELECT`, `ALTER`, `DROP`, `GRANT` |
+
+An assignment identity is `resource`, `access`, and `principal`. Granting the 
same identity replaces
+its expiry, and revocation is idempotent. `CATALOG`, `DATABASE`, `TABLE`, 
`COLUMN`, `VIEW`, and
+`FUNCTION` apply only to the exact referenced resource. `CATALOG_ALL` is an 
explicit scope over the
+configured catalog's database, table, view, and function descendants; 
`DATABASE_ALL` is an explicit
+scope over the named database's table, view, and function descendants. These 
scope assignments also
+apply to descendants created later. They remain direct assignments in listing 
responses; the server
+does not synthesize inherited assignments. Resolving group membership and role 
inheritance remains a
+server responsibility.
+
+### Column permissions
+
+A column permission uses a `COLUMN` resource whose locator is the containing 
table, `SELECT`
+access, and one `columns` object. Exactly one non-empty list is allowed:
+
+- `columnNames` is an allowlist. Only the named top-level columns are readable.
+- `excludedColumnNames` is a denylist. Every current top-level column except 
the named columns is
+  readable.
+
+For example, this assignment allows only `order_id` and `region`:
+
+```json
+{
+  "resource": {
+    "type": "COLUMN",
+    "database": "sales",
+    "table": "orders"
+  },
+  "access": "SELECT",
+  "principal": "role:analyst",
+  "columns": {
+    "columnNames": ["order_id", "region"]
+  }
+}
+```
+
+The assignment identity remains `(resource, access, principal)`; `columns` is 
not part of the
+identity. Granting the same identity replaces the entire previous allowlist or 
denylist rather than
+merging individual names. Revocation therefore omits `columns` and removes the 
whole column
+assignment.
+
+All named columns must exist when granted, and the table must enforce query 
authorization. A server
+may enable `query-auth.enabled` atomically with the grant; otherwise it must 
reject the grant. Column
+names refer only to top-level fields. For every effective caller principal, 
applicable column ranges
+are intersected. If any applicable range rejects a selected column, the query 
fails rather than
+silently dropping that column.
+
+Schema evolution keeps the assignment attached to the stable table identity. 
Renaming a referenced
+column updates its stored name. Dropping a referenced column removes it from 
the range; if that
+would leave the stored list empty, the assignment is removed. An allowlist 
denies columns added
+later, while a denylist allows them, so allowlists are safer when new columns 
may contain sensitive
+data.
+
+`expireTime`, when present, is an exclusive upper bound evaluated against the 
REST server clock.
+At `now >= expireTime`, the assignment must not authorize access. Expired 
direct assignments may
+remain visible in listings until server cleanup. Timestamps must not be more 
precise than
+milliseconds; the wire value uses UTC `Z` and contains at most three 
fractional digits.
+
+Resource objects in this API are wire locators, not persistence identities. 
Servers must bind direct
+assignments to a stable internal resource identity: renaming a database, 
table, function, or view
+retains its assignments and subsequent responses use the new locator; dropping 
it removes its direct
+assignments; recreating the same locator does not restore them.
+
+## Data policy model
+
+A data policy is attached directly to one table and one principal. It applies 
whenever that
+principal is effective for the caller after the server resolves group and role 
membership. A
+principal can have at most one row filter on a table and at most one column 
mask on each table
+column. A row-filter identity is `(table, ROW_FILTER, principal)`; a 
column-mask identity is
+`(table, COLUMN_MASKING, principal, onColumn)`.
+
+Each policy contains exactly one typed definition:
+
+| Definition | Required fields | Result |
+| --- | --- | --- |
+| `rowFilter` | `predicate` | One serialized Paimon `Predicate`, applied to 
every scan. |
+| `columnMask` | `onColumn`, `transform` | One serialized Paimon `Transform` 
whose result replaces the protected column. |
+
+The common field is one `principal`. `rowFilter.predicate` maps directly to 
one entry in
+`AuthTableQueryResponse.filter`. `columnMask.onColumn` and 
`columnMask.transform` map directly to
+one key and value in `AuthTableQueryResponse.columnMasking`. Each JSON value 
is limited to 60 KiB
+in UTF-8. This is Paimon's versioned serialization format rather than SQL text 
or a portable policy
+DSL; clients and servers must use compatible Paimon versions.
+
+Policy creation must be rejected unless all of these conditions hold:
+
+1. The target database and table exist.
+2. The table has `query-auth.enabled=true`; otherwise a stored policy could be 
silently bypassed.
+3. The referenced principal exists.
+4. The predicate or transform is recognized by the server, deserializes to a 
non-null Paimon
+   object, and is canonicalized before storage.
+5. Every referenced field and `onColumn` exists in the target table, and a 
transform's output type
+   matches its protected column.
+
+These invariants continue to apply for the whole table lifecycle. Servers must 
bind policies to a
+stable table identity, preserve that binding across table renames, and remove 
the policies when the
+table is dropped. A table with policies must reject changes that disable 
`query-auth.enabled` or
+remove or rename a protected or referenced column, unless the policy update 
and schema change are
+performed atomically. If an implementation persists all masks for one 
principal in one document,
+creating or dropping one column mask must atomically preserve masks for other 
columns.
+
+At authorization time, all applicable row filters must be combined with 
logical `AND`. More than
+one applicable column mask targeting the same column must fail closed. An 
invalid, unsupported, or
+schema-incompatible predicate or transform must also fail closed rather than 
omit a restriction.
+
+This experimental contract deliberately does not define governed tags, 
catalog/database policy
+inheritance, or tag-driven matching. Those features need explicit match 
conditions and conflict
+rules before being added.
diff --git a/docs/scripts/validate-rest-openapi.js 
b/docs/scripts/validate-rest-openapi.js
index 62e3598cf5..c4846178fb 100644
--- a/docs/scripts/validate-rest-openapi.js
+++ b/docs/scripts/validate-rest-openapi.js
@@ -20,8 +20,7 @@ const fs = require('fs');
 const path = require('path');
 const yaml = require('js-yaml');
 
-const specPath = path.resolve(__dirname, '..', 'static', 
'rest-catalog-open-api.yaml');
-const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
+const HTTP_METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 
'options', 'trace']);
 
 function check(condition, message) {
   if (!condition) {
@@ -33,82 +32,181 @@ function decodePointerSegment(segment) {
   return segment.replace(/~1/g, '/').replace(/~0/g, '~');
 }
 
-function resolveLocalRef(ref) {
-  check(ref.startsWith('#/'), `Only local OpenAPI references are supported, 
found: ${ref}`);
-  return ref
-    .slice(2)
-    .split('/')
-    .map(decodePointerSegment)
-    .reduce((current, segment) => {
-      check(
-        current && Object.prototype.hasOwnProperty.call(current, segment),
-        `Unresolved OpenAPI reference: ${ref}`,
-      );
-      return current[segment];
-    }, spec);
-}
+function validateCommon(fileName) {
+  const specPath = path.resolve(__dirname, '..', 'static', fileName);
+  const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
 
-function visit(value) {
-  if (Array.isArray(value)) {
-    value.forEach(visit);
-    return;
+  function checkSpec(condition, message) {
+    check(condition, `${fileName}: ${message}`);
   }
-  if (!value || typeof value !== 'object') {
-    return;
+
+  function resolveLocalRef(ref) {
+    checkSpec(ref.startsWith('#/'), `Only local OpenAPI references are 
supported, found: ${ref}`);
+    return ref
+      .slice(2)
+      .split('/')
+      .map(decodePointerSegment)
+      .reduce((current, segment) => {
+        checkSpec(
+          current && Object.prototype.hasOwnProperty.call(current, segment),
+          `Unresolved OpenAPI reference: ${ref}`,
+        );
+        return current[segment];
+      }, spec);
   }
-  check(!Object.prototype.hasOwnProperty.call(value, 'nullable'), 'OpenAPI 3.1 
schemas must not use nullable');
-  if (typeof value.$ref === 'string') {
-    resolveLocalRef(value.$ref);
+
+  function visit(value) {
+    if (Array.isArray(value)) {
+      value.forEach(visit);
+      return;
+    }
+    if (!value || typeof value !== 'object') {
+      return;
+    }
+    checkSpec(
+      !Object.prototype.hasOwnProperty.call(value, 'nullable'),
+      'OpenAPI 3.1 schemas must not use nullable',
+    );
+    if (typeof value.$ref === 'string') {
+      resolveLocalRef(value.$ref);
+    }
+    Object.values(value).forEach(visit);
   }
-  Object.values(value).forEach(visit);
-}
 
-function schema(name) {
-  const value = spec.components && spec.components.schemas && 
spec.components.schemas[name];
-  check(value, `Missing OpenAPI schema: ${name}`);
-  return value;
-}
+  function schema(name) {
+    const value = spec.components && spec.components.schemas && 
spec.components.schemas[name];
+    checkSpec(value, `Missing OpenAPI schema: ${name}`);
+    return value;
+  }
 
-function requireProperties(schemaName, names) {
-  const properties = schema(schemaName).properties || {};
-  names.forEach((name) => {
-    check(properties[name], `Schema ${schemaName} is missing property: 
${name}`);
-  });
-  return properties;
-}
+  function requireProperties(schemaName, names) {
+    const properties = schema(schemaName).properties || {};
+    names.forEach((name) => {
+      checkSpec(properties[name], `Schema ${schemaName} is missing property: 
${name}`);
+    });
+    return properties;
+  }
+
+  function requireRequiredProperties(schemaName, names) {
+    const required = schema(schemaName).required || [];
+    names.forEach((name) => {
+      checkSpec(required.includes(name), `Schema ${schemaName} must require 
property: ${name}`);
+    });
+  }
 
-function requireTypedIntegerProperties(schemaName, names) {
-  const properties = requireProperties(schemaName, names);
-  names.forEach((name) => {
-    check(
-      properties[name].type === 'integer' && properties[name].format === 
'int64',
-      `Schema ${schemaName}.${name} must be an int64 integer`,
+  function requireTypedIntegerProperties(schemaName, names) {
+    const properties = requireProperties(schemaName, names);
+    names.forEach((name) => {
+      checkSpec(
+        properties[name].type === 'integer' && properties[name].format === 
'int64',
+        `Schema ${schemaName}.${name} must be an int64 integer`,
+      );
+    });
+  }
+
+  function requireSchemaReference(schemaName, composition, 
referencedSchemaName) {
+    const references = schema(schemaName)[composition] || [];
+    const expected = `#/components/schemas/${referencedSchemaName}`;
+    checkSpec(
+      references.some((reference) => reference.$ref === expected),
+      `Schema ${schemaName}.${composition} is missing reference: ${expected}`,
     );
-  });
-}
+  }
+
+  function validatePathParameters(resourcePath, pathItem, operation) {
+    const templateNames = Array.from(resourcePath.matchAll(/\{([^}]+)\}/g), 
(match) => match[1]);
+    const parameters = [...(pathItem.parameters || []), 
...(operation.parameters || [])].map(
+      (parameter) => (parameter.$ref ? resolveLocalRef(parameter.$ref) : 
parameter),
+    );
+    const pathParameters = parameters.filter((parameter) => parameter.in === 
'path');
+    templateNames.forEach((name) => {
+      const parameter = pathParameters.find((candidate) => candidate.name === 
name);
+      checkSpec(parameter, `Path ${resourcePath} is missing path parameter: 
${name}`);
+      checkSpec(
+        parameter.required === true,
+        `Path parameter ${resourcePath}.${name} must be required`,
+      );
+    });
+    pathParameters.forEach((parameter) => {
+      checkSpec(
+        templateNames.includes(parameter.name),
+        `Path ${resourcePath} declares unused path parameter: 
${parameter.name}`,
+      );
+    });
+  }
 
-function requireSchemaReference(schemaName, composition, referencedSchemaName) 
{
-  const references = schema(schemaName)[composition] || [];
-  const expected = `#/components/schemas/${referencedSchemaName}`;
-  check(
-    references.some((reference) => reference.$ref === expected),
-    `Schema ${schemaName}.${composition} is missing reference: ${expected}`,
+  checkSpec(spec.openapi === '3.1.1', `Expected OpenAPI 3.1.1, found: 
${spec.openapi}`);
+  checkSpec(
+    spec.paths && spec.components && spec.components.schemas,
+    'Incomplete OpenAPI document',
   );
+  visit(spec);
+
+  const operations = new Map();
+  for (const [resourcePath, pathItem] of Object.entries(spec.paths)) {
+    for (const [method, operation] of Object.entries(pathItem)) {
+      if (!HTTP_METHODS.has(method)) {
+        continue;
+      }
+      validatePathParameters(resourcePath, pathItem, operation);
+      checkSpec(
+        operation.operationId,
+        `Operation ${method.toUpperCase()} ${resourcePath} has no operationId`,
+      );
+      checkSpec(
+        !operations.has(operation.operationId),
+        `Duplicate operationId: ${operation.operationId}`,
+      );
+      operations.set(operation.operationId, operation);
+    }
+  }
+
+  function requireOperation(operationId) {
+    const operation = operations.get(operationId);
+    checkSpec(operation, `Missing operationId: ${operationId}`);
+    return operation;
+  }
+
+  function requireResponses(operationId, statusCodes) {
+    const responses = requireOperation(operationId).responses || {};
+    statusCodes.forEach((statusCode) => {
+      checkSpec(
+        responses[statusCode],
+        `Operation ${operationId} is missing response: ${statusCode}`,
+      );
+    });
+  }
+
+  return {
+    spec,
+    operations,
+    schema,
+    requireOperation,
+    requireProperties,
+    requireRequiredProperties,
+    requireTypedIntegerProperties,
+    requireSchemaReference,
+    requireResponses,
+    checkSpec,
+  };
 }
 
-function requireArrayOfIdentifiers(schemaName, propertyName) {
-  const properties = requireProperties(schemaName, [propertyName]);
-  check(properties[propertyName].type === 'array', `Schema 
${schemaName}.${propertyName} must be an array`);
-  check(
+function requireArrayOfIdentifiers(contract, schemaName, propertyName) {
+  const properties = contract.requireProperties(schemaName, [propertyName]);
+  contract.checkSpec(
+    properties[propertyName].type === 'array',
+    `Schema ${schemaName}.${propertyName} must be an array`,
+  );
+  contract.checkSpec(
     properties[propertyName].items &&
       properties[propertyName].items.$ref === 
'#/components/schemas/Identifier',
     `Schema ${schemaName}.${propertyName} items must reference Identifier`,
   );
 }
 
-function requireNullableStringProperty(schemaName, propertyName) {
-  const property = requireProperties(schemaName, [propertyName])[propertyName];
-  check(
+function requireNullableStringProperty(contract, schemaName, propertyName) {
+  const property = contract.requireProperties(schemaName, 
[propertyName])[propertyName];
+  contract.checkSpec(
     Array.isArray(property.type) &&
       property.type.includes('string') &&
       property.type.includes('null'),
@@ -116,85 +214,618 @@ function requireNullableStringProperty(schemaName, 
propertyName) {
   );
 }
 
-check(spec.openapi === '3.1.1', `Expected OpenAPI 3.1.1, found: 
${spec.openapi}`);
-check(spec.paths && spec.components && spec.components.schemas, 'Incomplete 
OpenAPI document');
-visit(spec);
+function requireExactEnum(contract, schemaName, expectedValues) {
+  const actualValues = contract.schema(schemaName).enum || [];
+  contract.checkSpec(
+    actualValues.length === expectedValues.length &&
+      expectedValues.every((value) => actualValues.includes(value)),
+    `Schema ${schemaName} must define enum values: ${expectedValues.join(', 
')}`,
+  );
+}
 
-const operationIds = new Set();
-for (const pathItem of Object.values(spec.paths)) {
-  for (const operation of Object.values(pathItem)) {
-    if (!operation || typeof operation !== 'object' || !operation.operationId) 
{
-      continue;
-    }
-    check(!operationIds.has(operation.operationId), `Duplicate operationId: 
${operation.operationId}`);
-    operationIds.add(operation.operationId);
-  }
+function validateCatalogOpenApi() {
+  const contract = validateCommon('rest-catalog-open-api.yaml');
+  [
+    'getConfig',
+    'createDatabase',
+    'getDatabase',
+    'alterDatabase',
+    'dropDatabase',
+    'createTable',
+    'getTable',
+    'alterTable',
+    'dropTable',
+  ].forEach(contract.requireOperation);
+
+  contract.requireProperties('ConfigResponse', ['defaults', 'overrides']);
+  contract.requireProperties('CreateDatabaseRequest', ['name', 'options']);
+  contract.requireProperties('AlterDatabaseRequest', ['removals', 'updates']);
+  contract.requireProperties('CreateTableRequest', ['identifier', 'schema']);
+  contract.requireProperties('AlterTableRequest', ['changes']);
+  contract.requireProperties('Identifier', ['database', 'object']);
+  contract.requireProperties('Schema', [
+    'fields',
+    'partitionKeys',
+    'primaryKeys',
+    'options',
+    'comment',
+  ]);
+  contract.requireProperties('DataField', ['id', 'name', 'type', 
'description', 'defaultValue']);
+
+  contract.requireSchemaReference('DataType', 'oneOf', 'VectorType');
+  contract.requireProperties('VectorType', ['type', 'element', 'length']);
+  contract.requireSchemaReference('SchemaChange', 'anyOf', 'DropPrimaryKey');
+  contract.checkSpec(
+    contract.schema('BaseSchemaChange').discriminator.mapping.dropPrimaryKey 
===
+      '#/components/schemas/DropPrimaryKey',
+    'BaseSchemaChange discriminator is missing dropPrimaryKey',
+  );
+  const dropPrimaryKey = contract.requireProperties('DropPrimaryKey', 
['action']);
+  contract.checkSpec(
+    dropPrimaryKey.action.const === 'dropPrimaryKey',
+    'Schema DropPrimaryKey.action must be dropPrimaryKey',
+  );
+  contract.checkSpec(
+    contract.schema('BaseInstant').discriminator.propertyName === 'type',
+    'BaseInstant discriminator must use the JSON field type',
+  );
+
+  const updateViewComment = contract.requireProperties('UpdateViewComment', 
['action', 'comment']);
+  contract.checkSpec(
+    !updateViewComment.key,
+    'Schema UpdateViewComment must use comment instead of key',
+  );
+  ['UpdateComment', 'UpdateViewComment', 
'UpdateFunctionComment'].forEach((schemaName) =>
+    requireNullableStringProperty(contract, schemaName, 'comment'),
+  );
+
+  const errorResourceTypes =
+    contract.requireProperties('ErrorResponse', 
['resourceType']).resourceType.enum || [];
+  ['FUNCTION', 'DEFINITION'].forEach((resourceType) => {
+    contract.checkSpec(
+      errorResourceTypes.includes(resourceType),
+      `Schema ErrorResponse.resourceType is missing value: ${resourceType}`,
+    );
+  });
+
+  requireArrayOfIdentifiers(contract, 'ListTablesGloballyResponse', 'tables');
+  requireArrayOfIdentifiers(contract, 'ListViewsGloballyResponse', 'views');
+  requireArrayOfIdentifiers(contract, 'ListFunctionsGloballyResponse', 
'functions');
+  contract.requireProperties('ListFunctionsGloballyResponse', 
['nextPageToken']);
+  const getFunctionProperties = 
contract.requireProperties('GetFunctionResponse', ['uuid']);
+  contract.checkSpec(
+    getFunctionProperties.uuid.type === 'string',
+    'Schema GetFunctionResponse.uuid must be a string',
+  );
+
+  ['GetDatabaseResponse', 'GetTableResponse', 'GetViewResponse', 
'GetFunctionResponse'].forEach(
+    (schemaName) => contract.requireTypedIntegerProperties(schemaName, 
['createdAt', 'updatedAt']),
+  );
+  return contract.operations.size;
 }
 
-[
-  'getConfig',
-  'createDatabase',
-  'getDatabase',
-  'alterDatabase',
-  'dropDatabase',
-  'createTable',
-  'getTable',
-  'alterTable',
-  'dropTable',
-].forEach((operationId) => {
-  check(operationIds.has(operationId), `Missing provider-facing operationId: 
${operationId}`);
-});
-
-requireProperties('ConfigResponse', ['defaults', 'overrides']);
-requireProperties('CreateDatabaseRequest', ['name', 'options']);
-requireProperties('AlterDatabaseRequest', ['removals', 'updates']);
-requireProperties('CreateTableRequest', ['identifier', 'schema']);
-requireProperties('AlterTableRequest', ['changes']);
-requireProperties('Identifier', ['database', 'object']);
-requireProperties('Schema', ['fields', 'partitionKeys', 'primaryKeys', 
'options', 'comment']);
-requireProperties('DataField', ['id', 'name', 'type', 'description', 
'defaultValue']);
-
-requireSchemaReference('DataType', 'oneOf', 'VectorType');
-requireProperties('VectorType', ['type', 'element', 'length']);
-requireSchemaReference('SchemaChange', 'anyOf', 'DropPrimaryKey');
-check(
-  schema('BaseSchemaChange').discriminator.mapping.dropPrimaryKey ===
-    '#/components/schemas/DropPrimaryKey',
-  'BaseSchemaChange discriminator is missing dropPrimaryKey',
-);
-const dropPrimaryKey = requireProperties('DropPrimaryKey', ['action']);
-check(
-  dropPrimaryKey.action.const === 'dropPrimaryKey',
-  'Schema DropPrimaryKey.action must be dropPrimaryKey',
-);
-check(
-  schema('BaseInstant').discriminator.propertyName === 'type',
-  'BaseInstant discriminator must use the JSON field type',
-);
+function validateManagementOpenApi() {
+  const contract = validateCommon('rest-management-open-api.yaml');
+  const operationIds = [
+    'listPermissions',
+    'grantPermission',
+    'revokePermission',
+    'listTablePolicies',
+    'createTablePolicy',
+    'dropTablePolicy',
+  ];
+  const resourcePaths = [
+    '/v1/{prefix}/permissions',
+    '/v1/{prefix}/permissions/grant',
+    '/v1/{prefix}/permissions/revoke',
+    '/v1/{prefix}/databases/{database}/tables/{table}/policies',
+    '/v1/{prefix}/databases/{database}/tables/{table}/policies/drop',
+  ];
 
-const updateViewComment = requireProperties('UpdateViewComment', ['action', 
'comment']);
-check(!updateViewComment.key, 'Schema UpdateViewComment must use comment 
instead of key');
-['UpdateComment', 'UpdateViewComment', 
'UpdateFunctionComment'].forEach((schemaName) =>
-  requireNullableStringProperty(schemaName, 'comment'),
-);
+  resourcePaths.forEach((resourcePath) =>
+    contract.checkSpec(
+      contract.spec.paths[resourcePath],
+      `Missing management path: ${resourcePath}`,
+    ),
+  );
+  [
+    '/v1/{prefix}/policies',
+    '/v1/{prefix}/databases/{database}/policies',
+    '/v1/{prefix}/databases/{database}/tables/{table}/policies/{policyName}',
+  ].forEach((resourcePath) =>
+    contract.checkSpec(
+      !contract.spec.paths[resourcePath],
+      `Policies must not be attachable outside tables: ${resourcePath}`,
+    ),
+  );
+  operationIds.forEach(contract.requireOperation);
+  ['listPermissions', 'grantPermission', 
'revokePermission'].forEach((operationId) =>
+    contract.requireResponses(operationId, [
+      '200',
+      '400',
+      '401',
+      '403',
+      '404',
+      '429',
+      '500',
+      '503',
+    ]),
+  );
+  contract.requireResponses('grantPermission', ['409']);
+  ['listTablePolicies', 'createTablePolicy'].forEach((operationId) =>
+    contract.requireResponses(operationId, [
+      '200',
+      '400',
+      '401',
+      '403',
+      '404',
+      '429',
+      '500',
+      '503',
+    ]),
+  );
+  contract.requireResponses('dropTablePolicy', [
+    '200',
+    '400',
+    '401',
+    '403',
+    '404',
+    '429',
+    '500',
+    '503',
+  ]);
+  contract.requireResponses('createTablePolicy', ['409']);
+  contract.checkSpec(
+    contract.spec.info.version === '1.0' &&
+      contract.spec.info.description.toLowerCase().includes('experimental'),
+    'The management contract must be versioned 1.0 and marked experimental',
+  );
+  contract.checkSpec(
+    !Object.prototype.hasOwnProperty.call(contract.spec, 'security'),
+    'The management contract must not require one deployment-specific 
authentication scheme',
+  );
 
-const errorResourceTypes = requireProperties('ErrorResponse', 
['resourceType']).resourceType.enum || [];
-['FUNCTION', 'DEFINITION'].forEach((resourceType) => {
-  check(
-    errorResourceTypes.includes(resourceType),
-    `Schema ErrorResponse.resourceType is missing value: ${resourceType}`,
+  const assignmentFields = ['resource', 'access', 'principal', 'columns', 
'expireTime'];
+  contract.requireProperties('PermissionAssignment', assignmentFields);
+  contract.requireRequiredProperties('PermissionAssignment', ['resource', 
'access', 'principal']);
+  const grantProperties = contract.requireProperties('GrantPermissionRequest', 
[
+    'resource',
+    'access',
+    'principal',
+    'columns',
+    'expireTime',
+  ]);
+  contract.requireRequiredProperties('GrantPermissionRequest', [
+    'resource',
+    'access',
+    'principal',
+  ]);
+  ['policy', 'grantOption'].forEach((field) =>
+    contract.checkSpec(
+      !grantProperties[field],
+      `Schema GrantPermissionRequest must omit field: ${field}`,
+    ),
+  );
+  const revokeProperties = 
contract.requireProperties('RevokePermissionRequest', [
+    'resource',
+    'access',
+    'principal',
+  ]);
+  contract.requireRequiredProperties('RevokePermissionRequest', [
+    'resource',
+    'access',
+    'principal',
+  ]);
+  ['expireTime', 'columns', 'policy', 'grantOption'].forEach((field) =>
+    contract.checkSpec(
+      !revokeProperties[field],
+      `Schema RevokePermissionRequest must omit field: ${field}`,
+    ),
   );
-});
 
-requireArrayOfIdentifiers('ListTablesGloballyResponse', 'tables');
-requireArrayOfIdentifiers('ListViewsGloballyResponse', 'views');
-requireArrayOfIdentifiers('ListFunctionsGloballyResponse', 'functions');
-requireProperties('ListFunctionsGloballyResponse', ['nextPageToken']);
-const getFunctionProperties = requireProperties('GetFunctionResponse', 
['uuid']);
-check(getFunctionProperties.uuid.type === 'string', 'Schema 
GetFunctionResponse.uuid must be a string');
+  const permissionList = contract.requireProperties('ListPermissionsResponse', 
[
+    'permissions',
+    'nextPageToken',
+  ]);
+  contract.checkSpec(
+    permissionList.permissions.type === 'array' &&
+      permissionList.permissions.items.$ref === 
'#/components/schemas/PermissionAssignment',
+    'ListPermissionsResponse.permissions must contain PermissionAssignment 
values',
+  );
+  contract.requireRequiredProperties('ListPermissionsResponse', 
['permissions']);
 
-['GetDatabaseResponse', 'GetTableResponse', 'GetViewResponse', 
'GetFunctionResponse'].forEach(
-  (schemaName) => requireTypedIntegerProperties(schemaName, ['createdAt', 
'updatedAt']),
-);
+  const principal = contract.schema('Principal');
+  contract.checkSpec(
+    principal.type === 'string' && principal.minLength === 1 && 
principal.maxLength === 128,
+    'Principal must be a non-empty string that fits the 128-character 
persistence identity',
+  );
+  contract.checkSpec(
+    !contract.spec.components.schemas.PrincipalRef &&
+      !contract.spec.components.schemas.PrincipalType &&
+      !contract.spec.components.parameters.PrincipalTypeQuery,
+    'Principal must not expose a separate reference object or type',
+  );
+  ['PermissionAssignment', 'GrantPermissionRequest', 
'RevokePermissionRequest'].forEach(
+    (schemaName) =>
+      contract.checkSpec(
+        contract.requireProperties(schemaName, ['principal']).principal.$ref 
===
+          '#/components/schemas/Principal',
+        `Schema ${schemaName}.principal must reference Principal`,
+      ),
+  );
+  contract.checkSpec(
+    contract.spec.components.parameters.PrincipalQuery.schema.$ref ===
+      '#/components/schemas/Principal',
+    'PrincipalQuery must reference Principal',
+  );
+  requireExactEnum(contract, 'ResourceType', [
+    'CATALOG',
+    'CATALOG_ALL',
+    'DATABASE',
+    'DATABASE_ALL',
+    'TABLE',
+    'COLUMN',
+    'FUNCTION',
+    'VIEW',
+  ]);
+  requireExactEnum(contract, 'PolicyType', ['ROW_FILTER', 'COLUMN_MASKING']);
+  Object.entries({
+    CatalogResource: 'CATALOG',
+    CatalogAllResource: 'CATALOG_ALL',
+    DatabaseResource: 'DATABASE',
+    DatabaseAllResource: 'DATABASE_ALL',
+    TableResource: 'TABLE',
+    ColumnResource: 'COLUMN',
+    FunctionResource: 'FUNCTION',
+    ViewResource: 'VIEW',
+    RowFilterPolicyIdentity: 'ROW_FILTER',
+    ColumnMaskPolicyIdentity: 'COLUMN_MASKING',
+    TablePolicyResource: 'TABLE',
+  }).forEach(([schemaName, expectedType]) => {
+    const typeProperty = contract.requireProperties(schemaName, ['type']).type;
+    contract.checkSpec(
+      typeProperty.type === 'string' && typeProperty.const === expectedType,
+      `${schemaName}.type must be a typed string constant`,
+    );
+  });
+
+  const permissionAccess = contract.schema('PermissionAccess');
+  const expectedAccesses = [
+    'ALL',
+    'CREATEDATABASE',
+    'DESCRIBE',
+    'ALTER',
+    'DROP',
+    'CREATETABLE',
+    'CREATEFUNCTION',
+    'CREATEVIEW',
+    'LIST',
+    'SELECT',
+    'UPDATE',
+    'GRANT',
+  ];
+  contract.checkSpec(
+    permissionAccess.type === 'string' &&
+      permissionAccess.enum.length === expectedAccesses.length &&
+      expectedAccesses.every((access) => 
permissionAccess.enum.includes(access)),
+    'PermissionAccess must define the complete data access enum',
+  );
+  contract.checkSpec(
+    permissionAccess.maxLength === 32,
+    'PermissionAccess must fit the 32-character persistence field',
+  );
+  ['PermissionAssignment', 'GrantPermissionRequest', 
'RevokePermissionRequest'].forEach(
+    (schemaName) =>
+      contract.checkSpec(
+        contract.requireProperties(schemaName, ['access']).access.$ref ===
+          '#/components/schemas/PermissionAccess',
+        `Schema ${schemaName}.access must reference PermissionAccess`,
+      ),
+  );
+  contract.checkSpec(
+    contract.spec.components.parameters.AccessQuery.schema.$ref ===
+      '#/components/schemas/PermissionAccess',
+    'AccessQuery must reference PermissionAccess',
+  );
+  contract.requireSchemaReference('PermissionResource', 'oneOf', 
'ColumnResource');
+  contract.requireSchemaReference('PermissionResource', 'oneOf', 
'CatalogAllResource');
+  contract.requireSchemaReference('PermissionResource', 'oneOf', 
'DatabaseAllResource');
+  const catalogAllResource = contract.requireProperties('CatalogAllResource', 
['type']);
+  contract.requireRequiredProperties('CatalogAllResource', ['type']);
+  contract.checkSpec(
+    catalogAllResource.type.const === 'CATALOG_ALL',
+    'CatalogAllResource must use the CATALOG_ALL discriminator',
+  );
+  const databaseAllResource = 
contract.requireProperties('DatabaseAllResource', [
+    'type',
+    'database',
+  ]);
+  contract.requireRequiredProperties('DatabaseAllResource', ['type', 
'database']);
+  contract.checkSpec(
+    databaseAllResource.type.const === 'DATABASE_ALL',
+    'DatabaseAllResource must use the DATABASE_ALL discriminator',
+  );
+  const columnResource = contract.requireProperties('ColumnResource', [
+    'type',
+    'database',
+    'table',
+  ]);
+  contract.requireRequiredProperties('ColumnResource', ['type', 'database', 
'table']);
+  contract.checkSpec(
+    columnResource.type.const === 'COLUMN',
+    'ColumnResource must use the COLUMN discriminator',
+  );
+  const permissionColumns = contract.requireProperties('PermissionColumns', [
+    'columnNames',
+    'excludedColumnNames',
+  ]);
+  const permissionColumnsDescription = contract
+    .schema('PermissionColumns')
+    .description.toLowerCase()
+    .replace(/\s+/g, ' ');
+  ['intersected', 'fails the query', 'query authorization'].forEach((phrase) =>
+    contract.checkSpec(
+      permissionColumnsDescription.includes(phrase),
+      `PermissionColumns semantics are missing: ${phrase}`,
+    ),
+  );
+  ['columnNames', 'excludedColumnNames'].forEach((field) => {
+    const definition = permissionColumns[field];
+    contract.checkSpec(
+      definition.type === 'array' &&
+        definition.minItems === 1 &&
+        definition.uniqueItems === true &&
+        definition.items.type === 'string' &&
+        definition.items.minLength === 1,
+      `PermissionColumns.${field} must be a non-empty unique string array`,
+    );
+  });
+  const columnAlternatives = contract.schema('PermissionColumns').oneOf || [];
+  ['columnNames', 'excludedColumnNames'].forEach((field) =>
+    contract.checkSpec(
+      columnAlternatives.some(
+        (alternative) =>
+          alternative.required &&
+          alternative.required.length === 1 &&
+          alternative.required[0] === field,
+      ),
+      `PermissionColumns must define the ${field} alternative`,
+    ),
+  );
+  ['PermissionAssignment', 'GrantPermissionRequest'].forEach((schemaName) => {
+    const properties = contract.requireProperties(schemaName, ['columns']);
+    contract.checkSpec(
+      properties.columns.$ref === '#/components/schemas/PermissionColumns',
+      `${schemaName}.columns must reference PermissionColumns`,
+    );
+    contract.requireSchemaReference(schemaName, 'allOf', 
'ColumnAssignmentConstraint');
+  });
+  ['DatabaseQuery', 'TableQuery', 'FunctionQuery', 
'ViewQuery'].forEach((parameterName) => {
+    const locatorQuery = contract.spec.components.parameters[parameterName];
+    contract.checkSpec(
+      locatorQuery.schema.type === 'string' && locatorQuery.schema.minLength 
=== 1,
+      `${parameterName} must be a non-empty string`,
+    );
+  });
+
+  ['TooManyRequests', 'ServiceUnavailable'].forEach((responseName) => {
+    const response = contract.spec.components.responses[responseName];
+    contract.checkSpec(response, `Missing reusable response: ${responseName}`);
+    contract.checkSpec(
+      response.headers['Retry-After'].$ref === 
'#/components/headers/RetryAfter',
+      `Response ${responseName} must expose the optional Retry-After header`,
+    );
+  });
+  contract.checkSpec(
+    contract.spec.components.headers.RetryAfter.schema.type === 'string',
+    'RetryAfter must allow HTTP delta-seconds or an HTTP date as a string',
+  );
+
+  const expireTime = contract.schema('ExpireTime');
+  contract.checkSpec(
+    expireTime.type === 'string' &&
+      expireTime.format === 'date-time' &&
+      expireTime.pattern === 
'^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,3})?Z$',
+    'ExpireTime must use UTC Z with at most three fractional digits',
+  );
+  ['PermissionAssignment', 'GrantPermissionRequest'].forEach((schemaName) => {
+    const expiry = contract.requireProperties(schemaName, 
['expireTime']).expireTime;
+    contract.checkSpec(
+      expiry.$ref === '#/components/schemas/ExpireTime',
+      `${schemaName}.expireTime must reference the shared ExpireTime schema`,
+    );
+  });
+  const expireTimePattern = new RegExp(expireTime.pattern);
+  ['2027-01-01T00:00:00Z', '2027-01-01T00:00:00.1Z', 
'2027-01-01T00:00:00.123Z'].forEach(
+    (value) =>
+      contract.checkSpec(expireTimePattern.test(value), `ExpireTime must 
accept ${value}`),
+  );
+  ['2027-01-01T00:00:00.123456Z', '2027-01-01T00:00:00+00:00'].forEach((value) 
=>
+    contract.checkSpec(!expireTimePattern.test(value), `ExpireTime must reject 
${value}`),
+  );
+  const expiryDescription = expireTime.description.toLowerCase();
+  contract.checkSpec(
+    expiryDescription.includes('exclusive') &&
+      expiryDescription.includes('server clock') &&
+      expiryDescription.includes('millisecond') &&
+      expiryDescription.includes('must not authorize'),
+    'expireTime must define exclusive millisecond server-clock authorization 
semantics',
+  );
+  const resourceDescription = 
contract.schema('PermissionResource').description.toLowerCase();
+  ['stable internal resource identity', 'renaming', 'dropping', 
'recreating'].forEach((phrase) =>
+    contract.checkSpec(
+      resourceDescription.includes(phrase),
+      `PermissionResource lifecycle is missing: ${phrase}`,
+    ),
+  );
+  const policyDescription = 
contract.schema('DataPolicy').description.toLowerCase();
+  ['logical and', 'same column', 'fail closed'].forEach((phrase) =>
+    contract.checkSpec(
+      policyDescription.includes(phrase),
+      `DataPolicy composition is missing: ${phrase}`,
+    ),
+  );
 
-console.log(`Validated REST OpenAPI contract with ${operationIds.size} 
operations.`);
+  contract.requireSchemaReference('PolicyRequest', 'oneOf', 
'RowFilterPolicyRequest');
+  contract.requireSchemaReference('PolicyRequest', 'oneOf', 
'ColumnMaskPolicyRequest');
+  contract.requireSchemaReference('DataPolicy', 'oneOf', 
'RowFilterDataPolicy');
+  contract.requireSchemaReference('DataPolicy', 'oneOf', 
'ColumnMaskDataPolicy');
+  const rowFilter = contract.requireProperties('RowFilter', ['predicate']);
+  const columnMask = contract.requireProperties('ColumnMask', ['onColumn', 
'transform']);
+  contract.requireRequiredProperties('RowFilter', ['predicate']);
+  contract.requireRequiredProperties('ColumnMask', ['onColumn', 'transform']);
+  [rowFilter.predicate, columnMask.transform].forEach((definition) => {
+    contract.checkSpec(
+      definition.type === 'string' &&
+        definition.minLength === 1 &&
+        definition['x-maxUtf8Bytes'] === 61440 &&
+        definition.contentMediaType === 'application/json',
+      'Policy definitions must be bounded non-empty JSON strings',
+    );
+  });
+  ['RowFilterPolicyRequest', 'ColumnMaskPolicyRequest'].forEach((schemaName) 
=> {
+    const properties = contract.requireProperties(schemaName, ['principal']);
+    contract.checkSpec(
+      properties.principal.$ref === '#/components/schemas/Principal',
+      `${schemaName}.principal must reference Principal`,
+    );
+    contract.checkSpec(
+      !properties.type &&
+        !properties.resource &&
+        !properties.name &&
+        !properties.toPrincipals &&
+        !properties.exceptPrincipals,
+      `${schemaName} must expose only one principal and no path identity`,
+    );
+    contract.requireRequiredProperties(schemaName, ['principal']);
+  });
+  contract.requireProperties('RowFilterPolicyRequest', ['rowFilter']);
+  contract.requireProperties('ColumnMaskPolicyRequest', ['columnMask']);
+  contract.requireRequiredProperties('RowFilterPolicyRequest', ['rowFilter']);
+  contract.requireRequiredProperties('ColumnMaskPolicyRequest', 
['columnMask']);
+  contract.requireSchemaReference('DropPolicyRequest', 'oneOf', 
'RowFilterPolicyIdentity');
+  contract.requireSchemaReference('DropPolicyRequest', 'oneOf', 
'ColumnMaskPolicyIdentity');
+  contract.requireRequiredProperties('RowFilterPolicyIdentity', ['type', 
'principal']);
+  contract.requireRequiredProperties('ColumnMaskPolicyIdentity', [
+    'type',
+    'principal',
+    'column',
+  ]);
+  const tablePolicyResource = 
contract.requireProperties('TablePolicyResource', [
+    'type',
+    'database',
+    'table',
+  ]);
+  contract.checkSpec(
+    tablePolicyResource.type.const === 'TABLE',
+    'Data policies must use a TABLE attachment resource',
+  );
+  const policyList = contract.requireProperties('ListPoliciesResponse', [
+    'policies',
+    'nextPageToken',
+  ]);
+  contract.checkSpec(
+    policyList.policies.items.$ref === '#/components/schemas/DataPolicy',
+    'ListPoliciesResponse.policies must contain DataPolicy values',
+  );
+  ['RowFilterDataPolicy', 'ColumnMaskDataPolicy'].forEach((schemaName) => {
+    const properties = contract.requireProperties(schemaName, ['resource', 
'principal']);
+    contract.checkSpec(
+      properties.principal.$ref === '#/components/schemas/Principal',
+      `${schemaName}.principal must reference Principal`,
+    );
+    contract.checkSpec(
+      !properties.name && !properties.toPrincipals && 
!properties.exceptPrincipals,
+      `${schemaName} must use a single principal identity`,
+    );
+  });
+  contract.requireProperties('ErrorResponse', [
+    'message',
+    'resourceType',
+    'resourceName',
+    'code',
+  ]);
+  contract.requireRequiredProperties('ErrorResponse', ['message', 'code']);
+
+  const permissionParameters = contract
+    .requireOperation('listPermissions')
+    .parameters.map((parameter) =>
+      parameter.$ref ? parameter.$ref.split('/').pop() : parameter.name,
+    );
+  [
+    'ResourceTypeQuery',
+    'DatabaseQuery',
+    'TableQuery',
+    'FunctionQuery',
+    'ViewQuery',
+    'PrincipalQuery',
+    'AccessQuery',
+    'PageToken',
+    'MaxResults',
+  ].forEach((name) =>
+    contract.checkSpec(
+      permissionParameters.includes(name),
+      `Operation listPermissions is missing query parameter: ${name}`,
+    ),
+  );
+  contract.checkSpec(
+    permissionParameters.length === 9,
+    'Operation listPermissions must expose only exact-resource filters and 
pagination',
+  );
+  contract.checkSpec(
+    contract.spec.components.parameters.ResourceTypeQuery.required === true,
+    'Operation listPermissions must require resourceType',
+  );
+  const policyParameters = contract
+    .requireOperation('listTablePolicies')
+    .parameters.map((parameter) =>
+      parameter.$ref ? parameter.$ref.split('/').pop() : parameter.name,
+    );
+  ['PolicyTypeQuery', 'PrincipalQuery', 'PolicyColumnQuery', 'PageToken', 
'MaxResults'].forEach(
+    (name) =>
+      contract.checkSpec(
+        policyParameters.includes(name),
+        `Operation listTablePolicies is missing query parameter: ${name}`,
+      ),
+  );
+  contract.checkSpec(
+    !policyParameters.includes('PolicyNameQuery'),
+    'Principal-scoped policies must not expose a policy-name filter',
+  );
+  const tablePoliciesPath =
+    
contract.spec.paths['/v1/{prefix}/databases/{database}/tables/{table}/policies'];
+  const dropTablePolicyPath =
+    
contract.spec.paths['/v1/{prefix}/databases/{database}/tables/{table}/policies/drop'];
+  contract.checkSpec(
+    tablePoliciesPath.get &&
+      tablePoliciesPath.post &&
+      !tablePoliciesPath.put &&
+      !tablePoliciesPath.delete,
+    'The table policy collection must expose only list and strict creation',
+  );
+  contract.checkSpec(
+    dropTablePolicyPath.post && !dropTablePolicyPath.delete,
+    'Policy deletion must use a body-bearing POST action instead of DELETE',
+  );
+
+  contract.checkSpec(
+    contract.operations.size === operationIds.length,
+    `The management contract must define exactly ${operationIds.length} 
operations`,
+  );
+  resourcePaths.forEach((resourcePath) =>
+    contract.checkSpec(
+      contract.spec.paths[resourcePath].parameters.some(
+        (parameter) => parameter.$ref === '#/components/parameters/Prefix',
+      ),
+      `Path ${resourcePath} must reuse components.parameters.Prefix`,
+    ),
+  );
+  return contract.operations.size;
+}
+
+const catalogOperationCount = validateCatalogOpenApi();
+const managementOperationCount = validateManagementOpenApi();
+console.log(`Validated REST Catalog OpenAPI contract with 
${catalogOperationCount} operations.`);
+console.log(
+  `Validated REST Management OpenAPI contract with ${managementOperationCount} 
operations.`,
+);
diff --git a/docs/sidebars.js b/docs/sidebars.js
index cb70d4ad19..cd2b6b2d95 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -31,7 +31,8 @@ const sidebars = {
           "concepts/rest/dlf",
           "concepts/rest/tables",
           "concepts/rest/pvfs",
-          "concepts/rest/rest-api"
+          "concepts/rest/rest-api",
+          "concepts/rest/management-api"
         ]
       },
       {
diff --git a/docs/static/rest-management-open-api.yaml 
b/docs/static/rest-management-open-api.yaml
new file mode 100644
index 0000000000..4da5a572b5
--- /dev/null
+++ b/docs/static/rest-management-open-api.yaml
@@ -0,0 +1,852 @@
+# 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.
+
+openapi: 3.1.1
+info:
+  title: Paimon REST Management API
+  version: "1.0"
+  description: |
+    Experimental control-plane extension for object and column permissions, 
table row filters, and
+    table column masks in one Paimon REST Catalog prefix. A data policy 
restricts an
+    already-authorized read;
+    it never grants SELECT by itself. Each policy is attached to one 
principal: a principal has at
+    most one row filter per table and at most one mask per table column. 
Policy creation is accepted
+    only when the target table exists, has `query-auth.enabled=true`, and the 
referenced principal,
+    serialized Paimon predicate or transform, and columns are valid.
+    Authentication follows the REST Catalog deployment configuration and is 
not fixed by this
+    extension.
+    Principal lifecycle, audit, and persistence remain server 
responsibilities. This contract may
+    evolve incompatibly while experimental.
+  license:
+    name: Apache 2.0
+    url: https://www.apache.org/licenses/LICENSE-2.0.html
+servers:
+  - url: http://localhost:8080
+paths:
+  /v1/{prefix}/permissions:
+    parameters:
+      - $ref: '#/components/parameters/Prefix'
+    get:
+      tags: [permission]
+      summary: List direct permission assignments on a resource or scope
+      operationId: listPermissions
+      description: |
+        Returns direct assignments attached to the requested exact resource or 
explicit descendant
+        scope. This operation does not synthesize assignments effective 
through CATALOG_ALL or
+        DATABASE_ALL. Expired assignments may remain visible until server 
cleanup, but must not
+        authorize access. Following the catalog pagination contract, an empty 
page terminates
+        pagination and therefore must not carry a continuation token.
+      parameters:
+        - $ref: '#/components/parameters/ResourceTypeQuery'
+        - $ref: '#/components/parameters/DatabaseQuery'
+        - $ref: '#/components/parameters/TableQuery'
+        - $ref: '#/components/parameters/FunctionQuery'
+        - $ref: '#/components/parameters/ViewQuery'
+        - $ref: '#/components/parameters/PrincipalQuery'
+        - $ref: '#/components/parameters/AccessQuery'
+        - $ref: '#/components/parameters/PageToken'
+        - $ref: '#/components/parameters/MaxResults'
+      responses:
+        '200':
+          description: Permission assignments in stable pagination order.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ListPermissionsResponse'
+        '400':
+          $ref: '#/components/responses/BadRequest'
+        '401':
+          $ref: '#/components/responses/Unauthorized'
+        '403':
+          $ref: '#/components/responses/Forbidden'
+        '404':
+          $ref: '#/components/responses/NotFound'
+        '429':
+          $ref: '#/components/responses/TooManyRequests'
+        '500':
+          $ref: '#/components/responses/ServerError'
+        '503':
+          $ref: '#/components/responses/ServiceUnavailable'
+  /v1/{prefix}/permissions/grant:
+    parameters:
+      - $ref: '#/components/parameters/Prefix'
+    post:
+      tags: [permission]
+      summary: Grant or replace a permission assignment
+      operationId: grantPermission
+      description: |
+        Creates an assignment or replaces the assignment with the same 
`resource`, `access`, and
+        `principal`. For a `COLUMN` assignment this replaces the whole 
included or excluded column
+        range. The referenced resource, principal, and columns must exist. 
Servers bind assignments
+        to the resource lifecycle described by `PermissionResource`.
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/GrantPermissionRequest'
+      responses:
+        '200':
+          description: Permission assignment created or replaced.
+        '400':
+          $ref: '#/components/responses/BadRequest'
+        '401':
+          $ref: '#/components/responses/Unauthorized'
+        '403':
+          $ref: '#/components/responses/Forbidden'
+        '404':
+          $ref: '#/components/responses/NotFound'
+        '409':
+          $ref: '#/components/responses/Conflict'
+        '429':
+          $ref: '#/components/responses/TooManyRequests'
+        '500':
+          $ref: '#/components/responses/ServerError'
+        '503':
+          $ref: '#/components/responses/ServiceUnavailable'
+  /v1/{prefix}/permissions/revoke:
+    parameters:
+      - $ref: '#/components/parameters/Prefix'
+    post:
+      tags: [permission]
+      summary: Idempotently revoke a permission assignment
+      operationId: revokePermission
+      description: |
+        Makes the assignment absent. A 404 identifies a missing resource or 
principal, not an
+        already-absent assignment.
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/RevokePermissionRequest'
+      responses:
+        '200':
+          description: Permission assignment is absent.
+        '400':
+          $ref: '#/components/responses/BadRequest'
+        '401':
+          $ref: '#/components/responses/Unauthorized'
+        '403':
+          $ref: '#/components/responses/Forbidden'
+        '404':
+          $ref: '#/components/responses/NotFound'
+        '429':
+          $ref: '#/components/responses/TooManyRequests'
+        '500':
+          $ref: '#/components/responses/ServerError'
+        '503':
+          $ref: '#/components/responses/ServiceUnavailable'
+
+  /v1/{prefix}/databases/{database}/tables/{table}/policies:
+    parameters:
+      - $ref: '#/components/parameters/Prefix'
+      - $ref: '#/components/parameters/Database'
+      - $ref: '#/components/parameters/Table'
+    get:
+      tags: [policy]
+      summary: List policies attached directly to a table
+      operationId: listTablePolicies
+      description: |
+        Policies are attached directly to the requested table. Following the 
catalog pagination
+        contract, an empty page terminates pagination and therefore must not 
carry a continuation
+        token.
+      parameters:
+        - $ref: '#/components/parameters/PolicyTypeQuery'
+        - $ref: '#/components/parameters/PrincipalQuery'
+        - $ref: '#/components/parameters/PolicyColumnQuery'
+        - $ref: '#/components/parameters/PageToken'
+        - $ref: '#/components/parameters/MaxResults'
+      responses:
+        '200':
+          description: Table policies in stable pagination order.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ListPoliciesResponse'
+        '400':
+          $ref: '#/components/responses/BadRequest'
+        '401':
+          $ref: '#/components/responses/Unauthorized'
+        '403':
+          $ref: '#/components/responses/Forbidden'
+        '404':
+          $ref: '#/components/responses/NotFound'
+        '429':
+          $ref: '#/components/responses/TooManyRequests'
+        '500':
+          $ref: '#/components/responses/ServerError'
+        '503':
+          $ref: '#/components/responses/ServiceUnavailable'
+    post:
+      tags: [policy]
+      summary: Create a table policy
+      operationId: createTablePolicy
+      description: |
+        Fails with 409 when the same principal already has a row filter or a 
mask on the same
+        column, or when the table has not enabled query authorization. The 
principal, policy
+        predicate or transform, protected column, and referenced fields are 
validated before
+        persistence.
+      requestBody:
+        $ref: '#/components/requestBodies/PolicyRequest'
+      responses:
+        '200':
+          description: Policy created.
+        '400':
+          $ref: '#/components/responses/BadRequest'
+        '401':
+          $ref: '#/components/responses/Unauthorized'
+        '403':
+          $ref: '#/components/responses/Forbidden'
+        '404':
+          $ref: '#/components/responses/NotFound'
+        '409':
+          $ref: '#/components/responses/Conflict'
+        '429':
+          $ref: '#/components/responses/TooManyRequests'
+        '500':
+          $ref: '#/components/responses/ServerError'
+        '503':
+          $ref: '#/components/responses/ServiceUnavailable'
+  /v1/{prefix}/databases/{database}/tables/{table}/policies/drop:
+    parameters:
+      - $ref: '#/components/parameters/Prefix'
+      - $ref: '#/components/parameters/Database'
+      - $ref: '#/components/parameters/Table'
+    post:
+      tags: [policy]
+      summary: Drop a table policy
+      operationId: dropTablePolicy
+      description: Returns 404 when the policy is absent; clients may expose 
idempotency explicitly.
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/DropPolicyRequest'
+      responses:
+        '200':
+          description: Policy dropped.
+        '400':
+          $ref: '#/components/responses/BadRequest'
+        '401':
+          $ref: '#/components/responses/Unauthorized'
+        '403':
+          $ref: '#/components/responses/Forbidden'
+        '404':
+          $ref: '#/components/responses/NotFound'
+        '429':
+          $ref: '#/components/responses/TooManyRequests'
+        '500':
+          $ref: '#/components/responses/ServerError'
+        '503':
+          $ref: '#/components/responses/ServiceUnavailable'
+
+components:
+  securitySchemes:
+    BearerAuth:
+      type: http
+      scheme: bearer
+  parameters:
+    Prefix:
+      name: prefix
+      in: path
+      required: true
+      description: Opaque REST catalog prefix returned by the catalog config 
endpoint.
+      schema:
+        type: string
+        minLength: 1
+    Database:
+      name: database
+      in: path
+      required: true
+      schema:
+        type: string
+        minLength: 1
+    Table:
+      name: table
+      in: path
+      required: true
+      schema:
+        type: string
+        minLength: 1
+    ResourceTypeQuery:
+      name: resourceType
+      in: query
+      required: true
+      schema:
+        $ref: '#/components/schemas/ResourceType'
+    DatabaseQuery:
+      name: database
+      in: query
+      schema:
+        type: string
+        minLength: 1
+    TableQuery:
+      name: table
+      in: query
+      schema:
+        type: string
+        minLength: 1
+    FunctionQuery:
+      name: function
+      in: query
+      schema:
+        type: string
+        minLength: 1
+    ViewQuery:
+      name: view
+      in: query
+      schema:
+        type: string
+        minLength: 1
+    PrincipalQuery:
+      name: principal
+      in: query
+      description: Exact opaque principal identifier.
+      schema:
+        $ref: '#/components/schemas/Principal'
+    AccessQuery:
+      name: access
+      in: query
+      schema:
+        $ref: '#/components/schemas/PermissionAccess'
+    PolicyColumnQuery:
+      name: column
+      in: query
+      description: Valid only together with `type=COLUMN_MASKING`.
+      schema:
+        type: string
+        minLength: 1
+    PolicyTypeQuery:
+      name: type
+      in: query
+      schema:
+        $ref: '#/components/schemas/PolicyType'
+    PageToken:
+      name: pageToken
+      in: query
+      description: Opaque continuation token returned by the preceding 
response.
+      schema:
+        type: string
+    MaxResults:
+      name: maxResults
+      in: query
+      schema:
+        type: integer
+        minimum: 1
+        maximum: 1000
+
+  requestBodies:
+    PolicyRequest:
+      required: true
+      content:
+        application/json:
+          schema:
+            $ref: '#/components/schemas/PolicyRequest'
+
+  responses:
+    BadRequest:
+      description: Invalid request shape, resource identity, predicate, 
transform, or column.
+      content:
+        application/json:
+          schema:
+            $ref: '#/components/schemas/ErrorResponse'
+    Unauthorized:
+      description: Missing or invalid authentication.
+      content:
+        application/json:
+          schema:
+            $ref: '#/components/schemas/ErrorResponse'
+    Forbidden:
+      description: Caller cannot manage the target resource.
+      content:
+        application/json:
+          schema:
+            $ref: '#/components/schemas/ErrorResponse'
+    NotFound:
+      description: Referenced resource, principal, or policy does not exist.
+      content:
+        application/json:
+          schema:
+            $ref: '#/components/schemas/ErrorResponse'
+    Conflict:
+      description: Existing policy conflict or query authorization is not 
enabled.
+      content:
+        application/json:
+          schema:
+            $ref: '#/components/schemas/ErrorResponse'
+    TooManyRequests:
+      description: Request rate limit exceeded; retry only according to server 
guidance.
+      headers:
+        Retry-After:
+          $ref: '#/components/headers/RetryAfter'
+      content:
+        application/json:
+          schema:
+            $ref: '#/components/schemas/ErrorResponse'
+    ServerError:
+      description: Unexpected server error.
+      content:
+        application/json:
+          schema:
+            $ref: '#/components/schemas/ErrorResponse'
+    ServiceUnavailable:
+      description: Management service is temporarily unavailable.
+      headers:
+        Retry-After:
+          $ref: '#/components/headers/RetryAfter'
+      content:
+        application/json:
+          schema:
+            $ref: '#/components/schemas/ErrorResponse'
+
+  headers:
+    RetryAfter:
+      description: Delay before retrying, as HTTP delta-seconds or an HTTP 
date.
+      schema:
+        type: string
+
+  schemas:
+    ResourceType:
+      type: string
+      enum: [CATALOG, CATALOG_ALL, DATABASE, DATABASE_ALL, TABLE, COLUMN, 
FUNCTION, VIEW]
+    PermissionAccess:
+      description: |
+        Canonical data access value. The REST wire format uses upper case and 
creation accesses
+        follow their persisted names without underscores. UPDATE covers table 
writes, including
+        inserts and deletes. SELECT also covers function use. GRANT allows 
granting and revoking
+        assignments on the resource. The server validates applicability to the 
resource type.
+      type: string
+      maxLength: 32
+      enum:
+        - ALL
+        - CREATEDATABASE
+        - DESCRIBE
+        - ALTER
+        - DROP
+        - CREATETABLE
+        - CREATEFUNCTION
+        - CREATEVIEW
+        - LIST
+        - SELECT
+        - UPDATE
+        - GRANT
+    PolicyType:
+      type: string
+      enum: [ROW_FILTER, COLUMN_MASKING]
+    Principal:
+      type: string
+      minLength: 1
+      maxLength: 128
+      description: |
+        Opaque, canonical, globally unique identifier in the server principal 
namespace. Type and
+        membership resolution are server responsibilities.
+    PermissionResource:
+      description: |
+        Exact catalog-scoped wire locator or explicit descendant scope; fields 
not selected by type
+        are invalid. CATALOG_ALL covers descendants of the configured catalog, 
while DATABASE_ALL
+        covers descendants of the named database. Servers must bind stored 
direct assignments to a
+        stable internal resource identity. Renaming a database, table, 
function, or view retains its
+        direct assignments and responses use the new locator. Dropping a 
resource removes its direct
+        assignments, and recreating the same locator must not inherit them.
+      oneOf:
+        - $ref: '#/components/schemas/CatalogResource'
+        - $ref: '#/components/schemas/CatalogAllResource'
+        - $ref: '#/components/schemas/DatabaseResource'
+        - $ref: '#/components/schemas/DatabaseAllResource'
+        - $ref: '#/components/schemas/TableResource'
+        - $ref: '#/components/schemas/ColumnResource'
+        - $ref: '#/components/schemas/FunctionResource'
+        - $ref: '#/components/schemas/ViewResource'
+      discriminator:
+        propertyName: type
+        mapping:
+          CATALOG: '#/components/schemas/CatalogResource'
+          CATALOG_ALL: '#/components/schemas/CatalogAllResource'
+          DATABASE: '#/components/schemas/DatabaseResource'
+          DATABASE_ALL: '#/components/schemas/DatabaseAllResource'
+          TABLE: '#/components/schemas/TableResource'
+          COLUMN: '#/components/schemas/ColumnResource'
+          FUNCTION: '#/components/schemas/FunctionResource'
+          VIEW: '#/components/schemas/ViewResource'
+    CatalogResource:
+      type: object
+      additionalProperties: false
+      required: [type]
+      properties:
+        type:
+          type: string
+          const: CATALOG
+    CatalogAllResource:
+      type: object
+      additionalProperties: false
+      description: Explicit scope covering database, table, view, and function 
descendants.
+      required: [type]
+      properties:
+        type:
+          type: string
+          const: CATALOG_ALL
+    DatabaseResource:
+      type: object
+      additionalProperties: false
+      required: [type, database]
+      properties:
+        type:
+          type: string
+          const: DATABASE
+        database:
+          type: string
+          minLength: 1
+    DatabaseAllResource:
+      type: object
+      additionalProperties: false
+      description: Explicit scope covering table, view, and function 
descendants of one database.
+      required: [type, database]
+      properties:
+        type:
+          type: string
+          const: DATABASE_ALL
+        database:
+          type: string
+          minLength: 1
+    TableResource:
+      type: object
+      additionalProperties: false
+      required: [type, database, table]
+      properties:
+        type:
+          type: string
+          const: TABLE
+        database:
+          type: string
+          minLength: 1
+        table:
+          type: string
+          minLength: 1
+    ColumnResource:
+      type: object
+      additionalProperties: false
+      description: |
+        Column permission target. The resource identity is the containing 
table; the mutable column
+        range is carried by `PermissionAssignment.columns` and is not part of 
the identity.
+      required: [type, database, table]
+      properties:
+        type:
+          type: string
+          const: COLUMN
+        database:
+          type: string
+          minLength: 1
+        table:
+          type: string
+          minLength: 1
+    FunctionResource:
+      type: object
+      additionalProperties: false
+      required: [type, database, function]
+      properties:
+        type:
+          type: string
+          const: FUNCTION
+        database:
+          type: string
+          minLength: 1
+        function:
+          type: string
+          minLength: 1
+    ViewResource:
+      type: object
+      additionalProperties: false
+      required: [type, database, view]
+      properties:
+        type:
+          type: string
+          const: VIEW
+        database:
+          type: string
+          minLength: 1
+        view:
+          type: string
+          minLength: 1
+    ExpireTime:
+      type: string
+      format: date-time
+      pattern: '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$'
+      description: |
+        Exclusive authorization upper bound evaluated using the server clock. 
At
+        `now >= expireTime` the assignment must not authorize access. An 
expired record may remain
+        listable until cleanup. The value must use UTC `Z` and have at most 
millisecond precision.
+    PermissionAssignment:
+      type: object
+      additionalProperties: false
+      description: |
+        Direct assignment identity is `resource`, `access`, and `principal`. 
CATALOG_ALL and
+        DATABASE_ALL are explicit descendant scopes rather than computed 
effective assignments.
+        `columns` is required only for a COLUMN assignment and is replaced as 
one value when the
+        same identity is granted again.
+      required: [resource, access, principal]
+      properties:
+        resource:
+          $ref: '#/components/schemas/PermissionResource'
+        access:
+          $ref: '#/components/schemas/PermissionAccess'
+        principal:
+          $ref: '#/components/schemas/Principal'
+        columns:
+          $ref: '#/components/schemas/PermissionColumns'
+        expireTime:
+          $ref: '#/components/schemas/ExpireTime'
+      allOf:
+        - $ref: '#/components/schemas/ColumnAssignmentConstraint'
+    GrantPermissionRequest:
+      type: object
+      additionalProperties: false
+      required: [resource, access, principal]
+      properties:
+        resource:
+          $ref: '#/components/schemas/PermissionResource'
+        access:
+          $ref: '#/components/schemas/PermissionAccess'
+        principal:
+          $ref: '#/components/schemas/Principal'
+        columns:
+          $ref: '#/components/schemas/PermissionColumns'
+        expireTime:
+          $ref: '#/components/schemas/ExpireTime'
+      allOf:
+        - $ref: '#/components/schemas/ColumnAssignmentConstraint'
+    PermissionColumns:
+      type: object
+      additionalProperties: false
+      description: |
+        Exactly one non-empty list of top-level table column names. 
`columnNames` is an allowlist;
+        `excludedColumnNames` grants every current table column except the 
listed denylist. Column
+        names must exist when granted. An allowlist denies columns added 
later, while a denylist
+        allows columns added later. All applicable column ranges are 
intersected, and selecting any
+        column outside the effective range fails the query. The target table 
must enforce query
+        authorization before the grant becomes visible.
+      properties:
+        columnNames:
+          type: array
+          minItems: 1
+          uniqueItems: true
+          items:
+            type: string
+            minLength: 1
+        excludedColumnNames:
+          type: array
+          minItems: 1
+          uniqueItems: true
+          items:
+            type: string
+            minLength: 1
+      oneOf:
+        - required: [columnNames]
+        - required: [excludedColumnNames]
+    ColumnAssignmentConstraint:
+      if:
+        properties:
+          resource:
+            type: object
+            required: [type]
+            properties:
+              type:
+                type: string
+                const: COLUMN
+        required: [resource]
+      then:
+        required: [columns]
+      else:
+        not:
+          required: [columns]
+    RevokePermissionRequest:
+      type: object
+      additionalProperties: false
+      required: [resource, access, principal]
+      properties:
+        resource:
+          $ref: '#/components/schemas/PermissionResource'
+        access:
+          $ref: '#/components/schemas/PermissionAccess'
+        principal:
+          $ref: '#/components/schemas/Principal'
+    ListPermissionsResponse:
+      type: object
+      additionalProperties: false
+      required: [permissions]
+      properties:
+        permissions:
+          type: array
+          items:
+            $ref: '#/components/schemas/PermissionAssignment'
+        nextPageToken:
+          type: string
+
+    RowFilter:
+      type: object
+      additionalProperties: false
+      required: [predicate]
+      properties:
+        predicate:
+          type: string
+          minLength: 1
+          x-maxUtf8Bytes: 61440
+          contentMediaType: application/json
+          description: |
+            JSON serialization of one Paimon `Predicate`, using the same 
representation as one
+            entry in `AuthTableQueryResponse.filter`. The UTF-8 representation 
must not exceed
+            60 KiB. The server must deserialize, validate against the target 
table schema, and
+            canonicalize it when the policy is created.
+    ColumnMask:
+      type: object
+      additionalProperties: false
+      required: [onColumn, transform]
+      properties:
+        onColumn:
+          type: string
+          minLength: 1
+        transform:
+          type: string
+          minLength: 1
+          x-maxUtf8Bytes: 61440
+          contentMediaType: application/json
+          description: |
+            JSON serialization of one Paimon `Transform`, using the same 
representation as the
+            value for `onColumn` in `AuthTableQueryResponse.columnMasking`. 
The UTF-8
+            representation must not exceed 60 KiB. The server must deserialize 
it, validate all
+            field references and the output type against the target table 
schema, and canonicalize
+            it when the policy is created.
+    RowFilterPolicyRequest:
+      type: object
+      additionalProperties: false
+      required: [rowFilter, principal]
+      properties:
+        rowFilter:
+          $ref: '#/components/schemas/RowFilter'
+        principal:
+          $ref: '#/components/schemas/Principal'
+    ColumnMaskPolicyRequest:
+      type: object
+      additionalProperties: false
+      required: [columnMask, principal]
+      properties:
+        columnMask:
+          $ref: '#/components/schemas/ColumnMask'
+        principal:
+          $ref: '#/components/schemas/Principal'
+    PolicyRequest:
+      description: Exactly one typed policy definition for one principal is 
allowed.
+      oneOf:
+        - $ref: '#/components/schemas/RowFilterPolicyRequest'
+        - $ref: '#/components/schemas/ColumnMaskPolicyRequest'
+    RowFilterPolicyIdentity:
+      type: object
+      additionalProperties: false
+      required: [type, principal]
+      properties:
+        type:
+          type: string
+          const: ROW_FILTER
+        principal:
+          $ref: '#/components/schemas/Principal'
+    ColumnMaskPolicyIdentity:
+      type: object
+      additionalProperties: false
+      required: [type, principal, column]
+      properties:
+        type:
+          type: string
+          const: COLUMN_MASKING
+        principal:
+          $ref: '#/components/schemas/Principal'
+        column:
+          type: string
+          minLength: 1
+    DropPolicyRequest:
+      description: Exact principal policy identity on the table named by the 
request path.
+      oneOf:
+        - $ref: '#/components/schemas/RowFilterPolicyIdentity'
+        - $ref: '#/components/schemas/ColumnMaskPolicyIdentity'
+    TablePolicyResource:
+      type: object
+      additionalProperties: false
+      required: [type, database, table]
+      properties:
+        type:
+          type: string
+          const: TABLE
+        database:
+          type: string
+          minLength: 1
+        table:
+          type: string
+          minLength: 1
+    RowFilterDataPolicy:
+      type: object
+      additionalProperties: false
+      required: [resource, rowFilter, principal]
+      properties:
+        resource:
+          $ref: '#/components/schemas/TablePolicyResource'
+        rowFilter:
+          $ref: '#/components/schemas/RowFilter'
+        principal:
+          $ref: '#/components/schemas/Principal'
+    ColumnMaskDataPolicy:
+      type: object
+      additionalProperties: false
+      required: [resource, columnMask, principal]
+      properties:
+        resource:
+          $ref: '#/components/schemas/TablePolicyResource'
+        columnMask:
+          $ref: '#/components/schemas/ColumnMask'
+        principal:
+          $ref: '#/components/schemas/Principal'
+    DataPolicy:
+      description: |
+        A policy applies when its principal is effective for the caller. All 
applicable row filters
+        must be combined with logical AND. More than one applicable column 
mask for the same column
+        must fail closed. An invalid, unsupported, or schema-incompatible 
predicate or transform
+        must also fail closed rather than omit a restriction.
+      oneOf:
+        - $ref: '#/components/schemas/RowFilterDataPolicy'
+        - $ref: '#/components/schemas/ColumnMaskDataPolicy'
+    ListPoliciesResponse:
+      type: object
+      additionalProperties: false
+      required: [policies]
+      properties:
+        policies:
+          type: array
+          items:
+            $ref: '#/components/schemas/DataPolicy'
+        nextPageToken:
+          type: string
+    ErrorResponse:
+      type: object
+      additionalProperties: true
+      required: [message, code]
+      properties:
+        resourceType:
+          type: string
+        resourceName:
+          type: string
+        message:
+          type: string
+        code:
+          type: integer

Reply via email to