feat: add SSO History to support
This commit is contained in:
committed by
Ansab Gillani
parent
aba1f052df
commit
d7a60fd21b
@@ -0,0 +1,48 @@
|
||||
1. Registering SSO History Model in Support
|
||||
================================================
|
||||
|
||||
Status
|
||||
------
|
||||
|
||||
Accepted
|
||||
|
||||
Context
|
||||
-------
|
||||
|
||||
SSO History was one of the feature requested by support that provides the
|
||||
historical data of any particular SSO record (based on UserSocialAuth)
|
||||
in tools UI via support API. This SSO History can be utilized to track id changes,
|
||||
Additional data or any other relevant data changes inside SSO model.
|
||||
|
||||
Although the UserSocialAuth is applied within common apps but is not configured for
|
||||
cms. This has caused major breakage and a temporary outage in the authentication flow
|
||||
in cms stage.
|
||||
|
||||
Decision
|
||||
--------
|
||||
|
||||
The simple_django_history registration for UserSocialAuth model
|
||||
is introduced in the Support app for LMS instead of the
|
||||
third_party_auth in Common.
|
||||
|
||||
Consequences
|
||||
------------
|
||||
|
||||
The most optimum method to introduce the feature was to register the model
|
||||
in support app and get the data via support API.
|
||||
|
||||
Alternative/Rejected Approaches
|
||||
------------
|
||||
|
||||
Addition for third_party_auth was attempted for the studio,
|
||||
but failed in the stage. The primary reason was the failing migration
|
||||
tests in CMS with the current configurations in the studio.
|
||||
We tried to add the third_party_auth as an installed app on Studio
|
||||
but later found out that Studio is not configured for third_party_auth
|
||||
and configuring third_party_auth on studio would have caused auth issues.
|
||||
Consequently, there was over 6 hours of pipeline outage on stage
|
||||
and we had to revert the changes made in the system.
|
||||
|
||||
Third party auth is primarily LMS-only app but since it has been in common,
|
||||
we opted to go ahead with adding history in common,
|
||||
only to later realize the impact of enabling third party auth in studio.
|
||||
42
lms/djangoapps/support/migrations/0001_initial.py
Normal file
42
lms/djangoapps/support/migrations/0001_initial.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# Generated by Django 3.2.13 on 2022-05-17 08:26
|
||||
|
||||
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):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
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),
|
||||
),
|
||||
]
|
||||
0
lms/djangoapps/support/migrations/__init__.py
Normal file
0
lms/djangoapps/support/migrations/__init__.py
Normal file
9
lms/djangoapps/support/models.py
Normal file
9
lms/djangoapps/support/models.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
Models used to implement support related models in such as SSO History model
|
||||
"""
|
||||
|
||||
from simple_history import register
|
||||
from social_django.models import UserSocialAuth
|
||||
|
||||
# Registers UserSocialAuth with simple-django-history.
|
||||
register(UserSocialAuth, app=__package__)
|
||||
@@ -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,24 @@ class SsoRecordsTests(SupportViewTestCase): # lint-amnesty, pylint: disable=mis
|
||||
assert len(data) == 1
|
||||
self.assertContains(response, '"uid": "test@example.com"')
|
||||
|
||||
def test_history_response(self):
|
||||
'''Tests changes in SSO history for a user'''
|
||||
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,6 +19,7 @@ class SsoView(GenericAPIView):
|
||||
"""
|
||||
Returns a list of SSO records for a given user.
|
||||
Sample response:
|
||||
Sample response:
|
||||
[
|
||||
{
|
||||
"provider": "tpa-saml",
|
||||
@@ -26,6 +27,25 @@ class SsoView(GenericAPIView):
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"""
|
||||
@@ -36,5 +56,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