feat: look up remote_id by remote_id_field_name (#37228)

This commit is contained in:
Sameen Fatima
2025-11-13 09:20:26 +05:00
committed by GitHub
parent 4804c980d4
commit 8ca34141a6
4 changed files with 47 additions and 2 deletions

View File

@@ -20,4 +20,7 @@ class UserMappingSerializer(serializers.Serializer): # pylint: disable=abstract
def get_remote_id(self, social_user):
""" Gets remote id from social user based on provider """
remote_id_field_name = self.context.get('remote_id_field_name', None)
if remote_id_field_name:
return self.provider.get_remote_id_from_field_name(social_user, remote_id_field_name)
return self.provider.get_remote_id_from_social_auth(social_user)

View File

@@ -38,8 +38,10 @@ LINKED_USERS = (ALICE_USERNAME, STAFF_USERNAME, ADMIN_USERNAME)
PASSWORD = "edx"
def get_mapping_data_by_usernames(usernames):
def get_mapping_data_by_usernames(usernames, remote_id_field_name=False):
""" Generate mapping data used in response """
if remote_id_field_name:
return [{'username': username, 'remote_id': 'external_' + username} for username in usernames]
return [{'username': username, 'remote_id': 'remote_' + username} for username in usernames]
@@ -76,11 +78,13 @@ class TpaAPITestCase(ThirdPartyAuthTestMixin, APITestCase):
provider=google.backend_name,
uid=f'{username}@gmail.com',
)
UserSocialAuth.objects.create(
usa = UserSocialAuth.objects.create(
user=user,
provider=testshib.backend_name,
uid=f'{testshib.slug}:remote_{username}',
)
usa.set_extra_data({'external_user_id': f'external_{username}'})
usa.refresh_from_db()
# Create another user not linked to any providers:
UserFactory.create(username=CARL_USERNAME, email=f'{CARL_USERNAME}@example.com', password=PASSWORD)
@@ -304,12 +308,20 @@ class UserMappingViewAPITests(TpaAPITestCase):
@ddt.data(
({'username': [ALICE_USERNAME, STAFF_USERNAME]}, 200,
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
({'username': [ALICE_USERNAME, STAFF_USERNAME], 'remote_id_field_name': 'external_user_id'}, 200,
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
({'remote_id': ['remote_' + ALICE_USERNAME, 'remote_' + STAFF_USERNAME, 'remote_' + CARL_USERNAME]}, 200,
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
({'remote_id': ['remote_' + ALICE_USERNAME, 'remote_' + STAFF_USERNAME, 'remote_' + CARL_USERNAME],
'remote_id_field_name': 'external_user_id'}, 200,
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
({'username': [ALICE_USERNAME, CARL_USERNAME, STAFF_USERNAME]}, 200,
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
({'username': [ALICE_USERNAME], 'remote_id': ['remote_' + STAFF_USERNAME]}, 200,
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
({'username': [ALICE_USERNAME], 'remote_id': ['remote_' + STAFF_USERNAME],
'remote_id_field_name': 'external_user_id'}, 200,
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
)
@ddt.unpack
def test_user_mappings_with_query_params_comma_separated(self, query_params, expect_code, expect_data):
@@ -321,6 +333,8 @@ class UserMappingViewAPITests(TpaAPITestCase):
for attr in ['username', 'remote_id']:
if attr in query_params:
params.append('{}={}'.format(attr, ','.join(query_params[attr])))
if 'remote_id_field_name' in query_params:
params.append('remote_id_field_name={}'.format(query_params['remote_id_field_name']))
url = "{}?{}".format(base_url, '&'.join(params))
response = self.client.get(url, HTTP_X_EDX_API_KEY=VALID_API_KEY)
self._verify_response(response, expect_code, expect_data)
@@ -328,12 +342,20 @@ class UserMappingViewAPITests(TpaAPITestCase):
@ddt.data(
({'username': [ALICE_USERNAME, STAFF_USERNAME]}, 200,
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
({'username': [ALICE_USERNAME, STAFF_USERNAME], 'remote_id_field_name': 'external_user_id'}, 200,
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
({'remote_id': ['remote_' + ALICE_USERNAME, 'remote_' + STAFF_USERNAME, 'remote_' + CARL_USERNAME]}, 200,
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
({'remote_id': ['remote_' + ALICE_USERNAME, 'remote_' + STAFF_USERNAME, 'remote_' + CARL_USERNAME],
'remote_id_field_name': 'external_user_id'}, 200,
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
({'username': [ALICE_USERNAME, CARL_USERNAME, STAFF_USERNAME]}, 200,
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
({'username': [ALICE_USERNAME], 'remote_id': ['remote_' + STAFF_USERNAME]}, 200,
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
({'username': [ALICE_USERNAME], 'remote_id': ['remote_' + STAFF_USERNAME],
'remote_id_field_name': 'external_user_id'}, 200,
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
)
@ddt.unpack
def test_user_mappings_with_query_params_multi_value_key(self, query_params, expect_code, expect_data):
@@ -345,6 +367,8 @@ class UserMappingViewAPITests(TpaAPITestCase):
for attr in ['username', 'remote_id']:
if attr in query_params:
params.setlist(attr, query_params[attr])
if 'remote_id_field_name' in query_params:
params['remote_id_field_name'] = query_params['remote_id_field_name']
url = f"{base_url}?{params.urlencode()}"
response = self.client.get(url, HTTP_X_EDX_API_KEY=VALID_API_KEY)
self._verify_response(response, expect_code, expect_data)

View File

@@ -323,6 +323,9 @@ class UserMappingView(ListAPIView):
GET /api/third_party_auth/v0/providers/{provider_id}/users?username={username1},{username2}
GET /api/third_party_auth/v0/providers/{provider_id}/users?username={username1}&
remote_id_field_name={external_id_field_name}
GET /api/third_party_auth/v0/providers/{provider_id}/users?username={username1}&usernames={username2}
GET /api/third_party_auth/v0/providers/{provider_id}/users?remote_id={remote_id1},{remote_id2}
@@ -346,6 +349,9 @@ class UserMappingView(ListAPIView):
* usernames: Optional. List of comma separated edX usernames to filter the result set.
e.g. ?usernames=bob123,jane456
* remote_id_field_name: Optional. The field name to use for the remote id lookup.
Useful when learners are coming from external LMS. e.g. ?remote_id_field_name=ext_userid_sf
* page, page_size: Optional. Used for paging the result set, especially when getting
an unfiltered list.
@@ -415,6 +421,7 @@ class UserMappingView(ListAPIView):
remove idp_slug from the remote_id if there is any
"""
context = super().get_serializer_context()
context['remote_id_field_name'] = self.request.query_params.get('remote_id_field_name', None)
context['provider'] = self.provider
return context

View File

@@ -810,6 +810,17 @@ class SAMLProviderConfig(ProviderConfig):
prefix = self.slug + ":"
return self.backend_name == social_auth.provider and social_auth.uid.startswith(prefix)
def get_remote_id_from_field_name(self, social_auth, field_name):
""" Given a UserSocialAuth object, return the user remote ID against the field name provided. """
if not self.match_social_auth(social_auth):
raise ValueError(
f"UserSocialAuth record does not match given provider {self.provider_id}"
)
field_value = social_auth.extra_data.get(field_name, None)
if field_value and isinstance(field_value, list):
return field_value[0]
return field_value
def get_remote_id_from_social_auth(self, social_auth):
""" Given a UserSocialAuth object, return the remote ID used by this provider. """
assert self.match_social_auth(social_auth)