mirror of https://gitlab.crans.org/bde/nk20
Merge branch 'optional-scopes' into 'beta'
Implement optional scopes : clients can request scopes, but they are not guaranteed to get them See merge request bde/nk20!192
This commit is contained in:
commit
4e30f805a7
|
@ -7,8 +7,11 @@ from django.contrib.auth.models import User
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from member.api.serializers import ProfileSerializer, MembershipSerializer
|
from member.api.serializers import ProfileSerializer, MembershipSerializer
|
||||||
|
from member.models import Membership
|
||||||
from note.api.serializers import NoteSerializer
|
from note.api.serializers import NoteSerializer
|
||||||
from note.models import Alias
|
from note.models import Alias
|
||||||
|
from note_kfet.middlewares import get_current_request
|
||||||
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(serializers.ModelSerializer):
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
|
@ -45,18 +48,30 @@ class OAuthSerializer(serializers.ModelSerializer):
|
||||||
"""
|
"""
|
||||||
normalized_name = serializers.SerializerMethodField()
|
normalized_name = serializers.SerializerMethodField()
|
||||||
|
|
||||||
profile = ProfileSerializer()
|
profile = serializers.SerializerMethodField()
|
||||||
|
|
||||||
note = NoteSerializer()
|
note = serializers.SerializerMethodField()
|
||||||
|
|
||||||
memberships = serializers.SerializerMethodField()
|
memberships = serializers.SerializerMethodField()
|
||||||
|
|
||||||
def get_normalized_name(self, obj):
|
def get_normalized_name(self, obj):
|
||||||
return Alias.normalize(obj.username)
|
return Alias.normalize(obj.username)
|
||||||
|
|
||||||
|
def get_profile(self, obj):
|
||||||
|
# Display the profile of the user only if we have rights to see it.
|
||||||
|
return ProfileSerializer().to_representation(obj.profile) \
|
||||||
|
if PermissionBackend.has_perm(get_current_request(), obj.profile, 'view') else None
|
||||||
|
|
||||||
|
def get_note(self, obj):
|
||||||
|
# Display the note of the user only if we have rights to see it.
|
||||||
|
return NoteSerializer().to_representation(obj.note) \
|
||||||
|
if PermissionBackend.has_perm(get_current_request(), obj.note, 'view') else None
|
||||||
|
|
||||||
def get_memberships(self, obj):
|
def get_memberships(self, obj):
|
||||||
|
# Display only memberships that we are allowed to see.
|
||||||
return serializers.ListSerializer(child=MembershipSerializer()).to_representation(
|
return serializers.ListSerializer(child=MembershipSerializer()).to_representation(
|
||||||
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now()))
|
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now())
|
||||||
|
.filter(PermissionBackend.filter_queryset(get_current_request(), Membership, 'view')))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
from oauth2_provider.oauth2_validators import OAuth2Validator
|
||||||
from oauth2_provider.scopes import BaseScopes
|
from oauth2_provider.scopes import BaseScopes
|
||||||
from member.models import Club
|
from member.models import Club
|
||||||
from note_kfet.middlewares import get_current_request
|
from note_kfet.middlewares import get_current_request
|
||||||
|
@ -32,3 +32,26 @@ class PermissionScopes(BaseScopes):
|
||||||
return []
|
return []
|
||||||
return [f"{p.id}_{p.membership.club.id}"
|
return [f"{p.id}_{p.membership.club.id}"
|
||||||
for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
|
for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionOAuth2Validator(OAuth2Validator):
|
||||||
|
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
User can request as many scope as he wants, including invalid scopes,
|
||||||
|
but it will have only the permissions he has.
|
||||||
|
|
||||||
|
This allows clients to request more permission to get finally a
|
||||||
|
subset of permissions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
valid_scopes = set()
|
||||||
|
|
||||||
|
for t in Permission.PERMISSION_TYPES:
|
||||||
|
for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0]):
|
||||||
|
scope = f"{p.id}_{p.membership.club.id}"
|
||||||
|
if scope in scopes:
|
||||||
|
valid_scopes.add(scope)
|
||||||
|
|
||||||
|
request.scopes = valid_scopes
|
||||||
|
|
||||||
|
return valid_scopes
|
||||||
|
|
|
@ -41,8 +41,14 @@ On a ensuite besoin de définir nos propres scopes afin d'avoir des permissions
|
||||||
|
|
||||||
OAUTH2_PROVIDER = {
|
OAUTH2_PROVIDER = {
|
||||||
'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes',
|
'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes',
|
||||||
|
'OAUTH2_VALIDATOR_CLASS': "permission.scopes.PermissionOAuth2Validator",
|
||||||
|
'REFRESH_TOKEN_EXPIRE_SECONDS': timedelta(days=14),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Cela a pour effet d'avoir des scopes sous la forme ``PERMISSION_CLUB``,
|
||||||
|
et de demander des scopes facultatives (voir plus bas).
|
||||||
|
Un jeton de rafraîchissement expire de plus au bout de 14 jours, si non-renouvelé.
|
||||||
|
|
||||||
On ajoute enfin les routes dans ``urls.py`` :
|
On ajoute enfin les routes dans ``urls.py`` :
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
@ -94,6 +100,27 @@ du format renvoyé.
|
||||||
Vous pouvez donc contrôler le plus finement possible les permissions octroyées à vos
|
Vous pouvez donc contrôler le plus finement possible les permissions octroyées à vos
|
||||||
jetons.
|
jetons.
|
||||||
|
|
||||||
|
.. danger::
|
||||||
|
|
||||||
|
Demander des scopes n'implique pas de les avoir.
|
||||||
|
|
||||||
|
Lorsque des scopes sont demandées par un client, la Note
|
||||||
|
va considérer l'ensemble des permissions accessibles parmi
|
||||||
|
ce qui est demandé. Dans vos programmes, vous devrez donc
|
||||||
|
vérifier les permissions acquises (communiquées lors de la
|
||||||
|
récupération du jeton d'accès à partir du grant code),
|
||||||
|
et prévoir un comportement dans le cas où des permissions
|
||||||
|
sont manquantes.
|
||||||
|
|
||||||
|
Cela offre un intérêt supérieur par rapport au protocole
|
||||||
|
OAuth2 classique, consistant à demander trop de permissions
|
||||||
|
et agir en conséquence.
|
||||||
|
|
||||||
|
Par exemple, vous pourriez demander la permission d'accéder
|
||||||
|
aux membres d'un club ou de faire des transactions, et agir
|
||||||
|
uniquement dans le cas où l'utilisateur connecté possède la
|
||||||
|
permission problématique.
|
||||||
|
|
||||||
Avec Django-allauth
|
Avec Django-allauth
|
||||||
###################
|
###################
|
||||||
|
|
||||||
|
@ -116,6 +143,7 @@ installées (sur votre propre client), puis de bien ajouter l'application social
|
||||||
SOCIALACCOUNT_PROVIDERS = {
|
SOCIALACCOUNT_PROVIDERS = {
|
||||||
'notekfet': {
|
'notekfet': {
|
||||||
# 'DOMAIN': 'note.crans.org',
|
# 'DOMAIN': 'note.crans.org',
|
||||||
|
'SCOPE': ['1_1', '2_1'],
|
||||||
},
|
},
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
|
@ -123,6 +151,10 @@ installées (sur votre propre client), puis de bien ajouter l'application social
|
||||||
Le paramètre ``DOMAIN`` permet de changer d'instance de Note Kfet. Par défaut, il
|
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.
|
se connectera à ``note.crans.org`` si vous ne renseignez rien.
|
||||||
|
|
||||||
|
Le paramètre ``SCOPE`` permet de définir les scopes à demander.
|
||||||
|
Dans l'exemple ci-dessous, les permissions d'accéder à l'utilisateur
|
||||||
|
et au profil sont demandées.
|
||||||
|
|
||||||
En créant l'application sur la note, vous pouvez renseigner
|
En créant l'application sur la note, vous pouvez renseigner
|
||||||
``https://monsite.example.com/accounts/notekfet/login/callback/`` en URL de redirection,
|
``https://monsite.example.com/accounts/notekfet/login/callback/`` en URL de redirection,
|
||||||
à adapter selon votre configuration.
|
à adapter selon votre configuration.
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
|
@ -248,6 +250,8 @@ REST_FRAMEWORK = {
|
||||||
# OAuth2 Provider
|
# OAuth2 Provider
|
||||||
OAUTH2_PROVIDER = {
|
OAUTH2_PROVIDER = {
|
||||||
'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes',
|
'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes',
|
||||||
|
'OAUTH2_VALIDATOR_CLASS': "permission.scopes.PermissionOAuth2Validator",
|
||||||
|
'REFRESH_TOKEN_EXPIRE_SECONDS': timedelta(days=14),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Take control on how widget templates are sourced
|
# Take control on how widget templates are sourced
|
||||||
|
|
Loading…
Reference in New Issue