From 75fc188cdb4cf92b14797072ff7bef07a9c7cc6b Mon Sep 17 00:00:00 2001 From: Will Munslow Date: Thu, 2 Jul 2026 14:55:22 -0700 Subject: [PATCH 1/3] fix: send raw array body for remove_role_users/remove_role_admins [no-ado] The API parses DELETE /api/2/roles/{role_id}/users and /admins with a strict array parser and rejects the object form with 400 'Expected array in request' (issue #134). Both methods now send a raw JSON array and accept a plain List[int], matching add_role_users. The deprecated RemoveRoleUsersRequest form is still accepted and unwrapped. --- CHANGELOG.md | 15 +++++++ docs/RemoveRoleUsersRequest.md | 4 ++ docs/RolesApi.md | 8 ++-- onelogin/api/roles_api.py | 34 +++++++++------ onelogin/models/remove_role_users_request.py | 5 +++ pyproject.toml | 2 +- test/test_roles_api.py | 45 ++++++++++++++++++++ 7 files changed, 95 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a53ab31b..9163ff50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 4.0.1 (2026-07-02) + +### Bug Fixes + +- **`remove_role_users` / `remove_role_admins`** (`DELETE /api/2/roles/{role_id}/users` + and `.../admins`): both methods were broken since the SDK was generated — they + serialized the body as `{"user_id": [...]}`, but the API parses these endpoints with + a strict array parser and rejects the object form with + 400 `"Expected array in request"` (issue #134). The sibling `add_role_users` had the + correct raw-array body all along; the OpenAPI spec transcribed the wrong shape for + the DELETEs. Both methods now send a raw JSON array (e.g. `[123, 456]`) and accept a + plain `List[int]`, matching `add_role_users`. Passing a `RemoveRoleUsersRequest` is + still accepted for backward compatibility and is unwrapped to the raw array before + sending; the class is now documented as deprecated. + ## 4.0.0 (2026-05-12) ### Breaking Changes diff --git a/docs/RemoveRoleUsersRequest.md b/docs/RemoveRoleUsersRequest.md index a6caf1b7..33bb9508 100644 --- a/docs/RemoveRoleUsersRequest.md +++ b/docs/RemoveRoleUsersRequest.md @@ -1,5 +1,9 @@ # RemoveRoleUsersRequest +**Deprecated.** The remove-role-users and remove-role-admins endpoints expect a raw JSON +array of user ids (e.g. `[123, 456]`), not an object. Pass a plain `List[int]` to +`remove_role_users` / `remove_role_admins` instead. Instances of this class are still +accepted and are unwrapped to the raw array before sending. ## Properties Name | Type | Description | Notes diff --git a/docs/RolesApi.md b/docs/RolesApi.md index eb73cea7..e224459f 100644 --- a/docs/RolesApi.md +++ b/docs/RolesApi.md @@ -889,7 +889,7 @@ with onelogin.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = onelogin.RolesApi(api_client) role_id = 'role_id_example' # str | Set to the id of the role you want to return. - remove_role_users_request = onelogin.RemoveRoleUsersRequest() # RemoveRoleUsersRequest | + remove_role_users_request = [123, 456] # List[int] | user ids to remove try: # Remove Role Admins @@ -903,7 +903,7 @@ with onelogin.ApiClient(configuration) as api_client: Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **role_id** | **str**| Set to the id of the role you want to return. | - **remove_role_users_request** | [**RemoveRoleUsersRequest**](RemoveRoleUsersRequest.md)| | + **remove_role_users_request** | **List[int]**| user ids to remove, e.g. `[123, 456]` — the API expects a raw JSON array. Passing a [RemoveRoleUsersRequest](RemoveRoleUsersRequest.md) is deprecated but still accepted. | ### Return type @@ -961,7 +961,7 @@ with onelogin.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = onelogin.RolesApi(api_client) role_id = 'role_id_example' # str | Set to the id of the role you want to return. - remove_role_users_request = onelogin.RemoveRoleUsersRequest() # RemoveRoleUsersRequest | + remove_role_users_request = [123, 456] # List[int] | user ids to remove try: # Remove Role Users @@ -975,7 +975,7 @@ with onelogin.ApiClient(configuration) as api_client: Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **role_id** | **str**| Set to the id of the role you want to return. | - **remove_role_users_request** | [**RemoveRoleUsersRequest**](RemoveRoleUsersRequest.md)| | + **remove_role_users_request** | **List[int]**| user ids to remove, e.g. `[123, 456]` — the API expects a raw JSON array. Passing a [RemoveRoleUsersRequest](RemoveRoleUsersRequest.md) is deprecated but still accepted. | ### Return type diff --git a/onelogin/api/roles_api.py b/onelogin/api/roles_api.py index 8af922f4..f5f649c2 100644 --- a/onelogin/api/roles_api.py +++ b/onelogin/api/roles_api.py @@ -14,7 +14,7 @@ from pydantic import Field, StrictBool, StrictInt, StrictStr, conlist -from typing import List, Optional +from typing import List, Optional, Union from onelogin.models.create_role201_response_inner import CreateRole201ResponseInner from onelogin.models.get_role_apps200_response_inner import GetRoleApps200ResponseInner @@ -1785,7 +1785,7 @@ def list_roles_with_http_info(self, limit : Annotated[Optional[StrictInt], Field _request_auth=_params.get('_request_auth')) @validate_call - def remove_role_admins(self, role_id : Annotated[StrictStr, Field(..., description="Set to the id of the role you want to return.")], remove_role_users_request : RemoveRoleUsersRequest, **kwargs) -> None: # noqa: E501 + def remove_role_admins(self, role_id : Annotated[StrictStr, Field(..., description="Set to the id of the role you want to return.")], remove_role_users_request : Union[conlist(StrictInt), RemoveRoleUsersRequest], **kwargs) -> None: # noqa: E501 """Remove Role Admins # noqa: E501 Remove Role Admins # noqa: E501 @@ -1797,8 +1797,8 @@ def remove_role_admins(self, role_id : Annotated[StrictStr, Field(..., descripti :param role_id: Set to the id of the role you want to return. (required) :type role_id: str - :param remove_role_users_request: (required) - :type remove_role_users_request: RemoveRoleUsersRequest + :param remove_role_users_request: List of user ids to remove, e.g. [123, 456]. Passing a RemoveRoleUsersRequest is deprecated. (required) + :type remove_role_users_request: List[int] :param async_req: Whether to execute the request asynchronously. :type async_req: bool, optional :param _request_timeout: timeout setting for this request. If one @@ -1816,7 +1816,7 @@ def remove_role_admins(self, role_id : Annotated[StrictStr, Field(..., descripti return self.remove_role_admins_with_http_info(role_id, remove_role_users_request, **kwargs) # noqa: E501 @validate_call - def remove_role_admins_with_http_info(self, role_id : Annotated[StrictStr, Field(..., description="Set to the id of the role you want to return.")], remove_role_users_request : RemoveRoleUsersRequest, **kwargs) -> ApiResponse: # noqa: E501 + def remove_role_admins_with_http_info(self, role_id : Annotated[StrictStr, Field(..., description="Set to the id of the role you want to return.")], remove_role_users_request : Union[conlist(StrictInt), RemoveRoleUsersRequest], **kwargs) -> ApiResponse: # noqa: E501 """Remove Role Admins # noqa: E501 Remove Role Admins # noqa: E501 @@ -1828,8 +1828,8 @@ def remove_role_admins_with_http_info(self, role_id : Annotated[StrictStr, Field :param role_id: Set to the id of the role you want to return. (required) :type role_id: str - :param remove_role_users_request: (required) - :type remove_role_users_request: RemoveRoleUsersRequest + :param remove_role_users_request: List of user ids to remove, e.g. [123, 456]. Passing a RemoveRoleUsersRequest is deprecated. (required) + :type remove_role_users_request: List[int] :param async_req: Whether to execute the request asynchronously. :type async_req: bool, optional :param _preload_content: if False, the ApiResponse.data will @@ -1899,9 +1899,13 @@ def remove_role_admins_with_http_info(self, role_id : Annotated[StrictStr, Field _form_params = [] _files = {} # process the body parameter + # The API expects a raw JSON array of user ids; unwrap the deprecated + # RemoveRoleUsersRequest form so both call styles serialize correctly. _body_params = None if _params['remove_role_users_request'] is not None: _body_params = _params['remove_role_users_request'] + if isinstance(_body_params, RemoveRoleUsersRequest): + _body_params = _body_params.user_id or [] # set the HTTP header `Accept` _header_params['Accept'] = self.api_client.select_header_accept( @@ -1937,7 +1941,7 @@ def remove_role_admins_with_http_info(self, role_id : Annotated[StrictStr, Field _request_auth=_params.get('_request_auth')) @validate_call - def remove_role_users(self, role_id : Annotated[StrictStr, Field(..., description="Set to the id of the role you want to return.")], remove_role_users_request : RemoveRoleUsersRequest, **kwargs) -> None: # noqa: E501 + def remove_role_users(self, role_id : Annotated[StrictStr, Field(..., description="Set to the id of the role you want to return.")], remove_role_users_request : Union[conlist(StrictInt), RemoveRoleUsersRequest], **kwargs) -> None: # noqa: E501 """Remove Role Users # noqa: E501 Remove Role Users # noqa: E501 @@ -1949,8 +1953,8 @@ def remove_role_users(self, role_id : Annotated[StrictStr, Field(..., descriptio :param role_id: Set to the id of the role you want to return. (required) :type role_id: str - :param remove_role_users_request: (required) - :type remove_role_users_request: RemoveRoleUsersRequest + :param remove_role_users_request: List of user ids to remove, e.g. [123, 456]. Passing a RemoveRoleUsersRequest is deprecated. (required) + :type remove_role_users_request: List[int] :param async_req: Whether to execute the request asynchronously. :type async_req: bool, optional :param _request_timeout: timeout setting for this request. If one @@ -1968,7 +1972,7 @@ def remove_role_users(self, role_id : Annotated[StrictStr, Field(..., descriptio return self.remove_role_users_with_http_info(role_id, remove_role_users_request, **kwargs) # noqa: E501 @validate_call - def remove_role_users_with_http_info(self, role_id : Annotated[StrictStr, Field(..., description="Set to the id of the role you want to return.")], remove_role_users_request : RemoveRoleUsersRequest, **kwargs) -> ApiResponse: # noqa: E501 + def remove_role_users_with_http_info(self, role_id : Annotated[StrictStr, Field(..., description="Set to the id of the role you want to return.")], remove_role_users_request : Union[conlist(StrictInt), RemoveRoleUsersRequest], **kwargs) -> ApiResponse: # noqa: E501 """Remove Role Users # noqa: E501 Remove Role Users # noqa: E501 @@ -1980,8 +1984,8 @@ def remove_role_users_with_http_info(self, role_id : Annotated[StrictStr, Field( :param role_id: Set to the id of the role you want to return. (required) :type role_id: str - :param remove_role_users_request: (required) - :type remove_role_users_request: RemoveRoleUsersRequest + :param remove_role_users_request: List of user ids to remove, e.g. [123, 456]. Passing a RemoveRoleUsersRequest is deprecated. (required) + :type remove_role_users_request: List[int] :param async_req: Whether to execute the request asynchronously. :type async_req: bool, optional :param _preload_content: if False, the ApiResponse.data will @@ -2051,9 +2055,13 @@ def remove_role_users_with_http_info(self, role_id : Annotated[StrictStr, Field( _form_params = [] _files = {} # process the body parameter + # The API expects a raw JSON array of user ids; unwrap the deprecated + # RemoveRoleUsersRequest form so both call styles serialize correctly. _body_params = None if _params['remove_role_users_request'] is not None: _body_params = _params['remove_role_users_request'] + if isinstance(_body_params, RemoveRoleUsersRequest): + _body_params = _body_params.user_id or [] # set the HTTP header `Accept` _header_params['Accept'] = self.api_client.select_header_accept( diff --git a/onelogin/models/remove_role_users_request.py b/onelogin/models/remove_role_users_request.py index 6028a7b3..bf76e332 100644 --- a/onelogin/models/remove_role_users_request.py +++ b/onelogin/models/remove_role_users_request.py @@ -19,6 +19,11 @@ class RemoveRoleUsersRequest(BaseModel): """ RemoveRoleUsersRequest + + Deprecated: the remove_role_users / remove_role_admins endpoints expect a raw + JSON array of user ids, not an object. Pass a plain list (e.g. [123, 456]) to + those methods instead. This class is kept for backward compatibility; the + RolesApi methods unwrap it to the raw array before sending. """ user_id: Optional[conlist(StrictInt)] = None __properties = ["user_id"] diff --git a/pyproject.toml b/pyproject.toml index 0bc9210c..004fb09a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "onelogin" -version = "4.0.0" +version = "4.0.1" description = "OneLogin API Python SDK" authors = [ {name = "OneLogin", email = "developers@onelogin.com"} diff --git a/test/test_roles_api.py b/test/test_roles_api.py index 07416ae5..c23541cb 100644 --- a/test/test_roles_api.py +++ b/test/test_roles_api.py @@ -160,6 +160,51 @@ def test_create_role_response_type_is_not_list(self): result = self.api.create_role() self.assertNotIsInstance(result, list) + def _make_empty_204_response(self): + mock_resp = MagicMock() + mock_resp.status = 204 + mock_resp.reason = 'No Content' + mock_resp.data = b'' + mock_resp.headers = {} + return mock_resp + + def _sent_body(self, mock_request): + """Return the JSON-decoded body the SDK actually sent.""" + return json.loads(mock_request.call_args.kwargs['body']) + + def test_remove_role_users_sends_raw_array(self): + """remove_role_users must send a raw JSON array of user ids. + + The API rejects the object form with 400 'Expected array in request' + (GitHub issue #134): DELETE /api/2/roles/{role_id}/users parses its + body with a strict array parser. + """ + with patch.object( + self.api.api_client.rest_client.pool_manager, 'request', + return_value=self._make_empty_204_response() + ) as mock_request: + self.api.remove_role_users('789', [123, 456]) + self.assertEqual(self._sent_body(mock_request), [123, 456]) + + def test_remove_role_users_unwraps_legacy_request_model(self): + """The deprecated RemoveRoleUsersRequest form must still serialize to a raw array.""" + legacy = onelogin.RemoveRoleUsersRequest(user_id=[123, 456]) + with patch.object( + self.api.api_client.rest_client.pool_manager, 'request', + return_value=self._make_empty_204_response() + ) as mock_request: + self.api.remove_role_users('789', legacy) + self.assertEqual(self._sent_body(mock_request), [123, 456]) + + def test_remove_role_admins_sends_raw_array(self): + """remove_role_admins shares the same raw-array body requirement.""" + with patch.object( + self.api.api_client.rest_client.pool_manager, 'request', + return_value=self._make_empty_204_response() + ) as mock_request: + self.api.remove_role_admins('789', [42]) + self.assertEqual(self._sent_body(mock_request), [42]) + if __name__ == '__main__': unittest.main() From 6a4e54f39e94f9725037ba72d78616caf19b2219 Mon Sep 17 00:00:00 2001 From: Will Munslow Date: Thu, 2 Jul 2026 15:29:46 -0700 Subject: [PATCH 2/3] fix: sync __version__ constant with pyproject 4.0.1 bump [no-ado] --- onelogin/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onelogin/__init__.py b/onelogin/__init__.py index ea9a27e1..38f980dd 100644 --- a/onelogin/__init__.py +++ b/onelogin/__init__.py @@ -9,7 +9,7 @@ """ -__version__ = "4.0.0" +__version__ = "4.0.1" # import apis into sdk package from onelogin.api.api_auth_claims_api import APIAuthClaimsApi From 3a77aee8465dda1351b679803e56fb4ab57e4c7f Mon Sep 17 00:00:00 2001 From: Will Munslow Date: Thu, 2 Jul 2026 15:36:17 -0700 Subject: [PATCH 3/3] test: cover legacy RemoveRoleUsersRequest unwrap on remove_role_admins [no-ado] --- test/test_roles_api.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/test_roles_api.py b/test/test_roles_api.py index c23541cb..1001e1c0 100644 --- a/test/test_roles_api.py +++ b/test/test_roles_api.py @@ -205,6 +205,20 @@ def test_remove_role_admins_sends_raw_array(self): self.api.remove_role_admins('789', [42]) self.assertEqual(self._sent_body(mock_request), [42]) + def test_remove_role_admins_unwraps_legacy_request_model(self): + """The deprecated RemoveRoleUsersRequest form must also unwrap for admins. + + The unwrap logic is duplicated per method, not shared, so admins needs + its own regression coverage. + """ + legacy = onelogin.RemoveRoleUsersRequest(user_id=[42, 43]) + with patch.object( + self.api.api_client.rest_client.pool_manager, 'request', + return_value=self._make_empty_204_response() + ) as mock_request: + self.api.remove_role_admins('789', legacy) + self.assertEqual(self._sent_body(mock_request), [42, 43]) + if __name__ == '__main__': unittest.main()