Skip to content

Commit 82d0dcb

Browse files
authored
Merge pull request #631 from makerspace/emaus/improve-accessy-sync
Improve Accessy sync
2 parents 50b24ca + f0562fe commit 82d0dcb

File tree

1 file changed

+69
-72
lines changed

1 file changed

+69
-72
lines changed

api/src/multiaccessy/accessy.py

Lines changed: 69 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,14 @@ class AccessyUser(DataClassJsonMixin):
185185
msisdn: Optional[MSISDN] = None
186186

187187

188+
@dataclass
189+
class AccessyUserMembership(DataClassJsonMixin):
190+
id: UUID
191+
userId: UUID
192+
organizationId: UUID
193+
roles: list[str]
194+
195+
188196
class AccessySession:
189197
def __init__(self) -> None:
190198
self.session_token: str | None = None
@@ -232,10 +240,42 @@ def get_member_id_from_accessy_id(self, accessy_id: UUID) -> Optional[int]:
232240
def get_all_members(self) -> list[AccessyMember]:
233241
"""Get a list of all Accessy members in the ORG with GROUPS (lab and special)"""
234242

235-
org_member_ids = set(item["id"] for item in self._get_users_org())
243+
def fill_in_group_affiliation_and_membership_id(members: list[AccessyMember], group: UUID) -> None:
244+
group_members = self._get_users_in_access_group(group)
245+
userId_to_group_member = {m.userId: m for m in group_members}
246+
for member in members:
247+
assert member.user_id is not None
248+
membership = userId_to_group_member.get(member.user_id, None)
249+
250+
if not membership:
251+
continue
252+
253+
member.groups.add(group)
254+
if member.membership_id is None:
255+
member.membership_id = membership.id
256+
elif member.membership_id != membership.id:
257+
raise AccessyError(
258+
f"User {member.name!r} {member.user_id=} has different membership_id for different groups"
259+
f" ({member.membership_id} != {membership.id}). This is a bug"
260+
)
261+
262+
def fill_in_membership_id_if_missing(member: AccessyMember) -> None:
263+
if member.membership_id is None:
264+
member.membership_id = self._get(
265+
f"/asset/admin/user/{member.user_id}/organization/{self.organization_id()}/membership"
266+
)["id"]
267+
268+
org_members = [AccessyUser.from_dict(item) for item in self._get_users_org()]
269+
members = [
270+
AccessyMember(user_id=item.id, phone=item.msisdn, name=f"{item.firstName} {item.lastName}")
271+
for item in org_members
272+
]
236273

237-
members = self._user_ids_to_accessy_members(org_member_ids)
238-
self._populate_user_groups(members)
274+
for group in [ACCESSY_LABACCESS_GROUP, ACCESSY_SPECIAL_LABACCESS_GROUP]:
275+
fill_in_group_affiliation_and_membership_id(members, group)
276+
277+
for m in members:
278+
fill_in_membership_id_if_missing(m)
239279

240280
return members
241281

@@ -470,30 +510,20 @@ def _get_user_details(self, user_id: UUID) -> Any:
470510
return self._get(f"/org/admin/user/{user_id}", err_msg="Getting user details")
471511

472512
def _get_users_org(self) -> list[dict]:
473-
"""Get all user ID:s"""
474-
475-
def is_application(user: dict) -> bool:
476-
return user.get("msisdn", None) is None
513+
"""Get all users"""
477514

478515
return [
479516
v
480-
for v in self._get_json_paginated(f"/asset/admin/organization/{self.organization_id()}/user")
481-
if not is_application(v)
517+
for v in self._get_json_paginated(f"/org/organization/{self.organization_id()}/user")
518+
if not v["application"]
482519
]
483-
# {"items":[{"id":<uuid>,"msisdn":"+46...","firstName":str,"lastName":str}, ...],"totalItems":6,"pageSize":25,"pageNumber":0,"totalPages":1}
484520

485-
def _get_users_in_access_group(self, access_group_id: UUID) -> list[dict]:
521+
def _get_users_in_access_group(self, access_group_id: UUID) -> list[AccessyUserMembership]:
486522
"""Get all user ID:s in a specific access group"""
487-
return self._get_json_paginated(f"/asset/admin/access-permission-group/{access_group_id}/membership")
488-
# {"items":[{"id":<uuid>,"userId":<uuid>,"organizationId":<uuid>,"roles":[<roles>]}, ...],"totalItems":3,"pageSize":25,"pageNumber":0,"totalPages":1}
489-
490-
def _get_users_lab(self) -> list[dict]:
491-
"""Get all user ID:s with lab access"""
492-
return self._get_users_in_access_group(ACCESSY_LABACCESS_GROUP)
493-
494-
def _get_users_special(self) -> list[dict]:
495-
"""Get all user ID:s with special access"""
496-
return self._get_users_in_access_group(ACCESSY_SPECIAL_LABACCESS_GROUP)
523+
return [
524+
AccessyUserMembership.from_dict(d)
525+
for d in self._get_json_paginated(f"/asset/admin/access-permission-group/{access_group_id}/membership")
526+
]
497527

