From 1ff15e2b96aa791ae48bb032806ff86fa1e4511b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20R=C3=B8dahl=20Thingnes?= Date: Tue, 18 Apr 2023 21:54:37 +0200 Subject: [PATCH 1/8] End of session --- app/authentication/exceptions.py | 7 - app/authentication/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/populate_groups.py | 31 ---- app/authentication/migrations/__init__.py | 0 app/authentication/serializers/__init__.py | 3 - app/authentication/serializers/auth.py | 6 - .../serializers/change_password.py | 60 -------- .../serializers/reset_password.py | 47 ------ app/authentication/urls.py | 15 -- app/authentication/utils.py | 24 +++ app/authentication/views.py | 58 +------- app/settings.py | 139 +++++++----------- app/urls.py | 1 - requirements.txt | 15 +- 15 files changed, 92 insertions(+), 314 deletions(-) delete mode 100644 app/authentication/exceptions.py delete mode 100644 app/authentication/management/__init__.py delete mode 100644 app/authentication/management/commands/__init__.py delete mode 100644 app/authentication/management/commands/populate_groups.py delete mode 100644 app/authentication/migrations/__init__.py delete mode 100644 app/authentication/serializers/__init__.py delete mode 100644 app/authentication/serializers/auth.py delete mode 100644 app/authentication/serializers/change_password.py delete mode 100644 app/authentication/serializers/reset_password.py delete mode 100644 app/authentication/urls.py create mode 100644 app/authentication/utils.py diff --git a/app/authentication/exceptions.py b/app/authentication/exceptions.py deleted file mode 100644 index d895183d6..000000000 --- a/app/authentication/exceptions.py +++ /dev/null @@ -1,7 +0,0 @@ -from rest_framework import status -from rest_framework.exceptions import APIException - - -class APIAuthUserDoesNotExist(APIException): - status_code = status.HTTP_401_UNAUTHORIZED - default_detail = "Brukernavnet du har oppgitt tilhører ingen konto. Kontroller brukernavnet og prøv på nytt" diff --git a/app/authentication/management/__init__.py b/app/authentication/management/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/authentication/management/commands/__init__.py b/app/authentication/management/commands/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/authentication/management/commands/populate_groups.py b/app/authentication/management/commands/populate_groups.py deleted file mode 100644 index 09de34c48..000000000 --- a/app/authentication/management/commands/populate_groups.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.core.management.base import BaseCommand - -from app.authentication.models import Group - -groups = [ - ("Hovedstyret", "HS"), - ("Drift", "Drift"), - ("Promo", "Promo"), - ("Næringsliv og Kurs", "NoK"), - ("Sosialen", "Sos"), - ("NetKom", "NetKom"), - ("JubKom", "JubKom"), - ("TurKom", "TurKom"), - ("KosKom", "KosKom"), - ("ArrKom", "ArrKom"), - ("FestKom", "FestKom"), - ("ÅreKom", "ÅreKom"), -] - - -class Command(BaseCommand): - args = "" - help = "No help needed" - - def create_groups(self): - for group in groups: - newGroup = Group(name=group[0], abbr=group[1]) - newGroup.save() - - def handle(self, *args, **options): - self.create_groups() diff --git a/app/authentication/migrations/__init__.py b/app/authentication/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/authentication/serializers/__init__.py b/app/authentication/serializers/__init__.py deleted file mode 100644 index 61a0a1890..000000000 --- a/app/authentication/serializers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from app.authentication.serializers.auth import AuthSerializer -from app.authentication.serializers.change_password import ChangePasswordSerializer -from app.authentication.serializers.reset_password import PasswordResetSerializer diff --git a/app/authentication/serializers/auth.py b/app/authentication/serializers/auth.py deleted file mode 100644 index 7754733a4..000000000 --- a/app/authentication/serializers/auth.py +++ /dev/null @@ -1,6 +0,0 @@ -from rest_framework import serializers - - -class AuthSerializer(serializers.Serializer): - user_id = serializers.CharField(max_length=200) - password = serializers.CharField(max_length=200) diff --git a/app/authentication/serializers/change_password.py b/app/authentication/serializers/change_password.py deleted file mode 100644 index bc1dc685c..000000000 --- a/app/authentication/serializers/change_password.py +++ /dev/null @@ -1,60 +0,0 @@ -from django.conf import settings -from django.contrib.auth.forms import SetPasswordForm -from rest_framework import serializers - - -class ChangePasswordSerializer(serializers.Serializer): - old_password = serializers.CharField(max_length=128) - new_password1 = serializers.CharField(max_length=128) - new_password2 = serializers.CharField(max_length=128) - - set_password_form_class = SetPasswordForm - - def __init__(self, *args, **kwargs): - self.old_password_field_enabled = getattr( - settings, "OLD_PASSWORD_FIELD_ENABLED", True - ) - self.logout_on_password_change = getattr( - settings, "LOGOUT_ON_PASSWORD_CHANGE", False - ) - super(ChangePasswordSerializer, self).__init__(*args, **kwargs) - - if not self.old_password_field_enabled: - self.fields.pop("old_password") - - self.request = self.context.get("request") - self.user = getattr(self.request, "user", None) - - def validate_old_password(self, value): - invalid_password_conditions = ( - self.old_password_field_enabled, - self.user, - not self.user.check_password(value), - ) - - if all(invalid_password_conditions): - err_msg = ( - "Your old password was entered incorrectly. Please enter it again." - ) - raise serializers.ValidationError(err_msg) - return value - - def validate(self, attrs): - if attrs.get("old_password") == attrs.get("new_password1"): - err_msg = "Your new password can't be the same as the old one." - raise serializers.ValidationError(err_msg) - - self.set_password_form = self.set_password_form_class( - user=self.user, data=attrs - ) - - if not self.set_password_form.is_valid(): - raise serializers.ValidationError(self.set_password_form.errors) - return attrs - - def save(self): - self.set_password_form.save() - if not self.logout_on_password_change: - from django.contrib.auth import update_session_auth_hash - - update_session_auth_hash(self.request, self.user) diff --git a/app/authentication/serializers/reset_password.py b/app/authentication/serializers/reset_password.py deleted file mode 100644 index 73baeb6ff..000000000 --- a/app/authentication/serializers/reset_password.py +++ /dev/null @@ -1,47 +0,0 @@ -import os - -from django.contrib.auth.forms import PasswordResetForm -from rest_framework import serializers - -from sentry_sdk import capture_exception - -from app.settings import DOMAIN - - -class PasswordResetSerializer(serializers.Serializer): - """ - Serializer for requesting a password reset e-mail. - """ - - email = serializers.EmailField() - password_reset_form_class = PasswordResetForm - - def validate_email(self, value): - # Create PasswordResetForm with the serializer - try: - self.reset_form = self.password_reset_form_class(data=self.initial_data) - if not self.reset_form.is_valid(): - raise serializers.ValidationError(self.reset_form.errors) - except Exception as validate_email_fail: - capture_exception(validate_email_fail) - raise - return value - - def save(self): - try: - request = self.context.get("request") - # Set some values to trigger the send_email method. - opts = { - "use_https": request.is_secure(), - "from_email": os.environ.get("EMAIL_USER") or None, - "request": request, - "extra_email_context": {"domain": DOMAIN}, - "html_email_template_name": "passwordResetEmail.html", - "subject_template_name": "password_reset_subject.txt", - } - - self.reset_form.save(**opts) - - except Exception as save_fail: - capture_exception(save_fail) - raise diff --git a/app/authentication/urls.py b/app/authentication/urls.py deleted file mode 100644 index 54a220f5f..000000000 --- a/app/authentication/urls.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.urls import include, re_path -from rest_framework import routers - -from app.authentication.views import login - -router = routers.DefaultRouter() - -# Register content viewpoints here -urlpatterns = [ - re_path(r"", include(router.urls)), - re_path(r"^login", login), - re_path(r"^rest-auth/", include("dj_rest_auth.urls")), - re_path(r"^", include("django.contrib.auth.urls")), - # re_path(r'^token', obtain_auth_token), #Used to bypass all restrictions when getting token -] diff --git a/app/authentication/utils.py b/app/authentication/utils.py new file mode 100644 index 000000000..ce0919d10 --- /dev/null +++ b/app/authentication/utils.py @@ -0,0 +1,24 @@ +import json +import jwt +import requests +from django.contrib.auth import authenticate + + +def jwt_decode_token(token): + header = jwt.get_unverified_header(token) + jwks = requests.get('https://tihlde-dev.eu.auth0.com/.well-known/jwks.json').json() + public_key = None + for jwk in jwks['keys']: + if jwk['kid'] == header['kid']: + public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk)) + + if public_key is None: + raise Exception('Public key not found.') + + issuer = 'https://{}/'.format('tihlde-dev.eu.auth0.com') + return jwt.decode(token, public_key, audience='api-dev.tihlde.org', issuer=issuer, algorithms=['RS256']) + +def jwt_get_username_from_payload_handler(payload): + username = payload.get('sub').replace('|', '.') + authenticate(remote_user=username) + return username \ No newline at end of file diff --git a/app/authentication/views.py b/app/authentication/views.py index bcb34957c..2f1a0d81a 100644 --- a/app/authentication/views.py +++ b/app/authentication/views.py @@ -1,50 +1,8 @@ -from rest_framework import status -from rest_framework.authtoken.models import Token -from rest_framework.decorators import api_view -from rest_framework.response import Response - -from sentry_sdk import capture_exception - -from app.authentication.exceptions import APIAuthUserDoesNotExist -from app.authentication.serializers import AuthSerializer -from app.content.models.user import User - - -@api_view(["POST"]) -def login(request): - serializer = AuthSerializer(data=request.data) - if not serializer.is_valid(): - return Response( - {"detail": "Noe er feil i brukernavnet eller passordet ditt"}, - status=status.HTTP_401_UNAUTHORIZED, - ) - - user_id = serializer.data["user_id"] - password = serializer.data["password"] - - user = _try_to_get_user(user_id=user_id) - - if user.check_password(password): - if user.is_TIHLDE_member: - try: - token = Token.objects.get(user_id=user_id).key - return Response({"token": token}, status=status.HTTP_200_OK) - except Token.DoesNotExist as token_not_exist: - capture_exception(token_not_exist) - - return Response( - {"detail": "Du må aktiveres som TIHLDE-medlem før du kan logge inn"}, - status=status.HTTP_401_UNAUTHORIZED, - ) - else: - return Response( - {"detail": "Brukernavnet eller passordet ditt var feil"}, - status=status.HTTP_401_UNAUTHORIZED, - ) - - -def _try_to_get_user(user_id): - try: - return User.objects.get(user_id=user_id) - except User.DoesNotExist: - raise APIAuthUserDoesNotExist +def get_token_auth_header(request): + """Obtains the Access Token from the Authorization Header""" + + auth = request.META.get("HTTP_AUTHORIZATION", None) + parts = auth.split() + token = parts[1] + + return token \ No newline at end of file diff --git a/app/settings.py b/app/settings.py index b3d99f4fd..022ec81d9 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,15 +1,3 @@ -""" -Django settings for app project on Heroku. For more info, see: -https://github.com/heroku/heroku-django-template - -For more information on this file, see -https://docs.djangoproject.com/en/3.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/3.2/ref/settings/ -""" - -import logging import os import sentry_sdk @@ -22,13 +10,11 @@ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) +ROOT_URLCONF = "app.urls" DOMAIN = "api.tihlde.org" load_dotenv(str(BASE_DIR) + "/.env", override=True) -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ - # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.environ.get("DJANGO_SECRET") @@ -52,20 +38,6 @@ AZURE_BLOB_STORAGE_NAME = "tihldestorage.blob.core.windows.net" -# Application definition -sentry_sdk.init( - dsn=os.environ.get("SENTRY_DSN"), - environment=ENVIRONMENT.value, - integrations=[DjangoIntegration()], - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # Adjusting this value in production is recommended, - traces_sample_rate=1.0, - # If you wish to associate users to errors (assuming you are using - # django.contrib.auth) you may enable sending PII data. - send_default_pii=True, -) - # Application definition INSTALLED_APPS = [ "django.contrib.admin", @@ -99,33 +71,6 @@ "app.badge", ] -# Django rest framework -REST_FRAMEWORK = { - "DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema", - "DEFAULT_PERMISSION_CLASSES": [ - "rest_framework.permissions.AllowAny", - ], - "EXCEPTION_HANDLER": "app.util.exceptions.exception_handler", - "TEST_REQUEST_DEFAULT_FORMAT": "json", -} -SWAGGER_SETTINGS = { - "SECURITY_DEFINITIONS": { - "DRF Token": { - "type": "apiKey", - "description": "Auth token to be passed as a header as custom authentication. " - "Can be found in the django admin panel.", - "name": "X-CSRF-Token", - "in": "header", - } - } -} -# Django rest auth framework -REST_AUTH_SERIALIZERS = { - "PASSWORD_RESET_SERIALIZER": "app.authentication.serializers.reset_password.PasswordResetSerializer", - "PASSWORD_CHANGE_SERIALIZER": "app.authentication.serializers.change_password.ChangePasswordSerializer", - "USER_DETAILS_SERIALIZER": "app.content.serializers.user.UserSerializer", -} - MIDDLEWARE = [ # Django Cors Headers "corsheaders.middleware.CorsMiddleware", @@ -137,12 +82,43 @@ "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.auth.middleware.RemoteUserMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.locale.LocaleMiddleware", ] -ROOT_URLCONF = "app.urls" +# Django rest framework +REST_FRAMEWORK = { + "DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema", + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ), + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_jwt.authentication.JSONWebTokenAuthentication", + "rest_framework.authentication.SessionAuthentication", + "rest_framework.authentication.BasicAuthentication", + ), + "EXCEPTION_HANDLER": "app.util.exceptions.exception_handler", + "TEST_REQUEST_DEFAULT_FORMAT": "json", +} + +# Auth0 Authentication +AUTH_USER_MODEL = "content.User" + +AUTHENTICATION_BACKENDS = [ + 'django.contrib.auth.backends.ModelBackend', + 'django.contrib.auth.backends.RemoteUserBackend', +] + +JWT_AUTH = { + 'JWT_PAYLOAD_GET_USERNAME_HANDLER': 'app.authentication.utils.jwt_get_username_from_payload_handler', + 'JWT_DECODE_HANDLER': 'app.authentication.utils.jwt_decode_token', + 'JWT_ALGORITHM': 'RS256', + 'JWT_AUDIENCE': 'https://dev-api.tihlde.org', + 'JWT_ISSUER': 'https://tihlde-dev.eu.auth0.com/', + 'JWT_AUTH_HEADER_PREFIX': 'Bearer', +} TEMPLATES = [ { @@ -180,23 +156,15 @@ } } -AUTH_USER_MODEL = "content.User" - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, -] +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" +# Email +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_USE_TLS = True +EMAIL_HOST = os.environ.get("EMAIL_HOST") or "smtp.mailtrap.io" +EMAIL_PORT = os.environ.get("EMAIL_PORT") or "2525" +EMAIL_HOST_USER = os.environ.get("EMAIL_USER") or "75ecff025dcb39" +EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_PASSWORD") or "8b1a00e838d6b7" # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ @@ -218,22 +186,12 @@ STATIC_URL = "/api/static/" CORS_ORIGIN_ALLOW_ALL = True - CORS_ALLOW_HEADERS = default_headers + ("X-CSRF-Token",) # Simplified static file serving. # https://warehouse.python.org/project/whitenoise/ STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" -# EMAIL SMTP Server setup -EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" -EMAIL_HOST = os.environ.get("EMAIL_HOST") or "smtp.mailtrap.io" -EMAIL_PORT = os.environ.get("EMAIL_PORT") or "2525" -EMAIL_USE_TLS = True - -EMAIL_HOST_USER = os.environ.get("EMAIL_USER") or "75ecff025dcb39" -EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_PASSWORD") or "8b1a00e838d6b7" - LOGGING = { "version": 1, "disable_existing_loggers": False, @@ -267,8 +225,19 @@ }, } -DEFAULT_AUTO_FIELD = "django.db.models.AutoField" - CELERY_BROKER_URL = "amqp://guest:guest@rabbitmq:5672" if ENVIRONMENT == EnvironmentOptions.LOCAL: CELERY_TASK_ALWAYS_EAGER = True + +sentry_sdk.init( + dsn=os.environ.get("SENTRY_DSN"), + environment=ENVIRONMENT.value, + integrations=[DjangoIntegration()], + # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # Adjusting this value in production is recommended, + traces_sample_rate=1.0, + # If you wish to associate users to errors (assuming you are using + # django.contrib.auth) you may enable sending PII data. + send_default_pii=True, +) diff --git a/app/urls.py b/app/urls.py index e5237b1e2..5d4209b90 100644 --- a/app/urls.py +++ b/app/urls.py @@ -25,7 +25,6 @@ path("", include("app.communication.urls")), path("", include("app.content.urls")), path("", include("app.group.urls")), - path("auth/", include("app.authentication.urls")), path("badges/", include("app.badge.urls")), path("forms/", include("app.forms.urls")), path("galleries/", include("app.gallery.urls")), diff --git a/requirements.txt b/requirements.txt index 64e63f1e0..67ca146ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,28 +14,29 @@ django-ical == 1.8.0 slack-sdk == 3.19.3 # Django -# ------------------------------------------------------------------------------ Django==4.0.8 django-enumchoicefield == 3.0.0 django-filter == 22.1 +django-mptt == 0.14.0 django-ordered-model~=3.6 # Django REST Framework djangorestframework==3.13.1 django-cors-headers dj-rest-auth == 2.2.3 - -#django dry rest permissions django-dry-rest-permissions == 1.2.0 +djangorestframework-csv == 2.1.1 # Django Polymorphic django-polymorphic ~= 3.1 django-rest-polymorphic == 0.1.9 -django-mptt == 0.14.0 +# Auth0 API Authorization +cryptography ~= 40.0.2 +drf-jwt ~= 1.19.2 +pyjwt ~= 2.6.0 # Code quality -# ------------------------------------------------------------------------------ pylint black == 22.10.0 isort @@ -45,7 +46,6 @@ flake8-black pre-commit == 2.20.0 # Testing -# ------------------------------------------------------------------------------ coverage pdbpp pytest == 7.1.1 @@ -54,6 +54,3 @@ pytest-django == 4.5.2 factory-boy == 3.2.1 pytest-factoryboy == 2.5.0 pytest-lazy-fixture==0.6.3 - -# CSV -djangorestframework-csv==2.1.1 From 4fd266c2274503ddb433ffcf20bbe87ff9dca0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20R=C3=B8dahl=20Thingnes?= Date: Sat, 22 Apr 2023 10:07:01 +0200 Subject: [PATCH 2/8] Access with auth0 works --- app/authentication/utils.py | 21 +++++++++++-------- app/constants.py | 3 +++ .../migrations/0053_increase_userid_length.py | 18 ++++++++++++++++ app/content/models/user.py | 14 +------------ app/settings.py | 11 ++++------ 5 files changed, 38 insertions(+), 29 deletions(-) create mode 100644 app/content/migrations/0053_increase_userid_length.py diff --git a/app/authentication/utils.py b/app/authentication/utils.py index ce0919d10..78107721a 100644 --- a/app/authentication/utils.py +++ b/app/authentication/utils.py @@ -1,24 +1,27 @@ +import os import json +from app.constants import AUTH0_AUDIENCE, AUTH0_DOMAIN import jwt import requests from django.contrib.auth import authenticate +def jwt_get_username_from_payload_handler(payload): + username = payload.get('sub').replace('|', '.') + authenticate(remote_user=username) + return username + + def jwt_decode_token(token): header = jwt.get_unverified_header(token) - jwks = requests.get('https://tihlde-dev.eu.auth0.com/.well-known/jwks.json').json() + jwks = requests.get('https://{}/.well-known/jwks.json'.format(AUTH0_DOMAIN)).json() + public_key = None for jwk in jwks['keys']: if jwk['kid'] == header['kid']: public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk)) if public_key is None: - raise Exception('Public key not found.') - - issuer = 'https://{}/'.format('tihlde-dev.eu.auth0.com') - return jwt.decode(token, public_key, audience='api-dev.tihlde.org', issuer=issuer, algorithms=['RS256']) + raise Exception('Public key not found while decoding token.') -def jwt_get_username_from_payload_handler(payload): - username = payload.get('sub').replace('|', '.') - authenticate(remote_user=username) - return username \ No newline at end of file + return jwt.decode(token, public_key, audience=AUTH0_AUDIENCE, issuer='https://{}/'.format(AUTH0_DOMAIN), algorithms=['RS256']) \ No newline at end of file diff --git a/app/constants.py b/app/constants.py index 638b4d67e..b690605f0 100644 --- a/app/constants.py +++ b/app/constants.py @@ -1,5 +1,8 @@ import os +AUTH0_DOMAIN = os.environ.get("AUTH0_DOMAIN") +AUTH0_AUDIENCE = os.environ.get("AUTH0_AUDIENCE") + MAIL_HS = "hs@tihlde.org" if os.environ.get("PROD") else "test+hs@tihlde.org" MAIL_ECONOMY = ( "okonomi@tihlde.org" if os.environ.get("PROD") else "test+okonomi@tihlde.org" diff --git a/app/content/migrations/0053_increase_userid_length.py b/app/content/migrations/0053_increase_userid_length.py new file mode 100644 index 000000000..270fdfec1 --- /dev/null +++ b/app/content/migrations/0053_increase_userid_length.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.8 on 2023-04-22 07:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('content', '0052_event_rules_and_photo_in_user'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='user_id', + field=models.CharField(max_length=255, primary_key=True, serialize=False), + ), + ] diff --git a/app/content/models/user.py b/app/content/models/user.py index 57555e9f8..dc74b2e13 100644 --- a/app/content/models/user.py +++ b/app/content/models/user.py @@ -1,6 +1,5 @@ from datetime import timedelta -from django.conf import settings from django.contrib.auth.hashers import make_password from django.contrib.auth.models import ( AbstractBaseUser, @@ -9,9 +8,6 @@ ) from django.db import models from django.db.models import Q -from django.db.models.signals import post_save -from django.dispatch import receiver -from rest_framework.authtoken.models import Token from app.common.enums import AdminGroup, Groups, GroupType, MembershipType from app.common.permissions import check_has_access @@ -59,7 +55,7 @@ class User(AbstractBaseUser, PermissionsMixin, BaseModel, OptionalImage): write_access = [AdminGroup.INDEX] read_access = [Groups.TIHLDE] - user_id = models.CharField(max_length=15, primary_key=True) + user_id = models.CharField(max_length=255, primary_key=True) first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) @@ -220,11 +216,3 @@ def has_object_get_user_detail_strikes_permission(self, request): [AdminGroup.NOK, AdminGroup.INDEX, AdminGroup.HS, AdminGroup.SOSIALEN], request, ) - - -@receiver(post_save, sender=settings.AUTH_USER_MODEL) -@disable_for_loaddata -def create_auth_token(sender, instance=None, created=False, **kwargs): - """Generate token at creation of user""" - if created: - Token.objects.create(user=instance) diff --git a/app/settings.py b/app/settings.py index 022ec81d9..8a196ef09 100644 --- a/app/settings.py +++ b/app/settings.py @@ -11,7 +11,6 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) ROOT_URLCONF = "app.urls" -DOMAIN = "api.tihlde.org" load_dotenv(str(BASE_DIR) + "/.env", override=True) @@ -28,6 +27,7 @@ else EnvironmentOptions.LOCAL ) +DOMAIN = "api.tihlde.org" WEBSITE_URL = ( "https://tihlde.org" if ENVIRONMENT == EnvironmentOptions.PRODUCTION @@ -36,8 +36,6 @@ else "http://localhost:3000" ) -AZURE_BLOB_STORAGE_NAME = "tihldestorage.blob.core.windows.net" - # Application definition INSTALLED_APPS = [ "django.contrib.admin", @@ -54,8 +52,6 @@ "rest_framework", "corsheaders", "django_filters", - "rest_framework.authtoken", - "dj_rest_auth", "dry_rest_permissions", "polymorphic", # Our apps @@ -105,7 +101,6 @@ # Auth0 Authentication AUTH_USER_MODEL = "content.User" - AUTHENTICATION_BACKENDS = [ 'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.RemoteUserBackend', @@ -229,6 +224,8 @@ if ENVIRONMENT == EnvironmentOptions.LOCAL: CELERY_TASK_ALWAYS_EAGER = True +AZURE_BLOB_STORAGE_NAME = "tihldestorage.blob.core.windows.net" + sentry_sdk.init( dsn=os.environ.get("SENTRY_DSN"), environment=ENVIRONMENT.value, @@ -240,4 +237,4 @@ # If you wish to associate users to errors (assuming you are using # django.contrib.auth) you may enable sending PII data. send_default_pii=True, -) +) \ No newline at end of file From 8d076aee827b8fe566b00724482dd92b12d35347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20R=C3=B8dahl=20Thingnes?= Date: Sun, 23 Apr 2023 18:06:50 +0200 Subject: [PATCH 3/8] Adding user and viewing /users/me works --- app/authentication/utils.py | 25 +++++++++++++++++++------ app/authentication/views.py | 8 -------- app/common/permissions.py | 13 ++++++------- app/settings.py | 4 ++-- 4 files changed, 27 insertions(+), 23 deletions(-) delete mode 100644 app/authentication/views.py diff --git a/app/authentication/utils.py b/app/authentication/utils.py index 78107721a..3b26fa047 100644 --- a/app/authentication/utils.py +++ b/app/authentication/utils.py @@ -1,18 +1,20 @@ import os import json from app.constants import AUTH0_AUDIENCE, AUTH0_DOMAIN +from app.content.models.user import User import jwt import requests from django.contrib.auth import authenticate -def jwt_get_username_from_payload_handler(payload): - username = payload.get('sub').replace('|', '.') - authenticate(remote_user=username) - return username +def get_jwt_from_request(request): + auth = request.META.get("HTTP_AUTHORIZATION", None) + parts = auth.split() + token = parts[1] + return token -def jwt_decode_token(token): +def decode_jwt(token): header = jwt.get_unverified_header(token) jwks = requests.get('https://{}/.well-known/jwks.json'.format(AUTH0_DOMAIN)).json() @@ -24,4 +26,15 @@ def jwt_decode_token(token): if public_key is None: raise Exception('Public key not found while decoding token.') - return jwt.decode(token, public_key, audience=AUTH0_AUDIENCE, issuer='https://{}/'.format(AUTH0_DOMAIN), algorithms=['RS256']) \ No newline at end of file + return jwt.decode(token, public_key, audience=AUTH0_AUDIENCE, issuer='https://{}/'.format(AUTH0_DOMAIN), algorithms=['RS256']) + +def get_userid_from_decoded_jwt(payload): + user_id = payload.get('sub').replace('|', '.') + authenticate(remote_user=user_id) + + return user_id + +def get_user_from_request(request): + user_id = get_userid_from_decoded_jwt(decode_jwt(get_jwt_from_request(request))) + + return User.objects.get(user_id=user_id) \ No newline at end of file diff --git a/app/authentication/views.py b/app/authentication/views.py deleted file mode 100644 index 2f1a0d81a..000000000 --- a/app/authentication/views.py +++ /dev/null @@ -1,8 +0,0 @@ -def get_token_auth_header(request): - """Obtains the Access Token from the Authorization Header""" - - auth = request.META.get("HTTP_AUTHORIZATION", None) - parts = auth.split() - token = parts[1] - - return token \ No newline at end of file diff --git a/app/common/permissions.py b/app/common/permissions.py index a081ac09b..4aa979e8d 100644 --- a/app/common/permissions.py +++ b/app/common/permissions.py @@ -61,17 +61,16 @@ def check_has_access(groups_with_access, request): def set_user_id(request): - token = request.META.get("HTTP_X_CSRF_TOKEN") + # Import here to avoid circular import: This is why Python is a bad idea when writing APIs + from app.authentication.utils import get_user_from_request + request.id = None request.user = None - if token is None: - return None + user = get_user_from_request(request) - try: - user = Token.objects.get(key=token).user - except Token.DoesNotExist: - return + if user is None: + return None request.id = user.user_id request.user = user diff --git a/app/settings.py b/app/settings.py index 8a196ef09..4ba4f9a88 100644 --- a/app/settings.py +++ b/app/settings.py @@ -107,8 +107,8 @@ ] JWT_AUTH = { - 'JWT_PAYLOAD_GET_USERNAME_HANDLER': 'app.authentication.utils.jwt_get_username_from_payload_handler', - 'JWT_DECODE_HANDLER': 'app.authentication.utils.jwt_decode_token', + 'JWT_PAYLOAD_GET_USERNAME_HANDLER': 'app.authentication.utils.get_userid_from_decoded_jwt', + 'JWT_DECODE_HANDLER': 'app.authentication.utils.decode_jwt', 'JWT_ALGORITHM': 'RS256', 'JWT_AUDIENCE': 'https://dev-api.tihlde.org', 'JWT_ISSUER': 'https://tihlde-dev.eu.auth0.com/', From 9a854d071a12826ceb318f0d348cd57d051eed8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20R=C3=B8dahl=20Thingnes?= Date: Sun, 23 Apr 2023 22:42:37 +0200 Subject: [PATCH 4/8] Automatic retrieval of user programs --- app/authentication/auth0.py | 66 +++++++++++++++++++++++++++++++++++++ app/authentication/utils.py | 35 +++++++++++++------- app/constants.py | 2 ++ app/content/views/user.py | 1 + app/settings.py | 22 ++++++------- 5 files changed, 102 insertions(+), 24 deletions(-) create mode 100644 app/authentication/auth0.py diff --git a/app/authentication/auth0.py b/app/authentication/auth0.py new file mode 100644 index 000000000..416e340eb --- /dev/null +++ b/app/authentication/auth0.py @@ -0,0 +1,66 @@ +import time + +import requests + +from app.constants import AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_DOMAIN + + +def get_user_study_programs(user_id): + """ + Gets zipped study programs and start years from Auth0 given the user_id. + + Example: [('BIDATA', '2020')]. + """ + token_manager = ManagementTokenManager() + + response = requests.get( + f"https://{AUTH0_DOMAIN}/api/v2/users/{user_id}", + headers={"Authorization": f"Bearer {token_manager.get_token()}"}, + ).json() + + # Example format: ['fc:fs:fs:kull:ntnu.no:BIDATA:2020H'] + metadata = response["app_metadata"]["programs"] + + programs = [p.split(":")[5] for p in metadata] + years = [p.split(":")[6][:4] for p in metadata] + + return zip(programs, years) + + +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + + +class ManagementTokenManager(metaclass=Singleton): + """ + Singleton class for getting and refreshing Auth0 Management API tokens. + """ + + def __init__(self): + self.token, self.duration = self._get_management_token() + self.timestamp = time.time() + + def _get_management_token(self): + response = requests.post( + f"https://{AUTH0_DOMAIN}/oauth/token", + data={ + "grant_type": "client_credentials", + "client_id": AUTH0_CLIENT_ID, + "client_secret": AUTH0_CLIENT_SECRET, + "audience": f"https://{AUTH0_DOMAIN}/api/v2/", + }, + ).json() + + return response["access_token"], response["expires_in"] + + def get_token(self): + # Refresh token if expired, then return token. + if time.time() > self.timestamp + self.duration: + self.__init__() + + return self.token diff --git a/app/authentication/utils.py b/app/authentication/utils.py index 3b26fa047..c3f674d64 100644 --- a/app/authentication/utils.py +++ b/app/authentication/utils.py @@ -1,40 +1,51 @@ -import os import json -from app.constants import AUTH0_AUDIENCE, AUTH0_DOMAIN -from app.content.models.user import User + +from django.contrib.auth import authenticate + import jwt import requests -from django.contrib.auth import authenticate + +from app.constants import AUTH0_AUDIENCE, AUTH0_DOMAIN +from app.content.models.user import User -def get_jwt_from_request(request): +def get_jwt_from_request(request): auth = request.META.get("HTTP_AUTHORIZATION", None) parts = auth.split() token = parts[1] return token + def decode_jwt(token): header = jwt.get_unverified_header(token) - jwks = requests.get('https://{}/.well-known/jwks.json'.format(AUTH0_DOMAIN)).json() + jwks = requests.get(f"https://{AUTH0_DOMAIN}/.well-known/jwks.json").json() public_key = None - for jwk in jwks['keys']: - if jwk['kid'] == header['kid']: + for jwk in jwks["keys"]: + if jwk["kid"] == header["kid"]: public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk)) if public_key is None: - raise Exception('Public key not found while decoding token.') + raise Exception("Public key not found while decoding token.") + + return jwt.decode( + token, + public_key, + audience=AUTH0_AUDIENCE, + issuer=f"https://{AUTH0_DOMAIN}/", + algorithms=["RS256"], + ) - return jwt.decode(token, public_key, audience=AUTH0_AUDIENCE, issuer='https://{}/'.format(AUTH0_DOMAIN), algorithms=['RS256']) def get_userid_from_decoded_jwt(payload): - user_id = payload.get('sub').replace('|', '.') + user_id = payload.get("sub") authenticate(remote_user=user_id) return user_id + def get_user_from_request(request): user_id = get_userid_from_decoded_jwt(decode_jwt(get_jwt_from_request(request))) - return User.objects.get(user_id=user_id) \ No newline at end of file + return User.objects.get(user_id=user_id) diff --git a/app/constants.py b/app/constants.py index b690605f0..88e08c76e 100644 --- a/app/constants.py +++ b/app/constants.py @@ -2,6 +2,8 @@ AUTH0_DOMAIN = os.environ.get("AUTH0_DOMAIN") AUTH0_AUDIENCE = os.environ.get("AUTH0_AUDIENCE") +AUTH0_CLIENT_ID = os.environ.get("AUTH0_CLIENT_ID") +AUTH0_CLIENT_SECRET = os.environ.get("AUTH0_CLIENT_SECRET") MAIL_HS = "hs@tihlde.org" if os.environ.get("PROD") else "test+hs@tihlde.org" MAIL_ECONOMY = ( diff --git a/app/content/views/user.py b/app/content/views/user.py index 727c2197b..faaafe4ce 100644 --- a/app/content/views/user.py +++ b/app/content/views/user.py @@ -5,6 +5,7 @@ from rest_framework.decorators import action from rest_framework.response import Response +from app.authentication.auth0 import get_user_study_programs from app.badge.models import Badge, UserBadge from app.badge.serializers import BadgeSerializer, UserBadgeSerializer from app.common.enums import Groups, GroupType diff --git a/app/settings.py b/app/settings.py index 4ba4f9a88..f9685ff0f 100644 --- a/app/settings.py +++ b/app/settings.py @@ -87,9 +87,7 @@ # Django rest framework REST_FRAMEWORK = { "DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema", - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', - ), + "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_jwt.authentication.JSONWebTokenAuthentication", "rest_framework.authentication.SessionAuthentication", @@ -102,17 +100,17 @@ # Auth0 Authentication AUTH_USER_MODEL = "content.User" AUTHENTICATION_BACKENDS = [ - 'django.contrib.auth.backends.ModelBackend', - 'django.contrib.auth.backends.RemoteUserBackend', + "django.contrib.auth.backends.ModelBackend", + "django.contrib.auth.backends.RemoteUserBackend", ] JWT_AUTH = { - 'JWT_PAYLOAD_GET_USERNAME_HANDLER': 'app.authentication.utils.get_userid_from_decoded_jwt', - 'JWT_DECODE_HANDLER': 'app.authentication.utils.decode_jwt', - 'JWT_ALGORITHM': 'RS256', - 'JWT_AUDIENCE': 'https://dev-api.tihlde.org', - 'JWT_ISSUER': 'https://tihlde-dev.eu.auth0.com/', - 'JWT_AUTH_HEADER_PREFIX': 'Bearer', + "JWT_PAYLOAD_GET_USERNAME_HANDLER": "app.authentication.utils.get_userid_from_decoded_jwt", + "JWT_DECODE_HANDLER": "app.authentication.utils.decode_jwt", + "JWT_ALGORITHM": "RS256", + "JWT_AUDIENCE": "https://dev-api.tihlde.org", + "JWT_ISSUER": "https://tihlde-dev.eu.auth0.com/", + "JWT_AUTH_HEADER_PREFIX": "Bearer", } TEMPLATES = [ @@ -237,4 +235,4 @@ # If you wish to associate users to errors (assuming you are using # django.contrib.auth) you may enable sending PII data. send_default_pii=True, -) \ No newline at end of file +) From f1e435de86e85f821e63fff465e7075181dbbf79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20R=C3=B8dahl=20Thingnes?= Date: Mon, 24 Apr 2023 18:41:10 +0200 Subject: [PATCH 5/8] Remove authtoken from fixture --- app/fixture.json | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/app/fixture.json b/app/fixture.json index 4bc6bde80..f6917515f 100644 --- a/app/fixture.json +++ b/app/fixture.json @@ -143,30 +143,6 @@ "expire_date": "2021-04-05T15:36:54.314Z" } }, -{ - "model": "authtoken.token", - "pk": "606d9b0894c798fa511d85f876fd8611b7109e32", - "fields": { - "user": "index", - "created": "2020-10-01T18:22:57.720Z" - } -}, -{ - "model": "authtoken.token", - "pk": "64e443d5d4eb019b6055bece575d13e32553fb23", - "fields": { - "user": "eivindste", - "created": "2020-10-01T18:58:32.305Z" - } -}, -{ - "model": "authtoken.token", - "pk": "813e9d426d9cfe20b184492b417072aa533d29bc", - "fields": { - "user": "nok", - "created": "2021-03-04T16:59:43.225Z" - } -}, { "model": "communication.notification", "pk": 1, From c6b4894b60664752fd5974eed35f1a046df5f969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20R=C3=B8dahl=20Thingnes?= Date: Sun, 30 Apr 2023 20:34:14 +0200 Subject: [PATCH 6/8] Automatic registration of groups works --- app/authentication/apps.py | 8 ++++++ app/authentication/signals.py | 50 +++++++++++++++++++++++++++++++++++ app/authentication/utils.py | 4 +-- app/settings.py | 19 ++++++------- 4 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 app/authentication/apps.py create mode 100644 app/authentication/signals.py diff --git a/app/authentication/apps.py b/app/authentication/apps.py new file mode 100644 index 000000000..80ddfd134 --- /dev/null +++ b/app/authentication/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + +class AuthenticationConfig(AppConfig): + name = 'app.authentication' + + def ready(self): + # Add the set_user_groups signal receiver to user creation. + import app.authentication.signals \ No newline at end of file diff --git a/app/authentication/signals.py b/app/authentication/signals.py new file mode 100644 index 000000000..c6a6798f3 --- /dev/null +++ b/app/authentication/signals.py @@ -0,0 +1,50 @@ +from app.content.models.user import User +from django.db.models.signals import post_save +from django.dispatch import receiver + +from app.authentication.auth0 import get_user_study_programs +from app.common.enums import Groups +from app.group.models.group import Group +from app.group.models.membership import Membership + +@receiver(post_save, sender=User) +def set_user_groups(sender, instance: User, created, **kwargs): + # Only run this when the user if first created. + if not created: + return + + study_programs = get_user_study_programs(instance.user_id) # Return example: [('BIDATA', 2020), ...] + + # For every program reported by Feide, try to add user groups. + for program in study_programs: + program_slug = _get_program_group_slug(program) + program_year = _get_program_year(program) + + # Do not add any groups if program is not part of TIHLDE. + if not program_slug: + continue + + # Automatically activate account when program is verified. + TIHLDE = Group.objects.get(slug=Groups.TIHLDE) + Membership.objects.get_or_create(user=instance, group=TIHLDE) + + program_group = Group.objects.get(slug=program_slug) + Membership.objects.get_or_create(user=instance, group = program_group) + + year_group = Group.objects.get(slug=program_year) + Membership.objects.get_or_create(user=instance, group = year_group) + + +def _get_program_group_slug(program): + program_codes = ["BIDATA", "ITBAITBEDR", "BDIGSEC", "ITMAIKTSA", "ITBAINFODR", "ITBAINFO"] + program_slugs = ["dataingenir", "digital-forretningsutvikling", "digital-infrastruktur-og-cybersikkerhet", "digital-samhandling", "drift-studie", "informasjonsbehandling"] + + try: + index = program_codes.index(program[0]) + except: + return None + + return program_slugs[index] + +def _get_program_year(program): + return program[1] diff --git a/app/authentication/utils.py b/app/authentication/utils.py index c3f674d64..23fdd7cfe 100644 --- a/app/authentication/utils.py +++ b/app/authentication/utils.py @@ -38,7 +38,7 @@ def decode_jwt(token): ) -def get_userid_from_decoded_jwt(payload): +def authenticate_user_with_decoded_jwt(payload): user_id = payload.get("sub") authenticate(remote_user=user_id) @@ -46,6 +46,6 @@ def get_userid_from_decoded_jwt(payload): def get_user_from_request(request): - user_id = get_userid_from_decoded_jwt(decode_jwt(get_jwt_from_request(request))) + user_id = authenticate_user_with_decoded_jwt(decode_jwt(get_jwt_from_request(request))) return User.objects.get(user_id=user_id) diff --git a/app/settings.py b/app/settings.py index f9685ff0f..8808986d3 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,4 +1,5 @@ import os +from app.constants import AUTH0_AUDIENCE, AUTH0_DOMAIN import sentry_sdk from corsheaders.defaults import default_headers @@ -71,20 +72,22 @@ # Django Cors Headers "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", + # Base Middleware "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.auth.middleware.RemoteUserMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.locale.LocaleMiddleware", + + # Auth0 Middleware + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.auth.middleware.RemoteUserMiddleware", ] -# Django rest framework REST_FRAMEWORK = { "DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema", "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), @@ -100,16 +103,16 @@ # Auth0 Authentication AUTH_USER_MODEL = "content.User" AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.ModelBackend", "django.contrib.auth.backends.RemoteUserBackend", + "django.contrib.auth.backends.ModelBackend", ] JWT_AUTH = { - "JWT_PAYLOAD_GET_USERNAME_HANDLER": "app.authentication.utils.get_userid_from_decoded_jwt", + "JWT_PAYLOAD_GET_USERNAME_HANDLER": "app.authentication.utils.authenticate_user_with_decoded_jwt", "JWT_DECODE_HANDLER": "app.authentication.utils.decode_jwt", "JWT_ALGORITHM": "RS256", - "JWT_AUDIENCE": "https://dev-api.tihlde.org", - "JWT_ISSUER": "https://tihlde-dev.eu.auth0.com/", + "JWT_AUDIENCE": AUTH0_AUDIENCE, + "JWT_ISSUER": AUTH0_DOMAIN, "JWT_AUTH_HEADER_PREFIX": "Bearer", } @@ -135,7 +138,6 @@ # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases - DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", @@ -161,7 +163,6 @@ # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ - LANGUAGE_CODE = "nb-no" TIME_ZONE = "Europe/Oslo" USE_I18N = True From 29035168dfd52ba88bc1bdcdba3403375e5827a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20R=C3=B8dahl=20Thingnes?= Date: Sun, 30 Apr 2023 21:17:56 +0200 Subject: [PATCH 7/8] Automatically import name and feide ID --- app/authentication/auth0.py | 13 +++++++----- app/authentication/signals.py | 20 ++++++++++++++----- ...ncrease_userid_length.py => 0053_auth0.py} | 5 +++++ app/content/models/user.py | 1 + app/content/views/user.py | 2 +- 5 files changed, 30 insertions(+), 11 deletions(-) rename app/content/migrations/{0053_increase_userid_length.py => 0053_auth0.py} (72%) diff --git a/app/authentication/auth0.py b/app/authentication/auth0.py index 416e340eb..5c2f8f27c 100644 --- a/app/authentication/auth0.py +++ b/app/authentication/auth0.py @@ -3,13 +3,13 @@ import requests from app.constants import AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_DOMAIN + - -def get_user_study_programs(user_id): +def get_user_information(user_id): """ - Gets zipped study programs and start years from Auth0 given the user_id. + Gets zipped study programs + start years, name and Feide username from Auth0 given the user_id. - Example: [('BIDATA', '2020')]. + Example: "olanord", "Ola Nordmann", [('BIDATA', '2020')]. """ token_manager = ManagementTokenManager() @@ -24,7 +24,10 @@ def get_user_study_programs(user_id): programs = [p.split(":")[5] for p in metadata] years = [p.split(":")[6][:4] for p in metadata] - return zip(programs, years) + name = response["name"] + feide_username = response["nickname"] + + return feide_username, name, zip(programs, years) class Singleton(type): diff --git a/app/authentication/signals.py b/app/authentication/signals.py index c6a6798f3..0c4fdc2a3 100644 --- a/app/authentication/signals.py +++ b/app/authentication/signals.py @@ -2,18 +2,28 @@ from django.db.models.signals import post_save from django.dispatch import receiver -from app.authentication.auth0 import get_user_study_programs +from app.authentication.auth0 import get_user_information, get_user_information from app.common.enums import Groups from app.group.models.group import Group from app.group.models.membership import Membership @receiver(post_save, sender=User) -def set_user_groups(sender, instance: User, created, **kwargs): - # Only run this when the user if first created. - if not created: +def set_user_info_and_groups(sender, instance: User, created, **kwargs): + # Only run this when the user if first created and if not loading data + if kwargs.get("raw") or not created: return - study_programs = get_user_study_programs(instance.user_id) # Return example: [('BIDATA', 2020), ...] + feide_username, name, study_programs = get_user_information(instance.user_id) # Return example: [('BIDATA', 2020), ...] + + if feide_username: + instance.feide_id = feide_username + + if name: + instance.first_name = name.split()[0] + instance.last_name = " ".join(name.split()[1:]) + + if feide_username or name: + instance.save() # For every program reported by Feide, try to add user groups. for program in study_programs: diff --git a/app/content/migrations/0053_increase_userid_length.py b/app/content/migrations/0053_auth0.py similarity index 72% rename from app/content/migrations/0053_increase_userid_length.py rename to app/content/migrations/0053_auth0.py index 270fdfec1..36b3f2522 100644 --- a/app/content/migrations/0053_increase_userid_length.py +++ b/app/content/migrations/0053_auth0.py @@ -15,4 +15,9 @@ class Migration(migrations.Migration): name='user_id', field=models.CharField(max_length=255, primary_key=True, serialize=False), ), + migrations.AddField( + model_name='user', + name='feide_id', + field=models.CharField(max_length=64, null=True), + ), ] diff --git a/app/content/models/user.py b/app/content/models/user.py index dc74b2e13..22fa2387c 100644 --- a/app/content/models/user.py +++ b/app/content/models/user.py @@ -56,6 +56,7 @@ class User(AbstractBaseUser, PermissionsMixin, BaseModel, OptionalImage): read_access = [Groups.TIHLDE] user_id = models.CharField(max_length=255, primary_key=True) + feide_id = models.CharField(max_length=64, null=True) first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) diff --git a/app/content/views/user.py b/app/content/views/user.py index faaafe4ce..4778b2d6f 100644 --- a/app/content/views/user.py +++ b/app/content/views/user.py @@ -5,7 +5,7 @@ from rest_framework.decorators import action from rest_framework.response import Response -from app.authentication.auth0 import get_user_study_programs +from app.authentication.auth0 import get_user_information from app.badge.models import Badge, UserBadge from app.badge.serializers import BadgeSerializer, UserBadgeSerializer from app.common.enums import Groups, GroupType From 90f071e9d644a4f520e98c5cb7f3336d3996dc9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20R=C3=B8dahl=20Thingnes?= Date: Sun, 30 Apr 2023 21:27:00 +0200 Subject: [PATCH 8/8] Automatically add student email as default --- app/authentication/auth0.py | 11 ++++++----- app/authentication/signals.py | 7 +++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/authentication/auth0.py b/app/authentication/auth0.py index 5c2f8f27c..3e1919f8f 100644 --- a/app/authentication/auth0.py +++ b/app/authentication/auth0.py @@ -9,7 +9,7 @@ def get_user_information(user_id): """ Gets zipped study programs + start years, name and Feide username from Auth0 given the user_id. - Example: "olanord", "Ola Nordmann", [('BIDATA', '2020')]. + Example: "olanord", "Ola Nordmann", "olanord@stud.ntnu.no", [('BIDATA', '2020')]. """ token_manager = ManagementTokenManager() @@ -18,16 +18,17 @@ def get_user_information(user_id): headers={"Authorization": f"Bearer {token_manager.get_token()}"}, ).json() + feide_username = response["nickname"] + name = response["name"] + email = response["email"] + # Example format: ['fc:fs:fs:kull:ntnu.no:BIDATA:2020H'] metadata = response["app_metadata"]["programs"] programs = [p.split(":")[5] for p in metadata] years = [p.split(":")[6][:4] for p in metadata] - name = response["name"] - feide_username = response["nickname"] - - return feide_username, name, zip(programs, years) + return feide_username, name, email, zip(programs, years) class Singleton(type): diff --git a/app/authentication/signals.py b/app/authentication/signals.py index 0c4fdc2a3..106e4869b 100644 --- a/app/authentication/signals.py +++ b/app/authentication/signals.py @@ -13,7 +13,7 @@ def set_user_info_and_groups(sender, instance: User, created, **kwargs): if kwargs.get("raw") or not created: return - feide_username, name, study_programs = get_user_information(instance.user_id) # Return example: [('BIDATA', 2020), ...] + feide_username, name, email, study_programs = get_user_information(instance.user_id) # Return example: [('BIDATA', 2020), ...] if feide_username: instance.feide_id = feide_username @@ -22,7 +22,10 @@ def set_user_info_and_groups(sender, instance: User, created, **kwargs): instance.first_name = name.split()[0] instance.last_name = " ".join(name.split()[1:]) - if feide_username or name: + if email: + instance.email = email + + if feide_username or name or email: instance.save() # For every program reported by Feide, try to add user groups.