Skip to content

Commit ad79ecc

Browse files
dorshaclaude
andcommitted
fix(mgmt): correct import/fga wire format and document new endpoints
Address PR review feedback on the go-sdk parity sweep: - import_users now base64-encodes users/hashes to match go-sdk's []byte JSON marshaling (raw bytes are not JSON-serializable and would raise TypeError at runtime) - fga load_mappable_resources sends resourcesLimit as a native int in the POST body instead of a string - split awaited calls out of assert statements in delete tests - document the new management endpoints (lists, scope/claim mapping, third party applications, snapshots, user import, passkeys, trusted devices, access key rotate/batch, fga mappable, ws-fed, password settings, audit webhook, step-up impersonation) in README Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 41797ef commit ad79ecc

9 files changed

Lines changed: 216 additions & 11 deletions

File tree

‎README.md‎

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,9 @@ sso_link = descope_client.mgmt.tenant.generate_sso_configuration_link(
654654
expire_time=21600,
655655
actor_id="my-admin-actor-id",
656656
)
657+
658+
# Revoke a previously generated SSO configuration link
659+
descope_client.mgmt.tenant.revoke_sso_configuration_link(tenant_id="my-custom-id")
657660
```
658661

659662
### Manage Users
@@ -794,6 +797,29 @@ users = users_resp["users"]
794797
users_history_resp = descope_client.mgmt.user.history(["user-id-1", "user-id-2"])
795798
for user_history in users_history_resp:
796799
# Do something
800+
801+
# Delete multiple users at once. IMPORTANT: This action is irreversible. Use carefully.
802+
descope_client.mgmt.user.delete_batch(["<user-id-1>", "<user-id-2>"])
803+
804+
# Import users from an external source (users/hashes are JSON-encoded bytes)
805+
descope_client.mgmt.user.import_users(source="auth0", users=b'{"users":[]}', dryrun=True)
806+
807+
# Manage custom user attributes
808+
descope_client.mgmt.user.create_custom_attribute(name="favorite_color", display_name="Favorite Color", type="text")
809+
descope_client.mgmt.user.load_custom_attributes()
810+
descope_client.mgmt.user.delete_custom_attribute("favorite_color")
811+
812+
# Manage a user's passkeys (WebAuthn devices)
813+
passkeys = descope_client.mgmt.user.list_passkeys("<login-id>")
814+
descope_client.mgmt.user.delete_passkey("<login-id>", "<credential-id>")
815+
816+
# Manage trusted devices
817+
devices = descope_client.mgmt.user.list_trusted_devices(["<login-id>"])
818+
descope_client.mgmt.user.remove_trusted_device("<login-id>", ["<device-id>"])
819+
820+
# Update a user's recovery email / phone
821+
descope_client.mgmt.user.update_recovery_email("<login-id>", "recovery@example.com", verified=True)
822+
descope_client.mgmt.user.update_recovery_phone("<login-id>", "+15555555555", verified=True)
797823
```
798824

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

843+
You can also read and configure the password policy settings, at the project level or per tenant:
844+
845+
```python
846+
# Get password settings (project-level when tenant_id is omitted)
847+
settings = descope_client.mgmt.password.get_settings()
848+
tenant_settings = descope_client.mgmt.password.get_settings(tenant_id="my-tenant-id")
849+
850+
# Configure password settings for a tenant
851+
descope_client.mgmt.password.configure_settings(
852+
tenant_id="my-tenant-id",
853+
settings={"minLength": 8, "lowercase": True, "uppercase": True, "number": True},
854+
)
855+
```
856+
817857
### Manage Access Keys
818858

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