498528
def _get_organization_groups(self) -> list[dict]:
499529
"""Get information about all groups"""
@@ -512,14 +542,13 @@ def _get_group_description(self, group_id: UUID) -> str:
512542
assert isinstance(name, str)
513543
return name
514544

515-
def _user_ids_to_accessy_members(self, user_ids: Iterable[UUID]) -> list[AccessyMember]:
516-
"""Convert a list of User ID:s to AccessyMembers"""
545+
def _user_id_to_accessy_member(self, user_id: UUID) -> AccessyMember | None:
546+
"""Convert an Accessy User ID to an AccessyMember object"""
517547

518548
APPLICATION_PHONE_NUMBER = object() # Sentinel phone number for applications
519549

520550
def fill_user_details(user: AccessyMember) -> None:
521-
assert user.user_id is not None
522-
data = self.get_user_details(user.user_id)
551+
data = self.get_user_details(user_id)
523552

524553
# API keys do not have phone numbers, set it to sentinel object so we can filter out API keys further down
525554
if data.application:
@@ -529,60 +558,28 @@ def fill_user_details(user: AccessyMember) -> None:
529558
if data.msisdn is not None:
530559
user.phone = data.msisdn
531560
else:
532-
logger.warning(f"User {user.user_id} does not have a phone number in accessy. {data=}")
561+
logger.warning(f"User {user_id=} does not have a phone number in accessy. {data=}")
533562
user.name = f"{data.firstName} {data.lastName}"
534563

535564
def fill_membership_id(user: AccessyMember) -> None:
536-
data = self._get(f"/asset/admin/user/{user.user_id}/organization/{self.organization_id()}/membership")
565+
data = self._get(f"/asset/admin/user/{user_id}/organization/{self.organization_id()}/membership")
537566
user.membership_id = data["id"]
538567

539-
exception_during_fetch = None
540-
threads = []
541-
user_ids = list(user_ids)
542-
thread_count = min(4, len(user_ids))
543-
accessy_members: List[AccessyMember] = []
544-
for i in range(thread_count):
545-
# Chunk the user_ids into equal parts for each thread
546-
slice = user_ids[i::thread_count]
547-
member_slice = [AccessyMember(user_id=uid) for uid in slice]
548-
accessy_members.extend(member_slice)
549-
550-
# Start a thread for each chunk
551-
def worker(member_slice: list[AccessyMember]) -> None:
552-
nonlocal exception_during_fetch
553-
try:
554-
for member in member_slice:
555-
fill_user_details(member)
556-
fill_membership_id(member)
557-
except AccessyError as e:
558-
exception_during_fetch = e
559-
560-
t = threading.Thread(target=worker, args=(member_slice,))
561-
threads.append(t)
562-
t.start()
563-
564-
for t in threads:
565-
t.join()
566-
567-
# Filter out API keys
568-
accessy_members = [m for m in accessy_members if m.phone is not APPLICATION_PHONE_NUMBER]
569-
570-
if exception_during_fetch:
571-
raise RuntimeError(
572-
f"One or more threads failed to fetch user details. The last exception as: {exception_during_fetch}"
573-
)
568+
member = AccessyMember(user_id=user_id)
569+
fill_user_details(member)
570+
fill_membership_id(member)
574571

575-
return accessy_members
572+
if member.phone is APPLICATION_PHONE_NUMBER:
573+
return None
576574

577-
def _populate_user_groups(self, members: List[AccessyMember]) -> None:
578-
lab_ids = set(item["userId"] for item in self._get_users_lab())
579-
special_ids = set(item["userId"] for item in self._get_users_special())
575+
return member
580576

581-
for m in members:
582-
if m.user_id in lab_ids:
583-
m.groups.add(ACCESSY_LABACCESS_GROUP)
584-
if m.user_id in special_ids:
585-
m.groups.add(ACCESSY_SPECIAL_LABACCESS_GROUP)
577+
def _populate_user_groups(self, members: List[AccessyMember]) -> None:
578+
for group in [ACCESSY_LABACCESS_GROUP, ACCESSY_SPECIAL_LABACCESS_GROUP]:
579+
user_ids_in_group = set(item.userId for item in self._get_users_in_access_group(group))
580+
for m in members:
581+
if m.user_id in user_ids_in_group:
582+
m.groups.add(group)
586583

587584
def get_org_user_from_phone(
588585
self, phone_number: MSISDN, users_in_org: list[dict] | None = None
@@ -603,7 +600,7 @@ def _get_org_user_from_phone(
603600
for item in users_in_org:
604601
if item.get("msisdn", None) == phone_number:
605602
user_id = item["id"]
606-
return self._user_ids_to_accessy_members([user_id])[0]
603+
return self._user_id_to_accessy_member(user_id)
607604
else:
608605
return None
609606

0 commit comments

Comments
 (0)