Skip to content

Commit b9e3a1d

Browse files
committed
CH-167 Async support on Django related events
1 parent 8ab3502 commit b9e3a1d

File tree

6 files changed

+89
-107
lines changed

6 files changed

+89
-107
lines changed

infrastructure/common-images/cloudharness-django/libraries/cloudharness-django/cloudharness_django/admin.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from django.contrib import admin
33
from django.contrib.auth.admin import UserAdmin, GroupAdmin
44
from django.contrib.auth.models import User, Group
5-
5+
import asyncio
66
from admin_extra_buttons.api import ExtraButtonsMixin, button
77
from .models import Member
88
from cloudharness_django.services import get_user_service
@@ -17,6 +17,20 @@ class MemberAdmin(admin.StackedInline):
1717
model = Member
1818

1919

20+
def run_coroutine(coroutine):
21+
try:
22+
loop = asyncio.get_running_loop()
23+
except RuntimeError: # No running event loop
24+
loop = None
25+
26+
if loop and loop.is_running():
27+
# If the event loop is already running, create a task
28+
return asyncio.create_task(coroutine)
29+
else:
30+
# If no event loop is running, run the coroutine using asyncio.run
31+
return asyncio.run(coroutine)
32+
33+
2034
class CHUserAdmin(ExtraButtonsMixin, UserAdmin):
2135

2236
inlines = [MemberAdmin]
@@ -31,8 +45,8 @@ def has_delete_permission(self, request, obj=None):
3145
return settings.DEBUG or settings.USER_CHANGE_ENABLED
3246

3347
@button()
34-
def sync_keycloak(self, request):
35-
get_user_service().sync_kc_users_groups()
48+
async def sync_keycloak(self, request):
49+
run_coroutine(get_user_service().sync_kc_users_groups())
3650
self.message_user(request, 'Keycloak users & groups synced.')
3751

3852

@@ -48,8 +62,8 @@ def has_delete_permission(self, request, obj=None):
4862
return settings.DEBUG
4963

5064
@button()
51-
def sync_keycloak(self, request):
52-
get_user_service().sync_kc_users_groups()
65+
async def sync_keycloak(self, request):
66+
run_coroutine(get_user_service().sync_kc_users_groups())
5367
self.message_user(request, 'Keycloak users & groups synced.')
5468

5569

infrastructure/common-images/cloudharness-django/libraries/cloudharness-django/cloudharness_django/middleware.py

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.contrib.auth.models import User
55
from asgiref.sync import iscoroutinefunction
66
from django.utils.decorators import sync_and_async_middleware
7+
from asgiref.sync import async_to_sync, iscoroutinefunction
78

89
from keycloak.exceptions import KeycloakGetError
910

@@ -13,32 +14,7 @@
1314
from cloudharness import log
1415

1516

16-
def _get_user():
17-
bearer = get_authentication_token()
18-
if bearer:
19-
# found bearer token get the Django user
20-
try:
21-
token = bearer.split(" ")[-1]
22-
payload = jwt.decode(token, algorithms=["RS256"], options={"verify_signature": False}, audience="web-client")
23-
kc_id = payload["sub"]
24-
try:
25-
user = User.objects.get(member__kc_id=kc_id)
26-
except User.DoesNotExist:
27-
user = get_user_service().sync_kc_user(get_auth_service().get_auth_client().get_current_user())
28-
return user
29-
except KeycloakGetError:
30-
# KC user not found
31-
return None
32-
except InvalidToken:
33-
return None
34-
except Exception as e:
35-
log.exception("User mapping error, %s", payload["email"])
36-
return None
37-
38-
return None
39-
40-
41-
async def _aget_user():
17+
async def _get_user():
4218
bearer = get_authentication_token()
4319
if bearer:
4420
# found bearer token get the Django user
@@ -49,7 +25,7 @@ async def _aget_user():
4925
try:
5026
user = await User.objects.aget(member__kc_id=kc_id)
5127
except User.DoesNotExist:
52-
user = await get_user_service().async_kc_user(get_auth_service().get_auth_client().get_current_user())
28+
user = await get_user_service().sync_kc_user(get_auth_service().get_auth_client().get_current_user())
5329
return user
5430
except KeycloakGetError:
5531
# KC user not found
@@ -69,7 +45,7 @@ def BearerTokenMiddleware(get_response=None):
6945
if iscoroutinefunction(get_response):
7046
async def middleware(request):
7147
if (not request.path.startswith("/static")) and getattr(getattr(request, "user", {}), "is_anonymous", True):
72-
user = await _aget_user()
48+
user = await _get_user()
7349
if user:
7450
# auto login, set the user
7551
request.user = user
@@ -80,7 +56,7 @@ async def middleware(request):
8056
else:
8157
def middleware(request):
8258
if (not request.path.startswith("/static")) and getattr(getattr(request, "user", {}), "is_anonymous", True):
83-
user = _get_user()
59+
user = async_to_sync(_get_user)()
8460
if user:
8561
# auto login, set the user
8662
request.user = user