867907
# Access key deletion cannot be undone. Use carefully.
868908
descope_client.mgmt.access_key.delete("key-id")
909+
910+
# Rotate an access key - the previous cleartext is invalidated and a new one returned.
911+
rotate_resp = descope_client.mgmt.access_key.rotate("key-id")
912+
913+
# Activate, deactivate or delete multiple access keys at once.
914+
descope_client.mgmt.access_key.activate_batch(["key-id-1", "key-id-2"])
915+
descope_client.mgmt.access_key.deactivate_batch(["key-id-1", "key-id-2"])
916+
descope_client.mgmt.access_key.delete_batch(["key-id-1", "key-id-2"])
869917
```
870918

871919
Exchange the access key and provide optional access key login options:
@@ -1302,6 +1350,15 @@ refresh_jwt = descope_client.mgmt.jwt.impersonate(
13021350
custom_claims={"key1":"value1"},
13031351
tenant_id="<One of the tenants the impersonated user belongs to>"
13041352
)
1353+
1354+
# Impersonate with step-up, returning a fresh session and refresh JWT pair
1355+
jwt_resp = descope_client.mgmt.jwt.impersonate_stepup(
1356+
impersonator_id="<Login ID impersonator>",
1357+
login_id="<Login ID of impersonated person>",
1358+
validate_consent=True,
1359+
custom_claims={"key1": "value1"},
1360+
tenant_id="<One of the tenants the impersonated user belongs to>",
1361+
)
13051362
```
13061363

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

1405+
You can create an audit webhook to stream audit events to an external endpoint:
1406+
1407+
```python
1408+
descope_client.mgmt.audit.create_audit_webhook(
1409+
name="my-webhook",
1410+
url="https://example.com/audit",
1411+
headers={"Authorization": "Bearer <token>"},
1412+
)
1413+
```
1414+
13481415
### Manage FGA (Fine-grained Authorization)
13491416

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

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

1507+
You can also validate a schema with a dry run, and load the mappable schema and resources for a tenant:
1508+
1509+
```python
1510+
# Validate a schema without applying it
1511+
descope_client.mgmt.fga.save_schema_dryrun(schema)
1512+
1513+
# Load the mappable schema / resources for a tenant
1514+
descope_client.mgmt.fga.load_mappable_schema("tenant-id", resources_limit=100)
1515+
descope_client.mgmt.fga.load_mappable_resources("tenant-id", [{"query": "doc"}], resources_limit=50)
1516+
```
1517+
14401518
### Manage Project
14411519

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

1548+
You can also work with project snapshots and delete a project.
1549+
1550+
```python
1551+
# Export a snapshot of the project (optionally for a specific environment)
1552+
snapshot = descope_client.mgmt.project.export_snapshot()
1553+
1554+
# Validate a snapshot before importing it
1555+
validation = descope_client.mgmt.project.validate_snapshot(snapshot["files"])
1556+
1557+
# Import a previously exported snapshot
1558+
descope_client.mgmt.project.import_snapshot(snapshot["files"])
1559+
1560+
# Delete the current project. IMPORTANT: This action is irreversible. Use carefully.
1561+
descope_client.mgmt.project.delete()
1562+
```
1563+
14701564
### Manage SSO Applications
14711565

