feat: Add SSO History for Support

This commit is contained in:
ansabgillani
2022-03-15 16:45:53 +05:00
committed by Ansab Gillani
parent 4119e60fa9
commit 4af3d58b39
8 changed files with 133 additions and 14 deletions

View File

@@ -1673,6 +1673,7 @@ INSTALLED_APPS = [
# These are apps that aren't strictly needed by Studio, but are imported by
# other apps that are. Django 1.8 wants to have imported models supported
# by installed apps.
'common.djangoapps.third_party_auth',
'openedx.core.djangoapps.oauth_dispatch.apps.OAuthDispatchAppConfig',
'lms.djangoapps.courseware',
'lms.djangoapps.coursewarehistoryextended',

View File

@@ -295,7 +295,6 @@ SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
######### custom courses #########
INSTALLED_APPS += [
'openedx.core.djangoapps.ccxcon.apps.CCXConnectorConfig',
'common.djangoapps.third_party_auth.apps.ThirdPartyAuthConfig',
]
FEATURES['CUSTOM_COURSES_EDX'] = True

View File

@@ -0,0 +1,41 @@
# Generated by Django 3.2.12 on 2022-04-15 01:04
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import simple_history.models
import social_django.fields
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('third_party_auth', '0008_auto_20220324_1422'),
]
operations = [
migrations.CreateModel(
name='HistoricalUserSocialAuth',
fields=[
('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('provider', models.CharField(max_length=32)),
('uid', models.CharField(db_index=True, max_length=255)),
('extra_data', social_django.fields.JSONField(default=dict)),
('created', models.DateTimeField(blank=True, editable=False)),
('modified', models.DateTimeField(blank=True, editable=False)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField()),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical user social auth',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': 'history_date',
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
]

View File

@@ -9,6 +9,8 @@ import logging
import re
from config_models.models import ConfigurationModel, cache
from simple_history import register
from social_django.models import UserSocialAuth
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.exceptions import ValidationError
@@ -37,6 +39,11 @@ REGISTRATION_FORM_FIELD_BLACKLIST = [
'username'
]
# Registers UserSocialAuth with simple-django-history.
# This registration makes third_party_auth a required app for Studio,
# even when it is supposed to be for LMS only.
register(UserSocialAuth, app=__package__)
# A dictionary of {name: class} entries for each python-social-auth backend available.
# Because this setting can specify arbitrary code to load and execute, it is set via

View File

@@ -84,17 +84,35 @@ def serialize_user_info(user, user_social_auths=None):
return user_info
def serialize_sso_records(user_social_auths):
def serialize_sso_records(user_social_auth, user_social_auths_history):
"""
Serialize user social auth model object
"""
sso_records = []
for user_social_auth in user_social_auths:
sso_records.append({
'provider': user_social_auth.provider,
'uid': user_social_auth.uid,
'created': user_social_auth.created,
'modified': user_social_auth.modified,
'extraData': json.dumps(user_social_auth.extra_data),
})
sso_records = {
'provider': user_social_auth.provider,
'uid': user_social_auth.uid,
'created': user_social_auth.created,
'modified': user_social_auth.modified,
'history': serialize_sso_history(
user_social_auths_history
),
'extraData': json.dumps(user_social_auth.extra_data),
}
return sso_records
def serialize_sso_history(user_social_auths_history):
"""
Serialize history for user social auth model object
"""
history = []
for sso_history in user_social_auths_history:
history.append({
'uid': sso_history.uid,
'provider': sso_history.provider,
'created': sso_history.created,
'modified': sso_history.modified,
'extraData': json.dumps(sso_history.extra_data),
'history_date': sso_history.history_date
})
return history

View File

@@ -1542,6 +1542,23 @@ class SsoRecordsTests(SupportViewTestCase): # lint-amnesty, pylint: disable=mis
assert len(data) == 1
self.assertContains(response, '"uid": "test@example.com"')
def test_history_response(self):
user_social_auth = UserSocialAuth.objects.create( # lint-amnesty, pylint: disable=unused-variable
user=self.student,
uid=self.student.email,
provider='tpa-saml'
)
sso = UserSocialAuth.objects.get(user=self.student)
sso.uid = self.student.email + ':' + sso.provider
sso.save()
response = self.client.get(self.url)
data = json.loads(response.content.decode('utf-8'))
assert response.status_code == 200
assert len(data) == 1
assert len(data[0].get('history')) == 2
assert data[0].get('history')[0].get('uid') == "test@example.com:tpa-saml"
assert data[0].get('history')[1].get('uid') == "test@example.com"
class FeatureBasedEnrollmentSupportApiViewTests(SupportViewTestCase):
"""

View File

@@ -19,7 +19,9 @@ from .views.program_enrollments import (
SAMLProvidersWithOrg,
ProgramEnrollmentsInspectorAPIView,
)
from .views.sso_records import SsoView
from .views.sso_records import (
SsoView,
)
from .views.onboarding_status import OnboardingView
COURSE_ENTITLEMENTS_VIEW = EntitlementSupportView.as_view()

View File

@@ -10,12 +10,43 @@ from social_django.models import UserSocialAuth
from common.djangoapps.util.json_request import JsonResponse
from lms.djangoapps.support.decorators import require_support_permission
from lms.djangoapps.support.serializers import serialize_sso_records
from lms.djangoapps.support.serializers import (
serialize_sso_records,
)
class SsoView(GenericAPIView):
"""
Returns a list of SSO records for a given user.
Sample response:
[
{
"provider": "tpa-saml",
"uid": "new-channel:testuser",
"created": "2022-03-02T04:41:33.145Z",
"modified": "2022-03-15T11:28:17.809Z",
"extraData": "{}",
"history":
[
{
"uid": "new-channel:testuser",
"provider": "tpa-saml",
"created": "2022-03-02T04:41:33.145Z",
"modified": "2022-03-15T11:28:17.809Z",
"extraData": "{}",
"history_date": "2022-03-15T11:28:17.832Z"
},
{
"uid": "default-channel:testuser",
"provider": "tpa-saml",
"created": "2022-03-02T04:41:33.145Z",
"modified": "2022-03-10T12:28:32.720Z",
"extraData": "{}",
"history_date": "2022-03-15T11:12:02.420Z"
}
]
}
]
"""
@method_decorator(require_support_permission)
def get(self, request, username_or_email): # lint-amnesty, pylint: disable=missing-function-docstring
@@ -24,5 +55,8 @@ class SsoView(GenericAPIView):
except User.DoesNotExist:
return JsonResponse([])
user_social_auths = UserSocialAuth.objects.filter(user=user)
sso_records = serialize_sso_records(user_social_auths)
sso_records = []
for user_social_auth in user_social_auths:
user_social_auths_history = UserSocialAuth.history.filter(id=user_social_auth.id)
sso_records.append(serialize_sso_records(user_social_auth, user_social_auths_history))
return JsonResponse(sso_records)