Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Copy link

Copilot AI Jan 5, 2026

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])

Suggested change
)
)
descope_client.mgmt.user.patch_batch([user_obj])

Copilot uses AI. Check for mistakes.
```

#### Set or Expire User Password

You can set a new active password for a user that they can sign in with.
Expand Down
20 changes: 18 additions & 2 deletions descope/management/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -53,6 +54,7 @@ def __init__(
self.password = password
self.seed = seed
self.status = status
self.consent_expiration = consent_expiration


class CreateUserObj:
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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,
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The consent_expiration parameter was added to _compose_patch_body, but the patch method that calls this function doesn't accept or pass this parameter. This creates an API design inconsistency: users can set consent_expiration via patch_batch with UserObj, but cannot use the individual patch method. The status field follows the pattern of being available in both patch and patch_batch. Consider adding consent_expiration as a parameter to the patch method and passing it through to _compose_patch_body.

Copilot uses AI. Check for mistakes.
) -> dict:
res: dict[str, Any] = {
"loginId": login_id,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
56 changes: 56 additions & 0 deletions tests/management/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading