diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index bb5e2ed..0000000 --- a/.editorconfig +++ /dev/null @@ -1,17 +0,0 @@ -# editorconfig.org -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.py] -indent_size = 4 - -[{Makefile,debian/rules,make.d/*}] -indent_style = tab -indent_size = 4 diff --git a/.gitignore b/.gitignore index d0b2d8d..aaf09c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,35 @@ +# local env files +.env +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.DS_Store +node_modules +/dist __pycache__/ - -/data/* -!/data/.gitkeep /db.sqlite3 -/media/ - -/dist/ -*.egg-info/ -/build/ -/.pybuild/ -/.tox/ -.coverage* +/venv/ /debian/*debhelper* /debian/*.substvars /debian/files /debian/python3-userausfall/ /debian/userausfall/ /debian/userausfall-webapp/ - -node_modules/ -app/dist/ -npm-debug.log* - -.idea/ -*.env* -*.swp +/.pybuild/ +/build/ +/userausfall.egg-info/ diff --git a/djeveric/__init__.py b/djeveric/__init__.py new file mode 100644 index 0000000..5c0c9d3 --- /dev/null +++ b/djeveric/__init__.py @@ -0,0 +1,154 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.tokens import default_token_generator +from django.contrib.contenttypes.models import ContentType +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from rest_framework import serializers, status +from rest_framework.exceptions import ValidationError +from rest_framework.response import Response +from rest_framework.settings import api_settings +from rest_framework.views import APIView + + +def encode_pk(resource): + """Encode the primary key of a resource with Base64 for usage in URLs.""" + return urlsafe_base64_encode(force_bytes(resource.pk)) + + +class ConfirmationError(Exception): + """An error occurring while checking a confirmation.""" + + def __init__(self, message=None, errors=None): + if errors is None: + self.errors = {} + else: + self.errors = errors + if message is not None: + non_field_errors = self.errors.get(api_settings.NON_FIELD_ERRORS_KEY, []) + non_field_errors.append(message) + self.errors[api_settings.NON_FIELD_ERRORS_KEY] = non_field_errors + + +class Email: + """ + Base class for an email message. + """ + subject = "" + + def __init__(self, user): + self.user = user + + def get_subject(self): + return self.subject + + def get_message(self, context): + raise NotImplementedError() + + def send(self, context): + self.user.email_user(self.get_subject(), self.get_message(context)) + + +class Confirmation: + """ + Base class for handling a confirmation process. + """ + email_class = Email + + def __init__(self, user, resource): + self.user = user + self.resource = resource + + def check(self): + if not self.has_permission(self.user, self.resource): + raise ConfirmationError("Permission denied") + if self.is_confirmed(self.resource): + raise ConfirmationError("Already confirmed") + self.confirm(self.resource) + + def confirm(self, resource): + """Overwrite this method to supply operations to confirm the resource.""" + pass + + def get_email(self): + return self.email_class(self.user) + + def has_permission(self, user, resource) -> bool: + """Overwrite this method returning if a user may confirm a resource.""" + return False + + def is_confirmed(self, resource) -> bool: + """Overwrite this method to tell if a resource is confirmed.""" + return False + + def send_request(self): + if self.is_confirmed(self.resource): + return + self.get_email().send({ + "token": default_token_generator.make_token(self.user), + "uid": encode_pk(self.user), + "rtid": encode_pk(ContentType.objects.get_for_model(self.resource)), + "rid": encode_pk(self.resource), + }) + + +class ConfirmationSerializer(serializers.Serializer): + """ + Serializer class for confirmation requests. + """ + token = serializers.CharField() + uid = serializers.CharField() + rtid = serializers.CharField() + rid = serializers.CharField() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.user = None + self.resource_type = None + self.resource = None + + def validate_uid(self, value): + self.user = self.get_model_object(get_user_model(), value) + return value + + def validate_rtid(self, value): + self.resource_type = self.get_model_object(ContentType, value) + return value + + def validate(self, data): + # we need to be sure that the rtid was already decoded + self.resource = self.get_model_object(self.resource_type.model_class(), data["rid"]) + return data + + def get_model_object(self, model, oid): + try: + # urlsafe_base64_decode() decodes to bytestring + oid = urlsafe_base64_decode(oid).decode() + return model._default_manager.get(id=oid) + except (TypeError, ValueError, OverflowError, model.DoesNotExist): + raise ValidationError("Error while decoding object id") + + +class ConfirmationView(APIView): + """ + View for creating a confirmation API endpoint. + """ + confirmation_class = Confirmation + serializer_class = ConfirmationSerializer + + def post(self, request, format=None): + try: + self.check_confirmation(request.data) + return Response({}, status=status.HTTP_204_NO_CONTENT) + except ConfirmationError as e: + return Response(e.errors, status=status.HTTP_400_BAD_REQUEST) + + def check_confirmation(self, data): + serializer = self.serializer_class(data=data) + if not serializer.is_valid(): + raise ConfirmationError(errors=serializer.errors) + token = serializer.validated_data["token"] + if default_token_generator.check_token(serializer.user, token): + # confirm resource or raise an exception otherwise + self.confirmation_class(serializer.user, serializer.resource).check() + else: + raise ConfirmationError("Invalid token") diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 9b7a2f3..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,13 +0,0 @@ -[tool.black] -line-length = 120 -target-version = ['py39'] -exclude = ''' -( - /( - \.eggs - | \.git - | \.tox - | migrations - ) -) -''' diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..906b98f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +setuptools~=40.8.0 +django~=2.2.13 +djangorestframework~=3.9.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ac4ff4e..0000000 --- a/setup.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[flake8] -max-line-length = 120 -select = C,E,F,I,W,B,B950 -ignore = E203, E501, W503 -exclude = .tox, node_modules, src, **/migrations/*.py -import-order-style = google -application-import-names = userausfall diff --git a/setup.py b/setup.py index 2929038..0a4e9fc 100644 --- a/setup.py +++ b/setup.py @@ -1,45 +1,19 @@ -import os - -from setuptools import find_namespace_packages, setup +from setuptools import setup, find_packages from userausfall import __version__ -__dir__ = os.path.abspath(os.path.dirname(__file__)) - -try: - with open(os.path.join(__dir__, "README.md")) as f: - long_description = "\n" + f.read() -except FileNotFoundError: - long_description = "" - setup( name="userausfall", version=__version__, description="account management for systemausfall.org", - long_description=long_description, - long_description_content_type="text/markdown", url="https://git.systemausfall.org/systemausfall.org/userausfall", - author="userausfall developers", - author_email="hallo@roko.li", - license="AGPL-3.0-or-later", - packages=find_namespace_packages(include=["userausfall"]), - install_requires=[ - "django~=3.2.8", - "djangorestframework~=3.12.1", - "djangorestframework-camel-case~=1.2.0", - "django-filter~=2.4.0", - "django-rest-registration~=0.6.4", - "djeveric@https://git.hack-hro.de/memoorje/djeveric/-/archive/main/djeveric-main.tar.gz", - "drf-spectacular~=0.18.2", - "ldap3~=2.8.1", - ], + author="Robert Waltemath", + author_email="rw@roko.li", + packages=find_packages(), + install_requires=( + "django>=2.2<3.0", + # "djangorestframework>=3.12<4.0", + # "djoser>=2.1<3.0", + ), include_package_data=True, - classifiers=[ - "Development Status :: 3 - Alpha", - "Framework :: Django :: 3.2", - "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", - "Programming Language :: Python", - "Programming Language :: Python :: 3 :: Only", - "Topic :: Internet :: WWW/HTTP", - ], ) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index b0582a8..0000000 --- a/tox.ini +++ /dev/null @@ -1,25 +0,0 @@ -[tox] -envlist = lint, test-py3-app -skip_missing_interpreters = true -minversion = 3.21.0 - -[testenv:lint] -# no need to install package with deps to lint sources -skip_install = true -deps = - black - flake8 - flake8-import-order -setenv = - FORMAT_PATHS = userausfall{/} setup.py -commands = - python3 -m flake8 {env:FORMAT_PATHS} - python3 -m black --check {env:FORMAT_PATHS} - -[testenv:test-py3-app] -sitepackages = true -deps = - coverage -setenv = DJANGO_SETTINGS_MODULE=userausfall.settings -commands = - python3 -m coverage run --append --source='userausfall' -m django test --verbosity=2 diff --git a/userausfall/__init__.py b/userausfall/__init__.py index 3dc1f76..8257966 100644 --- a/userausfall/__init__.py +++ b/userausfall/__init__.py @@ -1 +1,3 @@ __version__ = "0.1.0" + +default_app_config = 'userausfall.apps.UserausfallConfig' diff --git a/userausfall/admin.py b/userausfall/admin.py index 43eab90..47505e8 100644 --- a/userausfall/admin.py +++ b/userausfall/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from userausfall.models import TrustBridge, User +from userausfall.models import User, TrustBridge admin.site.register(TrustBridge) admin.site.register(User) diff --git a/userausfall/apps.py b/userausfall/apps.py new file mode 100644 index 0000000..5c928df --- /dev/null +++ b/userausfall/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class UserausfallConfig(AppConfig): + name = 'userausfall' + + def ready(self): + from . import signals diff --git a/userausfall/confirmations.py b/userausfall/confirmations.py new file mode 100644 index 0000000..33614e0 --- /dev/null +++ b/userausfall/confirmations.py @@ -0,0 +1,17 @@ +from djeveric import Confirmation +from userausfall.emails import ConfidantConfirmationEmail +from userausfall.models import User + + +class ConfidantConfirmation(Confirmation): + email_class = ConfidantConfirmationEmail + + def has_permission(self, user: User, resource: User): + return user == resource.confidant_unconfirmed + + def is_confirmed(self, resource: User): + return resource.confidant_unconfirmed == resource.confidant + + def confirm(self, resource: User): + resource.confidant = resource.confidant_unconfirmed + resource.save() diff --git a/userausfall/emails.py b/userausfall/emails.py index 45efdd2..2d4d632 100644 --- a/userausfall/emails.py +++ b/userausfall/emails.py @@ -1,7 +1,7 @@ -from djeveric.emails import ConfirmationEmail +from djeveric import Email -class ConfidantConfirmationEmail(ConfirmationEmail): +class ConfidantConfirmationEmail(Email): subject = "TODO" def get_message(self, context): diff --git a/userausfall/ldap.py b/userausfall/ldap.py index 7e2e0b9..e1ff065 100644 --- a/userausfall/ldap.py +++ b/userausfall/ldap.py @@ -1,36 +1,8 @@ from django.conf import settings -from ldap3 import Connection, Server, SYNC +from ldap3 import Server, Connection, SYNC def create_account(username, raw_password): - connection = _get_connection() - is_success = connection.add( - f"cn={username},dc=local", - ["simpleSecurityObject", "organizationalRole"], - {"userPassword": raw_password}, - ) - return is_success - - -def account_exists(username): - connection = _get_connection() - exists = connection.search(f"cn={username},dc=local", "(objectclass=simpleSecurityObject)") - return exists - - -def is_valid_account_data(username, raw_password): - connection = _get_connection() - is_valid = connection.search( - f"cn={username},dc=local", - "(objectclass=simpleSecurityObject)", - attributes=["userPassword"], - ) - if is_valid: - is_valid = connection.entries[0]["userPassword"].value == raw_password - return is_valid - - -def _get_connection(): server = Server("localhost") # The SAFE_SYNC client strategy doesn't seem to be present in Buster version of ldap3. We might want to use it as # soon as it is available (multithreading). @@ -41,4 +13,9 @@ def _get_connection(): client_strategy=SYNC, auto_bind=True, ) - return connection + is_success = connection.add( + f"cn={username},dc=local", + ["simpleSecurityObject", "organizationalRole"], + {"userPassword": raw_password}, + ) + return is_success diff --git a/userausfall/models.py b/userausfall/models.py index 7331b19..158e8ba 100644 --- a/userausfall/models.py +++ b/userausfall/models.py @@ -52,13 +52,15 @@ class UserManager(BaseUserManager): return self._create_user(email, password, **extra_fields) -class User(PermissionsMixin, AbstractBaseUser): +class User(AbstractBaseUser, PermissionsMixin): username_validator = UnicodeUsernameValidator() username = models.CharField( _("username"), max_length=150, - help_text=_("Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."), + help_text=_( + "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only." + ), validators=[username_validator], error_messages={"unique": _("A user with that username already exists.")}, unique=True, @@ -94,7 +96,9 @@ class User(PermissionsMixin, AbstractBaseUser): if not self.username: raise MissingUserAttribute("User is missing a username.") if not self.check_password(raw_password): - raise PasswordMismatch("The given password does not match the user's password.") + raise PasswordMismatch( + "The given password does not match the user's password." + ) return ldap.create_account(self.username, raw_password) diff --git a/userausfall/rest_api/permissions.py b/userausfall/rest_api/permissions.py new file mode 100644 index 0000000..0291279 --- /dev/null +++ b/userausfall/rest_api/permissions.py @@ -0,0 +1,19 @@ +from rest_framework import permissions + + +class UserPermission(permissions.BasePermission): + def has_permission(self, request, view): + result = False + if view.action == "activate": + result = True + elif view.action == "create": + result = True + elif view.action == "retrieve_authenticated": + result = request.user.is_authenticated + return result + + def has_object_permission(self, request, view, obj): + result = False + if view.action == "activate": + result = request.user == obj + return result diff --git a/userausfall/rest_api/serializers.py b/userausfall/rest_api/serializers.py index a9a6479..4127b16 100644 --- a/userausfall/rest_api/serializers.py +++ b/userausfall/rest_api/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from userausfall.models import TrustBridge, User +from userausfall.models import User, TrustBridge class TrustBridgeSerializer(serializers.ModelSerializer): diff --git a/userausfall/rest_api/tests/__init__.py b/userausfall/rest_api/tests/__init__.py deleted file mode 100644 index 78ced3d..0000000 --- a/userausfall/rest_api/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .auth import * # noqa: F401, F403 diff --git a/userausfall/rest_api/tests/auth.py b/userausfall/rest_api/tests/auth.py deleted file mode 100644 index 48fb079..0000000 --- a/userausfall/rest_api/tests/auth.py +++ /dev/null @@ -1,68 +0,0 @@ -from rest_framework import status - -from userausfall.models import User -from userausfall.rest_api.tests.userausfall import UserausfallAPITestCase -from userausfall.tests import UserMixin - - -class UserTestCase(UserMixin, UserausfallAPITestCase): - base_url = "/api/auth" - - def test_signup(self): - """ - Create a new user account (signup) - """ - url = "/register/" - username = "test" - password = "test12345" - data = {"username": username, "password": password, "passwordConfirm": password} - response = self.client.post(self.get_api_url(url), data) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(User.objects.count(), 1) - self.assertEqual(User.objects.get().username, username) - - def test_login(self) -> None: - """ - Create a session cookie (login) - """ - url = "/login/" - self.create_user() - data = {"login": self.username, "password": self.password} - response = self.client.post(self.get_api_url(url), data) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.wsgi_request.user.is_authenticated) - - def test_logout(self) -> None: - """ - Remove the session (logout) - """ - url = "/logout/" - self.authenticate_user() - response = self.client.post(self.get_api_url(url)) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertFalse(response.wsgi_request.user.is_authenticated) - - def test_retrieve_user(self) -> None: - """ - Retrieve data of authenticated user - """ - url = "/profile/" - self.create_user() - self.authenticate_user() - response = self.client.get(self.get_api_url(url)) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertDictEqual( - response.data, - { - "username": self.username, - "id": self.user.id, - }, - ) - - def test_create_two_users(self) -> None: - """ - Create more than one user - """ - self.create_user() - self.create_user() - self.assertEqual(User.objects.count(), 2) diff --git a/userausfall/rest_api/tests/userausfall.py b/userausfall/rest_api/tests/userausfall.py deleted file mode 100644 index 30fd6ea..0000000 --- a/userausfall/rest_api/tests/userausfall.py +++ /dev/null @@ -1,13 +0,0 @@ -from rest_framework.test import APITestCase - - -class UserausfallAPITestCase(APITestCase): - base_url = "/api" - - def get_api_url(self, url, **kwargs): - """ - Prepend the path to the full url for this test class. - :param url: an url fragment - :return: the url fragment prepended with the base url - """ - return f"{self.base_url}{url.format(**kwargs)}" diff --git a/userausfall/rest_api/urls.py b/userausfall/rest_api/urls.py index a3a4ed8..76ba0ab 100644 --- a/userausfall/rest_api/urls.py +++ b/userausfall/rest_api/urls.py @@ -1,13 +1,13 @@ -from django.urls import include, path -from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView +from django.urls import path from rest_framework import routers -router = routers.SimpleRouter() +from userausfall.rest_api.views import UserViewSet + +router = routers.DefaultRouter(trailing_slash=True) +router.register(r'users', UserViewSet, basename="user") urlpatterns = [ - path("", include(router.urls)), - path("auth/", include("rest_registration.api.urls")), - path("schema/", SpectacularAPIView.as_view(), name="schema"), - path("schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), - path("schema/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), + # path("confirm/confidant/", ConfidantConfirmationView.as_view()) ] + +urlpatterns += router.urls diff --git a/userausfall/rest_api/views.py b/userausfall/rest_api/views.py index b64d9eb..577cbe8 100644 --- a/userausfall/rest_api/views.py +++ b/userausfall/rest_api/views.py @@ -1,11 +1,26 @@ -from rest_framework import status, viewsets +from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response -from userausfall.models import MissingUserAttribute, PasswordMismatch, User +from userausfall.models import User, MissingUserAttribute, PasswordMismatch +from userausfall.rest_api.permissions import UserPermission +from userausfall.rest_api.serializers import ( + ActivateUserSerializer, + CreateUserSerializer, + RetrieveUserSerializer, +) -class UserViewSet(viewsets.GenericViewSet): +class UserViewSet(viewsets.ModelViewSet): + permission_classes = [UserPermission] + queryset = User.objects.all() + + @action(detail=False, url_path="me") + def retrieve_authenticated(self, request): + """Retrieve user data for logged in user.""" + serializer = self.get_serializer(request.user) + return Response(serializer.data) + @action(detail=True, methods=["post"]) def activate(self, request, pk=None): """Create the corresponding LDAP account.""" @@ -23,3 +38,11 @@ class UserViewSet(viewsets.GenericViewSet): return Response(status=status.HTTP_204_NO_CONTENT) else: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def get_serializer_class(self): + if self.action == "activate": + return ActivateUserSerializer + elif self.action == "create": + return CreateUserSerializer + elif self.action == "retrieve_authenticated": + return RetrieveUserSerializer diff --git a/userausfall/settings.py b/userausfall/settings.py index 5fb82af..0c053fa 100644 --- a/userausfall/settings.py +++ b/userausfall/settings.py @@ -2,25 +2,24 @@ Django settings for userausfall. For more information on this file, see -https://docs.djangoproject.com/en/3.2/topics/settings/ +https://docs.djangoproject.com/en/2.2/topics/settings/ For the full list of settings and their values, see -https://docs.djangoproject.com/en/3.2/ref/settings/ +https://docs.djangoproject.com/en/2.2/ref/settings/ """ import os -from pathlib import Path -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent -DATA_DIR = Path(os.environ.get("USERAUSFALL_DATA_DIR", BASE_DIR)) +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DATA_DIR = os.environ.get("USERAUSFALL_DATA_DIR", BASE_DIR) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-ngy2599=i5c*5(*bw%gbs&jzb(^p-4zk&6!a8a76tevv$tb9xq" +SECRET_KEY = os.environ.get('LOCAL_DJANGO_SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -31,78 +30,80 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.sites", - "django.contrib.staticfiles", - "rest_framework", - "rest_registration", - "django_filters", - "drf_spectacular", - "userausfall", - "userausfall.rest_api", + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.sites', + 'django.contrib.staticfiles', + 'userausfall', + 'rest_framework', ] MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] -ROOT_URLCONF = "userausfall.urls" +ROOT_URLCONF = 'userausfall.urls' TEMPLATES = [ { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', ], }, }, ] -WSGI_APPLICATION = "userausfall.wsgi.application" +WSGI_APPLICATION = 'userausfall.wsgi.application' # Database # https://docs.djangoproject.com/en/2.2/ref/settings/#databases DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": DATA_DIR / "db.sqlite3", + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(DATA_DIR, 'db.sqlite3'), } } +# User model +# + +AUTH_USER_MODEL = 'userausfall.User' + + # Password validation # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] @@ -110,9 +111,9 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ -LANGUAGE_CODE = "de-de" +LANGUAGE_CODE = 'de-de' -TIME_ZONE = "UTC" +TIME_ZONE = 'UTC' USE_I18N = True @@ -124,69 +125,30 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ -STATIC_URL = "/static/" +STATIC_URL = '/static/' # Media files # https://docs.djangoproject.com/en/2.2/topics/files/ -MEDIA_ROOT = DATA_DIR / "media" +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') -MEDIA_URL = "/media/" - - -# Default primary key field type -# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" - - -# User model -# - -AUTH_USER_MODEL = "userausfall.User" +MEDIA_URL = '/media/' # Sending email # https://docs.djangoproject.com/en/3.2/topics/email/ -EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # Django Rest Framework # https://www.django-rest-framework.org/api-guide/settings/ REST_FRAMEWORK = { - "DEFAULT_FILTER_BACKENDS": [ - "django_filters.rest_framework.DjangoFilterBackend", - ], - "DEFAULT_PARSER_CLASSES": [ - "djangorestframework_camel_case.parser.CamelCaseFormParser", - "djangorestframework_camel_case.parser.CamelCaseMultiPartParser", - "djangorestframework_camel_case.parser.CamelCaseJSONParser", - ], - "DEFAULT_PERMISSION_CLASSES": [ - "rest_framework.permissions.IsAuthenticated", - ], - "DEFAULT_RENDERER_CLASSES": [ - "djangorestframework_camel_case.render.CamelCaseJSONRenderer", - "djangorestframework_camel_case.render.CamelCaseBrowsableAPIRenderer", - ], - "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", - "TEST_REQUEST_DEFAULT_FORMAT": "json", -} - -REST_REGISTRATION = { - "REGISTER_VERIFICATION_ENABLED": False, - "REGISTER_EMAIL_VERIFICATION_ENABLED": False, - "RESET_PASSWORD_VERIFICATION_ENABLED": False, -} - -SPECTACULAR_SETTINGS = { - "TITLE": "Userausfall API", - "DESCRIPTION": "Account management for systemausfall.org", - "VERSION": "0.0.1", - "POSTPROCESSING_HOOKS": ["drf_spectacular.contrib.djangorestframework_camel_case.camelize_serializer_fields"], + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.SessionAuthentication', + ), } @@ -197,6 +159,6 @@ SITE_ID = 1 USERAUSFALL_LDAP = { - "ADMIN_USER_DN": "cn=admin,dc=local", - "ADMIN_USER_PASSWORD": os.environ.get("USERAUSFALL_LDAP_PASSWORD"), + 'ADMIN_USER_DN': 'cn=admin,dc=local', + 'ADMIN_USER_PASSWORD': os.environ.get('USERAUSFALL_LDAP_PASSWORD'), } diff --git a/userausfall/signals.py b/userausfall/signals.py new file mode 100644 index 0000000..d3f1789 --- /dev/null +++ b/userausfall/signals.py @@ -0,0 +1,11 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver + +from userausfall.models import User + + +@receiver(post_save, sender=User) +def user_saved(sender, instance: User, **kwargs): + # if instance.confidant_unconfirmed is not None: + # ConfidantConfirmation(instance.confidant_unconfirmed, instance).send_request() + pass diff --git a/userausfall/tests.py b/userausfall/tests.py deleted file mode 100644 index ceb2f8b..0000000 --- a/userausfall/tests.py +++ /dev/null @@ -1,23 +0,0 @@ -from userausfall.models import User - - -class UserMixin: - user: User - password: str - username: str - - def create_user(self): - self.username = f"test{User.objects.count()}" - self.password = "test12345" - self.user = User.objects.create_user(self.username, self.password) - - def ensure_user_exists(self): - if not hasattr(self, "user"): - self.create_user() - - def authenticate_user(self): - self.ensure_user_exists() - if hasattr(self.client, "force_authentication"): - self.client.force_authenticate(user=self.user) - else: - self.client.force_login(user=self.user) diff --git a/userausfall/urls.py b/userausfall/urls.py index 575221f..64efbbd 100644 --- a/userausfall/urls.py +++ b/userausfall/urls.py @@ -1,7 +1,10 @@ from django.contrib import admin -from django.urls import include, path +from django.urls import path, include + +from userausfall.rest_api import urls as rest_api_urls urlpatterns = [ - path("admin/", admin.site.urls), - path("api/", include("userausfall.rest_api.urls")), + path('admin/', admin.site.urls), + path('api/', include("userausfall.rest_api.urls")), + path("api-auth/", include("rest_framework.urls")), ] diff --git a/userausfall/wsgi.py b/userausfall/wsgi.py index d128640..2d6fc6f 100644 --- a/userausfall/wsgi.py +++ b/userausfall/wsgi.py @@ -1,16 +1,16 @@ """ -WSGI config for memoorje project. +WSGI config for userausfall project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see -https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ +https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "userausfall.settings") +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'userausfall.settings') application = get_wsgi_application()