Skip to content

Commit bfcb192

Browse files
authored
feat: support status field on user create, document invite caveat (#1665)
* feat: support status field on user create, document invite caveat Adds an optional `status` parameter to `mgmt.user.create()` (sync and async), matching the status field already supported by the create-user API and by `patch()`. Also documents in the README that `invite` only controls whether an invitation message is sent, not the resulting user status. * style: run ruff format * refactor: centralize user status validation in UserBase create(), patch(), and patch_batch() (sync + async) each repeated the same status allowlist and error message. Moved it into a single UserBase._validate_status() so adding a new status only means editing one place.
1 parent 65a2791 commit bfcb192

5 files changed

Lines changed: 38 additions & 45 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,11 @@ descope_client.mgmt.user.invite(
700700
invite_url="invite.me"
701701
)
702702

703+
# NOTE: `invite` only controls whether an invitation message is sent - it does NOT
704+
# change the user's status. A newly invited user starts out in the "invited" status,
705+
# not "enabled". If your use case requires the user to be active immediately, set
706+
# `status="enabled"` explicitly via `create`/`patch`, or call `activate()` afterwards.
707+
703708
# Batch invite
704709
descope_client.mgmt.user.invite_batch(
705710
users=[

descope/management/_user_base.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
)
1010
from descope.management.user_pwd import UserPassword
1111

12+
VALID_USER_STATUSES = ["enabled", "disabled", "invited", "expired"]
13+
1214

1315
class UserObj:
1416
def __init__(
@@ -71,6 +73,16 @@ def __init__(
7173

7274

7375
class UserBase:
76+
@staticmethod
77+
def _validate_status(status: Optional[str], login_id: Optional[str] = None) -> None:
78+
if status is not None and status not in VALID_USER_STATUSES:
79+
suffix = f" for user {login_id}" if login_id is not None else ""
80+
raise AuthException(
81+
400,
82+
ERROR_TYPE_INVALID_ARGUMENT,
83+
f"Invalid status value: {status}{suffix}. Must be one of: {', '.join(VALID_USER_STATUSES)}",
84+
)
85+
7486
@staticmethod
7587
def _compose_create_body(
7688
login_id: str,
@@ -95,6 +107,7 @@ def _compose_create_body(
95107
sso_app_ids: Optional[List[str]] = None,
96108
template_id: str = "",
97109
locale: Optional[str] = None,
110+
status: Optional[str] = None,
98111
) -> dict:
99112
body = UserBase._compose_update_body(
100113
login_id=login_id,
@@ -127,6 +140,8 @@ def _compose_create_body(
127140
body["templateId"] = template_id
128141
if locale is not None:
129142
body["locale"] = locale
143+
if status is not None:
144+
body["status"] = status
130145
return body
131146

132147
@staticmethod

descope/management/user.py

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def create(
3232
invite_url: Optional[str] = None,
3333
additional_login_ids: Optional[List[str]] = None,
3434
sso_app_ids: Optional[List[str]] = None,
35+
status: Optional[str] = None,
3536
) -> dict:
3637
"""
3738
Create a new user. Users can have any number of optional fields, including email, phone number and authorization.
@@ -48,6 +49,7 @@ def create(
4849
picture (str): Optional url for user picture
4950
custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app
5051
sso_app_ids (List[str]): Optional, list of SSO applications IDs to be associated with the user.
52+
status (str): Optional status field. Can be one of: "enabled", "disabled", "invited", "expired".
5153
5254
Return value (dict):
5355
Return dict in the format
@@ -57,6 +59,7 @@ def create(
5759
Raise:
5860
AuthException: raised if create operation fails
5961
"""
62+
UserBase._validate_status(status)
6063
role_names = [] if role_names is None else role_names
6164
user_tenants = [] if user_tenants is None else user_tenants
6265

@@ -83,6 +86,7 @@ def create(
8386
None,
8487
additional_login_ids,
8588
sso_app_ids,
89+
status=status,
8690
),
8791
)
8892
return response.json()
@@ -389,17 +393,7 @@ def patch(
389393
Raise:
390394
AuthException: raised if patch operation fails
391395
"""
392-
if status is not None and status not in [
393-
"enabled",
394-
"disabled",
395-
"invited",
396-
"expired",
397-
]:
398-
raise AuthException(
399-
400,
400-
ERROR_TYPE_INVALID_ARGUMENT,
401-
f"Invalid status value: {status}. Must be one of: enabled, disabled, invited, expired",
402-
)
396+
UserBase._validate_status(status)
403397
response = self._http.patch(
404398
MgmtV1.user_patch_path,
405399
body=UserBase._compose_patch_body(
@@ -445,19 +439,8 @@ def patch_batch(
445439
Raise:
446440
AuthException: raised if patch batch operation fails
447441
"""
448-
# Validate status fields for all users
449442
for user in users:
450-
if user.status is not None and user.status not in [
451-
"enabled",
452-
"disabled",
453-
"invited",
454-
"expired",
455-
]:
456-
raise AuthException(
457-
400,
458-
ERROR_TYPE_INVALID_ARGUMENT,
459-
f"Invalid status value: {user.status} for user {user.login_id}. Must be one of: enabled, disabled, invited, expired",
460-
)
443+
UserBase._validate_status(user.status, user.login_id)
461444

462445
response = self._http.patch(
463446
MgmtV1.user_patch_batch_path,

descope/management/user_async.py

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ async def create(
3636
invite_url: Optional[str] = None,
3737
additional_login_ids: Optional[List[str]] = None,
3838
sso_app_ids: Optional[List[str]] = None,
39+
status: Optional[str] = None,
3940
) -> dict:
4041
"""
4142
Create a new user. Users can have any number of optional fields, including email, phone number and authorization.
@@ -52,6 +53,7 @@ async def create(
5253
picture (str): Optional url for user picture
5354
custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app
5455
sso_app_ids (List[str]): Optional, list of SSO applications IDs to be associated with the user.
56+
status (str): Optional status field. Can be one of: "enabled", "disabled", "invited", "expired".
5557
5658
Return value (dict):
5759
Return dict in the format
@@ -61,6 +63,7 @@ async def create(
6163
Raise:
6264
AuthException: raised if create operation fails
6365
"""
66+
UserBase._validate_status(status)
6467
role_names = [] if role_names is None else role_names
6568
user_tenants = [] if user_tenants is None else user_tenants
6669

@@ -87,6 +90,7 @@ async def create(
8790
None,
8891
additional_login_ids,
8992
sso_app_ids,
93+
status=status,
9094
),
9195
)
9296
return response.json()
@@ -393,17 +397,7 @@ async def patch(
393397
Raise:
394398
AuthException: raised if patch operation fails
395399
"""
396-
if status is not None and status not in [
397-
"enabled",
398-
"disabled",
399-
"invited",
400-
"expired",
401-
]:
402-
raise AuthException(
403-
400,
404-
ERROR_TYPE_INVALID_ARGUMENT,
405-
f"Invalid status value: {status}. Must be one of: enabled, disabled, invited, expired",
406-
)
400+
UserBase._validate_status(status)
407401
response = await self._http.patch(
408402
MgmtV1.user_patch_path,
409403
body=UserBase._compose_patch_body(
@@ -451,17 +445,7 @@ async def patch_batch(
451445
"""
452446
# Validate status fields for all users
453447
for user in users:
454-
if user.status is not None and user.status not in [
455-
"enabled",
456-
"disabled",
457-
"invited",
458-
"expired",
459-
]:
460-
raise AuthException(
461-
400,
462-
ERROR_TYPE_INVALID_ARGUMENT,
463-
f"Invalid status value: {user.status} for user {user.login_id}. Must be one of: enabled, disabled, invited, expired",
464-
)
448+
UserBase._validate_status(user.status, user.login_id)
465449

466450
response = await self._http.patch(
467451
MgmtV1.user_patch_batch_path,

tests/management/test_user.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ async def test_create(self, client_factory):
2525
with pytest.raises(AuthException):
2626
await client.invoke(client.mgmt.user.create("valid-id"))
2727

28+
with pytest.raises(AuthException) as exc_info:
29+
await client.invoke(client.mgmt.user.create("valid-id", status="invalid_status"))
30+
assert "Invalid status value: invalid_status" in str(exc_info.value)
31+
2832
# Test success flow
2933
with client.mock_mgmt_post(make_response({"user": {"id": "u1"}})) as mock_post:
3034
resp = await client.invoke(
@@ -40,6 +44,7 @@ async def test_create(self, client_factory):
4044
custom_attributes={"ak": "av"},
4145
additional_login_ids=["id-1", "id-2"],
4246
sso_app_ids=["app1", "app2"],
47+
status="disabled",
4348
)
4449
)
4550
user = resp["user"]
@@ -70,6 +75,7 @@ async def test_create(self, client_factory):
7075
"invite": False,
7176
"additionalLoginIds": ["id-1", "id-2"],
7277
"ssoAppIDs": ["app1", "app2"],
78+
"status": "disabled",
7379
},
7480
follow_redirects=False,
7581
)

0 commit comments

Comments
 (0)