mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-11-04 01:12:08 +01:00 
			
		
		
		
	Compare commits
	
		
			28 Commits
		
	
	
		
			faster_ci
			...
			2793fee58c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2793fee58c | |||
| 
						
						
							
						
						7a715df121
	
				 | 
					
					
						|||
| 
						
						
							
						
						9308878054
	
				 | 
					
					
						|||
| 
						
						
							
						
						b5ccf5b800
	
				 | 
					
					
						|||
| 5e63254439 | |||
| 
						
						
							
						
						da96506218
	
				 | 
					
					
						|||
| b4714b896a | |||
| 
						
						
							
						
						cdb2647a4d
	
				 | 
					
					
						|||
| 
						
						
							
						
						cc12e3ec63
	
				 | 
					
					
						|||
| 
						
						
							
						
						be168c5ada
	
				 | 
					
					
						|||
| 
						
						
							
						
						b46ae6f856
	
				 | 
					
					
						|||
| 
						
						
							
						
						ec0bcbf015
	
				 | 
					
					
						|||
| 81303b8ef8 | |||
| 
						
						
							
						
						910b98fefc
	
				 | 
					
					
						|||
| 
						
						
							
						
						5a7a219ba8
	
				 | 
					
					
						|||
| 116451603c | |||
| 
						
						
							
						
						b2437ef9b5
	
				 | 
					
					
						|||
| 
						
						
							
						
						d8c9618772
	
				 | 
					
					
						|||
| 
						
						
							
						
						c825dee95a
	
				 | 
					
					
						|||
| 
						
						
							
						
						73d27e820b
	
				 | 
					
					
						|||
| 
						
						
							
						
						40e1b42078
	
				 | 
					
					
						|||
| 
						
						
							
						
						72806f0ace
	
				 | 
					
					
						|||
| 
						
						
							
						
						b244e01231
	
				 | 
					
					
						|||
| 
						
						
							
						
						76d1784aea
	
				 | 
					
					
						|||
| 
						
						
							
						
						56c5fa4057
	
				 | 
					
					
						|||
| 
						
						
							
						
						b5ef937a03
	
				 | 
					
					
						|||
| 
						
						
							
						
						e95a8b6e18
	
				 | 
					
					
						|||
| 
						
						
							
						
						635adf1360
	
				 | 
					
					
						
@@ -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',
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
 | 
			
		||||
import json
 | 
			
		||||
from datetime import datetime, date
 | 
			
		||||
from decimal import Decimal
 | 
			
		||||
from urllib.parse import quote_plus
 | 
			
		||||
from warnings import warn
 | 
			
		||||
 | 
			
		||||
