feat: Add SSO History for Support
This commit is contained in:
committed by
Ansab Gillani
parent
4119e60fa9
commit
4af3d58b39
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user