Skip to content

Commit 0ca8bed

Browse files
Merge pull request #87 from kinde-oss/fix/management-client-mapping-and-claims
fix(management,auth): resolve mapping and claims logic issues
2 parents 4f40960 + 0ebe33e commit 0ca8bed

5 files changed

Lines changed: 357 additions & 13 deletions

File tree

kinde_sdk/auth/claims.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ async def get_claim(self, claim_name: str, token_type: str = "access_token") ->
2424
"value": None
2525
}
2626

27-
claims = token_manager.get_claims()
27+
claims = token_manager.get_claims(token_type)
2828
value = claims.get(claim_name)
2929

3030
return {
@@ -46,7 +46,7 @@ async def get_all_claims(self, token_type: str = "access_token") -> Dict[str, An
4646
if not token_manager:
4747
return {}
4848

49-
return token_manager.get_claims()
49+
return token_manager.get_claims(token_type)
5050

5151
# Create a singleton instance
5252
claims = Claims()

kinde_sdk/auth/token_manager.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,17 +153,46 @@ def get_id_token(self):
153153
"""Get the ID token if available."""
154154
return self.tokens.get("id_token")
155155

156-
def get_claims(self):
157-
"""Get the claims from the access token if available, falling back to ID token claims."""
158-
# First try to get claims from access token
159-
claims = self.tokens.get("access_token_claims", {})
156+
def get_claims(self, token_type: str = "access_token"):
157+
"""Get the claims from the specified token type.
158+
159+
Args:
160+
token_type (str): The type of token to get claims from.
161+
Valid values are "access_token" or "id_token".
162+
163+
Returns:
164+
dict: The claims from the specified token, or empty dict if not available.
165+
"""
166+
# Validate token type
167+
valid_token_types = ["access_token", "id_token"]
168+
if token_type not in valid_token_types:
169+
logging.warning(f"Invalid token_type '{token_type}'. Valid types are: {valid_token_types}")
170+
return {}
171+
172+
# Use f-string for safer string formatting
173+
claims_key = f"{token_type}_claims"
174+
claims = self.tokens.get(claims_key, {})
175+
160176
if not claims:
161-
# Fall back to ID token claims if access token claims are not available
162-
claims = self.tokens.get("id_token_claims", {})
163-
if not claims:
164-
logging.warning("No claims available in token manager")
177+
logging.warning(f"No claims available for token type: {token_type}")
178+
165179
return claims
166180

181+
def get_claim(self, key: str, token_type: str = "access_token"):
182+
"""Get a specific claim from the specified token type.
183+
184+
Args:
185+
key (str): The claim key to retrieve
186+
token_type (str): The type of token to get the claim from.
187+
Valid values are "access_token" or "id_token".
188+
189+
Returns:
190+
dict: Dictionary containing the claim name and value, or empty dict if not found.
191+
"""
192+
claims = self.get_claims(token_type)
193+
value = claims.get(key)
194+
return {"name": key, "value": value}
195+
167196
def revoke_token(self):
168197
""" Revoke the current access token. """
169198
if "access_token" not in self.tokens:

kinde_sdk/management/management_client.py

Lines changed: 198 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,28 @@ class ManagementClient:
4343
'delete': ('DELETE', '/api/v1/organizations/{org_code}'),
4444
},
4545

46+
# Organization Users API
47+
'organization_users': {
48+
'list': ('GET', '/api/v1/organizations/{org_code}/users'),
49+
'add': ('POST', '/api/v1/organizations/{org_code}/users'),
50+
'update': ('PATCH', '/api/v1/organizations/{org_code}/users'),
51+
'remove': ('DELETE', '/api/v1/organizations/{org_code}/users/{user_id}'),
52+
},
53+
54+
# Organization User Roles API
55+
'organization_user_roles': {
56+
'list': ('GET', '/api/v1/organizations/{org_code}/users/{user_id}/roles'),
57+
'add': ('POST', '/api/v1/organizations/{org_code}/users/{user_id}/roles'),
58+
'remove': ('DELETE', '/api/v1/organizations/{org_code}/users/{user_id}/roles/{role_id}'),
59+
},
60+
61+
# Organization User Permissions API
62+
'organization_user_permissions': {
63+
'list': ('GET', '/api/v1/organizations/{org_code}/users/{user_id}/permissions'),
64+
'add': ('POST', '/api/v1/organizations/{org_code}/users/{user_id}/permissions'),
65+
'remove': ('DELETE', '/api/v1/organizations/{org_code}/users/{user_id}/permissions/{permission_id}'),
66+
},
67+
4668
# Roles API
4769
'roles': {
4870
'list': ('GET', '/api/v1/roles'),
@@ -103,6 +125,94 @@ class ManagementClient:
103125
'industries': {
104126
'list': ('GET', '/api/v1/industries'),
105127
},
128+
129+
# Properties API
130+
'properties': {
131+
'list': ('GET', '/api/v1/properties'),
132+
'get': ('GET', '/api/v1/properties/{property_id}'),
133+
'create': ('POST', '/api/v1/properties'),
134+
'update': ('PATCH', '/api/v1/properties/{property_id}'),
135+
'delete': ('DELETE', '/api/v1/properties/{property_id}'),
136+
},
137+
138+
# User Properties API
139+
'user_properties': {
140+
'list': ('GET', '/api/v1/users/{user_id}/properties'),
141+
'get': ('GET', '/api/v1/users/{user_id}/properties/{property_key}'),
142+
'update': ('PUT', '/api/v1/users/{user_id}/properties/{property_key}'),
143+
},
144+
145+
# Organization Properties API
146+
'organization_properties': {
147+
'list': ('GET', '/api/v1/organizations/{org_code}/properties'),
148+
'get': ('GET', '/api/v1/organizations/{org_code}/properties/{property_key}'),
149+
'update': ('PUT', '/api/v1/organizations/{org_code}/properties/{property_key}'),
150+
},
151+
152+
# Webhooks API
153+
'webhooks': {
154+
'list': ('GET', '/api/v1/webhooks'),
155+
'get': ('GET', '/api/v1/webhooks/{webhook_id}'),
156+
'create': ('POST', '/api/v1/webhooks'),
157+
'update': ('PATCH', '/api/v1/webhooks/{webhook_id}'),
158+
'delete': ('DELETE', '/api/v1/webhooks/{webhook_id}'),
159+
},
160+
161+
# Events API
162+
'events': {
163+
'get': ('GET', '/api/v1/events/{event_id}'),
164+
},
165+
166+
# Event Types API
167+
'event_types': {
168+
'list': ('GET', '/api/v1/event_types'),
169+
},
170+
171+
# Connections API
172+
'connections': {
173+
'list': ('GET', '/api/v1/connections'),
174+
'get': ('GET', '/api/v1/connections/{connection_id}'),
175+
'create': ('POST', '/api/v1/connections'),
176+
'update': ('PATCH', '/api/v1/connections/{connection_id}'),
177+
'delete': ('DELETE', '/api/v1/connections/{connection_id}'),
178+
},
179+
180+
# Business API
181+
'business': {
182+
'get': ('GET', '/api/v1/business'),
183+
},
184+
185+
# Environment Feature Flags API
186+
'environment_feature_flags': {
187+
'list': ('GET', '/api/v1/environment/feature_flags'),
188+
'get': ('GET', '/api/v1/environment/feature_flags/{feature_flag_key}'),
189+
'update': ('PATCH', '/api/v1/environment/feature_flags/{feature_flag_key}'),
190+
'delete': ('DELETE', '/api/v1/environment/feature_flags/{feature_flag_key}'),
191+
},
192+
193+
# Organization Feature Flags API
194+
'organization_feature_flags': {
195+
'list': ('GET', '/api/v1/organizations/{org_code}/feature_flags'),
196+
'get': ('GET', '/api/v1/organizations/{org_code}/feature_flags/{feature_flag_key}'),
197+
'update': ('PATCH', '/api/v1/organizations/{org_code}/feature_flags/{feature_flag_key}'),
198+
'delete': ('DELETE', '/api/v1/organizations/{org_code}/feature_flags/{feature_flag_key}'),
199+
},
200+
201+
# User Feature Flags API
202+
'user_feature_flags': {
203+
'get': ('GET', '/api/v1/users/{user_id}/feature_flags/{feature_flag_key}'),
204+
'update': ('PATCH', '/api/v1/users/{user_id}/feature_flags/{feature_flag_key}'),
205+
},
206+
207+
# User Password API
208+
'user_password': {
209+
'update': ('PUT', '/api/v1/users/{user_id}/password'),
210+
},
211+
212+
# User Refresh Claims API
213+
'user_refresh_claims': {
214+
'refresh': ('POST', '/api/v1/users/{user_id}/refresh_claims'),
215+
},
106216
}
107217

108218
# Define response types for each endpoint
@@ -121,6 +231,22 @@ class ManagementClient:
121231
'update': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
122232
'delete': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
123233
},
234+
'organization_users': {
235+
'list': {'200': 'GetOrganizationUsersResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
236+
'add': {'201': 'AddOrganizationUsersResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
237+
'update': {'200': 'UpdateOrganizationUsersResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
238+
'remove': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
239+
},
240+
'organization_user_roles': {
241+
'list': {'200': 'GetOrganizationsUserRolesResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
242+
'add': {'201': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
243+
'remove': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
244+
},
245+
'organization_user_permissions': {
246+
'list': {'200': 'GetOrganizationsUserPermissionsResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
247+
'add': {'201': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
248+
'remove': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
249+
},
124250
'roles': {
125251
'list': {'200': 'GetRolesResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
126252
'get': {'200': 'GetRoleResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
@@ -166,6 +292,68 @@ class ManagementClient:
166292
'industries': {
167293
'list': {'200': 'GetIndustriesResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
168294
},
295+
'properties': {
296+
'list': {'200': 'GetPropertiesResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
297+
'get': {'200': 'GetPropertyResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
298+
'create': {'201': 'CreatePropertyResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
299+
'update': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
300+
'delete': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
301+
},
302+
'user_properties': {
303+
'list': {'200': 'GetUserPropertiesResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
304+
'get': {'200': 'GetUserPropertyResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
305+
'update': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
306+
},
307+
'organization_properties': {
308+
'list': {'200': 'GetOrganizationPropertiesResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
309+
'get': {'200': 'GetOrganizationPropertyResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
310+
'update': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
311+
},
312+
'webhooks': {
313+
'list': {'200': 'GetWebhooksResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
314+
'get': {'200': 'GetWebhookResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
315+
'create': {'201': 'CreateWebhookResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
316+
'update': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
317+
'delete': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
318+
},
319+
'events': {
320+
'get': {'200': 'GetEventResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
321+
},
322+
'event_types': {
323+
'list': {'200': 'GetEventTypesResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
324+
},
325+
'connections': {
326+
'list': {'200': 'GetConnectionsResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
327+
'get': {'200': 'GetConnectionResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
328+
'create': {'201': 'CreateConnectionResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
329+
'update': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
330+
'delete': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
331+
},
332+
'business': {
333+
'get': {'200': 'GetBusinessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
334+
},
335+
'environment_feature_flags': {
336+
'list': {'200': 'GetEnvironmentFeatureFlagsResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
337+
'get': {'200': 'GetEnvironmentFeatureFlagsResponseDataFeatureFlagsInner', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
338+
'update': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
339+
'delete': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
340+
},
341+
'organization_feature_flags': {
342+
'list': {'200': 'GetOrganizationFeatureFlagsResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '429': 'ErrorResponse'},
343+
'get': {'200': 'GetOrganizationFeatureFlagsResponseDataFeatureFlagsInner', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
344+
'update': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
345+
'delete': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
346+
},
347+
'user_feature_flags': {
348+
'get': {'200': 'GetUserFeatureFlagsResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
349+
'update': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
350+
},
351+
'user_password': {
352+
'update': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
353+
},
354+
'user_refresh_claims': {
355+
'refresh': {'200': 'SuccessResponse', '400': 'ErrorResponse', '403': 'ErrorResponse', '404': 'ErrorResponse', '429': 'ErrorResponse'},
356+
},
169357
}
170358

171359
def __init__(self, domain: str, client_id: str, client_secret: str):
@@ -199,7 +387,11 @@ def _setup_token_handling(self):
199387
def _generate_methods(self):
200388
"""Generate dynamic methods for each API endpoint."""
201389
for resource, endpoints in self.API_ENDPOINTS.items():
202-
resource_singular = resource[:-1] if resource.endswith('s') else resource
390+
# Handle special cases for singularization
391+
if resource == 'business':
392+
resource_singular = 'business' # Don't remove 's' from 'business'
393+
else:
394+
resource_singular = resource[:-1] if resource.endswith('s') else resource
203395

204396
for action, (method, path) in endpoints.items():
205397
# Create method name based on action and resource
@@ -229,7 +421,11 @@ def _create_api_method(self, http_method: str, path: str, resource: str, action:
229421
Returns:
230422
A callable method that makes the API request
231423
"""
232-
resource_singular = resource[:-1] if resource.endswith('s') else resource
424+
# Handle special cases for singularization
425+
if resource == 'business':
426+
resource_singular = 'business' # Don't remove 's' from 'business'
427+
else:
428+
resource_singular = resource[:-1] if resource.endswith('s') else resource
233429

234430
def api_method(*args, **kwargs) -> Dict[str, Any]:
235431
# Format path with any path parameters from args

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "kinde-python-sdk"
3-
version = "2.0.6"
3+
version = "2.0.8"
44
authors = [
55
{ name = "Kinde Engineering", email = "engineering@kinde.com" },
66
]

0 commit comments

Comments
 (0)