From da4dfca985b3fed3047cc908a21ec251a537c9cd Mon Sep 17 00:00:00 2001 From: Omer Cohen Date: Thu, 20 Oct 2022 19:00:32 +0400 Subject: [PATCH] feat: set staff and superuser from roles (#18) Update is_staff and is_superuser based based on roles --- README.md | 25 +++++++++++++++---------- django_descope/middleware.py | 18 ++++++++++++++---- django_descope/settings.py | 4 ++++ django_descope/views.py | 30 ++++++++++++++++++------------ example/poetry.lock | 27 ++++++++++++++------------- example/settings.py | 2 +- poetry.lock | 16 ++++++++-------- pyproject.toml | 2 +- requirements.txt | 6 +++--- 9 files changed, 78 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index d9ce18e..0ebda42 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,16 @@ This plugin integrates Descope with your Django app. ## Quick start +1. Sign up for Descope and set admin roles + +- Get your project id +- Create two roles in Descope, that will be mapped to Django permissions + - is_staff + - is_superuser + +Map these roles to any user you would like to make a staff or superuser in your Django app. +_The names of these roles can be customized in the settings below._ + 1. Add "django_descope" to your INSTALLED_APPS setting like this: ``` @@ -28,12 +38,12 @@ This plugin integrates Descope with your Django app. 1. Include descope URLconf in your project urls.py like this: ``` - path('descope/', include('django_descope.urls')), + path('auth/', include('django_descope.urls')), ``` -1. Start the development server and visit http://127.0.0.1:8000/descope/signup +1. Start the development server and visit http://127.0.0.1:8000/auth/signup -1. Visit http://127.0.0.1:8000/tokens to see the user tokens after login +1. Visit http://127.0.0.1:8000/auth/tokens to see the user tokens after login ## Settings @@ -48,11 +58,6 @@ DESCOPE_LOGIN_TEMPLATE_NAME DESCOPE_LOGIN_SENT_TEMPLATE_NAME DESCOPE_LOGIN_FAILED_TEMPLATE_NAME DESCOPE_SIGNUP_TEMPLATE_NAME +DESCOPE_IS_STAFF_ROLE +DESCOPE_IS_SUPERUSER_ROLE ``` - -### TODO: - -- [ ] Get user details (name?) from jwt -- [ ] Get user permissions from claims -- [ ] Add additional authentication methods -- [ ] Use descope web sdk for templates diff --git a/django_descope/middleware.py b/django_descope/middleware.py index b6ab0e9..da53131 100644 --- a/django_descope/middleware.py +++ b/django_descope/middleware.py @@ -29,21 +29,31 @@ def middleware(request: HttpRequest) -> HttpResponse: Returns: HttpResponse: Django HTTP Response """ - refresh_token: dict = request.session.get("descopeRefresh") - session_token: dict = request.session.get("descopeSession") + r: dict = request.session.get("descopeRefresh") + s: dict = request.session.get("descopeSession") - if not (refresh_token and session_token): + if not (r and s): logout(request) return get_response(request) try: jwt_response: dict = descope_client.validate_session_request( - session_token.get("jwt"), refresh_token.get("jwt") + s.get("jwt"), r.get("jwt") ) request.session["descopeSession"] = jwt_response[SESSION_TOKEN_NAME] except AuthException as e: logger.error(e) logout(request) + # Update roles + is_staff = settings.IS_STAFF_ROLE in s["roles"] + is_superuser = settings.IS_SUPERUSER_ROLE in s["roles"] + if request.user.is_staff != is_staff: + request.user.is_staff = is_staff + request.user.save(update_fields=["is_staff"]) + if request.user.is_superuser != is_superuser: + request.user.is_superuser = is_superuser + request.user.save(update_fields=["is_superuser"]) + return get_response(request) return middleware diff --git a/django_descope/settings.py b/django_descope/settings.py index b43a4ab..7ebdb45 100644 --- a/django_descope/settings.py +++ b/django_descope/settings.py @@ -32,3 +32,7 @@ REQUIRE_SIGNUP = getattr(settings, "DESCOPE_REQUIRE_SIGNUP", True) if not isinstance(REQUIRE_SIGNUP, bool): raise ImproperlyConfigured('"DESCOPE_REQUIRE_SIGNUP" must be a boolean') + +# Role names to create in Descope that will map to User attributes +IS_STAFF_ROLE = getattr(settings, "DESCOPE_IS_STAFF_ROLE", "is_staff") +IS_SUPERUSER_ROLE = getattr(settings, "DESCOPE_IS_SUPERUSER_ROLE", "is_superuser") diff --git a/django_descope/views.py b/django_descope/views.py index 8d9e917..cfcda30 100644 --- a/django_descope/views.py +++ b/django_descope/views.py @@ -46,9 +46,6 @@ def post(self, request: HttpRequest, *args, **kwargs): return self.render_to_response(context) email = form.cleaned_data["email"] - if not settings.REQUIRE_SIGNUP: - User.objects.get_or_create(username=email, email=email) - try: descope_client.magiclink.sign_in( DeliveryMethod.EMAIL, @@ -82,12 +79,21 @@ def get(self, request, *args, **kwargs): return self.render_to_response(context) logger.info("Login successful", jwt_response) - request.session["descopeSession"] = jwt_response[SESSION_TOKEN_NAME] + request.session["descopeUser"] = u = jwt_response["user"] + request.session["descopeSession"] = s = jwt_response[SESSION_TOKEN_NAME] request.session["descopeRefresh"] = jwt_response[REFRESH_SESSION_TOKEN_NAME] - user = User.objects.get(email=jwt_response["user"]["email"]) + + user, created = User.objects.get_or_create( + username=u["userId"], + email=u["email"], + is_staff=("is_staff" in s["roles"]), + first_name=u["name"].split()[0], + last_name=" ".join(u["name"].split()[0:]), + ) + login(request, user) - return HttpResponseRedirect(settings.LOGIN_SUCCESS_REDIRECT) + return HttpResponseRedirect(reverse(settings.LOGIN_SUCCESS_REDIRECT)) @method_decorator(csrf_protect, name="dispatch") @@ -110,11 +116,6 @@ def post(self, request, *args, **kwargs): return self.render_to_response(context) email = form.cleaned_data["email"] - - user, created = User.objects.get_or_create( - email=email, - username=email, - ) try: descope_client.magiclink.sign_up_or_in( DeliveryMethod.EMAIL, @@ -145,8 +146,13 @@ class ShowTokens(View): def get(self, request: HttpRequest, *args, **kwargs): return JsonResponse( { - "user": str(request.user), + "user": { + "user": str(request.user), + "is_staff": request.user.is_staff, + "is_superuser": request.user.is_superuser, + }, "session": request.session.get("descopeSession"), "refresh": request.session.get("descopeRefresh"), + "login": request.session.get("descopeUser"), } ) diff --git a/example/poetry.lock b/example/poetry.lock index 1554e9b..cc23da4 100644 --- a/example/poetry.lock +++ b/example/poetry.lock @@ -63,7 +63,7 @@ optional = false python-versions = ">=3.6.0" [package.extras] -unicode-backport = ["unicodedata2"] +unicode_backport = ["unicodedata2"] [[package]] name = "click" @@ -106,20 +106,20 @@ test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0 [[package]] name = "descope" -version = "0.1.0" +version = "0.2.0" description = "Descope Python SDK" category = "main" optional = false python-versions = ">=3.7,<4.0" [package.dependencies] -cryptography = ">=38.0.1,<39.0.0" -email-validator = ">=1.2.1,<2.0.0" -PyJWT = ">=2.4.0,<3.0.0" -requests = ">=2.28.1,<3.0.0" +cryptography = "38.0.1" +email-validator = "1.3.0" +PyJWT = "2.5.0" +requests = "2.28.1" [[package]] -name = "django" +name = "Django" version = "3.2.16" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" @@ -145,7 +145,8 @@ python-versions = ">=3.7,<4.0" develop = true [package.dependencies] -descope = "0.1.0" +descope = "0.2.0" +Django = ">=3.2,<5" [package.source] type = "directory" @@ -278,7 +279,7 @@ urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "sqlparse" @@ -340,7 +341,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = ">=3.7,<4.0" -content-hash = "65be46b250079dbe706dee10625a006ee3ef984011033c45afcf1d524dcbe8a5" +content-hash = "ed73862cb869873e0b7eea774cb30524bcc6425001cb5808c5ebbdf49a008987" [metadata.files] asgiref = [ @@ -483,10 +484,10 @@ cryptography = [ {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"}, ] descope = [ - {file = "descope-0.1.0-py3-none-any.whl", hash = "sha256:d20be12b9ee6aa8ac0a1d01c61b32903350c513f903e616591597a3aa0127fae"}, - {file = "descope-0.1.0.tar.gz", hash = "sha256:b00b2d108c0b90725a41752157083ff76b74ccbe7f2c27e3758d0219a32a6d0f"}, + {file = "descope-0.2.0-py3-none-any.whl", hash = "sha256:b8230a8a284582d72183d650274aa02e8116ba12008aee365b464b8a23b2c287"}, + {file = "descope-0.2.0.tar.gz", hash = "sha256:0e4980f9ee73d50771b11dd03661a5ecba3fd253002950fed340670e8f2be712"}, ] -django = [ +Django = [ {file = "Django-3.2.16-py3-none-any.whl", hash = "sha256:18ba8efa36b69cfcd4b670d0fa187c6fe7506596f0ababe580e16909bcdec121"}, {file = "Django-3.2.16.tar.gz", hash = "sha256:3adc285124244724a394fa9b9839cc8cd116faf7d159554c43ecdaa8cdf0b94d"}, ] diff --git a/example/settings.py b/example/settings.py index 7bdf601..44c7fdc 100644 --- a/example/settings.py +++ b/example/settings.py @@ -28,7 +28,7 @@ ALLOWED_HOSTS = ["*"] -DESCOPE_PROJECT_ID = "P2CqCdq2bnO9JS2awFKlIPngwPUK" # <-- Set this to your project ID +DESCOPE_PROJECT_ID = "P2GMsgxPSSQrq3Ig7M0ExAwoRGbP" # <-- Set this to your project ID # Application definition diff --git a/poetry.lock b/poetry.lock index 4e0afe4..8dbc24b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -126,17 +126,17 @@ test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0 [[package]] name = "descope" -version = "0.1.0" +version = "0.2.0" description = "Descope Python SDK" category = "main" optional = false python-versions = ">=3.7,<4.0" [package.dependencies] -cryptography = ">=38.0.1,<39.0.0" -email-validator = ">=1.2.1,<2.0.0" -PyJWT = ">=2.4.0,<3.0.0" -requests = ">=2.28.1,<3.0.0" +cryptography = "38.0.1" +email-validator = "1.3.0" +PyJWT = "2.5.0" +requests = "2.28.1" [[package]] name = "distlib" @@ -526,7 +526,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = ">=3.7,<4.0" -content-hash = "58fd582b7dfad76f8bc8d17de125a95c9d76efd6ff7bf6f934bf925408022f13" +content-hash = "7bd7b1dcdfe994023bb1b0508f182ec98e62a33f9428505927d7abfe4d428c3c" [metadata.files] asgiref = [ @@ -677,8 +677,8 @@ cryptography = [ {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"}, ] descope = [ - {file = "descope-0.1.0-py3-none-any.whl", hash = "sha256:d20be12b9ee6aa8ac0a1d01c61b32903350c513f903e616591597a3aa0127fae"}, - {file = "descope-0.1.0.tar.gz", hash = "sha256:b00b2d108c0b90725a41752157083ff76b74ccbe7f2c27e3758d0219a32a6d0f"}, + {file = "descope-0.2.0-py3-none-any.whl", hash = "sha256:b8230a8a284582d72183d650274aa02e8116ba12008aee365b464b8a23b2c287"}, + {file = "descope-0.2.0.tar.gz", hash = "sha256:0e4980f9ee73d50771b11dd03661a5ecba3fd253002950fed340670e8f2be712"}, ] distlib = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, diff --git a/pyproject.toml b/pyproject.toml index 94e5e79..413fba0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,8 +40,8 @@ classifiers = [ [tool.poetry.dependencies] python = ">=3.7,<4.0" -descope = "0.1.0" Django = ">=3.2,<5" +descope = "0.2.0" [tool.poetry.group.dev.dependencies] flake8 = "5.0.4" diff --git a/requirements.txt b/requirements.txt index 324f70b..99bc583 100644 --- a/requirements.txt +++ b/requirements.txt @@ -99,9 +99,9 @@ cryptography==38.0.1 ; python_version >= "3.7" and python_version < "4.0" \ --hash=sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9 \ --hash=sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd \ --hash=sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818 -descope==0.1.0 ; python_version >= "3.7" and python_version < "4.0" \ - --hash=sha256:b00b2d108c0b90725a41752157083ff76b74ccbe7f2c27e3758d0219a32a6d0f \ - --hash=sha256:d20be12b9ee6aa8ac0a1d01c61b32903350c513f903e616591597a3aa0127fae +descope==0.2.0 ; python_version >= "3.7" and python_version < "4.0" \ + --hash=sha256:0e4980f9ee73d50771b11dd03661a5ecba3fd253002950fed340670e8f2be712 \ + --hash=sha256:b8230a8a284582d72183d650274aa02e8116ba12008aee365b464b8a23b2c287 django==3.2.16 ; python_version >= "3.7" and python_version < "4.0" \ --hash=sha256:18ba8efa36b69cfcd4b670d0fa187c6fe7506596f0ababe580e16909bcdec121 \ --hash=sha256:3adc285124244724a394fa9b9839cc8cd116faf7d159554c43ecdaa8cdf0b94d