This is an automated email from the ASF dual-hosted git repository.
jerryshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 4b478373bd [#12464] improvement(client-python): Support metadata
object role operations (#12465)
4b478373bd is described below
commit 4b478373bd07e0a6f3e91917b291ca8acad4b81b
Author: Zhiguo Wu <[email protected]>
AuthorDate: Tue Aug 25 17:07:28 2026 +0800
[#12464] improvement(client-python): Support metadata object role
operations (#12465)
### What changes were proposed in this pull request?
Add metadata object role operations to the Python client.
This change:
- Adds `supports_roles()` to metalake, catalog, schema, table, fileset,
and model APIs.
- Supports listing directly bound role names for metalakes, catalogs,
schemas, tables, and filesets.
- Adds view and function helpers to `SecurableObjects`.
- Adds unit and integration test coverage.
### Why are the changes needed?
The Java client exposes role operations through metadata objects, but
the Python client does not provide the equivalent API. This change
brings the Python client into alignment with the Java client.
Fix: #12464
### Does this PR introduce _any_ user-facing change?
Yes. Python client users can call:
```python
metadata_object.supports_roles().list_binding_role_names()
```
The Python client also provides `SecurableObjects.of_view()` and
`SecurableObjects.of_function()`.
### How was this patch tested?
Added unit tests for the public APIs, client implementations, and
securable object helpers.
Added an integration test covering role bindings for metalakes,
catalogs, schemas, tables, and filesets.
---------
Co-authored-by: Jerry Shao <[email protected]>
---
.../api/authorization/securable_objects.py | 60 ++++++++
clients/client-python/gravitino/api/catalog.py | 13 +-
.../client-python/gravitino/api/file/fileset.py | 12 ++
clients/client-python/gravitino/api/metalake.py | 15 ++
clients/client-python/gravitino/api/model/model.py | 12 ++
clients/client-python/gravitino/api/rel/table.py | 12 ++
clients/client-python/gravitino/api/schema.py | 12 ++
.../gravitino/client/base_schema_catalog.py | 16 ++-
.../gravitino/client/generic_fileset.py | 14 ++
.../gravitino/client/generic_model.py | 5 +
.../gravitino/client/generic_schema.py | 16 +++
.../gravitino/client/gravitino_metalake.py | 20 ++-
.../gravitino/client/relational_table.py | 14 ++
.../tests/integration/test_supports_roles.py | 150 +++++++++++++++++++
.../authorization/test_securable_objects.py | 52 +++++++
.../tests/unittests/test_generic_model.py | 11 ++
.../tests/unittests/test_supports_roles.py | 158 +++++++++++++++++++++
17 files changed, 588 insertions(+), 4 deletions(-)
diff --git
a/clients/client-python/gravitino/api/authorization/securable_objects.py
b/clients/client-python/gravitino/api/authorization/securable_objects.py
index 491d637d5a..4c00463b70 100644
--- a/clients/client-python/gravitino/api/authorization/securable_objects.py
+++ b/clients/client-python/gravitino/api/authorization/securable_objects.py
@@ -37,8 +37,10 @@ class SecurableObject(MetadataObject, ABC):
- CATALOG
- SCHEMA
- TABLE
+ - VIEW
- FILESET
- TOPIC
+ - FUNCTION
- METALAKE
Use the helper class `SecurableObjects` to construct the securable object
you need.
@@ -65,6 +67,12 @@ class SecurableObject(MetadataObject, ABC):
- REST API:
full_name="catalog1.schema1.table1", type="TABLE"
+ View:
+ - Python code:
+ SecurableObjects.of_view(schema, "view1", privileges)
+ - REST API:
+ full_name="catalog1.schema1.view1", type="VIEW"
+
Topic:
- Python code:
SecurableObjects.topic("catalog1", "schema1", "topic1")
@@ -77,6 +85,12 @@ class SecurableObject(MetadataObject, ABC):
- REST API:
full_name="catalog1.schema1.fileset1", type="FILESET"
+ Function:
+ - Python code:
+ SecurableObjects.of_function(schema, "function1", privileges)
+ - REST API:
+ full_name="catalog1.schema1.function1", type="FUNCTION"
+
Metalake:
- Python code:
SecurableObjects.metalake("metalake1")
@@ -243,6 +257,29 @@ class SecurableObjects:
privileges,
)
+ @staticmethod
+ def of_view(
+ schema: SecurableObject,
+ view: str,
+ privileges: list[Privilege],
+ ) -> SecurableObjectImpl:
+ """Create a view securable object.
+
+ Args:
+ schema (SecurableObject): The schema securable object.
+ view (str): The view name.
+ privileges (list[Privilege]): The privileges of the view.
+
+ Returns:
+ SecurableObjectImpl: The created view securable object.
+ """
+ names = [*schema.full_name().split("."), view]
+ return SecurableObjects.of(
+ MetadataObject.Type.VIEW,
+ names,
+ privileges,
+ )
+
@staticmethod
def of_topic(
schema: SecurableObject,
@@ -315,6 +352,29 @@ class SecurableObjects:
privileges,
)
+ @staticmethod
+ def of_function(
+ schema: SecurableObject,
+ function: str,
+ privileges: list[Privilege],
+ ) -> SecurableObjectImpl:
+ """Create a function securable object.
+
+ Args:
+ schema (SecurableObject): The schema securable object.
+ function (str): The function name.
+ privileges (list[Privilege]): The privileges of the function.
+
+ Returns:
+ SecurableObjectImpl: The created function securable object.
+ """
+ names = [*schema.full_name().split("."), function]
+ return SecurableObjects.of(
+ MetadataObject.Type.FUNCTION,
+ names,
+ privileges,
+ )
+
@staticmethod
def of_tag(
tag_name: str,
diff --git a/clients/client-python/gravitino/api/catalog.py
b/clients/client-python/gravitino/api/catalog.py
index 27d6688415..a3156080ae 100644
--- a/clients/client-python/gravitino/api/catalog.py
+++ b/clients/client-python/gravitino/api/catalog.py
@@ -20,8 +20,10 @@ from enum import Enum
from typing import Dict, Optional
from gravitino.api.auditable import Auditable
+from gravitino.api.authorization.supports_roles import SupportsRoles
from gravitino.api.supports_schemas import SupportsSchemas
from gravitino.api.tag.supports_tags import SupportsTags
+from gravitino.exceptions.base import UnsupportedOperationException
class Catalog(Auditable):
@@ -224,6 +226,13 @@ class Catalog(Auditable):
"""
raise UnsupportedOperationException("Catalog does not support tag
operations")
+ def supports_roles(self) -> SupportsRoles:
+ """Return role operations supported by this catalog.
-class UnsupportedOperationException(Exception):
- pass
+ Returns:
+ SupportsRoles: The role operations supported by this catalog.
+
+ Raises:
+ UnsupportedOperationException: If this catalog does not support
role operations.
+ """
+ raise UnsupportedOperationException("Catalog does not support role
operations")
diff --git a/clients/client-python/gravitino/api/file/fileset.py
b/clients/client-python/gravitino/api/file/fileset.py
index 7639fe49a4..cda622ceaf 100644
--- a/clients/client-python/gravitino/api/file/fileset.py
+++ b/clients/client-python/gravitino/api/file/fileset.py
@@ -20,6 +20,7 @@ from enum import Enum
from typing import Dict, Optional
from gravitino.api.auditable import Auditable
+from gravitino.api.authorization.supports_roles import SupportsRoles
from gravitino.api.tag.supports_tags import SupportsTags
from gravitino.exceptions.base import UnsupportedOperationException
@@ -214,3 +215,14 @@ class Fileset(Auditable):
def supports_tags(self) -> SupportsTags:
raise UnsupportedOperationException("Fileset does not support tag
operations.")
+
+ def supports_roles(self) -> SupportsRoles:
+ """Return role operations supported by this fileset.
+
+ Returns:
+ SupportsRoles: The role operations supported by this fileset.
+
+ Raises:
+ UnsupportedOperationException: If this fileset does not support
role operations.
+ """
+ raise UnsupportedOperationException("Fileset does not support role
operations.")
diff --git a/clients/client-python/gravitino/api/metalake.py
b/clients/client-python/gravitino/api/metalake.py
index 0684774526..418ab74b05 100644
--- a/clients/client-python/gravitino/api/metalake.py
+++ b/clients/client-python/gravitino/api/metalake.py
@@ -19,6 +19,8 @@ from abc import abstractmethod
from typing import Optional, Dict
from gravitino.api.auditable import Auditable
+from gravitino.api.authorization.supports_roles import SupportsRoles
+from gravitino.exceptions.base import UnsupportedOperationException
class Metalake(Auditable):
@@ -55,3 +57,16 @@ class Metalake(Auditable):
Optional[Dict[str, str]]: The properties of the metalake.
"""
pass
+
+ def supports_roles(self) -> SupportsRoles:
+ """Return role operations supported by this metalake.
+
+ Returns:
+ SupportsRoles: The role operations supported by this metalake.
+
+ Raises:
+ UnsupportedOperationException: If this metalake does not support
role operations.
+ """
+ raise UnsupportedOperationException(
+ "Metalake does not support role operations."
+ )
diff --git a/clients/client-python/gravitino/api/model/model.py
b/clients/client-python/gravitino/api/model/model.py
index 8e21982478..0311ff4825 100644
--- a/clients/client-python/gravitino/api/model/model.py
+++ b/clients/client-python/gravitino/api/model/model.py
@@ -19,6 +19,7 @@ from abc import abstractmethod
from typing import Dict, Optional
from gravitino.api.auditable import Auditable
+from gravitino.api.authorization.supports_roles import SupportsRoles
from gravitino.api.tag.supports_tags import SupportsTags
from gravitino.exceptions.base import UnsupportedOperationException
@@ -77,3 +78,14 @@ class Model(Auditable):
def supports_tags(self) -> SupportsTags:
raise UnsupportedOperationException("Model does not support tag
operations.")
+
+ def supports_roles(self) -> SupportsRoles:
+ """Return role operations supported by this model.
+
+ Returns:
+ SupportsRoles: The role operations supported by this model.
+
+ Raises:
+ UnsupportedOperationException: If this model does not support role
operations.
+ """
+ raise UnsupportedOperationException("Model does not support role
operations.")
diff --git a/clients/client-python/gravitino/api/rel/table.py
b/clients/client-python/gravitino/api/rel/table.py
index a32f244b20..060b83b613 100644
--- a/clients/client-python/gravitino/api/rel/table.py
+++ b/clients/client-python/gravitino/api/rel/table.py
@@ -19,6 +19,7 @@ from abc import abstractmethod
from typing import Optional
from gravitino.api.auditable import Auditable
+from gravitino.api.authorization.supports_roles import SupportsRoles
from gravitino.api.rel.column import Column
from gravitino.api.rel.expressions.distributions.distribution import
Distribution
from gravitino.api.rel.expressions.distributions.distributions import
Distributions
@@ -117,6 +118,17 @@ class Table(Auditable):
def supports_tags(self) -> SupportsTags:
raise UnsupportedOperationException("Table does not support tag
operations.")
+ def supports_roles(self) -> SupportsRoles:
+ """Return role operations supported by this table.
+
+ Returns:
+ SupportsRoles: The role operations supported by this table.
+
+ Raises:
+ UnsupportedOperationException: If this table does not support role
operations.
+ """
+ raise UnsupportedOperationException("Table does not support role
operations.")
+
def supports_statistics(self) -> SupportsStatistics:
raise UnsupportedOperationException(
"Table does not support statistics operations."
diff --git a/clients/client-python/gravitino/api/schema.py
b/clients/client-python/gravitino/api/schema.py
index 5a7ec0a088..78705f5bd9 100644
--- a/clients/client-python/gravitino/api/schema.py
+++ b/clients/client-python/gravitino/api/schema.py
@@ -19,6 +19,7 @@ from abc import abstractmethod
from typing import Dict, Optional
from gravitino.api.auditable import Auditable
+from gravitino.api.authorization.supports_roles import SupportsRoles
from gravitino.api.tag.supports_tags import SupportsTags
from gravitino.exceptions.base import UnsupportedOperationException
@@ -48,3 +49,14 @@ class Schema(Auditable):
def supports_tags(self) -> SupportsTags:
raise UnsupportedOperationException("Schema does not support tag
operations.")
+
+ def supports_roles(self) -> SupportsRoles:
+ """Return role operations supported by this schema.
+
+ Returns:
+ SupportsRoles: The role operations supported by this schema.
+
+ Raises:
+ UnsupportedOperationException: If this schema does not support
role operations.
+ """
+ raise UnsupportedOperationException("Schema does not support role
operations.")
diff --git a/clients/client-python/gravitino/client/base_schema_catalog.py
b/clients/client-python/gravitino/client/base_schema_catalog.py
index e0fb8c20ce..88ab8f72cd 100644
--- a/clients/client-python/gravitino/client/base_schema_catalog.py
+++ b/clients/client-python/gravitino/client/base_schema_catalog.py
@@ -18,6 +18,7 @@
import logging
from typing import Dict, List, Optional
+from gravitino.api.authorization.supports_roles import SupportsRoles
from gravitino.api.catalog import Catalog
from gravitino.api.function.function import Function
from gravitino.api.function.function_catalog import FunctionCatalog
@@ -36,6 +37,9 @@ from gravitino.client.generic_schema import GenericSchema
from gravitino.client.metadata_object_credential_operations import (
MetadataObjectCredentialOperations,
)
+from gravitino.client.metadata_object_role_operations import (
+ MetadataObjectRoleOperations,
+)
from gravitino.client.metadata_object_secret_operations import (
MetadataObjectSecretOperations,
)
@@ -62,8 +66,9 @@ class BaseSchemaCatalog(
CatalogDTO,
SupportsSchemas,
FunctionCatalog,
+ SupportsRoles,
SupportsTags,
-):
+): # pylint: disable=too-many-ancestors
"""
BaseSchemaCatalog is the base abstract class for all the catalog with
schema. It provides the
common methods for managing schemas in a catalog. With BaseSchemaCatalog,
users can list,
@@ -119,6 +124,9 @@ class BaseSchemaCatalog(
self._object_tag_operations = MetadataObjectTagOperations(
catalog_namespace.level(0), metadata_object, rest_client
)
+ self._object_role_operations = MetadataObjectRoleOperations(
+ catalog_namespace.level(0), metadata_object, rest_client
+ )
self.validate()
@@ -389,3 +397,9 @@ class BaseSchemaCatalog(
def supports_tags(self) -> SupportsTags:
return self
+
+ def supports_roles(self) -> SupportsRoles:
+ return self
+
+ def list_binding_role_names(self) -> List[str]:
+ return self._object_role_operations.list_binding_role_names()
diff --git a/clients/client-python/gravitino/client/generic_fileset.py
b/clients/client-python/gravitino/client/generic_fileset.py
index 9516cea807..750627288e 100644
--- a/clients/client-python/gravitino/client/generic_fileset.py
+++ b/clients/client-python/gravitino/client/generic_fileset.py
@@ -16,6 +16,7 @@
# under the License.
from typing import Dict, List, Optional
+from gravitino.api.authorization.supports_roles import SupportsRoles
from gravitino.api.credential.credential import Credential
from gravitino.api.credential.supports_credentials import SupportsCredentials
from gravitino.api.secret.supports_secrets import SupportsSecrets
@@ -27,6 +28,9 @@ from gravitino.api.tag.tag import Tag
from gravitino.client.metadata_object_credential_operations import (
MetadataObjectCredentialOperations,
)
+from gravitino.client.metadata_object_role_operations import (
+ MetadataObjectRoleOperations,
+)
from gravitino.client.metadata_object_secret_operations import (
MetadataObjectSecretOperations,
)
@@ -40,6 +44,7 @@ from gravitino.utils import HTTPClient
class GenericFileset(
Fileset,
SupportsCredentials,
+ SupportsRoles,
SupportsSecrets,
SupportsTags,
):
@@ -69,6 +74,9 @@ class GenericFileset(
self._object_tag_operations = MetadataObjectTagOperations(
full_namespace.level(0), metadata_object, rest_client
)
+ self._object_role_operations = MetadataObjectRoleOperations(
+ full_namespace.level(0), metadata_object, rest_client
+ )
def name(self) -> str:
return self._fileset.name()
@@ -94,9 +102,15 @@ class GenericFileset(
def supports_tags(self) -> SupportsTags:
return self
+ def supports_roles(self) -> SupportsRoles:
+ return self
+
def get_credentials(self) -> List[Credential]:
return self._object_credential_operations.get_credentials()
+ def list_binding_role_names(self) -> List[str]:
+ return self._object_role_operations.list_binding_role_names()
+
def support_secrets(self) -> SupportsSecrets:
return self
diff --git a/clients/client-python/gravitino/client/generic_model.py
b/clients/client-python/gravitino/client/generic_model.py
index 41aba4562b..6305f0e4a3 100644
--- a/clients/client-python/gravitino/client/generic_model.py
+++ b/clients/client-python/gravitino/client/generic_model.py
@@ -19,6 +19,7 @@ from __future__ import annotations
from typing import Optional
+from gravitino.api.authorization.supports_roles import SupportsRoles
from gravitino.api.metadata_object import MetadataObject
from gravitino.api.metadata_objects import MetadataObjects
from gravitino.api.model.model import Model
@@ -27,6 +28,7 @@ from gravitino.api.tag.supports_tags import SupportsTags
from gravitino.client.metadata_object_tag_operations import
MetadataObjectTagOperations
from gravitino.dto.audit_dto import AuditDTO
from gravitino.dto.model_dto import ModelDTO
+from gravitino.exceptions.base import UnsupportedOperationException
from gravitino.namespace import Namespace
from gravitino.utils import HTTPClient
@@ -94,3 +96,6 @@ class GenericModel(Model, SupportsTags):
def supports_tags(self) -> SupportsTags:
return self
+
+ def supports_roles(self) -> SupportsRoles:
+ raise UnsupportedOperationException("Not supported yet.")
diff --git a/clients/client-python/gravitino/client/generic_schema.py
b/clients/client-python/gravitino/client/generic_schema.py
index 52ade4c630..a14944d29a 100644
--- a/clients/client-python/gravitino/client/generic_schema.py
+++ b/clients/client-python/gravitino/client/generic_schema.py
@@ -20,12 +20,16 @@ from __future__ import annotations
from typing import Dict
from gravitino.api.audit import Audit
+from gravitino.api.authorization.supports_roles import SupportsRoles
from gravitino.api.metadata_object import MetadataObject
from gravitino.api.metadata_objects import MetadataObjects
from gravitino.api.schema import Schema
from gravitino.api.secret.supports_secrets import SupportsSecrets
from gravitino.api.tag.supports_tags import SupportsTags
from gravitino.api.tag.tag import Tag
+from gravitino.client.metadata_object_role_operations import (
+ MetadataObjectRoleOperations,
+)
from gravitino.client.metadata_object_secret_operations import (
MetadataObjectSecretOperations,
)
@@ -36,6 +40,7 @@ from gravitino.utils.http_client import HTTPClient
class GenericSchema(
Schema,
+ SupportsRoles,
SupportsTags,
SupportsSecrets,
):
@@ -59,6 +64,11 @@ class GenericSchema(
metadata_object,
rest_client,
)
+ self._metadata_object_role_operations = MetadataObjectRoleOperations(
+ metalake,
+ metadata_object,
+ rest_client,
+ )
self._object_secret_operations = MetadataObjectSecretOperations(
metalake,
metadata_object,
@@ -114,6 +124,12 @@ class GenericSchema(
def supports_tags(self) -> SupportsTags:
return self
+ def supports_roles(self) -> SupportsRoles:
+ return self
+
+ def list_binding_role_names(self) -> list[str]:
+ return self._metadata_object_role_operations.list_binding_role_names()
+
def support_secrets(self) -> SupportsSecrets:
return self
diff --git a/clients/client-python/gravitino/client/gravitino_metalake.py
b/clients/client-python/gravitino/client/gravitino_metalake.py
index 1fcb75cddb..5e279ba629 100644
--- a/clients/client-python/gravitino/client/gravitino_metalake.py
+++ b/clients/client-python/gravitino/client/gravitino_metalake.py
@@ -23,6 +23,7 @@ from gravitino.api.authorization.owner import Owner
from gravitino.api.authorization.privileges import Privilege
from gravitino.api.authorization.role import Role
from gravitino.api.authorization.securable_objects import SecurableObject
+from gravitino.api.authorization.supports_roles import SupportsRoles
from gravitino.api.authorization.user import User
from gravitino.api.catalog import Catalog
from gravitino.api.catalog_change import CatalogChange
@@ -31,11 +32,15 @@ from gravitino.api.job.job_template import JobTemplate
from gravitino.api.job.job_template_change import JobTemplateChange
from gravitino.api.job.supports_jobs import SupportsJobs
from gravitino.api.metadata_object import MetadataObject
+from gravitino.api.metadata_objects import MetadataObjects
from gravitino.api.tag.tag import Tag
from gravitino.api.tag.tag_operations import TagOperations
from gravitino.client.dto_converters import DTOConverters
from gravitino.client.generic_job_handle import GenericJobHandle
from gravitino.client.generic_tag import GenericTag
+from gravitino.client.metadata_object_role_operations import (
+ MetadataObjectRoleOperations,
+)
from gravitino.dto.metalake_dto import MetalakeDTO
from gravitino.dto.requests.catalog_create_request import CatalogCreateRequest
from gravitino.dto.requests.catalog_set_request import CatalogSetRequest
@@ -108,8 +113,9 @@ logger = logging.getLogger(__name__)
class GravitinoMetalake(
MetalakeDTO,
SupportsJobs,
+ SupportsRoles,
TagOperations,
-):
+): # pylint: disable=too-many-ancestors
"""
Gravitino Metalake is the top-level metadata repository for users. It
contains a list of catalogs
as sub-level metadata collections. With GravitinoMetalake, users can list,
create, load,
@@ -151,6 +157,12 @@ class GravitinoMetalake(
_audit=metalake.audit_info(),
)
self.rest_client = client
+ metalake_object = MetadataObjects.of(
+ [self.name()], MetadataObject.Type.METALAKE
+ )
+ self._metadata_object_role_operations = MetadataObjectRoleOperations(
+ self.name(), metalake_object, client
+ )
def list_catalogs(self) -> List[str]:
"""List all the catalogs under this metalake.
@@ -1037,6 +1049,9 @@ class GravitinoMetalake(
# Role operations
#####################
+ def supports_roles(self) -> SupportsRoles:
+ return self
+
def create_role(
self,
role_name: str,
@@ -1136,6 +1151,9 @@ class GravitinoMetalake(
resp.validate()
return resp.names()
+ def list_binding_role_names(self) -> List[str]:
+ return self._metadata_object_role_operations.list_binding_role_names()
+
def grant_roles_to_user(self, role_names: List[str], user_name: str) ->
User:
"""Grant roles to a user.
diff --git a/clients/client-python/gravitino/client/relational_table.py
b/clients/client-python/gravitino/client/relational_table.py
index 17e59ad7f9..51f4e463c3 100644
--- a/clients/client-python/gravitino/client/relational_table.py
+++ b/clients/client-python/gravitino/client/relational_table.py
@@ -18,6 +18,7 @@
from typing import Any, Optional, cast
from gravitino.api.audit import Audit
+from gravitino.api.authorization.supports_roles import SupportsRoles
from gravitino.api.metadata_object import MetadataObject
from gravitino.api.metadata_objects import MetadataObjects
from gravitino.api.rel.column import Column
@@ -33,6 +34,9 @@ from gravitino.api.stats.supports_statistics import
SupportsStatistics
from gravitino.api.tag.supports_tags import SupportsTags
from gravitino.api.tag.tag import Tag
from gravitino.client.generic_column import GenericColumn
+from gravitino.client.metadata_object_role_operations import (
+ MetadataObjectRoleOperations,
+)
from gravitino.client.metadata_object_statistics_operations import (
MetadataObjectStatisticsOperations,
)
@@ -57,6 +61,7 @@ from gravitino.utils import HTTPClient
class RelationalTable(
Table,
+ SupportsRoles,
SupportsStatistics,
SupportsTags,
):
@@ -75,6 +80,9 @@ class RelationalTable(
self._object_tag_operations = MetadataObjectTagOperations(
namespace.level(0), table_object, rest_client
)
+ self._object_role_operations = MetadataObjectRoleOperations(
+ namespace.level(0), table_object, rest_client
+ )
self._object_statistics_operations =
MetadataObjectStatisticsOperations(
namespace.level(0), table_object, rest_client
)
@@ -267,6 +275,12 @@ class RelationalTable(
def supports_tags(self) -> SupportsTags:
return self
+ def supports_roles(self) -> SupportsRoles:
+ return self
+
+ def list_binding_role_names(self) -> list[str]:
+ return self._object_role_operations.list_binding_role_names()
+
def list_statistics(self) -> list[Statistic]:
return self._object_statistics_operations.list_statistics()
diff --git a/clients/client-python/tests/integration/test_supports_roles.py
b/clients/client-python/tests/integration/test_supports_roles.py
new file mode 100644
index 0000000000..103af31912
--- /dev/null
+++ b/clients/client-python/tests/integration/test_supports_roles.py
@@ -0,0 +1,150 @@
+# 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.
+
+import uuid
+
+from gravitino import Catalog, Fileset, NameIdentifier
+from gravitino.api.authorization.privileges import Privileges
+from gravitino.api.authorization.securable_objects import SecurableObjects
+from gravitino.api.rel.types.types import Types
+from gravitino.dto.rel.column_dto import ColumnDTO
+from tests.integration.containers.hdfs_container import HDFSContainer
+from tests.integration.integration_test_env import
AuthorizationIntegrationTestEnv
+
+
+class TestSupportsRoles(AuthorizationIntegrationTestEnv):
+ _metalake_name = f"test_supports_roles_{uuid.uuid4().hex[:8]}"
+ _metalake_comment = "test metadata object role operations"
+ _fileset_catalog_name = "fileset_catalog"
+ _fileset_schema_name = "fileset_schema"
+ _fileset_name = "fileset"
+ _relational_catalog_name = "relational_catalog"
+ _relational_schema_name = "relational_schema"
+ _table_name = "table"
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ super().setUpClass()
+ cls._hdfs_container = HDFSContainer()
+
+ @classmethod
+ def tearDownClass(cls) -> None:
+ try:
+ cls._hdfs_container.close()
+ finally:
+ super().tearDownClass()
+
+ def setUp(self) -> None:
+ super().setUp()
+ self._metalake = self._gravitino_client.get_metalake()
+ self._fileset_catalog = self._gravitino_client.create_catalog(
+ name=self._fileset_catalog_name,
+ catalog_type=Catalog.Type.FILESET,
+ provider=None,
+ comment="test fileset catalog",
+ properties={"location":
f"/tmp/{self._metalake_name}/test_supports_roles"},
+ )
+ self._fileset_schema =
self._fileset_catalog.as_schemas().create_schema(
+ schema_name=self._fileset_schema_name,
+ comment="test fileset schema",
+ properties={},
+ )
+ self._fileset =
self._fileset_catalog.as_fileset_catalog().create_fileset(
+ ident=NameIdentifier.of(self._fileset_schema_name,
self._fileset_name),
+ comment="test fileset",
+ fileset_type=Fileset.Type.MANAGED,
+
storage_location=f"/tmp/{self._metalake_name}/test_supports_roles/fileset",
+ properties={},
+ )
+ self._relational_catalog = self._gravitino_client.create_catalog(
+ name=self._relational_catalog_name,
+ catalog_type=Catalog.Type.RELATIONAL,
+ provider="hive",
+ comment="test relational catalog",
+ properties={
+ "metastore.uris":
f"thrift://{self._hdfs_container.get_ip()}:9083"
+ },
+ )
+ self._relational_schema =
self._relational_catalog.as_schemas().create_schema(
+ schema_name=self._relational_schema_name,
+ comment="test relational schema",
+ properties={},
+ )
+ self._table = self._relational_catalog.as_table_catalog().create_table(
+ identifier=NameIdentifier.of(
+ self._relational_schema_name, self._table_name
+ ),
+ columns=[
+ ColumnDTO.builder()
+ .with_name("id")
+ .with_data_type(Types.IntegerType.get())
+ .build()
+ ],
+ )
+
+ def test_list_binding_roles_for_metadata_objects(self) -> None:
+ catalog_object = SecurableObjects.of_catalog(
+ self._fileset_catalog_name, [Privileges.allow("USE_CATALOG")]
+ )
+ schema_object = SecurableObjects.of_schema(
+ catalog_object,
+ self._fileset_schema_name,
+ [Privileges.allow("USE_SCHEMA")],
+ )
+ fileset_object = SecurableObjects.of_fileset(
+ schema_object, self._fileset_name,
[Privileges.allow("READ_FILESET")]
+ )
+ relational_catalog_object = SecurableObjects.of_catalog(
+ self._relational_catalog_name, [Privileges.allow("USE_CATALOG")]
+ )
+ relational_schema_object = SecurableObjects.of_schema(
+ relational_catalog_object,
+ self._relational_schema_name,
+ [Privileges.allow("USE_SCHEMA")],
+ )
+ table_object = SecurableObjects.of_table(
+ relational_schema_object,
+ self._table_name,
+ [Privileges.allow("SELECT_TABLE")],
+ )
+
+ bindings = [
+ (
+ "metalake_role",
+ SecurableObjects.of_metalake(
+ self._metalake_name, [Privileges.allow("CREATE_CATALOG")]
+ ),
+ self._metalake,
+ ),
+ ("catalog_role", catalog_object, self._fileset_catalog),
+ ("schema_role", schema_object, self._fileset_schema),
+ ("fileset_role", fileset_object, self._fileset),
+ ("table_role", table_object, self._table),
+ ]
+
+ for role_name, securable_object, metadata_object in bindings:
+ with self.subTest(metadata_type=securable_object.type()):
+ self.assertEqual(
+ [],
metadata_object.supports_roles().list_binding_role_names()
+ )
+ self._gravitino_client.create_role(
+ role_name, securable_objects=[securable_object]
+ )
+ self.assertEqual(
+ [role_name],
+ metadata_object.supports_roles().list_binding_role_names(),
+ )
diff --git
a/clients/client-python/tests/unittests/authorization/test_securable_objects.py
b/clients/client-python/tests/unittests/authorization/test_securable_objects.py
index 35a1963b5c..c3de92d254 100644
---
a/clients/client-python/tests/unittests/authorization/test_securable_objects.py
+++
b/clients/client-python/tests/unittests/authorization/test_securable_objects.py
@@ -156,6 +156,31 @@ class TestSecurableObject(unittest.TestCase):
self.assertEqual(MetadataObject.Type.TABLE, table_object.type())
self.assertEqual(table_object, another_table_object)
+ def test_view_object(self) -> None:
+ catalog_object = SecurableObjects.of_catalog(
+ "catalog",
+ [MockPrivilege(Privilege.Name.USE_CATALOG,
Privilege.Condition.ALLOW)],
+ )
+ schema_object = SecurableObjects.of_schema(
+ catalog_object,
+ "schema",
+ [MockPrivilege(Privilege.Name.USE_SCHEMA,
Privilege.Condition.ALLOW)],
+ )
+ view_privilege = MockPrivilege(
+ Privilege.Name.SELECT_VIEW, Privilege.Condition.ALLOW
+ )
+
+ view_object = SecurableObjects.of_view(schema_object, "view",
[view_privilege])
+ another_view_object = SecurableObjects.of(
+ MetadataObject.Type.VIEW,
+ ["catalog", "schema", "view"],
+ [view_privilege],
+ )
+
+ self.assertEqual("catalog.schema.view", view_object.full_name())
+ self.assertEqual(MetadataObject.Type.VIEW, view_object.type())
+ self.assertEqual(view_object, another_view_object)
+
def test_fileset_object(self) -> None:
catalog_object = SecurableObjects.of_catalog(
"catalog",
@@ -198,6 +223,33 @@ class TestSecurableObject(unittest.TestCase):
self.assertEqual(MetadataObject.Type.FILESET, fileset_object.type())
self.assertEqual(fileset_object, another_fileset_object)
+ def test_function_object(self) -> None:
+ catalog_object = SecurableObjects.of_catalog(
+ "catalog",
+ [MockPrivilege(Privilege.Name.USE_CATALOG,
Privilege.Condition.ALLOW)],
+ )
+ schema_object = SecurableObjects.of_schema(
+ catalog_object,
+ "schema",
+ [MockPrivilege(Privilege.Name.USE_SCHEMA,
Privilege.Condition.ALLOW)],
+ )
+ function_privilege = MockPrivilege(
+ Privilege.Name.EXECUTE_FUNCTION, Privilege.Condition.ALLOW
+ )
+
+ function_object = SecurableObjects.of_function(
+ schema_object, "function", [function_privilege]
+ )
+ another_function_object = SecurableObjects.of(
+ MetadataObject.Type.FUNCTION,
+ ["catalog", "schema", "function"],
+ [function_privilege],
+ )
+
+ self.assertEqual("catalog.schema.function",
function_object.full_name())
+ self.assertEqual(MetadataObject.Type.FUNCTION, function_object.type())
+ self.assertEqual(function_object, another_function_object)
+
def test_topic_object(self) -> None:
catalog_object = SecurableObjects.of_catalog(
"catalog",
diff --git a/clients/client-python/tests/unittests/test_generic_model.py
b/clients/client-python/tests/unittests/test_generic_model.py
index c674530751..d95c516149 100644
--- a/clients/client-python/tests/unittests/test_generic_model.py
+++ b/clients/client-python/tests/unittests/test_generic_model.py
@@ -19,6 +19,7 @@ import unittest
from gravitino.api.tag.supports_tags import SupportsTags
from gravitino.client.generic_model import GenericModel
+from gravitino.exceptions.base import UnsupportedOperationException
from gravitino.name_identifier import NameIdentifier
from gravitino.namespace import Namespace
from gravitino.utils.http_client import HTTPClient
@@ -68,3 +69,13 @@ class TestGenericModel(unittest.TestCase):
for method in expected_methods
)
)
+
+ def test_supports_roles_is_not_supported_yet(self) -> None:
+ generic_model = GenericModel(
+ build_model_dto(),
+ TestGenericModel._rest_client,
+ TestGenericModel._model_ident.namespace(),
+ )
+
+ with self.assertRaisesRegex(UnsupportedOperationException, "Not
supported yet"):
+ generic_model.supports_roles()
diff --git a/clients/client-python/tests/unittests/test_supports_roles.py
b/clients/client-python/tests/unittests/test_supports_roles.py
new file mode 100644
index 0000000000..2f29161b92
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_supports_roles.py
@@ -0,0 +1,158 @@
+# 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.
+
+import unittest
+from unittest.mock import Mock, patch
+
+from gravitino.api.authorization.supports_roles import SupportsRoles
+from gravitino.api.catalog import Catalog
+from gravitino.api.file.fileset import Fileset
+from gravitino.api.metalake import Metalake
+from gravitino.api.metadata_object import MetadataObject
+from gravitino.api.metadata_objects import MetadataObjects
+from gravitino.api.model.model import Model
+from gravitino.api.rel.table import Table
+from gravitino.api.schema import Schema
+from gravitino.client.generic_fileset import GenericFileset
+from gravitino.client.generic_schema import GenericSchema
+from gravitino.client.gravitino_metalake import GravitinoMetalake
+from gravitino.client.relational_catalog import RelationalCatalog
+from gravitino.client.relational_table import RelationalTable
+from gravitino.dto.audit_dto import AuditDTO
+from gravitino.dto.fileset_dto import FilesetDTO
+from gravitino.dto.metalake_dto import MetalakeDTO
+from gravitino.dto.rel.table_dto import TableDTO
+from gravitino.dto.responses.name_list_response import NameListResponse
+from gravitino.exceptions.base import UnsupportedOperationException
+from gravitino.exceptions.handlers.role_error_handler import ROLE_ERROR_HANDLER
+from gravitino.namespace import Namespace
+from gravitino.utils import HTTPClient
+from tests.unittests import mock_base
+from tests.unittests.fixtures.table_fixtures import TABLE_DTO_JSON_STRING
+
+
+class TestSupportsRoles(unittest.TestCase):
+ METALAKE_NAME = "metalake"
+ CATALOG_NAME = "catalog"
+ SCHEMA_NAME = "schema"
+ REST_CLIENT = HTTPClient("http://localhost:8090")
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ audit = AuditDTO(_creator="test")
+ cls.metalake = GravitinoMetalake(
+ MetalakeDTO(cls.METALAKE_NAME, "comment", {}, audit),
cls.REST_CLIENT
+ )
+ cls.catalog = RelationalCatalog(
+ catalog_namespace=Namespace.of(cls.METALAKE_NAME),
+ name=cls.CATALOG_NAME,
+ catalog_type=Catalog.Type.RELATIONAL,
+ provider="test",
+ audit=audit,
+ rest_client=cls.REST_CLIENT,
+ )
+ cls.schema = GenericSchema(
+ mock_base.build_schema_dto(name=cls.SCHEMA_NAME),
+ cls.REST_CLIENT,
+ cls.METALAKE_NAME,
+ cls.CATALOG_NAME,
+ )
+ cls.table = RelationalTable(
+ Namespace.of(cls.METALAKE_NAME, cls.CATALOG_NAME, cls.SCHEMA_NAME),
+ TableDTO.from_json(TABLE_DTO_JSON_STRING),
+ cls.REST_CLIENT,
+ )
+ cls.fileset = GenericFileset(
+ FilesetDTO(
+ _name="fileset",
+ _comment="comment",
+ _type=Fileset.Type.EXTERNAL,
+ _properties={},
+ _storage_locations={Fileset.LOCATION_NAME_UNKNOWN:
"/tmp/fileset"},
+ _audit=audit,
+ ),
+ cls.REST_CLIENT,
+ Namespace.of(cls.METALAKE_NAME, cls.CATALOG_NAME, cls.SCHEMA_NAME),
+ )
+
+ def test_list_roles_for_metalake(self) -> None:
+ self._test_list_roles(
+ self.metalake.supports_roles(),
+ MetadataObjects.of([self.METALAKE_NAME],
MetadataObject.Type.METALAKE),
+ )
+
+ def test_list_roles_for_catalog(self) -> None:
+ self._test_list_roles(
+ self.catalog.supports_roles(),
+ MetadataObjects.of([self.CATALOG_NAME],
MetadataObject.Type.CATALOG),
+ )
+
+ def test_list_roles_for_schema(self) -> None:
+ self._test_list_roles(
+ self.schema.supports_roles(),
+ MetadataObjects.of(
+ [self.CATALOG_NAME, self.SCHEMA_NAME],
MetadataObject.Type.SCHEMA
+ ),
+ )
+
+ def test_list_roles_for_table(self) -> None:
+ self._test_list_roles(
+ self.table.supports_roles(),
+ MetadataObjects.of(
+ [self.CATALOG_NAME, self.SCHEMA_NAME, self.table.name()],
+ MetadataObject.Type.TABLE,
+ ),
+ )
+
+ def test_list_roles_for_fileset(self) -> None:
+ self._test_list_roles(
+ self.fileset.supports_roles(),
+ MetadataObjects.of(
+ [self.CATALOG_NAME, self.SCHEMA_NAME, self.fileset.name()],
+ MetadataObject.Type.FILESET,
+ ),
+ )
+
+ def test_default_supports_roles_raises_unsupported_operation(self) -> None:
+ metadata_object_types = [Metalake, Catalog, Schema, Table, Fileset,
Model]
+
+ for metadata_object_type in metadata_object_types:
+ with
self.subTest(metadata_object_type=metadata_object_type.__name__):
+ metadata_object = Mock(spec=metadata_object_type)
+ with self.assertRaises(UnsupportedOperationException):
+ metadata_object_type.supports_roles(metadata_object)
+
+ def _test_list_roles(
+ self, supports_roles: SupportsRoles, metadata_object: MetadataObject
+ ) -> None:
+ expected_roles = ["role1", "role2"]
+ mock_response = mock_base.mock_http_response(
+ NameListResponse(0, expected_roles).to_json()
+ )
+
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.get",
+ return_value=mock_response,
+ ) as mock_get:
+ self.assertEqual(expected_roles,
supports_roles.list_binding_role_names())
+ mock_get.assert_called_once_with(
+ "api/metalakes/metalake/objects/"
+ f"{metadata_object.type().name.lower()}/"
+ f"{metadata_object.full_name()}/roles",
+ params={},
+ error_handler=ROLE_ERROR_HANDLER,
+ )