auth-gateway-serverkit 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,645 @@
1
+ """ Keycloak Client API Module for the auth gateway serverkit."""
2
+ import os
3
+ import json
4
+ import aiohttp
5
+ import httpx
6
+ from .config import settings
7
+ from ..logger import init_logger
8
+
9
+ logger = init_logger("serverkit.keycloak.api")
10
+
11
+
12
+ async def retrieve_client_token(user_name, password):
13
+ """
14
+ Retrieve a token from Keycloak using the Resource Owner Password Credentials Grant.
15
+
16
+ Args:
17
+ user_name (str): The username of the user.
18
+ password (str): The password of the user.
19
+
20
+ Returns:
21
+ dict: A dictionary containing the access token and other token details.
22
+ """
23
+ try:
24
+ if settings.CLIENT_SECRET:
25
+ client_secret = settings.CLIENT_SECRET
26
+ else:
27
+ logger.info("Fetching client secret from Keycloak")
28
+ client_secret = await get_client_secret()
29
+ settings.CLIENT_SECRET = client_secret
30
+ if not client_secret:
31
+ logger.error("Failed to get client secret")
32
+ return None
33
+
34
+ url = f"{settings.SERVER_URL}/realms/{settings.REALM}/protocol/openid-connect/token"
35
+ headers = {
36
+ "Content-Type": "application/x-www-form-urlencoded"
37
+ }
38
+ payload = {
39
+ "username": user_name,
40
+ "password": password,
41
+ "grant_type": "password",
42
+ "scope": "openid",
43
+ "client_id": settings.CLIENT_ID,
44
+ "client_secret": client_secret,
45
+ }
46
+ async with httpx.AsyncClient(timeout=20) as client:
47
+ response = await client.post(url, data=payload, headers=headers)
48
+ return response
49
+ except Exception as e:
50
+ logger.error(f"Request error: {e}")
51
+ return None
52
+
53
+
54
+ async def get_admin_token() -> str | None:
55
+ """
56
+ Retrieve an admin token from Keycloak using the bootstrap admin credentials.
57
+ :return: Access token if successful, None otherwise
58
+ """
59
+ url = f"{settings.SERVER_URL}/realms/master/protocol/openid-connect/token"
60
+ payload = {
61
+ 'username': settings.KC_BOOTSTRAP_ADMIN_USERNAME,
62
+ 'password': settings.KC_BOOTSTRAP_ADMIN_PASSWORD,
63
+ 'grant_type': 'password',
64
+ 'client_id': 'admin-cli'
65
+ }
66
+ try:
67
+ async with aiohttp.ClientSession() as session:
68
+ async with session.post(url, data=payload) as response:
69
+ if response.status == 200:
70
+ data = await response.json()
71
+ return data['access_token']
72
+ else:
73
+ logger.error(f"Failed to get admin token. Status: {response.status}, Response: {await response.text()}")
74
+ return None
75
+ except aiohttp.ClientError as e:
76
+ logger.error(f"Connection error while getting admin token: {e}")
77
+ return None
78
+
79
+
80
+ async def get_client_uuid(admin_token) -> str | None:
81
+ """
82
+ Retrieve the UUID of the client with the specified clientId from Keycloak.
83
+ :param admin_token:
84
+ :return: Client UUID if found, None otherwise
85
+ """
86
+ url = f"{settings.SERVER_URL}/admin/realms/{settings.REALM}/clients?clientId={settings.CLIENT_ID}"
87
+ headers = {'Authorization': f'Bearer {admin_token}', 'Content-Type': 'application/json'}
88
+ async with aiohttp.ClientSession() as session:
89
+ async with session.get(url, headers=headers) as response:
90
+ if response.status == 200:
91
+ clients = await response.json()
92
+ if clients:
93
+ return clients[0]['id'] # UUID of the client
94
+ logger.error(f"Failed to find client UUID for clientId '{settings.CLIENT_ID}'. Status: {response.status}")
95
+ return None
96
+
97
+
98
+ async def get_client_secret() -> str | None:
99
+ """
100
+ Retrieve the client secret for the specified client in Keycloak.
101
+ This function first obtains an admin token, then retrieves the client UUID,
102
+ and finally fetches the client secret using the UUID.
103
+ :return: Client secret if found, None otherwise
104
+ """
105
+ try:
106
+ # Step 1: Obtain the admin token
107
+ admin_token = await get_admin_token()
108
+ if not admin_token:
109
+ logger.error("Unable to obtain admin token.")
110
+ return None
111
+
112
+ # Step 2: Retrieve the client UUID using the existing get_client_uuid function
113
+ client_uuid = await get_client_uuid(admin_token)
114
+ if not client_uuid:
115
+ logger.error(f"Unable to retrieve UUID for client_id: {settings.CLIENT_ID}")
116
+ return None
117
+
118
+ # Step 3: Fetch the client secret using the client UUID
119
+ secret_url = f"{settings.SERVER_URL}/admin/realms/{settings.REALM}/clients/{client_uuid}/client-secret"
120
+ headers = {
121
+ "Authorization": f"Bearer {admin_token}",
122
+ "Content-Type": "application/json"
123
+ }
124
+ async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=20)) as session:
125
+ async with session.get(secret_url, headers=headers) as secret_response:
126
+ if secret_response.status == 200:
127
+ secret_data = await secret_response.json()
128
+ client_secret = secret_data.get('value')
129
+
130
+ if not client_secret:
131
+ logger.error("Client secret not found in the response.")
132
+ return None
133
+
134
+ return client_secret
135
+ else:
136
+ response_text = await secret_response.text()
137
+ logger.error(f"Error fetching client secret: {response_text}")
138
+ return None
139
+
140
+ except aiohttp.ClientError as e:
141
+ logger.error(f"HTTP ClientError occurred while retrieving client secret: {e}")
142
+ return None
143
+ except Exception as e:
144
+ logger.error(f"Exception occurred while retrieving client secret: {e}")
145
+ return None
146
+
147
+
148
+ async def get_resource_id(resource_name, admin_token, client_uuid) -> str | None:
149
+ """
150
+ Retrieve the resource ID for a given resource name.
151
+ :param resource_name:
152
+ :param admin_token:
153
+ :param client_uuid:
154
+ :return: Resource ID if found, None otherwise
155
+ """
156
+ headers = {
157
+ 'Authorization': f'Bearer {admin_token}',
158
+ 'Content-Type': 'application/json'
159
+ }
160
+ url = f"{settings.SERVER_URL}/admin/realms/{settings.REALM}/clients/{client_uuid}/authz/resource-server/resource"
161
+ try:
162
+ async with aiohttp.ClientSession() as session:
163
+ async with session.get(url, headers=headers) as response:
164
+ if response.status == 200:
165
+ resources = await response.json()
166
+ for resource in resources:
167
+ if resource['name'] == resource_name:
168
+ return resource['_id']
169
+ else:
170
+ logger.error(f"Failed to fetch resources. Status: {response.status}, Response: {await response.text()}")
171
+ except aiohttp.ClientError as e:
172
+ logger.error(f"Connection error while retrieving resource ID for '{resource_name}': {e}")
173
+ return None
174
+
175
+
176
+ async def set_frontend_url(admin_token) -> bool:
177
+ """
178
+ Set the frontend URL for the Keycloak realm.
179
+ :param admin_token:
180
+ :return: True if successful, False otherwise
181
+ """
182
+ frontend_url = settings.KEYCLOAK_FRONTEND_URL
183
+ if not frontend_url:
184
+ logger.error("KEYCLOAK_FRONTEND_URL is not set")
185
+ return False
186
+
187
+ headers = {'Authorization': f'Bearer {admin_token}', 'Content-Type': 'application/json'}
188
+ url = f"{settings.SERVER_URL}/admin/realms/{settings.REALM}"
189
+ payload = {'attributes': {'frontendUrl': frontend_url}}
190
+ async with aiohttp.ClientSession() as session:
191
+ async with session.put(url, headers=headers, json=payload) as response:
192
+ if response.status == 204:
193
+ logger.info(f"Frontend URL set to {frontend_url}")
194
+ return True
195
+ logger.error(f"Failed to set Frontend URL. Status: {response.status}, Response: {await response.text()}")
196
+ return False
197
+
198
+
199
+ async def get_assigned_client_scopes(admin_token, client_uuid) -> list:
200
+ """
201
+ Retrieve default and optional client scopes assigned to a particular client.
202
+ :param admin_token:
203
+ :param client_uuid:
204
+ :return: List of assigned client scopes
205
+ """
206
+ headers = {
207
+ 'Authorization': f'Bearer {admin_token}',
208
+ 'Content-Type': 'application/json'
209
+ }
210
+
211
+ # Endpoint to list all scopes assigned to a client
212
+ url = f"{settings.SERVER_URL}/admin/realms/{settings.REALM}/clients/{client_uuid}/default-client-scopes"
213
+
214
+ async with aiohttp.ClientSession() as session:
215
+ async with session.get(url, headers=headers) as response:
216
+ if response.status == 200:
217
+ return await response.json()
218
+ else:
219
+ logger.error(
220
+ f"Failed to retrieve default client scopes. "
221
+ f"Status: {response.status}, Response: {await response.text()}"
222
+ )
223
+ return []
224
+
225
+
226
+ async def get_optional_client_scopes(admin_token, client_uuid) -> list:
227
+ """
228
+ Retrieve optional client scopes assigned to a particular client.
229
+ :param admin_token:
230
+ :param client_uuid:
231
+ :return: List of optional client scopes
232
+ """
233
+ headers = {
234
+ 'Authorization': f'Bearer {admin_token}',
235
+ 'Content-Type': 'application/json'
236
+ }
237
+
238
+ url = f"{settings.SERVER_URL}/admin/realms/{settings.REALM}/clients/{client_uuid}/optional-client-scopes"
239
+
240
+ async with aiohttp.ClientSession() as session:
241
+ async with session.get(url, headers=headers) as response:
242
+ if response.status == 200:
243
+ return await response.json()
244
+ else:
245
+ logger.error(
246
+ f"Failed to retrieve optional client scopes. "
247
+ f"Status: {response.status}, Response: {await response.text()}"
248
+ )
249
+ return []
250
+
251
+
252
+ async def remove_default_scopes(admin_token, client_uuid, scopes_to_remove=None) -> bool:
253
+ """
254
+ Removes specified scopes (e.g. 'email', 'profile', 'roles') from both
255
+ default and optional client scopes.
256
+ :param admin_token: Admin token for authentication
257
+ :param client_uuid: UUID of the client
258
+ :param scopes_to_remove: Set of scopes to remove
259
+ :return: True if successful, False otherwise
260
+ """
261
+ if scopes_to_remove is None:
262
+ scopes_to_remove = {"email", "profile"}
263
+
264
+ headers = {
265
+ 'Authorization': f'Bearer {admin_token}',
266
+ 'Content-Type': 'application/json'
267
+ }
268
+
269
+ base_url = f"{settings.SERVER_URL}/admin/realms/{settings.REALM}/clients/{client_uuid}"
270
+
271
+ # 1. Retrieve default client scopes
272
+ default_scopes = await get_assigned_client_scopes(admin_token, client_uuid)
273
+ # 2. Retrieve optional client scopes
274
+ optional_scopes = await get_optional_client_scopes(admin_token, client_uuid)
275
+
276
+ success = True
277
+
278
+ async with aiohttp.ClientSession() as session:
279
+ # Remove from default scopes
280
+ for scope in default_scopes:
281
+ if scope["name"] in scopes_to_remove:
282
+ scope_id = scope["id"]
283
+ remove_url = f"{base_url}/default-client-scopes/{scope_id}"
284
+ async with session.delete(remove_url, headers=headers) as resp:
285
+ if resp.status == 204:
286
+ logger.info(f"Removed default client scope '{scope['name']}' successfully.")
287
+ else:
288
+ logger.error(
289
+ f"Failed to remove default client scope '{scope['name']}'. "
290
+ f"Status: {resp.status}, Response: {await resp.text()}"
291
+ )
292
+ success = False
293
+
294
+ # Remove from optional scopes
295
+ for scope in optional_scopes:
296
+ if scope["name"] in scopes_to_remove:
297
+ scope_id = scope["id"]
298
+ remove_url = f"{base_url}/optional-client-scopes/{scope_id}"
299
+ async with session.delete(remove_url, headers=headers) as resp:
300
+ if resp.status == 204:
301
+ logger.info(f"Removed optional client scope '{scope['name']}' successfully.")
302
+ else:
303
+ logger.error(
304
+ f"Failed to remove optional client scope '{scope['name']}'. "
305
+ f"Status: {resp.status}, Response: {await resp.text()}"
306
+ )
307
+ success = False
308
+
309
+ return success
310
+
311
+
312
+ async def create_realm(admin_token) -> bool:
313
+ """
314
+ Create a new realm in Keycloak.
315
+ :param admin_token:
316
+ :return: True if successful, False otherwise
317
+ """
318
+
319
+ url = f"{settings.SERVER_URL}/admin/realms"
320
+ headers = {
321
+ 'Authorization': f'Bearer {admin_token}',
322
+ 'Content-Type': 'application/json'
323
+ }
324
+ payload = {
325
+ 'realm': settings.REALM,
326
+ 'enabled': True,
327
+ 'accessTokenLifespan': 36000, # Set token lifespan to 10 hours (in seconds)
328
+ }
329
+ try:
330
+ async with aiohttp.ClientSession() as session:
331
+ async with session.post(url, headers=headers, json=payload) as response:
332
+ if response.status == 201:
333
+ logger.info(f"Realm '{settings.REALM}' created successfully")
334
+ return True
335
+ elif response.status == 409:
336
+ logger.info(f"Realm '{settings.REALM}' already exists")
337
+ return True
338
+ else:
339
+ logger.error(f"Failed to create realm. Status: {response.status}, Response: {await response.text()}")
340
+ return False
341
+ except aiohttp.ClientError as e:
342
+ logger.error(f"Connection error while creating realm: {e}")
343
+ return False
344
+
345
+
346
+ async def create_client(admin_token) -> bool:
347
+ """
348
+ Create a new client in Keycloak.
349
+ :param admin_token:
350
+ :return: True if successful, False otherwise
351
+ """
352
+
353
+ url = f"{settings.SERVER_URL}/admin/realms/{settings.REALM}/clients"
354
+ headers = {
355
+ 'Authorization': f'Bearer {admin_token}',
356
+ 'Content-Type': 'application/json'
357
+ }
358
+
359
+ payload = {
360
+ 'clientId': settings.CLIENT_ID,
361
+ 'name': settings.CLIENT_ID,
362
+ 'enabled': True,
363
+ 'publicClient': False, # Must be False for Authorization Services
364
+ 'protocol': 'openid-connect',
365
+ 'redirectUris': ['*'], # Update based on your app's requirements
366
+ 'webOrigins': ['*'],
367
+ 'directAccessGrantsEnabled': True,
368
+ 'serviceAccountsEnabled': True, # REQUIRED for Authorization Services
369
+ 'standardFlowEnabled': True,
370
+ 'implicitFlowEnabled': False,
371
+ 'authorizationServicesEnabled': True, # Enable Authorization Services
372
+ }
373
+
374
+ try:
375
+ async with aiohttp.ClientSession() as session:
376
+ async with session.post(url, headers=headers, json=payload) as response:
377
+ if response.status == 201:
378
+ logger.info(f"Client '{settings.CLIENT_ID}' created successfully")
379
+ return True
380
+ elif response.status == 409:
381
+ logger.info(f"Client '{settings.CLIENT_ID}' already exists")
382
+ return True
383
+ else:
384
+ logger.error(f"Failed to create client. Status: {response.status}, Response: {await response.text()}")
385
+ return False
386
+ except aiohttp.ClientError as e:
387
+ logger.error(f"Connection error while creating client: {e}")
388
+ return False
389
+
390
+
391
+ async def create_realm_roles(admin_token) -> bool:
392
+ """
393
+ Create realm roles in Keycloak based on the configuration file.
394
+ :param admin_token:
395
+ :return: True if successful, False otherwise
396
+ """
397
+ config_path = os.path.join(os.getcwd(), "keycloak_config.json")
398
+ if not os.path.exists(config_path):
399
+ logger.error("Configuration file not found")
400
+ return False
401
+
402
+ with open(config_path, 'r') as file:
403
+ config = json.load(file)
404
+
405
+ roles_to_create = config.get("realm_roles", [])
406
+ if not roles_to_create:
407
+ logger.warning("No realm roles defined in the configuration")
408
+ return True # Nothing to create, but not a failure
409
+
410
+ headers = {
411
+ 'Authorization': f'Bearer {admin_token}',
412
+ 'Content-Type': 'application/json'
413
+ }
414
+
415
+ success = True
416
+ for role in roles_to_create:
417
+ url = f"{settings.SERVER_URL}/admin/realms/{settings.REALM}/roles"
418
+ payload = {
419
+ 'name': role['name'],
420
+ 'description': role.get('description', ''),
421
+ 'composite': False,
422
+ 'clientRole': False
423
+ }
424
+ try:
425
+ async with aiohttp.ClientSession() as session:
426
+ async with session.post(url, headers=headers, json=payload) as response:
427
+ if response.status == 201:
428
+ logger.info(f"Role '{role['name']}' created successfully in realm '{settings.REALM}'")
429
+ elif response.status == 409:
430
+ logger.info(f"Role '{role['name']}' already exists in realm '{settings.REALM}'")
431
+ # Optionally update the role description if it already exists
432
+ # await update_role_description(role['name'], role.get('description', ''), headers)
433
+ else:
434
+ logger.error(f"Failed to create role '{role['name']}'. Status: {response.status}, Response: {await response.text()}")
435
+ success = False
436
+ except aiohttp.ClientError as e:
437
+ logger.error(f"Connection error while creating role '{role['name']}': {e}")
438
+ success = False
439
+
440
+ return success
441
+
442
+
443
+ async def enable_edit_username(admin_token) -> bool:
444
+ """
445
+ Enable the option to edit usernames in the Keycloak realm.
446
+ :param admin_token:
447
+ :return: True if successful, False otherwise
448
+ """
449
+
450
+ url = f"{settings.SERVER_URL}/admin/realms/{settings.REALM}"
451
+ headers = {
452
+ 'Authorization': f'Bearer {admin_token}',
453
+ 'Content-Type': 'application/json'
454
+ }
455
+
456
+ payload = {
457
+ "realm": settings.REALM,
458
+ "editUsernameAllowed": True # Enable editing the username
459
+ }
460
+
461
+ try:
462
+ async with aiohttp.ClientSession() as session:
463
+ async with session.put(url, headers=headers, json=payload) as response:
464
+ if response.status == 204:
465
+ logger.info(f"Enabled edit username for realm '{settings.REALM}' successfully")
466
+ return True
467
+ else:
468
+ error_text = await response.text()
469
+ logger.error(f"Failed to enable edit username. Status: {response.status}, Response: {error_text}")
470
+ return False
471
+ except aiohttp.ClientError as e:
472
+ logger.error(f"Connection error while enabling edit username: {e}")
473
+ return False
474
+
475
+
476
+ async def add_audience_protocol_mapper(admin_token) -> bool:
477
+ """
478
+ Add an audience protocol mapper to the client in Keycloak.
479
+ :param admin_token:
480
+ :return: True if successful, False otherwise
481
+ """
482
+
483
+ headers = {
484
+ 'Authorization': f'Bearer {admin_token}',
485
+ 'Content-Type': 'application/json'
486
+ }
487
+
488
+ # First, get the client ID (UUID) for your client
489
+ url = f"{settings.SERVER_URL}/admin/realms/{settings.REALM}/clients?clientId={settings.CLIENT_ID}"
490
+ try:
491
+ async with aiohttp.ClientSession() as session:
492
+ async with session.get(url, headers=headers) as response:
493
+ if response.status == 200:
494
+ clients = await response.json()
495
+ if clients:
496
+ client_uuid = clients[0]['id']
497
+ else:
498
+ logger.error(f"Client '{settings.CLIENT_ID}' not found")
499
+ return False
500
+ else:
501
+ logger.error(f"Failed to retrieve client. Status: {response.status}, Response: {await response.text()}")
502
+ return False
503
+
504
+ # Now, add the Protocol Mapper to the client
505
+ url = f"{settings.SERVER_URL}/admin/realms/{settings.REALM}/clients/{client_uuid}/protocol-mappers/models"
506
+ payload = {
507
+ "name": "audience",
508
+ "protocol": "openid-connect",
509
+ "protocolMapper": "oidc-audience-mapper",
510
+ "consentRequired": False,
511
+ "config": {
512
+ "included.client.audience": settings.CLIENT_ID,
513
+ "id.token.claim": "true",
514
+ "access.token.claim": "true",
515
+ "claim.name": "aud",
516
+ "userinfo.token.claim": "false"
517
+ }
518
+ }
519
+ async with session.post(url, headers=headers, json=payload) as response:
520
+ if response.status == 201:
521
+ logger.info(f"Audience Protocol Mapper added successfully to client '{settings.CLIENT_ID}'")
522
+ return True
523
+ elif response.status == 409:
524
+ logger.info(f"Audience Protocol Mapper already exists for client '{settings.CLIENT_ID}'")
525
+ return True
526
+ else:
527
+ logger.error(f"Failed to add Audience Protocol Mapper. Status: {response.status}, Response: {await response.text()}")
528
+ return False
529
+ except aiohttp.ClientError as e:
530
+ logger.error(f"Connection error while adding Audience Protocol Mapper: {e}")
531
+ return False
532
+
533
+
534
+ async def create_policy(policy_name, description, roles, admin_token, client_uuid) -> bool:
535
+ """
536
+ Create a new policy in Keycloak.
537
+ :param policy_name:
538
+ :param description:
539
+ :param roles:
540
+ :param admin_token:
541
+ :param client_uuid:
542
+ :return: True if successful, False otherwise
543
+ """
544
+
545
+ headers = {
546
+ 'Authorization': f'Bearer {admin_token}',
547
+ 'Content-Type': 'application/json'
548
+ }
549
+ url = f"{settings.SERVER_URL}/admin/realms/{settings.REALM}/clients/{client_uuid}/authz/resource-server/policy/role"
550
+ payload = {
551
+ "name": policy_name,
552
+ "description": description,
553
+ "logic": "POSITIVE",
554
+ "roles": [{"id": role} for role in roles]
555
+ }
556
+ try:
557
+ async with aiohttp.ClientSession() as session:
558
+ async with session.post(url, headers=headers, json=payload) as response:
559
+ if response.status == 201:
560
+ logger.info(f"Policy '{policy_name}' created successfully")
561
+ elif response.status == 409:
562
+ logger.info(f"Policy '{policy_name}' already exists")
563
+ else:
564
+ logger.error(f"Failed to create policy '{policy_name}'. Status: {response.status}, Response: {await response.text()}")
565
+ return response.status == 201 or response.status == 409
566
+ except aiohttp.ClientError as e:
567
+ logger.error(f"Connection error while creating policy '{policy_name}': {e}")
568
+ return False
569
+
570
+
571
+ async def create_permission(permission_name, description, policies, resource_ids, admin_token, client_uuid) -> bool:
572
+ """
573
+ Create a new permission in Keycloak.
574
+ :param permission_name:
575
+ :param description:
576
+ :param policies:
577
+ :param resource_ids:
578
+ :param admin_token:
579
+ :param client_uuid:
580
+ :return: True if successful, False otherwise
581
+ """
582
+
583
+ headers = {
584
+ 'Authorization': f'Bearer {admin_token}',
585
+ 'Content-Type': 'application/json'
586
+ }
587
+ url = f"{settings.SERVER_URL}/admin/realms/{settings.REALM}/clients/{client_uuid}/authz/resource-server/permission/resource"
588
+ payload = {
589
+ "name": permission_name,
590
+ "description": description,
591
+ "type": "resource",
592
+ "resources": resource_ids,
593
+ "policies": policies
594
+ }
595
+ try:
596
+ async with aiohttp.ClientSession() as session:
597
+ async with session.post(url, headers=headers, json=payload) as response:
598
+ if response.status == 201:
599
+ logger.info(f"Permission '{permission_name}' created successfully")
600
+ elif response.status == 409:
601
+ logger.info(f"Permission '{permission_name}' already exists")
602
+ else:
603
+ logger.error(f"Failed to create permission '{permission_name}'. Status: {response.status}, Response: {await response.text()}")
604
+ return response.status == 201 or response.status == 409
605
+ except aiohttp.ClientError as e:
606
+ logger.error(f"Connection error while creating permission '{permission_name}': {e}")
607
+ return False
608
+
609
+
610
+ async def create_resource(resource_name, display_name, url,admin_token, client_uuid) -> bool:
611
+ """
612
+ Create a new resource in Keycloak.
613
+ :param resource_name:
614
+ :param display_name:
615
+ :param url:
616
+ :param admin_token:
617
+ :param client_uuid:
618
+ :return: True if successful, False otherwise
619
+ """
620
+
621
+ headers = {
622
+ 'Authorization': f'Bearer {admin_token}',
623
+ 'Content-Type': 'application/json'
624
+ }
625
+ resource_url = f"{settings.SERVER_URL}/admin/realms/{settings.REALM}/clients/{client_uuid}/authz/resource-server/resource"
626
+ payload = {
627
+ "owner": None,
628
+ "name": resource_name,
629
+ "displayName": display_name,
630
+ "uri": url,
631
+ "type": "REST API",
632
+ }
633
+ try:
634
+ async with aiohttp.ClientSession() as session:
635
+ async with session.post(resource_url, headers=headers, json=payload) as response:
636
+ if response.status == 201:
637
+ logger.info(f"Resource '{resource_name}' created successfully")
638
+ elif response.status == 409:
639
+ logger.info(f"Resource '{resource_name}' already exists")
640
+ else:
641
+ logger.error(f"Failed to create resource '{resource_name}'. Status: {response.status}, Response: {await response.text()}")
642
+ return response.status == 201 or response.status == 409
643
+ except aiohttp.ClientError as e:
644
+ logger.error(f"Connection error while creating resource '{resource_name}': {e}")
645
+ return False
@@ -0,0 +1,25 @@
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+ from pydantic import ValidationError
3
+ from typing import Optional
4
+
5
+
6
+ class Settings(BaseSettings):
7
+ SERVER_URL: str
8
+ CLIENT_ID: str
9
+ REALM: str
10
+ SCOPE: str
11
+ KEYCLOAK_FRONTEND_URL: str
12
+ KC_BOOTSTRAP_ADMIN_USERNAME: str
13
+ KC_BOOTSTRAP_ADMIN_PASSWORD: str
14
+
15
+ model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
16
+
17
+ CLIENT_SECRET: Optional[str] = None
18
+
19
+
20
+ try:
21
+ settings = Settings()
22
+ except ValidationError as e:
23
+ print("Configuration error:", e)
24
+ import sys
25
+ sys.exit(1)