infrastructure/common-images/cloudharness-django/libraries/cloudharness-django/cloudharness_django/services/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414
def get_auth_service():
1515
global _auth_service
1616
if not _auth_service:
17-
raise KeycloakOIDCAuthServiceNotInitError("Auth Service not initialized")
17+
init_services()
1818
return _auth_service
1919

2020

2121
def get_user_service():
2222
global _user_service
2323
if not _user_service:
24-
raise KeycloakOIDUserServiceNotInitError("User Service not initialized")
24+
init_services()
2525
return _user_service
2626

2727

infrastructure/common-images/cloudharness-django/libraries/cloudharness-django/cloudharness_django/services/events.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from cloudharness.applications import ConfigurationCallException
2-
2+
import asyncio
33
from django.conf import settings
44
from kafka.errors import TopicAlreadyExistsError
55

@@ -18,7 +18,7 @@ def __init__(self, kafka_group_id):
1818
self.topics_initialized = False
1919

2020
@staticmethod
21-
def event_handler(app, event_client, message):
21+
async def event_handler(app, event_client, message):
2222
resource = message["resource-type"]
2323
operation = message["operation-type"]
2424
resource_path = message["resource-path"].split("/")
@@ -32,20 +32,20 @@ def event_handler(app, event_client, message):
3232

3333
if resource == "GROUP":
3434
kc_group = auth_client.get_group(resource_path[1])
35-
user_service.sync_kc_group(kc_group)
35+
await user_service.sync_kc_group(kc_group)
3636
if resource == "USER":
3737
kc_user = auth_client.get_user(resource_path[1])
38-
user_service.sync_kc_user(kc_user, delete=operation == "DELETE")
38+
await user_service.sync_kc_user(kc_user, delete=operation == "DELETE")
3939
if resource == "CLIENT_ROLE_MAPPING":
4040
# adding/deleting user client roles
4141
# set/user user is_superuser
4242
kc_user = auth_client.get_user(resource_path[1])
43-
user_service.sync_kc_user(kc_user)
43+
await user_service.sync_kc_user(kc_user)
4444
if resource == "GROUP_MEMBERSHIP":
4545
# adding / deleting users from groups, update the user
4646
# updating the user will also update the user groups
4747
kc_user = auth_client.get_user(resource_path[1])
48-
user_service.sync_kc_user(kc_user)
48+
await user_service.sync_kc_user(kc_user)
4949
except Exception as e:
5050
log.error(e)
5151
raise e

infrastructure/common-images/cloudharness-django/libraries/cloudharness-django/cloudharness_django/services/user.py

Lines changed: 31 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
from django.contrib.auth.models import User, Group
23

34
from cloudharness_django.models import Team, Member
@@ -52,62 +53,52 @@ def update_team(self, group):
5253
self.auth_client.update_group(group.team.kc_id, group.name)
5354
return group
5455

55-
def add_user_to_team(self, user, team_name):
56+
async def add_user_to_team(self, user, team_name):
5657
# add a user from the group/team
57-
group = Group.objects.get(name=team_name)
58+
group = Group.objects.aget(name=team_name)
5859
kc_group_id = group.team.kc_id
5960
kc_user_id = user.member.kc_id
6061
self.auth_client.group_user_add(kc_user_id, kc_group_id)
6162

