mirror of
https://gitlab.crans.org/bde/nk20
synced 2024-11-26 18:37:12 +00:00
Merge branch 'cas' into 'beta'
CAS + OAuth2 See merge request bde/nk20!155
This commit is contained in:
commit
116451603c
@ -4,10 +4,14 @@
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from django.utils import timezone
|
||||
from rest_framework import serializers
|
||||
from member.api.serializers import ProfileSerializer, MembershipSerializer
|
||||
from note.api.serializers import NoteSerializer
|
||||
from note.models import Alias
|
||||
|
||||
|
||||
class UserSerializer(ModelSerializer):
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Users.
|
||||
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
|
||||
@ -22,7 +26,7 @@ class UserSerializer(ModelSerializer):
|
||||
)
|
||||
|
||||
|
||||
class ContentTypeSerializer(ModelSerializer):
|
||||
class ContentTypeSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Users.
|
||||
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
|
||||
@ -31,3 +35,42 @@ class ContentTypeSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = ContentType
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class OAuthSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Informations that are transmitted by OAuth.
|
||||
For now, this includes user, profile and valid memberships.
|
||||
This should be better managed later.
|
||||
"""
|
||||
normalized_name = serializers.SerializerMethodField()
|
||||
|
||||
profile = ProfileSerializer()
|
||||
|
||||
note = NoteSerializer()
|
||||
|
||||
memberships = serializers.SerializerMethodField()
|
||||
|
||||
def get_normalized_name(self, obj):
|
||||
return Alias.normalize(obj.username)
|
||||
|
||||
def get_memberships(self, obj):
|
||||
return serializers.ListSerializer(child=MembershipSerializer()).to_representation(
|
||||
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now()))
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
'id',
|
||||
'username',
|
||||
'normalized_name',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'email',
|
||||
'is_superuser',
|
||||
'is_active',
|
||||
'is_staff',
|
||||
'profile',
|
||||
'note',
|
||||
'memberships',
|
||||
)
|
||||
|
@ -5,6 +5,7 @@ from django.conf import settings
|
||||
from django.conf.urls import url, include
|
||||
from rest_framework import routers
|
||||
|
||||
from .views import UserInformationView
|
||||
from .viewsets import ContentTypeViewSet, UserViewSet
|
||||
|
||||
# Routers provide an easy way of automatically determining the URL conf.
|
||||
@ -47,5 +48,6 @@ app_name = 'api'
|
||||
# Additionally, we include login URLs for the browsable API.
|
||||
urlpatterns = [
|
||||
url('^', include(router.urls)),
|
||||
url('^me/', UserInformationView.as_view()),
|
||||
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
]
|
||||
|
20
apps/api/views.py
Normal file
20
apps/api/views.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.generics import RetrieveAPIView
|
||||
|
||||
from .serializers import OAuthSerializer
|
||||
|
||||
|
||||
class UserInformationView(RetrieveAPIView):
|
||||
"""
|
||||
These fields are give to OAuth authenticators.
|
||||
"""
|
||||
serializer_class = OAuthSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return User.objects.filter(pk=self.request.user.pk)
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
17
apps/member/auth.py
Normal file
17
apps/member/auth.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from cas_server.auth import DjangoAuthUser # pragma: no cover
|
||||
from note.models import Alias
|
||||
|
||||
|
||||
class CustomAuthUser(DjangoAuthUser): # pragma: no cover
|
||||
"""
|
||||
Override Django Auth User model to define a custom Matrix username.
|
||||
"""
|
||||
|
||||
def attributs(self):
|
||||
d = super().attributs()
|
||||
if self.user:
|
||||
d["normalized_name"] = Alias.normalize(self.user.username)
|
||||
return d
|
@ -134,8 +134,6 @@ class PermissionBackend(ModelBackend):
|
||||
return False
|
||||
|
||||
sess = get_current_session()
|
||||
if sess is not None and sess.session_key is None:
|
||||
return False
|
||||
|
||||
if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
|
||||
return True
|
||||
|
80
docs/external_services/cas.rst
Normal file
80
docs/external_services/cas.rst
Normal file
@ -0,0 +1,80 @@
|
||||
Service d'Authentification Centralisé (CAS)
|
||||
===========================================
|
||||
|
||||
Un `CAS <https://fr.wikipedia.org/wiki/Central_Authentication_Service>`_ est
|
||||
déployé sur la Note Kfet. Il est accessible à l'adresse `<https://note.crans.org/cas/>`_.
|
||||
Il a pour but uniquement d'authentifier les utilisateurs via la note et ne communique
|
||||
que peu d'informations.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
Le serveur CAS utilisé est implémenté grâce au paquet ``django-cas-server``. Il peut être
|
||||
installé soit par PIP soit sur une machine Debian via
|
||||
``apt install python3-django-cas-server``.
|
||||
|
||||
On ajoute ensuite ``cas_server`` aux applications Django installées. On n'oublie pas ni
|
||||
d'appliquer les migrations (``./manage.py migrate``) ni de collecter les fichiers
|
||||
statiques (``./manage.py collectstatic``).
|
||||
|
||||
On enregistre les routes dans ``note_kfet/urls.py`` :
|
||||
|
||||
.. code:: python
|
||||
|
||||
urlpatterns.append(
|
||||
path('cas/', include('cas_server.urls', namespace='cas_server'))
|
||||
)
|
||||
|
||||
Le CAS est désormais déjà prêt à être utilisé. Toutefois, puisque l'on utilise un site
|
||||
Django-admin personnalisé, on n'oublie pas d'enregistrer les pages d'administration :
|
||||
|
||||
.. code:: python
|
||||
|
||||
if "cas_server" in settings.INSTALLED_APPS:
|
||||
from cas_server.admin import *
|
||||
from cas_server.models import *
|
||||
admin_site.register(ServicePattern, ServicePatternAdmin)
|
||||
admin_site.register(FederatedIendityProvider, FederatedIendityProviderAdmin)
|
||||
|
||||
Enfin, on souhaite pouvoir fournir au besoin le pseudo normalisé. Pour cela, on crée une
|
||||
classe dans ``member.auth`` :
|
||||
|
||||
.. code:: python
|
||||
|
||||
class CustomAuthUser(DjangoAuthUser):
|
||||
def attributs(self):
|
||||
d = super().attributs()
|
||||
if self.user:
|
||||
d["normalized_name"] = Alias.normalize(self.user.username)
|
||||
return d
|
||||
|
||||
|
||||
Puis on source ce fichier dans les paramètres :
|
||||
|
||||
.. code:: python
|
||||
|
||||
CAS_AUTH_CLASS = 'member.auth.CustomAuthUser'
|
||||
|
||||
Utilisation
|
||||
-----------
|
||||
Le service est accessible sur `<https://note.crans.org/cas/>`_. C'est ce lien qu'il faut
|
||||
donner à votre application.
|
||||
|
||||
L'application doit néanmoins être autorisée à accéder au CAS. Pour cela, rendez-vous
|
||||
dans Django-admin (`<https://note.crans.org/cas/admin/>`_), dans
|
||||
``Service Central d'Authentification/Motifs de services``, ajoutez une nouvelle entrée.
|
||||
Choisissez votre position favorite puis le nom de l'application.
|
||||
|
||||
Les champs importants sont les deux suivants :
|
||||
|
||||
* **Motif :** il s'agit d'une expression régulière qui doit reconnaitre le site voulu.
|
||||
Par exemple, pour autoriser Belenios (`<https://belenios.crans.org>`_), on rentrera
|
||||
le motif ``^https?://belenios\.crans\.org/.*$``.
|
||||
* **Champ d'utilisateur :** C'est le pseudo que renverra le CAS. Par défaut, il s'agira
|
||||
du nom de note principal, mais il arrive parfois que certains sites supportent mal
|
||||
d'avoir des caractères UTF-8 dans le pseudo. C'est par exemple le cas de Belenios.
|
||||
On rentrera alors ``normalized_name`` dans ce champ, qui correspond à la version
|
||||
normalisée (sans accent ni espace ni aucun caractère non-ASCII) du pseudo, et qui
|
||||
suffit à identifier une personne.
|
||||
|
||||
On peut également utiliser le ``Single log out`` si besoin.
|
28
docs/external_services/index.rst
Normal file
28
docs/external_services/index.rst
Normal file
@ -0,0 +1,28 @@
|
||||
Applications externes
|
||||
=====================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Applications externes
|
||||
|
||||
cas
|
||||
oauth2
|
||||
|
||||
.. warning::
|
||||
L'utilisation de la note par des services externes est actuellement en beta. Il est
|
||||
fort à parier que cette utilisation sera revue et améliorée à l'avenir.
|
||||
|
||||
Puisque la Note Kfet recense tous les comptes des adhérents BDE, les clubs ont alors
|
||||
la possibilité de développer leurs propres applications et de les interfacer avec la
|
||||
note. De cette façon, chaque application peut authentifier ses utilisateurs via la note,
|
||||
et récupérer leurs adhésion, leur nom de note afin d'éventuellement faire des transferts
|
||||
via l'API.
|
||||
|
||||
Deux protocoles d'authentification sont implémentées :
|
||||
|
||||
* `CAS <cas>`_
|
||||
* `OAuth2 <oauth2>`_
|
||||
|
||||
À ce jour, il n'y a pas encore d'exemple d'utilisation d'application qui utilise ce
|
||||
mécanisme, mais on peut imaginer par exemple que la Mediatek ou l'AMAP implémentent
|
||||
ces protocoles pour récupérer leurs adhérents.
|
133
docs/external_services/oauth2.rst
Normal file
133
docs/external_services/oauth2.rst
Normal file
@ -0,0 +1,133 @@
|
||||
OAuth2
|
||||
======
|
||||
|
||||
L'authentification `OAuth2 <https://fr.wikipedia.org/wiki/OAuth>`_ est supportée par la
|
||||
Note Kfet. Elle offre l'avantage non seulement d'identifier les utilisateurs, mais aussi
|
||||
de transmettre des informations à un service tiers tels que des informations personnelles,
|
||||
le solde de la note ou encore les adhésions de l'utilisateur, en l'avertissant sur
|
||||
quelles données sont effectivement collectées.
|
||||
|
||||
.. danger::
|
||||
L'implémentation actuelle ne permet pas de choisir quels droits on offre. Se connecter
|
||||
par OAuth2 offre actuellement exactement les mêmes permissions que l'on n'aurait
|
||||
normalement, avec le masque le plus haut, y compris en écriture.
|
||||
|
||||
Faites alors très attention lorsque vous vous connectez à un service tiers via OAuth2,
|
||||
et contrôlez bien exactement ce que l'application fait de vos données, à savoir si
|
||||
elle ignore bien tout ce dont elle n'a pas besoin.
|
||||
|
||||
À l'avenir, la fenêtre d'authentification pourra vous indiquer clairement quels
|
||||
paramètres sont collectés.
|
||||
|
||||
Configuration du serveur
|
||||
------------------------
|
||||
|
||||
On utilise ``django-oauth-toolkit``, qui peut être installé grâce à PIP ou bien via APT,
|
||||
via le paquet ``python3-django-oauth-toolkit``.
|
||||
|
||||
On commence par ajouter ``oauth2_provider`` aux applications Django installées. On
|
||||
n'oublie pas ni d'appliquer les migrations (``./manage.py migrate``) ni de collecter
|
||||
les fichiers statiques (``./manage.py collectstatic``).
|
||||
|
||||
On souhaite que l'API gérée par ``django-rest-framework`` puisse être accessible via
|
||||
l'authentification OAuth2. On adapte alors la configuration pour permettre cela :
|
||||
|
||||
.. code:: python
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
|
||||
...
|
||||
],
|
||||
...
|
||||
}
|
||||
|
||||
On ajoute les routes dans ``urls.py`` :
|
||||
|
||||
.. code:: python
|
||||
|
||||
urlpatterns.append(
|
||||
path('o/', include('oauth2_provider.urls', namespace='oauth2_provider'))
|
||||
)
|
||||
|
||||
L'OAuth2 est désormais prêt à être utilisé.
|
||||
|
||||
|
||||
Configuration client
|
||||
--------------------
|
||||
|
||||
Contrairement au `CAS <cas>`_, n'importe qui peut en théorie créer une application OAuth2.
|
||||
En théorie, car pour l'instant les permissions ne leur permettent pas.
|
||||
|
||||
Pour créer une application, il faut se rendre à la page
|
||||
`/o/applications/ <https://note.crans.org/o/applications/>`_. Dans ``client type``,
|
||||
rentrez ``public`` (ou ``confidential`` selon vos choix), et vous rentrerez
|
||||
généralement ``authorization-code`` dans ``Authorization Grant Type``.
|
||||
Le champ ``Redirect Uris`` contient une liste d'adresses URL autorisées pour des
|
||||
redirections post-connexion.
|
||||
|
||||
Il vous suffit de donner à votre application :
|
||||
|
||||
* L'identifiant client (client-ID)
|
||||
* La clé secrète
|
||||
* Les scopes : sous-ensemble de ``[read, write]`` (ignoré pour l'instant, cf premier paragraphe)
|
||||
* L'URL d'autorisation : `<https://note.crans.org/o/authorize/>`_
|
||||
* L'URL d'obtention de jeton : `<https://note.crans.org/o/token/>`_
|
||||
* L'URL de récupération des informations de l'utilisateur : `<https://note.crans.org/api/me/>`_
|
||||
|
||||
N'hésitez pas à consulter la page `<https://note.crans.org/api/me/>`_ pour s'imprégner
|
||||
du format renvoyé.
|
||||
|
||||
Avec Django-allauth
|
||||
###################
|
||||
|
||||
Si vous utilisez Django-allauth pour votre propre application, vous pouvez utiliser
|
||||
le module pré-configuré disponible ici :
|
||||
`<https://gitlab.crans.org/bde/allauth-note-kfet>`_. Pour l'installer, vous
|
||||
pouvez simplement faire :
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ pip3 install git+https://gitlab.crans.org/bde/allauth-note-kfet.git
|
||||
|
||||
L'installation du module se fera automatiquement.
|
||||
|
||||
Il vous suffit ensuite d'inclure l'application ``allauth_note_kfet`` à vos applications
|
||||
installées (sur votre propre client), puis de bien ajouter l'application sociale :
|
||||
|
||||
.. code:: python
|
||||
|
||||
SOCIALACCOUNT_PROVIDERS = {
|
||||
'notekfet': {
|
||||
# 'DOMAIN': 'note.crans.org',
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
Le paramètre ``DOMAIN`` permet de changer d'instance de Note Kfet. Par défaut, il
|
||||
se connectera à ``note.crans.org`` si vous ne renseignez rien.
|
||||
|
||||
En créant l'application sur la note, vous pouvez renseigner
|
||||
``https://monsite.example.com/accounts/notekfet/login/callback/`` en URL de redirection,
|
||||
à adapter selon votre configuration.
|
||||
|
||||
Vous devrez ensuite enregistrer l'application sociale dans la base de données.
|
||||
Vous pouvez passer par Django-admin, mais cela peut nécessiter d'avoir déjà un compte,
|
||||
alors autant le faire via un shell python :
|
||||
|
||||
.. code:: python
|
||||
|
||||
from allauth.socialaccount.models import SocialApp
|
||||
SocialApp.objects.create(
|
||||
name="Note Kfet",
|
||||
provider="notekfet",
|
||||
client_id="VOTRECLIENTID",
|
||||
secret="VOTRESECRET",
|
||||
key="",
|
||||
)
|
||||
|
||||
Si vous avez bien configuré ``django-allauth``, vous êtes désormais prêts par à vous
|
||||
connecter via la note :) Par défaut, nom, prénom, pseudo et adresse e-mail sont
|
||||
récupérés. Les autres données sont stockées mais inutilisées.
|
@ -12,3 +12,4 @@ Des informations complémentaires sont également disponibles sur le `Wiki Crans
|
||||
:caption: Développement de la NK20
|
||||
|
||||
apps/index
|
||||
external_services/index
|
||||
|
@ -52,3 +52,9 @@ if "rest_framework" in settings.INSTALLED_APPS:
|
||||
from rest_framework.authtoken.admin import *
|
||||
from rest_framework.authtoken.models import *
|
||||
admin_site.register(Token, TokenAdmin)
|
||||
|
||||
if "cas_server" in settings.INSTALLED_APPS:
|
||||
from cas_server.admin import *
|
||||
from cas_server.models import *
|
||||
admin_site.register(ServicePattern, ServicePatternAdmin)
|
||||
admin_site.register(FederatedIendityProvider, FederatedIendityProviderAdmin)
|
||||
|
@ -12,7 +12,7 @@ def read_env():
|
||||
directory.
|
||||
"""
|
||||
try:
|
||||
with open('.env') as f:
|
||||
with open(os.path.join(BASE_DIR, '.env')) as f:
|
||||
content = f.read()
|
||||
except IOError:
|
||||
content = ''
|
||||
@ -30,6 +30,7 @@ def read_env():
|
||||
|
||||
|
||||
# Try to load environment variables from project .env
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
read_env()
|
||||
|
||||
# Load base settings
|
||||
|
@ -239,6 +239,7 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
|
||||
],
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
'PAGE_SIZE': 20,
|
||||
@ -273,3 +274,6 @@ PIC_RATIO = 1
|
||||
# Custom phone number format
|
||||
PHONENUMBER_DB_FORMAT = 'NATIONAL'
|
||||
PHONENUMBER_DEFAULT_REGION = 'FR'
|
||||
|
||||
# We add custom information to CAS, in order to give a normalized name to other services
|
||||
CAS_AUTH_CLASS = 'member.auth.CustomAuthUser'
|
||||
|
@ -45,6 +45,11 @@ if "oauth2_provider" in settings.INSTALLED_APPS:
|
||||
path('o/', include('oauth2_provider.urls', namespace='oauth2_provider'))
|
||||
)
|
||||
|
||||
if "cas_server" in settings.INSTALLED_APPS:
|
||||
urlpatterns.append(
|
||||
path('cas/', include('cas_server.urls', namespace='cas_server'))
|
||||
)
|
||||
|
||||
if "debug_toolbar" in settings.INSTALLED_APPS:
|
||||
import debug_toolbar
|
||||
urlpatterns = [
|
||||
|
@ -1,6 +1,7 @@
|
||||
beautifulsoup4~=4.7.1
|
||||
Django~=2.2.15
|
||||
django-bootstrap-datepicker-plus~=3.0.5
|
||||
django-cas-server~=1.2.0
|
||||
django-colorfield~=0.3.2
|
||||
django-crispy-forms~=1.7.2
|
||||
django-extensions~=2.1.4
|
||||
|
Loading…
Reference in New Issue
Block a user