14721566
You can create, update, delete or load sso applications:
@@ -1522,6 +1616,25 @@ apps_resp = descope_client.mgmt.sso_application.load_all()
15221616
apps = apps_resp["apps"]
15231617
for app in apps:
15241618
# Do something
1619+
1620+
# Create / update a WS-Fed SSO application
1621+
descope_client.mgmt.sso_application.create_wsfed_application(
1622+
name="My WS-Fed app",
1623+
login_page_url="http://dummy.com",
1624+
realm="urn:my-realm",
1625+
reply_url="http://dummy.com/reply",
1626+
)
1627+
descope_client.mgmt.sso_application.update_wsfed_application(
1628+
id="my-custom-id",
1629+
name="My WS-Fed app",
1630+
login_page_url="http://dummy.com",
1631+
realm="urn:my-realm",
1632+
reply_url="http://dummy.com/reply",
1633+
)
1634+
1635+
# Get or rotate the SSO application secret
1636+
secret = descope_client.mgmt.sso_application.get_application_secret("my-custom-id")
1637+
new_secret = descope_client.mgmt.sso_application.rotate_application_secret("my-custom-id")
15251638
```
15261639

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

2010+
### Manage Lists
2011+
2012+
Lists let you maintain reusable allow/deny collections of IPs, text values or arbitrary JSON.
2013+
The lists client is available as `descope_client.mgmt.list`.
2014+
2015+
```python
2016+
# Create a list (list_type is one of "ips", "texts" or "json")
2017+
my_list = descope_client.mgmt.list.create(
2018+
name="blocked-ips",
2019+
list_type="ips",
2020+
description="IPs to block",
2021+
data=["1.2.3.4"],
2022+
)
2023+
list_id = my_list["id"]
2024+
2025+
# Update / load / delete
2026+
descope_client.mgmt.list.update(list_id, name="blocked-ips", description="updated")
2027+
descope_client.mgmt.list.load(list_id)
2028+
descope_client.mgmt.list.load_by_name("blocked-ips")
2029+
descope_client.mgmt.list.load_all()
2030+
descope_client.mgmt.list.delete(list_id)
2031+
2032+
# Bulk import lists
2033+
descope_client.mgmt.list.import_lists([{"name": "blocked-ips", "type": "ips", "data": ["1.2.3.4"]}])
2034+
2035+
# IP list operations
2036+
descope_client.mgmt.list.add_ips(list_id, ["5.6.7.8"])
2037+
descope_client.mgmt.list.remove_ips(list_id, ["1.2.3.4"])
2038+
is_blocked = descope_client.mgmt.list.check_ip(list_id, "5.6.7.8")
2039+
2040+
# Text list operations
2041+
descope_client.mgmt.list.add_texts(list_id, ["foo"])
2042+
descope_client.mgmt.list.remove_texts(list_id, ["foo"])
2043+
has_text = descope_client.mgmt.list.check_text(list_id, "foo")
2044+
2045+
# Remove all entries from a list
2046+
descope_client.mgmt.list.clear(list_id)
2047+
```
2048+
2049+
### Manage Scope and Claim Mappings
2050+
2051+
Scope/claim mappings control which claims are emitted for a given OAuth scope.
2052+
2053+
```python
2054+
# Get the current mappings
2055+
mappings = descope_client.mgmt.scope_claim_mapping.get()
2056+
2057+
# Replace all mappings
2058+
descope_client.mgmt.scope_claim_mapping.set(
2059+
[{"scope": "profile", "claims": {"name": "name"}, "description": "profile scope"}]
2060+
)
2061+
2062+
# Delete all mappings
2063+
descope_client.mgmt.scope_claim_mapping.delete()
2064+
```
2065+
2066+
### Manage Third Party Applications
2067+
2068+
You can create, update, delete or load third party (OIDC) applications, manage their
2069+
secrets and the consents granted to them.
2070+
2071+
```python
2072+
# Create a third party application
2073+
app = descope_client.mgmt.third_party_application.create(
2074+
name="My App",
2075+
login_page_url="http://dummy.com",
2076+
approved_callback_urls=["http://dummy.com/callback"],
2077+
)
2078+
app_id = app["id"]
2079+
2080+
# Update / patch / load / delete
2081+
descope_client.mgmt.third_party_application.update(app_id, name="My App", login_page_url="http://dummy.com")
2082+
descope_client.mgmt.third_party_application.patch(app_id, name="Renamed App")
2083+
descope_client.mgmt.third_party_application.load(app_id)
2084+
descope_client.mgmt.third_party_application.load_all()
2085+
descope_client.mgmt.third_party_application.delete(app_id)
2086+
descope_client.mgmt.third_party_application.delete_batch([app_id])
2087+
2088+
# Manage the application secret
2089+
descope_client.mgmt.third_party_application.get_secret(app_id)
2090+
descope_client.mgmt.third_party_application.rotate_secret(app_id)
2091+
2092+
# Manage consents
2093+
descope_client.mgmt.third_party_application.search_consents(app_id=app_id)
2094+
descope_client.mgmt.third_party_application.delete_consents(app_id=app_id)
2095+
descope_client.mgmt.third_party_application.delete_tenant_consents("tenant-id")
2096+
```
2097+
18972098
### Utils for your end to end (e2e) tests and integration tests
18982099

18992100
To ease your e2e tests, we exposed dedicated management methods,

‎descope/management/fga.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def load_mappable_resources(
194194
"""
195195
body = {"tenantId": tenant_id, "resourcesQueries": resources_queries}
196196
if resources_limit is not None:
197-
body["resourcesLimit"] = str(resources_limit)
197+
body["resourcesLimit"] = resources_limit
198198
response = self._http.post(MgmtV1.fga_mappable_resources_path, body=body)
199199
return response.json().get("mappableResources", [])
200200

‎descope/management/fga_async.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ async def load_mappable_resources(
198198
"""
199199
body = {"tenantId": tenant_id, "resourcesQueries": resources_queries}
200200
if resources_limit is not None:
201-
body["resourcesLimit"] = str(resources_limit)
201+
body["resourcesLimit"] = resources_limit
202202
response = await self._http.post(MgmtV1.fga_mappable_resources_path, body=body)
203203
return response.json().get("mappableResources", [])
204204

‎descope/management/user.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import base64
12
from typing import List, Optional, Union
23

34
from descope._http_base import HTTPBase
@@ -1899,9 +1900,9 @@ def import_users(
18991900
"dryrun": dryrun,
19001901
}
19011902
if users is not None:
1902-
body["users"] = users
1903+
body["users"] = base64.b64encode(users).decode("utf-8")
19031904
if hashes is not None:
1904-
body["hashes"] = hashes
1905+
body["hashes"] = base64.b64encode(hashes).decode("utf-8")
19051906

19061907
response = self._http.post(
19071908
MgmtV1.user_import_path,

‎descope/management/user_async.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import base64
34
from typing import List, Optional, Union
45

56
from descope._http_base import AsyncHTTPBase
@@ -1907,9 +1908,9 @@ async def import_users(
19071908
"dryrun": dryrun,
19081909
}
19091910
if users is not None:
1910-
body["users"] = users
1911+
body["users"] = base64.b64encode(users).decode("utf-8")
19111912
if hashes is not None:
1912-
body["hashes"] = hashes
1913+
body["hashes"] = base64.b64encode(hashes).decode("utf-8")
19131914

19141915
response = await self._http.post(
19151916
MgmtV1.user_import_path,

‎tests/management/test_fga.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ async def test_load_mappable_resources(self, client_factory):
204204
f"{DEFAULT_BASE_URL}{MgmtV1.fga_mappable_resources_path}",
205205
headers=MGMT_HEADERS,
206206
params=None,
207-
json={"tenantId": "tenant1", "resourcesQueries": [{"query": "test"}], "resourcesLimit": "5"},
207+
json={"tenantId": "tenant1", "resourcesQueries": [{"query": "test"}], "resourcesLimit": 5},
208208
follow_redirects=False,
209209
)
210210

‎tests/management/test_project.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ async def test_delete(self, client_factory):
209209

210210
# Test success flow
211211
with client.mock_mgmt_post(make_response()) as mock_post:
212-
assert await client.invoke(client.mgmt.project.delete()) is None
212+
resp = await client.invoke(client.mgmt.project.delete())
213+
assert resp is None
213214
assert_http_called(
214215
mock_post,
215216
client.mode,

‎tests/management/test_scope_claim_mapping.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ async def test_delete(self, client_factory):
9696

9797
# Test success flow
9898
with client.mock_mgmt_post(make_response()) as mock_post:
99-
assert await client.invoke(client.mgmt.scope_claim_mapping.delete()) is None
99+
resp = await client.invoke(client.mgmt.scope_claim_mapping.delete())
100+
assert resp is None
100101
assert_http_called(
101102
mock_post,
102103
client.mode,

‎tests/management/test_user.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2653,8 +2653,8 @@ async def test_import_users(self, client_factory):
26532653
params=None,
26542654
json={
26552655
"source": "auth0",
2656-
"users": b'{"users":[]}',
2657-
"hashes": b'{"hashes":[]}',
2656+
"users": "eyJ1c2VycyI6W119",
2657+
"hashes": "eyJoYXNoZXMiOltdfQ==",
26582658
"dryrun": True,
26592659
},
26602660
follow_redirects=False,

0 commit comments

Comments
 (0)