diff --git a/README.md b/README.md index 917b59da5..79c581366 100644 --- a/README.md +++ b/README.md @@ -706,6 +706,12 @@ descope_client.mgmt.user.logout_user("") # Logout user from all devices by user ID descope_client.mgmt.user.logout_user_by_user_id("") +# Load users by their user id +users_resp = descope_client.mgmt.user.load_users(user_ids=[""]) +users = users_resp["users"] + for user in users: + # Do something + # Search all users, optionally according to tenant and/or role filter # results can be paginated using the limit and page parameters, as well as by time with the from_created_time, to_created_time, from_modified_time, and to_modified_time users_resp = descope_client.mgmt.user.search_all(tenant_ids=["my-tenant-id"]) diff --git a/descope/management/common.py b/descope/management/common.py index ebdc45dc9..151bef478 100644 --- a/descope/management/common.py +++ b/descope/management/common.py @@ -87,6 +87,7 @@ class MgmtV1: user_logout_path = "/v1/mgmt/user/logout" user_delete_all_test_users_path = "/v1/mgmt/user/test/delete/all" user_load_path = "/v1/mgmt/user" + users_load_path = "/v1/mgmt/users/load" users_search_path = "/v2/mgmt/user/search" test_users_search_path = "/v2/mgmt/user/search/test" user_get_provider_token = "/v1/mgmt/user/provider/token" diff --git a/descope/management/user.py b/descope/management/user.py index 24cfd6c2b..a49179a5b 100644 --- a/descope/management/user.py +++ b/descope/management/user.py @@ -653,6 +653,46 @@ def logout_user_by_user_id( body={"userId": user_id}, ) + def load_users( + self, + user_ids: List[str], + include_invalid_users: Optional[bool] = None, + ) -> dict: + """ + Load users by their user IDs. + + Args: + user_ids (List[str]): Optional list of user IDs to filter by + include_invalid_users (bool): Optional flag to include invalid users in the response + + Return value (dict): + Return dict in the format + {"users": []} + "users" contains a list of all of the found users and their information + + Raise: + AuthException: raised if search operation fails + """ + if user_ids is None or len(user_ids) == 0: + raise AuthException( + 400, + ERROR_TYPE_INVALID_ARGUMENT, + "At least one user id needs to be supplied", + ) + + body: dict[str, Union[List[str], bool]] = { + "userIds": user_ids, + } + + if include_invalid_users is not None: + body["includeInvalidUsers"] = include_invalid_users + + response = self._http.post( + MgmtV1.users_load_path, + body=body, + ) + return response.json() + def search_all( self, tenant_ids: Optional[List[str]] = None, diff --git a/tests/management/test_user.py b/tests/management/test_user.py index 0af438ac6..e8ff7aa00 100644 --- a/tests/management/test_user.py +++ b/tests/management/test_user.py @@ -1013,6 +1013,55 @@ def test_load_by_user_id(self): timeout=DEFAULT_TIMEOUT_SECONDS, ) + def test_load_users(self): + # Test failed flows + with patch("requests.post") as mock_post: + mock_post.return_value.ok = False + self.assertRaises( + AuthException, + self.client.mgmt.user.load_users, + [""], + ) + + with patch("requests.post") as mock_post: + mock_post.return_value.ok = True + self.assertRaises( + AuthException, self.client.mgmt.user.load_users, None, False + ) + + # Test success flow + with patch("requests.post") as mock_post: + network_resp = mock.Mock() + network_resp.ok = True + network_resp.json.return_value = json.loads( + """{"users": [{"id": "u1"}, {"id": "u2"}]}""" + ) + mock_post.return_value = network_resp + resp = self.client.mgmt.user.load_users( + ["uid"], + include_invalid_users=True, + ) + users = resp["users"] + self.assertEqual(len(users), 2) + self.assertEqual(users[0]["id"], "u1") + self.assertEqual(users[1]["id"], "u2") + mock_post.assert_called_with( + f"{common.DEFAULT_BASE_URL}{MgmtV1.users_load_path}", + headers={ + **common.default_headers, + "Authorization": f"Bearer {self.dummy_project_id}:{self.dummy_management_key}", + "x-descope-project-id": self.dummy_project_id, + }, + params=None, + json={ + "userIds": ["uid"], + "includeInvalidUsers": True, + }, + allow_redirects=False, + verify=True, + timeout=DEFAULT_TIMEOUT_SECONDS, + ) + def test_search_all(self): # Test failed flows with patch("requests.post") as mock_post: diff --git a/tests/test_descope_client.py b/tests/test_descope_client.py index d63ab83d4..c91ac87f4 100644 --- a/tests/test_descope_client.py +++ b/tests/test_descope_client.py @@ -1076,7 +1076,7 @@ def test_base_url_none(self): public_key=self.public_key_dict, ) - expected_base_url = common.DEFAULT_BASE_URL + expected_base_url = common.DEFAULT_BASE_URL self.assertEqual(client._auth.http_client.base_url, expected_base_url) self.assertEqual(client._mgmt._http.base_url, expected_base_url)