""" Third Party Auth REST API views """ from django.contrib.auth.models import User from django.db.models import Q from django.http import Http404 from rest_framework import exceptions, status from rest_framework.generics import ListAPIView from rest_framework.response import Response from rest_framework.views import APIView from rest_framework_oauth.authentication import OAuth2Authentication from social_django.models import UserSocialAuth from openedx.core.lib.api.authentication import ( OAuth2AuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser ) from openedx.core.lib.api.permissions import ApiKeyHeaderPermission from third_party_auth import pipeline from third_party_auth.api import serializers from third_party_auth.api.permissions import ThirdPartyAuthProviderApiPermission from third_party_auth.provider import Registry class UserView(APIView): """ List the third party auth accounts linked to the specified user account. **Example Request** GET /api/third_party_auth/v0/users/{username} **Response Values** If the request for information about the user is successful, an HTTP 200 "OK" response is returned. The HTTP 200 response has the following values. * active: A list of all the third party auth providers currently linked to the given user's account. Each object in this list has the following attributes: * provider_id: The unique identifier of this provider (string) * name: The name of this provider (string) * remote_id: The ID of the user according to the provider. This ID is what is used to link the user to their edX account during login. """ authentication_classes = ( # Users may want to view/edit the providers used for authentication before they've # activated their account, so we allow inactive users. OAuth2AuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser, ) def get(self, request, username): """Create, read, or update enrollment information for a user. HTTP Endpoint for all CRUD operations for a user course enrollment. Allows creation, reading, and updates of the current enrollment for a particular course. Args: request (Request): The HTTP GET request username (str): Fetch the list of providers linked to this user Return: JSON serialized list of the providers linked to this user. """ if request.user.username != username: # We are querying permissions for a user other than the current user. if not request.user.is_superuser and not ApiKeyHeaderPermission().has_permission(request, self): # Return a 403 (Unauthorized) without validating 'username', so that we # do not let users probe the existence of other user accounts. return Response(status=status.HTTP_403_FORBIDDEN) try: user = User.objects.get(username=username) except User.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) providers = pipeline.get_provider_user_states(user) active_providers = [ { "provider_id": assoc.provider.provider_id, "name": assoc.provider.name, "remote_id": assoc.remote_id, } for assoc in providers if assoc.has_account ] # In the future this can be trivially modified to return the inactive/disconnected providers as well. return Response({ "active": active_providers }) class UserMappingView(ListAPIView): """ Map between the third party auth account IDs (remote_id) and EdX username. This API is intended to be a server-to-server endpoint. An on-campus middleware or system should consume this. **Use Case** Get a paginated list of mappings between edX users and remote user IDs for all users currently linked to the given backend. The list can be filtered by edx username or third party ids. The filter is limited by the max length of URL. It is suggested to query no more than 50 usernames or remote_ids in each request to stay within above limitation The page size can be changed by specifying `page_size` parameter in the request. **Example Requests** GET /api/third_party_auth/v0/providers/{provider_id}/users 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}&usernames={username2} GET /api/third_party_auth/v0/providers/{provider_id}/users?remote_id={remote_id1},{remote_id2} GET /api/third_party_auth/v0/providers/{provider_id}/users?remote_id={remote_id1}&remote_id={remote_id2} GET /api/third_party_auth/v0/providers/{provider_id}/users?username={username1}&remote_id={remote_id1} **URL Parameters** * provider_id: The unique identifier of third_party_auth provider (e.g. "saml-ubc", "oa2-google", etc. This is not the same thing as the backend_name.). (Optional/future: We may also want to allow this to be an 'external domain' like 'ssl:MIT' so that this API can also search the legacy ExternalAuthMap table used by Standford/MIT) **Query Parameters** * remote_ids: Optional. List of comma separated remote (third party) user IDs to filter the result set. e.g. ?remote_ids=8721384623 * usernames: Optional. List of comma separated edX usernames to filter the result set. e.g. ?usernames=bob123,jane456 * page, page_size: Optional. Used for paging the result set, especially when getting an unfiltered list. **Response Values** If the request for information about the user is successful, an HTTP 200 "OK" response is returned. The HTTP 200 response has the following values: * count: The number of mappings for the backend. * next: The URI to the next page of the mappings. * previous: The URI to the previous page of the mappings. * num_pages: The number of pages listing the mappings. * results: A list of mappings returned. Each collection in the list contains these fields. * username: The edx username * remote_id: The Id from third party auth provider """ authentication_classes = ( OAuth2Authentication, ) serializer_class = serializers.UserMappingSerializer provider = None def get_queryset(self): provider_id = self.kwargs.get('provider_id') # permission checking. We allow both API_KEY access and OAuth2 client credential access if not ( self.request.user.is_superuser or ApiKeyHeaderPermission().has_permission(self.request, self) or ThirdPartyAuthProviderApiPermission(provider_id).has_permission(self.request, self) ): raise exceptions.PermissionDenied() # provider existence checking self.provider = Registry.get(provider_id) if not self.provider: raise Http404 query_set = UserSocialAuth.objects.select_related('user').filter(provider=self.provider.backend_name) # build our query filters # When using multi-IdP backend, we only retrieve the ones that are for current IdP. # test if the current provider has a slug uid = self.provider.get_social_auth_uid('uid') if uid is not 'uid': # if yes, we add a filter for the slug on uid column query_set = query_set.filter(uid__startswith=uid[:-3]) query = Q() usernames = self.request.query_params.getlist('username', None) remote_ids = self.request.query_params.getlist('remote_id', None) if usernames: usernames = ','.join(usernames) usernames = set(usernames.split(',')) if usernames else set() if len(usernames): query = query | Q(user__username__in=usernames) if remote_ids: remote_ids = ','.join(remote_ids) remote_ids = set(remote_ids.split(',')) if remote_ids else set() if len(remote_ids): query = query | Q(uid__in=[self.provider.get_social_auth_uid(remote_id) for remote_id in remote_ids]) return query_set.filter(query) def get_serializer_context(self): """ Extra context provided to the serializer class with current provider. We need the provider to remove idp_slug from the remote_id if there is any """ context = super(UserMappingView, self).get_serializer_context() context['provider'] = self.provider return context