Compare commits

...

6 commits

Author SHA1 Message Date
b4c686bfaa test: Add auth tests 2021-10-21 10:46:26 +02:00
a887be1368 refactor: Adapt python and settings to memoorje 2021-10-21 10:29:37 +02:00
ceb032dddb fix: Make tox tests run 2021-10-21 10:04:15 +02:00
79b7bc8364 chore: Reformat files 2021-10-21 09:58:18 +02:00
96f4a81a3b chore: Adapt project env to memoorje 2021-10-21 09:43:01 +02:00
5206f95e3f Latest changes 2021-10-21 09:05:08 +02:00
27 changed files with 370 additions and 368 deletions

17
.editorconfig Normal file
View file

@ -0,0 +1,17 @@
# 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

46
.gitignore vendored
View file

@ -1,35 +1,27 @@
# 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__/ __pycache__/
/data/*
!/data/.gitkeep
/db.sqlite3 /db.sqlite3
/venv/ /media/
/dist/
*.egg-info/
/build/
/.pybuild/
/.tox/
.coverage*
/debian/*debhelper* /debian/*debhelper*
/debian/*.substvars /debian/*.substvars
/debian/files /debian/files
/debian/python3-userausfall/ /debian/python3-userausfall/
/debian/userausfall/ /debian/userausfall/
/debian/userausfall-webapp/ /debian/userausfall-webapp/
/.pybuild/
/build/ node_modules/
/userausfall.egg-info/ app/dist/
npm-debug.log*
.idea/
*.env*
*.swp

View file

@ -1,154 +0,0 @@
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")

13
pyproject.toml Normal file
View file

@ -0,0 +1,13 @@
[tool.black]
line-length = 120
target-version = ['py39']
exclude = '''
(
/(
\.eggs
| \.git
| \.tox
| migrations
)
)
'''

View file

@ -1,3 +0,0 @@
setuptools~=40.8.0
django~=2.2.13
djangorestframework~=3.9.0

7
setup.cfg Normal file
View file

@ -0,0 +1,7 @@
[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

View file

@ -1,19 +1,45 @@
from setuptools import setup, find_packages import os
from setuptools import find_namespace_packages, setup
from userausfall import __version__ 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( setup(
name="userausfall", name="userausfall",
version=__version__, version=__version__,
description="account management for systemausfall.org", description="account management for systemausfall.org",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://git.systemausfall.org/systemausfall.org/userausfall", url="https://git.systemausfall.org/systemausfall.org/userausfall",
author="Robert Waltemath", author="userausfall developers",
author_email="rw@roko.li", author_email="hallo@roko.li",
packages=find_packages(), license="AGPL-3.0-or-later",
install_requires=( packages=find_namespace_packages(include=["userausfall"]),
"django>=2.2<3.0", install_requires=[
# "djangorestframework>=3.12<4.0", "django~=3.2.8",
# "djoser>=2.1<3.0", "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",
],
include_package_data=True, 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",
],
) )

25
tox.ini Normal file
View file

@ -0,0 +1,25 @@
[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

View file

@ -1,3 +1 @@
__version__ = "0.1.0" __version__ = "0.1.0"
default_app_config = 'userausfall.apps.UserausfallConfig'

View file

@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from userausfall.models import User, TrustBridge from userausfall.models import TrustBridge, User
admin.site.register(TrustBridge) admin.site.register(TrustBridge)
admin.site.register(User) admin.site.register(User)

View file

@ -1,8 +0,0 @@
from django.apps import AppConfig
class UserausfallConfig(AppConfig):
name = 'userausfall'
def ready(self):
from . import signals

View file

@ -1,17 +0,0 @@
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()

View file

@ -1,7 +1,7 @@
from djeveric import Email from djeveric.emails import ConfirmationEmail
class ConfidantConfirmationEmail(Email): class ConfidantConfirmationEmail(ConfirmationEmail):
subject = "TODO" subject = "TODO"
def get_message(self, context): def get_message(self, context):

View file

@ -1,8 +1,36 @@
from django.conf import settings from django.conf import settings
from ldap3 import Server, Connection, SYNC from ldap3 import Connection, Server, SYNC
def create_account(username, raw_password): 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") 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 # 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). # soon as it is available (multithreading).
@ -13,9 +41,4 @@ def create_account(username, raw_password):
client_strategy=SYNC, client_strategy=SYNC,
auto_bind=True, auto_bind=True,
) )
is_success = connection.add( return connection
f"cn={username},dc=local",
["simpleSecurityObject", "organizationalRole"],
{"userPassword": raw_password},
)
return is_success

View file

@ -52,15 +52,13 @@ class UserManager(BaseUserManager):
return self._create_user(email, password, **extra_fields) return self._create_user(email, password, **extra_fields)
class User(AbstractBaseUser, PermissionsMixin): class User(PermissionsMixin, AbstractBaseUser):
username_validator = UnicodeUsernameValidator() username_validator = UnicodeUsernameValidator()
username = models.CharField( username = models.CharField(
_("username"), _("username"),
max_length=150, max_length=150,
help_text=_( help_text=_("Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."),
"Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
),
validators=[username_validator], validators=[username_validator],
error_messages={"unique": _("A user with that username already exists.")}, error_messages={"unique": _("A user with that username already exists.")},
unique=True, unique=True,
@ -96,9 +94,7 @@ class User(AbstractBaseUser, PermissionsMixin):
if not self.username: if not self.username:
raise MissingUserAttribute("User is missing a username.") raise MissingUserAttribute("User is missing a username.")
if not self.check_password(raw_password): if not self.check_password(raw_password):
raise PasswordMismatch( raise PasswordMismatch("The given password does not match the user's password.")
"The given password does not match the user's password."
)
return ldap.create_account(self.username, raw_password) return ldap.create_account(self.username, raw_password)

View file

@ -1,19 +0,0 @@
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

View file

@ -1,6 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from userausfall.models import User, TrustBridge from userausfall.models import TrustBridge, User
class TrustBridgeSerializer(serializers.ModelSerializer): class TrustBridgeSerializer(serializers.ModelSerializer):

View file

@ -0,0 +1 @@
from .auth import * # noqa: F401, F403

View file

@ -0,0 +1,68 @@
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)

View file

@ -0,0 +1,13 @@
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)}"

View file

@ -1,13 +1,13 @@
from django.urls import path from django.urls import include, path
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from rest_framework import routers from rest_framework import routers
from userausfall.rest_api.views import UserViewSet router = routers.SimpleRouter()
router = routers.DefaultRouter(trailing_slash=True)
router.register(r'users', UserViewSet, basename="user")
urlpatterns = [ urlpatterns = [
# path("confirm/confidant/", ConfidantConfirmationView.as_view()) 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"),
] ]
urlpatterns += router.urls

View file

@ -1,26 +1,11 @@
from rest_framework import viewsets, status from rest_framework import status, viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from userausfall.models import User, MissingUserAttribute, PasswordMismatch from userausfall.models import MissingUserAttribute, PasswordMismatch, User
from userausfall.rest_api.permissions import UserPermission
from userausfall.rest_api.serializers import (
ActivateUserSerializer,
CreateUserSerializer,
RetrieveUserSerializer,
)
class UserViewSet(viewsets.ModelViewSet): class UserViewSet(viewsets.GenericViewSet):
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"]) @action(detail=True, methods=["post"])
def activate(self, request, pk=None): def activate(self, request, pk=None):
"""Create the corresponding LDAP account.""" """Create the corresponding LDAP account."""
@ -38,11 +23,3 @@ class UserViewSet(viewsets.ModelViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
else: else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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

View file

@ -2,24 +2,25 @@
Django settings for userausfall. Django settings for userausfall.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/2.2/topics/settings/ https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.2/ref/settings/ https://docs.djangoproject.com/en/3.2/ref/settings/
""" """
import os import os
from pathlib import Path
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = Path(__file__).resolve().parent.parent
DATA_DIR = os.environ.get("USERAUSFALL_DATA_DIR", BASE_DIR) DATA_DIR = Path(os.environ.get("USERAUSFALL_DATA_DIR", BASE_DIR))
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('LOCAL_DJANGO_SECRET_KEY') SECRET_KEY = "django-insecure-ngy2599=i5c*5(*bw%gbs&jzb(^p-4zk&6!a8a76tevv$tb9xq"
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
@ -30,80 +31,78 @@ ALLOWED_HOSTS = []
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', "django.contrib.admin",
'django.contrib.auth', "django.contrib.auth",
'django.contrib.contenttypes', "django.contrib.contenttypes",
'django.contrib.sessions', "django.contrib.sessions",
'django.contrib.messages', "django.contrib.messages",
'django.contrib.sites', "django.contrib.sites",
'django.contrib.staticfiles', "django.contrib.staticfiles",
'userausfall', "rest_framework",
'rest_framework', "rest_registration",
"django_filters",
"drf_spectacular",
"userausfall",
"userausfall.rest_api",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', "django.middleware.security.SecurityMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
ROOT_URLCONF = 'userausfall.urls' ROOT_URLCONF = "userausfall.urls"
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'DIRS': [], "DIRS": [],
'APP_DIRS': True, "APP_DIRS": True,
'OPTIONS': { "OPTIONS": {
'context_processors': [ "context_processors": [
'django.template.context_processors.debug', "django.template.context_processors.debug",
'django.template.context_processors.request', "django.template.context_processors.request",
'django.contrib.auth.context_processors.auth', "django.contrib.auth.context_processors.auth",
'django.contrib.messages.context_processors.messages', "django.contrib.messages.context_processors.messages",
], ],
}, },
}, },
] ]
WSGI_APPLICATION = 'userausfall.wsgi.application' WSGI_APPLICATION = "userausfall.wsgi.application"
# Database # Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases # https://docs.djangoproject.com/en/2.2/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { "default": {
'ENGINE': 'django.db.backends.sqlite3', "ENGINE": "django.db.backends.sqlite3",
'NAME': os.path.join(DATA_DIR, 'db.sqlite3'), "NAME": DATA_DIR / "db.sqlite3",
} }
} }
# User model
#
AUTH_USER_MODEL = 'userausfall.User'
# Password validation # Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
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",
}, },
] ]
@ -111,9 +110,9 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/ # 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 USE_I18N = True
@ -125,30 +124,69 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/ # https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = "/static/"
# Media files # Media files
# https://docs.djangoproject.com/en/2.2/topics/files/ # https://docs.djangoproject.com/en/2.2/topics/files/
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_ROOT = DATA_DIR / "media"
MEDIA_URL = '/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"
# Sending email # Sending email
# https://docs.djangoproject.com/en/3.2/topics/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 # Django Rest Framework
# https://www.django-rest-framework.org/api-guide/settings/ # https://www.django-rest-framework.org/api-guide/settings/
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ( "DEFAULT_FILTER_BACKENDS": [
'rest_framework.authentication.SessionAuthentication', "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"],
} }
@ -159,6 +197,6 @@ SITE_ID = 1
USERAUSFALL_LDAP = { USERAUSFALL_LDAP = {
'ADMIN_USER_DN': 'cn=admin,dc=local', "ADMIN_USER_DN": "cn=admin,dc=local",
'ADMIN_USER_PASSWORD': os.environ.get('USERAUSFALL_LDAP_PASSWORD'), "ADMIN_USER_PASSWORD": os.environ.get("USERAUSFALL_LDAP_PASSWORD"),
} }

View file

@ -1,11 +0,0 @@
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

23
userausfall/tests.py Normal file
View file

@ -0,0 +1,23 @@
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)

View file

@ -1,10 +1,7 @@
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import include, path
from userausfall.rest_api import urls as rest_api_urls
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path("admin/", admin.site.urls),
path('api/', include("userausfall.rest_api.urls")), path("api/", include("userausfall.rest_api.urls")),
path("api-auth/", include("rest_framework.urls")),
] ]

View file

@ -1,16 +1,16 @@
""" """
WSGI config for userausfall project. WSGI config for memoorje project.
It exposes the WSGI callable as a module-level variable named ``application``. It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
""" """
import os import os
from django.core.wsgi import get_wsgi_application 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() application = get_wsgi_application()