-
Notifications
You must be signed in to change notification settings - Fork 347
Implement GenericTableCatalogAdapter #1264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
dd02e55
bb16a2a
a127b3f
a2c5bd9
1eb01e7
ed07f0b
c352d7c
25e0373
d06221d
43b355a
b5e360a
92560a6
351bbcf
c99a123
7ff385c
73cafdf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -183,4 +183,11 @@ protected FeatureConfiguration( | |
| "How many times to retry refreshing metadata when the previous error was retryable") | ||
| .defaultValue(2) | ||
| .buildFeatureConfiguration(); | ||
|
|
||
| public static final FeatureConfiguration<Boolean> ENABLE_GENERIC_TABLES = | ||
| PolarisConfiguration.<Boolean>builder() | ||
| .key("ENABLE_GENERIC_TABLES") | ||
| .description("If true, the generic-tables endpoints are enabled") | ||
| .defaultValue(false) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess we want it to be enabled by default. Do we?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we want to keep this as false until the whole feature finish development. Once all server change is done, @eric-maynard will have a PR to switch it to true.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now the REST API is not wired up, so I think it's okay to keep it flagged off for the time being. Once the series of PR is finished and the feature is "complete" we should enable by default
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My understanding is that we will enable by default when all the relevant code parts are ready. The same shall apply for policy endpoints |
||
| .buildFeatureConfiguration(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,7 @@ | |
| public class GenericTableEntity extends TableLikeEntity { | ||
|
|
||
| public static final String FORMAT_KEY = "format"; | ||
| public static final String DOC_KEY = "doc"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a blocker: either
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The spec calls this field
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The "doc" is used as the API field, to keep things consistent, how about let's just keep it as doc. I was using doc since it was some term used in iceberg https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml#L2079. i was originally intended to use name "comment", it was a more commonly used name in other table catalog like unity and gravitino |
||
|
|
||
| public GenericTableEntity(PolarisBaseEntity sourceEntity) { | ||
| super(sourceEntity); | ||
|
|
@@ -52,6 +53,11 @@ public String getFormat() { | |
| return getInternalPropertiesAsMap().get(GenericTableEntity.FORMAT_KEY); | ||
| } | ||
|
|
||
| @JsonIgnore | ||
| public String getDoc() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Non-blocking question] Just to confirm my understanding, we reserve "properties" for generic table's location, connection info, etc, so we put other information like format and doc in the internalProperties? Do we have any convention like things in the internal properties should not expose to users?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's right, internalProperties and properties work that way right now. This is specifically relevant when we need to build the |
||
| return getInternalPropertiesAsMap().get(GenericTableEntity.DOC_KEY); | ||
| } | ||
|
|
||
| public static class Builder | ||
| extends PolarisEntity.BaseBuilder<GenericTableEntity, GenericTableEntity.Builder> { | ||
| public Builder(TableIdentifier tableIdentifier, String format) { | ||
|
|
@@ -68,6 +74,11 @@ public GenericTableEntity.Builder setFormat(String format) { | |
| return this; | ||
| } | ||
|
|
||
| public GenericTableEntity.Builder setDoc(String doc) { | ||
| internalProperties.put(GenericTableEntity.DOC_KEY, doc); | ||
| return this; | ||
| } | ||
|
|
||
| public GenericTableEntity.Builder setTableIdentifier(TableIdentifier identifier) { | ||
| Namespace namespace = identifier.namespace(); | ||
| setParentNamespace(namespace); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,277 @@ | ||
| /* | ||
| * 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.polaris.service.quarkus.catalog; | ||
|
|
||
| import io.quarkus.test.junit.QuarkusTest; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
| import org.apache.iceberg.catalog.TableIdentifier; | ||
| import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal; | ||
| import org.apache.polaris.core.entity.PolarisPrivilege; | ||
| import org.apache.polaris.service.catalog.generic.GenericTableCatalogHandlerWrapper; | ||
| import org.apache.polaris.service.quarkus.admin.PolarisAuthzTestBase; | ||
| import org.assertj.core.api.Assertions; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| @QuarkusTest | ||
| public class GenericTableCatalogHandlerWrapperAuthzTest extends PolarisAuthzTestBase { | ||
|
|
||
| private GenericTableCatalogHandlerWrapper newWrapper() { | ||
| return newWrapper(Set.of()); | ||
| } | ||
|
|
||
| private GenericTableCatalogHandlerWrapper newWrapper(Set<String> activatedPrincipalRoles) { | ||
| return newWrapper(activatedPrincipalRoles, CATALOG_NAME); | ||
| } | ||
|
|
||
| private GenericTableCatalogHandlerWrapper newWrapper( | ||
| Set<String> activatedPrincipalRoles, String catalogName) { | ||
| final AuthenticatedPolarisPrincipal authenticatedPrincipal = | ||
| new AuthenticatedPolarisPrincipal(principalEntity, activatedPrincipalRoles); | ||
| return new GenericTableCatalogHandlerWrapper( | ||
| callContext, | ||
| entityManager, | ||
| metaStoreManager, | ||
| securityContext(authenticatedPrincipal, activatedPrincipalRoles), | ||
| catalogName, | ||
| polarisAuthorizer); | ||
| } | ||
|
|
||
| /** | ||
| * Tests each "sufficient" privilege individually using CATALOG_ROLE1 by granting at the | ||
| * CATALOG_NAME level, revoking after each test, and also ensuring that the request fails after | ||
| * revocation. | ||
| * | ||
| * @param sufficientPrivileges List of privileges that should be sufficient each in isolation for | ||
| * {@code action} to succeed. | ||
| * @param action The operation being tested; could also be multiple operations that should all | ||
| * succeed with the sufficient privilege | ||
| * @param cleanupAction If non-null, additional action to run to "undo" a previous success action | ||
| * in case the action has side effects. Called before revoking the sufficient privilege; | ||
| * either the cleanup privileges must be latent, or the cleanup action could be run with | ||
| * PRINCIPAL_ROLE2 while runnint {@code action} with PRINCIPAL_ROLE1. | ||
| */ | ||
| private void doTestSufficientPrivileges( | ||
| List<PolarisPrivilege> sufficientPrivileges, Runnable action, Runnable cleanupAction) { | ||
| doTestSufficientPrivilegeSets( | ||
| sufficientPrivileges.stream().map(priv -> Set.of(priv)).toList(), | ||
| action, | ||
| cleanupAction, | ||
| PRINCIPAL_NAME); | ||
| } | ||
|
|
||
| /** | ||
| * @param sufficientPrivileges each set of concurrent privileges expected to be sufficient | ||
| * together. | ||
| * @param action | ||
| * @param cleanupAction | ||
| * @param principalName | ||
| */ | ||
| private void doTestSufficientPrivilegeSets( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These helpers seems identical/similar to ones in IcebergCatalogHandlerWrapperAuthzTest. Probably in a follow-up PR, shall we consider extracting these to testBase or some util classes?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Totally agree. Actually, I want to refactor the Generic table & Iceberg table catalogs & handlers as well as the tests to see if we can find more common code. But for now, I just kept it totally isolated to make the PR mostly additive. |
||
| List<Set<PolarisPrivilege>> sufficientPrivileges, | ||
| Runnable action, | ||
| Runnable cleanupAction, | ||
| String principalName) { | ||
| doTestSufficientPrivilegeSets( | ||
| sufficientPrivileges, action, cleanupAction, principalName, CATALOG_NAME); | ||
| } | ||
|
|
||
| /** | ||
| * @param sufficientPrivileges each set of concurrent privileges expected to be sufficient | ||
| * together. | ||
| * @param action | ||
| * @param cleanupAction | ||
| * @param principalName | ||
| * @param catalogName | ||
| */ | ||
| private void doTestSufficientPrivilegeSets( | ||
| List<Set<PolarisPrivilege>> sufficientPrivileges, | ||
| Runnable action, | ||
| Runnable cleanupAction, | ||
| String principalName, | ||
| String catalogName) { | ||
| doTestSufficientPrivilegeSets( | ||
| sufficientPrivileges, | ||
| action, | ||
| cleanupAction, | ||
| principalName, | ||
| (privilege) -> | ||
| adminService.grantPrivilegeOnCatalogToRole(catalogName, CATALOG_ROLE1, privilege), | ||
| (privilege) -> | ||
| adminService.revokePrivilegeOnCatalogFromRole(catalogName, CATALOG_ROLE1, privilege)); | ||
| } | ||
|
|
||
| private void doTestInsufficientPrivileges( | ||
| List<PolarisPrivilege> insufficientPrivileges, Runnable action) { | ||
| doTestInsufficientPrivileges(insufficientPrivileges, PRINCIPAL_NAME, action); | ||
| } | ||
|
|
||
| /** | ||
| * Tests each "insufficient" privilege individually using CATALOG_ROLE1 by granting at the | ||
| * CATALOG_NAME level, ensuring the action fails, then revoking after each test case. | ||
| */ | ||
| private void doTestInsufficientPrivileges( | ||
| List<PolarisPrivilege> insufficientPrivileges, String principalName, Runnable action) { | ||
| doTestInsufficientPrivileges( | ||
| insufficientPrivileges, | ||
| principalName, | ||
| action, | ||
| (privilege) -> | ||
| adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), | ||
| (privilege) -> | ||
| adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); | ||
| } | ||
|
|
||
| @Test | ||
| public void testListGenericTablesAllSufficientPrivileges() { | ||
| doTestSufficientPrivileges( | ||
| List.of( | ||
| PolarisPrivilege.TABLE_LIST, | ||
| PolarisPrivilege.TABLE_READ_PROPERTIES, | ||
| PolarisPrivilege.TABLE_WRITE_PROPERTIES, | ||
| PolarisPrivilege.TABLE_READ_DATA, | ||
| PolarisPrivilege.TABLE_WRITE_DATA, | ||
| PolarisPrivilege.TABLE_CREATE, | ||
| PolarisPrivilege.TABLE_FULL_METADATA, | ||
| PolarisPrivilege.CATALOG_MANAGE_CONTENT), | ||
| () -> newWrapper().listGenericTables(NS1A), | ||
| null /* cleanupAction */); | ||
| } | ||
|
|
||
| @Test | ||
| public void testListGenericTablesInsufficientPermissions() { | ||
| doTestInsufficientPrivileges( | ||
| List.of( | ||
| PolarisPrivilege.NAMESPACE_FULL_METADATA, | ||
| PolarisPrivilege.VIEW_FULL_METADATA, | ||
| PolarisPrivilege.TABLE_DROP), | ||
| () -> newWrapper().listGenericTables(NS1A)); | ||
| } | ||
|
|
||
| @Test | ||
| public void testCreateGenericTableAllSufficientPrivileges() { | ||
| Assertions.assertThat( | ||
| adminService.grantPrivilegeOnCatalogToRole( | ||
| CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_DROP)) | ||
| .isTrue(); | ||
| Assertions.assertThat( | ||
| adminService.grantPrivilegeOnCatalogToRole( | ||
| CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_WRITE_DATA)) | ||
| .isTrue(); | ||
|
|
||
| final TableIdentifier newtable = TableIdentifier.of(NS2, "newtable"); | ||
|
|
||
| // Use PRINCIPAL_ROLE1 for privilege-testing, PRINCIPAL_ROLE2 for cleanup. | ||
| doTestSufficientPrivileges( | ||
| List.of( | ||
| PolarisPrivilege.TABLE_CREATE, | ||
| PolarisPrivilege.TABLE_FULL_METADATA, | ||
| PolarisPrivilege.CATALOG_MANAGE_CONTENT), | ||
| () -> { | ||
| newWrapper(Set.of(PRINCIPAL_ROLE1)) | ||
| .createGenericTable(newtable, "format", "doc", Map.of()); | ||
| }, | ||
| () -> { | ||
| newWrapper(Set.of(PRINCIPAL_ROLE2)).dropGenericTable(newtable); | ||
| }); | ||
| } | ||
|
|
||
| @Test | ||
| public void testCreateGenericTableInsufficientPermissions() { | ||
| doTestInsufficientPrivileges( | ||
| List.of( | ||
| PolarisPrivilege.NAMESPACE_FULL_METADATA, | ||
| PolarisPrivilege.VIEW_FULL_METADATA, | ||
| PolarisPrivilege.TABLE_DROP, | ||
| PolarisPrivilege.TABLE_READ_PROPERTIES, | ||
| PolarisPrivilege.TABLE_WRITE_PROPERTIES, | ||
| PolarisPrivilege.TABLE_READ_DATA, | ||
| PolarisPrivilege.TABLE_WRITE_DATA, | ||
| PolarisPrivilege.TABLE_LIST), | ||
| () -> { | ||
| newWrapper(Set.of(PRINCIPAL_ROLE1)) | ||
| .createGenericTable(TableIdentifier.of(NS2, "newtable"), "format", "doc", Map.of()); | ||
| }); | ||
| } | ||
|
|
||
| @Test | ||
| public void testLoadGenericTableSufficientPrivileges() { | ||
| doTestSufficientPrivileges( | ||
| List.of( | ||
| PolarisPrivilege.TABLE_READ_PROPERTIES, | ||
| PolarisPrivilege.TABLE_WRITE_PROPERTIES, | ||
| PolarisPrivilege.TABLE_READ_DATA, | ||
| PolarisPrivilege.TABLE_WRITE_DATA, | ||
| PolarisPrivilege.TABLE_FULL_METADATA, | ||
| PolarisPrivilege.CATALOG_MANAGE_CONTENT), | ||
| () -> newWrapper().loadGenericTable(TABLE_NS1_1_GENERIC), | ||
| null /* cleanupAction */); | ||
| } | ||
|
|
||
| @Test | ||
| public void testLoadTableInsufficientPermissions() { | ||
| doTestInsufficientPrivileges( | ||
| List.of( | ||
| PolarisPrivilege.NAMESPACE_FULL_METADATA, | ||
| PolarisPrivilege.VIEW_FULL_METADATA, | ||
| PolarisPrivilege.TABLE_CREATE, | ||
| PolarisPrivilege.TABLE_LIST, | ||
| PolarisPrivilege.TABLE_DROP), | ||
| () -> newWrapper().loadGenericTable(TABLE_NS1_1_GENERIC)); | ||
| } | ||
|
|
||
| @Test | ||
| public void testDropGenericTableAllSufficientPrivileges() { | ||
| Assertions.assertThat( | ||
| adminService.grantPrivilegeOnCatalogToRole( | ||
| CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_CREATE)) | ||
| .isTrue(); | ||
|
|
||
| doTestSufficientPrivileges( | ||
| List.of( | ||
| PolarisPrivilege.TABLE_DROP, | ||
| PolarisPrivilege.TABLE_FULL_METADATA, | ||
| PolarisPrivilege.CATALOG_MANAGE_CONTENT), | ||
| () -> { | ||
| newWrapper(Set.of(PRINCIPAL_ROLE1)).dropGenericTable(TABLE_NS1_1_GENERIC); | ||
| }, | ||
| () -> { | ||
| newWrapper(Set.of(PRINCIPAL_ROLE2)) | ||
| .createGenericTable(TABLE_NS1_1_GENERIC, "format", "doc", Map.of()); | ||
| }); | ||
| } | ||
|
|
||
| @Test | ||
| public void testDropGenericTableInsufficientPermissions() { | ||
| doTestInsufficientPrivileges( | ||
| List.of( | ||
| PolarisPrivilege.NAMESPACE_FULL_METADATA, | ||
| PolarisPrivilege.VIEW_FULL_METADATA, | ||
| PolarisPrivilege.TABLE_CREATE, | ||
| PolarisPrivilege.TABLE_READ_PROPERTIES, | ||
| PolarisPrivilege.TABLE_WRITE_PROPERTIES, | ||
| PolarisPrivilege.TABLE_READ_DATA, | ||
| PolarisPrivilege.TABLE_WRITE_DATA, | ||
| PolarisPrivilege.TABLE_LIST), | ||
| () -> { | ||
| newWrapper(Set.of(PRINCIPAL_ROLE1)).dropGenericTable(TABLE_NS1_1_GENERIC); | ||
| }); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor Question: how do we configure this? Do we just put the key in the file application.properties like this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The syntax is a little different, but basically yeah. It will be similar to existing configs, so:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we will need a doc for this config. Not a blocker though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The config
descriptions are meant to be self-describing, but I really want us to auto-generate some docs based on these. I think we've talked about this for a while but not sure if anyone is working on it yet