Skip to content
Closed
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
201 changes: 201 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,9 @@ sso_link = descope_client.mgmt.tenant.generate_sso_configuration_link(
expire_time=21600,
actor_id="my-admin-actor-id",
)

# Revoke a previously generated SSO configuration link
descope_client.mgmt.tenant.revoke_sso_configuration_link(tenant_id="my-custom-id")
```

### Manage Users
Expand Down Expand Up @@ -794,6 +797,29 @@ users = users_resp["users"]
users_history_resp = descope_client.mgmt.user.history(["user-id-1", "user-id-2"])
for user_history in users_history_resp:
# Do something

# Delete multiple users at once. IMPORTANT: This action is irreversible. Use carefully.
descope_client.mgmt.user.delete_batch(["<user-id-1>", "<user-id-2>"])

# Import users from an external source (users/hashes are JSON-encoded bytes)
descope_client.mgmt.user.import_users(source="auth0", users=b'{"users":[]}', dryrun=True)

# Manage custom user attributes
descope_client.mgmt.user.create_custom_attribute(name="favorite_color", display_name="Favorite Color", type="text")
descope_client.mgmt.user.load_custom_attributes()
descope_client.mgmt.user.delete_custom_attribute("favorite_color")

# Manage a user's passkeys (WebAuthn devices)
passkeys = descope_client.mgmt.user.list_passkeys("<login-id>")
descope_client.mgmt.user.delete_passkey("<login-id>", "<credential-id>")

# Manage trusted devices
devices = descope_client.mgmt.user.list_trusted_devices(["<login-id>"])
descope_client.mgmt.user.remove_trusted_device("<login-id>", ["<device-id>"])

# Update a user's recovery email / phone
descope_client.mgmt.user.update_recovery_email("<login-id>", "recovery@example.com", verified=True)
descope_client.mgmt.user.update_recovery_phone("<login-id>", "+15555555555", verified=True)
```

#### Set or Expire User Password
Expand All @@ -814,6 +840,20 @@ descope_client.mgmt.user.set_active_password('<login-id>', '<some-password>');
descope_client.mgmt.user.expirePassword('<login-id>');
```

You can also read and configure the password policy settings, at the project level or per tenant:

```python
# Get password settings (project-level when tenant_id is omitted)
settings = descope_client.mgmt.password.get_settings()
tenant_settings = descope_client.mgmt.password.get_settings(tenant_id="my-tenant-id")

# Configure password settings for a tenant
descope_client.mgmt.password.configure_settings(
tenant_id="my-tenant-id",
settings={"minLength": 8, "lowercase": True, "uppercase": True, "number": True},
)
```

### Manage Access Keys

You can create, update, delete or load access keys, as well as search according to filters:
Expand Down Expand Up @@ -866,6 +906,14 @@ descope_client.mgmt.access_key.activate("key-id")

# Access key deletion cannot be undone. Use carefully.
descope_client.mgmt.access_key.delete("key-id")

# Rotate an access key - the previous cleartext is invalidated and a new one returned.
rotate_resp = descope_client.mgmt.access_key.rotate("key-id")

# Activate, deactivate or delete multiple access keys at once.
descope_client.mgmt.access_key.activate_batch(["key-id-1", "key-id-2"])
descope_client.mgmt.access_key.deactivate_batch(["key-id-1", "key-id-2"])
descope_client.mgmt.access_key.delete_batch(["key-id-1", "key-id-2"])
```

Exchange the access key and provide optional access key login options:
Expand Down Expand Up @@ -1302,6 +1350,15 @@ refresh_jwt = descope_client.mgmt.jwt.impersonate(
custom_claims={"key1":"value1"},
tenant_id="<One of the tenants the impersonated user belongs to>"
)

# Impersonate with step-up, returning a fresh session and refresh JWT pair
jwt_resp = descope_client.mgmt.jwt.impersonate_stepup(
impersonator_id="<Login ID impersonator>",
login_id="<Login ID of impersonated person>",
validate_consent=True,
custom_claims={"key1": "value1"},
tenant_id="<One of the tenants the impersonated user belongs to>",
)
```

# Note 1: The generate code/link functions, work only for test users, will not work for regular users.
Expand Down Expand Up @@ -1345,6 +1402,16 @@ await descopeClient.management.audit.create_event(
)
```

You can create an audit webhook to stream audit events to an external endpoint:

```python
descope_client.mgmt.audit.create_audit_webhook(
name="my-webhook",
url="https://example.com/audit",
headers={"Authorization": "Bearer <token>"},
)
```

### Manage FGA (Fine-grained Authorization)

Descope supports full relation based access control (ReBAC) using a zanzibar like schema and operations.
Expand Down Expand Up @@ -1437,6 +1504,17 @@ When the `fga_cache_url` is configured, the following FGA methods will automatic

Other FGA operations like `load_schema` will continue to use the standard Descope API endpoints.

You can also validate a schema with a dry run, and load the mappable schema and resources for a tenant:

```python
# Validate a schema without applying it
descope_client.mgmt.fga.save_schema_dryrun(schema)

# Load the mappable schema / resources for a tenant
descope_client.mgmt.fga.load_mappable_schema("tenant-id", resources_limit=100)
descope_client.mgmt.fga.load_mappable_resources("tenant-id", [{"query": "doc"}], resources_limit=50)
```

