From e2aa645bbf477d1e304d4553093f5a67985fc905 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 4 Nov 2021 11:29:03 +0100 Subject: [PATCH] Start implementation of OAuth client --- med/settings.py | 15 +++++++ requirements.txt | 1 + theme/templates/admin/base_site.html | 2 +- users/migrations/0043_accesstoken.py | 31 ++++++++++++++ users/models.py | 62 ++++++++++++++++++++++++++++ users/urls.py | 3 +- users/views.py | 59 ++++++++++++-------------- 7 files changed, 139 insertions(+), 34 deletions(-) create mode 100644 users/migrations/0043_accesstoken.py diff --git a/med/settings.py b/med/settings.py index be9456a..2b89e58 100644 --- a/med/settings.py +++ b/med/settings.py @@ -169,6 +169,21 @@ AUTH_USER_MODEL = 'users.User' MAX_EMPRUNT = 5 # Max emprunts +# AUTHLIB CLIENTS +AUTHLIB_OAUTH_CLIENTS = { + 'notekfet': { + 'client_id': 'qtElmOUj67YNvSZjA5l70ITUMxd3NJ9kksBsK5e9', + 'client_secret': 'SwF909sLIeU5GhruXsFzKfdBhFNgs8nvkVpFKgP4pIQ80BmLLlf3ZkMoNL7Cpox6Ke3MXNWGswTtbKkM8AiB9v6pys8PNfYH0MDFWAi3tnffjwaMQBzRFhjx20qb6S4W', + 'access_token_url': 'https://note-dev.crans.org/o/token/', + 'refresh_token_url': 'https://note-dev.crans.org/o/token/', + 'authorize_url': 'https://note-dev.crans.org/o/authorize/', + 'userinfo_endpoint': 'https://note-dev.crans.org/api/me/', + 'client_kwargs': { + 'scope': '1_1 2_1 48_1', + } + } +} + try: from .settings_local import * except ImportError: diff --git a/requirements.txt b/requirements.txt index 7f2237e..19a6e17 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +authlib~=0.15 docutils~=0.16 # for Django-admin docs Django~=2.2 django-filter~=2.4 diff --git a/theme/templates/admin/base_site.html b/theme/templates/admin/base_site.html index 4adb2d8..5a4d054 100644 --- a/theme/templates/admin/base_site.html +++ b/theme/templates/admin/base_site.html @@ -54,7 +54,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% if user.is_authenticated %} {% trans 'Log out' %} {% else %} - {% trans 'Log in' %} + {% trans 'Log in' %} {% endif %} {% endblock %} diff --git a/users/migrations/0043_accesstoken.py b/users/migrations/0043_accesstoken.py new file mode 100644 index 0000000..709235c --- /dev/null +++ b/users/migrations/0043_accesstoken.py @@ -0,0 +1,31 @@ +# Generated by Django 2.2.24 on 2021-11-02 15:11 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0042_delete_adhesion'), + ] + + operations = [ + migrations.CreateModel( + name='AccessToken', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('access_token', models.CharField(max_length=32, verbose_name='access token')), + ('expires_in', models.PositiveSmallIntegerField(verbose_name='expires in')), + ('scopes', models.CharField(max_length=255, verbose_name='scopes')), + ('refresh_token', models.CharField(max_length=32, verbose_name='refresh token')), + ('expires_at', models.DateTimeField(verbose_name='expires at')), + ('owner', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='owner')), + ], + options={ + 'verbose_name': 'access token', + 'verbose_name_plural': 'access tokens', + }, + ), + ] diff --git a/users/models.py b/users/models.py index 3b5cf51..91203ea 100644 --- a/users/models.py +++ b/users/models.py @@ -2,6 +2,10 @@ # Copyright (C) 2017-2019 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from datetime import datetime + +from authlib.integrations.django_client import OAuth +from django.conf import settings from django.contrib.auth.models import AbstractUser from django.db import models from django.utils import timezone @@ -44,3 +48,61 @@ class User(AbstractUser): def is_member(self): # FIXME Use NK20 return True + + +class AccessToken(models.Model): + owner = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + null=True, + default=None, + verbose_name=_('owner'), + ) + + access_token = models.CharField( + max_length=32, + verbose_name=_('access token'), + ) + + expires_in = models.PositiveSmallIntegerField( + verbose_name=_('expires in'), + ) + + scopes = models.CharField( + max_length=255, + verbose_name=_('scopes'), + ) + + refresh_token = models.CharField( + max_length=32, + verbose_name=_('refresh token'), + ) + + expires_at = models.DateTimeField( + verbose_name=_('expires at'), + ) + + def refresh(self): + """ + Refresh the access token. + """ + oauth = OAuth() + oauth.register('notekfet') + # Get the OAuth client + oauth_client = oauth.notekfet._get_oauth_client() + # Actually refresh the token + token = oauth_client.refresh_token(oauth.notekfet.access_token_url, + refresh_token=self.refresh_token) + self.access_token = token['access_token'] + self.expires_in = token['expires_in'] + self.scopes = token['scope'] + self.refresh_token = token['refresh_token'] + self.expires_at = timezone.utc.fromutc( + datetime.fromtimestamp(token['expires_at']) + ) + + self.save() + + class Meta: + verbose_name = _('access token') + verbose_name_plural = _('access tokens') diff --git a/users/urls.py b/users/urls.py index 457e218..54fb5f1 100644 --- a/users/urls.py +++ b/users/urls.py @@ -8,5 +8,6 @@ from . import views app_name = 'users' urlpatterns = [ - url(r'^edit_info/$', views.edit_info, name='edit-info'), + url('login/', views.LoginView.as_view(), name='login'), + url('authorize/', views.AuthorizeView.as_view(), name='auth'), ] diff --git a/users/views.py b/users/views.py index 0f5b17a..2a4e7ec 100644 --- a/users/views.py +++ b/users/views.py @@ -1,47 +1,42 @@ # -*- mode: python; coding: utf-8 -*- # Copyright (C) 2017-2019 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from datetime import datetime -from django.contrib import messages -from django.contrib.auth.decorators import login_required +from authlib.integrations.django_client import OAuth from django.contrib.auth.models import Group -from django.db import transaction -from django.shortcuts import redirect, render -from django.template.context_processors import csrf -from django.utils.translation import ugettext_lazy as _ +from django.urls import reverse +from django.utils import timezone +from django.views.generic import RedirectView from rest_framework import viewsets -from reversion import revisions as reversion -from users.forms import BaseInfoForm -from users.models import User +from users.models import User, AccessToken from .serializers import GroupSerializer, UserSerializer -def form(ctx, template, request): - c = ctx - c.update(csrf(request)) - return render(request, template, c) +class LoginView(RedirectView): + def get_redirect_url(self, *args, **kwargs): + oauth = OAuth() + oauth.register('notekfet') + redirect_url = self.request.build_absolute_uri(reverse('users:auth')) + return oauth.notekfet.authorize_redirect(self.request, redirect_url).url -@login_required -def edit_info(request): - """ - Edite son utilisateur - """ - user = BaseInfoForm(request.POST or None, instance=request.user) - if user.is_valid(): - with transaction.atomic(), reversion.create_revision(): - user.save() - reversion.set_user(request.user) - reversion.set_comment("Champs modifié(s) : %s" % ', '.join( - field for field in user.changed_data)) - messages.success(request, "L'user a bien été modifié") - return redirect("index") - return form({ - 'form': user, - 'password_change': True, - 'title': _('Edit user profile'), - }, 'users/user.html', request) +class AuthorizeView(RedirectView): + def get_redirect_url(self, *args, **kwargs): + oauth = OAuth() + oauth.register('notekfet') + token = oauth.notekfet.authorize_access_token(self.request) + token_obj = AccessToken.objects.create( + access_token=token['access_token'], + expires_in=token['expires_in'], + scopes=token['scope'], + refresh_token=token['refresh_token'], + expires_at=timezone.utc.fromutc( + datetime.fromtimestamp(token['expires_at'])), + ) + # TODO Log in or create user + return '/' class UserViewSet(viewsets.ModelViewSet):