From 87f646ca2825dcbb165d41e94b461b7eeb6a41c9 Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Tue, 27 Jun 2023 14:46:50 -0400 Subject: [PATCH 01/12] Use custom user model --- rodan-main/code/rodan/admin/admin.py | 4 ++- .../code/rodan/migrations/0001_initial.py | 34 ++++++++++++++++--- rodan-main/code/rodan/models/__init__.py | 3 +- rodan-main/code/rodan/models/project.py | 3 +- rodan-main/code/rodan/models/resource.py | 2 +- rodan-main/code/rodan/models/resourcelist.py | 2 +- .../code/rodan/models/resultspackage.py | 2 +- rodan-main/code/rodan/models/runjob.py | 2 +- rodan-main/code/rodan/models/tempauthtoken.py | 2 +- rodan-main/code/rodan/models/user.py | 6 ++++ .../code/rodan/models/userpreference.py | 2 +- rodan-main/code/rodan/models/workflow.py | 3 +- rodan-main/code/rodan/models/workflowrun.py | 3 +- rodan-main/code/rodan/serializers/user.py | 2 +- rodan-main/code/rodan/settings.py | 3 ++ rodan-main/code/rodan/test/helpers.py | 2 +- .../code/rodan/test/models/test_resource.py | 3 +- .../rodan/test/models/test_resourcelabel.py | 3 +- .../rodan/test/models/test_userpreference.py | 3 +- .../code/rodan/test/models/test_workflow.py | 3 +- .../rodan/test/models/test_workflowrun.py | 3 +- .../rodan/test/testIndividual_permission.py | 4 +-- rodan-main/code/rodan/views/auth.py | 2 +- rodan-main/code/rodan/views/project.py | 3 +- rodan-main/code/rodan/views/user.py | 2 +- rodan-main/code/rodan/views/userpreference.py | 3 +- scripts/start | 2 +- 27 files changed, 69 insertions(+), 37 deletions(-) create mode 100644 rodan-main/code/rodan/models/user.py diff --git a/rodan-main/code/rodan/admin/admin.py b/rodan-main/code/rodan/admin/admin.py index 8d19942e3..e91fa8cd0 100644 --- a/rodan-main/code/rodan/admin/admin.py +++ b/rodan-main/code/rodan/admin/admin.py @@ -1,8 +1,9 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin 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 @@ -79,3 +80,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/models/__init__.py b/rodan-main/code/rodan/models/__init__.py index a5e86a18f..8660dd3e7 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: 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..635a68be9 --- /dev/null +++ b/rodan-main/code/rodan/models/user.py @@ -0,0 +1,6 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser + +class User(AbstractUser): + class Meta: + db_table = "auth_user" \ No newline at end of file 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..471043b87 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 ############################################################################### diff --git a/rodan-main/code/rodan/test/helpers.py b/rodan-main/code/rodan/test/helpers.py index 7f5507c9d..84978059c 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): 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..8ef7df847 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 diff --git a/rodan-main/code/rodan/views/auth.py b/rodan-main/code/rodan/views/auth.py index 619969cae..7e4b6c9bd 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): 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..ca8aaaca9 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 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/start b/scripts/start index a062dd59e..d69993e38 100644 --- a/scripts/start +++ b/scripts/start @@ -23,7 +23,7 @@ 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 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')) From 454e356291b99e453691a5693b200256661539c0 Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Tue, 27 Jun 2023 16:10:13 -0400 Subject: [PATCH 02/12] Make email field mandatory and switch to email for login --- .../Main/Login/template-main_login.html | 2 +- rodan-main/code/rodan/admin/admin.py | 37 +++++--------- rodan-main/code/rodan/auth/backends.py | 18 +++++++ .../code/rodan/migrations/0001_initial.py | 4 ++ .../migrations/0003_auto_20230627_1526.py | 31 +++++++++++ rodan-main/code/rodan/models/user.py | 51 ++++++++++++++++++- rodan-main/code/rodan/settings.py | 11 ++-- rodan-main/code/rodan/test/helpers.py | 4 +- .../rodan/test/testIndividual_permission.py | 16 +++--- rodan-main/code/rodan/views/user.py | 3 +- scripts/local.env | 1 + scripts/production.sample | 1 + scripts/staging.sample | 1 + scripts/start | 2 +- 14 files changed, 136 insertions(+), 46 deletions(-) create mode 100644 rodan-main/code/rodan/auth/backends.py create mode 100644 rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py 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..0da030291 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,7 +4,7 @@
- +
diff --git a/rodan-main/code/rodan/admin/admin.py b/rodan-main/code/rodan/admin/admin.py index e91fa8cd0..36ea06818 100644 --- a/rodan-main/code/rodan/admin/admin.py +++ b/rodan-main/code/rodan/admin/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from django.contrib.auth.admin import UserAdmin +from django.contrib.auth import forms from guardian.admin import GuardedModelAdmin from rodan.models.project import Project @@ -13,27 +13,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',) @@ -57,20 +44,20 @@ class ResourceAdmin(admin.ModelAdmin): class ResourceListAdmin(admin.ModelAdmin): list_display = ('created', 'updated') +class UserChangeForm(forms.UserChangeForm): + class Meta(forms.UserChangeForm.Meta): + model = User -# class UserProfileInline(admin.StackedInline): -# model = RodanUser -# can_delete = False -# verbose_name_plural = 'profile' - - -# class RodanUserAdmin(UserAdmin): -# inlines = (UserProfileInline, ) +class UserCreationForm(forms.UserCreationForm): + class Meta(forms.UserCreationForm.Meta): + model = User -# admin.site.unregister(User) -# admin.site.register(User, RodanUserAdmin) +class UserAdmin(admin.ModelAdmin): + list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') + list_filter = ('is_staff', 'is_superuser', 'is_active') + form = UserChangeForm + add_form = UserCreationForm -# admin.site.register(RodanUser) admin.site.register(Project, ProjectAdmin) admin.site.register(WorkflowRun, WorkflowRunAdmin) admin.site.register(RunJob, RunJobAdmin) diff --git a/rodan-main/code/rodan/auth/backends.py b/rodan-main/code/rodan/auth/backends.py new file mode 100644 index 000000000..c53985555 --- /dev/null +++ b/rodan-main/code/rodan/auth/backends.py @@ -0,0 +1,18 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend +from rodan.models.user import User + + +class EmailBackend(ModelBackend): + def authenticate(self, request, email=None, password=None, **kwargs): + if email is None: + email = kwargs.get(User.USERNAME_FIELD) + try: + user = User.objects.get(email=email) + except User.DoesNotExist: + # Run the default password hasher once to reduce the timing + # difference between an existing and a non-existing user. + User().set_password(password) + else: + if user.check_password(password) and self.user_can_authenticate(user): + return user \ 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 7e07789dd..a85d51f3c 100644 --- a/rodan-main/code/rodan/migrations/0001_initial.py +++ b/rodan-main/code/rodan/migrations/0001_initial.py @@ -70,8 +70,10 @@ class Migration(migrations.Migration): ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('label', models.CharField(blank=True, db_index=True, max_length=255, null=True)), ('extern', models.BooleanField(db_index=True, default=False)), + ('created', models.DateTimeField(auto_now_add=True)), ], options={ + 'ordering': ['created'], 'permissions': (('view_inputport', 'View InputPort'),), }, ), @@ -115,8 +117,10 @@ class Migration(migrations.Migration): ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('label', models.CharField(blank=True, db_index=True, max_length=255, null=True)), ('extern', models.BooleanField(db_index=True, default=False)), + ('created', models.DateTimeField(auto_now_add=True)), ], options={ + 'ordering': ['created'], 'permissions': (('view_outputport', 'View OutputPort'),), }, ), 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..26e0ca857 --- /dev/null +++ b/rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py @@ -0,0 +1,31 @@ +from django.db import migrations, models +import rodan.models.user + + +class Migration(migrations.Migration): + """ + This migration makes the email field unique and non-blank. + """ + + dependencies = [ + ('rodan', '0002_auto_20230522_1420'), + ] + + operations = [ + migrations.AlterModelManagers( + name='user', + managers=[ + ('objects', rodan.models.user.UserManager()), + ], + ), + # 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(max_length=254, unique=True, verbose_name='email address'), + ), + ] diff --git a/rodan-main/code/rodan/models/user.py b/rodan-main/code/rodan/models/user.py index 635a68be9..b3e04684b 100644 --- a/rodan-main/code/rodan/models/user.py +++ b/rodan-main/code/rodan/models/user.py @@ -1,6 +1,53 @@ from django.db import models -from django.contrib.auth.models import AbstractUser +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(using=self._db) + 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) + + objects = UserManager() + class Meta: - db_table = "auth_user" \ No newline at end of file + db_table = "auth_user" diff --git a/rodan-main/code/rodan/settings.py b/rodan-main/code/rodan/settings.py index 471043b87..f8a2bf0f6 100644 --- a/rodan-main/code/rodan/settings.py +++ b/rodan-main/code/rodan/settings.py @@ -371,15 +371,14 @@ "DEFAULT_PAGINATION_CLASS": "rodan.paginators.pagination.CustomPagination", } -# used by django-guardian AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.ModelBackend", # defaults + # "django.contrib.auth.backends.ModelBackend", # Username and password authentication + "rodan.auth.backends.EmailBackend", # Email and password authentication "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 84978059c..43822804b 100644 --- a/rodan-main/code/rodan/test/helpers.py +++ b/rodan-main/code/rodan/test/helpers.py @@ -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/testIndividual_permission.py b/rodan-main/code/rodan/test/testIndividual_permission.py index 8ef7df847..9d5d35e79 100644 --- a/rodan-main/code/rodan/test/testIndividual_permission.py +++ b/rodan-main/code/rodan/test/testIndividual_permission.py @@ -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/views/user.py b/rodan-main/code/rodan/views/user.py index ca8aaaca9..819fe0c33 100644 --- a/rodan-main/code/rodan/views/user.py +++ b/rodan-main/code/rodan/views/user.py @@ -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/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 d69993e38..28976510a 100644 --- a/scripts/start +++ b/scripts/start @@ -26,7 +26,7 @@ import os 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')) + User.objects.create_superuser(os.getenv('ADMIN_USER'), os.getenv('ADMIN_EMAIL'), os.getenv('ADMIN_PASS')) print ("Created new user.") EOF From a9a04a5c4a8fdfc6b5333f753b418674842bea73 Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Tue, 27 Jun 2023 16:52:20 -0400 Subject: [PATCH 03/12] Allow users to change username --- .../Views/Master/Main/User/Individual/ViewUser.js | 13 +++++++++---- .../Individual/template-main_user_individual.html | 2 ++ 2 files changed, 11 insertions(+), 4 deletions(-) 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..f4f9d264f 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 @@ -38,10 +38,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')}); @@ -93,6 +97,7 @@ 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', 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..41bdb7ae0 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,4 +1,6 @@
+ + From fd11ed98da107753c77c34ceefe90526d917a0e3 Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Wed, 28 Jun 2023 12:44:04 -0400 Subject: [PATCH 04/12] Add permissions to user model and migrations --- rodan-main/code/rodan/admin/admin.py | 18 +++++++++++++++--- rodan-main/code/rodan/auth/__init__.py | 0 .../migrations/0003_auto_20230627_1526.py | 6 +++++- rodan-main/code/rodan/models/user.py | 5 ++++- 4 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 rodan-main/code/rodan/auth/__init__.py diff --git a/rodan-main/code/rodan/admin/admin.py b/rodan-main/code/rodan/admin/admin.py index 36ea06818..4098e373f 100644 --- a/rodan-main/code/rodan/admin/admin.py +++ b/rodan-main/code/rodan/admin/admin.py @@ -1,5 +1,6 @@ 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 @@ -52,12 +53,23 @@ class UserCreationForm(forms.UserCreationForm): class Meta(forms.UserCreationForm.Meta): model = User -class UserAdmin(admin.ModelAdmin): - list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') - list_filter = ('is_staff', 'is_superuser', 'is_active') +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') + + add_fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ('username', 'email', 'password1', 'password2'), + }), + ) + + search_fields = ('username', 'email') + ordering = ('username', 'email') + admin.site.register(Project, ProjectAdmin) admin.site.register(WorkflowRun, WorkflowRunAdmin) admin.site.register(RunJob, RunJobAdmin) diff --git a/rodan-main/code/rodan/auth/__init__.py b/rodan-main/code/rodan/auth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py b/rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py index 26e0ca857..c9ca9836a 100644 --- a/rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py +++ b/rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py @@ -4,7 +4,7 @@ class Migration(migrations.Migration): """ - This migration makes the email field unique and non-blank. + This migration makes the email field unique and non-blank and adds permissions to custom user model. """ dependencies = [ @@ -18,6 +18,10 @@ class Migration(migrations.Migration): ('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 = ''", diff --git a/rodan-main/code/rodan/models/user.py b/rodan-main/code/rodan/models/user.py index b3e04684b..577d099d0 100644 --- a/rodan-main/code/rodan/models/user.py +++ b/rodan-main/code/rodan/models/user.py @@ -21,7 +21,7 @@ def _create_user(self, username, email, password, **extra_fields): username = self.model.normalize_username(username) user = self.model(username=username, email=email, **extra_fields) user.set_password(password) - user.save(using=self._db) + user.save() return user def create_user(self, username, email, password=None, **extra_fields): @@ -51,3 +51,6 @@ class User(AbstractUser): class Meta: db_table = "auth_user" + permissions = ( + ("view_user", "View user"), + ) From 1abdc64e658b56e6315f1c62b759a0a877b8657a Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Wed, 28 Jun 2023 13:09:27 -0400 Subject: [PATCH 05/12] Handle validation errors when saving user --- .../Controllers/ControllerAuthentication.js | 11 ++++++- .../code/src/js/Shared/RODAN_EVENTS.js | 2 ++ .../Master/Main/User/Individual/ViewUser.js | 30 +++++++++++++++++++ .../template-main_user_individual.html | 8 +++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/rodan-client/code/src/js/Controllers/ControllerAuthentication.js b/rodan-client/code/src/js/Controllers/ControllerAuthentication.js index 6120d0f23..2d5471920 100644 --- a/rodan-client/code/src/js/Controllers/ControllerAuthentication.js +++ b/rodan-client/code/src/js/Controllers/ControllerAuthentication.js @@ -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,15 @@ 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 }); + } + /** * Handle success response from changing password. */ 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/User/Individual/ViewUser.js b/rodan-client/code/src/js/Views/Master/Main/User/Individual/ViewUser.js index f4f9d264f..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)); } /** @@ -90,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' @@ -101,6 +127,10 @@ ViewUser.prototype.ui = { 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/User/Individual/template-main_user_individual.html b/rodan-client/code/templates/Views/Master/Main/User/Individual/template-main_user_individual.html index 41bdb7ae0..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,12 +1,20 @@
+

+ +

+ +

+ +

+
From 6a5d49fb666e15047c9d04d0d1cd6b03c359ce64 Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Thu, 29 Jun 2023 09:33:46 -0400 Subject: [PATCH 06/12] Change user model username field to email --- .../js/Controllers/ControllerAuthentication.js | 4 ++-- .../js/Views/Master/Main/Login/ViewLogin.js | 4 ++-- .../Master/Main/Login/template-main_login.html | 4 ++-- rodan-main/code/rodan/auth/__init__.py | 0 rodan-main/code/rodan/auth/backends.py | 18 ------------------ rodan-main/code/rodan/models/user.py | 3 +++ rodan-main/code/rodan/settings.py | 3 +-- rodan-main/code/rodan/views/auth.py | 10 +++++----- 8 files changed, 15 insertions(+), 31 deletions(-) delete mode 100644 rodan-main/code/rodan/auth/__init__.py delete mode 100644 rodan-main/code/rodan/auth/backends.py diff --git a/rodan-client/code/src/js/Controllers/ControllerAuthentication.js b/rodan-client/code/src/js/Controllers/ControllerAuthentication.js index 2d5471920..3ba567ccf 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); } /** 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/templates/Views/Master/Main/Login/template-main_login.html b/rodan-client/code/templates/Views/Master/Main/Login/template-main_login.html index 0da030291..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-main/code/rodan/auth/__init__.py b/rodan-main/code/rodan/auth/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/rodan-main/code/rodan/auth/backends.py b/rodan-main/code/rodan/auth/backends.py deleted file mode 100644 index c53985555..000000000 --- a/rodan-main/code/rodan/auth/backends.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.contrib.auth import get_user_model -from django.contrib.auth.backends import ModelBackend -from rodan.models.user import User - - -class EmailBackend(ModelBackend): - def authenticate(self, request, email=None, password=None, **kwargs): - if email is None: - email = kwargs.get(User.USERNAME_FIELD) - try: - user = User.objects.get(email=email) - except User.DoesNotExist: - # Run the default password hasher once to reduce the timing - # difference between an existing and a non-existing user. - User().set_password(password) - else: - if user.check_password(password) and self.user_can_authenticate(user): - return user \ No newline at end of file diff --git a/rodan-main/code/rodan/models/user.py b/rodan-main/code/rodan/models/user.py index 577d099d0..1e0c9ad01 100644 --- a/rodan-main/code/rodan/models/user.py +++ b/rodan-main/code/rodan/models/user.py @@ -47,6 +47,9 @@ class User(AbstractUser): email = models.EmailField(verbose_name="email address", unique=True) + USERNAME_FIELD = "email" + REQUIRED_FIELDS = ["username"] + objects = UserManager() class Meta: diff --git a/rodan-main/code/rodan/settings.py b/rodan-main/code/rodan/settings.py index f8a2bf0f6..7db0c72a5 100644 --- a/rodan-main/code/rodan/settings.py +++ b/rodan-main/code/rodan/settings.py @@ -372,8 +372,7 @@ } AUTHENTICATION_BACKENDS = [ - # "django.contrib.auth.backends.ModelBackend", # Username and password authentication - "rodan.auth.backends.EmailBackend", # Email and password authentication + "django.contrib.auth.backends.ModelBackend", # Authenticate with User.USERNAME_FIELD and password "guardian.backends.ObjectPermissionBackend", ] diff --git a/rodan-main/code/rodan/views/auth.py b/rodan-main/code/rodan/views/auth.py index 7e4b6c9bd..46269d0a9 100644 --- a/rodan-main/code/rodan/views/auth.py +++ b/rodan-main/code/rodan/views/auth.py @@ -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 From a3e7127a18473d85e270654ac5b150365ec699fa Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Thu, 29 Jun 2023 09:44:20 -0400 Subject: [PATCH 07/12] Custom error message for duplicate emails --- rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py | 2 +- rodan-main/code/rodan/models/user.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py b/rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py index c9ca9836a..2053af20b 100644 --- a/rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py +++ b/rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py @@ -30,6 +30,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='user', name='email', - field=models.EmailField(max_length=254, unique=True, verbose_name='email address'), + 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/user.py b/rodan-main/code/rodan/models/user.py index 1e0c9ad01..9fd7ac76f 100644 --- a/rodan-main/code/rodan/models/user.py +++ b/rodan-main/code/rodan/models/user.py @@ -45,7 +45,7 @@ class User(AbstractUser): A custom user model that uses email as the username field. """ - email = models.EmailField(verbose_name="email address", unique=True) + 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"] From 1c694a34ba39473feedce7df3c39cd665eb5a46b Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Thu, 29 Jun 2023 09:51:39 -0400 Subject: [PATCH 08/12] Show message in modal footer for save user errors --- rodan-client/code/src/js/Controllers/ControllerAuthentication.js | 1 + rodan-client/code/src/js/Controllers/ControllerModal.js | 1 + 2 files changed, 2 insertions(+) diff --git a/rodan-client/code/src/js/Controllers/ControllerAuthentication.js b/rodan-client/code/src/js/Controllers/ControllerAuthentication.js index 3ba567ccf..333f6274d 100644 --- a/rodan-client/code/src/js/Controllers/ControllerAuthentication.js +++ b/rodan-client/code/src/js/Controllers/ControllerAuthentication.js @@ -363,6 +363,7 @@ export default class ControllerAuthentication extends BaseController { 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.'}); } /** 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'); } /** From 8838929fc870a76dc554180dde514fed870b7931 Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Thu, 29 Jun 2023 10:33:22 -0400 Subject: [PATCH 09/12] Update check for existing superuser in start script --- scripts/start | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/start b/scripts/start index 28976510a..003b042fa 100644 --- a/scripts/start +++ b/scripts/start @@ -23,9 +23,10 @@ runuser -u root -- yes | MIGRATE="True" python3 manage.py migrate --noinput cat << EOF | runuser -u root -- python3 manage.py shell import os +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(): +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.") From 3e5d3947361bf5253aa604f2daebafe09f4450c6 Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Thu, 29 Jun 2023 10:42:48 -0400 Subject: [PATCH 10/12] Fix initial migration --- rodan-main/code/rodan/migrations/0001_initial.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rodan-main/code/rodan/migrations/0001_initial.py b/rodan-main/code/rodan/migrations/0001_initial.py index a85d51f3c..7e07789dd 100644 --- a/rodan-main/code/rodan/migrations/0001_initial.py +++ b/rodan-main/code/rodan/migrations/0001_initial.py @@ -70,10 +70,8 @@ class Migration(migrations.Migration): ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('label', models.CharField(blank=True, db_index=True, max_length=255, null=True)), ('extern', models.BooleanField(db_index=True, default=False)), - ('created', models.DateTimeField(auto_now_add=True)), ], options={ - 'ordering': ['created'], 'permissions': (('view_inputport', 'View InputPort'),), }, ), @@ -117,10 +115,8 @@ class Migration(migrations.Migration): ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('label', models.CharField(blank=True, db_index=True, max_length=255, null=True)), ('extern', models.BooleanField(db_index=True, default=False)), - ('created', models.DateTimeField(auto_now_add=True)), ], options={ - 'ordering': ['created'], 'permissions': (('view_outputport', 'View OutputPort'),), }, ), From e73ff46c1e963e9fff3170a9cdbe13aab1e6ae26 Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Thu, 29 Jun 2023 10:52:44 -0400 Subject: [PATCH 11/12] Fix auth test case --- rodan-main/code/rodan/test/views/test_auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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}) From 745098e2341b52f5cf660aab38b2d1c7fa31fa9a Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Thu, 29 Jun 2023 14:32:52 -0400 Subject: [PATCH 12/12] Fix content type in post migrate handler --- rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py | 2 +- rodan-main/code/rodan/models/__init__.py | 2 +- rodan-main/code/rodan/models/user.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py b/rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py index 2053af20b..939acd00b 100644 --- a/rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py +++ b/rodan-main/code/rodan/migrations/0003_auto_20230627_1526.py @@ -20,7 +20,7 @@ class Migration(migrations.Migration): ), migrations.AlterModelOptions( name='user', - options={'permissions': (('view_user', 'View user'),)}, + options={'permissions': (('view_user', 'View User'),)}, ), # Make sure there are no empty email fields by giving them a default value migrations.RunSQL( diff --git a/rodan-main/code/rodan/models/__init__.py b/rodan-main/code/rodan/models/__init__.py index 8660dd3e7..e6a4a4500 100644 --- a/rodan-main/code/rodan/models/__init__.py +++ b/rodan-main/code/rodan/models/__init__.py @@ -58,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/user.py b/rodan-main/code/rodan/models/user.py index 9fd7ac76f..bd5e87aab 100644 --- a/rodan-main/code/rodan/models/user.py +++ b/rodan-main/code/rodan/models/user.py @@ -55,5 +55,5 @@ class User(AbstractUser): class Meta: db_table = "auth_user" permissions = ( - ("view_user", "View user"), + ("view_user", "View User"), )