diff --git a/rodan-client/code/src/js/Controllers/ControllerAuthentication.js b/rodan-client/code/src/js/Controllers/ControllerAuthentication.js index 6120d0f23..333f6274d 100644 --- a/rodan-client/code/src/js/Controllers/ControllerAuthentication.js +++ b/rodan-client/code/src/js/Controllers/ControllerAuthentication.js @@ -115,7 +115,7 @@ export default class ControllerAuthentication extends BaseController break; case 401: Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SYSTEM_HANDLE_ERROR, {response: request, - message: 'Incorrect username/password.'}); + message: 'Incorrect email/password.'}); Radio.channel('rodan').trigger(RODAN_EVENTS.EVENT__AUTHENTICATION_LOGINREQUIRED); break; case 403: @@ -209,7 +209,7 @@ export default class ControllerAuthentication extends BaseController } request.setRequestHeader('Accept', 'application/json'); request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - request.send('username=' + options.username + '&password=' + options.password); + request.send('email=' + options.email + '&password=' + options.password); } /** @@ -322,7 +322,7 @@ export default class ControllerAuthentication extends BaseController { var route = Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SERVER_GET_ROUTE, 'auth-me'); var ajaxSettings = {success: (response) => this._handleSaveUserSuccess(response), - error: (response) => Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__SYSTEM_HANDLE_ERROR, {response: response}), + error: (response) => this._handleSaveUserError(response), type: 'PATCH', url: route, dataType: 'json', @@ -356,6 +356,16 @@ export default class ControllerAuthentication extends BaseController Radio.channel('rodan').trigger(RODAN_EVENTS.EVENT__USER_SAVED, {user: this._user}); } + /** + * Handles errors from saving user. + */ + _handleSaveUserError(response) + { + const errors = response.responseJSON; + Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__MODAL_FORM_VALIDATION_ERROR, { errors }); + Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__MODAL_ERROR, {content: 'An error occured while saving the user.'}); + } + /** * Handle success response from changing password. */ diff --git a/rodan-client/code/src/js/Controllers/ControllerModal.js b/rodan-client/code/src/js/Controllers/ControllerModal.js index 50dd9cd28..41e3b7ed1 100644 --- a/rodan-client/code/src/js/Controllers/ControllerModal.js +++ b/rodan-client/code/src/js/Controllers/ControllerModal.js @@ -102,6 +102,7 @@ export default class ControllerModal extends BaseController { Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__MODAL_SHOW, options); } + $('.modal-footer').removeClass('modal-footer-error'); } /** diff --git a/rodan-client/code/src/js/Shared/RODAN_EVENTS.js b/rodan-client/code/src/js/Shared/RODAN_EVENTS.js index 7653d8465..0381edb26 100644 --- a/rodan-client/code/src/js/Shared/RODAN_EVENTS.js +++ b/rodan-client/code/src/js/Shared/RODAN_EVENTS.js @@ -148,6 +148,8 @@ class RODAN_EVENTS /////////////////////////////////////////////////////////////////////////////////////// /** Request an error be displayed. This is a "convenience" request -- if a modal is currently visible (which is probably related somehow to the error) the footer will be updated with the error message. If no modal visible, REQUEST__MODAL_SHOW will be called. Takes {content: Marionette.View OR string}. */ this.REQUEST__MODAL_ERROR = 'REQUEST__MODAL_ERROR'; + /** Request form validation errors to be displayed. Takes {errors: {[string]: string[]}} mapping input field name to an array of error strings. */ + this.REQUEST__MODAL_FORM_VALIDATION_ERROR = 'REQUEST__MODAL_FORM_VALIDATION_ERROR'; /** Request modal window to hide/close. */ this.REQUEST__MODAL_HIDE = 'REQUEST__MODAL_HIDE'; /** Request modal window to show/open with provided Marionette View. If another modal is currently open the request will not show. Takes {content: string, title: string}. */ diff --git a/rodan-client/code/src/js/Views/Master/Main/Login/ViewLogin.js b/rodan-client/code/src/js/Views/Master/Main/Login/ViewLogin.js index 4b47be0c2..c800e8a94 100644 --- a/rodan-client/code/src/js/Views/Master/Main/Login/ViewLogin.js +++ b/rodan-client/code/src/js/Views/Master/Main/Login/ViewLogin.js @@ -28,14 +28,14 @@ export default class ViewLogin extends Marionette.View */ _handleButton() { - Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__AUTHENTICATION_LOGIN, {username: this.ui.textUsername.val(), password: this.ui.textPassword.val()}); + Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__AUTHENTICATION_LOGIN, { email: this.ui.textEmail.val(), password: this.ui.textPassword.val() }); } } ViewLogin.prototype.modelEvents = { 'all': 'render' }; ViewLogin.prototype.ui = { - textUsername: '#text-login_username', + textEmail: '#text-login_email', textPassword: '#text-login_password', buttonLogin: '#button-login' }; diff --git a/rodan-client/code/src/js/Views/Master/Main/User/Individual/ViewUser.js b/rodan-client/code/src/js/Views/Master/Main/User/Individual/ViewUser.js index 1c2f0c6cd..713390e82 100644 --- a/rodan-client/code/src/js/Views/Master/Main/User/Individual/ViewUser.js +++ b/rodan-client/code/src/js/Views/Master/Main/User/Individual/ViewUser.js @@ -20,6 +20,7 @@ export default class ViewUser extends Marionette.CollectionView { /** @ignore */ Radio.channel('rodan').on(RODAN_EVENTS.EVENT__USER_PREFERENCE_LOADED, (options) => this._handleUserPreferenceLoaded(options)); + Radio.channel('rodan').reply(RODAN_EVENTS.REQUEST__MODAL_FORM_VALIDATION_ERROR, (options) => this._handleErrors(options)); } /** @@ -38,10 +39,14 @@ export default class ViewUser extends Marionette.CollectionView */ _handleButtonSave() { - Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__USER_SAVE, - {fields: {first_name: _.escape(this.ui.textFirstName.val()), - last_name: _.escape(this.ui.textLastName.val()), - email: _.escape(this.ui.textEmail.val())}}); + Radio.channel('rodan').request(RODAN_EVENTS.REQUEST__USER_SAVE, { + fields: { + username: _.escape(this.ui.textUsername.val()), + first_name: _.escape(this.ui.textFirstName.val()), + last_name: _.escape(this.ui.textLastName.val()), + email: _.escape(this.ui.textEmail.val()) + } + }); if (this._userPreference) { this._userPreference.set({'send_email': $(this.ui.checkboxSendEmail).prop('checked')}); @@ -86,6 +91,31 @@ export default class ViewUser extends Marionette.CollectionView $(this.ui.divUserPreferenceLoading).show(); } } + + /** + * Handle errors. + */ + _handleErrors(options) + { + for (const [key, errors] of Object.entries(options.errors)) { + switch (key) { + case 'username': + $(this.ui.errorUsername).text(errors[0]); + break; + case 'first_name': + $(this.ui.errorFirstName).text(errors[0]); + break; + case 'last_name': + $(this.ui.errorLastName).text(errors[0]); + break; + case 'email': + $(this.ui.errorEmail).text(errors[0]); + break; + default: + break; + } + } + } } ViewUser.prototype.modelEvents = { 'all': 'render' @@ -93,9 +123,14 @@ ViewUser.prototype.modelEvents = { ViewUser.prototype.ui = { buttonSave: '#button-save_user', buttonPassword: '#button-change_password', + textUsername: '#text-user_username', textFirstName: '#text-user_firstname', textLastName: '#text-user_lastname', textEmail: '#text-user_email', + errorUsername: '#error-user_username', + errorFirstName: '#error-user_firstname', + errorLastName: '#error-user_lastname', + errorEmail: '#error-user_email', checkboxSendEmail: '#checkbox-send_email', divUserPreference: '#div-user_preference', divUserPreferenceLoading: '#div-user_preference_loading' diff --git a/rodan-client/code/templates/Views/Master/Main/Login/template-main_login.html b/rodan-client/code/templates/Views/Master/Main/Login/template-main_login.html index 3b93d0a90..ebea17db8 100644 --- a/rodan-client/code/templates/Views/Master/Main/Login/template-main_login.html +++ b/rodan-client/code/templates/Views/Master/Main/Login/template-main_login.html @@ -4,8 +4,8 @@
- - + +
diff --git a/rodan-client/code/templates/Views/Master/Main/User/Individual/template-main_user_individual.html b/rodan-client/code/templates/Views/Master/Main/User/Individual/template-main_user_individual.html index 63ce1f70b..3057cfedb 100644 --- a/rodan-client/code/templates/Views/Master/Main/User/Individual/template-main_user_individual.html +++ b/rodan-client/code/templates/Views/Master/Main/User/Individual/template-main_user_individual.html @@ -1,10 +1,20 @@
+ + +

+ +

+ +

+ +

+
diff --git a/rodan-main/code/rodan/admin/admin.py b/rodan-main/code/rodan/admin/admin.py index 8d19942e3..4098e373f 100644 --- a/rodan-main/code/rodan/admin/admin.py +++ b/rodan-main/code/rodan/admin/admin.py @@ -1,8 +1,10 @@ from django.contrib import admin +from django.contrib.auth import forms +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from guardian.admin import GuardedModelAdmin from rodan.models.project import Project - +from rodan.models.user import User from rodan.models.workflow import Workflow from rodan.models.workflowjob import WorkflowJob from rodan.models.workflowrun import WorkflowRun @@ -12,27 +14,14 @@ from rodan.models.resource import Resource from rodan.models.resourcelist import ResourceList -# from rodan.models.rodanuser import RodanUser - - -# class WorkflowJobInline(admin.StackedInline): -# model = WorkflowJob -# extra = 5 - - -# class WorkflowAdmin(admin.ModelAdmin): -# inlines = [WorkflowJobInline] - class JobAdmin(admin.ModelAdmin): list_display = ('name', 'enabled', 'category') -# @admin.register(Project) class ProjectAdmin(GuardedModelAdmin): list_display = ('name', 'uuid', 'creator', 'created', 'updated') readonly_fields = ('uuid',) -# @admin.register(Workflow) class WorkflowJobAdmin(admin.ModelAdmin): list_display = ('job_name', 'created', 'updated') list_filter = ('workflow__name',) @@ -56,20 +45,31 @@ class ResourceAdmin(admin.ModelAdmin): class ResourceListAdmin(admin.ModelAdmin): list_display = ('created', 'updated') +class UserChangeForm(forms.UserChangeForm): + class Meta(forms.UserChangeForm.Meta): + model = User + +class UserCreationForm(forms.UserCreationForm): + class Meta(forms.UserCreationForm.Meta): + model = User -# class UserProfileInline(admin.StackedInline): -# model = RodanUser -# can_delete = False -# verbose_name_plural = 'profile' +class UserAdmin(BaseUserAdmin): + form = UserChangeForm + add_form = UserCreationForm + list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') + list_filter = ('is_staff', 'is_superuser', 'is_active') -# class RodanUserAdmin(UserAdmin): -# inlines = (UserProfileInline, ) + add_fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ('username', 'email', 'password1', 'password2'), + }), + ) -# admin.site.unregister(User) -# admin.site.register(User, RodanUserAdmin) + search_fields = ('username', 'email') + ordering = ('username', 'email') -# admin.site.register(RodanUser) admin.site.register(Project, ProjectAdmin) admin.site.register(WorkflowRun, WorkflowRunAdmin) admin.site.register(RunJob, RunJobAdmin) @@ -79,3 +79,4 @@ class ResourceListAdmin(admin.ModelAdmin): admin.site.register(ResultsPackage, ResultsPackageAdmin) admin.site.register(Resource) admin.site.register(ResourceList, ResourceListAdmin) +admin.site.register(User, UserAdmin) \ No newline at end of file diff --git a/rodan-main/code/rodan/migrations/0001_initial.py b/rodan-main/code/rodan/migrations/0001_initial.py index 341cd216b..7e07789dd 100644 --- a/rodan-main/code/rodan/migrations/0001_initial.py +++ b/rodan-main/code/rodan/migrations/0001_initial.py @@ -1,10 +1,11 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.29 on 2022-07-13 19:44 -from __future__ import unicode_literals +# Generated by Django 2.0.13 on 2023-06-26 17:12 from django.conf import settings +import django.contrib.auth.models +import django.contrib.auth.validators from django.db import migrations, models import django.db.models.deletion +import django.utils.timezone import jsonfield.fields import rodan.models.resource import sortedm2m.fields @@ -16,11 +17,34 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('auth', '0008_alter_user_username_max_length'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('auth', '0009_alter_user_last_name_max_length'), ] operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(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')), + ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('groups', 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')), + ('user_permissions', 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')), + ], + options={ + 'db_table': 'auth_user', + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), migrations.CreateModel( name='Connection', fields=[ diff --git a/rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py b/rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py new file mode 100644 index 000000000..939acd00b --- /dev/null +++ b/rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py @@ -0,0 +1,35 @@ +from django.db import migrations, models +import rodan.models.user + + +class Migration(migrations.Migration): + """ + This migration makes the email field unique and non-blank and adds permissions to custom user model. + """ + + dependencies = [ + ('rodan', '0002_auto_20230522_1420'), + ] + + operations = [ + migrations.AlterModelManagers( + name='user', + managers=[ + ('objects', rodan.models.user.UserManager()), + ], + ), + migrations.AlterModelOptions( + name='user', + options={'permissions': (('view_user', 'View User'),)}, + ), + # Make sure there are no empty email fields by giving them a default value + migrations.RunSQL( + sql="UPDATE auth_user SET email = CONCAT(username, '@rodan2.simssa.ca') WHERE email = ''", + reverse_sql="UPDATE auth_user SET email = '' WHERE email LIKE '%@rodan2.simssa.ca'" + ), + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(error_messages={'unique': 'A user with that email already exists.'}, max_length=254, unique=True, verbose_name='email address'), + ), + ] diff --git a/rodan-main/code/rodan/models/__init__.py b/rodan-main/code/rodan/models/__init__.py index a5e86a18f..e6a4a4500 100644 --- a/rodan-main/code/rodan/models/__init__.py +++ b/rodan-main/code/rodan/models/__init__.py @@ -5,7 +5,7 @@ import subprocess from django.contrib.contenttypes.models import ContentType -from django.contrib.auth.models import Permission, User, Group +from django.contrib.auth.models import Permission, Group from django.db.models.signals import ( pre_migrate, post_migrate, @@ -41,6 +41,7 @@ from rodan.models.resourcetype import ResourceType from rodan.models.connection import Connection from rodan.models.tempauthtoken import Tempauthtoken +from rodan.models.user import User if sys.version_info.major == 2: @@ -57,7 +58,7 @@ def add_view_user_permission(sender, **kwargs): """ # don't set permissions in test database if not settings.TEST and sender.name == 'guardian': - content_type = ContentType.objects.get(app_label='auth', model='user') + content_type = ContentType.objects.get(app_label='rodan', model='user') Permission.objects.get_or_create(codename='view_user', name='View User', content_type=content_type) group = Group.objects.get_or_create(name="view_user_permission") diff --git a/rodan-main/code/rodan/models/project.py b/rodan-main/code/rodan/models/project.py index 84031282e..c6c4a2eb1 100644 --- a/rodan-main/code/rodan/models/project.py +++ b/rodan-main/code/rodan/models/project.py @@ -4,9 +4,10 @@ import traceback import uuid from django.conf import settings -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import Group from django.urls import reverse from django.db import models +from rodan.models.user import User logger = logging.getLogger("rodan") diff --git a/rodan-main/code/rodan/models/resource.py b/rodan-main/code/rodan/models/resource.py index fb30b6306..422cf1547 100644 --- a/rodan-main/code/rodan/models/resource.py +++ b/rodan-main/code/rodan/models/resource.py @@ -3,10 +3,10 @@ import shutil from django.conf import settings from django.db import models -from django.contrib.auth.models import User from django.urls import reverse from rodan.constants import task_status from rodan.models.resourcelabel import ResourceLabel +from rodan.models.user import User import logging diff --git a/rodan-main/code/rodan/models/resourcelist.py b/rodan-main/code/rodan/models/resourcelist.py index 4663e7af0..5ccfcc79f 100644 --- a/rodan-main/code/rodan/models/resourcelist.py +++ b/rodan-main/code/rodan/models/resourcelist.py @@ -1,8 +1,8 @@ import uuid from django.db import models from sortedm2m.fields import SortedManyToManyField -from django.contrib.auth.models import User from django.apps import apps +from rodan.models.user import User import logging diff --git a/rodan-main/code/rodan/models/resultspackage.py b/rodan-main/code/rodan/models/resultspackage.py index 2811dbdaa..739646c06 100644 --- a/rodan-main/code/rodan/models/resultspackage.py +++ b/rodan-main/code/rodan/models/resultspackage.py @@ -77,7 +77,7 @@ class Meta: ) packaging_mode = models.IntegerField(choices=PACKAGING_MODE_CHOICES, db_index=True) creator = models.ForeignKey( - "auth.User", + settings.AUTH_USER_MODEL, related_name="results_packages", on_delete=models.SET_NULL, blank=True, diff --git a/rodan-main/code/rodan/models/runjob.py b/rodan-main/code/rodan/models/runjob.py index d7667efae..6aae91d81 100644 --- a/rodan-main/code/rodan/models/runjob.py +++ b/rodan-main/code/rodan/models/runjob.py @@ -3,7 +3,7 @@ from jsonfield import JSONField from rodan.models.job import Job from rodan.constants import task_status -from django.contrib.auth.models import User +from rodan.models.user import User class RunJob(models.Model): diff --git a/rodan-main/code/rodan/models/tempauthtoken.py b/rodan-main/code/rodan/models/tempauthtoken.py index c850d3b5b..66719cb8b 100644 --- a/rodan-main/code/rodan/models/tempauthtoken.py +++ b/rodan-main/code/rodan/models/tempauthtoken.py @@ -1,6 +1,6 @@ import uuid from django.db import models -from django.contrib.auth.models import User +from rodan.models.user import User class Tempauthtoken(models.Model): diff --git a/rodan-main/code/rodan/models/user.py b/rodan-main/code/rodan/models/user.py new file mode 100644 index 000000000..bd5e87aab --- /dev/null +++ b/rodan-main/code/rodan/models/user.py @@ -0,0 +1,59 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser, BaseUserManager + +class UserManager(BaseUserManager): + """ + Custom user model manager where email is the unique identifiers + for authentication instead of usernames. + """ + + use_in_migrations = True + + def _create_user(self, username, email, password, **extra_fields): + """ + Creates and saves a User with the given username, email and password. + """ + if not username: + raise ValueError('The given username must be set') + if not email: + raise ValueError('The given email must be set') + email = self.normalize_email(email) + username = self.model.normalize_username(username) + user = self.model(username=username, email=email, **extra_fields) + user.set_password(password) + user.save() + return user + + def create_user(self, username, email, password=None, **extra_fields): + extra_fields.setdefault('is_staff', False) + extra_fields.setdefault('is_superuser', False) + return self._create_user(username, email, password, **extra_fields) + + def create_superuser(self, username, email, password, **extra_fields): + extra_fields.setdefault('is_staff', True) + extra_fields.setdefault('is_superuser', True) + + if extra_fields.get('is_staff') is not True: + raise ValueError('Superuser must have is_staff=True.') + if extra_fields.get('is_superuser') is not True: + raise ValueError('Superuser must have is_superuser=True.') + + return self._create_user(username, email, password, **extra_fields) + +class User(AbstractUser): + """ + A custom user model that uses email as the username field. + """ + + email = models.EmailField(verbose_name="email address", unique=True, error_messages={"unique": "A user with that email already exists."}) + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = ["username"] + + objects = UserManager() + + class Meta: + db_table = "auth_user" + permissions = ( + ("view_user", "View User"), + ) diff --git a/rodan-main/code/rodan/models/userpreference.py b/rodan-main/code/rodan/models/userpreference.py index d172f9b73..5e3a5bffe 100644 --- a/rodan-main/code/rodan/models/userpreference.py +++ b/rodan-main/code/rodan/models/userpreference.py @@ -1,6 +1,6 @@ import uuid from django.db import models -from django.contrib.auth.models import User +from rodan.models.user import User class UserPreference(models.Model): diff --git a/rodan-main/code/rodan/models/workflow.py b/rodan-main/code/rodan/models/workflow.py index 881b3544e..c556eca80 100644 --- a/rodan-main/code/rodan/models/workflow.py +++ b/rodan-main/code/rodan/models/workflow.py @@ -3,6 +3,7 @@ import uuid from django.db import models from django.apps import apps +from django.conf import settings class Workflow(models.Model): @@ -42,7 +43,7 @@ class Meta: ) description = models.TextField(blank=True, null=True) creator = models.ForeignKey( - "auth.User", + settings.AUTH_USER_MODEL, related_name="workflows", null=True, blank=True, diff --git a/rodan-main/code/rodan/models/workflowrun.py b/rodan-main/code/rodan/models/workflowrun.py index 9ce0050e3..10ea25e0a 100644 --- a/rodan-main/code/rodan/models/workflowrun.py +++ b/rodan-main/code/rodan/models/workflowrun.py @@ -2,6 +2,7 @@ import logging import uuid from django.db import models +from django.conf import settings from rodan.constants import task_status # import shutil @@ -69,7 +70,7 @@ class Meta: db_index=True, ) creator = models.ForeignKey( - "auth.User", + settings.AUTH_USER_MODEL, related_name="workflow_runs", blank=True, null=True, diff --git a/rodan-main/code/rodan/serializers/user.py b/rodan-main/code/rodan/serializers/user.py index 3a0962e38..3675f1c2d 100644 --- a/rodan-main/code/rodan/serializers/user.py +++ b/rodan-main/code/rodan/serializers/user.py @@ -1,5 +1,5 @@ -from django.contrib.auth.models import User from rest_framework import serializers +from rodan.models.user import User class UserSerializer(serializers.HyperlinkedModelSerializer): diff --git a/rodan-main/code/rodan/settings.py b/rodan-main/code/rodan/settings.py index ad1dcc350..7db0c72a5 100644 --- a/rodan-main/code/rodan/settings.py +++ b/rodan-main/code/rodan/settings.py @@ -113,6 +113,9 @@ # trailing slash. MEDIA_URL = "/uploads/" +# Custom user model +AUTH_USER_MODEL = "rodan.User" + ############################################################################### # 1.b General Rodan Configuration ############################################################################### @@ -368,15 +371,13 @@ "DEFAULT_PAGINATION_CLASS": "rodan.paginators.pagination.CustomPagination", } -# used by django-guardian AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.ModelBackend", # defaults + "django.contrib.auth.backends.ModelBackend", # Authenticate with User.USERNAME_FIELD and password "guardian.backends.ObjectPermissionBackend", ] -# [TODO] This is completely depricated. -# https://django-guardian.readthedocs.io/en/stable/develop/changes.html?highlight=User%20ID#release-1-4-2-mar-09-2016 -# Fix it per the suggestions above. -ANONYMOUS_USER_ID = -1 + +# Disable anonymous user for django-guardian +ANONYMOUS_USER_NAME = None ############################################################################### # 2.b CORS Configuration diff --git a/rodan-main/code/rodan/test/helpers.py b/rodan-main/code/rodan/test/helpers.py index 7f5507c9d..43822804b 100644 --- a/rodan-main/code/rodan/test/helpers.py +++ b/rodan-main/code/rodan/test/helpers.py @@ -3,13 +3,13 @@ # import uuid # import time from model_mommy import mommy -from django.contrib.auth.models import User from rodan.models import Job, ResourceType from django.core.files.base import ContentFile from django.conf import settings from importlib import reload from rodan.celery import app +from rodan.models.user import User class RodanTestSetUpMixin(object): def url(self, obj): @@ -53,10 +53,10 @@ def setUp_rodan(self): def setUp_user(self): self.test_user = User.objects.create_user( - username="ahankins", password="hahaha" + username="ahankins", email="ahankins@rodan2.simssa.ca", password="hahaha" ) self.test_superuser = User.objects.create_superuser( - username="super", email="s@s.com", password="hahaha" + username="super", email="super@rodan2.simssa.ca", password="hahaha" ) def setUp_basic_workflow(self): diff --git a/rodan-main/code/rodan/test/models/test_resource.py b/rodan-main/code/rodan/test/models/test_resource.py index 314084153..ff5e7f9b6 100644 --- a/rodan-main/code/rodan/test/models/test_resource.py +++ b/rodan-main/code/rodan/test/models/test_resource.py @@ -1,6 +1,5 @@ from django.test import TestCase -from django.contrib.auth.models import User -from rodan.models import ResourceType, Resource +from rodan.models import ResourceType, Resource, User from model_mommy import mommy from rodan.test.helpers import RodanTestTearDownMixin, RodanTestSetUpMixin diff --git a/rodan-main/code/rodan/test/models/test_resourcelabel.py b/rodan-main/code/rodan/test/models/test_resourcelabel.py index bfd3cf264..6ad4cd364 100644 --- a/rodan-main/code/rodan/test/models/test_resourcelabel.py +++ b/rodan-main/code/rodan/test/models/test_resourcelabel.py @@ -1,6 +1,5 @@ from django.test import TestCase -from django.contrib.auth.models import User -from rodan.models import Resource, ResourceLabel, ResourceType +from rodan.models import Resource, ResourceLabel, ResourceType, User from model_mommy import mommy from rodan.test.helpers import RodanTestTearDownMixin, RodanTestSetUpMixin diff --git a/rodan-main/code/rodan/test/models/test_userpreference.py b/rodan-main/code/rodan/test/models/test_userpreference.py index d8ca29136..516686019 100644 --- a/rodan-main/code/rodan/test/models/test_userpreference.py +++ b/rodan-main/code/rodan/test/models/test_userpreference.py @@ -1,6 +1,5 @@ from django.test import TestCase -from django.contrib.auth.models import User -from rodan.models import UserPreference +from rodan.models import UserPreference, User from model_mommy import mommy from rodan.test.helpers import RodanTestTearDownMixin, RodanTestSetUpMixin diff --git a/rodan-main/code/rodan/test/models/test_workflow.py b/rodan-main/code/rodan/test/models/test_workflow.py index eea7f15f7..8d96c795a 100644 --- a/rodan-main/code/rodan/test/models/test_workflow.py +++ b/rodan-main/code/rodan/test/models/test_workflow.py @@ -1,6 +1,5 @@ from django.test import TestCase -from django.contrib.auth.models import User -from rodan.models import Workflow +from rodan.models import Workflow, User # from rodan.models import Project from model_mommy import mommy from rodan.test.helpers import RodanTestTearDownMixin, RodanTestSetUpMixin diff --git a/rodan-main/code/rodan/test/models/test_workflowrun.py b/rodan-main/code/rodan/test/models/test_workflowrun.py index bf7c17a49..5802e0938 100644 --- a/rodan-main/code/rodan/test/models/test_workflowrun.py +++ b/rodan-main/code/rodan/test/models/test_workflowrun.py @@ -1,6 +1,5 @@ from django.test import TestCase -from django.contrib.auth.models import User -from rodan.models import WorkflowRun +from rodan.models import WorkflowRun, User from model_mommy import mommy from rodan.test.helpers import RodanTestTearDownMixin, RodanTestSetUpMixin diff --git a/rodan-main/code/rodan/test/testIndividual_permission.py b/rodan-main/code/rodan/test/testIndividual_permission.py index 8d019435f..9d5d35e79 100644 --- a/rodan-main/code/rodan/test/testIndividual_permission.py +++ b/rodan-main/code/rodan/test/testIndividual_permission.py @@ -3,7 +3,6 @@ """ import random -from django.contrib.auth.models import User from rest_framework.test import APITestCase from rest_framework import status from rest_framework.reverse import reverse @@ -15,7 +14,8 @@ WorkflowRun, Input, Output, - RunJob + RunJob, + User ) from rodan.test.helpers import RodanTestSetUpMixin, RodanTestTearDownMixin @@ -32,16 +32,16 @@ def setUp(self): self.setUp_user() self.test_creator = self.test_user self.test_admin = User.objects.create_user( - username="test_admin", password="hahaha" + username="test_admin", email="test_admin@rodan2.simssa.ca", password="hahaha" ) self.test_worker = User.objects.create_user( - username="test_worker", password="hahaha" + username="test_worker", email="test_worker@rodan2.simssa.ca", password="hahaha" ) self.test_worker2 = User.objects.create_user( - username="test_worker2", password="hahaha" + username="test_worker2", email="test_worker2@rodan2.simssa.ca", password="hahaha" ) self.test_outsider = User.objects.create_user( - username="test_outsider", password="hahaha" + username="test_outsider", email="test_outsider@rodan2.simssa.ca", password="hahaha" ) def test_all(self): @@ -936,16 +936,16 @@ def setUp(self): self.setUp_rodan() self.setUp_user() self.test_admin = User.objects.create_user( - username="test_admin", password="hahaha" + username="test_admin", email="test_admin@rodan2.simssa.ca", password="hahaha" ) self.test_worker = User.objects.create_user( - username="test_worker", password="hahaha" + username="test_worker", email="test_worker@rodan2.simssa.ca", password="hahaha" ) self.test_worker2 = User.objects.create_user( - username="test_worker2", password="hahaha" + username="test_worker2", email="test_worker2@rodan2.simssa.ca", password="hahaha" ) self.test_outsider = User.objects.create_user( - username="test_outsider", password="hahaha" + username="test_outsider", email="test_outsider@rodan2.simssa.ca", password="hahaha" ) def test_construct_workflow(self): diff --git a/rodan-main/code/rodan/test/views/test_auth.py b/rodan-main/code/rodan/test/views/test_auth.py index 5f4e228e3..2b1d2754d 100644 --- a/rodan-main/code/rodan/test/views/test_auth.py +++ b/rodan-main/code/rodan/test/views/test_auth.py @@ -12,7 +12,7 @@ def setUp(self): def test_token_auth_pass(self): token = self.client.post( "/api/auth/token/", - {"username": "ahankins", "password": "hahaha"}, + {"email": "ahankins@rodan2.simssa.ca", "password": "hahaha"}, format="multipart", ) token_header = "Token {0}".format(token.data["token"]) @@ -26,7 +26,7 @@ def test_token_auth_pass(self): def test_token_auth_fail(self): token = self.client.post( "/api/auth/token/", - {"username": "ahankins", "password": "wrongg"}, + {"email": "ahankins@rodan2.simssa.ca", "password": "wrongg"}, format="multipart", ) self.assertEqual(token.data, {"is_logged_in": False}) diff --git a/rodan-main/code/rodan/views/auth.py b/rodan-main/code/rodan/views/auth.py index 619969cae..46269d0a9 100644 --- a/rodan-main/code/rodan/views/auth.py +++ b/rodan-main/code/rodan/views/auth.py @@ -3,7 +3,6 @@ # login, # logout ) -from django.contrib.auth.models import User from rest_framework import status from rest_framework.response import Response from rest_framework import views @@ -15,6 +14,7 @@ from rest_framework.authtoken.serializers import AuthTokenSerializer from rodan.serializers.user import UserSerializer +from rodan.models.user import User class AuthMeView(generics.RetrieveUpdateAPIView): @@ -42,12 +42,12 @@ class AuthTokenView(views.APIView): model = Token def post(self, request): - username = request.data.get("username", None) + email = request.data.get("email", None) password = request.data.get("password", None) - if not username: + if not email: return Response( - {"detail": "You must supply a username"}, + {"detail": "You must supply a email"}, status=status.HTTP_401_UNAUTHORIZED, ) if not password: @@ -56,7 +56,7 @@ def post(self, request): status=status.HTTP_401_UNAUTHORIZED, ) - user = authenticate(username=username, password=password) + user = authenticate(email=email, password=password) if user is not None: if user.is_active: @@ -72,7 +72,7 @@ def post(self, request): {"is_logged_in": False}, status=status.HTTP_403_FORBIDDEN ) else: - # user does not exist. Assume a typo in the username or password + # user does not exist. Assume a typo in the email or password # and allow the user to re-authenticate return Response( {"is_logged_in": False}, status=status.HTTP_401_UNAUTHORIZED diff --git a/rodan-main/code/rodan/views/project.py b/rodan-main/code/rodan/views/project.py index fdac5494b..0dca309f7 100644 --- a/rodan-main/code/rodan/views/project.py +++ b/rodan-main/code/rodan/views/project.py @@ -1,8 +1,7 @@ from rest_framework import generics from rest_framework import permissions, exceptions from rest_framework.response import Response -from django.contrib.auth.models import User -from rodan.models.project import Project +from rodan.models.project import Project, User from rodan.serializers.project import ProjectListSerializer, ProjectDetailSerializer from rodan.permissions import CustomObjectPermissions from django.db.models import Q diff --git a/rodan-main/code/rodan/views/user.py b/rodan-main/code/rodan/views/user.py index 64ad0a123..819fe0c33 100644 --- a/rodan-main/code/rodan/views/user.py +++ b/rodan-main/code/rodan/views/user.py @@ -1,5 +1,4 @@ from django.core.exceptions import PermissionDenied -from django.contrib.auth.models import User from django.dispatch import receiver from django.db.models.signals import post_save @@ -11,6 +10,7 @@ from rodan.permissions import CustomObjectPermissions from rodan.serializers.user import UserSerializer, UserListSerializer +from rodan.models.user import User import django_filters @@ -54,8 +54,9 @@ def get_queryset(self): def post(self, request, *args, **kwargs): userName = request.data.get("username", None) + email = request.data.get("email", None) userPass = request.data.get("password", None) - user = User.objects.create_user(username=userName, password=userPass) + user = User.objects.create_user(username=userName, email=email, password=userPass) if not user: return Response( {"message": "error creating user"}, status=status.HTTP_200_OK diff --git a/rodan-main/code/rodan/views/userpreference.py b/rodan-main/code/rodan/views/userpreference.py index bfc52e3ea..1293ee847 100644 --- a/rodan-main/code/rodan/views/userpreference.py +++ b/rodan-main/code/rodan/views/userpreference.py @@ -1,5 +1,5 @@ from __future__ import absolute_import -from rodan.models import UserPreference +from rodan.models import UserPreference, User from rodan.serializers.userpreference import ( UserPreferenceListSerializer, UserPreferenceSerializer, @@ -7,7 +7,6 @@ from rodan.exceptions import CustomAPIException from rodan.permissions import CustomObjectPermissions -from django.contrib.auth.models import User from django.urls import reverse, Resolver404, resolve from rest_framework import generics diff --git a/scripts/local.env b/scripts/local.env index 350b579cf..50b7acae7 100644 --- a/scripts/local.env +++ b/scripts/local.env @@ -3,6 +3,7 @@ ############################################################################### ADMIN_USER=rodan +ADMIN_EMAIL=admin@rodan2.simssa.ca ADMIN_PASS=rodan DJANGO_DEBUG_MODE=False DJANGO_SECRET_KEY=localdev diff --git a/scripts/production.sample b/scripts/production.sample index 4d4638d27..2bd07c666 100644 --- a/scripts/production.sample +++ b/scripts/production.sample @@ -3,6 +3,7 @@ ############################################################################### ADMIN_USER=someadmin +ADMIN_EMAIL=admin@rodan2.simssa.ca ADMIN_PASS=123456Seven DJANGO_DEBUG_MODE=False DJANGO_SECRET_KEY=AVeryLongSecretKeyThatWasGeneratedByOurSpecialScript_DoNotForgetToChangeThis diff --git a/scripts/staging.sample b/scripts/staging.sample index ec163bb83..3f8a62eb2 100644 --- a/scripts/staging.sample +++ b/scripts/staging.sample @@ -3,6 +3,7 @@ ############################################################################### ADMIN_USER=someadmin +ADMIN_EMAIL=admin@rodan2.simssa.ca ADMIN_PASS=123456Seven DJANGO_DEBUG_MODE=False DJANGO_SECRET_KEY=AVeryLongSecretKeyThatWasGeneratedByOurSpecialScript_DoNotForgetToChangeThis diff --git a/scripts/start b/scripts/start index a062dd59e..003b042fa 100644 --- a/scripts/start +++ b/scripts/start @@ -23,10 +23,11 @@ runuser -u root -- yes | MIGRATE="True" python3 manage.py migrate --noinput cat << EOF | runuser -u root -- python3 manage.py shell import os -from django.contrib.auth.models import User +from django.db.models import Q +from rodan.models.user import User print ("Checking if Django super user exists...") -if not User.objects.filter(username=os.getenv('ADMIN_USER')).exists(): - User.objects.create_superuser(os.getenv('ADMIN_USER'), '', os.getenv('ADMIN_PASS')) +if not User.objects.filter(Q(username=os.getenv('ADMIN_USER')) | Q(email=os.getenv('ADMIN_EMAIL'))).exists(): + User.objects.create_superuser(os.getenv('ADMIN_USER'), os.getenv('ADMIN_EMAIL'), os.getenv('ADMIN_PASS')) print ("Created new user.") EOF