Skip to content

Commit 1d03ef3

Browse files
authored
Add option to impersonate on management sdk (#364)
+ test related to descope/etc#5584
1 parent 22a582a commit 1d03ef3

File tree

4 files changed

+106
-6
lines changed

4 files changed

+106
-6
lines changed

README.md

+21-6
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,12 @@ These sections show how to use the SDK to perform permission and user management
6666
7. [Query SSO Groups](#query-sso-groups)
6767
8. [Manage Flows](#manage-flows-and-theme)
6868
9. [Manage JWTs](#manage-jwts)
69-
10. [Embedded links](#embedded-links)
70-
11. [Search Audit](#search-audit)
71-
12. [Manage ReBAC Authz](#manage-rebac-authz)
72-
13. [Manage Project](#manage-project)
73-
14. [Manage SSO Applications](#manage-sso-applications)
69+
10. [Impersonate](#impersonate)
70+
12. [Embedded links](#embedded-links)
71+
13. [Search Audit](#search-audit)
72+
14. [Manage ReBAC Authz](#manage-rebac-authz)
73+
15. [Manage Project](#manage-project)
74+
16. [Manage SSO Applications](#manage-sso-applications)
7475

7576
If you wish to run any of our code samples and play with them, check out our [Code Examples](#code-examples) section.
7677

@@ -668,7 +669,7 @@ users_history_resp = descope_client.mgmt.user.history(["user-id-1", "user-id-2"]
668669

669670
#### Set or Expire User Password
670671

671-
You can set a new active password for a user that they can sign in with.
672+
You can set a new active password for a user that they can sign in with.
672673
You can also set a temporary password that the user will be forced to change on the next login.
673674
For a user that already has an active password, you can expire their current password, effectively requiring them to change it on the next login.
674675

@@ -1009,6 +1010,20 @@ updated_jwt = descope_client.mgmt.jwt.update_jwt(
10091010
)
10101011
```
10111012

1013+
### Impersonate
1014+
1015+
You can impersonate to another user
1016+
The impersonator user must have the `impersonation` permission in order for this request to work.
1017+
The response would be a refresh JWT of the impersonated user
1018+
1019+
```python
1020+
refresh_jwt = descope_client.mgmt.jwt.impersonate(
1021+
impersonator_id="<Login ID impersonator>",
1022+
login_id="<Login ID of impersonated person>",
1023+
validate_consent=True
1024+
)
1025+
```
1026+
10121027
# Note 1: The generate code/link functions, work only for test users, will not work for regular users.
10131028

10141029
# Note 2: In case of testing sign-in / sign-up operations with test users, need to make sure to generate the code prior calling the sign-in / sign-up operations.

descope/management/common.py

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class MgmtV1:
7575

7676
# jwt
7777
update_jwt_path = "/v1/mgmt/jwt/update"
78+
impersonate_path = "/v1/mgmt/impersonate"
7879

7980
# permission
8081
permission_create_path = "/v1/mgmt/permission/create"

descope/management/jwt.py

+35
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,38 @@ def update_jwt(self, jwt: str, custom_claims: dict) -> str:
2525
pswd=self._auth.management_key,
2626
)
2727
return response.json().get("jwt", "")
28+
29+
def impersonate(
30+
self, impersonator_id: str, login_id: str, validate_consent: bool
31+
) -> str:
32+
"""
33+
Impersonate to another user
34+
35+
Args:
36+
impersonator_id (str): login id / user id of impersonator, must have "impersonation" permission.
37+
login_id (str): login id of the user whom to which to impersonate to.
38+
validate_consent (bool): Indicate whether to allow impersonation in any case or only if a consent to this operation was granted.
39+
40+
Return value (str): A JWT of the impersonated user
41+
42+
Raise:
43+
AuthException: raised if update failed
44+
"""
45+
if not impersonator_id:
46+
raise AuthException(
47+
400, ERROR_TYPE_INVALID_ARGUMENT, "impersonator_id cannot be empty"
48+
)
49+
if not login_id:
50+
raise AuthException(
51+
400, ERROR_TYPE_INVALID_ARGUMENT, "login_id cannot be empty"
52+
)
53+
response = self._auth.do_post(
54+
MgmtV1.impersonate_path,
55+
{
56+
"loginId": login_id,
57+
"impersonatorId": impersonator_id,
58+
"validateConsent": validate_consent,
59+
},
60+
pswd=self._auth.management_key,
61+
)
62+
return response.json().get("jwt", "")

tests/management/test_jwt.py

+49
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,52 @@ def test_update_jwt(self):
6464
params=None,
6565
timeout=DEFAULT_TIMEOUT_SECONDS,
6666
)
67+
68+
def test_impersonate(self):
69+
client = DescopeClient(
70+
self.dummy_project_id,
71+
self.public_key_dict,
72+
False,
73+
self.dummy_management_key,
74+
)
75+
76+
# Test failed flows
77+
with patch("requests.post") as mock_post:
78+
mock_post.return_value.ok = False
79+
self.assertRaises(
80+
AuthException, client.mgmt.jwt.impersonate, "imp1", "imp2", False
81+
)
82+
83+
self.assertRaises(
84+
AuthException, client.mgmt.jwt.impersonate, "", "imp2", False
85+
)
86+
87+
self.assertRaises(
88+
AuthException, client.mgmt.jwt.impersonate, "imp1", "", False
89+
)
90+
91+
# Test success flow
92+
with patch("requests.post") as mock_post:
93+
network_resp = mock.Mock()
94+
network_resp.ok = True
95+
network_resp.json.return_value = json.loads("""{"jwt": "response"}""")
96+
mock_post.return_value = network_resp
97+
resp = client.mgmt.jwt.impersonate("imp1", "imp2", True)
98+
self.assertEqual(resp, "response")
99+
expected_uri = f"{common.DEFAULT_BASE_URL}{MgmtV1.impersonate_path}"
100+
mock_post.assert_called_with(
101+
expected_uri,
102+
headers={
103+
**common.default_headers,
104+
"Authorization": f"Bearer {self.dummy_project_id}:{self.dummy_management_key}",
105+
},
106+
json={
107+
"loginId": "imp2",
108+
"impersonatorId": "imp1",
109+
"validateConsent": True,
110+
},
111+
allow_redirects=False,
112+
verify=True,
113+
params=None,
114+
timeout=DEFAULT_TIMEOUT_SECONDS,
115+
)

0 commit comments

Comments
 (0)