mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
OAuth: Add profile-only access
This commit is contained in:
@@ -84,3 +84,15 @@ class EventCRUDPermission(EventPermission):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ProfilePermission(BasePermission):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if not request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
if isinstance(request.auth, OAuthAccessToken):
|
||||
if not (request.auth.allow_scopes(['read']) or request.auth.allow_scopes(['profile'])) and request.method in SAFE_METHODS:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -9,7 +9,7 @@ from oauth2_provider.settings import oauth2_settings
|
||||
class Validator(OAuth2Validator):
|
||||
|
||||
def save_authorization_code(self, client_id, code, request, *args, **kwargs):
|
||||
if not getattr(request, 'organizers', None):
|
||||
if not getattr(request, 'organizers', None) and request.scopes != ['profile']:
|
||||
raise FatalClientError('No organizers selected.')
|
||||
|
||||
expires = timezone.now() + timedelta(
|
||||
@@ -18,7 +18,8 @@ class Validator(OAuth2Validator):
|
||||
expires=expires, redirect_uri=request.redirect_uri,
|
||||
scope=" ".join(request.scopes))
|
||||
g.save()
|
||||
g.organizers.add(*request.organizers.all())
|
||||
if request.scopes != ['profile']:
|
||||
g.organizers.add(*request.organizers.all())
|
||||
|
||||
def validate_code(self, client_id, code, client, request, *args, **kwargs):
|
||||
try:
|
||||
@@ -34,12 +35,14 @@ class Validator(OAuth2Validator):
|
||||
return False
|
||||
|
||||
def _create_access_token(self, expires, request, token, source_refresh_token=None):
|
||||
if not getattr(request, 'organizers', None) and not getattr(source_refresh_token, 'access_token'):
|
||||
if not getattr(request, 'organizers', None) and not getattr(source_refresh_token, 'access_token', None) and token["scope"] != 'profile':
|
||||
raise FatalClientError('No organizers selected.')
|
||||
if hasattr(request, 'organizers'):
|
||||
orgs = list(request.organizers.all())
|
||||
else:
|
||||
orgs = list(source_refresh_token.access_token.organizers.all())
|
||||
if token['scope'] != 'profile':
|
||||
if hasattr(request, 'organizers'):
|
||||
orgs = list(request.organizers.all())
|
||||
else:
|
||||
orgs = list(source_refresh_token.access_token.organizers.all())
|
||||
access_token = super()._create_access_token(expires, request, token, source_refresh_token=None)
|
||||
access_token.organizers.add(*orgs)
|
||||
if token['scope'] != 'profile':
|
||||
access_token.organizers.add(*orgs)
|
||||
return access_token
|
||||
|
||||
@@ -3,8 +3,9 @@ import logging
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
from oauth2_provider.exceptions import OAuthToolkitError
|
||||
from oauth2_provider.exceptions import OAuthToolkitError, FatalClientError
|
||||
from oauth2_provider.forms import AllowForm
|
||||
from oauth2_provider.settings import oauth2_settings
|
||||
from oauth2_provider.views import (
|
||||
AuthorizationView as BaseAuthorizationView,
|
||||
RevokeTokenView as BaseRevokeTokenView, TokenView as BaseTokenView,
|
||||
@@ -24,9 +25,12 @@ class OAuthAllowForm(AllowForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop('user')
|
||||
scope = kwargs.pop('scope')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['organizers'].queryset = Organizer.objects.filter(
|
||||
pk__in=user.teams.values_list('organizer', flat=True))
|
||||
if scope == 'profile':
|
||||
del self.fields['organizers']
|
||||
|
||||
|
||||
class AuthorizationView(BaseAuthorizationView):
|
||||
@@ -36,6 +40,7 @@ class AuthorizationView(BaseAuthorizationView):
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['user'] = self.request.user
|
||||
kwargs['scope'] = self.request.GET.get('scope')
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -43,8 +48,14 @@ class AuthorizationView(BaseAuthorizationView):
|
||||
ctx['settings'] = settings
|
||||
return ctx
|
||||
|
||||
def create_authorization_response(self, request, scopes, credentials, allow, organizers):
|
||||
credentials["organizers"] = organizers
|
||||
def validate_authorization_request(self, request):
|
||||
require_approval = request.GET.get("approval_prompt", oauth2_settings.REQUEST_APPROVAL_PROMPT)
|
||||
if require_approval != 'force' and request.GET.get('scope') != 'profile':
|
||||
raise FatalClientError('Combnination of require_approval and scope values not allowed.')
|
||||
return super().validate_authorization_request(request)
|
||||
|
||||
def create_authorization_response(self, request, scopes, credentials, allow, organizers=None):
|
||||
credentials["organizers"] = organizers or []
|
||||
return super().create_authorization_response(request, scopes, credentials, allow)
|
||||
|
||||
def form_valid(self, form):
|
||||
|
||||
@@ -3,14 +3,18 @@ from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from pretix.api.auth.permission import ProfilePermission
|
||||
|
||||
|
||||
class MeView(APIView):
|
||||
authentication_classes = (SessionAuthentication, OAuth2Authentication)
|
||||
permission_classes = (ProfilePermission,)
|
||||
|
||||
def get(self, request, format=None):
|
||||
return Response({
|
||||
'email': request.user.email,
|
||||
'fullname': request.user.fullname,
|
||||
'locale': request.user.locale,
|
||||
'is_staff': request.user.is_staff,
|
||||
'timezone': request.user.timezone
|
||||
})
|
||||
|
||||
@@ -98,7 +98,10 @@ class BaseAuthBackend:
|
||||
|
||||
class NativeAuthBackend(BaseAuthBackend):
|
||||
identifier = 'native'
|
||||
verbose_name = _('pretix User')
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return _('{system} User').format(system=settings.PRETIX_INSTANCE_NAME)
|
||||
|
||||
@property
|
||||
def login_form_fields(self) -> dict:
|
||||
|
||||
@@ -26,8 +26,10 @@
|
||||
<li>{{ scope }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p>{% trans "Please select the organizer accounts this application should get access to:" %}</p>
|
||||
{% bootstrap_field form.organizers layout="inline" %}
|
||||
{% if form.organizers %}
|
||||
<p>{% trans "Please select the organizer accounts this application should get access to:" %}</p>
|
||||
{% bootstrap_field form.organizers layout="inline" %}
|
||||
{% endif %}
|
||||
|
||||
{% bootstrap_form_errors form layout="control" %}
|
||||
<p class="text-danger">
|
||||
|
||||
@@ -574,3 +574,37 @@ def test_user_revoke(client, admin_user, organizer, application: OAuthApplicatio
|
||||
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
|
||||
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_allow_profile_only(client, admin_user, organizer, application: OAuthApplication):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&scope=profile' % (
|
||||
application.client_id, quote(application.redirect_uris)
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
resp = client.post('/api/v1/oauth/authorize', data={
|
||||
'organizers': [str(organizer.pk)],
|
||||
'redirect_uri': application.redirect_uris,
|
||||
'scope': 'profile',
|
||||
'client_id': application.client_id,
|
||||
'response_type': 'code',
|
||||
'allow': 'Authorize',
|
||||
})
|
||||
assert resp.status_code == 302
|
||||
assert resp['Location'].startswith('https://pretalx.com?code=')
|
||||
code = resp['Location'].split("=")[1]
|
||||
client.logout()
|
||||
resp = client.post('/api/v1/oauth/token', data={
|
||||
'code': code,
|
||||
'redirect_uri': application.redirect_uris,
|
||||
'grant_type': 'authorization_code',
|
||||
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
|
||||
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
|
||||
assert resp.status_code == 200
|
||||
data = json.loads(resp.content.decode())
|
||||
access_token = data['access_token']
|
||||
resp = client.get('/api/v1/organizers/', HTTP_AUTHORIZATION='Bearer %s' % access_token)
|
||||
assert resp.status_code == 403
|
||||
resp = client.get('/api/v1/me', HTTP_AUTHORIZATION='Bearer %s' % access_token)
|
||||
assert resp.status_code == 200
|
||||
|
||||
Reference in New Issue
Block a user