From 3930e89ad48cd1e5e7efd6c6e51729a09a01a865 Mon Sep 17 00:00:00 2001 From: Hironsan Date: Wed, 26 Oct 2022 15:12:26 +0900 Subject: [PATCH] Add user creation api --- backend/config/settings/base.py | 4 + backend/poetry.lock | 135 ++++++++++++++++++++++++++++-- backend/pyproject.toml | 2 +- backend/users/tests/test_views.py | 19 +++++ backend/users/tests/utils.py | 4 +- backend/users/urls.py | 3 +- backend/users/views.py | 21 ++++- 7 files changed, 176 insertions(+), 12 deletions(-) diff --git a/backend/config/settings/base.py b/backend/config/settings/base.py index 863292ac..d3b7d78e 100644 --- a/backend/config/settings/base.py +++ b/backend/config/settings/base.py @@ -56,7 +56,11 @@ INSTALLED_APPS = [ "polymorphic", "corsheaders", "drf_yasg", + "allauth", + "allauth.account", + "allauth.socialaccount", "dj_rest_auth", + "dj_rest_auth.registration", "django_celery_results", "django_drf_filepond", "health_check", diff --git a/backend/poetry.lock b/backend/poetry.lock index ccbe4476..0d2be01d 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -186,6 +186,17 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + [[package]] name = "chardet" version = "4.0.0" @@ -298,6 +309,25 @@ python-versions = ">=3.7" [package.extras] toml = ["tomli"] +[[package]] +name = "cryptography" +version = "38.0.2" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools-rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + [[package]] name = "defusedxml" version = "0.7.1" @@ -316,7 +346,7 @@ python-versions = "*" [[package]] name = "dj-rest-auth" -version = "2.2.3" +version = "2.2.5" description = "Authentication and Registration in Django Rest Framework" category = "main" optional = false @@ -324,10 +354,11 @@ python-versions = ">=3.5" [package.dependencies] Django = ">=2.0" +django-allauth = {version = ">=0.40.0,<0.51.0", optional = true, markers = "extra == \"with_social\""} djangorestframework = ">=3.7.0" [package.extras] -with_social = ["django-allauth (>=0.40.0,<0.48.0)"] +with_social = ["django-allauth (>=0.40.0,<0.51.0)"] [[package]] name = "django" @@ -347,6 +378,21 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-allauth" +version = "0.50.0" +description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Django = ">=2.0" +pyjwt = {version = ">=1.7", extras = ["crypto"]} +python3-openid = ">=3.0.8" +requests = "*" +requests-oauthlib = ">=0.3.0" + [[package]] name = "django-celery-results" version = "2.4.0" @@ -930,6 +976,19 @@ category = "main" optional = false python-versions = ">=3.8" +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + [[package]] name = "openpyxl" version = "3.0.9" @@ -1072,6 +1131,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "pydantic" version = "1.9.0" @@ -1142,6 +1209,23 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pyjwt" +version = "2.6.0" +description = "JSON Web Token implementation in Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.4.0)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "pre-commit"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"] + [[package]] name = "pyparsing" version = "3.0.7" @@ -1187,6 +1271,21 @@ python-versions = ">=3.5" [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "python3-openid" +version = "3.2.0" +description = "OpenID support for modern servers and consumers." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +defusedxml = "*" + +[package.extras] +mysql = ["mysql-connector-python"] +postgresql = ["psycopg2"] + [[package]] name = "pytz" version = "2021.3" @@ -1213,6 +1312,21 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +[[package]] +name = "requests-oauthlib" +version = "1.3.1" +description = "OAuthlib authentication support for Requests." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + [[package]] name = "rsa" version = "4.8" @@ -1554,7 +1668,7 @@ postgresql = [] [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "f56b900c4334045e2ce4e2dd4f57262e9ea236435e89c07c9ab53c4559050cf6" +content-hash = "a93579bf687bac1c8f4132105b0beb395c14e4151d40f81cd9bdbd490faf2c2c" [metadata.files] amqp = [ @@ -1637,6 +1751,7 @@ certifi = [ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, ] +cffi = [] chardet = [ {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, @@ -1716,6 +1831,7 @@ coverage = [ {file = "coverage-6.3.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2"}, {file = "coverage-6.3.1.tar.gz", hash = "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8"}, ] +cryptography = [] defusedxml = [ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, @@ -1724,10 +1840,9 @@ dj-database-url = [ {file = "dj-database-url-0.5.0.tar.gz", hash = "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163"}, {file = "dj_database_url-0.5.0-py2.py3-none-any.whl", hash = "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9"}, ] -dj-rest-auth = [ - {file = "dj-rest-auth-2.2.3.tar.gz", hash = "sha256:f292f3dffb8fc896da10adf47e94a254fb8bf42e672a066e63566363d764ad42"}, -] +dj-rest-auth = [] django = [] +django-allauth = [] django-celery-results = [] django-cleanup = [ {file = "django-cleanup-6.0.0.tar.gz", hash = "sha256:922e06ef8839c92bd3ab37a84db6058b8764f3fe44dbb4487bbca941d288280a"}, @@ -2084,6 +2199,7 @@ numpy = [ {file = "numpy-1.22.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a176959b6e7e00b5a0d6f549a479f869829bfd8150282c590deee6d099bbb6e"}, {file = "numpy-1.22.2.zip", hash = "sha256:076aee5a3763d41da6bef9565fdf3cb987606f567cd8b104aded2b38b7b47abf"}, ] +oauthlib = [] openpyxl = [ {file = "openpyxl-3.0.9-py2.py3-none-any.whl", hash = "sha256:8f3b11bd896a95468a4ab162fc4fcd260d46157155d1f8bfaabb99d88cfcf79f"}, {file = "openpyxl-3.0.9.tar.gz", hash = "sha256:40f568b9829bf9e446acfffce30250ac1fa39035124d55fc024025c41481c90f"}, @@ -2204,6 +2320,10 @@ pycodestyle = [ {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, ] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] pydantic = [ {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"}, {file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"}, @@ -2257,6 +2377,7 @@ pyflakes = [ {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, ] +pyjwt = [] pyparsing = [ {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, @@ -2273,6 +2394,7 @@ python-dotenv = [ {file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"}, {file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"}, ] +python3-openid = [] pytz = [ {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, @@ -2281,6 +2403,7 @@ requests = [ {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, ] +requests-oauthlib = [] rsa = [ {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 9f4828a4..6b4c4221 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -52,7 +52,7 @@ dj-database-url = "^0.5.0" pyexcel-xlsx = "^0.6.0" gunicorn = "^20.1.0" auto-labeling-pipeline = "^0.1.21" -dj-rest-auth = "^2.2.3" +dj-rest-auth = {extras = ["with_social"], version = "^2.2.5"} django-drf-filepond = "^0.4.1" celery = "^5.2.3" django-celery-results = "2.4.0" diff --git a/backend/users/tests/test_views.py b/backend/users/tests/test_views.py index 13f534f6..dea51170 100644 --- a/backend/users/tests/test_views.py +++ b/backend/users/tests/test_views.py @@ -38,3 +38,22 @@ class TestMeAPI(APITestCase): def test_does_not_return_information_to_unauthenticated_user(self): response = self.client.get(self.url) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class TestUserCreationAPI(APITestCase): + @classmethod + def setUpTestData(cls): + cls.staff = make_user(username="bob", is_staff=True) + cls.non_staff = make_user(username="tom", is_staff=False) + cls.url = reverse(viewname="user_create") + cls.payload = {"username": "hironsan", "password1": "foobarbaz", "password2": "foobarbaz"} + + def test_staff_can_create_user(self): + self.client.force_login(self.staff) + response = self.client.post(self.url, data=self.payload) + self.assertEqual(response.data["username"], "hironsan") + + def test_non_staff_cannot_create_user(self): + self.client.force_login(self.non_staff) + response = self.client.post(self.url, data=self.payload) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/backend/users/tests/utils.py b/backend/users/tests/utils.py index f3492efb..dffabbb1 100644 --- a/backend/users/tests/utils.py +++ b/backend/users/tests/utils.py @@ -1,7 +1,7 @@ from django.contrib.auth import get_user_model -def make_user(username: str = "bob"): +def make_user(username: str = "bob", is_staff: bool = False): user_model = get_user_model() - user, _ = user_model.objects.get_or_create(username=username, password="pass") + user, _ = user_model.objects.get_or_create(username=username, password="pass", is_staff=is_staff) return user diff --git a/backend/users/urls.py b/backend/users/urls.py index cadbc1da..47b04439 100644 --- a/backend/users/urls.py +++ b/backend/users/urls.py @@ -1,9 +1,10 @@ from django.urls import include, path -from .views import Me, Users +from .views import Me, UserCreation, Users urlpatterns = [ path(route="me", view=Me.as_view(), name="me"), path(route="users", view=Users.as_view(), name="user_list"), + path(route="users/create", view=UserCreation.as_view(), name="user_create"), path("auth/", include("dj_rest_auth.urls")), ] diff --git a/backend/users/views.py b/backend/users/views.py index c21ddc54..e8fd8ee4 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -1,7 +1,8 @@ +from dj_rest_auth.registration.serializers import RegisterSerializer from django.contrib.auth.models import User from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters, generics -from rest_framework.permissions import IsAuthenticated +from rest_framework import filters, generics, status +from rest_framework.permissions import IsAdminUser, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView @@ -24,3 +25,19 @@ class Users(generics.ListAPIView): pagination_class = None filter_backends = (DjangoFilterBackend, filters.SearchFilter) search_fields = ("username",) + + +class UserCreation(generics.CreateAPIView): + serializer_class = RegisterSerializer + permission_classes = [IsAuthenticated & IsAdminUser] + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(UserSerializer(user).data, status=status.HTTP_201_CREATED, headers=headers) + + def perform_create(self, serializer): + user = serializer.save(self.request) + return user