Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/RemoveRoleUsersRequest.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 4 additions & 4 deletions docs/RolesApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion onelogin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 21 additions & 13 deletions onelogin/api/roles_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down
5 changes: 5 additions & 0 deletions onelogin/models/remove_role_users_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "onelogin"
version = "4.0.0"
version = "4.0.1"
description = "OneLogin API Python SDK"
Comment thread
Subterrane marked this conversation as resolved.
authors = [
{name = "OneLogin", email = "developers@onelogin.com"}
Expand Down
59 changes: 59 additions & 0 deletions test/test_roles_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,65 @@ 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])

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()
Loading