Skip to content

Commit 3101fd3

Browse files
committed
squash!: Refactored to extern most of the filtering logic to an api function
1 parent 04092bc commit 3101fd3

8 files changed

Lines changed: 178 additions & 142 deletions

File tree

openedx_authz/api/data.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,3 +1189,10 @@ def __repr__(self):
11891189
class UserAssignments:
11901190
user: "User"
11911191
assignments: list[RoleAssignmentData]
1192+
1193+
1194+
class UserAssignmentsFilter(Enum):
1195+
"""Enum for the filters that can be applied over UserAssignments."""
1196+
1197+
SCOPES = "scopes"
1198+
ORGS = "orgs"

openedx_authz/api/users.py

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@
1111

1212
from openedx_authz.api.data import (
1313
ActionData,
14+
ContentLibraryData,
15+
CourseOverviewData,
16+
OrgContentLibraryGlobData,
17+
OrgCourseOverviewGlobData,
1418
PermissionData,
1519
RoleAssignmentData,
1620
RoleData,
1721
ScopeData,
22+
UserAssignments,
23+
UserAssignmentsFilter,
1824
UserData,
1925
)
2026
from openedx_authz.api.permissions import is_subject_allowed
@@ -33,18 +39,21 @@
3339
unassign_role_from_subject_in_scope,
3440
unassign_subject_from_all_roles,
3541
)
42+
from openedx_authz.api.utils import filter_user_assignments, get_user_assignment_map
43+
from openedx_authz.constants.permissions import COURSES_VIEW_COURSE, VIEW_LIBRARY
3644

