Skip to content

Commit 972a025

Browse files
dorshaclaude
andauthored
feat(mgmt): load external groups scoped to a specific SSO config (#1641)
* feat(mgmt): load external groups scoped to a specific SSO config Co-Authored-By: Claude <noreply@anthropic.com> * fix(deps): restore mypy <1.12 bound on the Python 3.9 types pin Renovate bumped the python_version < '3.10' mypy pin to 2.3.0 (#1569), which requires Python 3.10+, making the 3.9 resolution split unsatisfiable and breaking `uv sync --locked` for every CI job. Use a bounded range like the pytest entry so renovate cannot re-bump it past the last 3.9-compatible line. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8386f19 commit 972a025

4 files changed

Lines changed: 107 additions & 24 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,14 @@ groups_resp = descope_client.mgmt.group.load_all_groups(
12361236
tenant_id="tenant-id",
12371237
)
12381238

1239+
# Load only the groups that came from a specific SSO configuration (each returned group
1240+
# carries an "ssoId" field identifying its origin; use "default_ssoid" for the tenant's
1241+
# default SSO configuration)
1242+
groups_resp = descope_client.mgmt.group.load_all_groups(
1243+
tenant_id="tenant-id",
1244+
sso_id="sso-config-id",
1245+
)
1246+
12391247
# Load all groups for the given user IDs (can be found in the user's JWT)
12401248
groups_resp = descope_client.mgmt.group.load_all_groups_for_members(
12411249
tenant_id="tenant-id",

descope/management/group.py

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@ class Group(HTTPBase):
88
def load_all_groups(
99
self,
1010
tenant_id: str,
11+
sso_id: Optional[str] = None,
1112
) -> dict:
1213
"""
1314
Load all groups for a specific tenant id.
1415
1516
Args:
1617
tenant_id (str): Tenant ID to load groups from.
18+
sso_id (str): Optional SSO configuration id (ssoId) to load only groups that came from
19+
that SSO configuration. Use the reserved id "default_ssoid" for the tenant's default
20+
SSO configuration. When omitted, all the tenant's groups are returned.
1721
1822
Return value (dict):
1923
Return dict in the format
@@ -22,6 +26,7 @@ def load_all_groups(
2226
"id": <group id>,
2327
"display": <display name>,
2428
"source": <"scim" or "jit">,
29+
"ssoId": <sso configuration id>,
2530
"members":[
2631
{
2732
"loginId": <loginId>,
@@ -36,11 +41,14 @@ def load_all_groups(
3641
Raise:
3742
AuthException: raised if load operation fails
3843
"""
44+
body = {
45+
"tenantId": tenant_id,
46+
}
47+
if sso_id is not None:
48+
body["ssoId"] = sso_id
3949
response = self._http.post(
4050
MgmtV1.group_load_all_path,
41-
body={
42-
"tenantId": tenant_id,
43-
},
51+
body=body,
4452
)
4553
return response.json()
4654

@@ -49,6 +57,7 @@ def load_all_groups_for_members(
4957
tenant_id: str,
5058
user_ids: Optional[List[str]] = None,
5159
login_ids: Optional[List[str]] = None,
60+
sso_id: Optional[str] = None,
5261
) -> dict:
5362
"""
5463
Load all groups for the provided user IDs or login IDs.
@@ -57,6 +66,9 @@ def load_all_groups_for_members(
5766
tenant_id (str): Tenant ID to load groups from.
5867
user_ids (List[str]): Optional List of user IDs, with the format of "U2J5ES9S8TkvCgOvcrkpzUgVTEBM" (example), which can be found on the user's JWT.
5968
login_ids (List[str]): Optional List of login IDs, how the users identify when logging in.
69+
sso_id (str): Optional SSO configuration id (ssoId) to load only groups that came from
70+
that SSO configuration. Use the reserved id "default_ssoid" for the tenant's default
71+
SSO configuration. When omitted, all matching groups are returned.
6072
6173
Return value (dict):
6274
Return dict in the format
@@ -65,6 +77,7 @@ def load_all_groups_for_members(
6577
"id": <group id>,
6678
"display": <display name>,
6779
"source": <"scim" or "jit">,
80+
"ssoId": <sso configuration id>,
6881
"members":[
6982
{
7083
"loginId": <loginId>,
@@ -82,27 +95,34 @@ def load_all_groups_for_members(
8295
user_ids = [] if user_ids is None else user_ids
8396
login_ids = [] if login_ids is None else login_ids
8497

98+
body = {
99+
"tenantId": tenant_id,
100+
"loginIds": login_ids,
101+
"userIds": user_ids,
102+
}
103+
if sso_id is not None:
104+
body["ssoId"] = sso_id
85105
response = self._http.post(
86106
MgmtV1.group_load_all_for_member_path,
87-
body={
88-
"tenantId": tenant_id,
89-
"loginIds": login_ids,
90-
"userIds": user_ids,
91-
},
107+
body=body,
92108
)
93109
return response.json()
94110

95111
def load_all_group_members(
96112
self,
97113
tenant_id: str,
98114
group_id: str,
115+
sso_id: Optional[str] = None,
99116
) -> dict:
100117
"""
101118
Load all members of the provided group id.
102119
103120
Args:
104121
tenant_id (str): Tenant ID to load groups from.
105122
group_id (str): Group ID to load members for.
123+
sso_id (str): Optional SSO configuration id (ssoId): return the group only if it came
124+
from that SSO configuration. Use the reserved id "default_ssoid" for the tenant's
125+
default SSO configuration.
106126
107127
Return value (dict):
108128
Return dict in the format
@@ -111,6 +131,7 @@ def load_all_group_members(
111131
"id": <group id>,
112132
"display": <display name>,
113133
"source": <"scim" or "jit">,
134+
"ssoId": <sso configuration id>,
114135
"members":[
115136
{
116137
"loginId": <loginId>,
@@ -125,11 +146,14 @@ def load_all_group_members(
125146
Raise:
126147
AuthException: raised if load operation fails
127148
"""
149+
body = {
150+
"tenantId": tenant_id,
151+
"groupId": group_id,
152+
}
153+
if sso_id is not None:
154+
body["ssoId"] = sso_id
128155
response = self._http.post(
129156
MgmtV1.group_load_all_group_members_path,
130-
body={
131-
"tenantId": tenant_id,
132-
"groupId": group_id,
133-
},
157+
body=body,
134158
)
135159
return response.json()

descope/management/group_async.py

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ class GroupAsync(AsyncHTTPBase):
1212
async def load_all_groups(
1313
self,
1414
tenant_id: str,
15+
sso_id: Optional[str] = None,
1516
) -> dict:
1617
"""
1718
Load all groups for a specific tenant id.
1819
1920
Args:
2021
tenant_id (str): Tenant ID to load groups from.
22+
sso_id (str): Optional SSO configuration id (ssoId) to load only groups that came from
23+
that SSO configuration. Use the reserved id "default_ssoid" for the tenant's default
24+
SSO configuration. When omitted, all the tenant's groups are returned.
2125
2226
Return value (dict):
2327
Return dict in the format
@@ -26,6 +30,7 @@ async def load_all_groups(
2630
"id": <group id>,
2731
"display": <display name>,
2832
"source": <"scim" or "jit">,
33+
"ssoId": <sso configuration id>,
2934
"members":[
3035
{
3136
"loginId": <loginId>,
@@ -40,11 +45,14 @@ async def load_all_groups(
4045
Raise:
4146
AuthException: raised if load operation fails
4247
"""
48+
body = {
49+
"tenantId": tenant_id,
50+
}
51+
if sso_id is not None:
52+
body["ssoId"] = sso_id
4353
response = await self._http.post(
4454
MgmtV1.group_load_all_path,
45-
body={
46-
"tenantId": tenant_id,
47-
},
55+
body=body,
4856
)
4957
return response.json()
5058

@@ -53,6 +61,7 @@ async def load_all_groups_for_members(
5361
tenant_id: str,
5462
user_ids: Optional[List[str]] = None,
5563
login_ids: Optional[List[str]] = None,
64+
sso_id: Optional[str] = None,
5665
) -> dict:
5766
"""
5867
Load all groups for the provided user IDs or login IDs.
@@ -61,6 +70,9 @@ async def load_all_groups_for_members(
6170
tenant_id (str): Tenant ID to load groups from.
6271
user_ids (List[str]): Optional List of user IDs, with the format of "U2J5ES9S8TkvCgOvcrkpzUgVTEBM" (example), which can be found on the user's JWT.
6372
login_ids (List[str]): Optional List of login IDs, how the users identify when logging in.
73+
sso_id (str): Optional SSO configuration id (ssoId) to load only groups that came from
74+
that SSO configuration. Use the reserved id "default_ssoid" for the tenant's default
75+
SSO configuration. When omitted, all matching groups are returned.
6476
6577
Return value (dict):
6678
Return dict in the format
@@ -69,6 +81,7 @@ async def load_all_groups_for_members(
6981
"id": <group id>,
7082
"display": <display name>,
7183
"source": <"scim" or "jit">,
84+
"ssoId": <sso configuration id>,
7285
"members":[
7386
{
7487
"loginId": <loginId>,
@@ -86,27 +99,34 @@ async def load_all_groups_for_members(
8699
user_ids = [] if user_ids is None else user_ids
87100
login_ids = [] if login_ids is None else login_ids
88101

102+
body = {
103+
"tenantId": tenant_id,
104+
"loginIds": login_ids,
105+
"userIds": user_ids,
106+
}
107+
if sso_id is not None:
108+
body["ssoId"] = sso_id
89109
response = await self._http.post(
90110
MgmtV1.group_load_all_for_member_path,
91-
body={
92-
"tenantId": tenant_id,
93-
"loginIds": login_ids,
94-
"userIds": user_ids,
95-
},
111+
body=body,
96112
)
97113
return response.json()
98114

99115
async def load_all_group_members(
100116
self,
101117
tenant_id: str,
102118
group_id: str,
119+
sso_id: Optional[str] = None,
103120
) -> dict:
104121
"""
105122
Load all members of the provided group id.
106123
107124
Args:
108125
tenant_id (str): Tenant ID to load groups from.
109126
group_id (str): Group ID to load members for.
127+
sso_id (str): Optional SSO configuration id (ssoId): return the group only if it came
128+
from that SSO configuration. Use the reserved id "default_ssoid" for the tenant's
129+
default SSO configuration.
110130
111131
Return value (dict):
112132
Return dict in the format
@@ -115,6 +135,7 @@ async def load_all_group_members(
115135
"id": <group id>,
116136
"display": <display name>,
117137
"source": <"scim" or "jit">,
138+
"ssoId": <sso configuration id>,
118139
"members":[
119140
{
120141
"loginId": <loginId>,
@@ -129,11 +150,14 @@ async def load_all_group_members(
129150
Raise:
130151
AuthException: raised if load operation fails
131152
"""
153+
body = {
154+
"tenantId": tenant_id,
155+
"groupId": group_id,
156+
}
157+
if sso_id is not None:
158+
body["ssoId"] = sso_id
132159
response = await self._http.post(
133160
MgmtV1.group_load_all_group_members_path,
134-
body={
135-
"tenantId": tenant_id,
136-
"groupId": group_id,
137-
},
161+
body=body,
138162
)
139163
return response.json()

tests/management/test_group.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,33 @@ async def test_load_all_groups(self, client_factory):
3535
follow_redirects=False,
3636
)
3737

38+
async def test_load_all_groups_with_sso_id(self, client_factory):
39+
client = client_factory.make(PROJECT_ID, PUBLIC_KEY_DICT, False, "key")
40+
41+
# sso_id scopes the load to one SSO configuration; it is sent only when provided
42+
# (the exact-match json assertion in test_load_all_groups proves it is absent otherwise)
43+
with client.mock_mgmt_post(make_response({})) as mock_post:
44+
assert (
45+
await client.invoke(client.mgmt.group.load_all_groups("someTenantId", sso_id="sso-config-1"))
46+
is not None
47+
)
48+
assert_http_called(
49+
mock_post,
50+
client.mode,
51+
f"{DEFAULT_BASE_URL}{MgmtV1.group_load_all_path}",
52+
headers={
53+
**default_headers,
54+
"Authorization": f"Bearer {PROJECT_ID}:key",
55+
"x-descope-project-id": PROJECT_ID,
56+
},
57+
params=None,
58+
json={
59+
"tenantId": "someTenantId",
60+
"ssoId": "sso-config-1",
61+
},
62+
follow_redirects=False,
63+
)
64+
3865
async def test_load_all_groups_for_members(self, client_factory):
3966
client = client_factory.make(PROJECT_ID, PUBLIC_KEY_DICT, False, "key")
4067

0 commit comments

Comments
 (0)