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")
+            )

Reply via email to