3745
__all__ = [
3846
"assign_role_to_user_in_scope",
3947
"batch_assign_role_to_users_in_scope",
4048
"unassign_role_from_user",
4149
"batch_unassign_role_from_users",
42-
"get_all_user_role_assignments",
4350
"get_user_role_assignments",
4451
"get_user_role_assignments_in_scope",
4552
"get_user_role_assignments_for_role_in_scope",
4653
"get_user_role_assignments_filtered",
54+
"get_all_user_role_assignments",
4755
"get_all_user_role_assignments_in_scope",
56+
"get_all_user_role_assignments_by_user_filtered",
4857
"is_user_allowed",
4958
"get_scopes_for_user_and_permission",
5059
"get_users_for_role_in_scope",
@@ -120,15 +129,6 @@ def batch_unassign_role_from_users(users: list[str], role_external_key: str, sco
120129
)
121130

122131

123-
def get_all_user_role_assignments() -> list[RoleAssignmentData]:
124-
"""Get all roles for all users across all scopes.
125-
126-
Returns:
127-
list[RoleAssignmentData]: A list of role assignments and all their metadata assigned to the user.
128-
"""
129-
return get_all_subject_role_assignments()
130-
131-
132132
def get_user_role_assignments(user_external_key: str) -> list[RoleAssignmentData]:
133133
"""Get all roles for a user across all scopes.
134134
@@ -202,6 +202,15 @@ def get_user_role_assignments_filtered(
202202
)
203203

204204

205+
def get_all_user_role_assignments() -> list[RoleAssignmentData]:
206+
"""Get all roles for all users across all scopes.
207+
208+
Returns:
209+
list[RoleAssignmentData]: A list of role assignments and all their metadata assigned to the user.
210+
"""
211+
return get_all_subject_role_assignments()
212+
213+
205214
def get_all_user_role_assignments_in_scope(
206215
scope_external_key: str,
207216
) -> list[RoleAssignmentData]:
@@ -216,6 +225,71 @@ def get_all_user_role_assignments_in_scope(
216225
return get_all_subject_role_assignments_in_scope(ScopeData(external_key=scope_external_key))
217226

218227

228+
def _filter_allowed_assignments(
229+
user_external_key: str, assignments: list[RoleAssignmentData]
230+
) -> list[RoleAssignmentData]:
231+
"""
232+
Filter the given role assignments to only include those that the user has permission to view.
233+
"""
234+
allowed_assignments: list[RoleAssignmentData] = []
235+
for assignment in assignments:
236+
permission = None
237+
238+
# For CourseOverviewData and ContentLibraryData, check for the view permission
239+
if isinstance(assignment.scope, (CourseOverviewData, OrgCourseOverviewGlobData)):
240+
permission = COURSES_VIEW_COURSE.identifier
241+
elif isinstance(assignment.scope, (ContentLibraryData, OrgContentLibraryGlobData)):
242+
permission = VIEW_LIBRARY.identifier
243+
244+
if permission and is_user_allowed(
245+
user_external_key=user_external_key,
246+
action_external_key=permission,
247+
scope_external_key=assignment.scope.external_key,
248+
):
249+
allowed_assignments.append(assignment)
250+
251+
return allowed_assignments
252+
253+
254+
def get_all_user_role_assignments_by_user_filtered(
255+
orgs: list[str] = None,
256+
scopes: list[str] = None,
257+
allowed_for_user_external_key: str = None,
258+
) -> list[UserAssignments]:
259+
"""
260+
Get all user role assignments filtered by orgs and/or scopes, and only include
261+
assignments that the specified user has permission to view.
262+
263+
Args:
264+
orgs: Optional list of orgs to filter by (e.g., ['edX', 'MITx']).
265+
scopes: Optional list of scopes to filter by (e.g., ['lib:DemoX:CSPROB']).
266+
allowed_for_user_external_key: The username to check permissions against (e.g., 'john_doe').
267+
268+
Returns:
269+
list[UserAssignments]: A list of users with their role assignments, filtered by orgs/scopes and permissions.
270+
"""
271+
user_role_assignments = get_all_user_role_assignments()
272+
# Filter assignments based on the user's permissions
273+
user_role_assignments = _filter_allowed_assignments(
274+
user_external_key=allowed_for_user_external_key,
275+
assignments=user_role_assignments,
276+
)
277+
# Group assignments by user
278+
users_with_assignments = get_user_assignment_map(user_role_assignments)
279+
280+
users_with_assignments = filter_user_assignments(
281+
users_with_assignments=users_with_assignments,
282+
by=UserAssignmentsFilter.SCOPES,
283+
values=scopes,
284+
)
285+
users_with_assignments = filter_user_assignments(
286+
users_with_assignments=users_with_assignments,
287+
by=UserAssignmentsFilter.ORGS,
288+
values=orgs,
289+
)
290+
return users_with_assignments
291+
292+
219293
def is_user_allowed(
220294
user_external_key: str,
221295
action_external_key: str,

openedx_authz/api/utils.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Utility functions used on api"""
2+
3+
from django.contrib.auth import get_user_model
4+
5+
from openedx_authz.api.data import (
6+
RoleAssignmentData,
7+
UserAssignments,
8+
UserAssignmentsFilter,
9+
)
10+
11+
User = get_user_model()
12+
13+
14+
def get_user_map(usernames: list[str]) -> dict[str, User]:
15+
"""
16+
Retrieve a dictionary mapping usernames to User objects for efficient batch lookups.
17+
18+
This function performs a single optimized database query to fetch multiple users,
19+
making it ideal for scenarios where we need to look up several users at once
20+
(e.g., when serializing multiple user role assignments).
21+
22+
Args:
23+
usernames (list[str]): List of usernames to retrieve. Duplicates are automatically
24+
handled by the database query.
25+
26+
Returns:
27+
dict[str, User]: Dictionary mapping each username to its corresponding User object.
28+
Only users that exist in the database are included in the returned dictionary.
29+
"""
30+
users = User.objects.filter(username__in=usernames).select_related("profile")
31+
return {user.username: user for user in users}
32+
33+
34+
def get_user_assignment_map(role_assignments: list[RoleAssignmentData]) -> list[UserAssignments]:
35+
"""
36+
Group role assignments by user
37+
"""
38+
usernames = {assignment.subject.username for assignment in role_assignments}
39+
user_map = get_user_map(usernames)
40+
41+
users_with_assignments: list[UserAssignments] = []
42+
43+
for username, user in user_map.items():
44+
assignments = [a for a in role_assignments if a.subject.username == username]
45+
users_with_assignments.append(UserAssignments(user=user, assignments=assignments))
46+
47+
return users_with_assignments
48+
49+
50+
def filter_user_assignments(
51+
users_with_assignments: list[UserAssignments],
52+
by: UserAssignmentsFilter,
53+
values: list[str],
54+
) -> list[UserAssignments]:
55+
"""
56+
Filter user assignments by orgs or scopes.
57+
"""
58+
if not values:
59+
return users_with_assignments
60+
61+
def _get_value_to_filter(assignment: RoleAssignmentData) -> str:
62+
if by == UserAssignmentsFilter.SCOPES:
63+
return assignment.scope.external_key
64+
elif by == UserAssignmentsFilter.ORGS:
65+
return assignment.scope.org
66+
else:
67+
raise ValueError(f"Invalid filter: '{by}'. Must be one of {[f.value for f in UserAssignmentsFilter]}")
68+
69+
filtered_users: list[UserAssignments] = []
70+
for uwa in users_with_assignments:
71+
if any(_get_value_to_filter(a) in values for a in uwa.assignments):
72+
# Also filter assignments to reflect the correct number of assignments
73+
filtered_assignments = [a for a in uwa.assignments if _get_value_to_filter(a) in values]
74+
filtered_users.append(UserAssignments(user=uwa.user, assignments=filtered_assignments))
75+
users_with_assignments = filtered_users
76+
77+
return filtered_users

openedx_authz/rest_api/data.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,3 @@ class RoleOperationError(BaseEnum):
5050
USER_DOES_NOT_HAVE_ROLE = "user_does_not_have_role"
5151
ROLE_ASSIGNMENT_ERROR = "role_assignment_error"
5252
ROLE_REMOVAL_ERROR = "role_removal_error"
53-
54-
55-
class UserAssignmentsFilter(BaseEnum):
56-
"""Enum for the filters that can be applied over UserAssignments."""
57-
58-
SCOPES = "scopes"
59-
ORGS = "orgs"

openedx_authz/rest_api/utils.py

Lines changed: 1 addition & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,14 @@
22

33
import logging
44

5-
from django.contrib.auth import get_user_model
6-
75
from openedx_authz.api.data import (
86
GLOBAL_SCOPE_WILDCARD,
9-
ContentLibraryData,
10-
CourseOverviewData,
11-
OrgContentLibraryGlobData,
12-
OrgCourseOverviewGlobData,
13-
RoleAssignmentData,
147
ScopeData,
15-
UserAssignments,
168
)
17-
from openedx_authz.api.users import is_user_allowed
18-
from openedx_authz.constants.permissions import COURSES_VIEW_COURSE, VIEW_LIBRARY
19-
from openedx_authz.rest_api.data import SearchField, SortField, SortOrder, UserAssignmentsFilter
9+
from openedx_authz.rest_api.data import SearchField, SortField, SortOrder
2010

2111
logger = logging.getLogger(__name__)
2212

23-
User = get_user_model()
24-
2513

2614
def get_generic_scope(scope: ScopeData) -> ScopeData:
2715
"""
@@ -45,26 +33,6 @@ def get_generic_scope(scope: ScopeData) -> ScopeData:
4533
return ScopeData(namespaced_key=f"{scope.NAMESPACE}{ScopeData.SEPARATOR}{GLOBAL_SCOPE_WILDCARD}")
4634

4735

48-
def get_user_map(usernames: list[str]) -> dict[str, User]:
49-
"""
50-
Retrieve a dictionary mapping usernames to User objects for efficient batch lookups.
51-
52-
This function performs a single optimized database query to fetch multiple users,
53-
making it ideal for scenarios where we need to look up several users at once
54-
(e.g., when serializing multiple user role assignments).
55-
56-
Args:
57-
usernames (list[str]): List of usernames to retrieve. Duplicates are automatically
58-
handled by the database query.
59-
60-
Returns:
61-
dict[str, User]: Dictionary mapping each username to its corresponding User object.
62-
Only users that exist in the database are included in the returned dictionary.
63-
"""
64-
users = User.objects.filter(username__in=usernames).select_related("profile")
65-
return {user.username: user for user in users}
66-
67-
6836
def sort_users(
6937
users: list[dict],
7038
sort_by: SortField = SortField.USERNAME,
@@ -129,73 +97,3 @@ def filter_users(users: list[dict], search: str | None, roles: list[str] | None)
12997
filtered_users.append(user)
13098

13199
return filtered_users
132-
133-
134-
def get_user_assignment_map(role_assignments: list[RoleAssignmentData]) -> list[UserAssignments]:
135-
"""
136-
Group role assignments by user
137-
"""
138-
usernames = {assignment.subject.username for assignment in role_assignments}
139-
user_map = get_user_map(usernames)
140-
141-
users_with_assignments: list[UserAssignments] = []
142-
143-
for username, user in user_map.items():
144-
assignments = [a for a in role_assignments if a.subject.username == username]
145-
users_with_assignments.append(UserAssignments(user=user, assignments=assignments))
146-
147-
return users_with_assignments
148-
149-
150-
def filter_allowed_assignments(user: "User", assignments: list[RoleAssignmentData]) -> list[RoleAssignmentData]:
151-
"""
152-
Filter the given role assignments to only include those that the user has permission to view.
153-
"""
154-
allowed_assignments: list[RoleAssignmentData] = []
155-
for assignment in assignments:
156-
permission = None
157-
158-
# For CourseOverviewData and ContentLibraryData, check for the view permission
159-
if isinstance(assignment.scope, (CourseOverviewData, OrgCourseOverviewGlobData)):
160-
permission = COURSES_VIEW_COURSE.identifier
161-
elif isinstance(assignment.scope, (ContentLibraryData, OrgContentLibraryGlobData)):
162-
permission = VIEW_LIBRARY.identifier
163-
164-
if permission and is_user_allowed(
165-
user_external_key=user.username,
166-
action_external_key=permission,
167-
scope_external_key=assignment.scope.external_key,
168-
):
169-
allowed_assignments.append(assignment)
170-
171-
return allowed_assignments
172-
173-
174-
def filter_user_assignments(
175-
users_with_assignments: list[UserAssignments],
176-
by: UserAssignmentsFilter,
177-
values: list[str],
178-
) -> list[UserAssignments]:
179-
"""
180-
Filter user assignments by orgs or scopes.
181-
"""
182-
if not values:
183-
return users_with_assignments
184-
185-
def _get_value_to_filter(assignment: RoleAssignmentData) -> str:
186-
if by == UserAssignmentsFilter.SCOPES:
187-
return assignment.scope.external_key
188-
elif by == UserAssignmentsFilter.ORGS:
189-
return assignment.scope.org
190-
else:
191-
raise ValueError(f"Invalid filter: '{by}'. Must be one of {UserAssignmentsFilter.values()}")
192-
193-
filtered_users: list[UserAssignments] = []
194-
for uwa in users_with_assignments:
195-
if any(_get_value_to_filter(a) in values for a in uwa.assignments):
196-
# Also filter assignments to reflect the correct number of assignments
197-
filtered_assignments = [a for a in uwa.assignments if _get_value_to_filter(a) in values]
198-
filtered_users.append(UserAssignments(user=uwa.user, assignments=filtered_assignments))
199-
users_with_assignments = filtered_users
200-
201-
return filtered_users

openedx_authz/rest_api/v1/serializers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
from rest_framework import serializers
55

66
from openedx_authz import api
7+
from openedx_authz.api.data import UserAssignments
78
from openedx_authz.rest_api.data import SortField, SortOrder
8-
from openedx_authz.rest_api.utils import UserAssignments, get_generic_scope
9+
from openedx_authz.rest_api.utils import get_generic_scope
910
from openedx_authz.rest_api.v1.fields import (
1011
CaseSensitiveCommaSeparatedListField,
1112
CommaSeparatedListField,

0 commit comments

Comments
 (0)