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.
- auth_gateway_serverkit/__init__.py +0 -0
- auth_gateway_serverkit/email.py +54 -0
- auth_gateway_serverkit/http_client.py +104 -0
- auth_gateway_serverkit/keycloak/__init__.py +3 -0
- auth_gateway_serverkit/keycloak/authorization_api.py +88 -0
- auth_gateway_serverkit/keycloak/client_api.py +645 -0
- auth_gateway_serverkit/keycloak/config.py +25 -0
- auth_gateway_serverkit/keycloak/initializer.py +106 -0
- auth_gateway_serverkit/keycloak/json_transformer.py +65 -0
- auth_gateway_serverkit/keycloak/roles_api.py +85 -0
- auth_gateway_serverkit/keycloak/user_api.py +268 -0
- auth_gateway_serverkit/logger.py +47 -0
- auth_gateway_serverkit/main.py +2 -0
- auth_gateway_serverkit/middleware/__init__.py +0 -0
- auth_gateway_serverkit/middleware/auth.py +173 -0
- auth_gateway_serverkit/middleware/config.py +36 -0
- auth_gateway_serverkit/middleware/schemas.py +19 -0
- auth_gateway_serverkit/password.py +25 -0
- auth_gateway_serverkit/request_handler.py +90 -0
- auth_gateway_serverkit/string.py +41 -0
- auth_gateway_serverkit-0.0.0.dist-info/METADATA +25 -0
- auth_gateway_serverkit-0.0.0.dist-info/RECORD +24 -0
- auth_gateway_serverkit-0.0.0.dist-info/WHEEL +5 -0
- auth_gateway_serverkit-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -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)
|