Skip to content

Commit 2dda1d2

Browse files
authored
Management SDK additions (#324)
1 parent 5654eb2 commit 2dda1d2

File tree

3 files changed

+112
-8
lines changed

3 files changed

+112
-8
lines changed

descope/management/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class MgmtV1:
3232
user_remove_role_path = "/v1/mgmt/user/update/role/remove"
3333
user_set_password_path = "/v1/mgmt/user/password/set"
3434
user_expire_password_path = "/v1/mgmt/user/password/expire"
35+
user_remove_all_passkeys_path = "/v1/mgmt/user/passkeys/delete"
3536
user_add_tenant_path = "/v1/mgmt/user/update/tenant/add"
3637
user_remove_tenant_path = "/v1/mgmt/user/update/tenant/remove"
3738
user_generate_otp_for_test_path = "/v1/mgmt/tests/generate/otp"

descope/management/user.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def create(
8585
Containing the created user information.
8686
8787
Raise:
88-
AuthException: raised if update operation fails
88+
AuthException: raised if create operation fails
8989
"""
9090
role_names = [] if role_names is None else role_names
9191
user_tenants = [] if user_tenants is None else user_tenants
@@ -158,7 +158,7 @@ def create_test_user(
158158
Containing the created test user information.
159159
160160
Raise:
161-
AuthException: raised if update operation fails
161+
AuthException: raised if create operation fails
162162
"""
163163
role_names = [] if role_names is None else role_names
164164
user_tenants = [] if user_tenants is None else user_tenants
@@ -324,7 +324,7 @@ def update(
324324
custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app
325325
326326
Raise:
327-
AuthException: raised if creation operation fails
327+
AuthException: raised if update operation fails
328328
"""
329329
role_names = [] if role_names is None else role_names
330330
user_tenants = [] if user_tenants is None else user_tenants
@@ -362,22 +362,41 @@ def delete(
362362
login_id (str): The login ID of the user to be deleted.
363363
364364
Raise:
365-
AuthException: raised if creation operation fails
365+
AuthException: raised if delete operation fails
366366
"""
367367
self._auth.do_post(
368368
MgmtV1.user_delete_path,
369369
{"loginId": login_id},
370370
pswd=self._auth.management_key,
371371
)
372372

373+
def delete_by_user_id(
374+
self,
375+
user_id: str,
376+
):
377+
"""
378+
Delete an existing user by user ID. IMPORTANT: This action is irreversible. Use carefully.
379+
380+
Args:
381+
user_id (str): The user ID from the user's JWT.
382+
383+
Raise:
384+
AuthException: raised if delete operation fails
385+
"""
386+
self._auth.do_post(
387+
MgmtV1.user_delete_path,
388+
{"userId": user_id},
389+
pswd=self._auth.management_key,
390+
)
391+
373392
def delete_all_test_users(
374393
self,
375394
):
376395
"""
377396
Delete all test users in the project. IMPORTANT: This action is irreversible. Use carefully.
378397
379398
Raise:
380-
AuthException: raised if creation operation fails
399+
AuthException: raised if delete operation fails
381400
"""
382401
self._auth.do_delete(
383402
MgmtV1.user_delete_all_test_users_path,
@@ -446,7 +465,7 @@ def logout_user(
446465
login_id (str): The login ID of the user to be logged out.
447466
448467
Raise:
449-
AuthException: raised if creation operation fails
468+
AuthException: raised if logout operation fails
450469
"""
451470
self._auth.do_post(
452471
MgmtV1.user_logout_path,
@@ -465,7 +484,7 @@ def logout_user_by_user_id(
465484
user_id (str): The login ID of the user to be logged out.
466485
467486
Raise:
468-
AuthException: raised if creation operation fails
487+
AuthException: raised if logout operation fails
469488
"""
470489
self._auth.do_post(
471490
MgmtV1.user_logout_path,
@@ -1069,7 +1088,7 @@ def expire_password(
10691088
Use the `password.send_reset` or `password.replace` methods to reset/replace the password.
10701089
10711090
Args:
1072-
login_id (str): The login ID of the user expire the password to.
1091+
login_id (str): The login ID of the user to expire the password to.
10731092
10741093
Raise:
10751094
AuthException: raised if the operation fails
@@ -1081,6 +1100,28 @@ def expire_password(
10811100
)
10821101
return
10831102

1103+
def remove_all_passkeys(
1104+
self,
1105+
login_id: str,
1106+
) -> None:
1107+
"""
1108+
Removes all registered passkeys (WebAuthn devices) for the user with the given login ID.
1109+
Note: The user might not be able to login anymore if they have no other authentication
1110+
methods or a verified email/phone.
1111+
1112+
Args:
1113+
login_id (str): The login ID of the user to remove passkeys for.
1114+
1115+
Raise:
1116+
AuthException: raised if the operation fails
1117+
"""
1118+
self._auth.do_post(
1119+
MgmtV1.user_remove_all_passkeys_path,
1120+
{"loginId": login_id},
1121+
pswd=self._auth.management_key,
1122+
)
1123+
return
1124+
10841125
def generate_otp_for_test_user(
10851126
self,
10861127
method: DeliveryMethod,

tests/management/test_user.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,35 @@ def test_delete(self):
437437
timeout=DEFAULT_TIMEOUT_SECONDS,
438438
)
439439

440+
def test_delete_by_user_id(self):
441+
# Test failed flows
442+
with patch("requests.post") as mock_post:
443+
mock_post.return_value.ok = False
444+
self.assertRaises(
445+
AuthException,
446+
self.client.mgmt.user.delete_by_user_id,
447+
"valid-id",
448+
)
449+
450+
# Test success flow
451+
with patch("requests.post") as mock_post:
452+
mock_post.return_value.ok = True
453+
self.assertIsNone(self.client.mgmt.user.delete_by_user_id("u1"))
454+
mock_post.assert_called_with(
455+
f"{common.DEFAULT_BASE_URL}{MgmtV1.user_delete_path}",
456+
headers={
457+
**common.default_headers,
458+
"Authorization": f"Bearer {self.dummy_project_id}:{self.dummy_management_key}",
459+
},
460+
params=None,
461+
json={
462+
"userId": "u1",
463+
},
464+
allow_redirects=False,
465+
verify=True,
466+
timeout=DEFAULT_TIMEOUT_SECONDS,
467+
)
468+
440469
def test_logout(self):
441470
# Test failed flows
442471
with patch("requests.post") as mock_post:
@@ -1426,6 +1455,39 @@ def test_user_expire_password(self):
14261455
timeout=DEFAULT_TIMEOUT_SECONDS,
14271456
)
14281457

1458+
def test_user_remove_all_passkeys(self):
1459+
# Test failed flows
1460+
with patch("requests.post") as mock_post:
1461+
mock_post.return_value.ok = False
1462+
self.assertRaises(
1463+
AuthException,
1464+
self.client.mgmt.user.remove_all_passkeys,
1465+
"login-id",
1466+
)
1467+
1468+
# Test success flow
1469+
with patch("requests.post") as mock_post:
1470+
network_resp = mock.Mock()
1471+
network_resp.ok = True
1472+
mock_post.return_value = network_resp
1473+
self.client.mgmt.user.remove_all_passkeys(
1474+
"login-id",
1475+
)
1476+
mock_post.assert_called_with(
1477+
f"{common.DEFAULT_BASE_URL}{MgmtV1.user_remove_all_passkeys_path}",
1478+
headers={
1479+
**common.default_headers,
1480+
"Authorization": f"Bearer {self.dummy_project_id}:{self.dummy_management_key}",
1481+
},
1482+
params=None,
1483+
json={
1484+
"loginId": "login-id",
1485+
},
1486+
allow_redirects=False,
1487+
verify=True,
1488+
timeout=DEFAULT_TIMEOUT_SECONDS,
1489+
)
1490+
14291491
def test_generate_magic_link_for_test_user(self):
14301492
# Test failed flows
14311493
with patch("requests.post") as mock_post:

0 commit comments

Comments
 (0)