This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 0ddd3a5 [api] Add authTableQuery method to RESTApi (#498)
0ddd3a5 is described below
commit 0ddd3a5af5b2143ac1e12e06607bd313ed467e53
Author: Jiajia Li <[email protected]>
AuthorDate: Mon Jul 13 13:10:45 2026 +0800
[api] Add authTableQuery method to RESTApi (#498)
---
crates/paimon/src/api/api_request.rs | 27 ++++++++++++++++++++
crates/paimon/src/api/api_response.rs | 45 +++++++++++++++++++++++++++++++++
crates/paimon/src/api/mod.rs | 12 ++++-----
crates/paimon/src/api/resource_paths.rs | 14 ++++++++++
crates/paimon/src/api/rest_api.rs | 25 ++++++++++++++----
5 files changed, 112 insertions(+), 11 deletions(-)
diff --git a/crates/paimon/src/api/api_request.rs
b/crates/paimon/src/api/api_request.rs
index 018673c..84ee7c5 100644
--- a/crates/paimon/src/api/api_request.rs
+++ b/crates/paimon/src/api/api_request.rs
@@ -167,6 +167,21 @@ impl AlterTableRequest {
}
}
+/// Request for auth table query: the projected columns of the query.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct AuthTableQueryRequest {
+ /// Projected column names; `None` means all columns.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub select: Option<Vec<String>>,
+}
+
+impl AuthTableQueryRequest {
+ /// Create a new AuthTableQueryRequest.
+ pub fn new(select: Option<Vec<String>>) -> Self {
+ Self { select }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -193,6 +208,18 @@ mod tests {
assert!(json.contains("\"updates\""));
}
+ #[test]
+ fn test_auth_table_query_request_serialization() {
+ let req = AuthTableQueryRequest::new(Some(vec!["a".to_string(),
"b".to_string()]));
+ assert_eq!(
+ serde_json::to_string(&req).unwrap(),
+ r#"{"select":["a","b"]}"#
+ );
+ // `None` omits the key entirely (matches the server's optional field).
+ let req = AuthTableQueryRequest::new(None);
+ assert_eq!(serde_json::to_string(&req).unwrap(), "{}");
+ }
+
#[test]
fn test_rename_table_request_serialization() {
let source = Identifier::new("db1".to_string(), "table1".to_string());
diff --git a/crates/paimon/src/api/api_response.rs
b/crates/paimon/src/api/api_response.rs
index 9586c7f..0e31137 100644
--- a/crates/paimon/src/api/api_response.rs
+++ b/crates/paimon/src/api/api_response.rs
@@ -427,10 +427,55 @@ pub struct GetTableTokenResponse {
pub expires_at_millis: Option<i64>,
}
+/// Response for auth table query: the per-user row filter and column masking
the
+/// client must enforce at read time for a `query-auth.enabled` table.
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AuthTableQueryResponse {
+ /// JSON-serialized row-filter predicates, ANDed together. Empty/None = no
filter.
+ pub filter: Option<Vec<String>>,
+ /// column name -> JSON-serialized masking transform. Empty/None = no
masking.
+ pub column_masking: Option<HashMap<String, String>>,
+}
+
+impl AuthTableQueryResponse {
+ /// True when the server imposes no row filter and no column masking.
+ pub fn is_unrestricted(&self) -> bool {
+ self.filter.as_ref().is_none_or(|f| f.is_empty())
+ && self.column_masking.as_ref().is_none_or(|m| m.is_empty())
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
+ #[test]
+ fn test_auth_table_query_response_deserialization() {
+ // A restricted grant: pin the exact wire field names. A drift in
either
+ // (`filter` / `columnMasking`) would deserialize to None and silently
+ // skip authorization, so this test fails closed against that.
+ let resp: AuthTableQueryResponse =
+
serde_json::from_str(r#"{"filter":["p0","p1"],"columnMasking":{"ssn":"m0"}}"#).unwrap();
+ assert_eq!(resp.filter, Some(vec!["p0".to_string(),
"p1".to_string()]));
+ assert_eq!(
+ resp.column_masking,
+ Some(HashMap::from([("ssn".to_string(), "m0".to_string())]))
+ );
+ assert!(!resp.is_unrestricted());
+
+ // The real server sends `{}` for an unrestricted grant (both fields
are
+ // `@JsonInclude(NON_NULL)` in Java); it must parse to an empty grant.
+ let empty: AuthTableQueryResponse =
serde_json::from_str("{}").unwrap();
+ assert_eq!(empty, AuthTableQueryResponse::default());
+ assert!(empty.is_unrestricted());
+
+ // Present-but-empty collections are also unrestricted.
+ let blank: AuthTableQueryResponse =
+
serde_json::from_str(r#"{"filter":[],"columnMasking":{}}"#).unwrap();
+ assert!(blank.is_unrestricted());
+ }
+
#[test]
fn test_error_response_serialization() {
let resp = ErrorResponse::new(
diff --git a/crates/paimon/src/api/mod.rs b/crates/paimon/src/api/mod.rs
index fb2da9f..809b902 100644
--- a/crates/paimon/src/api/mod.rs
+++ b/crates/paimon/src/api/mod.rs
@@ -31,16 +31,16 @@ mod api_response;
// Re-export request types
pub use api_request::{
- AlterDatabaseRequest, AlterTableRequest, CreateDatabaseRequest,
CreateFunctionRequest,
- CreateTableRequest, CreateViewRequest, RenameTableRequest,
+ AlterDatabaseRequest, AlterTableRequest, AuthTableQueryRequest,
CreateDatabaseRequest,
+ CreateFunctionRequest, CreateTableRequest, CreateViewRequest,
RenameTableRequest,
};
// Re-export response types
pub use api_response::{
- AuditRESTResponse, ConfigResponse, ErrorResponse, GetDatabaseResponse,
GetFunctionResponse,
- GetTableResponse, GetTableTokenResponse, GetViewResponse,
ListDatabasesResponse,
- ListFunctionsResponse, ListPartitionsResponse, ListTablesResponse,
ListViewsResponse,
- PagedList,
+ AuditRESTResponse, AuthTableQueryResponse, ConfigResponse, ErrorResponse,
GetDatabaseResponse,
+ GetFunctionResponse, GetTableResponse, GetTableTokenResponse,
GetViewResponse,
+ ListDatabasesResponse, ListFunctionsResponse, ListPartitionsResponse,
ListTablesResponse,
+ ListViewsResponse, PagedList,
};
// Re-export error types
diff --git a/crates/paimon/src/api/resource_paths.rs
b/crates/paimon/src/api/resource_paths.rs
index 4983ae7..38c39c9 100644
--- a/crates/paimon/src/api/resource_paths.rs
+++ b/crates/paimon/src/api/resource_paths.rs
@@ -147,6 +147,11 @@ impl ResourcePaths {
)
}
+ /// Get the auth-table-query endpoint path (`.../tables/{table}/auth`).
+ pub fn auth_table(&self, database_name: &str, table_name: &str) -> String {
+ format!("{}/auth", self.table(database_name, table_name))
+ }
+
/// Get the table details endpoint path.
pub fn table_details(&self, database_name: &str) -> String {
format!(
@@ -242,6 +247,15 @@ mod tests {
assert!(table_path.contains("my-table"));
}
+ #[test]
+ fn test_auth_table_path_encodes_names() {
+ let paths = ResourcePaths::new("catalog");
+ assert_eq!(
+ paths.auth_table("analytics db", "user events"),
+ "/v1/catalog/databases/analytics+db/tables/user+events/auth"
+ );
+ }
+
#[test]
fn test_config_path() {
assert_eq!(ResourcePaths::config(), "/v1/config");
diff --git a/crates/paimon/src/api/rest_api.rs
b/crates/paimon/src/api/rest_api.rs
index 5110133..cdde2b2 100644
--- a/crates/paimon/src/api/rest_api.rs
+++ b/crates/paimon/src/api/rest_api.rs
@@ -29,13 +29,13 @@ use crate::spec::{Partition, PartitionStatistics, Schema,
SchemaChange, Snapshot
use crate::Result;
use super::api_request::{
- AlterDatabaseRequest, AlterTableRequest, CreateDatabaseRequest,
CreateFunctionRequest,
- CreateTableRequest, CreateViewRequest, RenameTableRequest,
+ AlterDatabaseRequest, AlterTableRequest, AuthTableQueryRequest,
CreateDatabaseRequest,
+ CreateFunctionRequest, CreateTableRequest, CreateViewRequest,
RenameTableRequest,
};
use super::api_response::{
- ConfigResponse, GetDatabaseResponse, GetFunctionResponse,
GetTableResponse, GetViewResponse,
- ListDatabasesResponse, ListFunctionsResponse, ListPartitionsResponse,
ListTablesResponse,
- ListViewsResponse, PagedList,
+ AuthTableQueryResponse, ConfigResponse, GetDatabaseResponse,
GetFunctionResponse,
+ GetTableResponse, GetViewResponse, ListDatabasesResponse,
ListFunctionsResponse,
+ ListPartitionsResponse, ListTablesResponse, ListViewsResponse, PagedList,
};
use super::auth::{AuthProviderFactory, RESTAuthFunction};
use super::resource_paths::ResourcePaths;
@@ -628,6 +628,21 @@ impl RESTApi {
self.client.get(&path, None::<&[(&str, &str)]>).await
}
+ /// Fetch the per-user row filter and column masking for a
`query-auth.enabled`
+ /// table. `select` is the query's projected columns (`None` = all
columns).
+ pub async fn auth_table_query(
+ &self,
+ identifier: &Identifier,
+ select: Option<Vec<String>>,
+ ) -> Result<AuthTableQueryResponse> {
+ let database = identifier.database();
+ let table = identifier.object();
+ validate_non_empty_multi(&[(database, "database name"), (table, "table
name")])?;
+ let path = self.resource_paths.auth_table(database, table);
+ let request = AuthTableQueryRequest::new(select);
+ self.client.post(&path, &request).await
+ }
+
// ==================== Commit Operations ====================
/// Commit a snapshot for a table.