Browse Source

DIV-1159 - converting the app to use OIDC authentication

pull/172/head
Michael Olund 5 years ago
parent
commit
ca928efd60
12 changed files with 237 additions and 57 deletions
  1. +29
    -0
      edivorce/apps/core/middleware/keycloak.py
  2. +10
    -0
      edivorce/apps/core/migrations/0001_initial.py
  3. +0
    -9
      edivorce/apps/core/migrations/0007_auto_20170210_1702.py
  4. +110
    -0
      edivorce/apps/core/migrations/0024_auto_20201009_1235.py
  5. +1
    -1
      edivorce/apps/core/migrations/0024_bceiduser_is_bcsc.py
  6. +5
    -10
      edivorce/apps/core/models.py
  7. +7
    -1
      edivorce/apps/core/static/css/main.scss
  8. +6
    -2
      edivorce/apps/core/templates/base.html
  9. +16
    -9
      edivorce/apps/core/tests/test_api.py
  10. +9
    -6
      edivorce/settings/base.py
  11. +12
    -5
      requirements.txt
  12. +32
    -14
      vue/src/components/Uploader/Uploader.vue

+ 29
- 0
edivorce/apps/core/middleware/keycloak.py View File

@ -1,4 +1,5 @@
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
from ..models import BceidUser
class EDivorceKeycloakBackend(OIDCAuthenticationBackend):
@ -8,3 +9,31 @@ class EDivorceKeycloakBackend(OIDCAuthenticationBackend):
print(claims)
return verified
def create_user(self, claims):
user = super(EDivorceKeycloakBackend, self).create_user(claims)
user.first_name = claims.get('given_name', '')
user.last_name = claims.get('family_name', '')
user.display_name = "{} {}".format(user.first_name, user.last_name).strip()
user.sm_user = claims.get('preferred_username', '')
user.user_guid = claims.get('universal-id', '')
user.save()
return user
def update_user(self, user, claims):
user.first_name = claims.get('given_name', '')
user.last_name = claims.get('family_name', '')
user.display_name = "{} {}".format(user.first_name, user.last_name).strip()
user.sm_user = claims.get('preferred_username', '')
user.user_guid = claims.get('universal-id', '')
user.save()
return user
def filter_users_by_claims(self, claims):
user_guid = claims.get('universal-id')
if not user_guid:
return self.UserModel.objects.none()
return self.UserModel.objects.filter(user_guid=user_guid)

