From 99a167c9a9467607ef320fafe99a9b7dbedb2bc1 Mon Sep 17 00:00:00 2001 From: Li Jiajia Date: Tue, 13 Jan 2026 16:31:39 +0800 Subject: [PATCH 1/2] [rest] add column masking rules in authTableQuery response --- docs/static/rest-catalog-open-api.yaml | 5 ++ .../java/org/apache/paimon/rest/RESTApi.java | 18 +++---- .../responses/AuthTableQueryResponse.java | 16 +++++- .../paimon/catalog/AbstractCatalog.java | 2 +- .../org/apache/paimon/catalog/Catalog.java | 7 +-- .../paimon/catalog/DelegateCatalog.java | 2 +- .../paimon/catalog/TableQueryAuthResult.java | 53 +++++++++++++++++++ .../org/apache/paimon/rest/RESTCatalog.java | 53 ++++++++++++++++++- .../paimon/table/CatalogEnvironment.java | 4 +- .../paimon/table/source/TableQueryAuth.java | 4 +- .../apache/paimon/rest/RESTCatalogServer.java | 3 +- 11 files changed, 145 insertions(+), 22 deletions(-) create mode 100644 paimon-core/src/main/java/org/apache/paimon/catalog/TableQueryAuthResult.java diff --git a/docs/static/rest-catalog-open-api.yaml b/docs/static/rest-catalog-open-api.yaml index fcf5c53d11a0..88e7396514c9 100644 --- a/docs/static/rest-catalog-open-api.yaml +++ b/docs/static/rest-catalog-open-api.yaml @@ -2923,6 +2923,11 @@ components: type: array items: type: string + columnMasking: + type: object + description: Column masking rules as a map from column name to transform entry JSON string. + additionalProperties: + type: string AlterDatabaseRequest: type: object properties: diff --git a/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java b/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java index a6bf162d55a6..99266d1dba34 100644 --- a/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java +++ b/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java @@ -672,21 +672,19 @@ public void alterTable(Identifier identifier, List changes) { * * @param identifier database name and table name. * @param select select columns, null if select all - * @return additional filter for row level access control + * @return additional filter for row level access control and column masking rules * @throws NoSuchResourceException Exception thrown on HTTP 404 means the table not exists * @throws ForbiddenException Exception thrown on HTTP 403 means don't have the permission for * this table */ - public List authTableQuery(Identifier identifier, @Nullable List select) { + public AuthTableQueryResponse authTableQuery( + Identifier identifier, @Nullable List select) { AuthTableQueryRequest request = new AuthTableQueryRequest(select); - AuthTableQueryResponse response = - client.post( - resourcePaths.authTable( - identifier.getDatabaseName(), identifier.getObjectName()), - request, - AuthTableQueryResponse.class, - restAuthFunction); - return response.filter(); + return client.post( + resourcePaths.authTable(identifier.getDatabaseName(), identifier.getObjectName()), + request, + AuthTableQueryResponse.class, + restAuthFunction); } /** diff --git a/paimon-api/src/main/java/org/apache/paimon/rest/responses/AuthTableQueryResponse.java b/paimon-api/src/main/java/org/apache/paimon/rest/responses/AuthTableQueryResponse.java index 0f833b03302a..3aab9ea14e27 100644 --- a/paimon-api/src/main/java/org/apache/paimon/rest/responses/AuthTableQueryResponse.java +++ b/paimon-api/src/main/java/org/apache/paimon/rest/responses/AuthTableQueryResponse.java @@ -27,24 +27,38 @@ import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; +import java.util.Map; /** Response for auth table query. */ @JsonIgnoreProperties(ignoreUnknown = true) public class AuthTableQueryResponse implements RESTResponse { private static final String FIELD_FILTER = "filter"; + private static final String FIELD_COLUMN_MASKING = "columnMasking"; @JsonInclude(JsonInclude.Include.NON_NULL) @JsonProperty(FIELD_FILTER) private final List filter; @JsonCreator - public AuthTableQueryResponse(@JsonProperty(FIELD_FILTER) List filter) { + public AuthTableQueryResponse( + @JsonProperty(FIELD_FILTER) List filter, + @JsonProperty(FIELD_COLUMN_MASKING) Map columnMasking) { this.filter = filter; + this.columnMasking = columnMasking; } @JsonGetter(FIELD_FILTER) public List filter() { return filter; } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonProperty(FIELD_COLUMN_MASKING) + private final Map columnMasking; + + @JsonGetter(FIELD_COLUMN_MASKING) + public Map columnMasking() { + return columnMasking; + } } diff --git a/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java b/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java index 045710356bd2..5d6c102208a1 100644 --- a/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java +++ b/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java @@ -577,7 +577,7 @@ public boolean supportsVersionManagement() { } @Override - public List authTableQuery(Identifier identifier, List select) { + public TableQueryAuthResult authTableQuery(Identifier identifier, List select) { throw new UnsupportedOperationException(); } diff --git a/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java b/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java index 74e35dde3dac..f4e7913c244a 100644 --- a/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java +++ b/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java @@ -1032,14 +1032,15 @@ void alterFunction( // ==================== Table Auth ========================== /** - * Auth table query select and get the filter for row level access control. + * Auth table query select and get the filter for row level access control and column masking + * rules. * * @param identifier path of the table to alter partitions * @param select selected fields, null if select all - * @return additional filter for row level access control + * @return additional filter for row level access control and column masking rules * @throws TableNotExistException if the table does not exist */ - List authTableQuery(Identifier identifier, @Nullable List select) + TableQueryAuthResult authTableQuery(Identifier identifier, @Nullable List select) throws TableNotExistException; // ==================== Catalog Information ========================== diff --git a/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java b/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java index 5e286191de6a..b92cd63d941f 100644 --- a/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java +++ b/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java @@ -409,7 +409,7 @@ public PagedList listPartitionsPaged( } @Override - public List authTableQuery(Identifier identifier, @Nullable List select) + public TableQueryAuthResult authTableQuery(Identifier identifier, @Nullable List select) throws TableNotExistException { return wrapped.authTableQuery(identifier, select); } diff --git a/paimon-core/src/main/java/org/apache/paimon/catalog/TableQueryAuthResult.java b/paimon-core/src/main/java/org/apache/paimon/catalog/TableQueryAuthResult.java new file mode 100644 index 000000000000..dcc94031a8ec --- /dev/null +++ b/paimon-core/src/main/java/org/apache/paimon/catalog/TableQueryAuthResult.java @@ -0,0 +1,53 @@ +/* + * 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. + */ + +package org.apache.paimon.catalog; + +import org.apache.paimon.predicate.Predicate; +import org.apache.paimon.predicate.Transform; + +import javax.annotation.Nullable; + +import java.util.Collections; +import java.util.Map; + +/** Auth result for table query, including row level filter and optional column masking rules. */ +public class TableQueryAuthResult { + + @Nullable private final Predicate rowFilter; + private final Map columnMasking; + + public TableQueryAuthResult( + @Nullable Predicate rowFilter, Map columnMasking) { + this.rowFilter = rowFilter; + this.columnMasking = columnMasking == null ? Collections.emptyMap() : columnMasking; + } + + public static TableQueryAuthResult empty() { + return new TableQueryAuthResult(null, Collections.emptyMap()); + } + + @Nullable + public Predicate rowFilter() { + return rowFilter; + } + + public Map columnMasking() { + return columnMasking; + } +} diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java index d8254bb58fee..8f57879cd40b 100644 --- a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java +++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java @@ -30,6 +30,7 @@ import org.apache.paimon.catalog.Identifier; import org.apache.paimon.catalog.PropertyChange; import org.apache.paimon.catalog.TableMetadata; +import org.apache.paimon.catalog.TableQueryAuthResult; import org.apache.paimon.fs.FileIO; import org.apache.paimon.fs.Path; import org.apache.paimon.function.Function; @@ -37,12 +38,17 @@ import org.apache.paimon.options.Options; import org.apache.paimon.partition.Partition; import org.apache.paimon.partition.PartitionStatistics; +import org.apache.paimon.predicate.And; +import org.apache.paimon.predicate.CompoundPredicate; +import org.apache.paimon.predicate.Predicate; +import org.apache.paimon.predicate.Transform; import org.apache.paimon.rest.exceptions.AlreadyExistsException; import org.apache.paimon.rest.exceptions.BadRequestException; import org.apache.paimon.rest.exceptions.ForbiddenException; import org.apache.paimon.rest.exceptions.NoSuchResourceException; import org.apache.paimon.rest.exceptions.NotImplementedException; import org.apache.paimon.rest.exceptions.ServiceFailureException; +import org.apache.paimon.rest.responses.AuthTableQueryResponse; import org.apache.paimon.rest.responses.ErrorResponse; import org.apache.paimon.rest.responses.GetDatabaseResponse; import org.apache.paimon.rest.responses.GetFunctionResponse; @@ -58,6 +64,7 @@ import org.apache.paimon.table.TableSnapshot; import org.apache.paimon.table.sink.BatchTableCommit; import org.apache.paimon.table.system.SystemTableLoader; +import org.apache.paimon.utils.JsonSerdeUtil; import org.apache.paimon.utils.Pair; import org.apache.paimon.utils.SnapshotNotExistException; import org.apache.paimon.view.View; @@ -79,6 +86,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.TreeMap; import java.util.stream.Collectors; import static org.apache.paimon.CoreOptions.BRANCH; @@ -524,11 +532,52 @@ public void alterTable( } @Override - public List authTableQuery(Identifier identifier, @Nullable List select) + public TableQueryAuthResult authTableQuery(Identifier identifier, @Nullable List select) throws TableNotExistException { checkNotSystemTable(identifier, "authTable"); try { - return api.authTableQuery(identifier, select); + AuthTableQueryResponse response = api.authTableQuery(identifier, select); + + List predicateJsons = response == null ? null : response.filter(); + Predicate rowFilter = null; + if (predicateJsons != null && !predicateJsons.isEmpty()) { + List predicates = new ArrayList<>(); + for (String json : predicateJsons) { + if (json == null || json.trim().isEmpty()) { + continue; + } + Predicate predicate = JsonSerdeUtil.fromJson(json, Predicate.class); + if (predicate != null) { + predicates.add(predicate); + } + } + if (predicates.size() == 1) { + rowFilter = predicates.get(0); + } else if (!predicates.isEmpty()) { + rowFilter = new CompoundPredicate(And.INSTANCE, predicates); + } + } + + Map columnMasking = new TreeMap<>(); + Map maskingJsons = response == null ? null : response.columnMasking(); + if (maskingJsons != null && !maskingJsons.isEmpty()) { + for (Map.Entry e : maskingJsons.entrySet()) { + String column = e.getKey(); + String json = e.getValue(); + if (column == null + || column.trim().isEmpty() + || json == null + || json.trim().isEmpty()) { + continue; + } + Transform transform = JsonSerdeUtil.fromJson(json, Transform.class); + if (transform == null) { + continue; + } + columnMasking.put(column, transform); + } + } + return new TableQueryAuthResult(rowFilter, columnMasking); } catch (NoSuchResourceException e) { throw new TableNotExistException(identifier); } catch (ForbiddenException e) { diff --git a/paimon-core/src/main/java/org/apache/paimon/table/CatalogEnvironment.java b/paimon-core/src/main/java/org/apache/paimon/table/CatalogEnvironment.java index 0f61f8fa0978..d07e009214bb 100644 --- a/paimon-core/src/main/java/org/apache/paimon/table/CatalogEnvironment.java +++ b/paimon-core/src/main/java/org/apache/paimon/table/CatalogEnvironment.java @@ -28,6 +28,7 @@ import org.apache.paimon.catalog.Identifier; import org.apache.paimon.catalog.RenamingSnapshotCommit; import org.apache.paimon.catalog.SnapshotCommit; +import org.apache.paimon.catalog.TableQueryAuthResult; import org.apache.paimon.operation.Lock; import org.apache.paimon.table.source.TableQueryAuth; import org.apache.paimon.tag.SnapshotLoaderImpl; @@ -37,7 +38,6 @@ import javax.annotation.Nullable; import java.io.Serializable; -import java.util.Collections; import java.util.Optional; /** Catalog environment in table which contains log factory, metastore client factory. */ @@ -154,7 +154,7 @@ public CatalogEnvironment copy(Identifier identifier) { public TableQueryAuth tableQueryAuth(CoreOptions options) { if (!options.queryAuthEnabled() || catalogLoader == null) { - return select -> Collections.emptyList(); + return select -> TableQueryAuthResult.empty(); } return select -> { try (Catalog catalog = catalogLoader.load()) { diff --git a/paimon-core/src/main/java/org/apache/paimon/table/source/TableQueryAuth.java b/paimon-core/src/main/java/org/apache/paimon/table/source/TableQueryAuth.java index 96a0dfb3a591..3d45ec2f33b0 100644 --- a/paimon-core/src/main/java/org/apache/paimon/table/source/TableQueryAuth.java +++ b/paimon-core/src/main/java/org/apache/paimon/table/source/TableQueryAuth.java @@ -18,6 +18,8 @@ package org.apache.paimon.table.source; +import org.apache.paimon.catalog.TableQueryAuthResult; + import javax.annotation.Nullable; import java.util.List; @@ -25,5 +27,5 @@ /** Table query auth. */ public interface TableQueryAuth { - List auth(@Nullable List select); + TableQueryAuthResult auth(@Nullable List select); } diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java index 544b4903850c..94afb8cb725f 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java @@ -829,7 +829,8 @@ private MockResponse authTable(Identifier identifier, String data) throws Except } }); } - AuthTableQueryResponse response = new AuthTableQueryResponse(Collections.emptyList()); + AuthTableQueryResponse response = + new AuthTableQueryResponse(Collections.emptyList(), ImmutableMap.of()); return mockResponse(response, 200); } From 8f15ade20c639fd4dcc2b4375f7c2bba998c9a8f Mon Sep 17 00:00:00 2001 From: Li Jiajia Date: Tue, 13 Jan 2026 18:05:25 +0800 Subject: [PATCH 2/2] add test case. --- .../org/apache/paimon/rest/RESTCatalog.java | 85 ++++++++++--------- .../paimon/rest/MockRESTCatalogTest.java | 46 ++++++++++ .../apache/paimon/rest/MockRESTMessage.java | 19 +++++ .../apache/paimon/rest/RESTApiJsonTest.java | 11 +++ .../apache/paimon/rest/RESTCatalogServer.java | 13 ++- 5 files changed, 131 insertions(+), 43 deletions(-) diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java index 8f57879cd40b..c41810c0fd86 100644 --- a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java +++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java @@ -74,6 +74,8 @@ import org.apache.paimon.shade.org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + import javax.annotation.Nullable; import java.io.IOException; @@ -537,47 +539,7 @@ public TableQueryAuthResult authTableQuery(Identifier identifier, @Nullable List checkNotSystemTable(identifier, "authTable"); try { AuthTableQueryResponse response = api.authTableQuery(identifier, select); - - List predicateJsons = response == null ? null : response.filter(); - Predicate rowFilter = null; - if (predicateJsons != null && !predicateJsons.isEmpty()) { - List predicates = new ArrayList<>(); - for (String json : predicateJsons) { - if (json == null || json.trim().isEmpty()) { - continue; - } - Predicate predicate = JsonSerdeUtil.fromJson(json, Predicate.class); - if (predicate != null) { - predicates.add(predicate); - } - } - if (predicates.size() == 1) { - rowFilter = predicates.get(0); - } else if (!predicates.isEmpty()) { - rowFilter = new CompoundPredicate(And.INSTANCE, predicates); - } - } - - Map columnMasking = new TreeMap<>(); - Map maskingJsons = response == null ? null : response.columnMasking(); - if (maskingJsons != null && !maskingJsons.isEmpty()) { - for (Map.Entry e : maskingJsons.entrySet()) { - String column = e.getKey(); - String json = e.getValue(); - if (column == null - || column.trim().isEmpty() - || json == null - || json.trim().isEmpty()) { - continue; - } - Transform transform = JsonSerdeUtil.fromJson(json, Transform.class); - if (transform == null) { - continue; - } - columnMasking.put(column, transform); - } - } - return new TableQueryAuthResult(rowFilter, columnMasking); + return getTableQueryAuthResult(response); } catch (NoSuchResourceException e) { throw new TableNotExistException(identifier); } catch (ForbiddenException e) { @@ -1203,4 +1165,45 @@ private Schema inferSchemaIfExternalPaimonTable(Schema schema) throws Exception } return schema; } + + private static @NotNull TableQueryAuthResult getTableQueryAuthResult( + AuthTableQueryResponse response) { + List predicateJsons = response == null ? null : response.filter(); + Predicate rowFilter = null; + if (predicateJsons != null && !predicateJsons.isEmpty()) { + List predicates = new ArrayList<>(); + for (String json : predicateJsons) { + if (StringUtils.isEmpty(json)) { + continue; + } + Predicate predicate = JsonSerdeUtil.fromJson(json, Predicate.class); + if (predicate != null) { + predicates.add(predicate); + } + } + if (predicates.size() == 1) { + rowFilter = predicates.get(0); + } else if (!predicates.isEmpty()) { + rowFilter = new CompoundPredicate(And.INSTANCE, predicates); + } + } + + Map columnMasking = new TreeMap<>(); + Map maskingJsons = response == null ? null : response.columnMasking(); + if (maskingJsons != null && !maskingJsons.isEmpty()) { + for (Map.Entry e : maskingJsons.entrySet()) { + String column = e.getKey(); + String json = e.getValue(); + if (StringUtils.isEmpty(column) || StringUtils.isEmpty(json)) { + continue; + } + Transform transform = JsonSerdeUtil.fromJson(json, Transform.class); + if (transform == null) { + continue; + } + columnMasking.put(column, transform); + } + } + return new TableQueryAuthResult(rowFilter, columnMasking); + } } diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTCatalogTest.java b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTCatalogTest.java index 150a8e28eb96..7a52bf1af9c8 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTCatalogTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTCatalogTest.java @@ -25,8 +25,14 @@ import org.apache.paimon.catalog.Catalog; import org.apache.paimon.catalog.CatalogContext; import org.apache.paimon.catalog.Identifier; +import org.apache.paimon.catalog.TableQueryAuthResult; import org.apache.paimon.options.CatalogOptions; import org.apache.paimon.options.Options; +import org.apache.paimon.predicate.FieldRef; +import org.apache.paimon.predicate.Predicate; +import org.apache.paimon.predicate.PredicateBuilder; +import org.apache.paimon.predicate.Transform; +import org.apache.paimon.predicate.UpperTransform; import org.apache.paimon.rest.auth.AuthProvider; import org.apache.paimon.rest.auth.AuthProviderEnum; import org.apache.paimon.rest.auth.BearTokenAuthProvider; @@ -35,9 +41,12 @@ import org.apache.paimon.rest.auth.DLFTokenLoaderFactory; import org.apache.paimon.rest.auth.RESTAuthParameter; import org.apache.paimon.rest.exceptions.NotAuthorizedException; +import org.apache.paimon.rest.responses.AuthTableQueryResponse; import org.apache.paimon.rest.responses.ConfigResponse; import org.apache.paimon.schema.Schema; import org.apache.paimon.types.DataTypes; +import org.apache.paimon.types.RowType; +import org.apache.paimon.utils.JsonSerdeUtil; import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableMap; @@ -55,6 +64,7 @@ import static org.apache.paimon.catalog.Catalog.TABLE_DEFAULT_OPTION_PREFIX; import static org.apache.paimon.rest.RESTApi.HEADER_PREFIX; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -246,6 +256,42 @@ void testCreateFormatTableWhenEnableDataToken() throws Exception { catalog.dropTable(identifier, true); } + @Test + void testAuthTableQueryResponseWithColumnMasking() throws Exception { + Identifier identifier = Identifier.create("test_db", "auth_table"); + catalog.createDatabase(identifier.getDatabaseName(), true); + catalog.createTable( + Identifier.create(identifier.getDatabaseName(), identifier.getTableName()), + DEFAULT_TABLE_SCHEMA, + false); + + PredicateBuilder builder = + new PredicateBuilder(RowType.of(DataTypes.INT(), DataTypes.STRING())); + Predicate predicate = builder.equal(0, 100); + String predicateJson = JsonSerdeUtil.toFlatJson(predicate); + + Transform transform = + new UpperTransform( + Collections.singletonList(new FieldRef(1, "col2", DataTypes.STRING()))); + String transformJson = JsonSerdeUtil.toFlatJson(transform); + + // Set up mock response with filter and columnMasking + List filter = Collections.singletonList(predicateJson); + Map columnMasking = new HashMap<>(); + columnMasking.put("col2", transformJson); + AuthTableQueryResponse response = new AuthTableQueryResponse(filter, columnMasking); + restCatalogServer.setTableQueryAuthResponse(identifier, response); + + TableQueryAuthResult result = catalog.authTableQuery(identifier, null); + assertThat(result.rowFilter()).isEqualTo(predicate); + assertThat(result.columnMasking()).isNotEmpty(); + assertThat(result.columnMasking()).containsKey("col2"); + assertThat(result.columnMasking().get("col2")).isEqualTo(transform); + + catalog.dropTable(identifier, true); + catalog.dropDatabase(identifier.getDatabaseName(), true, true); + } + private void checkHeader(String headerName, String headerValue) { // Verify that the header were included in the requests List> receivedHeaders = restCatalogServer.getReceivedHeaders(); diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java index f487815e4d61..d202d41ea1c3 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java @@ -24,6 +24,10 @@ import org.apache.paimon.function.FunctionDefinition; import org.apache.paimon.function.FunctionImpl; import org.apache.paimon.partition.Partition; +import org.apache.paimon.predicate.Equal; +import org.apache.paimon.predicate.FieldRef; +import org.apache.paimon.predicate.LeafPredicate; +import org.apache.paimon.predicate.UpperTransform; import org.apache.paimon.rest.requests.AlterDatabaseRequest; import org.apache.paimon.rest.requests.AlterFunctionRequest; import org.apache.paimon.rest.requests.AlterTableRequest; @@ -35,6 +39,7 @@ import org.apache.paimon.rest.requests.RenameTableRequest; import org.apache.paimon.rest.requests.RollbackTableRequest; import org.apache.paimon.rest.responses.AlterDatabaseResponse; +import org.apache.paimon.rest.responses.AuthTableQueryResponse; import org.apache.paimon.rest.responses.GetDatabaseResponse; import org.apache.paimon.rest.responses.GetFunctionResponse; import org.apache.paimon.rest.responses.GetTableResponse; @@ -52,6 +57,7 @@ import org.apache.paimon.types.DataTypes; import org.apache.paimon.types.IntType; import org.apache.paimon.types.RowType; +import org.apache.paimon.utils.JsonSerdeUtil; import org.apache.paimon.view.ViewChange; import org.apache.paimon.view.ViewSchema; @@ -382,4 +388,17 @@ private static Schema schema(Map options) { List primaryKeys = Arrays.asList("f0", "f1"); return new Schema(fields, partitionKeys, primaryKeys, options, "comment"); } + + public static AuthTableQueryResponse authTableQueryResponse() { + LeafPredicate predicate = + new LeafPredicate( + Equal.INSTANCE, DataTypes.INT(), 0, "id", Collections.singletonList(1)); + List filter = java.util.Arrays.asList(JsonSerdeUtil.toFlatJson(predicate)); + Map columnMasking = new HashMap<>(); + FieldRef fieldRef = new FieldRef(1, "f1", DataTypes.STRING()); + UpperTransform upperTransform = new UpperTransform(Collections.singletonList(fieldRef)); + columnMasking.put("c1", JsonSerdeUtil.toFlatJson(upperTransform)); + + return new AuthTableQueryResponse(filter, columnMasking); + } } diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/RESTApiJsonTest.java b/paimon-core/src/test/java/org/apache/paimon/rest/RESTApiJsonTest.java index 10aea7f3b40e..1b8b1f71d0c0 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTApiJsonTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTApiJsonTest.java @@ -29,6 +29,7 @@ import org.apache.paimon.rest.requests.RenameTableRequest; import org.apache.paimon.rest.requests.RollbackTableRequest; import org.apache.paimon.rest.responses.AlterDatabaseResponse; +import org.apache.paimon.rest.responses.AuthTableQueryResponse; import org.apache.paimon.rest.responses.ConfigResponse; import org.apache.paimon.rest.responses.ErrorResponse; import org.apache.paimon.rest.responses.GetDatabaseResponse; @@ -298,4 +299,14 @@ public void alterFunctionRequestParseTest() throws JsonProcessingException { AlterFunctionRequest parseData = RESTApi.fromJson(requestStr, AlterFunctionRequest.class); assertEquals(parseData.changes().size(), request.changes().size()); } + + @Test + public void authTableQueryResponseParseTest() throws Exception { + AuthTableQueryResponse response = MockRESTMessage.authTableQueryResponse(); + String responseStr = RESTApi.toJson(response); + AuthTableQueryResponse parseData = + RESTApi.fromJson(responseStr, AuthTableQueryResponse.class); + assertEquals(response.filter(), parseData.filter()); + assertEquals(response.columnMasking(), parseData.columnMasking()); + } } diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java index 94afb8cb725f..ff4d932501c2 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java @@ -107,6 +107,7 @@ import org.apache.paimon.view.ViewImpl; import org.apache.paimon.view.ViewSchema; +import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableMap; import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonProcessingException; import org.apache.paimon.shade.org.apache.commons.lang3.StringUtils; @@ -116,7 +117,6 @@ import okhttp3.mockwebserver.RecordedRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.testcontainers.shaded.com.google.common.collect.ImmutableMap; import javax.annotation.Nullable; @@ -185,6 +185,8 @@ public class RESTCatalogServer { private final List noPermissionTables = new ArrayList<>(); private final Map functionStore = new HashMap<>(); private final Map> columnAuthHandler = new HashMap<>(); + private final Map tableQueryAuthResponseHandler = + new HashMap<>(); public final ConfigResponse configResponse; public final String warehouse; @@ -266,6 +268,10 @@ public void addTableColumnAuth(Identifier identifier, List select) { columnAuthHandler.put(identifier.getFullName(), select); } + public void setTableQueryAuthResponse(Identifier identifier, AuthTableQueryResponse response) { + tableQueryAuthResponseHandler.put(identifier.getFullName(), response); + } + public RESTToken getDataToken(Identifier identifier) { return DataTokenStore.getDataToken(warehouse, identifier.getFullName()); } @@ -830,7 +836,10 @@ private MockResponse authTable(Identifier identifier, String data) throws Except }); } AuthTableQueryResponse response = - new AuthTableQueryResponse(Collections.emptyList(), ImmutableMap.of()); + tableQueryAuthResponseHandler.get(identifier.getFullName()); + if (response == null) { + response = new AuthTableQueryResponse(Collections.emptyList(), ImmutableMap.of()); + } return mockResponse(response, 200); }