Skip to content

Commit a0e4b85

Browse files
committed
feat(user): add consent expiration field to user apis
1 parent 326c246 commit a0e4b85

3 files changed

Lines changed: 104 additions & 2 deletions

File tree

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,36 @@ users_history_resp = descope_client.mgmt.user.history(["user-id-1", "user-id-2"]
780780
# Do something
781781
```
782782

783+
#### User Impersonation Consent
784+
785+
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:
786+
787+
- Identify which users have granted impersonation consent
788+
- Filter users by consent status
789+
- Track consent expiration times
790+
791+
```Python
792+
# Load a user and check consent expiration
793+
user_resp = descope_client.mgmt.user.load("desmond@descope.com")
794+
user = user_resp["user"]
795+
consent_expiration = user.get("consentExpiration") # Unix timestamp or None
796+
797+
if consent_expiration:
798+
print(f"User has granted consent until: {consent_expiration}")
799+
800+
# Search users and filter by consent status
801+
users_resp = descope_client.mgmt.user.search_all()
802+
users_with_consent = [u for u in users_resp["users"] if u.get("consentExpiration")]
803+
804+
# The consentExpiration field is also available in UserObj for batch operations
805+
from descope import UserObj
806+
user_obj = UserObj(
807+
login_id="desmond@descope.com",
808+
email="desmond@descope.com",
809+
consent_expiration=1735689600, # Optional Unix timestamp
810+
)
811+
```
812+
783813
#### Set or Expire User Password
784814

785815
You can set a new active password for a user that they can sign in with.

descope/management/user.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def __init__(
3434
password: Optional[UserPassword] = None,
3535
seed: Optional[str] = None,
3636
status: Optional[str] = None,
37+
consent_expiration: Optional[int] = None,
3738
):
3839
self.login_id = login_id
3940
self.email = email
@@ -53,6 +54,7 @@ def __init__(
5354
self.password = password
5455
self.seed = seed
5556
self.status = status
57+
self.consent_expiration = consent_expiration
5658

5759

5860
class CreateUserObj:
@@ -1082,7 +1084,12 @@ def update_email(
10821084
"""
10831085
response = self._http.post(
10841086
MgmtV1.user_update_email_path,
1085-
body={"loginId": login_id, "email": email, "verified": verified, "failOnConflict": fail_on_conflict},
1087+
body={
1088+
"loginId": login_id,
1089+
"email": email,
1090+
"verified": verified,
1091+
"failOnConflict": fail_on_conflict,
1092+
},
10861093
)
10871094
return response.json()
10881095

@@ -1112,7 +1119,12 @@ def update_phone(
11121119
"""
11131120
response = self._http.post(
11141121
MgmtV1.user_update_phone_path,
1115-
body={"loginId": login_id, "phone": phone, "verified": verified, "failOnConflict": fail_on_conflict},
1122+
body={
1123+
"loginId": login_id,
1124+
"phone": phone,
1125+
"verified": verified,
1126+
"failOnConflict": fail_on_conflict,
1127+
},
11161128
)
11171129
return response.json()
11181130

@@ -2026,6 +2038,7 @@ def _compose_patch_body(
20262038
sso_app_ids: Optional[List[str]],
20272039
status: Optional[str],
20282040
test: bool = False,
2041+
consent_expiration: Optional[int] = None,
20292042
) -> dict:
20302043
res: dict[str, Any] = {
20312044
"loginId": login_id,
@@ -2058,6 +2071,8 @@ def _compose_patch_body(
20582071
res["ssoAppIds"] = sso_app_ids
20592072
if status is not None:
20602073
res["status"] = status
2074+
if consent_expiration is not None:
2075+
res["consentExpiration"] = consent_expiration
20612076
if test:
20622077
res["test"] = test
20632078
return res
@@ -2086,6 +2101,7 @@ def _compose_patch_batch_body(
20862101
sso_app_ids=user.sso_app_ids,
20872102
status=user.status,
20882103
test=test,
2104+
consent_expiration=user.consent_expiration,
20892105
)
20902106
users_body.append(user_body)
20912107

tests/management/test_user.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,62 @@ def test_patch_batch(self):
803803
json_payload = call_args[1]["json"]
804804
self.assertTrue(json_payload["users"][0]["test"])
805805

806+
def test_patch_batch_with_consent_expiration(self):
807+
# Test batch patch with consent_expiration field
808+
users = [
809+
UserObj(
810+
login_id="user1", email="user1@test.com", consent_expiration=1735689600
811+
),
812+
UserObj(
813+
login_id="user2", display_name="User Two", consent_expiration=1767225600
814+
),
815+
UserObj(login_id="user3", phone="+123456789"), # No consent_expiration
816+
]
817+
818+
with patch("requests.patch") as mock_patch:
819+
network_resp = mock.Mock()
820+
network_resp.ok = True
821+
network_resp.json.return_value = json.loads(
822+
"""{"patchedUsers": [{"id": "u1"}, {"id": "u2"}, {"id": "u3"}], "failedUsers": []}"""
823+
)
824+
mock_patch.return_value = network_resp
825+
826+
resp = self.client.mgmt.user.patch_batch(users)
827+
828+
self.assertEqual(len(resp["patchedUsers"]), 3)
829+
self.assertEqual(len(resp["failedUsers"]), 0)
830+
831+
mock_patch.assert_called_with(
832+
f"{common.DEFAULT_BASE_URL}{MgmtV1.user_patch_batch_path}",
833+
headers={
834+
**common.default_headers,
835+
"Authorization": f"Bearer {self.dummy_project_id}:{self.dummy_management_key}",
836+
"x-descope-project-id": self.dummy_project_id,
837+
},
838+
params=None,
839+
json={
840+
"users": [
841+
{
842+
"loginId": "user1",
843+
"email": "user1@test.com",
844+
"consentExpiration": 1735689600,
845+
},
846+
{
847+
"loginId": "user2",
848+
"displayName": "User Two",
849+
"consentExpiration": 1767225600,
850+
},
851+
{
852+
"loginId": "user3",
853+
"phone": "+123456789",
854+
},
855+
]
856+
},
857+
allow_redirects=False,
858+
verify=True,
859+
timeout=DEFAULT_TIMEOUT_SECONDS,
860+
)
861+
806862
def test_delete(self):
807863
# Test failed flows
808864
with patch("requests.post") as mock_post:

0 commit comments

Comments
 (0)