+ 10
- 0
edivorce/apps/core/migrations/0001_initial.py View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.utils.timezone
class Migration(migrations.Migration):
@ -12,6 +13,15 @@ class Migration(migrations.Migration):
]
operations = [
migrations.CreateModel(
name='BceidUser',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('user_guid', models.CharField(unique=True, max_length=36, db_index=True)),
('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
('last_login', models.DateTimeField(default=django.utils.timezone.now)),
],
),
migrations.CreateModel(
name='FormQuestions',
fields=[


+ 0
- 9
edivorce/apps/core/migrations/0007_auto_20170210_1702.py View File

@ -12,15 +12,6 @@ class Migration(migrations.Migration):
]
operations = [
migrations.CreateModel(
name='BceidUser',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('user_guid', models.CharField(unique=True, max_length=36, db_index=True)),
('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
('last_login', models.DateTimeField(default=django.utils.timezone.now)),
],
),
migrations.RemoveField(
model_name='profile',
name='user',


+ 110
- 0
edivorce/apps/core/migrations/0024_auto_20201009_1235.py View File

@ -0,0 +1,110 @@
# Generated by Django 2.2.15 on 2020-10-09 21:12
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
def set_username(apps, schema_editor):
Series = apps.get_model('core', 'bceiduser')
for series in Series.objects.all().iterator():
if series.sm_user:
series.username = series.sm_user
else:
series.username = 'user' + str(series.id)
try:
series.save()
except:
series.username = 'user' + str(series.id)
series.save()
def reverse_func(apps, schema_editor):
pass # code for reverting migration, if any
class Migration(migrations.Migration):
dependencies = [
('core', '0023_auto_20201006_1314'),
('auth', '0011_update_proxy_permissions'),
]
operations = [
migrations.AlterModelOptions(
name='bceiduser',
options={'verbose_name': 'user', 'verbose_name_plural': 'users'},
),
migrations.AlterModelManagers(
name='bceiduser',
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.AddField(
model_name='bceiduser',
name='email',
field=models.EmailField(blank=True, max_length=254, verbose_name='email address'),
),
migrations.AddField(
model_name='bceiduser',
name='first_name',
field=models.CharField(blank=True, max_length=30, verbose_name='first name'),
),
migrations.AddField(
model_name='bceiduser',
name='groups',
field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'),
),
migrations.AddField(
model_name='bceiduser',
name='is_superuser',
field=models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status'),
),
migrations.AddField(
model_name='bceiduser',
name='last_name',
field=models.CharField(blank=True, max_length=150, verbose_name='last name'),
),
migrations.AddField(
model_name='bceiduser',
name='password',
field=models.CharField(default='', max_length=128, verbose_name='password'),
preserve_default=False,
),
migrations.AddField(
model_name='bceiduser',
name='user_permissions',
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
),
migrations.AddField(
model_name='bceiduser',
name='username',
field=models.CharField(blank=True, default='', max_length=150),
preserve_default=False,
),
migrations.RunPython(set_username, reverse_func),
migrations.AlterField(
model_name='bceiduser',
name='username',
field=models.CharField(default='', error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'),
preserve_default=False,
),
migrations.AlterField(
model_name='bceiduser',
name='date_joined',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined'),
),
migrations.AlterField(
model_name='bceiduser',
name='last_login',
field=models.DateTimeField(blank=True, null=True, verbose_name='last login'),
),
migrations.AddField(
model_name='bceiduser',
name='is_staff',
field=models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status'),
),
]

+ 1
- 1
edivorce/apps/core/migrations/0024_bceiduser_is_bcsc.py View File

@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0023_auto_20201006_1314'),
('core', '0024_auto_20201009_1235'),
]
operations = [


+ 5
- 10
edivorce/apps/core/models.py View File

@ -6,12 +6,13 @@ from django.db.models import F
from django.urls import reverse
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from django.contrib.auth.models import AbstractUser
from edivorce.apps.core import redis
@python_2_unicode_compatible
class BceidUser(models.Model):
class BceidUser(AbstractUser):
"""
BCeID user table
"""
@ -25,12 +26,6 @@ class BceidUser(models.Model):
sm_user = models.TextField(blank=True)
""" SiteMinder user value """
date_joined = models.DateTimeField(default=timezone.now)
""" First login timestamp """
last_login = models.DateTimeField(default=timezone.now)
""" Most recent login timestamp """
has_seen_orders_page = models.BooleanField(default=False)
""" Flag for intercept page """
@ -48,9 +43,9 @@ class BceidUser(models.Model):
def is_anonymous(self):
return False
is_staff = True
is_active = True
@property
def is_active(self):
return True
def has_module_perms(self, *args):
return True


+ 7
- 1
edivorce/apps/core/static/css/main.scss View File

@ -983,12 +983,18 @@ textarea {
float: right;
margin-top: 16px;
a {
input[type=submit] {
color: #ffffff;
background-color: transparent;
display: inline;
padding: 0;
line-height: 1.5;
border: none;
&:active,
&:hover {
color: #fcfcfc;
text-decoration: underline;
}
}
}


+ 6
- 2
edivorce/apps/core/templates/base.html View File

@ -54,8 +54,12 @@
<div class="top_banner-user">
{% if request.user.is_authenticated %}
<span>
{{ request.user.display_name}}
&nbsp;&nbsp;|&nbsp;&nbsp; <a href="{% url 'oidc_logout' %}">Log out</a>
<form action="{% url 'oidc_logout' %}" method="post">
{{ request.user.display_name}}
&nbsp;&nbsp;|&nbsp;&nbsp;
{% csrf_token %}
<input type="submit" value="Log out">
</form>
</span>
{% endif %}
</div>


+ 16
- 9
edivorce/apps/core/tests/test_api.py View File

@ -44,8 +44,8 @@ class MockRedis:
@override_settings(CLAMAV_ENABLED=False)
class APITest(APITestCase):
def setUp(self):
self.user = BceidUser.objects.create(user_guid='1234')
self.another_user = BceidUser.objects.create(user_guid='5678')
self.user = _get_or_create_user(user_guid='1234')
self.another_user = _get_or_create_user(user_guid='5678')
self.client = APIClient()
self.default_doc_type = 'MC'
self.default_party_code = 0
@ -53,10 +53,10 @@ class APITest(APITestCase):
def test_get_documents(self):
url = reverse('documents')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
response = self.client.post(url, {})
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
self.client.force_authenticate(self.user)
response = self.client.get(url)
@ -195,7 +195,7 @@ class APITest(APITestCase):
url = document.get_file_url()
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
self.client.force_authenticate(self.another_user)
response = self.client.get(url)
@ -246,7 +246,7 @@ class APITest(APITestCase):
url = document.get_file_url()
response = self.client.delete(url)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
self.assertEqual(Document.objects.count(), 1)
self.client.force_authenticate(self.another_user)
@ -267,7 +267,7 @@ class APITest(APITestCase):
'rotation': 90
}
response = self.client.put(url, data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
self.client.force_authenticate(self.another_user)
response = self.client.put(url, data)
@ -313,8 +313,8 @@ class GraphQLAPITest(GraphQLTestCase):
GRAPHQL_URL = reverse('graphql')
def setUp(self):
self.user = BceidUser.objects.create(user_guid='1234')
self.another_user = BceidUser.objects.create(user_guid='5678')
self.user = _get_or_create_user(user_guid='1234')
self.another_user = _get_or_create_user(user_guid='5678')
self.default_doc_type = 'MC'
self.default_party_code = 0
@ -512,3 +512,10 @@ def _create_file(extension='jpg'):
num_documents = Document.objects.count()
new_file = SimpleUploadedFile(f'test_file_{num_documents + 1}.{extension}', b'test content')
return new_file
def _get_or_create_user(user_guid):
try:
return BceidUser.objects.get(user_guid=user_guid)
except BceidUser.DoesNotExist:
return BceidUser.objects.create(user_guid=user_guid, username=user_guid)

+ 9
- 6
edivorce/settings/base.py View File

@ -79,6 +79,8 @@ MIDDLEWARE = (
'whitenoise.middleware.WhiteNoiseMiddleware',
)
AUTH_USER_MODEL = 'core.BceidUser'
AUTHENTICATION_BACKENDS = (
'edivorce.apps.core.middleware.keycloak.EDivorceKeycloakBackend',
)
@ -106,11 +108,12 @@ WSGI_APPLICATION = 'wsgi.application'
# need to disable auth in Django Rest Framework so it doesn't get triggered
# by presence of Basic Auth headers
# REST_FRAMEWORK = {
# 'DEFAULT_AUTHENTICATION_CLASSES': [
# 'edivorce.apps.core.authenticators.BCeIDAuthentication',
# ]
# }
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'mozilla_django_oidc.contrib.drf.OIDCAuthentication',
'rest_framework.authentication.SessionAuthentication'
]
}
LOGGING = {
@ -208,5 +211,5 @@ OIDC_RP_CLIENT_SECRET = env('OIDC_RP_CLIENT_SECRET', '')
OIDC_OP_AUTHORIZATION_ENDPOINT = env('OIDC_OP_AUTHORIZATION_ENDPOINT', '')
OIDC_OP_TOKEN_ENDPOINT = env('OIDC_OP_TOKEN_ENDPOINT', '')
OIDC_OP_USER_ENDPOINT = env('OIDC_OP_USER_ENDPOINT', '')
LOGIN_REDIRECT_URL = env('LOGIN_REDIRECT_URL', '/')
LOGIN_REDIRECT_URL = env('LOGIN_REDIRECT_URL', '/overview')
LOGOUT_REDIRECT_URL = env('LOGOUT_REDIRECT_URL', '/')

+ 12
- 5
requirements.txt View File

@ -1,14 +1,17 @@
-i https://pypi.org/simple
aniso8601==7.0.0
certifi==2020.6.20
cffi==1.14.2
chardet==3.0.4
clamd==1.0.2
Django==2.2.15
cryptography==3.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
django-appconf==1.0.4
django-compressor==2.4
django-cors-headers==3.5.0
django-crispy-forms==1.9.2
django-debug-toolbar==2.2
django-sass-processor==0.8
django==2.2.15
djangorestframework==3.11.1
environs==8.0.0
graphene==2.1.8
@ -17,22 +20,26 @@ graphql-core==2.3.2
graphql-relay==2.0.1
gunicorn==20.0.4
idna==2.10
josepy==1.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
libsass==0.20.0
marshmallow==3.7.1
Pillow==7.2.0
mozilla-django-oidc==1.2.4
pillow==7.2.0
promise==2.3
psycopg2==2.8.5
pycparser==2.20; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
pyopenssl==19.1.0
python-dotenv==0.14.0
pytz==2020.1
rcssmin==1.0.6
redis==3.5.3
requests==2.24.0
rjsmin==1.1.0
Rx==1.6.1
rx==1.6.1
singledispatch==3.4.0.3
six==1.15.0
sqlparse==0.3.1
Unidecode==1.1.1
Unipath==1.1
unidecode==1.1.1
unipath==1.1
urllib3==1.25.10
whitenoise==3.3.1

+ 32
- 14
vue/src/components/Uploader/Uploader.vue View File

@ -32,6 +32,7 @@
:post-action="postAction"
:input-id="inputId"
name="file"
:headers="{ 'X-CSRFToken': getCSRFToken() }"
:class="['drop-zone', dragging ? 'dragging' : '']"
:data="inputKeys"
@input-file="inputFile"
@ -185,7 +186,7 @@
},
pdfURL() {
return `${this.$parent.proxyRootPath}pdf-images/${this.docType}/${this.party}/`;
},
}
},
methods: {
inputFile(newFile, oldFile) {
@ -220,7 +221,7 @@
// Automatically activate upload after compression completes
if (newFile && newFile.compressed && !newFile.active) {
newFile.active = true;
}
}
},
inputFilter(newFile, oldFile, prevent) {
if (newFile && !oldFile) {
@ -249,14 +250,14 @@
quality: 0.9,
maxWidth: 3300,
maxHeight: 3300,
convertSize: Infinity,
convertSize: Infinity,
success(result) {
self.$refs.upload.update(newFile, {
error: false,
file: result,
size: result.size,
type: result.type,
compressed: true
compressed: true,
});
},
error(err) {
@ -346,11 +347,12 @@
remove(file) {
const urlbase = `${this.$parent.proxyRootPath}api/documents`;
const encFilename = encodeURIComponent(file.name);
const token = this.getCSRFToken();
if (!file.error) {
// we add an extra 'x' to the file extension so the siteminder proxy doesn't treat it as an image
const url = `${urlbase}/${this.docType}/${this.party}/${encFilename}x/${file.size}/`;
axios
.delete(url)
.delete(url, { headers: { "X-CSRFToken": token } })
.then((response) => {
const pos = this.files.findIndex(
(f) => f.docType === file.docType && f.size === file.size
@ -410,13 +412,15 @@
saveMetaData() {
let allFiles = [];
this.files.forEach((file) => {
allFiles.push({
filename: file.name,
size: file.size,
width: file.width,
height: file.height,
rotation: rotateFix(file.rotation),
});
if (!file.error) {
allFiles.push({
filename: file.name,
size: file.size,
width: file.width,
height: file.height,
rotation: rotateFix(file.rotation),
});
}
});
const data = {
docType: this.docType,
@ -440,20 +444,34 @@
})
.then((response) => {
// check for errors in the graphQL response
this.retries = 0;
this.retries = 0;
if (response.data.errors && response.data.errors.length) {
response.data.errors.forEach((error) => {
console.log("error", error.message || error);
// if there was an error it's probably because the upload isn't finished yet
// mark the metadata as dirty so it will save metadata again
this.retries++;
this.isDirty = true;
});
}
})
.catch((error) => {
this.showError("Error saving metadata");
console.log("error", error);
this.retries++;
});
},
getCSRFToken() {
const name = "csrftoken";
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === name + "=") {
return decodeURIComponent(cookie.substring(name.length + 1));
}
}
}
return null;
}
},
created() {


Loading…
Cancel
Save