diff --git a/edivorce/apps/core/middleware/keycloak.py b/edivorce/apps/core/middleware/keycloak.py index 13a4fac3..57e7b82f 100644 --- a/edivorce/apps/core/middleware/keycloak.py +++ b/edivorce/apps/core/middleware/keycloak.py @@ -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) diff --git a/edivorce/apps/core/migrations/0001_initial.py b/edivorce/apps/core/migrations/0001_initial.py index 275da625..2fe26e4d 100644 --- a/edivorce/apps/core/migrations/0001_initial.py +++ b/edivorce/apps/core/migrations/0001_initial.py @@ -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=[ diff --git a/edivorce/apps/core/migrations/0007_auto_20170210_1702.py b/edivorce/apps/core/migrations/0007_auto_20170210_1702.py index efb1d8bf..88046996 100644 --- a/edivorce/apps/core/migrations/0007_auto_20170210_1702.py +++ b/edivorce/apps/core/migrations/0007_auto_20170210_1702.py @@ -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', diff --git a/edivorce/apps/core/migrations/0024_auto_20201009_1235.py b/edivorce/apps/core/migrations/0024_auto_20201009_1235.py new file mode 100644 index 00000000..ec37da5d --- /dev/null +++ b/edivorce/apps/core/migrations/0024_auto_20201009_1235.py @@ -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'), + ), + ] diff --git a/edivorce/apps/core/migrations/0024_bceiduser_is_bcsc.py b/edivorce/apps/core/migrations/0024_bceiduser_is_bcsc.py index 9a2ca2b1..2fb076d8 100644 --- a/edivorce/apps/core/migrations/0024_bceiduser_is_bcsc.py +++ b/edivorce/apps/core/migrations/0024_bceiduser_is_bcsc.py @@ -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 = [ diff --git a/edivorce/apps/core/models.py b/edivorce/apps/core/models.py index a55430dc..03c96759 100644 --- a/edivorce/apps/core/models.py +++ b/edivorce/apps/core/models.py @@ -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 diff --git a/edivorce/apps/core/static/css/main.scss b/edivorce/apps/core/static/css/main.scss index 17352b16..053b65ae 100644 --- a/edivorce/apps/core/static/css/main.scss +++ b/edivorce/apps/core/static/css/main.scss @@ -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; } } } diff --git a/edivorce/apps/core/templates/base.html b/edivorce/apps/core/templates/base.html index 3afc0993..351ac03d 100644 --- a/edivorce/apps/core/templates/base.html +++ b/edivorce/apps/core/templates/base.html @@ -54,8 +54,12 @@
diff --git a/edivorce/apps/core/tests/test_api.py b/edivorce/apps/core/tests/test_api.py index 82eca245..e3fdfa13 100644 --- a/edivorce/apps/core/tests/test_api.py +++ b/edivorce/apps/core/tests/test_api.py @@ -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) diff --git a/edivorce/settings/base.py b/edivorce/settings/base.py index 0a10f513..5defb450 100644 --- a/edivorce/settings/base.py +++ b/edivorce/settings/base.py @@ -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', '/') diff --git a/requirements.txt b/requirements.txt index 31d972b5..19ef9fa8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/vue/src/components/Uploader/Uploader.vue b/vue/src/components/Uploader/Uploader.vue index 78e848dd..49995332 100644 --- a/vue/src/components/Uploader/Uploader.vue +++ b/vue/src/components/Uploader/Uploader.vue @@ -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() {