feat: add SSO History to support

This commit is contained in:
ansabgillani
2022-05-17 13:30:23 +05:00
committed by Ansab Gillani
parent aba1f052df
commit d7a60fd21b
7 changed files with 169 additions and 11 deletions

View File

@@ -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.

View 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),
),
]

View 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__)

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,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):
"""

View File

@@ -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)