Skip to content

Commit 59d2da3

Browse files
sameenfatima78anfbermudezme
authored andcommitted
feat: look up remote_id by remote_id_field_name (openedx#37228)
1 parent ce6cb5a commit 59d2da3

4 files changed

Lines changed: 47 additions & 2 deletions

File tree

common/djangoapps/third_party_auth/api/serializers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,7 @@ def get_username(self, social_user):
2020

2121
def get_remote_id(self, social_user):
2222
""" Gets remote id from social user based on provider """
23+
remote_id_field_name = self.context.get('remote_id_field_name', None)
24+
if remote_id_field_name:
25+
return self.provider.get_remote_id_from_field_name(social_user, remote_id_field_name)
2326
return self.provider.get_remote_id_from_social_auth(social_user)

common/djangoapps/third_party_auth/api/tests/test_views.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@
3838
PASSWORD = "edx"
3939

4040

41-
def get_mapping_data_by_usernames(usernames):
41+
def get_mapping_data_by_usernames(usernames, remote_id_field_name=False):
4242
""" Generate mapping data used in response """
43+
if remote_id_field_name:
44+
return [{'username': username, 'remote_id': 'external_' + username} for username in usernames]
4345
return [{'username': username, 'remote_id': 'remote_' + username} for username in usernames]
4446

4547

@@ -76,11 +78,13 @@ def setUp(self): # pylint: disable=arguments-differ
7678
provider=google.backend_name,
7779
uid=f'{username}@gmail.com',
7880
)
79-
UserSocialAuth.objects.create(
81+
usa = UserSocialAuth.objects.create(
8082
user=user,
8183
provider=testshib.backend_name,
8284
uid=f'{testshib.slug}:remote_{username}',
8385
)
86+
usa.set_extra_data({'external_user_id': f'external_{username}'})
87+
usa.refresh_from_db()
8488
# Create another user not linked to any providers:
8589
UserFactory.create(username=CARL_USERNAME, email=f'{CARL_USERNAME}@example.com', password=PASSWORD)
8690

@@ -304,12 +308,20 @@ def test_list_all_user_mappings_oauth2(self, valid_call, expect_code, expect_dat
304308
@ddt.data(
305309
({'username': [ALICE_USERNAME, STAFF_USERNAME]}, 200,
306310
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
311+
({'username': [ALICE_USERNAME, STAFF_USERNAME], 'remote_id_field_name': 'external_user_id'}, 200,
312+
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
307313
({'remote_id': ['remote_' + ALICE_USERNAME, 'remote_' + STAFF_USERNAME, 'remote_' + CARL_USERNAME]}, 200,
308314
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
315+
({'remote_id': ['remote_' + ALICE_USERNAME, 'remote_' + STAFF_USERNAME, 'remote_' + CARL_USERNAME],
316+
'remote_id_field_name': 'external_user_id'}, 200,
317+
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
309318
({'username': [ALICE_USERNAME, CARL_USERNAME, STAFF_USERNAME]}, 200,
310319
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
311320
({'username': [ALICE_USERNAME], 'remote_id': ['remote_' + STAFF_USERNAME]}, 200,
312321
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
322+
({'username': [ALICE_USERNAME], 'remote_id': ['remote_' + STAFF_USERNAME],
323+
'remote_id_field_name': 'external_user_id'}, 200,
324+
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
313325
)
314326
@ddt.unpack
315327
def test_user_mappings_with_query_params_comma_separated(self, query_params, expect_code, expect_data):
@@ -321,19 +333,29 @@ def test_user_mappings_with_query_params_comma_separated(self, query_params, exp
321333
for attr in ['username', 'remote_id']:
322334
if attr in query_params:
323335
params.append('{}={}'.format(attr, ','.join(query_params[attr])))
336+
if 'remote_id_field_name' in query_params:
337+
params.append('remote_id_field_name={}'.format(query_params['remote_id_field_name']))
324338
url = "{}?{}".format(base_url, '&'.join(params))
325339
response = self.client.get(url, HTTP_X_EDX_API_KEY=VALID_API_KEY)
326340
self._verify_response(response, expect_code, expect_data)
327341

328342
@ddt.data(
329343
({'username': [ALICE_USERNAME, STAFF_USERNAME]}, 200,
330344
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
345+
({'username': [ALICE_USERNAME, STAFF_USERNAME], 'remote_id_field_name': 'external_user_id'}, 200,
346+
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
331347
({'remote_id': ['remote_' + ALICE_USERNAME, 'remote_' + STAFF_USERNAME, 'remote_' + CARL_USERNAME]}, 200,
332348
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
349+
({'remote_id': ['remote_' + ALICE_USERNAME, 'remote_' + STAFF_USERNAME, 'remote_' + CARL_USERNAME],
350+
'remote_id_field_name': 'external_user_id'}, 200,
351+
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
333352
({'username': [ALICE_USERNAME, CARL_USERNAME, STAFF_USERNAME]}, 200,
334353
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
335354
({'username': [ALICE_USERNAME], 'remote_id': ['remote_' + STAFF_USERNAME]}, 200,
336355
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
356+
({'username': [ALICE_USERNAME], 'remote_id': ['remote_' + STAFF_USERNAME],
357+
'remote_id_field_name': 'external_user_id'}, 200,
358+
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
337359
)
338360
@ddt.unpack
339361
def test_user_mappings_with_query_params_multi_value_key(self, query_params, expect_code, expect_data):
@@ -345,6 +367,8 @@ def test_user_mappings_with_query_params_multi_value_key(self, query_params, exp
345367
for attr in ['username', 'remote_id']:
346368
if attr in query_params:
347369
params.setlist(attr, query_params[attr])
370+
if 'remote_id_field_name' in query_params:
371+
params['remote_id_field_name'] = query_params['remote_id_field_name']
348372
url = f"{base_url}?{params.urlencode()}"
349373
response = self.client.get(url, HTTP_X_EDX_API_KEY=VALID_API_KEY)
350374
self._verify_response(response, expect_code, expect_data)

common/djangoapps/third_party_auth/api/views.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,9 @@ class UserMappingView(ListAPIView):
323323
324324
GET /api/third_party_auth/v0/providers/{provider_id}/users?username={username1},{username2}
325325
326+
GET /api/third_party_auth/v0/providers/{provider_id}/users?username={username1}&
327+
remote_id_field_name={external_id_field_name}
328+
326329
GET /api/third_party_auth/v0/providers/{provider_id}/users?username={username1}&usernames={username2}
327330
328331
GET /api/third_party_auth/v0/providers/{provider_id}/users?remote_id={remote_id1},{remote_id2}
@@ -346,6 +349,9 @@ class UserMappingView(ListAPIView):
346349
* usernames: Optional. List of comma separated edX usernames to filter the result set.
347350
e.g. ?usernames=bob123,jane456
348351
352+
* remote_id_field_name: Optional. The field name to use for the remote id lookup.
353+
Useful when learners are coming from external LMS. e.g. ?remote_id_field_name=ext_userid_sf
354+
349355
* page, page_size: Optional. Used for paging the result set, especially when getting
350356
an unfiltered list.
351357
@@ -415,6 +421,7 @@ def get_serializer_context(self):
415421
remove idp_slug from the remote_id if there is any
416422
"""
417423
context = super().get_serializer_context()
424+
context['remote_id_field_name'] = self.request.query_params.get('remote_id_field_name', None)
418425
context['provider'] = self.provider
419426

420427
return context

common/djangoapps/third_party_auth/models.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,17 @@ def match_social_auth(self, social_auth):
810810
prefix = self.slug + ":"
811811
return self.backend_name == social_auth.provider and social_auth.uid.startswith(prefix)
812812

813+
def get_remote_id_from_field_name(self, social_auth, field_name):
814+
""" Given a UserSocialAuth object, return the user remote ID against the field name provided. """
815+
if not self.match_social_auth(social_auth):
816+
raise ValueError(
817+
f"UserSocialAuth record does not match given provider {self.provider_id}"
818+
)
819+
field_value = social_auth.extra_data.get(field_name, None)
820+
if field_value and isinstance(field_value, list):
821+
return field_value[0]
822+
return field_value
823+
813824
def get_remote_id_from_social_auth(self, social_auth):
814825
""" Given a UserSocialAuth object, return the remote ID used by this provider. """
815826
assert self.match_social_auth(social_auth)

0 commit comments

Comments
 (0)