diff --git a/api/src/main/java/org/apache/gravitino/Catalog.java b/api/src/main/java/org/apache/gravitino/Catalog.java index 052a04d9484..431a798d51d 100644 --- a/api/src/main/java/org/apache/gravitino/Catalog.java +++ b/api/src/main/java/org/apache/gravitino/Catalog.java @@ -21,6 +21,7 @@ import java.util.Locale; import java.util.Map; import org.apache.gravitino.annotation.Evolving; +import org.apache.gravitino.authorization.SupportsRoles; import org.apache.gravitino.file.FilesetCatalog; import org.apache.gravitino.messaging.TopicCatalog; import org.apache.gravitino.rel.TableCatalog; @@ -181,4 +182,12 @@ default TopicCatalog asTopicCatalog() throws UnsupportedOperationException { default SupportsTags supportsTags() throws UnsupportedOperationException { throw new UnsupportedOperationException("Catalog does not support tag operations"); } + + /** + * @return the {@link SupportsRoles} if the catalog supports role operations. + * @throws UnsupportedOperationException if the catalog does not support role operations. + */ + default SupportsRoles supportsRoles() throws UnsupportedOperationException { + throw new UnsupportedOperationException("Catalog does not support role operations"); + } } diff --git a/api/src/main/java/org/apache/gravitino/Metalake.java b/api/src/main/java/org/apache/gravitino/Metalake.java index 6b4ac76ba3c..fb6fdbee094 100644 --- a/api/src/main/java/org/apache/gravitino/Metalake.java +++ b/api/src/main/java/org/apache/gravitino/Metalake.java @@ -20,6 +20,7 @@ import java.util.Map; import org.apache.gravitino.annotation.Evolving; +import org.apache.gravitino.authorization.SupportsRoles; /** * The interface of a metalake. The metalake is the top level entity in the Apache Gravitino system, @@ -50,4 +51,12 @@ public interface Metalake extends Auditable { * @return The properties of the metalake. */ Map properties(); + + /** + * @return the {@link SupportsRoles} if the metalake supports role operations. + * @throws UnsupportedOperationException if the metalake does not support role operations. + */ + default SupportsRoles supportsRoles() { + throw new UnsupportedOperationException("Metalake does not support role operations."); + } } diff --git a/api/src/main/java/org/apache/gravitino/Schema.java b/api/src/main/java/org/apache/gravitino/Schema.java index 872b0a25e33..7cedf94f694 100644 --- a/api/src/main/java/org/apache/gravitino/Schema.java +++ b/api/src/main/java/org/apache/gravitino/Schema.java @@ -22,6 +22,7 @@ import java.util.Map; import javax.annotation.Nullable; import org.apache.gravitino.annotation.Evolving; +import org.apache.gravitino.authorization.SupportsRoles; import org.apache.gravitino.tag.SupportsTags; /** @@ -56,4 +57,12 @@ default Map properties() { default SupportsTags supportsTags() { throw new UnsupportedOperationException("Schema does not support tag operations."); } + + /** + * @return the {@link SupportsRoles} if the schema supports role operations. + * @throws UnsupportedOperationException if the schema does not support role operations. + */ + default SupportsRoles supportsRoles() { + throw new UnsupportedOperationException("Schema does not support role operations."); + } } diff --git a/api/src/main/java/org/apache/gravitino/authorization/SupportsRoles.java b/api/src/main/java/org/apache/gravitino/authorization/SupportsRoles.java new file mode 100644 index 00000000000..e83a7e20ecc --- /dev/null +++ b/api/src/main/java/org/apache/gravitino/authorization/SupportsRoles.java @@ -0,0 +1,36 @@ +/* + * 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.gravitino.authorization; + +import org.apache.gravitino.annotation.Evolving; + +/** + * Interface for supporting list role names for objects. This interface will be mixed with metadata + * objects to provide listing role operations. + */ +@Evolving +public interface SupportsRoles { + + /** + * List all the role names associated with this metadata object. + * + * @return The role name list associated with this metadata object. + */ + String[] listBindingRoleNames(); +} diff --git a/api/src/main/java/org/apache/gravitino/file/Fileset.java b/api/src/main/java/org/apache/gravitino/file/Fileset.java index ccff039da92..97afcc650f1 100644 --- a/api/src/main/java/org/apache/gravitino/file/Fileset.java +++ b/api/src/main/java/org/apache/gravitino/file/Fileset.java @@ -24,6 +24,7 @@ import org.apache.gravitino.Auditable; import org.apache.gravitino.Namespace; import org.apache.gravitino.annotation.Evolving; +import org.apache.gravitino.authorization.SupportsRoles; import org.apache.gravitino.tag.SupportsTags; /** @@ -114,4 +115,12 @@ default Map properties() { default SupportsTags supportsTags() { throw new UnsupportedOperationException("Fileset does not support tag operations."); } + + /** + * @return The {@link SupportsRoles} if the fileset supports role operations. + * @throws UnsupportedOperationException If the fileset does not support role operations. + */ + default SupportsRoles supportsRoles() { + throw new UnsupportedOperationException("Fileset does not support role operations."); + } } diff --git a/api/src/main/java/org/apache/gravitino/messaging/Topic.java b/api/src/main/java/org/apache/gravitino/messaging/Topic.java index 78607f4865d..7162c45d2b0 100644 --- a/api/src/main/java/org/apache/gravitino/messaging/Topic.java +++ b/api/src/main/java/org/apache/gravitino/messaging/Topic.java @@ -24,6 +24,7 @@ import org.apache.gravitino.Auditable; import org.apache.gravitino.Namespace; import org.apache.gravitino.annotation.Evolving; +import org.apache.gravitino.authorization.SupportsRoles; import org.apache.gravitino.tag.SupportsTags; /** @@ -58,4 +59,12 @@ default Map properties() { default SupportsTags supportsTags() { throw new UnsupportedOperationException("Topic does not support tag operations."); } + + /** + * @return the {@link SupportsRoles} if the topic supports role operations. + * @throws UnsupportedOperationException if the topic does not support role operations. + */ + default SupportsRoles supportsRoles() { + throw new UnsupportedOperationException("Topic does not support role operations."); + } } diff --git a/api/src/main/java/org/apache/gravitino/rel/Table.java b/api/src/main/java/org/apache/gravitino/rel/Table.java index c6bafb97a43..8bb9e3c1206 100644 --- a/api/src/main/java/org/apache/gravitino/rel/Table.java +++ b/api/src/main/java/org/apache/gravitino/rel/Table.java @@ -24,6 +24,7 @@ import org.apache.gravitino.Auditable; import org.apache.gravitino.Namespace; import org.apache.gravitino.annotation.Evolving; +import org.apache.gravitino.authorization.SupportsRoles; import org.apache.gravitino.rel.expressions.distributions.Distribution; import org.apache.gravitino.rel.expressions.distributions.Distributions; import org.apache.gravitino.rel.expressions.sorts.SortOrder; @@ -103,4 +104,12 @@ default SupportsPartitions supportPartitions() throws UnsupportedOperationExcept default SupportsTags supportsTags() { throw new UnsupportedOperationException("Table does not support tag operations."); } + + /** + * @return The {@link SupportsRoles} if the table supports role operations. + * @throws UnsupportedOperationException If the table does not support role operations. + */ + default SupportsRoles supportsRoles() { + throw new UnsupportedOperationException("Table does not support role operations."); + } } diff --git a/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java b/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java index 7d46af3a5a2..9359ea439b0 100644 --- a/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java +++ b/clients/client-java/src/main/java/org/apache/gravitino/client/BaseSchemaCatalog.java @@ -31,6 +31,7 @@ import org.apache.gravitino.Schema; import org.apache.gravitino.SchemaChange; import org.apache.gravitino.SupportsSchemas; +import org.apache.gravitino.authorization.SupportsRoles; import org.apache.gravitino.dto.AuditDTO; import org.apache.gravitino.dto.CatalogDTO; import org.apache.gravitino.dto.requests.SchemaCreateRequest; @@ -53,7 +54,7 @@ * create, load, alter and drop a schema with specified identifier. */ abstract class BaseSchemaCatalog extends CatalogDTO - implements Catalog, SupportsSchemas, SupportsTags { + implements Catalog, SupportsSchemas, SupportsTags, SupportsRoles { /** The REST client to send the requests. */ protected final RESTClient restClient; @@ -61,6 +62,7 @@ abstract class BaseSchemaCatalog extends CatalogDTO private final Namespace catalogNamespace; private final MetadataObjectTagOperations objectTagOperations; + private final MetadataObjectRoleOperations objectRoleOperations; BaseSchemaCatalog( Namespace catalogNamespace, @@ -84,6 +86,8 @@ abstract class BaseSchemaCatalog extends CatalogDTO MetadataObjects.of(null, this.name(), MetadataObject.Type.CATALOG); this.objectTagOperations = new MetadataObjectTagOperations(catalogNamespace.level(0), metadataObject, restClient); + this.objectRoleOperations = + new MetadataObjectRoleOperations(catalogNamespace.level(0), metadataObject, restClient); } @Override @@ -96,6 +100,11 @@ public SupportsTags supportsTags() throws UnsupportedOperationException { return this; } + @Override + public SupportsRoles supportsRoles() throws UnsupportedOperationException { + return this; + } + /** * List all the schemas under the given catalog namespace. * @@ -239,6 +248,11 @@ public String[] associateTags(String[] tagsToAdd, String[] tagsToRemove) { return objectTagOperations.associateTags(tagsToAdd, tagsToRemove); } + @Override + public String[] listBindingRoleNames() { + return objectRoleOperations.listBindingRoleNames(); + } + /** * Get the namespace of the current catalog, which is "metalake". * diff --git a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java index 32e1d7392e2..68eda6985ab 100644 --- a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java +++ b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericFileset.java @@ -26,6 +26,7 @@ import org.apache.gravitino.MetadataObject; import org.apache.gravitino.MetadataObjects; import org.apache.gravitino.Namespace; +import org.apache.gravitino.authorization.SupportsRoles; import org.apache.gravitino.dto.file.FilesetDTO; import org.apache.gravitino.exceptions.NoSuchTagException; import org.apache.gravitino.file.Fileset; @@ -33,11 +34,12 @@ import org.apache.gravitino.tag.Tag; /** Represents a generic fileset. */ -class GenericFileset implements Fileset, SupportsTags { +class GenericFileset implements Fileset, SupportsTags, SupportsRoles { private final FilesetDTO filesetDTO; private final MetadataObjectTagOperations objectTagOperations; + private final MetadataObjectRoleOperations objectRoleOperations; GenericFileset(FilesetDTO filesetDTO, RESTClient restClient, Namespace filesetNs) { this.filesetDTO = filesetDTO; @@ -46,6 +48,8 @@ class GenericFileset implements Fileset, SupportsTags { MetadataObject filesetObject = MetadataObjects.of(filesetFullName, MetadataObject.Type.FILESET); this.objectTagOperations = new MetadataObjectTagOperations(filesetNs.level(0), filesetObject, restClient); + this.objectRoleOperations = + new MetadataObjectRoleOperations(filesetNs.level(0), filesetObject, restClient); } @Override @@ -84,6 +88,11 @@ public SupportsTags supportsTags() { return this; } + @Override + public SupportsRoles supportsRoles() { + return this; + } + @Override public String[] listTags() { return objectTagOperations.listTags(); @@ -104,6 +113,11 @@ public String[] associateTags(String[] tagsToAdd, String[] tagsToRemove) { return objectTagOperations.associateTags(tagsToAdd, tagsToRemove); } + @Override + public String[] listBindingRoleNames() { + return objectRoleOperations.listBindingRoleNames(); + } + @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java index e595a53ab11..22af2e3a271 100644 --- a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java +++ b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericSchema.java @@ -23,23 +23,27 @@ import org.apache.gravitino.MetadataObject; import org.apache.gravitino.MetadataObjects; import org.apache.gravitino.Schema; +import org.apache.gravitino.authorization.SupportsRoles; import org.apache.gravitino.dto.SchemaDTO; import org.apache.gravitino.exceptions.NoSuchTagException; import org.apache.gravitino.tag.SupportsTags; import org.apache.gravitino.tag.Tag; /** Represents a generic schema. */ -class GenericSchema implements Schema, SupportsTags { +class GenericSchema implements Schema, SupportsTags, SupportsRoles { private final SchemaDTO schemaDTO; private final MetadataObjectTagOperations objectTagOperations; + private final MetadataObjectRoleOperations objectRoleOperations; GenericSchema(SchemaDTO schemaDTO, RESTClient restClient, String metalake, String catalog) { this.schemaDTO = schemaDTO; MetadataObject schemaObject = MetadataObjects.of(catalog, schemaDTO.name(), MetadataObject.Type.SCHEMA); this.objectTagOperations = new MetadataObjectTagOperations(metalake, schemaObject, restClient); + this.objectRoleOperations = + new MetadataObjectRoleOperations(metalake, schemaObject, restClient); } @Override @@ -47,6 +51,11 @@ public SupportsTags supportsTags() { return this; } + @Override + public SupportsRoles supportsRoles() { + return this; + } + @Override public String name() { return schemaDTO.name(); @@ -87,6 +96,11 @@ public String[] associateTags(String[] tagsToAdd, String[] tagsToRemove) { return objectTagOperations.associateTags(tagsToAdd, tagsToRemove); } + @Override + public String[] listBindingRoleNames() { + return objectRoleOperations.listBindingRoleNames(); + } + @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTopic.java b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTopic.java index e317df06919..9debbc8c28b 100644 --- a/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTopic.java +++ b/clients/client-java/src/main/java/org/apache/gravitino/client/GenericTopic.java @@ -25,6 +25,7 @@ import org.apache.gravitino.MetadataObject; import org.apache.gravitino.MetadataObjects; import org.apache.gravitino.Namespace; +import org.apache.gravitino.authorization.SupportsRoles; import org.apache.gravitino.dto.messaging.TopicDTO; import org.apache.gravitino.exceptions.NoSuchTagException; import org.apache.gravitino.messaging.Topic; @@ -32,11 +33,12 @@ import org.apache.gravitino.tag.Tag; /** Represents a generic topic. */ -class GenericTopic implements Topic, SupportsTags { +class GenericTopic implements Topic, SupportsTags, SupportsRoles { private final TopicDTO topicDTO; private final MetadataObjectTagOperations objectTagOperations; + private final MetadataObjectRoleOperations objectRoleOperations; GenericTopic(TopicDTO topicDTO, RESTClient restClient, Namespace topicNs) { this.topicDTO = topicDTO; @@ -45,6 +47,8 @@ class GenericTopic implements Topic, SupportsTags { MetadataObject topicObject = MetadataObjects.of(topicFullName, MetadataObject.Type.TOPIC); this.objectTagOperations = new MetadataObjectTagOperations(topicNs.level(0), topicObject, restClient); + this.objectRoleOperations = + new MetadataObjectRoleOperations(topicNs.level(0), topicObject, restClient); } @Override @@ -72,6 +76,11 @@ public SupportsTags supportsTags() { return this; } + @Override + public SupportsRoles supportsRoles() { + return this; + } + @Override public String[] listTags() { return objectTagOperations.listTags(); @@ -92,6 +101,11 @@ public String[] associateTags(String[] tagsToAdd, String[] tagsToRemove) { return objectTagOperations.associateTags(tagsToAdd, tagsToRemove); } + @Override + public String[] listBindingRoleNames() { + return objectRoleOperations.listBindingRoleNames(); + } + @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java index 58973b4cf63..8f98b6fd3c8 100644 --- a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java +++ b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java @@ -32,12 +32,14 @@ import org.apache.gravitino.Catalog; import org.apache.gravitino.CatalogChange; import org.apache.gravitino.MetadataObject; +import org.apache.gravitino.MetadataObjects; import org.apache.gravitino.NameIdentifier; import org.apache.gravitino.SupportsCatalogs; import org.apache.gravitino.authorization.Group; import org.apache.gravitino.authorization.Owner; import org.apache.gravitino.authorization.Role; import org.apache.gravitino.authorization.SecurableObject; +import org.apache.gravitino.authorization.SupportsRoles; import org.apache.gravitino.authorization.User; import org.apache.gravitino.dto.AuditDTO; import org.apache.gravitino.dto.MetalakeDTO; @@ -93,7 +95,8 @@ * catalogs as sub-level metadata collections. With {@link GravitinoMetalake}, users can list, * create, load, alter and drop a catalog with specified identifier. */ -public class GravitinoMetalake extends MetalakeDTO implements SupportsCatalogs, TagOperations { +public class GravitinoMetalake extends MetalakeDTO + implements SupportsCatalogs, TagOperations, SupportsRoles { private static final String API_METALAKES_CATALOGS_PATH = "api/metalakes/%s/catalogs/%s"; private static final String API_PERMISSION_PATH = "api/metalakes/%s/permissions/%s"; private static final String API_METALAKES_USERS_PATH = "api/metalakes/%s/users/%s"; @@ -105,6 +108,7 @@ public class GravitinoMetalake extends MetalakeDTO implements SupportsCatalogs, private static final String BLANK_PLACEHOLDER = ""; private final RESTClient restClient; + private final MetadataObjectRoleOperations metadataObjectRoleOperations; GravitinoMetalake( String name, @@ -114,6 +118,9 @@ public class GravitinoMetalake extends MetalakeDTO implements SupportsCatalogs, RESTClient restClient) { super(name, comment, properties, auditDTO); this.restClient = restClient; + this.metadataObjectRoleOperations = + new MetadataObjectRoleOperations( + name, MetadataObjects.of(null, name, MetadataObject.Type.METALAKE), restClient); } /** @@ -308,6 +315,11 @@ public void testConnection( ErrorHandlers.catalogErrorHandler().accept(resp); } + @Override + public SupportsRoles supportsRoles() { + return this; + } + /* * List all the tag names under a metalake. * @@ -896,6 +908,11 @@ public void setOwner(MetadataObject object, String ownerName, Owner.Type ownerTy resp.validate(); } + @Override + public String[] listBindingRoleNames() { + return metadataObjectRoleOperations.listBindingRoleNames(); + } + static class Builder extends MetalakeDTO.Builder { private RESTClient restClient; diff --git a/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectRoleOperations.java b/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectRoleOperations.java new file mode 100644 index 00000000000..54a6634355f --- /dev/null +++ b/clients/client-java/src/main/java/org/apache/gravitino/client/MetadataObjectRoleOperations.java @@ -0,0 +1,56 @@ +/* + * 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.gravitino.client; + +import java.util.Collections; +import java.util.Locale; +import org.apache.gravitino.MetadataObject; +import org.apache.gravitino.authorization.SupportsRoles; +import org.apache.gravitino.dto.responses.NameListResponse; + +class MetadataObjectRoleOperations implements SupportsRoles { + + private final RESTClient restClient; + + private final String roleRequestPath; + + MetadataObjectRoleOperations( + String metalakeName, MetadataObject metadataObject, RESTClient restClient) { + this.restClient = restClient; + this.roleRequestPath = + String.format( + "api/metalakes/%s/objects/%s/%s/roles", + metalakeName, + metadataObject.type().name().toLowerCase(Locale.ROOT), + metadataObject.fullName()); + } + + @Override + public String[] listBindingRoleNames() { + NameListResponse resp = + restClient.get( + roleRequestPath, + NameListResponse.class, + Collections.emptyMap(), + ErrorHandlers.roleErrorHandler()); + resp.validate(); + + return resp.getNames(); + } +} diff --git a/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalTable.java b/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalTable.java index af7e094b114..83634295f95 100644 --- a/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalTable.java +++ b/clients/client-java/src/main/java/org/apache/gravitino/client/RelationalTable.java @@ -32,6 +32,7 @@ import org.apache.gravitino.MetadataObject; import org.apache.gravitino.MetadataObjects; import org.apache.gravitino.Namespace; +import org.apache.gravitino.authorization.SupportsRoles; import org.apache.gravitino.dto.rel.TableDTO; import org.apache.gravitino.dto.rel.partitions.PartitionDTO; import org.apache.gravitino.dto.requests.AddPartitionsRequest; @@ -55,7 +56,7 @@ import org.apache.gravitino.tag.Tag; /** Represents a relational table. */ -class RelationalTable implements Table, SupportsPartitions, SupportsTags { +class RelationalTable implements Table, SupportsPartitions, SupportsTags, SupportsRoles { private static final Joiner DOT_JOINER = Joiner.on("."); @@ -66,6 +67,7 @@ class RelationalTable implements Table, SupportsPartitions, SupportsTags { private final Namespace namespace; private final MetadataObjectTagOperations objectTagOperations; + private final MetadataObjectRoleOperations objectRoleOperations; /** * Creates a new RelationalTable. @@ -94,6 +96,8 @@ private RelationalTable(Namespace namespace, TableDTO tableDTO, RESTClient restC MetadataObjects.parse(tableFullName(namespace, tableDTO.name()), MetadataObject.Type.TABLE); this.objectTagOperations = new MetadataObjectTagOperations(namespace.level(0), tableObject, restClient); + this.objectRoleOperations = + new MetadataObjectRoleOperations(namespace.level(0), tableObject, restClient); } /** @@ -284,6 +288,11 @@ public SupportsTags supportsTags() { return this; } + @Override + public SupportsRoles supportsRoles() { + return this; + } + private static String tableFullName(Namespace tableNS, String tableName) { return DOT_JOINER.join(tableNS.level(1), tableNS.level(2), tableName); } @@ -307,4 +316,9 @@ public Tag getTag(String name) throws NoSuchTagException { public String[] associateTags(String[] tagsToAdd, String[] tagsToRemove) { return objectTagOperations.associateTags(tagsToAdd, tagsToRemove); } + + @Override + public String[] listBindingRoleNames() { + return objectRoleOperations.listBindingRoleNames(); + } } diff --git a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportRoles.java b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportRoles.java new file mode 100644 index 00000000000..b22d5b21b41 --- /dev/null +++ b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportRoles.java @@ -0,0 +1,256 @@ +/* + * 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.gravitino.client; + +import static org.apache.hc.core5.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; +import static org.apache.hc.core5.http.HttpStatus.SC_NOT_FOUND; +import static org.apache.hc.core5.http.HttpStatus.SC_OK; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.Collections; +import java.util.Locale; +import org.apache.gravitino.Catalog; +import org.apache.gravitino.MetadataObject; +import org.apache.gravitino.MetadataObjects; +import org.apache.gravitino.Metalake; +import org.apache.gravitino.Namespace; +import org.apache.gravitino.Schema; +import org.apache.gravitino.authorization.SupportsRoles; +import org.apache.gravitino.dto.AuditDTO; +import org.apache.gravitino.dto.SchemaDTO; +import org.apache.gravitino.dto.file.FilesetDTO; +import org.apache.gravitino.dto.messaging.TopicDTO; +import org.apache.gravitino.dto.rel.ColumnDTO; +import org.apache.gravitino.dto.rel.TableDTO; +import org.apache.gravitino.dto.responses.ErrorResponse; +import org.apache.gravitino.dto.responses.NameListResponse; +import org.apache.gravitino.exceptions.NotFoundException; +import org.apache.gravitino.file.Fileset; +import org.apache.gravitino.messaging.Topic; +import org.apache.gravitino.rel.Table; +import org.apache.gravitino.rel.types.Types; +import org.apache.hc.core5.http.Method; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class TestSupportRoles extends TestBase { + private static final String METALAKE_NAME = "metalake"; + + private static Catalog relationalCatalog; + + private static Catalog filesetCatalog; + + private static Catalog messagingCatalog; + + private static Schema genericSchema; + + private static Table relationalTable; + + private static Fileset genericFileset; + + private static Topic genericTopic; + private static Metalake metalake; + + @BeforeAll + public static void setUp() throws Exception { + TestBase.setUp(); + metalake = TestGravitinoMetalake.createMetalake(client, METALAKE_NAME); + + relationalCatalog = + new RelationalCatalog( + Namespace.of(METALAKE_NAME), + "catalog1", + Catalog.Type.RELATIONAL, + "test", + "comment", + Collections.emptyMap(), + AuditDTO.builder().build(), + client.restClient()); + + filesetCatalog = + new FilesetCatalog( + Namespace.of(METALAKE_NAME), + "catalog2", + Catalog.Type.FILESET, + "test", + "comment", + Collections.emptyMap(), + AuditDTO.builder().build(), + client.restClient()); + + messagingCatalog = + new MessagingCatalog( + Namespace.of(METALAKE_NAME), + "catalog3", + Catalog.Type.MESSAGING, + "test", + "comment", + Collections.emptyMap(), + AuditDTO.builder().build(), + client.restClient()); + + genericSchema = + new GenericSchema( + SchemaDTO.builder() + .withName("schema1") + .withComment("comment1") + .withProperties(Collections.emptyMap()) + .withAudit(AuditDTO.builder().withCreator("test").build()) + .build(), + client.restClient(), + METALAKE_NAME, + "catalog1"); + + relationalTable = + RelationalTable.from( + Namespace.of(METALAKE_NAME, "catalog1", "schema1"), + TableDTO.builder() + .withName("table1") + .withComment("comment1") + .withColumns( + new ColumnDTO[] { + ColumnDTO.builder() + .withName("col1") + .withDataType(Types.IntegerType.get()) + .build() + }) + .withProperties(Collections.emptyMap()) + .withAudit(AuditDTO.builder().withCreator("test").build()) + .build(), + client.restClient()); + + genericFileset = + new GenericFileset( + FilesetDTO.builder() + .name("fileset1") + .comment("comment1") + .type(Fileset.Type.EXTERNAL) + .storageLocation("s3://bucket/path") + .properties(Collections.emptyMap()) + .audit(AuditDTO.builder().withCreator("test").build()) + .build(), + client.restClient(), + Namespace.of(METALAKE_NAME, "catalog1", "schema1")); + + genericTopic = + new GenericTopic( + TopicDTO.builder() + .withName("topic1") + .withComment("comment1") + .withProperties(Collections.emptyMap()) + .withAudit(AuditDTO.builder().withCreator("test").build()) + .build(), + client.restClient(), + Namespace.of(METALAKE_NAME, "catalog1", "schema1")); + } + + @Test + public void testListRolesForMetalake() throws JsonProcessingException { + testListRoles( + metalake.supportsRoles(), + MetadataObjects.of(null, metalake.name(), MetadataObject.Type.METALAKE)); + } + + @Test + public void testListRolesForCatalog() throws JsonProcessingException { + testListRoles( + relationalCatalog.supportsRoles(), + MetadataObjects.of(null, relationalCatalog.name(), MetadataObject.Type.CATALOG)); + + testListRoles( + filesetCatalog.supportsRoles(), + MetadataObjects.of(null, filesetCatalog.name(), MetadataObject.Type.CATALOG)); + + testListRoles( + messagingCatalog.supportsRoles(), + MetadataObjects.of(null, messagingCatalog.name(), MetadataObject.Type.CATALOG)); + } + + @Test + public void testListRolesForSchema() throws JsonProcessingException { + testListRoles( + genericSchema.supportsRoles(), + MetadataObjects.of("catalog1", genericSchema.name(), MetadataObject.Type.SCHEMA)); + } + + @Test + public void testListRolesForTable() throws JsonProcessingException { + testListRoles( + relationalTable.supportsRoles(), + MetadataObjects.of("catalog1.schema1", relationalTable.name(), MetadataObject.Type.TABLE)); + } + + @Test + public void testListRolesForFileset() throws JsonProcessingException { + testListRoles( + genericFileset.supportsRoles(), + MetadataObjects.of("catalog1.schema1", genericFileset.name(), MetadataObject.Type.FILESET)); + } + + @Test + public void testListRolesForTopic() throws JsonProcessingException { + testListRoles( + genericTopic.supportsRoles(), + MetadataObjects.of("catalog1.schema1", genericTopic.name(), MetadataObject.Type.TOPIC)); + } + + private void testListRoles(SupportsRoles supportsRoles, MetadataObject metadataObject) + throws JsonProcessingException { + String path = + "/api/metalakes/" + + METALAKE_NAME + + "/objects/" + + metadataObject.type().name().toLowerCase(Locale.ROOT) + + "/" + + metadataObject.fullName() + + "/roles"; + + String[] roles = new String[] {"role1", "role2"}; + NameListResponse resp = new NameListResponse(roles); + buildMockResource(Method.GET, path, null, resp, SC_OK); + + String[] actualTags = supportsRoles.listBindingRoleNames(); + Assertions.assertArrayEquals(roles, actualTags); + + // Return empty list + NameListResponse resp1 = new NameListResponse(new String[0]); + buildMockResource(Method.GET, path, null, resp1, SC_OK); + + String[] actualRoles1 = supportsRoles.listBindingRoleNames(); + Assertions.assertArrayEquals(new String[0], actualRoles1); + + // Test throw NotFoundException + ErrorResponse errorResp = + ErrorResponse.notFound(NotFoundException.class.getSimpleName(), "mock error"); + buildMockResource(Method.GET, path, null, errorResp, SC_NOT_FOUND); + + Throwable ex = + Assertions.assertThrows(NotFoundException.class, supportsRoles::listBindingRoleNames); + Assertions.assertTrue(ex.getMessage().contains("mock error")); + + // Test throw internal error + ErrorResponse errorResp1 = ErrorResponse.internalError("mock error"); + buildMockResource(Method.GET, path, null, errorResp1, SC_INTERNAL_SERVER_ERROR); + + Throwable ex1 = + Assertions.assertThrows(RuntimeException.class, supportsRoles::listBindingRoleNames); + Assertions.assertTrue(ex1.getMessage().contains("mock error")); + } +} diff --git a/core/src/main/java/org/apache/gravitino/SupportsRelationOperations.java b/core/src/main/java/org/apache/gravitino/SupportsRelationOperations.java index 617f72ab95d..d203b94deb4 100644 --- a/core/src/main/java/org/apache/gravitino/SupportsRelationOperations.java +++ b/core/src/main/java/org/apache/gravitino/SupportsRelationOperations.java @@ -40,7 +40,7 @@ enum Type { } /** - * List the entities according to a give entity in a specific relation. + * List the entities according to a given entity in a specific relation. * * @param relType The type of relation. * @param nameIdentifier The given entity identifier @@ -48,8 +48,27 @@ enum Type { * @return The list of entities * @throws IOException When occurs storage issues, it will throw IOException. */ + default List listEntitiesByRelation( + Type relType, NameIdentifier nameIdentifier, Entity.EntityType identType) throws IOException { + return listEntitiesByRelation(relType, nameIdentifier, identType, true /* allFields*/); + } + + /** + * List the entities according to a given entity in a specific relation. + * + * @param relType The type of relation. + * @param nameIdentifier The given entity identifier + * @param identType The given entity type. + * @param allFields Some fields may have a relatively high acquisition cost, EntityStore provide + * an optional setting to avoid fetching these high-cost fields to improve the performance. If + * true, the method will fetch all the fields, Otherwise, the method will fetch all the fields + * except for high-cost fields. + * @return The list of entities + * @throws IOException When occurs storage issues, it will throw IOException. + */ List listEntitiesByRelation( - Type relType, NameIdentifier nameIdentifier, Entity.EntityType identType) throws IOException; + Type relType, NameIdentifier nameIdentifier, Entity.EntityType identType, boolean allFields) + throws IOException; /** * insert a relation between two entities diff --git a/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java b/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java index 95cb304de26..3214c187fc4 100644 --- a/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java +++ b/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java @@ -20,8 +20,10 @@ import java.util.List; import java.util.Map; +import org.apache.gravitino.MetadataObject; import org.apache.gravitino.exceptions.GroupAlreadyExistsException; import org.apache.gravitino.exceptions.NoSuchGroupException; +import org.apache.gravitino.exceptions.NoSuchMetadataObjectException; import org.apache.gravitino.exceptions.NoSuchMetalakeException; import org.apache.gravitino.exceptions.NoSuchRoleException; import org.apache.gravitino.exceptions.NoSuchUserException; @@ -246,4 +248,16 @@ Role createRole( * @throws NoSuchMetalakeException If the Metalake with the given name does not exist. */ String[] listRoleNames(String metalake) throws NoSuchMetalakeException; + + /** + * Lists the role names associated the metadata object. + * + * @param metalake The Metalake of the Role. + * @return The role list. + * @throws NoSuchMetalakeException If the Metalake with the given name does not exist. + * @throws NoSuchMetadataObjectException If the Metadata object with the given name does not + * exist. + */ + String[] listRoleNamesByObject(String metalake, MetadataObject object) + throws NoSuchMetalakeException, NoSuchMetadataObjectException; } diff --git a/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java index 8872afade70..c2f2976aa3c 100644 --- a/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java +++ b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java @@ -18,14 +18,15 @@ */ package org.apache.gravitino.authorization; -import com.google.common.annotations.VisibleForTesting; import java.util.List; import java.util.Map; import org.apache.gravitino.Config; import org.apache.gravitino.Configs; import org.apache.gravitino.EntityStore; +import org.apache.gravitino.MetadataObject; import org.apache.gravitino.exceptions.GroupAlreadyExistsException; import org.apache.gravitino.exceptions.NoSuchGroupException; +import org.apache.gravitino.exceptions.NoSuchMetadataObjectException; import org.apache.gravitino.exceptions.NoSuchMetalakeException; import org.apache.gravitino.exceptions.NoSuchRoleException; import org.apache.gravitino.exceptions.NoSuchUserException; @@ -148,8 +149,9 @@ public String[] listRoleNames(String metalake) throws NoSuchMetalakeException { return roleManager.listRoleNames(metalake); } - @VisibleForTesting - RoleManager getRoleManager() { - return roleManager; + @Override + public String[] listRoleNamesByObject(String metalake, MetadataObject object) + throws NoSuchMetalakeException, NoSuchMetadataObjectException { + return roleManager.listRoleNamesByObject(metalake, object); } } diff --git a/core/src/main/java/org/apache/gravitino/authorization/FutureGrantManager.java b/core/src/main/java/org/apache/gravitino/authorization/FutureGrantManager.java index c24817ea5eb..b838e195686 100644 --- a/core/src/main/java/org/apache/gravitino/authorization/FutureGrantManager.java +++ b/core/src/main/java/org/apache/gravitino/authorization/FutureGrantManager.java @@ -20,6 +20,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.common.collect.Sets; import java.io.IOException; import java.util.List; import java.util.Map; @@ -37,7 +38,6 @@ import org.apache.gravitino.meta.GroupEntity; import org.apache.gravitino.meta.RoleEntity; import org.apache.gravitino.meta.UserEntity; -import org.glassfish.jersey.internal.guava.Sets; /** * FutureGrantManager is responsible for granting privileges to future object. When you grant a diff --git a/core/src/main/java/org/apache/gravitino/authorization/RoleManager.java b/core/src/main/java/org/apache/gravitino/authorization/RoleManager.java index 8b195894f4a..dc675fdcef5 100644 --- a/core/src/main/java/org/apache/gravitino/authorization/RoleManager.java +++ b/core/src/main/java/org/apache/gravitino/authorization/RoleManager.java @@ -27,15 +27,19 @@ import org.apache.gravitino.Entity; import org.apache.gravitino.EntityAlreadyExistsException; import org.apache.gravitino.EntityStore; +import org.apache.gravitino.MetadataObject; import org.apache.gravitino.NameIdentifier; import org.apache.gravitino.Namespace; +import org.apache.gravitino.SupportsRelationOperations; import org.apache.gravitino.exceptions.NoSuchEntityException; +import org.apache.gravitino.exceptions.NoSuchMetadataObjectException; import org.apache.gravitino.exceptions.NoSuchMetalakeException; import org.apache.gravitino.exceptions.NoSuchRoleException; import org.apache.gravitino.exceptions.RoleAlreadyExistsException; import org.apache.gravitino.meta.AuditInfo; import org.apache.gravitino.meta.RoleEntity; import org.apache.gravitino.storage.IdGenerator; +import org.apache.gravitino.utils.MetadataObjectUtil; import org.apache.gravitino.utils.PrincipalUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -148,6 +152,35 @@ String[] listRoleNames(String metalake) { } } + String[] listRoleNamesByObject(String metalake, MetadataObject object) { + try { + AuthorizationUtils.checkMetalakeExists(metalake); + + return store.relationOperations() + .listEntitiesByRelation( + SupportsRelationOperations.Type.METADATA_OBJECT_ROLE_REL, + MetadataObjectUtil.toEntityIdent(metalake, object), + MetadataObjectUtil.toEntityType(object), + false /* allFields */) + .stream() + .map(entity -> ((RoleEntity) entity).name()) + .toArray(String[]::new); + + } catch (NoSuchEntityException nse) { + LOG.error("Metadata object {} (type {}) doesn't exist", object.fullName(), object.type()); + throw new NoSuchMetadataObjectException( + "Metadata object %s (type %s) doesn't exist", object.fullName(), object.type()); + } catch (IOException ioe) { + LOG.error( + "Listing roles under metalake {} by object full name {} and type {} failed due to storage issues", + metalake, + object.fullName(), + object.type(), + ioe); + throw new RuntimeException(ioe); + } + } + private RoleEntity getRoleEntity(NameIdentifier identifier) { try { return store.get(identifier, Entity.EntityType.ROLE, RoleEntity.class); diff --git a/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java b/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java index 7882e9c8a5e..65ed2c9da09 100644 --- a/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java +++ b/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java @@ -22,6 +22,7 @@ import java.util.Map; import org.apache.gravitino.Entity; import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.MetadataObject; import org.apache.gravitino.authorization.AccessControlDispatcher; import org.apache.gravitino.authorization.AuthorizationUtils; import org.apache.gravitino.authorization.Group; @@ -32,6 +33,7 @@ import org.apache.gravitino.authorization.User; import org.apache.gravitino.exceptions.GroupAlreadyExistsException; import org.apache.gravitino.exceptions.NoSuchGroupException; +import org.apache.gravitino.exceptions.NoSuchMetadataObjectException; import org.apache.gravitino.exceptions.NoSuchMetalakeException; import org.apache.gravitino.exceptions.NoSuchRoleException; import org.apache.gravitino.exceptions.NoSuchUserException; @@ -162,4 +164,10 @@ public boolean deleteRole(String metalake, String role) throws NoSuchMetalakeExc public String[] listRoleNames(String metalake) throws NoSuchMetalakeException { return dispatcher.listRoleNames(metalake); } + + @Override + public String[] listRoleNamesByObject(String metalake, MetadataObject object) + throws NoSuchMetalakeException, NoSuchMetadataObjectException { + return dispatcher.listRoleNamesByObject(metalake, object); + } } diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java index 2b9a6d0e4ed..42b079234a8 100644 --- a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java +++ b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java @@ -369,9 +369,7 @@ public List associateTagsWithMetadataObject( @Override public List listEntitiesByRelation( - SupportsRelationOperations.Type relType, - NameIdentifier nameIdentifier, - Entity.EntityType identType) { + Type relType, NameIdentifier nameIdentifier, Entity.EntityType identType, boolean allFields) { switch (relType) { case OWNER_REL: List list = Lists.newArrayList(); @@ -382,7 +380,7 @@ public List listEntitiesByRelation( case METADATA_OBJECT_ROLE_REL: return (List) RoleMetaService.getInstance() - .listRolesByMetadataObjectIdentAndType(nameIdentifier, identType); + .listRolesByMetadataObjectIdentAndType(nameIdentifier, identType, allFields); case ROLE_GROUP_REL: if (identType == Entity.EntityType.ROLE) { return (List) GroupMetaService.getInstance().listGroupsByRoleIdent(nameIdentifier); diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java index c95db1a0710..a337e7a785e 100644 --- a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java +++ b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java @@ -188,11 +188,9 @@ public List associateTagsWithMetadataObject( @Override public List listEntitiesByRelation( - SupportsRelationOperations.Type relType, - NameIdentifier nameIdentifier, - Entity.EntityType identType) + Type relType, NameIdentifier nameIdentifier, Entity.EntityType identType, boolean allFields) throws IOException { - return backend.listEntitiesByRelation(relType, nameIdentifier, identType); + return backend.listEntitiesByRelation(relType, nameIdentifier, identType, allFields); } @Override diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java b/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java index 1e914f59ad4..915a1495022 100644 --- a/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java +++ b/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java @@ -57,21 +57,6 @@ public static RoleMetaService getInstance() { private RoleMetaService() {} - private RolePO getRolePOByMetalakeIdAndName(Long metalakeId, String roleName) { - RolePO rolePO = - SessionUtils.getWithoutCommit( - RoleMetaMapper.class, - mapper -> mapper.selectRoleMetaByMetalakeIdAndName(metalakeId, roleName)); - - if (rolePO == null) { - throw new NoSuchEntityException( - NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE, - Entity.EntityType.ROLE.name().toLowerCase(), - roleName); - } - return rolePO; - } - public Long getRoleIdByMetalakeIdAndName(Long metalakeId, String roleName) { Long roleId = SessionUtils.getWithoutCommit( @@ -93,7 +78,7 @@ public List listRolesByUserId(Long userId) { } public List listRolesByMetadataObjectIdentAndType( - NameIdentifier metadataObjectIdent, Entity.EntityType metadataObjectType) { + NameIdentifier metadataObjectIdent, Entity.EntityType metadataObjectType, boolean allFields) { String metalake = NameIdentifierUtil.getMetalake(metadataObjectIdent); long metalakeId = MetalakeMetaService.getInstance().getMetalakeIdByName(metalake); MetadataObject metadataObject = @@ -109,35 +94,18 @@ public List listRolesByMetadataObjectIdentAndType( metadataObjectId, metadataObject.type().name())); return rolePOs.stream() .map( - po -> - POConverters.fromRolePO( - po, listSecurableObjects(po), AuthorizationUtils.ofRoleNamespace(metalake))) + po -> { + if (allFields) { + return POConverters.fromRolePO( + po, listSecurableObjects(po), AuthorizationUtils.ofRoleNamespace(metalake)); + } else { + return POConverters.fromRolePO( + po, Collections.emptyList(), AuthorizationUtils.ofRoleNamespace(metalake)); + } + }) .collect(Collectors.toList()); } - private List listSecurableObjects(RolePO po) { - List securableObjectPOs = listSecurableObjectsByRoleId(po.getRoleId()); - List securableObjects = Lists.newArrayList(); - - for (SecurableObjectPO securableObjectPO : securableObjectPOs) { - String fullName = - MetadataObjectService.getMetadataObjectFullName( - securableObjectPO.getType(), securableObjectPO.getMetadataObjectId()); - if (fullName != null) { - securableObjects.add( - POConverters.fromSecurableObjectPO( - fullName, securableObjectPO, getType(securableObjectPO.getType()))); - } else { - LOG.info( - "The securable object {} {} may be deleted", - securableObjectPO.getMetadataObjectId(), - securableObjectPO.getType()); - } - } - - return securableObjects; - } - public List listRolesByGroupId(Long groupId) { return SessionUtils.getWithoutCommit( RoleMetaMapper.class, mapper -> mapper.listRolesByGroupId(groupId)); @@ -234,7 +202,7 @@ public boolean deleteRole(NameIdentifier identifier) { return true; } - private List listSecurableObjectsByRoleId(Long roleId) { + private static List listSecurableObjectsByRoleId(Long roleId) { return SessionUtils.getWithoutCommit( SecurableObjectMapper.class, mapper -> mapper.listSecurableObjectsByRoleId(roleId)); } @@ -291,11 +259,49 @@ public int deleteRoleMetasByLegacyTimeline(long legacyTimeline, int limit) { + securableObjectsCount[0]; } - private MetadataObject.Type getType(String type) { + private static List listSecurableObjects(RolePO po) { + List securableObjectPOs = listSecurableObjectsByRoleId(po.getRoleId()); + List securableObjects = Lists.newArrayList(); + + for (SecurableObjectPO securableObjectPO : securableObjectPOs) { + String fullName = + MetadataObjectService.getMetadataObjectFullName( + securableObjectPO.getType(), securableObjectPO.getMetadataObjectId()); + if (fullName != null) { + securableObjects.add( + POConverters.fromSecurableObjectPO( + fullName, securableObjectPO, getType(securableObjectPO.getType()))); + } else { + LOG.warn( + "The securable object {} {} may be deleted", + securableObjectPO.getMetadataObjectId(), + securableObjectPO.getType()); + } + } + + return securableObjects; + } + + private static RolePO getRolePOByMetalakeIdAndName(Long metalakeId, String roleName) { + RolePO rolePO = + SessionUtils.getWithoutCommit( + RoleMetaMapper.class, + mapper -> mapper.selectRoleMetaByMetalakeIdAndName(metalakeId, roleName)); + + if (rolePO == null) { + throw new NoSuchEntityException( + NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE, + Entity.EntityType.ROLE.name().toLowerCase(), + roleName); + } + return rolePO; + } + + private static MetadataObject.Type getType(String type) { return MetadataObject.Type.valueOf(type); } - private String getEntityType(SecurableObject securableObject) { + private static String getEntityType(SecurableObject securableObject) { return securableObject.type().name(); } } diff --git a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java index 6dfaf54fecf..b299c15ef97 100644 --- a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java +++ b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java @@ -115,6 +115,7 @@ public class TestAccessControlManager { public static void setUp() throws Exception { File dbDir = new File(DB_DIR); dbDir.mkdirs(); + Mockito.when(config.get(SERVICE_ADMINS)).thenReturn(Lists.newArrayList("admin1", "admin2")); Mockito.when(config.get(ENTITY_STORE)).thenReturn(RELATIONAL_ENTITY_STORE); Mockito.when(config.get(ENTITY_RELATIONAL_STORE)).thenReturn(DEFAULT_ENTITY_RELATIONAL_STORE); @@ -125,10 +126,12 @@ public static void setUp() throws Exception { Mockito.when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 1000L); Mockito.when(config.get(VERSION_RETENTION_COUNT)).thenReturn(1L); Mockito.when(config.get(CATALOG_CACHE_EVICTION_INTERVAL_MS)).thenReturn(1000L); + Mockito.doReturn(100000L).when(config).get(TREE_LOCK_MAX_NODE_IN_MEMORY); Mockito.doReturn(1000L).when(config).get(TREE_LOCK_MIN_NODE_IN_MEMORY); Mockito.doReturn(36000L).when(config).get(TREE_LOCK_CLEAN_INTERVAL); FieldUtils.writeField(GravitinoEnv.getInstance(), "lockManager", new LockManager(config), true); + entityStore = EntityStoreFactory.createEntityStore(config); entityStore.initialize(config); @@ -146,6 +149,7 @@ public static void setUp() throws Exception { AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build()) .build(); entityStore.put(catalogEntity, true); + CatalogEntity anotherCatalogEntity = CatalogEntity.builder() .withId(4L) @@ -421,6 +425,31 @@ public void testListRoles() { String[] actualRoles = accessControlManager.listRoleNames("metalake_list"); Arrays.sort(actualRoles); Assertions.assertArrayEquals(new String[] {"testList1", "testList2"}, actualRoles); + + accessControlManager.deleteRole("metalake_list", "testList1"); + accessControlManager.deleteRole("metalake_list", "testList2"); + } + + @Test + public void testListRolesByObject() { + Map props = ImmutableMap.of("k1", "v1"); + SecurableObject catalogObject = + SecurableObjects.ofCatalog("catalog", Lists.newArrayList(Privileges.UseCatalog.allow())); + + accessControlManager.createRole( + "metalake_list", "testList1", props, Lists.newArrayList(catalogObject)); + + accessControlManager.createRole( + "metalake_list", "testList2", props, Lists.newArrayList(catalogObject)); + + // Test to list roles + String[] listedRoles = + accessControlManager.listRoleNamesByObject("metalake_list", catalogObject); + Arrays.sort(listedRoles); + Assertions.assertArrayEquals(new String[] {"testList1", "testList2"}, listedRoles); + + accessControlManager.deleteRole("metalake_list", "testList1"); + accessControlManager.deleteRole("metalake_list", "testList2"); } private void testProperties(Map expectedProps, Map testProps) { diff --git a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestRoleMetaService.java b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestRoleMetaService.java index 4a781f01861..1f818b11253 100644 --- a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestRoleMetaService.java +++ b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestRoleMetaService.java @@ -441,7 +441,7 @@ void listRolesBySecurableObject() throws IOException { List roleEntities = roleMetaService.listRolesByMetadataObjectIdentAndType( - catalog.nameIdentifier(), catalog.type()); + catalog.nameIdentifier(), catalog.type(), true); roleEntities.sort(Comparator.comparing(RoleEntity::name)); Assertions.assertEquals(Lists.newArrayList(role1, role2), roleEntities); } diff --git a/integration-test/src/test/java/org/apache/gravitino/integration/test/authorization/AccessControlIT.java b/integration-test/src/test/java/org/apache/gravitino/integration/test/authorization/AccessControlIT.java index a4167f1af3b..0501ffb4618 100644 --- a/integration-test/src/test/java/org/apache/gravitino/integration/test/authorization/AccessControlIT.java +++ b/integration-test/src/test/java/org/apache/gravitino/integration/test/authorization/AccessControlIT.java @@ -26,7 +26,10 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.apache.gravitino.Catalog; import org.apache.gravitino.Configs; +import org.apache.gravitino.NameIdentifier; +import org.apache.gravitino.Schema; import org.apache.gravitino.auth.AuthConstants; import org.apache.gravitino.authorization.Group; import org.apache.gravitino.authorization.Privilege; @@ -42,6 +45,7 @@ import org.apache.gravitino.exceptions.NoSuchRoleException; import org.apache.gravitino.exceptions.NoSuchUserException; import org.apache.gravitino.exceptions.UserAlreadyExistsException; +import org.apache.gravitino.file.Fileset; import org.apache.gravitino.integration.test.util.AbstractIT; import org.apache.gravitino.utils.RandomNameUtils; import org.junit.jupiter.api.Assertions; @@ -61,6 +65,15 @@ public static void startIntegrationTest() throws Exception { registerCustomConfigs(configs); AbstractIT.startIntegrationTest(); metalake = client.createMetalake(metalakeName, "metalake comment", Collections.emptyMap()); + + Catalog filesetCatalog = + metalake.createCatalog( + "fileset_catalog", Catalog.Type.FILESET, "hadoop", "comment", Collections.emptyMap()); + NameIdentifier fileIdent = NameIdentifier.of("fileset_schema", "fileset"); + filesetCatalog.asSchemas().createSchema("fileset_schema", "comment", Collections.emptyMap()); + filesetCatalog + .asFilesetCatalog() + .createFileset(fileIdent, "comment", Fileset.Type.EXTERNAL, "tmp", Collections.emptyMap()); } @Test @@ -187,6 +200,46 @@ void testManageRoles() { Assertions.assertEquals( Lists.newArrayList(anotherRoleName, roleName), Arrays.asList(roleNames)); + // List roles by the object (metalake) + roleNames = metalake.listBindingRoleNames(); + Arrays.sort(roleNames); + Assertions.assertEquals( + Lists.newArrayList(anotherRoleName, roleName), Arrays.asList(roleNames)); + + String testObjectRole = "testObjectRole"; + SecurableObject anotherCatalogObject = + SecurableObjects.ofCatalog( + "fileset_catalog", Lists.newArrayList(Privileges.UseCatalog.allow())); + SecurableObject schemaObject = + SecurableObjects.ofSchema( + anotherCatalogObject, + "fileset_schema", + Lists.newArrayList(Privileges.UseSchema.allow())); + SecurableObject filesetObject = + SecurableObjects.ofFileset( + schemaObject, "fileset", Lists.newArrayList(Privileges.ReadFileset.allow())); + + metalake.createRole( + testObjectRole, + properties, + Lists.newArrayList(anotherCatalogObject, schemaObject, filesetObject)); + + // List roles by the object (catalog) + Catalog catalog = metalake.loadCatalog("fileset_catalog"); + roleNames = catalog.supportsRoles().listBindingRoleNames(); + Assertions.assertEquals(Lists.newArrayList(testObjectRole), Arrays.asList(roleNames)); + + // List roles by the object (schema) + Schema schema = catalog.asSchemas().loadSchema("fileset_schema"); + roleNames = schema.supportsRoles().listBindingRoleNames(); + Assertions.assertEquals(Lists.newArrayList(testObjectRole), Arrays.asList(roleNames)); + + // List roles by the object (fileset) + Fileset fileset = + catalog.asFilesetCatalog().loadFileset(NameIdentifier.of("fileset_schema", "fileset")); + roleNames = fileset.supportsRoles().listBindingRoleNames(); + Assertions.assertEquals(Lists.newArrayList(testObjectRole), Arrays.asList(roleNames)); + // Verify the object Assertions.assertEquals(1, role.securableObjects().size()); createdObject = role.securableObjects().get(0); diff --git a/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectRoleOperations.java b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectRoleOperations.java new file mode 100644 index 00000000000..ad27b22a3eb --- /dev/null +++ b/server/src/main/java/org/apache/gravitino/server/web/rest/MetadataObjectRoleOperations.java @@ -0,0 +1,89 @@ +/* + * 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.gravitino.server.web.rest; + +import com.codahale.metrics.annotation.ResponseMetered; +import com.codahale.metrics.annotation.Timed; +import java.util.Locale; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.MetadataObject; +import org.apache.gravitino.MetadataObjects; +import org.apache.gravitino.NameIdentifier; +import org.apache.gravitino.authorization.AccessControlDispatcher; +import org.apache.gravitino.dto.responses.NameListResponse; +import org.apache.gravitino.lock.LockType; +import org.apache.gravitino.lock.TreeLockUtils; +import org.apache.gravitino.metrics.MetricNames; +import org.apache.gravitino.server.authorization.NameBindings; +import org.apache.gravitino.server.web.Utils; +import org.apache.gravitino.utils.MetadataObjectUtil; + +@NameBindings.AccessControlInterfaces +@Path("/metalakes/{metalake}/objects/{type}/{fullName}/roles") +public class MetadataObjectRoleOperations { + + private final AccessControlDispatcher accessControlDispatcher; + + @Context private HttpServletRequest httpRequest; + + public MetadataObjectRoleOperations() { + // Because accessControlManager may be null when Gravitino doesn't enable authorization, + // and Jersey injection doesn't support null value. So MedataObjectRoleOperations chooses to + // retrieve + // accessControlDispatcher from GravitinoEnv instead of injection here. + this.accessControlDispatcher = GravitinoEnv.getInstance().accessControlDispatcher(); + } + + @GET + @Produces("application/vnd.gravitino.v1+json") + @Timed(name = "list-role-by-object." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) + @ResponseMetered(name = "list-role-by-object", absolute = true) + public Response listRoles( + @PathParam("metalake") String metalake, + @PathParam("type") String type, + @PathParam("fullName") String fullName) { + try { + MetadataObject object = + MetadataObjects.parse( + fullName, MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT))); + + NameIdentifier identifier = MetadataObjectUtil.toEntityIdent(metalake, object); + return Utils.doAs( + httpRequest, + () -> + TreeLockUtils.doWithTreeLock( + identifier, + LockType.READ, + () -> { + String[] names = + accessControlDispatcher.listRoleNamesByObject(metalake, object); + return Utils.ok(new NameListResponse(names)); + })); + } catch (Exception e) { + return ExceptionHandlers.handleRoleException(OperationType.LIST, "", metalake, e); + } + } +} diff --git a/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectRoleOperations.java b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectRoleOperations.java new file mode 100644 index 00000000000..19c545a08af --- /dev/null +++ b/server/src/test/java/org/apache/gravitino/server/web/rest/TestMetadataObjectRoleOperations.java @@ -0,0 +1,146 @@ +/* + * 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.gravitino.server.web.rest; + +import static org.apache.gravitino.Configs.TREE_LOCK_CLEAN_INTERVAL; +import static org.apache.gravitino.Configs.TREE_LOCK_MAX_NODE_IN_MEMORY; +import static org.apache.gravitino.Configs.TREE_LOCK_MIN_NODE_IN_MEMORY; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.gravitino.Config; +import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.authorization.AccessControlManager; +import org.apache.gravitino.dto.responses.ErrorConstants; +import org.apache.gravitino.dto.responses.ErrorResponse; +import org.apache.gravitino.dto.responses.NameListResponse; +import org.apache.gravitino.exceptions.NoSuchMetalakeException; +import org.apache.gravitino.lock.LockManager; +import org.apache.gravitino.rest.RESTUtils; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class TestMetadataObjectRoleOperations extends JerseyTest { + + private static final AccessControlManager manager = mock(AccessControlManager.class); + + private static class MockServletRequestFactory extends ServletRequestFactoryBase { + @Override + public HttpServletRequest get() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRemoteUser()).thenReturn(null); + return request; + } + } + + @BeforeAll + public static void setup() throws IllegalAccessException { + Config config = mock(Config.class); + Mockito.doReturn(100000L).when(config).get(TREE_LOCK_MAX_NODE_IN_MEMORY); + Mockito.doReturn(1000L).when(config).get(TREE_LOCK_MIN_NODE_IN_MEMORY); + Mockito.doReturn(36000L).when(config).get(TREE_LOCK_CLEAN_INTERVAL); + FieldUtils.writeField(GravitinoEnv.getInstance(), "lockManager", new LockManager(config), true); + FieldUtils.writeField(GravitinoEnv.getInstance(), "accessControlDispatcher", manager, true); + } + + @Override + protected Application configure() { + try { + forceSet( + TestProperties.CONTAINER_PORT, String.valueOf(RESTUtils.findAvailablePort(2000, 3000))); + } catch (IOException e) { + throw new RuntimeException(e); + } + + ResourceConfig resourceConfig = new ResourceConfig(); + resourceConfig.register(MetadataObjectRoleOperations.class); + resourceConfig.register( + new AbstractBinder() { + @Override + protected void configure() { + bindFactory(MockServletRequestFactory.class).to(HttpServletRequest.class); + } + }); + + return resourceConfig; + } + + @Test + public void testListRoleNames() { + when(manager.listRoleNamesByObject(any(), any())).thenReturn(new String[] {"role"}); + + Response resp = + target("/metalakes/metalake1/objects/metalake/metalake1/roles/") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .get(); + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + + NameListResponse listResponse = resp.readEntity(NameListResponse.class); + Assertions.assertEquals(0, listResponse.getCode()); + + Assertions.assertEquals(1, listResponse.getNames().length); + Assertions.assertEquals("role", listResponse.getNames()[0]); + + // Test to throw NoSuchMetalakeException + doThrow(new NoSuchMetalakeException("mock error")) + .when(manager) + .listRoleNamesByObject(any(), any()); + Response resp1 = + target("/metalakes/metalake1/objects/metalake/metalake1/roles/") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .get(); + + Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), resp1.getStatus()); + + ErrorResponse errorResponse = resp1.readEntity(ErrorResponse.class); + Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, errorResponse.getCode()); + Assertions.assertEquals(NoSuchMetalakeException.class.getSimpleName(), errorResponse.getType()); + + // Test to throw internal RuntimeException + doThrow(new RuntimeException("mock error")).when(manager).listRoleNamesByObject(any(), any()); + Response resp3 = + target("/metalakes/metalake1/objects/metalake/metalake1/roles") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .get(); + + Assertions.assertEquals( + Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp3.getStatus()); + + ErrorResponse errorResponse2 = resp3.readEntity(ErrorResponse.class); + Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, errorResponse2.getCode()); + Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResponse2.getType()); + } +} diff --git a/server/src/test/java/org/apache/gravitino/server/web/rest/TestRoleOperations.java b/server/src/test/java/org/apache/gravitino/server/web/rest/TestRoleOperations.java index eb365d1ac69..a2f0c4847d6 100644 --- a/server/src/test/java/org/apache/gravitino/server/web/rest/TestRoleOperations.java +++ b/server/src/test/java/org/apache/gravitino/server/web/rest/TestRoleOperations.java @@ -334,23 +334,6 @@ public void testGetRole() { Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResponse2.getType()); } - private Role buildRole(String role) { - SecurableObject catalog = - SecurableObjects.ofCatalog("catalog", Lists.newArrayList(Privileges.UseCatalog.allow())); - SecurableObject anotherSecurableObject = - SecurableObjects.ofCatalog( - "another_catalog", Lists.newArrayList(Privileges.CreateSchema.deny())); - - return RoleEntity.builder() - .withId(1L) - .withName(role) - .withProperties(Collections.emptyMap()) - .withSecurableObjects(Lists.newArrayList(catalog, anotherSecurableObject)) - .withAuditInfo( - AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build()) - .build(); - } - @Test public void testDeleteRole() { when(manager.deleteRole(any(), any())).thenReturn(true); @@ -502,4 +485,21 @@ public void testListRoleNames() { Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, errorResponse2.getCode()); Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResponse2.getType()); } + + private Role buildRole(String role) { + SecurableObject catalog = + SecurableObjects.ofCatalog("catalog", Lists.newArrayList(Privileges.UseCatalog.allow())); + SecurableObject anotherSecurableObject = + SecurableObjects.ofCatalog( + "another_catalog", Lists.newArrayList(Privileges.CreateSchema.deny())); + + return RoleEntity.builder() + .withId(1L) + .withName(role) + .withProperties(Collections.emptyMap()) + .withSecurableObjects(Lists.newArrayList(catalog, anotherSecurableObject)) + .withAuditInfo( + AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build()) + .build(); + } }