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
133 changes: 133 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1729,6 +1729,139 @@ latest_tenant_token = descope_client.mgmt.outbound_application_by_token.fetch_te
)
```

### Manage Third Party Applications

Third party applications let external apps use Descope as an identity provider via OAuth/OIDC.

```python
# Create a third party application (returns the generated id and secret)
resp = descope_client.mgmt.third_party_application.create(
name="my-app",
login_page_url="https://my-app.com/login",
description="My third party app", # Optional
approved_callback_urls=["https://my-app.com/callback"], # Optional
permissions_scopes=[{"name": "read", "description": "Read access", "values": ["roleA"]}], # Optional
force_pkce=True, # Optional
)
app_id = resp["id"]

# Update will override all fields as is. Use carefully.
descope_client.mgmt.third_party_application.update(
id=app_id,
name="my-app",
login_page_url="https://my-app.com/login",
)

# Patch only the provided fields
descope_client.mgmt.third_party_application.patch(id=app_id, description="updated")

# Load / load all
app = descope_client.mgmt.third_party_application.load(app_id)
all_apps = descope_client.mgmt.third_party_application.load_all()

# Rotate or fetch the application secret
new_secret = descope_client.mgmt.third_party_application.rotate_secret(app_id)
secret = descope_client.mgmt.third_party_application.get_secret(app_id)

# Search and delete user consents
consents = descope_client.mgmt.third_party_application.search_consents(app_id=app_id, limit=10)
descope_client.mgmt.third_party_application.delete_consents(app_id=app_id, user_ids=["u1"])
descope_client.mgmt.third_party_application.delete_tenant_consents("tenant-id")

# Application deletion cannot be undone. Use carefully.
descope_client.mgmt.third_party_application.delete(app_id)
descope_client.mgmt.third_party_application.delete_batch([app_id])
```

### Manage Lists

Manage allow/deny lists of texts, IPs, or arbitrary JSON.

```python
# Create a list. list_type is one of "texts", "ips", or "json".
resp = descope_client.mgmt.list.create(
name="blocked-ips",
list_type="ips",
description="Blocked addresses", # Optional
data=["1.2.3.0/24"], # Optional initial data
)
list_id = resp["list"]["id"]

# Load
descope_client.mgmt.list.load(list_id)
descope_client.mgmt.list.load_by_name("blocked-ips")
descope_client.mgmt.list.load_all()

# Update / delete (deletion cannot be undone)
descope_client.mgmt.list.update(id=list_id, name="blocked-ips", description="updated")
descope_client.mgmt.list.delete(list_id)

# Import multiple lists at once
descope_client.mgmt.list.import_lists([{"name": "deny", "type": "texts", "data": ["foo"]}])

# IP lists
descope_client.mgmt.list.add_ips(list_id, ["10.0.0.1"])
descope_client.mgmt.list.remove_ips(list_id, ["10.0.0.1"])
is_blocked = descope_client.mgmt.list.check_ip(list_id, "10.0.0.1")

# Text lists
descope_client.mgmt.list.add_texts(list_id, ["spam@example.com"])
descope_client.mgmt.list.remove_texts(list_id, ["spam@example.com"])
has_text = descope_client.mgmt.list.check_text(list_id, "spam@example.com")

# Clear all entries
descope_client.mgmt.list.clear(list_id)
```

### Manage Password Settings

Get and configure the password policy for the project or a specific tenant.

```python
# Get password settings (omit tenant_id for project-level settings)
settings = descope_client.mgmt.password.get_settings()
tenant_settings = descope_client.mgmt.password.get_settings("tenant-id")

# Configure settings. The settings dict mirrors the fields returned by get_settings,
# e.g. {"minLength": 8, "expiration": True, "expirationWeeks": 4, "reuse": True, "reuseAmount": 6}
descope_client.mgmt.password.configure_settings(
tenant_id="tenant-id",
settings={"minLength": 10, "nonAlphanumeric": True, "number": True, "uppercase": True},
)
```

### Analytics

Search analytics records, optionally filtered and grouped.

```python
from datetime import datetime, timedelta

