Skip to content
Merged
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,11 @@ descope_client.mgmt.user.invite(
invite_url="invite.me"
)

# NOTE: `invite` only controls whether an invitation message is sent - it does NOT
# change the user's status. A newly invited user starts out in the "invited" status,
# not "enabled". If your use case requires the user to be active immediately, set
# `status="enabled"` explicitly via `create`/`patch`, or call `activate()` afterwards.
Comment thread
ruvenzx marked this conversation as resolved.

# Batch invite
descope_client.mgmt.user.invite_batch(
users=[
Expand Down
15 changes: 15 additions & 0 deletions descope/management/_user_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
)
from descope.management.user_pwd import UserPassword

VALID_USER_STATUSES = ["enabled", "disabled", "invited", "expired"]


class UserObj:
def __init__(
Expand Down Expand Up @@ -71,6 +73,16 @@ def __init__(


class UserBase:
@staticmethod
def _validate_status(status: Optional[str], login_id: Optional[str] = None) -> None:
if status is not None and status not in VALID_USER_STATUSES:
suffix = f" for user {login_id}" if login_id is not None else ""
raise AuthException(
400,
ERROR_TYPE_INVALID_ARGUMENT,
f"Invalid status value: {status}{suffix}. Must be one of: {', '.join(VALID_USER_STATUSES)}",
)

@staticmethod
def _compose_create_body(
login_id: str,
Expand All @@ -95,6 +107,7 @@ def _compose_create_body(
sso_app_ids: Optional[List[str]] = None,
template_id: str = "",
locale: Optional[str] = None,
status: Optional[str] = None,
) -> dict:
body = UserBase._compose_update_body(
login_id=login_id,
Expand Down Expand Up @@ -127,6 +140,8 @@ def _compose_create_body(
body["templateId"] = template_id
if locale is not None:
body["locale"] = locale
if status is not None:
body["status"] = status
return body

@staticmethod
Expand Down
29 changes: 6 additions & 23 deletions descope/management/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def create(
invite_url: Optional[str] = None,
additional_login_ids: Optional[List[str]] = None,
sso_app_ids: Optional[List[str]] = None,
status: Optional[str] = None,
) -> dict:
"""
Create a new user. Users can have any number of optional fields, including email, phone number and authorization.
Expand All @@ -48,6 +49,7 @@ def create(
picture (str): Optional url for user picture
custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app
sso_app_ids (List[str]): Optional, list of SSO applications IDs to be associated with the user.
status (str): Optional status field. Can be one of: "enabled", "disabled", "invited", "expired".

Return value (dict):
Return dict in the format
Expand All @@ -57,6 +59,7 @@ def create(
Raise:
AuthException: raised if create operation fails
"""
UserBase._validate_status(status)
role_names = [] if role_names is None else role_names
user_tenants = [] if user_tenants is None else user_tenants

Expand All @@ -83,6 +86,7 @@ def create(
None,
additional_login_ids,
sso_app_ids,
status=status,
),
)
return response.json()
Expand Down Expand Up @@ -389,17 +393,7 @@ def patch(
Raise:
AuthException: raised if patch operation fails
"""
if status is not None and status not in [
"enabled",
"disabled",
"invited",
"expired",
]:
raise AuthException(
400,
ERROR_TYPE_INVALID_ARGUMENT,
f"Invalid status value: {status}. Must be one of: enabled, disabled, invited, expired",
)
UserBase._validate_status(status)
response = self._http.patch(
MgmtV1.user_patch_path,
body=UserBase._compose_patch_body(
Expand Down Expand Up @@ -445,19 +439,8 @@ def patch_batch(
Raise:
AuthException: raised if patch batch operation fails
"""
# Validate status fields for all users
for user in users:
if user.status is not None and user.status not in [
"enabled",
"disabled",
"invited",
"expired",
]:
raise AuthException(
400,
ERROR_TYPE_INVALID_ARGUMENT,
f"Invalid status value: {user.status} for user {user.login_id}. Must be one of: enabled, disabled, invited, expired",
)
UserBase._validate_status(user.status, user.login_id)

response = self._http.patch(
MgmtV1.user_patch_batch_path,
Expand Down
28 changes: 6 additions & 22 deletions descope/management/user_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ async def create(
invite_url: Optional[str] = None,
additional_login_ids: Optional[List[str]] = None,
sso_app_ids: Optional[List[str]] = None,
status: Optional[str] = None,
) -> dict:
"""
Create a new user. Users can have any number of optional fields, including email, phone number and authorization.
Expand All @@ -52,6 +53,7 @@ async def create(
picture (str): Optional url for user picture
custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app
sso_app_ids (List[str]): Optional, list of SSO applications IDs to be associated with the user.
status (str): Optional status field. Can be one of: "enabled", "disabled", "invited", "expired".

Return value (dict):
Return dict in the format
Expand All @@ -61,6 +63,7 @@ async def create(
Raise:
AuthException: raised if create operation fails
"""
UserBase._validate_status(status)
role_names = [] if role_names is None else role_names
user_tenants = [] if user_tenants is None else user_tenants

Expand All @@ -87,6 +90,7 @@ async def create(
None,
additional_login_ids,
sso_app_ids,
status=status,
),
)
return response.json()
Expand Down Expand Up @@ -393,17 +397,7 @@ async def patch(
Raise:
AuthException: raised if patch operation fails
"""
if status is not None and status not in [
"enabled",
"disabled",
"invited",
"expired",
]:
raise AuthException(
400,
ERROR_TYPE_INVALID_ARGUMENT,
f"Invalid status value: {status}. Must be one of: enabled, disabled, invited, expired",
)
UserBase._validate_status(status)
response = await self._http.patch(
MgmtV1.user_patch_path,
body=UserBase._compose_patch_body(
Expand Down Expand Up @@ -451,17 +445,7 @@ async def patch_batch(
"""
# Validate status fields for all users
for user in users:
if user.status is not None and user.status not in [
"enabled",
"disabled",
"invited",
"expired",
]:
raise AuthException(
400,
ERROR_TYPE_INVALID_ARGUMENT,
f"Invalid status value: {user.status} for user {user.login_id}. Must be one of: enabled, disabled, invited, expired",
)
UserBase._validate_status(user.status, user.login_id)

response = await self._http.patch(
MgmtV1.user_patch_batch_path,
Expand Down
6 changes: 6 additions & 0 deletions tests/management/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ async def test_create(self, client_factory):
with pytest.raises(AuthException):
await client.invoke(client.mgmt.user.create("valid-id"))

with pytest.raises(AuthException) as exc_info:
await client.invoke(client.mgmt.user.create("valid-id", status="invalid_status"))
assert "Invalid status value: invalid_status" in str(exc_info.value)

# Test success flow
with client.mock_mgmt_post(make_response({"user": {"id": "u1"}})) as mock_post:
resp = await client.invoke(
Expand All @@ -40,6 +44,7 @@ async def test_create(self, client_factory):
custom_attributes={"ak": "av"},
additional_login_ids=["id-1", "id-2"],
sso_app_ids=["app1", "app2"],
status="disabled",
)
)
user = resp["user"]
Expand Down Expand Up @@ -70,6 +75,7 @@ async def test_create(self, client_factory):
"invite": False,
"additionalLoginIds": ["id-1", "id-2"],
"ssoAppIDs": ["app1", "app2"],
"status": "disabled",
},
follow_redirects=False,
)
Expand Down
Loading