62-
def rm_user_from_team(self, user, team_name):
63+
async def rm_user_from_team(self, user, team_name):
6364
# delete a user from the group/team
64-
group = Group.objects.get(name=team_name)
65+
group = await Group.objects.aget(name=team_name)
6566
kc_group_id = group.team.kc_id
6667
kc_user_id = user.member.kc_id
6768
self.auth_client.group_user_remove(kc_user_id, kc_group_id)
6869

69-
def sync_kc_group(self, kc_group):
70+
async def sync_kc_group(self, kc_group):
7071
# sync the kc group with the django group
7172
try:
72-
team = Team.objects.get(kc_id=kc_group["id"])
73-
group, created = Group.objects.get_or_create(team=team)
73+
team = await Team.objects.aget(kc_id=kc_group["id"])
74+
group, created = await Group.objects.aget_or_create(team=team)
7475
group.name = kc_group["name"]
7576
except Team.DoesNotExist:
76-
group, created = Group.objects.get_or_create(name=kc_group["name"])
77+
group, created = await Group.objects.aget_or_create(name=kc_group["name"])
7778
try:
7879
# check if group has a team
7980
team = group.team
8081
except Exception as e:
8182
# create the team
82-
superusers = User.objects.filter(is_superuser=True)
83-
if superusers and len(superusers) > 0:
84-
team = Team.objects.create(
85-
owner=superusers[0], # one of the superusers will be the default team owner
83+
try:
84+
superuser = User.objects.filter(is_superuser=True).afirst()
85+
86+
team = await Team.objects.acreate(
87+
owner=superuser, # one of the superusers will be the default team owner
8688
kc_id=kc_group["id"],
8789
group=group)
88-
team.save()
89-
group.save()
90+
await team.asave()
91+
except User.DoesNotExist as ex:
92+
raise Exception("There is no superuser") from ex
93+
await group.asave()
9094

91-
def sync_kc_groups(self, kc_groups=None):
95+
async def sync_kc_groups(self, kc_groups=None):
9296
# sync all groups
9397
if not kc_groups:
9498
kc_groups = self.auth_client.get_groups()
95-
for kc_group in kc_groups:
96-
self.sync_kc_group(kc_group)
97-
98-
def sync_kc_user(self, kc_user, is_superuser=False, delete=False):
99-
# sync the kc user with the django user
100-
101-
user, created = User.objects.get_or_create(username=kc_user["username"])
102-
103-
member, created = Member.objects.get_or_create(user=user)
104-
member.kc_id = kc_user["id"]
105-
member.save()
106-
user = self._map_kc_user(user, kc_user, is_superuser, delete)
107-
user.save()
108-
return user
99+
await asyncio.gather(self.sync_kc_group(kc_group) for kc_group in kc_groups)
109100

110-
async def async_kc_user(self, kc_user, is_superuser=False, delete=False):
101+
async def sync_kc_user(self, kc_user, is_superuser=False, delete=False):
111102
# sync the kc user with the django user
112103

113104
user, created = await User.objects.aget_or_create(username=kc_user["username"])
@@ -117,23 +108,23 @@ async def async_kc_user(self, kc_user, is_superuser=False, delete=False):
117108
user.save()
118109
return user
119110

120-
def sync_kc_user_groups(self, kc_user):
111+
async def sync_kc_user_groups(self, kc_user):
121112
# Sync the user usergroups and memberships
122-
user = User.objects.get(username=kc_user["email"])
113+
user = await User.objects.aget(username=kc_user["email"])
123114
user_groups = []
124115
for kc_group in kc_user["userGroups"]:
125-
user_groups += [Group.objects.get(name=kc_group["name"])]
126-
user.groups.set(user_groups)
127-
user.save()
116+
user_groups += [await Group.objects.aget(name=kc_group["name"])]
117+
await user.groups.aset(user_groups)
118+
user.asave()
128119

129120
try:
130121
if user.member.kc_id != kc_user["id"]:
131122
user.member.kc_id = kc_user["id"]
132123
except Member.DoesNotExist:
133124
member = Member(user=user, kc_id=kc_user["id"])
134-
member.save()
125+
await member.asave()
135126

136-
def sync_kc_users_groups(self):
127+
async def sync_kc_users_groups(self):
137128
# cache all admin users to minimize KC rest api calls
138129
all_admin_users = self.auth_client.get_client_role_members(
139130
self.auth_service.get_client_name(),
@@ -144,11 +135,10 @@ def sync_kc_users_groups(self):
144135
for kc_user in self.auth_client.get_users():
145136
# check if user in all_admin_users
146137
is_superuser = any([admin_user for admin_user in all_admin_users if admin_user["email"] == kc_user["email"]])
147-
self.sync_kc_user(kc_user, is_superuser)
138+
await self.sync_kc_user(kc_user, is_superuser)
148139

149140
# sync the groups
150-
self.sync_kc_groups()
141+
await self.sync_kc_groups()
151142

152143
# sync the user groups and memberships
153-
for kc_user in self.auth_client.get_users():
154-
self.sync_kc_user_groups(kc_user)
144+
await asyncio.gather(self.sync_kc_user_groups(kc_user) for kc_user in self.auth_client.get_users())

libraries/cloudharness-common/cloudharness/events/client.py

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import time
55
from typing import List, Generator
66
import logging
7-
7+
import asyncio
8+
from asgiref.sync import iscoroutinefunction
89
from time import sleep
910
from cloudharness import json, dumps
1011

@@ -241,14 +242,18 @@ def close(self):
241242
# for now no cleanup tasks to do
242243
pass
243244

244-
def _consume_task(self, app=None, group_id=None, handler=None):
245+
async def _consume_task(self, app=None, group_id=None, handler=None):
246+
245247
log.info(f'Kafka consumer thread started, listening for messages in queue: {self.topic_id}')
246248
while True:
247249
try:
248250
self.consumer = self._get_consumer(group_id)
249251
for message in self.consumer:
250252
try:
251-
handler(event_client=self, app=app, message=message.value)
253+
if iscoroutinefunction(handler):
254+
await handler(event_client=self, app=app, message=message.value)
255+
else:
256+
handler(event_client=self, app=app, message=message.value)
252257
except Exception as e:
253258
log.error(f"Error during execution of the consumer Topic {self.topic_id} --> {e}", exc_info=True)
254259
self.consumer.close()
@@ -262,28 +267,25 @@ def async_consume(self, app=None, handler=None, group_id='default'):
262267
log.debug('get current object from app')
263268
app = app._get_current_object()
264269
self._consumer_thread = threading.Thread(
265-
target=self._consume_task,
266-
kwargs={'app': app,
267-
'group_id': group_id,
268-
'handler': handler})
270+
target=asyncio.run(self._consume_task(app, group_id, handler))
271+
)
269272
self._consumer_thread.daemon = True
270273
self._consumer_thread.start()
271274
log.debug('thread started')
272275

273-
274-
if __name__ == "__main__":
275-
# creat the required os env variables
276-
os.environ['CLOUDHARNESS_EVENTS_CLIENT_ID'] = env.get_cloudharness_events_client_id()
277-
os.environ['CLOUDHARNESS_EVENTS_SERVICE'] = env.get_cloudharness_events_service()
278-
279-
# instantiate the client
280-
client = EventClient('test-sync-op-results-qcwbc')
281-
282-
# create a topic from env variables
283-
# print(client.create_topic())
284-
# publish to the prev created topic
285-
# print(client.produce({"message": "In God we trust, all others bring data..."}))
286-
# read from the topic
287-
print(client.consume_all('my-group'))
288-
# delete the topic
289-
# print(client.delete_topic())
276+
if __name__ == "__main__":
277+
# creat the required os env variables
278+
os.environ['CLOUDHARNESS_EVENTS_CLIENT_ID'] = env.get_cloudharness_events_client_id()
279+
os.environ['CLOUDHARNESS_EVENTS_SERVICE'] = env.get_cloudharness_events_service()
280+
281+
# instantiate the client
282+
client = EventClient('test-sync-op-results-qcwbc')
283+
284+
# create a topic from env variables
285+
# print(client.create_topic())
286+
# publish to the prev created topic
287+
# print(client.produce({"message": "In God we trust, all others bring data..."}))
288+
# read from the topic
289+
print(client.consume_all('my-group'))
290+
# delete the topic
291+
# print(client.delete_topic())

0 commit comments

Comments
 (0)