-
Notifications
You must be signed in to change notification settings - Fork 9
feat(user): add consent expiration field to user apis #738
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -780,6 +780,36 @@ users_history_resp = descope_client.mgmt.user.history(["user-id-1", "user-id-2"] | |
| # Do something | ||
| ``` | ||
|
|
||
| #### User Impersonation Consent | ||
|
|
||
| When using the User Impersonation feature with consent validation, user objects returned from `load()`, `load_by_user_id()`, and `search_all()` methods will include a `consentExpiration` field (Unix timestamp in seconds). This field indicates when the user's consent for impersonation expires, allowing you to: | ||
|
|
||
| - Identify which users have granted impersonation consent | ||
| - Filter users by consent status | ||
| - Track consent expiration times | ||
|
|
||
| ```Python | ||
| # Load a user and check consent expiration | ||
| user_resp = descope_client.mgmt.user.load("[email protected]") | ||
| user = user_resp["user"] | ||
| consent_expiration = user.get("consentExpiration") # Unix timestamp or None | ||
|
|
||
| if consent_expiration: | ||
| print(f"User has granted consent until: {consent_expiration}") | ||
|
|
||
| # Search users and filter by consent status | ||
| users_resp = descope_client.mgmt.user.search_all() | ||
| users_with_consent = [u for u in users_resp["users"] if u.get("consentExpiration")] | ||
|
|
||
| # The consentExpiration field is also available in UserObj for batch operations | ||
| from descope import UserObj | ||
| user_obj = UserObj( | ||
| login_id="[email protected]", | ||
| email="[email protected]", | ||
| consent_expiration=1735689600, # Optional Unix timestamp | ||
| ) | ||
| ``` | ||
|
|
||
| #### Set or Expire User Password | ||
|
|
||
| You can set a new active password for a user that they can sign in with. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,7 @@ def __init__( | |
| password: Optional[UserPassword] = None, | ||
| seed: Optional[str] = None, | ||
| status: Optional[str] = None, | ||
| consent_expiration: Optional[int] = None, | ||
| ): | ||
| self.login_id = login_id | ||
| self.email = email | ||
|
|
@@ -53,6 +54,7 @@ def __init__( | |
| self.password = password | ||
| self.seed = seed | ||
| self.status = status | ||
| self.consent_expiration = consent_expiration | ||
|
|
||
|
|
||
| class CreateUserObj: | ||
|
|
@@ -1082,7 +1084,12 @@ def update_email( | |
| """ | ||
| response = self._http.post( | ||
| MgmtV1.user_update_email_path, | ||
| body={"loginId": login_id, "email": email, "verified": verified, "failOnConflict": fail_on_conflict}, | ||
| body={ | ||
| "loginId": login_id, | ||
| "email": email, | ||
| "verified": verified, | ||
| "failOnConflict": fail_on_conflict, | ||
| }, | ||
| ) | ||
| return response.json() | ||
|
|
||
|
|
@@ -1112,7 +1119,12 @@ def update_phone( | |
| """ | ||
| response = self._http.post( | ||
| MgmtV1.user_update_phone_path, | ||
| body={"loginId": login_id, "phone": phone, "verified": verified, "failOnConflict": fail_on_conflict}, | ||
| body={ | ||
| "loginId": login_id, | ||
| "phone": phone, | ||
| "verified": verified, | ||
| "failOnConflict": fail_on_conflict, | ||
| }, | ||
| ) | ||
| return response.json() | ||
|
|
||
|
|
@@ -2026,6 +2038,7 @@ def _compose_patch_body( | |
| sso_app_ids: Optional[List[str]], | ||
| status: Optional[str], | ||
| test: bool = False, | ||
| consent_expiration: Optional[int] = None, | ||
|
||
| ) -> dict: | ||
| res: dict[str, Any] = { | ||
| "loginId": login_id, | ||
|
|
@@ -2058,6 +2071,8 @@ def _compose_patch_body( | |
| res["ssoAppIds"] = sso_app_ids | ||
| if status is not None: | ||
| res["status"] = status | ||
| if consent_expiration is not None: | ||
| res["consentExpiration"] = consent_expiration | ||
| if test: | ||
| res["test"] = test | ||
| return res | ||
|
|
@@ -2086,6 +2101,7 @@ def _compose_patch_batch_body( | |
| sso_app_ids=user.sso_app_ids, | ||
| status=user.status, | ||
| test=test, | ||
| consent_expiration=user.consent_expiration, | ||
| ) | ||
| users_body.append(user_body) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -803,6 +803,62 @@ def test_patch_batch(self): | |
| json_payload = call_args[1]["json"] | ||
| self.assertTrue(json_payload["users"][0]["test"]) | ||
|
|
||
| def test_patch_batch_with_consent_expiration(self): | ||
| # Test batch patch with consent_expiration field | ||
| users = [ | ||
| UserObj( | ||
| login_id="user1", email="[email protected]", consent_expiration=1735689600 | ||
| ), | ||
| UserObj( | ||
| login_id="user2", display_name="User Two", consent_expiration=1767225600 | ||
| ), | ||
| UserObj(login_id="user3", phone="+123456789"), # No consent_expiration | ||
| ] | ||
|
|
||
| with patch("requests.patch") as mock_patch: | ||
| network_resp = mock.Mock() | ||
| network_resp.ok = True | ||
| network_resp.json.return_value = json.loads( | ||
| """{"patchedUsers": [{"id": "u1"}, {"id": "u2"}, {"id": "u3"}], "failedUsers": []}""" | ||
| ) | ||
| mock_patch.return_value = network_resp | ||
|
|
||
| resp = self.client.mgmt.user.patch_batch(users) | ||
|
|
||
| self.assertEqual(len(resp["patchedUsers"]), 3) | ||
| self.assertEqual(len(resp["failedUsers"]), 0) | ||
|
|
||
| mock_patch.assert_called_with( | ||
| f"{common.DEFAULT_BASE_URL}{MgmtV1.user_patch_batch_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={ | ||
| "users": [ | ||
| { | ||
| "loginId": "user1", | ||
| "email": "[email protected]", | ||
| "consentExpiration": 1735689600, | ||
| }, | ||
| { | ||
| "loginId": "user2", | ||
| "displayName": "User Two", | ||
| "consentExpiration": 1767225600, | ||
| }, | ||
| { | ||
| "loginId": "user3", | ||
| "phone": "+123456789", | ||
| }, | ||
| ] | ||
| }, | ||
| allow_redirects=False, | ||
| verify=True, | ||
| timeout=DEFAULT_TIMEOUT_SECONDS, | ||
| ) | ||
|
|
||
| def test_delete(self): | ||
| # Test failed flows | ||
| with patch("requests.post") as mock_post: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The UserObj example creates an object but doesn't show how to use it. Consider completing the example by adding a line that shows how to use this object with patch_batch, such as: descope_client.mgmt.user.patch_batch([user_obj])