### Manage Project

You can change the project name, as well as clone the current project to
Expand Down Expand Up @@ -1467,6 +1545,22 @@ export = descope_client.mgmt.project.export_project()
descope_client.mgmt.project.import_project(export)
```

You can also work with project snapshots and delete a project.

```python
# Export a snapshot of the project (optionally for a specific environment)
snapshot = descope_client.mgmt.project.export_snapshot()

# Validate a snapshot before importing it
validation = descope_client.mgmt.project.validate_snapshot(snapshot["files"])

# Import a previously exported snapshot
descope_client.mgmt.project.import_snapshot(snapshot["files"])

# Delete the current project. IMPORTANT: This action is irreversible. Use carefully.
descope_client.mgmt.project.delete()
```

### Manage SSO Applications

You can create, update, delete or load sso applications:
Expand Down Expand Up @@ -1522,6 +1616,25 @@ apps_resp = descope_client.mgmt.sso_application.load_all()
apps = apps_resp["apps"]
for app in apps:
# Do something

# Create / update a WS-Fed SSO application
descope_client.mgmt.sso_application.create_wsfed_application(
name="My WS-Fed app",
login_page_url="http://dummy.com",
realm="urn:my-realm",
reply_url="http://dummy.com/reply",
)
descope_client.mgmt.sso_application.update_wsfed_application(
id="my-custom-id",
name="My WS-Fed app",
login_page_url="http://dummy.com",
realm="urn:my-realm",
reply_url="http://dummy.com/reply",
)

# Get or rotate the SSO application secret
secret = descope_client.mgmt.sso_application.get_application_secret("my-custom-id")
new_secret = descope_client.mgmt.sso_application.rotate_application_secret("my-custom-id")
```

### Manage Outbound Applications
Expand Down Expand Up @@ -1894,6 +2007,94 @@ new_secret = descope_client.mgmt.engine.rotate_secret(engine_id)["secret"]
descope_client.mgmt.engine.delete(engine_id)
```

### Manage Lists

Lists let you maintain reusable allow/deny collections of IPs, text values or arbitrary JSON.
The lists client is available as `descope_client.mgmt.list`.

```python
# Create a list (list_type is one of "ips", "texts" or "json")
my_list = descope_client.mgmt.list.create(
name="blocked-ips",
list_type="ips",
description="IPs to block",
data=["1.2.3.4"],
)
list_id = my_list["id"]

# Update / load / delete
descope_client.mgmt.list.update(list_id, name="blocked-ips", description="updated")
descope_client.mgmt.list.load(list_id)
descope_client.mgmt.list.load_by_name("blocked-ips")
descope_client.mgmt.list.load_all()
descope_client.mgmt.list.delete(list_id)

# Bulk import lists
descope_client.mgmt.list.import_lists([{"name": "blocked-ips", "type": "ips", "data": ["1.2.3.4"]}])

# IP list operations
descope_client.mgmt.list.add_ips(list_id, ["5.6.7.8"])
descope_client.mgmt.list.remove_ips(list_id, ["1.2.3.4"])
is_blocked = descope_client.mgmt.list.check_ip(list_id, "5.6.7.8")

# Text list operations
descope_client.mgmt.list.add_texts(list_id, ["foo"])
descope_client.mgmt.list.remove_texts(list_id, ["foo"])
has_text = descope_client.mgmt.list.check_text(list_id, "foo")

# Remove all entries from a list
descope_client.mgmt.list.clear(list_id)
```

### Manage Scope and Claim Mappings

Scope/claim mappings control which claims are emitted for a given OAuth scope.

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

# Replace all mappings
descope_client.mgmt.scope_claim_mapping.set(
[{"scope": "profile", "claims": {"name": "name"}, "description": "profile scope"}]
)

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

### Manage Third Party Applications

You can create, update, delete or load third party (OIDC) applications, manage their
secrets and the consents granted to them.

```python
# Create a third party application
app = descope_client.mgmt.third_party_application.create(
name="My App",
login_page_url="http://dummy.com",
approved_callback_urls=["http://dummy.com/callback"],
)
app_id = app["id"]

# Update / patch / load / delete
descope_client.mgmt.third_party_application.update(app_id, name="My App", login_page_url="http://dummy.com")
descope_client.mgmt.third_party_application.patch(app_id, name="Renamed App")
descope_client.mgmt.third_party_application.load(app_id)
descope_client.mgmt.third_party_application.load_all()
descope_client.mgmt.third_party_application.delete(app_id)
descope_client.mgmt.third_party_application.delete_batch([app_id])

# Manage the application secret
descope_client.mgmt.third_party_application.get_secret(app_id)
descope_client.mgmt.third_party_application.rotate_secret(app_id)

# Manage consents
descope_client.mgmt.third_party_application.search_consents(app_id=app_id)
descope_client.mgmt.third_party_application.delete_consents(app_id=app_id)
descope_client.mgmt.third_party_application.delete_tenant_consents("tenant-id")
```

### Utils for your end to end (e2e) tests and integration tests

To ease your e2e tests, we exposed dedicated management methods,
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
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
Loading
Loading