@@ -152,6 +153,8 @@ class TestAPI(TestCase):
 | 
			
		||||
                value = value.isoformat()
 | 
			
		||||
            elif isinstance(value, ImageFieldFile):
 | 
			
		||||
                value = value.name
 | 
			
		||||
            elif isinstance(value, Decimal):
 | 
			
		||||
                value = str(value)
 | 
			
		||||
            query = json.dumps({field.name: value})
 | 
			
		||||
 | 
			
		||||
            # Create sample permission
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
							
								
								
									
										23
									
								
								apps/member/migrations/0007_auto_20210313_1235.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								apps/member/migrations/0007_auto_20210313_1235.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 2.2.19 on 2021-03-13 11:35
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('member', '0006_create_note_account_bde_membership'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='membership',
 | 
			
		||||
            name='roles',
 | 
			
		||||
            field=models.ManyToManyField(related_name='memberships', to='permission.Role', verbose_name='roles'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='profile',
 | 
			
		||||
            name='promotion',
 | 
			
		||||
            field=models.PositiveSmallIntegerField(default=2021, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										19
									
								
								apps/note/migrations/0005_auto_20210313_1235.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								apps/note/migrations/0005_auto_20210313_1235.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
# Generated by Django 2.2.19 on 2021-03-13 11:35
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('note', '0004_remove_null_tag_on_charfields'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='alias',
 | 
			
		||||
            name='note',
 | 
			
		||||
            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='alias', to='note.Note'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -3511,6 +3511,7 @@
 | 
			
		||||
				56,
 | 
			
		||||
				57,
 | 
			
		||||
				58,
 | 
			
		||||
				135,
 | 
			
		||||
				137,
 | 
			
		||||
				143,
 | 
			
		||||
				147,
 | 
			
		||||
@@ -3521,8 +3522,7 @@
 | 
			
		||||
				176,
 | 
			
		||||
				177,
 | 
			
		||||
				180,
 | 
			
		||||
				181,
 | 
			
		||||
				182
 | 
			
		||||
				181
 | 
			
		||||
			]
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 
 | 
			
		||||
 Submodule apps/scripts updated: 8ec7d68a16...cf8b05d20a
									
								
							
							
								
								
									
										25
									
								
								apps/treasury/migrations/0003_auto_20210321_1034.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								apps/treasury/migrations/0003_auto_20210321_1034.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
# Generated by Django 2.2.19 on 2021-03-21 09:34
 | 
			
		||||
 | 
			
		||||
import django.core.validators
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('treasury', '0002_invoice_remove_png_extension'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='product',
 | 
			
		||||
            name='quantity',
 | 
			
		||||
            field=models.DecimalField(decimal_places=2, max_digits=7, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='specialtransactionproxy',
 | 
			
		||||
            name='remittance',
 | 
			
		||||
            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='transaction_proxies', to='treasury.Remittance', verbose_name='Remittance'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -5,6 +5,7 @@ from datetime import date
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth.models import User
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.core.validators import MinValueValidator
 | 
			
		||||
from django.db import models, transaction
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.template.loader import render_to_string
 | 
			
		||||
@@ -131,12 +132,15 @@ class Product(models.Model):
 | 
			
		||||
        verbose_name=_("Designation"),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    quantity = models.PositiveIntegerField(
 | 
			
		||||
        verbose_name=_("Quantity")
 | 
			
		||||
    quantity = models.DecimalField(
 | 
			
		||||
        decimal_places=2,
 | 
			
		||||
        max_digits=7,
 | 
			
		||||
        verbose_name=_("Quantity"),
 | 
			
		||||
        validators=[MinValueValidator(0)],
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    amount = models.IntegerField(
 | 
			
		||||
        verbose_name=_("Unit price")
 | 
			
		||||
        verbose_name=_("Unit price"),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
{% load escape_tex %}
 | 
			
		||||
{% load escape_tex i18n %}
 | 
			
		||||
{% language fr %}
 | 
			
		||||
\documentclass[a4paper,11pt]{article}
 | 
			
		||||
 | 
			
		||||
\usepackage{fontspec}
 | 
			
		||||
@@ -176,3 +177,4 @@ TVA non applicable, article 293 B du CGI.
 | 
			
		||||
\end{center}
 | 
			
		||||
 | 
			
		||||
\end{document}
 | 
			
		||||
{% endlanguage %}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								apps/wei/migrations/0002_auto_20210313_1235.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								apps/wei/migrations/0002_auto_20210313_1235.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 2.2.19 on 2021-03-13 11:35
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('wei', '0001_initial'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='weiclub',
 | 
			
		||||
            name='year',
 | 
			
		||||
            field=models.PositiveIntegerField(default=2021, unique=True, verbose_name='year'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='weiregistration',
 | 
			
		||||
            name='information_json',
 | 
			
		||||
            field=models.TextField(default='{}', help_text='Information about the registration (buses for old members, survey for the new members), encoded in JSON', verbose_name='registration information'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								note.cron
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								note.cron
									
									
									
									
									
								
							@@ -1,24 +1,27 @@
 | 
			
		||||
# {{ ansible_managed }}
 | 
			
		||||
# Les cronjobs dont a besoin la Note Kfet
 | 
			
		||||
 | 
			
		||||
# Envoi des mails aux respos info
 | 
			
		||||
MAILTO=notekfet2020@lists.crans.org
 | 
			
		||||
 | 
			
		||||
# m  h   dom mon dow     user   command
 | 
			
		||||
# Envoyer les mails en attente
 | 
			
		||||
 *   *     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py send_mail -c 1
 | 
			
		||||
 *   *     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py retry_deferred -c 1
 | 
			
		||||
 00  0     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py purge_mail_log 7 -c 1
 | 
			
		||||
 *   *     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py send_mail -v 0
 | 
			
		||||
 *   *     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py retry_deferred -v 0
 | 
			
		||||
 00  0     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py purge_mail_log 7 -v 0
 | 
			
		||||
# Faire une sauvegarde de la base de données
 | 
			
		||||
 00  2     *   *   *     root   cd /var/www/note_kfet && apps/scripts/shell/backup_db
 | 
			
		||||
# Vérifier la cohérence de la base et mailer en cas de problème
 | 
			
		||||
 00  4     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py check_consistency --sum-all --check-all --mail
 | 
			
		||||
 00  4     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py check_consistency --sum-all --check-all --mail -v 0
 | 
			
		||||
# Mettre à jour le wiki (modification sans (dé)validation, activités passées)
 | 
			
		||||
 30  5     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py refresh_activities --raw --human --comment refresh --wiki
 | 
			
		||||
 30  5     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py refresh_activities --raw --human --comment refresh --wiki -v 0
 | 
			
		||||
# Spammer les gens en négatif
 | 
			
		||||
 00  5     *   *   2     root   cd /var/www/note_kfet && env/bin/python manage.py send_mail_to_negative_balances --spam
 | 
			
		||||
 00  5     *   *   2     root   cd /var/www/note_kfet && env/bin/python manage.py send_mail_to_negative_balances --spam -v 0
 | 
			
		||||
# Envoyer le rapport mensuel aux trésoriers et respos info
 | 
			
		||||
 00  8     6   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py send_mail_to_negative_balances --report
 | 
			
		||||
 00  8     6   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py send_mail_to_negative_balances --report --add-years 1 -v 0
 | 
			
		||||
# Envoyer les rapports aux gens
 | 
			
		||||
 55  6     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py send_reports
 | 
			
		||||
 55  6     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py send_reports -v 0
 | 
			
		||||
# Mettre à jour les boutons mis en avant
 | 
			
		||||
 00  9     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py refresh_highlighted_buttons
 | 
			
		||||
 00  9     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py refresh_highlighted_buttons -v 0
 | 
			
		||||
# Vider les tokens Oauth2
 | 
			
		||||
 00  6     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py cleartokens
 | 
			
		||||
 00  6     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py cleartokens -v 0
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user