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 1305ae1832 [#11096] feat(client-python): add Grant/Revoke
authorization operations (#11274)
1305ae1832 is described below
commit 1305ae183212a14deb178c8e5395029c7ce8d032
Author: Sun Yuhan <[email protected]>
AuthorDate: Tue Jun 2 12:27:12 2026 +0800
[#11096] feat(client-python): add Grant/Revoke authorization operations
(#11274)
### What changes were proposed in this pull request?
Add Grant/Revoke authorization operations to the Python client SDK:
- **PermissionErrorHandler**: Error handler for grant/revoke REST API
calls
- **Grant/Revoke roles**: `grant_roles_to_user`,
`revoke_roles_from_user`, `grant_roles_to_group`,
`revoke_roles_from_group`
- **Grant/Revoke privileges**: `grant_privileges_to_role`,
`revoke_privileges_from_role`
- **Input validation**: All grant/revoke methods validate parameters
with `Precondition.check_string_not_empty`
### Why are the changes needed?
This completes the Role authorization piece of issue #10782, building on
top of PR #11210 (Role CRUD) which is already merged.
Fix: #11096
### Does this PR introduce _any_ user-facing change?
Yes — new public APIs on `GravitinoClient` and `GravitinoMetalake`:
- `grant_roles_to_user(role_names, user_name)`
- `revoke_roles_from_user(role_names, user_name)`
- `grant_roles_to_group(role_names, group_name)`
- `revoke_roles_from_group(role_names, group_name)`
- `grant_privileges_to_role(role_name, securable_object, privileges)`
- `revoke_privileges_from_role(role_name, securable_object, privileges)`
### How was this patch tested?
- Unit tests: 6 grant/revoke mock tests, 6 client delegate tests, 13
permission error handler assertions
- Integration tests: 3 tests against a live Gravitino server
(grant/revoke roles to user, grant/revoke roles to group, grant/revoke
privileges to role)
- Linting: `ruff check` clean, `pylint` 10/10
---------
Co-authored-by: Sun Yuhan <[email protected]>
---
.../gravitino/client/gravitino_client.py | 123 ++++++++
.../gravitino/client/gravitino_metalake.py | 220 ++++++++++++++
.../dto/requests/privilege_grant_request.py | 46 +++
.../dto/requests/privilege_revoke_request.py | 46 +++
.../gravitino/dto/requests/role_grant_request.py | 45 +++
.../gravitino/dto/requests/role_revoke_request.py | 45 +++
clients/client-python/gravitino/exceptions/base.py | 4 +
.../handlers/permission_error_handler.py | 81 +++++
.../tests/integration/test_role_management.py | 48 +++
.../client/test_metalake_role_operations.py | 326 +++++++++++++++++++++
.../dto/requests/test_grant_revoke_requests.py | 97 ++++++
.../tests/unittests/test_error_handler.py | 93 ++++++
12 files changed, 1174 insertions(+)
diff --git a/clients/client-python/gravitino/client/gravitino_client.py
b/clients/client-python/gravitino/client/gravitino_client.py
index d8aaf74dad..3e936d2eb0 100644
--- a/clients/client-python/gravitino/client/gravitino_client.py
+++ b/clients/client-python/gravitino/client/gravitino_client.py
@@ -21,6 +21,7 @@ from typing import Dict, List, Optional
from gravitino.api.authorization.group import Group
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.user import User
@@ -573,3 +574,125 @@ class GravitinoClient(GravitinoClientBase, SupportsJobs,
TagOperations):
NoSuchMetalakeException: If the metalake does not exist.
"""
return self.get_metalake().list_role_names()
+
+ # Grant/Revoke operations
+
+ def grant_roles_to_user(self, role_names: List[str], user_name: str) ->
User:
+ """Grant roles to a user.
+
+ Args:
+ role_names: The names of the roles to grant.
+ user_name: The name of the user.
+
+ Returns:
+ The updated User object.
+
+ Raises:
+ NoSuchRoleException: If the role does not exist.
+ NoSuchUserException: If the user does not exist.
+ NoSuchMetalakeException: If the metalake does not exist.
+ """
+ return self.get_metalake().grant_roles_to_user(role_names, user_name)
+
+ def revoke_roles_from_user(self, role_names: List[str], user_name: str) ->
User:
+ """Revoke roles from a user.
+
+ Args:
+ role_names: The names of the roles to revoke.
+ user_name: The name of the user.
+
+ Returns:
+ The updated User object.
+
+ Raises:
+ NoSuchRoleException: If the role does not exist.
+ NoSuchUserException: If the user does not exist.
+ NoSuchMetalakeException: If the metalake does not exist.
+ """
+ return self.get_metalake().revoke_roles_from_user(role_names,
user_name)
+
+ def grant_roles_to_group(self, role_names: List[str], group_name: str) ->
Group:
+ """Grant roles to a group.
+
+ Args:
+ role_names: The names of the roles to grant.
+ group_name: The name of the group.
+
+ Returns:
+ The updated Group object.
+
+ Raises:
+ NoSuchRoleException: If the role does not exist.
+ NoSuchGroupException: If the group does not exist.
+ NoSuchMetalakeException: If the metalake does not exist.
+ """
+ return self.get_metalake().grant_roles_to_group(role_names, group_name)
+
+ def revoke_roles_from_group(self, role_names: List[str], group_name: str)
-> Group:
+ """Revoke roles from a group.
+
+ Args:
+ role_names: The names of the roles to revoke.
+ group_name: The name of the group.
+
+ Returns:
+ The updated Group object.
+
+ Raises:
+ NoSuchRoleException: If the role does not exist.
+ NoSuchGroupException: If the group does not exist.
+ NoSuchMetalakeException: If the metalake does not exist.
+ """
+ return self.get_metalake().revoke_roles_from_group(role_names,
group_name)
+
+ def grant_privileges_to_role(
+ self,
+ role_name: str,
+ securable_object: SecurableObject,
+ privileges: List[Privilege],
+ ) -> Role:
+ """Grant privileges to a role on a securable object.
+
+ Args:
+ role_name: The name of the role.
+ securable_object: The securable object.
+ privileges: The privileges to grant.
+
+ Returns:
+ The updated Role object.
+
+ Raises:
+ NoSuchRoleException: If the role does not exist.
+ NoSuchMetalakeException: If the metalake does not exist.
+ NoSuchMetadataObjectException: If the securable object does not
exist.
+ IllegalPrivilegeException: If a privilege is invalid.
+ """
+ return self.get_metalake().grant_privileges_to_role(
+ role_name, securable_object, privileges
+ )
+
+ def revoke_privileges_from_role(
+ self,
+ role_name: str,
+ securable_object: SecurableObject,
+ privileges: List[Privilege],
+ ) -> Role:
+ """Revoke privileges from a role on a securable object.
+
+ Args:
+ role_name: The name of the role.
+ securable_object: The securable object.
+ privileges: The privileges to revoke.
+
+ Returns:
+ The updated Role object.
+
+ Raises:
+ NoSuchRoleException: If the role does not exist.
+ NoSuchMetalakeException: If the metalake does not exist.
+ NoSuchMetadataObjectException: If the securable object does not
exist.
+ IllegalPrivilegeException: If a privilege is invalid.
+ """
+ return self.get_metalake().revoke_privileges_from_role(
+ role_name, securable_object, privileges
+ )
diff --git a/clients/client-python/gravitino/client/gravitino_metalake.py
b/clients/client-python/gravitino/client/gravitino_metalake.py
index 553b0528dc..75394a1208 100644
--- a/clients/client-python/gravitino/client/gravitino_metalake.py
+++ b/clients/client-python/gravitino/client/gravitino_metalake.py
@@ -21,6 +21,7 @@ from typing import Dict, List, Optional
from gravitino.api.authorization.group import Group
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.user import User
@@ -53,6 +54,10 @@ from gravitino.dto.requests.tag_updates_request import
TagUpdatesRequest
from gravitino.dto.requests.user_add_request import UserAddRequest
from gravitino.dto.requests.group_add_request import GroupAddRequest
from gravitino.dto.requests.role_create_request import RoleCreateRequest
+from gravitino.dto.requests.privilege_grant_request import
PrivilegeGrantRequest
+from gravitino.dto.requests.privilege_revoke_request import
PrivilegeRevokeRequest
+from gravitino.dto.requests.role_grant_request import RoleGrantRequest
+from gravitino.dto.requests.role_revoke_request import RoleRevokeRequest
from gravitino.dto.responses.catalog_list_response import CatalogListResponse
from gravitino.dto.responses.catalog_response import CatalogResponse
from gravitino.dto.responses.drop_response import DropResponse
@@ -85,6 +90,9 @@ from gravitino.dto.responses.role_response import (
)
from gravitino.exceptions.handlers.catalog_error_handler import
CATALOG_ERROR_HANDLER
from gravitino.exceptions.handlers.group_error_handler import
GROUP_ERROR_HANDLER
+from gravitino.exceptions.handlers.permission_error_handler import (
+ PERMISSION_ERROR_HANDLER,
+)
from gravitino.exceptions.handlers.role_error_handler import ROLE_ERROR_HANDLER
from gravitino.exceptions.handlers.job_error_handler import JOB_ERROR_HANDLER
from gravitino.exceptions.handlers.owner_error_handler import
OWNER_ERROR_HANDLER
@@ -125,6 +133,16 @@ class GravitinoMetalake(
API_METALAKES_GROUP_PATH = "api/metalakes/{}/groups/{}"
API_METALAKES_ROLES_PATH = "api/metalakes/{}/roles"
API_METALAKES_ROLE_PATH = "api/metalakes/{}/roles/{}"
+ API_PERMISSIONS_USER_GRANT_PATH =
"api/metalakes/{}/permissions/users/{}/grant"
+ API_PERMISSIONS_USER_REVOKE_PATH =
"api/metalakes/{}/permissions/users/{}/revoke"
+ API_PERMISSIONS_GROUP_GRANT_PATH =
"api/metalakes/{}/permissions/groups/{}/grant"
+ API_PERMISSIONS_GROUP_REVOKE_PATH =
"api/metalakes/{}/permissions/groups/{}/revoke"
+ API_PERMISSIONS_ROLE_GRANT_PATH = (
+ "api/metalakes/{}/permissions/roles/{}/{}/{}/grant"
+ )
+ API_PERMISSIONS_ROLE_REVOKE_PATH = (
+ "api/metalakes/{}/permissions/roles/{}/{}/{}/revoke"
+ )
def __init__(self, metalake: MetalakeDTO = None, client: HTTPClient =
None):
super().__init__(
@@ -1116,3 +1134,205 @@ class GravitinoMetalake(
resp = RoleNamesListResponse.from_json(response.body,
infer_missing=True)
resp.validate()
return resp.names()
+
+ def grant_roles_to_user(self, role_names: List[str], user_name: str) ->
User:
+ """Grant roles to a user.
+
+ Args:
+ role_names: The names of the roles to grant.
+ user_name: The name of the user.
+
+ Returns:
+ The updated User object.
+
+ Raises:
+ NoSuchRoleException: If the role does not exist.
+ NoSuchUserException: If the user does not exist.
+ NoSuchMetalakeException: If the metalake does not exist.
+ """
+ Precondition.check_string_not_empty(
+ user_name, "user name must not be null or empty"
+ )
+ req = RoleGrantRequest(role_names)
+ req.validate()
+ url = self.API_PERMISSIONS_USER_GRANT_PATH.format(
+ encode_string(self.name()), encode_string(user_name)
+ )
+ response = self.rest_client.put(
+ url, json=req, error_handler=PERMISSION_ERROR_HANDLER
+ )
+ resp = UserResponse.from_json(response.body, infer_missing=True)
+ resp.validate()
+ return resp.user()
+
+ def revoke_roles_from_user(self, role_names: List[str], user_name: str) ->
User:
+ """Revoke roles from a user.
+
+ Args:
+ role_names: The names of the roles to revoke.
+ user_name: The name of the user.
+
+ Returns:
+ The updated User object.
+
+ Raises:
+ NoSuchRoleException: If the role does not exist.
+ NoSuchUserException: If the user does not exist.
+ NoSuchMetalakeException: If the metalake does not exist.
+ """
+ Precondition.check_string_not_empty(
+ user_name, "user name must not be null or empty"
+ )
+ req = RoleRevokeRequest(role_names)
+ req.validate()
+ url = self.API_PERMISSIONS_USER_REVOKE_PATH.format(
+ encode_string(self.name()), encode_string(user_name)
+ )
+ response = self.rest_client.put(
+ url, json=req, error_handler=PERMISSION_ERROR_HANDLER
+ )
+ resp = UserResponse.from_json(response.body, infer_missing=True)
+ resp.validate()
+ return resp.user()
+
+ def grant_roles_to_group(self, role_names: List[str], group_name: str) ->
Group:
+ """Grant roles to a group.
+
+ Args:
+ role_names: The names of the roles to grant.
+ group_name: The name of the group.
+
+ Returns:
+ The updated Group object.
+
+ Raises:
+ NoSuchRoleException: If the role does not exist.
+ NoSuchGroupException: If the group does not exist.
+ NoSuchMetalakeException: If the metalake does not exist.
+ """
+ Precondition.check_string_not_empty(
+ group_name, "group name must not be null or empty"
+ )
+ req = RoleGrantRequest(role_names)
+ req.validate()
+ url = self.API_PERMISSIONS_GROUP_GRANT_PATH.format(
+ encode_string(self.name()), encode_string(group_name)
+ )
+ response = self.rest_client.put(
+ url, json=req, error_handler=PERMISSION_ERROR_HANDLER
+ )
+ resp = GroupResponse.from_json(response.body, infer_missing=True)
+ resp.validate()
+ return resp.group()
+
+ def revoke_roles_from_group(self, role_names: List[str], group_name: str)
-> Group:
+ """Revoke roles from a group.
+
+ Args:
+ role_names: The names of the roles to revoke.
+ group_name: The name of the group.
+
+ Returns:
+ The updated Group object.
+
+ Raises:
+ NoSuchRoleException: If the role does not exist.
+ NoSuchGroupException: If the group does not exist.
+ NoSuchMetalakeException: If the metalake does not exist.
+ """
+ Precondition.check_string_not_empty(
+ group_name, "group name must not be null or empty"
+ )
+ req = RoleRevokeRequest(role_names)
+ req.validate()
+ url = self.API_PERMISSIONS_GROUP_REVOKE_PATH.format(
+ encode_string(self.name()), encode_string(group_name)
+ )
+ response = self.rest_client.put(
+ url, json=req, error_handler=PERMISSION_ERROR_HANDLER
+ )
+ resp = GroupResponse.from_json(response.body, infer_missing=True)
+ resp.validate()
+ return resp.group()
+
+ def grant_privileges_to_role(
+ self,
+ role_name: str,
+ securable_object: SecurableObject,
+ privileges: List[Privilege],
+ ) -> Role:
+ """Grant privileges to a role on a securable object.
+
+ Args:
+ role_name: The name of the role.
+ securable_object: The securable object.
+ privileges: The privileges to grant.
+
+ Returns:
+ The updated Role object.
+
+ Raises:
+ NoSuchRoleException: If the role does not exist.
+ NoSuchMetalakeException: If the metalake does not exist.
+ NoSuchMetadataObjectException: If the securable object does not
exist.
+ IllegalPrivilegeException: If a privilege is invalid.
+ """
+ Precondition.check_string_not_empty(
+ role_name, "role name must not be null or empty"
+ )
+ privilege_dtos = [DTOConverters.to_privilege_dto(p) for p in
privileges]
+ req = PrivilegeGrantRequest(privilege_dtos)
+ req.validate()
+ url = self.API_PERMISSIONS_ROLE_GRANT_PATH.format(
+ encode_string(self.name()),
+ encode_string(role_name),
+ encode_string(securable_object.type().name.lower()),
+ encode_string(securable_object.full_name()),
+ )
+ response = self.rest_client.put(
+ url, json=req, error_handler=PERMISSION_ERROR_HANDLER
+ )
+ resp = RoleResponse.from_json(response.body, infer_missing=True)
+ resp.validate()
+ return resp.role()
+
+ def revoke_privileges_from_role(
+ self,
+ role_name: str,
+ securable_object: SecurableObject,
+ privileges: List[Privilege],
+ ) -> Role:
+ """Revoke privileges from a role on a securable object.
+
+ Args:
+ role_name: The name of the role.
+ securable_object: The securable object.
+ privileges: The privileges to revoke.
+
+ Returns:
+ The updated Role object.
+
+ Raises:
+ NoSuchRoleException: If the role does not exist.
+ NoSuchMetalakeException: If the metalake does not exist.
+ NoSuchMetadataObjectException: If the securable object does not
exist.
+ IllegalPrivilegeException: If a privilege is invalid.
+ """
+ Precondition.check_string_not_empty(
+ role_name, "role name must not be null or empty"
+ )
+ privilege_dtos = [DTOConverters.to_privilege_dto(p) for p in
privileges]
+ req = PrivilegeRevokeRequest(privilege_dtos)
+ req.validate()
+ url = self.API_PERMISSIONS_ROLE_REVOKE_PATH.format(
+ encode_string(self.name()),
+ encode_string(role_name),
+ encode_string(securable_object.type().name.lower()),
+ encode_string(securable_object.full_name()),
+ )
+ response = self.rest_client.put(
+ url, json=req, error_handler=PERMISSION_ERROR_HANDLER
+ )
+ resp = RoleResponse.from_json(response.body, infer_missing=True)
+ resp.validate()
+ return resp.role()
diff --git
a/clients/client-python/gravitino/dto/requests/privilege_grant_request.py
b/clients/client-python/gravitino/dto/requests/privilege_grant_request.py
new file mode 100644
index 0000000000..d7dea8c932
--- /dev/null
+++ b/clients/client-python/gravitino/dto/requests/privilege_grant_request.py
@@ -0,0 +1,46 @@
+# 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.
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import List
+
+from dataclasses_json import config, dataclass_json
+
+from gravitino.dto.authorization.privilege_dto import PrivilegeDTO
+from gravitino.rest.rest_message import RESTRequest
+from gravitino.utils.precondition import Precondition
+
+
+@dataclass_json
+@dataclass
+class PrivilegeGrantRequest(RESTRequest):
+ """Represents a request to grant privileges."""
+
+ _privileges: List[PrivilegeDTO] = field(
+ default_factory=list, metadata=config(field_name="privileges")
+ )
+
+ def __init__(self, privileges: List[PrivilegeDTO]):
+ self._privileges = privileges
+
+ def validate(self) -> None:
+ Precondition.check_argument(
+ self._privileges is not None and len(self._privileges) > 0,
+ "privileges cannot be null or empty",
+ )
diff --git
a/clients/client-python/gravitino/dto/requests/privilege_revoke_request.py
b/clients/client-python/gravitino/dto/requests/privilege_revoke_request.py
new file mode 100644
index 0000000000..a835972b18
--- /dev/null
+++ b/clients/client-python/gravitino/dto/requests/privilege_revoke_request.py
@@ -0,0 +1,46 @@
+# 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.
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import List
+
+from dataclasses_json import config, dataclass_json
+
+from gravitino.dto.authorization.privilege_dto import PrivilegeDTO
+from gravitino.rest.rest_message import RESTRequest
+from gravitino.utils.precondition import Precondition
+
+
+@dataclass_json
+@dataclass
+class PrivilegeRevokeRequest(RESTRequest):
+ """Represents a request to revoke privileges."""
+
+ _privileges: List[PrivilegeDTO] = field(
+ default_factory=list, metadata=config(field_name="privileges")
+ )
+
+ def __init__(self, privileges: List[PrivilegeDTO]):
+ self._privileges = privileges
+
+ def validate(self) -> None:
+ Precondition.check_argument(
+ self._privileges is not None and len(self._privileges) > 0,
+ "privileges cannot be null or empty",
+ )
diff --git a/clients/client-python/gravitino/dto/requests/role_grant_request.py
b/clients/client-python/gravitino/dto/requests/role_grant_request.py
new file mode 100644
index 0000000000..38f3fe58c4
--- /dev/null
+++ b/clients/client-python/gravitino/dto/requests/role_grant_request.py
@@ -0,0 +1,45 @@
+# 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.
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import List
+
+from dataclasses_json import config, dataclass_json
+
+from gravitino.rest.rest_message import RESTRequest
+from gravitino.utils.precondition import Precondition
+
+
+@dataclass_json
+@dataclass
+class RoleGrantRequest(RESTRequest):
+ """Represents a request to grant roles."""
+
+ _role_names: List[str] = field(
+ default_factory=list, metadata=config(field_name="roleNames")
+ )
+
+ def __init__(self, role_names: List[str]):
+ self._role_names = role_names
+
+ def validate(self) -> None:
+ Precondition.check_argument(
+ self._role_names is not None and len(self._role_names) > 0,
+ "roleNames cannot be null or empty",
+ )
diff --git
a/clients/client-python/gravitino/dto/requests/role_revoke_request.py
b/clients/client-python/gravitino/dto/requests/role_revoke_request.py
new file mode 100644
index 0000000000..56cb2562bf
--- /dev/null
+++ b/clients/client-python/gravitino/dto/requests/role_revoke_request.py
@@ -0,0 +1,45 @@
+# 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.
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import List
+
+from dataclasses_json import config, dataclass_json
+
+from gravitino.rest.rest_message import RESTRequest
+from gravitino.utils.precondition import Precondition
+
+
+@dataclass_json
+@dataclass
+class RoleRevokeRequest(RESTRequest):
+ """Represents a request to revoke roles."""
+
+ _role_names: List[str] = field(
+ default_factory=list, metadata=config(field_name="roleNames")
+ )
+
+ def __init__(self, role_names: List[str]):
+ self._role_names = role_names
+
+ def validate(self) -> None:
+ Precondition.check_argument(
+ self._role_names is not None and len(self._role_names) > 0,
+ "roleNames cannot be null or empty",
+ )
diff --git a/clients/client-python/gravitino/exceptions/base.py
b/clients/client-python/gravitino/exceptions/base.py
index e7fe653a4e..023f5d490e 100644
--- a/clients/client-python/gravitino/exceptions/base.py
+++ b/clients/client-python/gravitino/exceptions/base.py
@@ -245,6 +245,10 @@ class
IllegalMetadataObjectException(IllegalArgumentException):
"""An exception thrown when a metadata object is invalid."""
+class IllegalRoleException(IllegalArgumentException):
+ """An exception thrown when a role is invalid."""
+
+
class NoSuchRoleException(NotFoundException):
"""Exception thrown when a role with specified name is not existed."""
diff --git
a/clients/client-python/gravitino/exceptions/handlers/permission_error_handler.py
b/clients/client-python/gravitino/exceptions/handlers/permission_error_handler.py
new file mode 100644
index 0000000000..d5a6fc904c
--- /dev/null
+++
b/clients/client-python/gravitino/exceptions/handlers/permission_error_handler.py
@@ -0,0 +1,81 @@
+# 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.
+
+
+from gravitino.constants.error import ErrorConstants
+from gravitino.dto.responses.error_response import ErrorResponse
+from gravitino.exceptions.base import (
+ IllegalArgumentException,
+ IllegalMetadataObjectException,
+ IllegalPrivilegeException,
+ IllegalRoleException,
+ MetalakeNotInUseException,
+ NoSuchGroupException,
+ NoSuchMetadataObjectException,
+ NoSuchMetalakeException,
+ NoSuchRoleException,
+ NoSuchUserException,
+ NotFoundException,
+ UnsupportedOperationException,
+)
+from gravitino.exceptions.handlers.rest_error_handler import RestErrorHandler
+
+
+class PermissionErrorHandler(RestErrorHandler):
+ """Error handler for permission operations (grant/revoke)."""
+
+ # pylint: disable=too-many-branches
+ def handle(self, error_response: ErrorResponse):
+ error_message = error_response.format_error_message()
+ code = error_response.code()
+ exception_type = error_response.type()
+
+ if code == ErrorConstants.ILLEGAL_ARGUMENTS_CODE:
+ if exception_type == IllegalPrivilegeException.__name__:
+ raise IllegalPrivilegeException(error_message)
+ if exception_type == IllegalMetadataObjectException.__name__:
+ raise IllegalMetadataObjectException(error_message)
+ if exception_type == IllegalRoleException.__name__:
+ raise IllegalRoleException(error_message)
+ raise IllegalArgumentException(error_message)
+
+ if code == ErrorConstants.NOT_FOUND_CODE:
+ if exception_type == NoSuchMetalakeException.__name__:
+ raise NoSuchMetalakeException(error_message)
+ if exception_type == NoSuchUserException.__name__:
+ raise NoSuchUserException(error_message)
+ if exception_type == NoSuchGroupException.__name__:
+ raise NoSuchGroupException(error_message)
+ if exception_type == NoSuchRoleException.__name__:
+ raise NoSuchRoleException(error_message)
+ if exception_type == NoSuchMetadataObjectException.__name__:
+ raise NoSuchMetadataObjectException(error_message)
+ raise NotFoundException(error_message)
+
+ if code == ErrorConstants.UNSUPPORTED_OPERATION_CODE:
+ raise UnsupportedOperationException(error_message)
+
+ if code == ErrorConstants.NOT_IN_USE_CODE:
+ raise MetalakeNotInUseException(error_message)
+
+ if code == ErrorConstants.INTERNAL_ERROR_CODE:
+ raise RuntimeError(error_message)
+
+ super().handle(error_response)
+
+
+PERMISSION_ERROR_HANDLER = PermissionErrorHandler()
diff --git a/clients/client-python/tests/integration/test_role_management.py
b/clients/client-python/tests/integration/test_role_management.py
index a2f2519d31..99e6df2dfd 100644
--- a/clients/client-python/tests/integration/test_role_management.py
+++ b/clients/client-python/tests/integration/test_role_management.py
@@ -120,3 +120,51 @@ class TestRoleManagement(IntegrationTestEnv):
names = self._gravitino_client.list_role_names()
self.assertIn("role_a", names)
self.assertIn("role_b", names)
+
+ def test_grant_revoke_roles_to_user(self):
+ self._gravitino_client.create_role("user_role")
+ self._gravitino_client.add_user("alice")
+
+ granted = self._gravitino_client.grant_roles_to_user(["user_role"],
"alice")
+ self.assertIn("user_role", granted.roles())
+
+ revoked = self._gravitino_client.revoke_roles_from_user(["user_role"],
"alice")
+ self.assertNotIn("user_role", revoked.roles())
+
+ def test_grant_revoke_roles_to_group(self):
+ self._gravitino_client.create_role("group_role")
+ self._gravitino_client.add_group("engineers")
+
+ granted = self._gravitino_client.grant_roles_to_group(
+ ["group_role"], "engineers"
+ )
+ self.assertIn("group_role", granted.roles())
+
+ revoked = self._gravitino_client.revoke_roles_from_group(
+ ["group_role"], "engineers"
+ )
+ self.assertNotIn("group_role", revoked.roles())
+
+ def test_grant_revoke_privileges_to_role(self):
+ self._gravitino_client.create_role("priv_role")
+
+ privileges = [Privileges.allow("USE_CATALOG")]
+ securable_obj = SecurableObjects.of_metalake(self._metalake_name,
privileges)
+
+ granted = self._gravitino_client.grant_privileges_to_role(
+ "priv_role", securable_obj, privileges
+ )
+ self.assertEqual("priv_role", granted.name())
+ self.assertGreater(len(granted.securable_objects()), 0)
+ granted_privs = granted.securable_objects()[0].privileges()
+ granted_names = [p.name().name for p in granted_privs]
+ self.assertIn("USE_CATALOG", granted_names)
+
+ revoked = self._gravitino_client.revoke_privileges_from_role(
+ "priv_role", securable_obj, privileges
+ )
+ self.assertEqual("priv_role", revoked.name())
+ revoked_objs = revoked.securable_objects()
+ if revoked_objs:
+ revoked_names = [p.name().name for p in
revoked_objs[0].privileges()]
+ self.assertNotIn("USE_CATALOG", revoked_names)
diff --git
a/clients/client-python/tests/unittests/client/test_metalake_role_operations.py
b/clients/client-python/tests/unittests/client/test_metalake_role_operations.py
index b553c87ecb..50a2b34270 100644
---
a/clients/client-python/tests/unittests/client/test_metalake_role_operations.py
+++
b/clients/client-python/tests/unittests/client/test_metalake_role_operations.py
@@ -19,23 +19,34 @@ import unittest
from unittest.mock import patch
from gravitino.api.authorization.privileges import Privilege
+from gravitino.api.authorization.securable_objects import SecurableObjects
from gravitino.api.metadata_object import MetadataObject
from gravitino.client.gravitino_client import GravitinoClient
from gravitino.dto.audit_dto import AuditDTO
+from gravitino.dto.authorization.group_dto import GroupDTO
from gravitino.dto.authorization.privilege_dto import PrivilegeDTO
from gravitino.dto.authorization.role_dto import RoleDTO
from gravitino.dto.authorization.securable_object_dto import SecurableObjectDTO
+from gravitino.dto.authorization.user_dto import UserDTO
from gravitino.dto.requests.role_create_request import RoleCreateRequest
+from gravitino.dto.requests.role_grant_request import RoleGrantRequest
+from gravitino.dto.requests.role_revoke_request import RoleRevokeRequest
+from gravitino.dto.requests.privilege_grant_request import
PrivilegeGrantRequest
from gravitino.dto.responses.drop_response import DropResponse
+from gravitino.dto.responses.group_response import GroupResponse
from gravitino.dto.responses.role_response import (
RoleNamesListResponse,
RoleResponse,
)
+from gravitino.dto.responses.user_response import UserResponse
from gravitino.exceptions.base import (
IllegalArgumentException,
NoSuchRoleException,
RoleAlreadyExistsException,
)
+from gravitino.exceptions.handlers.permission_error_handler import (
+ PERMISSION_ERROR_HANDLER,
+)
from gravitino.exceptions.handlers.role_error_handler import ROLE_ERROR_HANDLER
from tests.unittests import mock_base
@@ -59,9 +70,43 @@ def _build_role_dto(
)
+def _build_user_dto(name: str = "alice", roles: list | None = None) -> UserDTO:
+ return (
+ UserDTO.builder()
+ .with_name(name)
+ .with_roles(roles or [])
+ .with_audit(_audit())
+ .build()
+ )
+
+
+def _build_group_dto(name: str = "engineers", roles: list | None = None) ->
GroupDTO:
+ return (
+ GroupDTO.builder()
+ .with_name(name)
+ .with_roles(roles if roles is not None else [])
+ .with_audit(_audit())
+ .build()
+ )
+
+
class TestMetalakeRoleOperations(unittest.TestCase):
METALAKE_ROLES_PATH = "api/metalakes/metalake_demo/roles"
METALAKE_ROLE_PATH = "api/metalakes/metalake_demo/roles/admin_role"
+ PERMISSIONS_USER_GRANT_PATH = (
+ "api/metalakes/metalake_demo/permissions/users/alice/grant"
+ )
+ PERMISSIONS_USER_REVOKE_PATH = (
+ "api/metalakes/metalake_demo/permissions/users/alice/revoke"
+ )
+ PERMISSIONS_GROUP_GRANT_PATH = (
+ "api/metalakes/metalake_demo/permissions/groups/engineers/grant"
+ )
+ PERMISSIONS_GROUP_REVOKE_PATH = (
+ "api/metalakes/metalake_demo/permissions/groups/engineers/revoke"
+ )
+ PERMISSIONS_ROLE_GRANT_PATH =
"api/metalakes/metalake_demo/permissions/roles/admin_role/catalog/my_catalog/grant"
+ PERMISSIONS_ROLE_REVOKE_PATH =
"api/metalakes/metalake_demo/permissions/roles/admin_role/catalog/my_catalog/revoke"
def test_create_role(self):
metalake = mock_base.mock_load_metalake()
@@ -190,6 +235,163 @@ class TestMetalakeRoleOperations(unittest.TestCase):
ROLE_ERROR_HANDLER, mock_get.call_args.kwargs["error_handler"]
)
+ def test_grant_roles_to_user(self):
+ metalake = mock_base.mock_load_metalake()
+ user = _build_user_dto(roles=["admin_role"])
+ mock_resp = mock_base.mock_http_response(UserResponse(0,
user).to_json())
+
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.put",
+ return_value=mock_resp,
+ ) as mock_put:
+ result = metalake.grant_roles_to_user(["admin_role"], "alice")
+
+ self.assertEqual("alice", result.name())
+ self.assertEqual(["admin_role"], result.roles())
+ mock_put.assert_called_once()
+ self.assertEqual(
+ self.PERMISSIONS_USER_GRANT_PATH, mock_put.call_args.args[0]
+ )
+ self.assertIsInstance(mock_put.call_args.kwargs["json"],
RoleGrantRequest)
+ self.assertIs(
+ PERMISSION_ERROR_HANDLER,
+ mock_put.call_args.kwargs["error_handler"],
+ )
+
+ def test_revoke_roles_from_user(self):
+ metalake = mock_base.mock_load_metalake()
+ user = _build_user_dto(roles=[])
+ mock_resp = mock_base.mock_http_response(UserResponse(0,
user).to_json())
+
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.put",
+ return_value=mock_resp,
+ ) as mock_put:
+ result = metalake.revoke_roles_from_user(["admin_role"], "alice")
+
+ self.assertEqual([], result.roles())
+ mock_put.assert_called_once()
+ self.assertEqual(
+ self.PERMISSIONS_USER_REVOKE_PATH, mock_put.call_args.args[0]
+ )
+ self.assertIsInstance(mock_put.call_args.kwargs["json"],
RoleRevokeRequest)
+ self.assertIs(
+ PERMISSION_ERROR_HANDLER,
+ mock_put.call_args.kwargs["error_handler"],
+ )
+
+ def test_grant_roles_to_group(self):
+ metalake = mock_base.mock_load_metalake()
+ group = _build_group_dto(roles=["admin_role"])
+ mock_resp = mock_base.mock_http_response(GroupResponse(0,
group).to_json())
+
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.put",
+ return_value=mock_resp,
+ ) as mock_put:
+ result = metalake.grant_roles_to_group(["admin_role"], "engineers")
+
+ self.assertEqual("engineers", result.name())
+ self.assertEqual(["admin_role"], result.roles())
+ mock_put.assert_called_once()
+ self.assertEqual(
+ self.PERMISSIONS_GROUP_GRANT_PATH, mock_put.call_args.args[0]
+ )
+ self.assertIs(
+ PERMISSION_ERROR_HANDLER,
+ mock_put.call_args.kwargs["error_handler"],
+ )
+
+ def test_revoke_roles_from_group(self):
+ metalake = mock_base.mock_load_metalake()
+ group = _build_group_dto(roles=[])
+ mock_resp = mock_base.mock_http_response(GroupResponse(0,
group).to_json())
+
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.put",
+ return_value=mock_resp,
+ ) as mock_put:
+ result = metalake.revoke_roles_from_group(["admin_role"],
"engineers")
+
+ self.assertEqual([], result.roles())
+ mock_put.assert_called_once()
+ self.assertEqual(
+ self.PERMISSIONS_GROUP_REVOKE_PATH, mock_put.call_args.args[0]
+ )
+ self.assertIs(
+ PERMISSION_ERROR_HANDLER,
+ mock_put.call_args.kwargs["error_handler"],
+ )
+
+ def test_grant_privileges_to_role(self):
+ metalake = mock_base.mock_load_metalake()
+ sec_obj = SecurableObjectDTO(
+ "my_catalog",
+ MetadataObject.Type.CATALOG,
+ [
+ PrivilegeDTO(Privilege.Name.USE_CATALOG,
Privilege.Condition.ALLOW),
+ PrivilegeDTO(Privilege.Name.CREATE_SCHEMA,
Privilege.Condition.ALLOW),
+ ],
+ )
+ role = _build_role_dto(sec_objs=[sec_obj])
+ mock_resp = mock_base.mock_http_response(RoleResponse(0,
role).to_json())
+
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.put",
+ return_value=mock_resp,
+ ) as mock_put:
+ securable_obj = SecurableObjects.of_catalog(
+ "my_catalog",
+ [PrivilegeDTO(Privilege.Name.USE_CATALOG,
Privilege.Condition.ALLOW)],
+ )
+ result = metalake.grant_privileges_to_role(
+ "admin_role",
+ securable_obj,
+ [PrivilegeDTO(Privilege.Name.USE_CATALOG,
Privilege.Condition.ALLOW)],
+ )
+
+ self.assertEqual("admin_role", result.name())
+ mock_put.assert_called_once()
+ self.assertEqual(
+ self.PERMISSIONS_ROLE_GRANT_PATH, mock_put.call_args.args[0]
+ )
+ self.assertIsInstance(
+ mock_put.call_args.kwargs["json"], PrivilegeGrantRequest
+ )
+ self.assertIs(
+ PERMISSION_ERROR_HANDLER,
+ mock_put.call_args.kwargs["error_handler"],
+ )
+
+ def test_revoke_privileges_from_role(self):
+ metalake = mock_base.mock_load_metalake()
+ role = _build_role_dto(sec_objs=[])
+ mock_resp = mock_base.mock_http_response(RoleResponse(0,
role).to_json())
+
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.put",
+ return_value=mock_resp,
+ ) as mock_put:
+ securable_obj = SecurableObjects.of_catalog(
+ "my_catalog",
+ [PrivilegeDTO(Privilege.Name.USE_CATALOG,
Privilege.Condition.ALLOW)],
+ )
+ result = metalake.revoke_privileges_from_role(
+ "admin_role",
+ securable_obj,
+ [PrivilegeDTO(Privilege.Name.USE_CATALOG,
Privilege.Condition.ALLOW)],
+ )
+
+ self.assertEqual("admin_role", result.name())
+ mock_put.assert_called_once()
+ self.assertEqual(
+ self.PERMISSIONS_ROLE_REVOKE_PATH, mock_put.call_args.args[0]
+ )
+ self.assertIs(
+ PERMISSION_ERROR_HANDLER,
+ mock_put.call_args.kwargs["error_handler"],
+ )
+
class TestGravitinoClientRoleDelegates(unittest.TestCase):
"""Verify that GravitinoClient correctly delegates Role operations."""
@@ -268,3 +470,127 @@ class TestGravitinoClientRoleDelegates(unittest.TestCase):
):
result = client.list_role_names()
self.assertEqual(["role1", "role2"], result)
+
+ def test_client_grant_roles_to_user(self):
+ client = self._make_client()
+ user = _build_user_dto(roles=["admin_role"])
+ mock_resp = mock_base.mock_http_response(UserResponse(0,
user).to_json())
+ with (
+ patch.object(
+ GravitinoClient,
+ "get_metalake",
+ return_value=mock_base.mock_load_metalake(),
+ ),
+ patch(
+ "gravitino.utils.http_client.HTTPClient.put",
+ return_value=mock_resp,
+ ),
+ ):
+ result = client.grant_roles_to_user(["admin_role"], "alice")
+ self.assertEqual(["admin_role"], result.roles())
+
+ def test_client_revoke_roles_from_user(self):
+ client = self._make_client()
+ user = _build_user_dto(roles=[])
+ mock_resp = mock_base.mock_http_response(UserResponse(0,
user).to_json())
+ with (
+ patch.object(
+ GravitinoClient,
+ "get_metalake",
+ return_value=mock_base.mock_load_metalake(),
+ ),
+ patch(
+ "gravitino.utils.http_client.HTTPClient.put",
+ return_value=mock_resp,
+ ),
+ ):
+ result = client.revoke_roles_from_user(["admin_role"], "alice")
+ self.assertEqual([], result.roles())
+
+ def test_client_grant_roles_to_group(self):
+ client = self._make_client()
+ group = _build_group_dto(roles=["admin_role"])
+ mock_resp = mock_base.mock_http_response(GroupResponse(0,
group).to_json())
+ with (
+ patch.object(
+ GravitinoClient,
+ "get_metalake",
+ return_value=mock_base.mock_load_metalake(),
+ ),
+ patch(
+ "gravitino.utils.http_client.HTTPClient.put",
+ return_value=mock_resp,
+ ),
+ ):
+ result = client.grant_roles_to_group(["admin_role"], "engineers")
+ self.assertEqual(["admin_role"], result.roles())
+
+ def test_client_revoke_roles_from_group(self):
+ client = self._make_client()
+ group = _build_group_dto(roles=[])
+ mock_resp = mock_base.mock_http_response(GroupResponse(0,
group).to_json())
+ with (
+ patch.object(
+ GravitinoClient,
+ "get_metalake",
+ return_value=mock_base.mock_load_metalake(),
+ ),
+ patch(
+ "gravitino.utils.http_client.HTTPClient.put",
+ return_value=mock_resp,
+ ),
+ ):
+ result = client.revoke_roles_from_group(["admin_role"],
"engineers")
+ self.assertEqual([], result.roles())
+
+ def test_client_grant_privileges_to_role(self):
+ client = self._make_client()
+ role = _build_role_dto()
+ mock_resp = mock_base.mock_http_response(RoleResponse(0,
role).to_json())
+ with (
+ patch.object(
+ GravitinoClient,
+ "get_metalake",
+ return_value=mock_base.mock_load_metalake(),
+ ),
+ patch(
+ "gravitino.utils.http_client.HTTPClient.put",
+ return_value=mock_resp,
+ ),
+ ):
+ securable_obj = SecurableObjects.of_catalog(
+ "my_catalog",
+ [PrivilegeDTO(Privilege.Name.USE_CATALOG,
Privilege.Condition.ALLOW)],
+ )
+ result = client.grant_privileges_to_role(
+ "admin_role",
+ securable_obj,
+ [PrivilegeDTO(Privilege.Name.USE_CATALOG,
Privilege.Condition.ALLOW)],
+ )
+ self.assertEqual("admin_role", result.name())
+
+ def test_client_revoke_privileges_from_role(self):
+ client = self._make_client()
+ role = _build_role_dto()
+ mock_resp = mock_base.mock_http_response(RoleResponse(0,
role).to_json())
+ with (
+ patch.object(
+ GravitinoClient,
+ "get_metalake",
+ return_value=mock_base.mock_load_metalake(),
+ ),
+ patch(
+ "gravitino.utils.http_client.HTTPClient.put",
+ return_value=mock_resp,
+ ),
+ ):
+ securable_obj = SecurableObjects.of_catalog(
+ "my_catalog",
+ [PrivilegeDTO(Privilege.Name.USE_CATALOG,
Privilege.Condition.ALLOW)],
+ )
+ result = client.revoke_privileges_from_role(
+ "admin_role",
+ securable_obj,
+ [PrivilegeDTO(Privilege.Name.USE_CATALOG,
Privilege.Condition.ALLOW)],
+ )
+ self.assertEqual("admin_role", result.name())
diff --git
a/clients/client-python/tests/unittests/dto/requests/test_grant_revoke_requests.py
b/clients/client-python/tests/unittests/dto/requests/test_grant_revoke_requests.py
new file mode 100644
index 0000000000..37c281de4f
--- /dev/null
+++
b/clients/client-python/tests/unittests/dto/requests/test_grant_revoke_requests.py
@@ -0,0 +1,97 @@
+# 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 json
+import unittest
+
+from gravitino.api.authorization.privileges import Privilege
+from gravitino.dto.authorization.privilege_dto import PrivilegeDTO
+from gravitino.dto.requests.privilege_grant_request import
PrivilegeGrantRequest
+from gravitino.dto.requests.privilege_revoke_request import
PrivilegeRevokeRequest
+from gravitino.dto.requests.role_grant_request import RoleGrantRequest
+from gravitino.dto.requests.role_revoke_request import RoleRevokeRequest
+
+
+class TestRoleGrantRequest(unittest.TestCase):
+ def test_create(self):
+ req = RoleGrantRequest(["role1", "role2"])
+ data = json.loads(req.to_json())
+ self.assertEqual(["role1", "role2"], data["roleNames"])
+
+ def test_validate(self):
+ req = RoleGrantRequest(["role1"])
+ req.validate()
+
+ def test_validate_empty_rejected(self):
+ req = RoleGrantRequest([])
+ with self.assertRaises(ValueError):
+ req.validate()
+
+ def test_json_roundtrip(self):
+ req = RoleGrantRequest(["role1"])
+ encoded = req.to_json()
+ self.assertIn("roleNames", encoded)
+
+
+class TestRoleRevokeRequest(unittest.TestCase):
+ def test_create(self):
+ req = RoleRevokeRequest(["role1"])
+ data = json.loads(req.to_json())
+ self.assertEqual(["role1"], data["roleNames"])
+
+ def test_validate_empty_rejected(self):
+ req = RoleRevokeRequest([])
+ with self.assertRaises(ValueError):
+ req.validate()
+
+
+class TestPrivilegeGrantRequest(unittest.TestCase):
+ def test_create(self):
+ privileges = [
+ PrivilegeDTO(Privilege.Name.USE_CATALOG, Privilege.Condition.ALLOW)
+ ]
+ req = PrivilegeGrantRequest(privileges)
+ data = json.loads(req.to_json())
+ self.assertEqual(1, len(data["privileges"]))
+
+ def test_validate_empty_rejected(self):
+ req = PrivilegeGrantRequest([])
+ with self.assertRaises(ValueError):
+ req.validate()
+
+ def test_json_roundtrip(self):
+ privileges = [
+ PrivilegeDTO(Privilege.Name.USE_CATALOG, Privilege.Condition.ALLOW)
+ ]
+ req = PrivilegeGrantRequest(privileges)
+ encoded = req.to_json()
+ self.assertIn("privileges", encoded)
+
+
+class TestPrivilegeRevokeRequest(unittest.TestCase):
+ def test_create(self):
+ privileges = [
+ PrivilegeDTO(Privilege.Name.USE_CATALOG, Privilege.Condition.DENY)
+ ]
+ req = PrivilegeRevokeRequest(privileges)
+ data = json.loads(req.to_json())
+ self.assertEqual(1, len(data["privileges"]))
+
+ def test_validate_empty_rejected(self):
+ req = PrivilegeRevokeRequest([])
+ with self.assertRaises(ValueError):
+ req.validate()
diff --git a/clients/client-python/tests/unittests/test_error_handler.py
b/clients/client-python/tests/unittests/test_error_handler.py
index a244e300fe..a7e52b443f 100644
--- a/clients/client-python/tests/unittests/test_error_handler.py
+++ b/clients/client-python/tests/unittests/test_error_handler.py
@@ -27,6 +27,7 @@ from gravitino.exceptions.base import (
IllegalArgumentException,
IllegalMetadataObjectException,
IllegalPrivilegeException,
+ IllegalRoleException,
InternalError,
MetalakeAlreadyExistsException,
MetalakeNotInUseException,
@@ -65,6 +66,9 @@ from gravitino.exceptions.handlers.partition_error_handler
import (
PARTITION_ERROR_HANDLER,
)
from gravitino.exceptions.handlers.rest_error_handler import REST_ERROR_HANDLER
+from gravitino.exceptions.handlers.permission_error_handler import (
+ PERMISSION_ERROR_HANDLER,
+)
from gravitino.exceptions.handlers.role_error_handler import ROLE_ERROR_HANDLER
from gravitino.exceptions.handlers.schema_error_handler import
SCHEMA_ERROR_HANDLER
from gravitino.exceptions.handlers.table_error_handler import
TABLE_ERROR_HANDLER
@@ -610,3 +614,92 @@ class TestErrorHandler(unittest.TestCase):
ROLE_ERROR_HANDLER.handle(
ErrorResponse.generate_error_response(Exception, "mock error")
)
+
+ def test_permission_error_handler(self):
+ with self.assertRaises(IllegalPrivilegeException):
+ PERMISSION_ERROR_HANDLER.handle(
+ ErrorResponse.generate_error_response(
+ IllegalPrivilegeException, "mock error"
+ )
+ )
+
+ with self.assertRaises(IllegalMetadataObjectException):
+ PERMISSION_ERROR_HANDLER.handle(
+ ErrorResponse.generate_error_response(
+ IllegalMetadataObjectException, "mock error"
+ )
+ )
+
+ with self.assertRaises(IllegalRoleException):
+ PERMISSION_ERROR_HANDLER.handle(
+ ErrorResponse.generate_error_response(
+ IllegalRoleException, "mock error"
+ )
+ )
+
+ with self.assertRaises(NoSuchMetalakeException):
+ PERMISSION_ERROR_HANDLER.handle(
+ ErrorResponse.generate_error_response(
+ NoSuchMetalakeException, "mock error"
+ )
+ )
+
+ with self.assertRaises(NoSuchUserException):
+ PERMISSION_ERROR_HANDLER.handle(
+ ErrorResponse.generate_error_response(NoSuchUserException,
"mock error")
+ )
+
+ with self.assertRaises(NoSuchGroupException):
+ PERMISSION_ERROR_HANDLER.handle(
+ ErrorResponse.generate_error_response(
+ NoSuchGroupException, "mock error"
+ )
+ )
+
+ with self.assertRaises(NoSuchRoleException):
+ PERMISSION_ERROR_HANDLER.handle(
+ ErrorResponse.generate_error_response(NoSuchRoleException,
"mock error")
+ )
+
+ with self.assertRaises(NoSuchMetadataObjectException):
+ PERMISSION_ERROR_HANDLER.handle(
+ ErrorResponse.generate_error_response(
+ NoSuchMetadataObjectException, "mock error"
+ )
+ )
+
+ with self.assertRaises(UnsupportedOperationException):
+ PERMISSION_ERROR_HANDLER.handle(
+ ErrorResponse.generate_error_response(
+ UnsupportedOperationException, "mock error"
+ )
+ )
+
+ with self.assertRaises(MetalakeNotInUseException):
+ PERMISSION_ERROR_HANDLER.handle(
+ ErrorResponse.generate_error_response(
+ MetalakeNotInUseException, "mock error"
+ )
+ )
+
+ with self.assertRaises(RuntimeError):
+ PERMISSION_ERROR_HANDLER.handle(
+ ErrorResponse.generate_error_response(InternalError, "mock
error")
+ )
+
+ with self.assertRaises(RESTException):
+ PERMISSION_ERROR_HANDLER.handle(
+ ErrorResponse.generate_error_response(Exception, "mock error")
+ )
+
+ with self.assertRaises(IllegalArgumentException):
+ PERMISSION_ERROR_HANDLER.handle(
+ ErrorResponse.generate_error_response(
+ IllegalArgumentException, "mock error"
+ )
+ )
+
+ with self.assertRaises(NotFoundException):
+ PERMISSION_ERROR_HANDLER.handle(
+ ErrorResponse.generate_error_response(NotFoundException, "mock
error")
+ )