results = descope_client.mgmt.analytics.search(
from_ts=datetime.now() - timedelta(days=7), # Optional, no older than 12 months
methods=["password", "otp"], # Optional auth methods
geos=["US", "IL"], # Optional country codes
group_by_action=True, # Optional grouping flags
)
```

### Manage Scope Claim Mapping

Map OAuth/OIDC scopes to JWT claims for the project.

```python
# Get the current mappings
mappings = descope_client.mgmt.scope_claim_mapping.get()

# Set replaces all existing mappings. Each entry: {"scope": str, "claims": dict, "description": str}
descope_client.mgmt.scope_claim_mapping.set(
mappings=[{"scope": "profile", "claims": {"name": "name"}, "description": "Profile scope"}],
)

# Delete all mappings
descope_client.mgmt.scope_claim_mapping.delete()
```

### Manage Descopers

You can create, update, delete, load or list Descopers (users who have access to the Descope console):
Expand Down
51 changes: 51 additions & 0 deletions descope/management/_lists_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from __future__ import annotations

from typing import Any, List, Optional


class ListsBase:
@staticmethod
def _compose_create_body(name: str, description: Optional[str], list_type: str, data: Any) -> dict:
body = {"name": name, "type": list_type}
if description is not None:
body["description"] = description
if data is not None:
body["data"] = data
return body

@staticmethod
def _compose_update_body(id: str, name: str, description: Optional[str], list_type: str, data: Any) -> dict:
body = {"id": id, "name": name, "type": list_type}
if description is not None:
body["description"] = description

Check warning on line 20 in descope/management/_lists_base.py

View workflow job for this annotation

GitHub Actions / Coverage

This line has no coverage
if data is not None:
body["data"] = data
return body

@staticmethod
def _compose_delete_body(id: str) -> dict:
return {"id": id}

@staticmethod
def _compose_import_body(lists: List[dict]) -> dict:
return {"lists": lists}

@staticmethod
def _compose_ip_body(id: str, ips: List[str]) -> dict:
return {"id": id, "ips": ips}

@staticmethod
def _compose_check_ip_body(id: str, ip: str) -> dict:
return {"id": id, "ip": ip}

@staticmethod
def _compose_text_body(id: str, texts: List[str]) -> dict:
return {"id": id, "texts": texts}

@staticmethod
def _compose_check_text_body(id: str, text: str) -> dict:
return {"id": id, "text": text}

@staticmethod
def _compose_clear_body(id: str) -> dict:
return {"id": id}
35 changes: 35 additions & 0 deletions descope/management/_sso_application_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,38 @@ def _compose_create_update_saml_body(
"defaultSignatureAlgorithm": default_signature_algorithm,
}
return body

@staticmethod
def _compose_create_update_wsfed_body(
name: str,
login_page_url: str,
realm: str,
reply_url: str,
id: Optional[str] = None,
description: Optional[str] = None,
logo: Optional[str] = None,
enabled: Optional[bool] = True,
reply_allowed_callbacks: Optional[List[str]] = None,
attribute_mapping: Optional[List[SAMLIDPAttributeMappingInfo]] = None,
groups_mapping: Optional[List[SAMLIDPGroupsMappingInfo]] = None,
force_authentication: Optional[bool] = False,
logout_redirect_url: Optional[str] = None,
error_redirect_url: Optional[str] = None,
) -> dict:
body: dict[str, Any] = {
"id": id,
"name": name,
"description": description,
"enabled": enabled,
"logo": logo,
"loginPageUrl": login_page_url,
"realm": realm,
"replyUrl": reply_url,
"replyAllowedCallbacks": reply_allowed_callbacks if reply_allowed_callbacks else [],
"attributeMapping": saml_idp_attribute_mapping_info_to_dict(attribute_mapping),
"groupsMapping": saml_idp_groups_mapping_info_to_dict(groups_mapping),
"forceAuthentication": force_authentication,
"logoutRedirectUrl": logout_redirect_url,
"errorRedirectUrl": error_redirect_url,
}
return body
43 changes: 43 additions & 0 deletions descope/management/_third_party_application_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from typing import Any, Dict, List, Optional


# This is not part of the public API but a code helper
def compose_create_update_body(
name: str,
login_page_url: str,
id: Optional[str] = None,
description: Optional[str] = None,
logo: Optional[str] = None,
approved_callback_urls: Optional[List[str]] = None,
permissions_scopes: Optional[List[Dict[str, Any]]] = None,
attributes_scopes: Optional[List[Dict[str, Any]]] = None,
jwt_bearer_settings: Optional[Dict[str, Any]] = None,
custom_attributes: Optional[Dict[str, Any]] = None,
force_pkce: Optional[bool] = None,
default_audience: Optional[str] = None,
) -> dict:
body: Dict[str, Any] = {
"name": name,
"loginPageUrl": login_page_url,
}
if id is not None:
body["id"] = id
if description is not None:
body["description"] = description
if logo is not None:
body["logo"] = logo
if approved_callback_urls is not None:
body["approvedCallbackUrls"] = approved_callback_urls
if permissions_scopes is not None:
body["permissionsScopes"] = permissions_scopes

Check warning on line 32 in descope/management/_third_party_application_base.py

View workflow job for this annotation

GitHub Actions / Coverage

This line has no coverage
if attributes_scopes is not None:
body["attributesScopes"] = attributes_scopes

Check warning on line 34 in descope/management/_third_party_application_base.py

View workflow job for this annotation

GitHub Actions / Coverage

This line has no coverage
if jwt_bearer_settings is not None:
body["jwtBearerSettings"] = jwt_bearer_settings

Check warning on line 36 in descope/management/_third_party_application_base.py

View workflow job for this annotation

GitHub Actions / Coverage

This line has no coverage
if custom_attributes is not None:
body["customAttributes"] = custom_attributes

Check warning on line 38 in descope/management/_third_party_application_base.py

View workflow job for this annotation

GitHub Actions / Coverage

This line has no coverage
if force_pkce is not None:
body["forcePkce"] = force_pkce
if default_audience is not None:
body["defaultAudience"] = default_audience
return body
83 changes: 83 additions & 0 deletions descope/management/access_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,86 @@ def delete(
MgmtV1.access_key_delete_path,
body={"id": id},
)

def rotate(
self,
id: str,
) -> dict:
"""
Rotate an existing access key. Regenerates the secret while preserving the same access key ID,
name, roles, tenants, expiry, and metadata.

Args:
id (str): The id of the access key to be rotated.

Return value (dict):
Return dict in the format
{
"key": {},
"cleartext": {}
}
The cleartext is the new secret and is only visible once. Save it immediately.
The previous secret stops working as soon as this call returns.

Raise:
AuthException: raised if rotation operation fails
"""
response = self._http.post(
MgmtV1.access_key_rotate_path,
body={"id": id},
)
return response.json()

def activate_batch(
self,
ids: List[str],
):
"""
Activate multiple existing access keys in a single request.

Args:
ids (List[str]): The list of access key IDs to be activated.

Raise:
AuthException: raised if batch activation operation fails
"""
self._http.post(
MgmtV1.access_key_activate_batch_path,
body={"ids": ids},
)

def deactivate_batch(
self,
ids: List[str],
):
"""
Deactivate multiple existing access keys in a single request.

Args:
ids (List[str]): The list of access key IDs to be deactivated.

Raise:
AuthException: raised if batch deactivation operation fails
"""
self._http.post(
MgmtV1.access_key_deactivate_batch_path,
body={"ids": ids},
)

def delete_batch(
self,
ids: List[str],
):
"""
Delete multiple existing access keys in a single request. IMPORTANT: This action is irreversible. Use carefully.

Args:
ids (List[str]): The list of access key IDs to be deleted.

Raise:
AuthException: raised if batch deletion operation fails
"""
self._http.post(
MgmtV1.access_key_delete_batch_path,
body={"ids": ids},
)
Loading
Loading