Copilot commented on code in PR #11274:
URL: https://github.com/apache/gravitino/pull/11274#discussion_r3331536590
##########
clients/client-python/tests/integration/test_role_management.py:
##########
@@ -120,3 +120,43 @@ def test_list_role_names(self):
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())
+
+ revoked = self._gravitino_client.revoke_privileges_from_role(
+ "priv_role", securable_obj, privileges
+ )
+ self.assertEqual("priv_role", revoked.name())
Review Comment:
This integration test only asserts that the returned role keeps the same
name, so it would still pass if the grant/revoke request hit the endpoint but
did not actually attach or remove the privilege. Assert the returned securable
object's privileges after each operation so the new behavior is covered
end-to-end.
##########
clients/client-python/gravitino/client/gravitino_metalake.py:
##########
@@ -1116,3 +1134,201 @@ def list_role_names(self) -> list[str]:
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.
+ """
Review Comment:
The privilege grant API can raise `NoSuchMetadataObjectException` and
`IllegalPrivilegeException` through `PERMISSION_ERROR_HANDLER`, but the public
docstring only documents role/metalake failures. This omits two expected error
modes that callers need to handle.
##########
clients/client-python/gravitino/client/gravitino_metalake.py:
##########
@@ -1116,3 +1134,201 @@ def list_role_names(self) -> list[str]:
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.
+ """
+ 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.
+ """
Review Comment:
The privilege revoke API can raise `NoSuchMetadataObjectException` and
`IllegalPrivilegeException` through `PERMISSION_ERROR_HANDLER`, but the public
docstring only documents role/metalake failures. This omits two expected error
modes that callers need to handle.
##########
clients/client-python/gravitino/client/gravitino_client.py:
##########
@@ -573,3 +574,121 @@ def list_role_names(self) -> list[str]:
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.
+ """
+ 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.
+ """
Review Comment:
The `GravitinoClient` privilege revoke docstring omits
`NoSuchMetadataObjectException` and `IllegalPrivilegeException`, both of which
can be raised by the delegated metalake call via `PERMISSION_ERROR_HANDLER`.
Document these so callers of the top-level client see the same error contract.
##########
clients/client-python/gravitino/client/gravitino_client.py:
##########
@@ -573,3 +574,121 @@ def list_role_names(self) -> list[str]:
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.
+ """
Review Comment:
The `GravitinoClient` privilege grant docstring omits
`NoSuchMetadataObjectException` and `IllegalPrivilegeException`, both of which
can be raised by the delegated metalake call via `PERMISSION_ERROR_HANDLER`.
Document these so callers of the top-level client see the same error contract.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]