Implement OAuth2 provider (#927)

- [x] Application management
  - [x] Link
  - [ ] Tests
- [x] Authorize flow
  - [x] Tests
- [x] Refresh token handling
  - [x] Tests
- [x] Revocation endpoint
  - [x] Tests
  - [x] Mitigate: https://github.com/jazzband/django-oauth-toolkit/issues/585
- [x] API authenticator / permission driver
  - [x] Test
- [x] Enforce organizer restriction
  - [x] Tests
- [x] Enforce scope restriction
  - [x] Tests
- [x] Show current applications to user
  - [x] Revoke
  - [x] Tests
- [x] Log new authorizations
  - [x] notify user
- [x] Ensure other grant types are not available
- [x] Documentation
- [x] check if revoking access toking, then refreshing gets rid of organizer constraint
- [x] Show logentry foo
This commit is contained in:
Raphael Michel
2018-06-05 12:58:04 +02:00
committed by GitHub
parent df031b2222
commit 69d10489b8
53 changed files with 1786 additions and 116 deletions
+2 -2
View File
@@ -39,11 +39,11 @@ if [ "$1" == "translation-spelling" ]; then
potypo
fi
if [ "$1" == "tests" ]; then
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt pytest-xdist
cd src
python manage.py check
make all compress
py.test --reruns 5 tests
py.test --reruns 5 -n 2 tests
fi
if [ "$1" == "tests-cov" ]; then
pip3 install -r src/requirements.txt -Ur src/requirements/dev.txt -r src/requirements/py34.txt
+1 -1
View File
@@ -15,7 +15,7 @@ matrix:
- python: 3.6
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
- python: 3.6
env: JOB=tests-cov
env: JOB=tests-cov PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
- python: 3.6
env: JOB=style
- python: 3.4
+22 -18
View File
@@ -6,27 +6,13 @@ with pretix' REST API, such as authentication, pagination and similar definition
.. _`rest-auth`:
Obtaining an API token
----------------------
To authenticate your API requests, you need to obtain an API token. You can create a
token in the pretix web interface on the level of organizer teams. Create a new team
or choose an existing team that has the level of permissions the token should have and
create a new token using the form below the list of team members:
.. image:: img/token_form.png
:class: screenshot
You can enter a description for the token to distinguish from other tokens later on.
Once you click "Add", you will be provided with an API token in the success message.
Copy this token, as you won't be able to retrieve it again.
.. image:: img/token_success.png
:class: screenshot
Authentication
--------------
If you're building an application for end users, we strongly recommend that you use our
:ref:`OAuth-based authentication progress <rest-oauth>`. However, for simpler needs, you
can also go with static API tokens that you can create on a per-team basis (see below).
You need to include the API token with every request to pretix' API in the ``Authorization`` header
like the following:
@@ -44,6 +30,24 @@ like the following:
adding OAuth2 support in the future for user-level authentication. If you want
to use session authentication, be sure to comply with Django's `CSRF policies`_.
Obtaining an API token
----------------------
To authenticate your API requests, you need to obtain an API token. You can create a
token in the pretix web interface on the level of organizer teams. Create a new team
or choose an existing team that has the level of permissions the token should have and
create a new token using the form below the list of team members:
.. image:: img/token_form.png
:class: screenshot
You can enter a description for the token to distinguish from other tokens later on.
Once you click "Add", you will be provided with an API token in the success message.
Copy this token, as you won't be able to retrieve it again.
.. image:: img/token_success.png
:class: screenshot
Permissions
-----------
+1
View File
@@ -14,4 +14,5 @@ in functionality over time.
:maxdepth: 2
fundamentals
oauth
resources/index
+171
View File
@@ -0,0 +1,171 @@
.. _`rest-oauth`:
OAuth support / "Connect with pretix"
=====================================
In addition to static tokens, pretix supports `OAuth2`_-based authentication starting with
pretix 1.16. This allows you to put a "Connect with pretix" button into your website or tool
that allows the user to easily set up a connection between the two systems.
If you haven't worked with OAuth before, have a look at the `OAuth2 Simplified`_ tutorial.
Registering an application
--------------------------
To use OAuth, you need to register your application with the pretix instance you want to connect to.
In order to do this, log in to your pretix account and go to your user settings. Click on "Authorized applications"
first and then on "Manage your own apps". From there, you can "Create a new application".
You should fill in a descriptive name of your application that allows users to recognize who you are. You also need to
give a list of fully-qualified URLs that users will be redirected to after a successful authorization. After you pressed
"Save", you will be presented with a client ID and a client secret. Please note them down and treat the client secret
like a password; it should not become available to your users.
Obtaining an authorization grant
--------------------------------
To authorize a new user, link or redirect them to the ``authorize`` endpoint, passing your client ID as a query
parameter. Additionally, you can pass a scope (currently either ``read``, ``write``, or ``read write``)
and an URL the user should be redirected to after successful or failed authorization. You also need to pass the
``response_type`` parameter with a value of ``code``. Example::
https://pretix.eu/api/v1/oauth/authorize?client_id=lsLi0hNL0vk53mEdYjNJxHUn1PcO1R6wVg81dLNT&response_type=code&scope=read+write&redirect_uri=https://pretalx.com
To prevent CSRF attacks, you can also optionally pass a ``state`` parameter with a random string. Later, when
redirecting back to your application, we will pass the same ``state`` parameter back to you, so you can compare if they
match.
After the user granted or denied access, they will be redirected back either to the ``redirect_url`` you passed in the
query or to the first redirect URL configured in your application settings.
On successful registration, we will append the query parameter ``code`` to the URL containing an authorization code.
For example, we might redirect the user to this URL::
https://pretalx.com/?code=eYBBf8gmeD4E01HLoj0XflqO4Lg3Cw&state=e3KCh9mfx07qxU4bRpXk
You will need this ``code`` parameter to perform the next step.
On a failed registration, a query string like ``?error=access_denied`` will be appended to the redirection URL.
.. note:: In this step, the user is allowed to restrict your access to certain organizer accounts. If you try to
re-authenticate the user later, the user might be instantly redirected back to you if authorization is already
given and would therefore be unable to review their organizer restriction settings. You can append the
``approval_prompt=force`` query parameter if you want to make sure the user actively needs to confirm the
authorization.
Getting an access token
-----------------------
Using the ``code`` value you obtained above and your client ID, you can now request an access token that actually gives
access to the API. The ``token`` endpoint expects you to authenticate using `HTTP Basic authentication`_ using your client
ID as a username and your client secret as a password. You are also required to again supply the same ``redirect_uri``
parameter that you used for the authorization.
.. http:get:: /api/v1/oauth/token
Request a new access token
**Example request**:
.. sourcecode:: http
POST /api/v1/oauth/token HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Authorization: Basic bHNMaTBoTkwwdms1M21FZFlqTkp4SFVuMVBjTzFSNndWZzgxZExOVDplSmpzZVA0UjJMN0hMcjBiS0p1b3BmbnJtT2cyY3NDeTdYaFVVZ0FoalhUU0NhZHhRTjk3cVNvMkpPaXlWTFpQOEozaTVQd1FVdFIwNUNycG5ac2Z0bXJjdmNTbkZ1SkFmb2ZsUTdZUDRpSjZNTWFYTHIwQ0FpNlhIRFJjV1Awcg==
grant_type=authorization_code&code=eYBBf8gmeD4E01HLoj0XflqO4Lg3Cw&redirect_uri=https://pretalx.com
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"access_token": "i3ytqTSRWsKp16fqjekHXa4tdM4qNC",
"expires_in": 86400,
"token_type": "Bearer",
"scope": "read write",
"refresh_token": "XBK0r8z4A4TTeR9LyMUyU2AM5rqpXp"
}
:statuscode 200: no error
:statuscode 401: Authentication failure
As you can see, you receive two types of tokens: One "access token", and one "refresh token". The access token is valid
for a day and can be used to actually access the API. The refresh token does not have an expiration date and can be used
to obtain a new access_token after a day, so you should make sure to store the access token safely if you need long-term
access.
Using the API with an access token
----------------------------------
You can supply a valid access token as a ``Bearer``-type token in the ``Authorization`` header to get API access.
.. sourcecode:: http
:emphasize-lines: 3
GET /api/v1/organizers/ HTTP/1.1
Host: pretix.eu
Authorization: Bearer i3ytqTSRWsKp16fqjekHXa4tdM4qNC
Refreshing an access token
--------------------------
You can obtain a new access token using your refresh token any time. This can be done using the same ``token`` endpoint
used to obtain the first access token above, but with a different set of parameters:
.. sourcecode:: http
POST /api/v1/oauth/token HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Authorization: Basic bHNMaTBoTkwwdms1M21FZFlqTkp4SFVuMVBjTzFSNndWZzgxZExOVDplSmpzZVA0UjJMN0hMcjBiS0p1b3BmbnJtT2cyY3NDeTdYaFVVZ0FoalhUU0NhZHhRTjk3cVNvMkpPaXlWTFpQOEozaTVQd1FVdFIwNUNycG5ac2Z0bXJjdmNTbkZ1SkFmb2ZsUTdZUDRpSjZNTWFYTHIwQ0FpNlhIRFJjV1Awcg==
grant_type=refresh_token&refresh_token=XBK0r8z4A4TTeR9LyMUyU2AM5rqpXp
The previous access token will instantly become invalid.
Revoking a token
----------------
If you don't need a token any more or if you believe it may have been compromised, you can use the ``revoke_token``
endpoint to revoke it.
.. http:get:: /api/v1/oauth/revoke_token
Revoke an access or refresh token. If you revoke an access token, you can still create a new one using the refresh token. If you
revoke a refresh token, the connected access token will also be revoked.
**Example request**:
.. sourcecode:: http
POST /api/v1/oauth/revoke_token HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Authorization: Basic bHNMaTBoTkwwdms1M21FZFlqTkp4SFVuMVBjTzFSNndWZzgxZExOVDplSmpzZVA0UjJMN0hMcjBiS0p1b3BmbnJtT2cyY3NDeTdYaFVVZ0FoalhUU0NhZHhRTjk3cVNvMkpPaXlWTFpQOEozaTVQd1FVdFIwNUNycG5ac2Z0bXJjdmNTbkZ1SkFmb2ZsUTdZUDRpSjZNTWFYTHIwQ0FpNlhIRFJjV1Awcg==
token=XBK0r8z4A4TTeR9LyMUyU2AM5rqpXp
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
:statuscode 200: no error
:statuscode 401: Authentication failure
If you want to revoke your client secret, you can generate a new one in the list of your managed applications in the
pretix user interface.
.. _OAuth2: https://en.wikipedia.org/wiki/OAuth
.. _OAuth2 Simplified: https://aaronparecki.com/oauth-2-simplified/
.. _HTTP Basic authentication: https://en.wikipedia.org/wiki/Basic_access_authentication
+9
View File
@@ -0,0 +1,9 @@
from django.apps import AppConfig
class PretixApiConfig(AppConfig):
name = 'pretix.api'
label = 'pretixapi'
default_app_config = 'pretix.api.PretixApiConfig'
+10
View File
@@ -1,5 +1,6 @@
from rest_framework.permissions import SAFE_METHODS, BasePermission
from pretix.api.models import OAuthAccessToken
from pretix.base.models import Event
from pretix.base.models.organizer import Organizer, TeamAPIToken
from pretix.helpers.security import (
@@ -55,6 +56,15 @@ class EventPermission(BasePermission):
if required_permission and required_permission not in request.orgapermset:
return False
if isinstance(request.auth, OAuthAccessToken):
if not request.auth.allow_scopes(['write']) and request.method not in SAFE_METHODS:
return False
if not request.auth.allow_scopes(['read']) and request.method in SAFE_METHODS:
return False
if isinstance(request.auth, OAuthAccessToken) and hasattr(request, 'organizer'):
if not request.auth.organizers.filter(pk=request.organizer.pk).exists():
return False
return True
+128
View File
@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-06-04 11:19
from __future__ import unicode_literals
import django.db.models.deletion
import oauth2_provider.generators
import oauth2_provider.validators
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='OAuthAccessToken',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('token', models.CharField(max_length=255, unique=True)),
('expires', models.DateTimeField()),
('scope', models.TextField(blank=True)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='OAuthApplication',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('client_type',
models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], max_length=32)),
('authorization_grant_type', models.CharField(
choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'),
('password', 'Resource owner password-based'),
('client-credentials', 'Client credentials')], max_length=32)),
('skip_authorization', models.BooleanField(default=False)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=255, verbose_name='Application name')),
('redirect_uris', models.TextField(help_text='Allowed URIs list, space separated',
validators=[oauth2_provider.validators.validate_uris],
verbose_name='Redirection URIs')),
('client_id',
models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100,
unique=True, verbose_name='Client ID')),
('client_secret',
models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_secret,
max_length=255, verbose_name='Client secret')),
('active', models.BooleanField(default=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
related_name='pretixapi_oauthapplication', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='OAuthGrant',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('code', models.CharField(max_length=255, unique=True)),
('expires', models.DateTimeField()),
('redirect_uri', models.CharField(max_length=255)),
('scope', models.TextField(blank=True)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
('user',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pretixapi_oauthgrant',
to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='OAuthRefreshToken',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('token', models.CharField(max_length=255)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('revoked', models.DateTimeField(null=True)),
('access_token',
models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name='refresh_token', to=settings.OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL)),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='pretixapi_oauthrefreshtoken', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='oauthaccesstoken',
name='application',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
),
migrations.AddField(
model_name='oauthaccesstoken',
name='source_refresh_token',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
related_name='refreshed_access_token',
to=settings.OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL),
),
migrations.AddField(
model_name='oauthaccesstoken',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
related_name='pretixapi_oauthaccesstoken', to=settings.AUTH_USER_MODEL),
),
migrations.AlterUniqueTogether(
name='oauthrefreshtoken',
unique_together=set([('token', 'revoked')]),
),
]
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-06-04 11:20
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0001_initial'),
('pretixapi', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='oauthaccesstoken',
name='organizers',
field=models.ManyToManyField(to='pretixbase.Organizer'),
),
migrations.AddField(
model_name='oauthgrant',
name='organizers',
field=models.ManyToManyField(to='pretixbase.Organizer'),
),
]
+70
View File
@@ -0,0 +1,70 @@
from datetime import timedelta
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from oauth2_provider.generators import (
generate_client_id, generate_client_secret,
)
from oauth2_provider.models import (
AbstractAccessToken, AbstractApplication, AbstractGrant,
AbstractRefreshToken,
)
from oauth2_provider.validators import validate_uris
class OAuthApplication(AbstractApplication):
name = models.CharField(verbose_name=_("Application name"), max_length=255, blank=False)
redirect_uris = models.TextField(
blank=False, validators=[validate_uris],
verbose_name=_("Redirection URIs"),
help_text=_("Allowed URIs list, space separated")
)
client_id = models.CharField(
verbose_name=_("Client ID"),
max_length=100, unique=True, default=generate_client_id, db_index=True
)
client_secret = models.CharField(
verbose_name=_("Client secret"),
max_length=255, blank=False, default=generate_client_secret, db_index=True
)
active = models.BooleanField(default=True)
def get_absolute_url(self):
return reverse("control:user.settings.oauth.app", kwargs={'pk': self.id})
def is_usable(self, request):
return self.active and super().is_usable(request)
class OAuthGrant(AbstractGrant):
application = models.ForeignKey(
OAuthApplication, on_delete=models.CASCADE
)
organizers = models.ManyToManyField('pretixbase.Organizer')
class OAuthAccessToken(AbstractAccessToken):
source_refresh_token = models.OneToOneField(
# unique=True implied by the OneToOneField
'OAuthRefreshToken', on_delete=models.SET_NULL, blank=True, null=True,
related_name="refreshed_access_token"
)
application = models.ForeignKey(
OAuthApplication, on_delete=models.CASCADE, blank=True, null=True,
)
organizers = models.ManyToManyField('pretixbase.Organizer')
def revoke(self):
self.expires = now() - timedelta(hours=1)
self.save(update_fields=['expires'])
class OAuthRefreshToken(AbstractRefreshToken):
application = models.ForeignKey(
OAuthApplication, on_delete=models.CASCADE)
access_token = models.OneToOneField(
OAuthAccessToken, on_delete=models.SET_NULL, blank=True, null=True,
related_name="refresh_token"
)
+45
View File
@@ -0,0 +1,45 @@
from datetime import timedelta
from django.utils import timezone
from oauth2_provider.exceptions import FatalClientError
from oauth2_provider.oauth2_validators import Grant, OAuth2Validator
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):
raise FatalClientError('No organizers selected.')
expires = timezone.now() + timedelta(
seconds=oauth2_settings.AUTHORIZATION_CODE_EXPIRE_SECONDS)
g = Grant(application=request.client, user=request.user, code=code["code"],
expires=expires, redirect_uri=request.redirect_uri,
scope=" ".join(request.scopes))
g.save()
g.organizers.add(*request.organizers.all())
def validate_code(self, client_id, code, client, request, *args, **kwargs):
try:
grant = Grant.objects.get(code=code, application=client)
if not grant.is_expired():
request.scopes = grant.scope.split(" ")
request.user = grant.user
request.organizers = grant.organizers.all()
return True
return False
except Grant.DoesNotExist:
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'):
raise FatalClientError('No organizers selected.')
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)
return access_token
+6 -1
View File
@@ -4,7 +4,9 @@ from django.apps import apps
from django.conf.urls import include, url
from rest_framework import routers
from .views import checkin, event, item, order, organizer, voucher, waitinglist
from .views import (
checkin, event, item, oauth, order, organizer, voucher, waitinglist,
)
router = routers.DefaultRouter()
router.register(r'organizers', organizer.OrganizerViewSet)
@@ -52,4 +54,7 @@ urlpatterns = [
include(question_router.urls)),
url(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/checkinlists/(?P<list>[^/]+)/',
include(checkinlist_router.urls)),
url(r"^oauth/authorize$", oauth.AuthorizationView.as_view(), name="authorize"),
url(r"^oauth/token$", oauth.TokenView.as_view(), name="token"),
url(r"^oauth/revoke_token$", oauth.RevokeTokenView.as_view(), name="revoke-token"),
]
+3 -4
View File
@@ -16,7 +16,6 @@ from pretix.api.serializers.order import OrderPositionSerializer
from pretix.api.views import RichOrderingFilter
from pretix.api.views.order import OrderPositionFilter
from pretix.base.models import Checkin, CheckinList, Order, OrderPosition
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, perform_checkin,
)
@@ -49,7 +48,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.checkinlist.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -63,7 +62,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.checkinlist.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -71,7 +70,7 @@ class CheckinListViewSet(viewsets.ModelViewSet):
instance.log_action(
'pretix.event.checkinlist.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
)
super().perform_destroy(instance)
+8 -9
View File
@@ -12,7 +12,6 @@ from pretix.api.serializers.event import (
from pretix.api.views import ConditionalListView
from pretix.base.models import Event, ItemCategory, TaxRule
from pretix.base.models.event import SubEvent
from pretix.base.models.organizer import TeamAPIToken
from pretix.helpers.dicts import merge_dicts
@@ -39,7 +38,7 @@ class EventViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
log_action,
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -52,7 +51,7 @@ class EventViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.plugins.' + action,
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data={'plugin': module}
)
@@ -61,7 +60,7 @@ class EventViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -70,7 +69,7 @@ class EventViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -115,7 +114,7 @@ class CloneEventViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -151,7 +150,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.taxrule.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -160,7 +159,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.taxrule.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -171,6 +170,6 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
instance.log_action(
'pretix.event.taxrule.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
)
super().perform_destroy(instance)
+26 -27
View File
@@ -18,7 +18,6 @@ from pretix.base.models import (
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
Quota,
)
from pretix.base.models.organizer import TeamAPIToken
from pretix.helpers.dicts import merge_dicts
@@ -54,7 +53,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.item.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -69,7 +68,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.item.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -82,7 +81,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
instance.log_action(
'pretix.event.item.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
)
super().perform_destroy(instance)
@@ -114,7 +113,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
item.log_action(
'pretix.event.item.variation.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk},
{'value': serializer.instance.value})
)
@@ -124,7 +123,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
serializer.instance.item.log_action(
'pretix.event.item.variation.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk},
{'value': serializer.instance.value})
)
@@ -141,7 +140,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
instance.item.log_action(
'pretix.event.item.variation.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data={
'value': instance.value,
'id': self.kwargs['pk']
@@ -175,7 +174,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
item.log_action(
'pretix.event.item.addons.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
@@ -184,7 +183,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
serializer.instance.base_item.log_action(
'pretix.event.item.addons.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
@@ -193,7 +192,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
instance.base_item.log_action(
'pretix.event.item.addons.removed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data={'category': instance.addon_category.pk}
)
@@ -222,7 +221,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.category.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -236,7 +235,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.category.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -247,7 +246,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
instance.log_action(
'pretix.event.category.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
)
super().perform_destroy(instance)
@@ -275,7 +274,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.question.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -289,7 +288,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.question.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -297,7 +296,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
instance.log_action(
'pretix.event.question.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
)
super().perform_destroy(instance)
@@ -327,7 +326,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
q.log_action(
'pretix.event.question.option.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
@@ -336,7 +335,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
serializer.instance.question.log_action(
'pretix.event.question.option.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=merge_dicts(self.request.data, {'ORDER': serializer.instance.position}, {'id': serializer.instance.pk})
)
@@ -344,7 +343,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
instance.question.log_action(
'pretix.event.question.option.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data={'id': instance.pk}
)
super().perform_destroy(instance)
@@ -374,14 +373,14 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.quota.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
if serializer.instance.subevent:
serializer.instance.subevent.log_action(
'pretix.subevent.quota.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -397,7 +396,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.quota.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
if current_subevent == request_subevent:
@@ -405,7 +404,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
current_subevent.log_action(
'pretix.subevent.quota.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
else:
@@ -413,14 +412,14 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
request_subevent.log_action(
'pretix.subevent.quota.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
if current_subevent is not None:
current_subevent.log_action(
'pretix.subevent.quota.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
)
serializer.instance.rebuild_cache()
@@ -428,13 +427,13 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
instance.log_action(
'pretix.event.quota.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
)
if instance.subevent:
instance.subevent.log_action(
'pretix.subevent.quota.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
)
super().perform_destroy(instance)
+92
View File
@@ -0,0 +1,92 @@
import logging
from django import forms
from django.conf import settings
from django.utils.translation import ugettext as _
from oauth2_provider.exceptions import OAuthToolkitError
from oauth2_provider.forms import AllowForm
from oauth2_provider.views import (
AuthorizationView as BaseAuthorizationView,
RevokeTokenView as BaseRevokeTokenView, TokenView as BaseTokenView,
)
from pretix.api.models import OAuthApplication
from pretix.base.models import Organizer
logger = logging.getLogger(__name__)
class OAuthAllowForm(AllowForm):
organizers = forms.ModelMultipleChoiceField(
queryset=Organizer.objects.none(),
widget=forms.CheckboxSelectMultiple
)
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['organizers'].queryset = Organizer.objects.filter(
pk__in=user.teams.values_list('organizer', flat=True))
class AuthorizationView(BaseAuthorizationView):
template_name = "pretixcontrol/auth/oauth_authorization.html"
form_class = OAuthAllowForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['settings'] = settings
return ctx
def create_authorization_response(self, request, scopes, credentials, allow, organizers):
credentials["organizers"] = organizers
return super().create_authorization_response(request, scopes, credentials, allow)
def form_valid(self, form):
client_id = form.cleaned_data["client_id"]
application = OAuthApplication.objects.get(client_id=client_id)
credentials = {
"client_id": form.cleaned_data.get("client_id"),
"redirect_uri": form.cleaned_data.get("redirect_uri"),
"response_type": form.cleaned_data.get("response_type", None),
"state": form.cleaned_data.get("state", None),
}
scopes = form.cleaned_data.get("scope")
allow = form.cleaned_data.get("allow")
try:
uri, headers, body, status = self.create_authorization_response(
request=self.request, scopes=scopes, credentials=credentials, allow=allow,
organizers=form.cleaned_data.get("organizers")
)
except OAuthToolkitError as error:
return self.error_response(error, application)
self.success_url = uri
logger.debug("Success url for the request: {0}".format(self.success_url))
msgs = [
_('The application "{application_name}" has been authorized to access your account.').format(
application_name=application.name
)
]
self.request.user.send_security_notice(msgs)
self.request.user.log_action('pretix.user.oauth.authorized', user=self.request.user, data={
'application_id': application.pk,
'application_name': application.name,
})
return self.redirect(self.success_url, application)
class TokenView(BaseTokenView):
pass
class RevokeTokenView(BaseRevokeTokenView):
pass
+13 -10
View File
@@ -17,12 +17,14 @@ from rest_framework.filters import OrderingFilter
from rest_framework.mixins import CreateModelMixin
from rest_framework.response import Response
from pretix.api.models import OAuthAccessToken
from pretix.api.serializers.order import (
InvoiceSerializer, OrderCreateSerializer, OrderPositionSerializer,
OrderSerializer,
)
from pretix.base.models import Invoice, Order, OrderPosition, Quota
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.models import (
Invoice, Order, OrderPosition, Quota, TeamAPIToken,
)
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
regenerate_invoice,
@@ -124,7 +126,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
mark_order_paid(
order, manual=True,
user=request.user if request.user.is_authenticated else None,
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
auth=request.auth,
)
except Quota.QuotaExceededException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
@@ -151,7 +153,8 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
cancel_order(
order,
user=request.user if request.user.is_authenticated else None,
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
send_mail=send_mail
)
return self.retrieve(request, [], **kwargs)
@@ -172,7 +175,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
order.log_action(
'pretix.event.order.unpaid',
user=request.user if request.user.is_authenticated else None,
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
auth=request.auth,
)
return self.retrieve(request, [], **kwargs)
@@ -189,7 +192,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
mark_order_expired(
order,
user=request.user if request.user.is_authenticated else None,
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
auth=request.auth,
)
return self.retrieve(request, [], **kwargs)
@@ -243,7 +246,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
new_date=new_date,
force=force,
user=request.user if request.user.is_authenticated else None,
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
auth=request.auth,
)
return self.retrieve(request, [], **kwargs)
except OrderError as e:
@@ -263,7 +266,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
order.log_action(
'pretix.event.order.placed',
user=request.user if request.user.is_authenticated else None,
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
auth=request.auth,
)
order_placed.send(self.request.event, order=order)
@@ -437,7 +440,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
'invoice': inv.pk
},
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
)
return Response(status=204)
@@ -460,6 +463,6 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
'invoice': inv.pk
},
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
)
return Response(status=204)
+7
View File
@@ -1,5 +1,6 @@
from rest_framework import viewsets
from pretix.api.models import OAuthAccessToken
from pretix.api.serializers.organizer import OrganizerSerializer
from pretix.base.models import Organizer
@@ -14,6 +15,12 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
if self.request.user.is_authenticated():
if self.request.user.has_active_staff_session(self.request.session.session_key):
return Organizer.objects.all()
elif isinstance(self.request.auth, OAuthAccessToken):
return Organizer.objects.filter(
pk__in=self.request.user.teams.values_list('organizer', flat=True)
).filter(
pk__in=self.request.auth.organizers.values_list('pk', flat=True)
)
else:
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
else:
+3 -4
View File
@@ -9,7 +9,6 @@ from rest_framework.filters import OrderingFilter
from pretix.api.serializers.voucher import VoucherSerializer
from pretix.base.models import Voucher
from pretix.base.models.organizer import TeamAPIToken
class VoucherFilter(FilterSet):
@@ -51,7 +50,7 @@ class VoucherViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.voucher.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -69,7 +68,7 @@ class VoucherViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.voucher.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
data=self.request.data
)
@@ -80,6 +79,6 @@ class VoucherViewSet(viewsets.ModelViewSet):
instance.log_action(
'pretix.voucher.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
)
super().perform_destroy(instance)
+5 -5
View File
@@ -7,7 +7,7 @@ from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
from pretix.api.serializers.waitinglist import WaitingListSerializer
from pretix.base.models import TeamAPIToken, WaitingListEntry
from pretix.base.models import WaitingListEntry
from pretix.base.models.waitinglist import WaitingListException
@@ -45,7 +45,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.orders.waitinglist.added',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
)
def perform_update(self, serializer):
@@ -55,7 +55,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
serializer.instance.log_action(
'pretix.event.orders.waitinglist.changed',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
)
def perform_destroy(self, instance):
@@ -65,7 +65,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
instance.log_action(
'pretix.event.orders.waitinglist.deleted',
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
)
super().perform_destroy(instance)
@@ -74,7 +74,7 @@ class WaitingListViewSet(viewsets.ModelViewSet):
try:
self.get_object().send_voucher(
user=self.request.user,
api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None),
auth=self.request.auth,
)
except WaitingListException as e:
raise ValidationError(str(e))
@@ -21,7 +21,6 @@ def set_pids(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0092_auto_20180511_1224'),
]
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-06-04 11:19
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0093_auto_20180528_1432'),
('pretixapi', '0001_initial')
]
operations = [
]
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-06-04 11:29
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0094_auto_20180604_1119'),
]
operations = [
migrations.AddField(
model_name='logentry',
name='oauth_application',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
to='pretixapi.OAuthApplication'),
),
]
+2 -2
View File
@@ -25,13 +25,13 @@ class UserManager(BaseUserManager):
model documentation to see what's so special about our user model.
"""
def create_user(self, email: str, password: str=None, **kwargs):
def create_user(self, email: str, password: str = None, **kwargs):
user = self.model(email=email, **kwargs)
user.set_password(password)
user.save()
return user
def create_superuser(self, email: str, password: str=None): # NOQA
def create_superuser(self, email: str, password: str = None): # NOQA
# Not used in the software but required by Django
if password is None:
raise Exception("You must provide a password")
+16 -3
View File
@@ -36,7 +36,7 @@ def cached_file_delete(sender, instance, **kwargs):
class LoggingMixin:
def log_action(self, action, data=None, user=None, api_token=None, save=True):
def log_action(self, action, data=None, user=None, api_token=None, auth=None, save=True):
"""
Create a LogEntry object that is related to this object.
See the LogEntry documentation for details.
@@ -47,6 +47,8 @@ class LoggingMixin:
"""
from .log import LogEntry
from .event import Event
from pretix.api.models import OAuthAccessToken, OAuthApplication
from .organizer import TeamAPIToken
from ..notifications import get_all_notification_types
from ..services.notifications import notify
@@ -57,7 +59,18 @@ class LoggingMixin:
event = self.event
if user and not user.is_authenticated:
user = None
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event, api_token=api_token)
kwargs = {}
if isinstance(auth, OAuthAccessToken):
kwargs['oauth_application'] = auth.application
elif isinstance(auth, OAuthApplication):
kwargs['oauth_application'] = auth
elif isinstance(auth, TeamAPIToken):
kwargs['api_token'] = auth
elif isinstance(api_token, TeamAPIToken):
kwargs['api_token'] = api_token
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event, **kwargs)
if data:
logentry.data = json.dumps(data, cls=CustomJSONEncoder)
if save:
@@ -83,4 +96,4 @@ class LoggedModel(models.Model, LoggingMixin):
return LogEntry.objects.filter(
content_type=ContentType.objects.get_for_model(type(self)), object_id=self.pk
).select_related('user', 'event')
).select_related('user', 'event', 'oauth_application', 'api_token')
+1
View File
@@ -41,6 +41,7 @@ class LogEntry(models.Model):
datetime = models.DateTimeField(auto_now_add=True, db_index=True)
user = models.ForeignKey('User', null=True, blank=True, on_delete=models.PROTECT)
api_token = models.ForeignKey('TeamAPIToken', null=True, blank=True, on_delete=models.PROTECT)
oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT)
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.SET_NULL)
action_type = models.CharField(max_length=255)
data = models.TextField(default='{}')
+3 -3
View File
@@ -77,7 +77,7 @@ class WaitingListEntry(LoggedModel):
WaitingListEntry.clean_itemvar(self.event, self.item, self.variation)
WaitingListEntry.clean_subevent(self.event, self.subevent)
def send_voucher(self, quota_cache=None, user=None, api_token=None):
def send_voucher(self, quota_cache=None, user=None, auth=None):
availability = (
self.variation.check_quotas(count_waitinglist=False, subevent=self.subevent, _cache=quota_cache)
if self.variation
@@ -114,8 +114,8 @@ class WaitingListEntry(LoggedModel):
'email': self.email,
'waitinglistentry': self.pk,
'subevent': self.subevent.pk if self.subevent else None,
}, user=user, api_token=api_token)
self.log_action('pretix.waitinglist.voucher', user=user, api_token=api_token)
}, user=user, auth=auth)
self.log_action('pretix.waitinglist.voucher', user=user, auth=auth)
self.voucher = v
self.save()
+14 -14
View File
@@ -16,6 +16,7 @@ from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from pretix.api.models import OAuthApplication
from pretix.base.i18n import (
LazyCurrencyNumber, LazyDate, LazyLocaleException, LazyNumber, language,
)
@@ -80,7 +81,7 @@ logger = logging.getLogger(__name__)
def mark_order_paid(order: Order, provider: str=None, info: str=None, date: datetime=None, manual: bool=None,
force: bool=False, send_mail: bool=True, user: User=None, mail_text='',
count_waitinglist=True, api_token=None) -> Order:
count_waitinglist=True, auth=None) -> Order:
"""
Marks an order as paid. This sets the payment provider, info and date and returns
the order object.
@@ -123,7 +124,7 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
'date': date or now_dt,
'manual': manual,
'force': force
}, user=user, api_token=api_token)
}, user=user, auth=auth)
order_paid.send(order.event, order=order)
invoice = None
@@ -173,7 +174,7 @@ def mark_order_paid(order: Order, provider: str=None, info: str=None, date: date
return order
def extend_order(order: Order, new_date: datetime, force: bool=False, user: User=None, api_token=None):
def extend_order(order: Order, new_date: datetime, force: bool=False, user: User=None, auth=None):
"""
Extends the deadline of an order. If the order is already expired, the quota will be checked to
see if this is actually still possible. If ``force`` is set to ``True``, the result of this check
@@ -187,7 +188,7 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User
order.log_action(
'pretix.event.order.expirychanged',
user=user,
api_token=api_token,
auth=auth,
data={
'expires': order.expires,
'state_change': False
@@ -203,7 +204,7 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User
order.log_action(
'pretix.event.order.expirychanged',
user=user,
api_token=api_token,
auth=auth,
data={
'expires': order.expires,
'state_change': True
@@ -237,24 +238,21 @@ def mark_order_refunded(order, user=None, api_token=None):
@transaction.atomic
def mark_order_expired(order, user=None, api_token=None):
def mark_order_expired(order, user=None, auth=None):
"""
Mark this order as expired. This sets the payment status and returns the order object.
:param order: The order to change
:param user: The user that performed the change
:param api_token: The API token used to performed the change
"""
if isinstance(order, int):
order = Order.objects.get(pk=order)
if isinstance(user, int):
user = User.objects.get(pk=user)
if isinstance(api_token, int):
api_token = TeamAPIToken.objects.get(pk=api_token)
with order.event.lock():
order.status = Order.STATUS_EXPIRED
order.save()
order.log_action('pretix.event.order.expired', user=user, api_token=api_token)
order.log_action('pretix.event.order.expired', user=user, auth=auth)
i = order.invoices.filter(is_cancellation=False).last()
if i:
generate_cancellation(i)
@@ -263,7 +261,7 @@ def mark_order_expired(order, user=None, api_token=None):
@transaction.atomic
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None):
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, oauth_application=None):
"""
Mark this order as canceled
:param order: The order to change
@@ -275,13 +273,15 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None):
user = User.objects.get(pk=user)
if isinstance(api_token, int):
api_token = TeamAPIToken.objects.get(pk=api_token)
if isinstance(oauth_application, int):
oauth_application = OAuthApplication.objects.get(pk=oauth_application)
with order.event.lock():
if not order.cancel_allowed():
raise OrderError(_('You cannot cancel this order.'))
order.status = Order.STATUS_CANCELED
order.save()
order.log_action('pretix.event.order.canceled', user=user, api_token=api_token)
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application)
i = order.invoices.filter(is_cancellation=False).last()
if i:
generate_cancellation(i)
@@ -1163,10 +1163,10 @@ def perform_order(self, event: str, payment_provider: str, positions: List[str],
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None):
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None):
try:
try:
return _cancel_order(order, user, send_mail, api_token)
return _cancel_order(order, user, send_mail, api_token, oauth_application)
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):
+2
View File
@@ -197,6 +197,8 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.user.settings.notifications.enabled': _('Notifications have been enabled.'),
'pretix.user.settings.notifications.disabled': _('Notifications have been disabled.'),
'pretix.user.settings.notifications.changed': _('Your notification settings have been changed.'),
'pretix.user.oauth.authorized': _('The application "{application_name}" has been authorized to access your '
'account.'),
'pretix.control.auth.user.forgot_password.mail_sent': _('Password reset mail sent.'),
'pretix.control.auth.user.forgot_password.recovered': _('The password has been reset.'),
'pretix.voucher.added': _('The voucher has been created.'),
@@ -0,0 +1,51 @@
{% extends "pretixcontrol/auth/base.html" %}
{% load bootstrap3 %}
{% load staticfiles %}
{% load i18n %}
{% block content %}
{% if not error %}
<form class="form-signin" action="" method="post">
<h3>{% trans "Authorize an application" %}</h3>
{% csrf_token %}
{% for field in form %}
{% if field.is_hidden %}
{{ field }}
{% endif %}
{% endfor %}
<p>
{% blocktrans trimmed with application=application.name %}
Do you really want to grant the application <strong>{{ application }}</strong> access to your
pretix account?
{% endblocktrans %}
</p>
<p>{% trans "The application requires the following permissions:" %}</p>
<ul>
{% for scope in scopes_descriptions %}
<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" %}
{% bootstrap_form_errors form layout="control" %}
<p class="text-danger">
{% blocktrans trimmed %}
This application has <strong>not</strong> been reviewed by the pretix team. Granting access to your
pretix account happens at your own risk.
{% endblocktrans %}
</p>
<div class="form-group buttons">
<input type="submit" class="btn btn-large btn-default" value="Cancel"/>
<input type="submit" class="btn btn-large btn-primary" name="allow" value="Authorize"/>
</div>
</form>
{% else %}
<form class="form-signin" action="" method="post">
<h3>{% trans "Error:" %} {{ error.error }}</h3>
<p>{{ error.description }}</p>
</form>
{% endif %}
{% endblock %}
@@ -1,4 +1,4 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
this is to inform you that the account information of your pretix account has been
changed. In particular, the following changes have been performed:
@@ -124,6 +124,13 @@
<span class="fa fa-user fa-fw"></span>
{% endif %}
{{ log.user.get_full_name }}
{% if log.oauth_application %}
<br><span class="fa fa-plug fa-fw"></span>
{{ log.oauth_application.name }}
{% endif %}
{% elif log.api_token %}
<span class="fa fa-key fa-fw"></span>
{{ log.api_token.name }}
{% endif %}
</div>
<div class="col-lg-2 col-sm-12 col-xs-12">
@@ -16,7 +16,8 @@
</option>
{% for up in userlist %}
{% if up.user__id %}
<option value="{{ up.user__id }}" {% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
<option value="{{ up.user__id }}"
{% if request.GET.user == up.user__id %}selected="selected"{% endif %}>
{{ up.user__email }}
</option>
{% endif %}
@@ -42,13 +43,20 @@
{% if log.user %}
{% if log.user.is_staff %}
<span class="fa fa-id-card fa-danger fa-fw"
data-toggle="tooltip"
title="{% trans "This change was performed by a pretix administrator." %}">
data-toggle="tooltip"
title="{% trans "This change was performed by a pretix administrator." %}">
</span>
{% else %}
<span class="fa fa-user fa-fw"></span>
{% endif %}
{{ log.user.get_full_name }}
{% if log.oauth_application %}
<br><span class="fa fa-plug fa-fw"></span>
{{ log.oauth_application.name }}
{% endif %}
{% elif log.api_token %}
<span class="fa fa-key fa-fw"></span>
{{ log.api_token.name }}
{% endif %}
</div>
<div class="col-lg-2 col-sm-12 col-xs-12">
@@ -61,7 +69,7 @@
</div>
</div>
</li>
{% empty %}
{% empty %}
<div class="list-group-item">
<em>{% trans "No results" %}</em>
</div>
@@ -15,6 +15,13 @@
<span class="fa fa-user fa-fw"></span>
{% endif %}
{{ log.user.get_full_name }}
{% if log.oauth_application %}
<span class="fa fa-plug fa-fw"></span>
{{ log.oauth_application.name }}
{% endif %}
{% elif log.api_token %}
<span class="fa fa-key fa-fw"></span>
{{ log.api_token.name }}
{% endif %}
{% if log.shredded %}
<span class="fa fa-eraser fa-danger fa-fw"
@@ -0,0 +1,19 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Disable application" %}{% endblock %}
{% block content %}
<h1>{% trans "Disable application" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans %}Are you sure you want to disable the application <strong>{{ application }}</strong> permanently?{% endblocktrans %}</p>
<div class="form-group submit-group">
<a href="{% url "control:user.settings.oauth.apps" %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Disable" %}
</button>
</div>
</form>
{% endblock %}
@@ -0,0 +1,50 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Your applications" %}{% endblock %}
{% block content %}
<h1>{% trans "Your applications" %}</h1>
{% if applications %}
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th class="action-col-2"></th>
</tr>
</thead>
<tbody>
{% for application in applications %}
<tr>
<td><strong><a href="{% url "control:user.settings.oauth.app" pk=application.pk %}">{{ application.name }}</a></strong></td>
<td class="text-right">
<a href="{% url "control:user.settings.oauth.app" pk=application.pk %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:user.settings.oauth.app.roll" pk=application.pk %}" class="btn btn-default btn-sm"><i class="fa fa-repeat"></i></a>
<a href="{% url "control:user.settings.oauth.app.disable" pk=application.pk %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p>
<a class="btn btn-primary" href="{% url "control:user.settings.oauth.apps.register" %}">
<span class="fa fa-plus"></span>
{% trans "Create new application" %}
</a>
</p>
{% else %}
<div class="empty-collection">
<p>
{% blocktrans trimmed %}
No applications registered yet.
{% endblocktrans %}
</p>
<a href="{% url "control:user.settings.oauth.apps.register" %}"
class="btn btn-primary btn-lg">
{% trans "Register a new application" %}
</a>
</div>
{% endif %}
{% endblock %}
@@ -0,0 +1,16 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Register a new application" %}{% endblock %}
{% block content %}
<h1>{% trans "Register a new application" %}</h1>
<form class="form-horizontal" method="post" action="">
{% csrf_token %}
{% bootstrap_form form layout='control' %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}
@@ -0,0 +1,19 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Generate new application secret" %}{% endblock %}
{% block content %}
<h1>{% trans "Generate new application secret" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans %}Are you sure you want to generate a new client secret for the application <strong>{{ application }}</strong>?{% endblocktrans %}</p>
<div class="form-group submit-group">
<a href="{% url "control:user.settings.oauth.apps" %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Roll secret" %}
</button>
</div>
</form>
{% endblock %}
@@ -0,0 +1,16 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Update an application" %}{% endblock %}
{% block content %}
<h1>{% trans "Update an application" %}</h1>
<form class="form-horizontal" method="post" action="">
{% csrf_token %}
{% bootstrap_form form layout='control' %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}
@@ -0,0 +1,19 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Revoke access" %}{% endblock %}
{% block content %}
<h1>{% trans "Revoke access" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans %}Are you sure you want to revoke access to your account for the application <strong>{{ application }}</strong>?{% endblocktrans %}</p>
<div class="form-group submit-group">
<a href="{% url "control:user.settings.oauth.list" %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Revoke" %}
</button>
</div>
</form>
{% endblock %}
@@ -0,0 +1,65 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Authorized applications" %}{% endblock %}
{% block content %}
<h1>{% trans "Authorized applications" %}</h1>
<p>
<a href="{% url "control:user.settings.oauth.apps" %}" class="btn btn-default">
{% trans "Manage your own apps" %}
</a>
</p>
{% if tokens %}
<div class="table-responsive">
<table class="table table-condensed table-hover table-quotas">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Permissions" %}</th>
<th>{% trans "Organizers" %}</th>
<th class="action-col-2"></th>
</tr>
</thead>
<tbody>
{% for token in tokens %}
<tr>
<td><strong>{{ token.application.name }}</strong></td>
<td>
<ul>
{% for scope in token.scopes_descriptions %}
<li>
{{ scope }}
</li>
{% endfor %}
</ul>
</td>
<td>
<ul>
{% for o in token.organizers.all %}
<li>
<a href="{% url "control:organizer" organizer=o.slug %}">
{{ o.name }}
</a>
</li>
{% endfor %}
</ul>
</td>
<td class="text-right">
<a href="{% url "control:user.settings.oauth.revoke" pk=token.pk %}"
class="btn btn-danger btn-sm">{% trans "Revoke access" %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-collection">
<p>
{% blocktrans trimmed %}
No applications have access to your pretix account.
{% endblocktrans %}
</p>
</div>
{% endif %}
{% endblock %}
@@ -54,7 +54,16 @@
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label" for="id_new_pw_repeat">{% trans "Account history" %}</label>
<label class="col-md-3 control-label" for="">{% trans "Authorized applications" %}</label>
<div class="col-md-9 static-form-row">
<a href="{% url "control:user.settings.oauth.list" %}">
<span class="fa fa-plug"></span>
{% trans "Show applications" %}
</a>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label" for="">{% trans "Account history" %}</label>
<div class="col-md-9 static-form-row">
<a href="{% url "control:user.settings.history" %}">
<span class="fa fa-history"></span>
+17 -3
View File
@@ -1,9 +1,9 @@
from django.conf.urls import include, url
from pretix.control.views import (
auth, checkin, dashboards, event, global_settings, item, main, orders,
organizer, pdf, search, shredder, subevents, typeahead, user, users,
vouchers, waitinglist,
auth, checkin, dashboards, event, global_settings, item, main, oauth,
orders, organizer, pdf, search, shredder, subevents, typeahead, user,
users, vouchers, waitinglist,
)
urlpatterns = [
@@ -35,6 +35,20 @@ urlpatterns = [
url(r'^settings/notifications/$', user.UserNotificationsEditView.as_view(), name='user.settings.notifications'),
url(r'^settings/notifications/off/(?P<id>\d+)/(?P<token>[^/]+)/$', user.UserNotificationsDisableView.as_view(),
name='user.settings.notifications.off'),
url(r'^settings/oauth/authorized/$', oauth.AuthorizationListView.as_view(),
name='user.settings.oauth.list'),
url(r'^settings/oauth/authorized/(?P<pk>\d+)/revoke$', oauth.AuthorizationRevokeView.as_view(),
name='user.settings.oauth.revoke'),
url(r'^settings/oauth/apps/$', oauth.OAuthApplicationListView.as_view(),
name='user.settings.oauth.apps'),
url(r'^settings/oauth/apps/add$', oauth.OAuthApplicationRegistrationView.as_view(),
name='user.settings.oauth.apps.register'),
url(r'^settings/oauth/apps/(?P<pk>\d+)/$', oauth.OAuthApplicationUpdateView.as_view(),
name='user.settings.oauth.app'),
url(r'^settings/oauth/apps/(?P<pk>\d+)/disable$', oauth.OAuthApplicationDeleteView.as_view(),
name='user.settings.oauth.app.disable'),
url(r'^settings/oauth/apps/(?P<pk>\d+)/roll$', oauth.OAuthApplicationRollView.as_view(),
name='user.settings.oauth.app.roll'),
url(r'^settings/2fa/$', user.User2FAMainView.as_view(), name='user.settings.2fa'),
url(r'^settings/2fa/add$', user.User2FADeviceAddView.as_view(), name='user.settings.2fa.add'),
url(r'^settings/2fa/enable', user.User2FAEnableView.as_view(), name='user.settings.2fa.enable'),
+1 -1
View File
@@ -250,7 +250,7 @@ def event_index(request, organizer, event):
can_change_orders = request.user.has_event_permission(request.organizer, request.event, 'can_change_orders',
request=request)
qs = request.event.logentry_set.all().select_related('user', 'content_type').order_by('-datetime')
qs = request.event.logentry_set.all().select_related('user', 'content_type', 'api_token', 'oauth_application').order_by('-datetime')
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
if not request.user.has_event_permission(request.organizer, request.event, 'can_view_orders', request=request):
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))
+4 -1
View File
@@ -347,6 +347,7 @@ class EventSettingsFormView(EventPermissionRequiredMixin, FormView):
for k in form.changed_data
}
)
self.form_success()
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
else:
@@ -824,7 +825,9 @@ class EventLog(EventPermissionRequiredMixin, ListView):
paginate_by = 20
def get_queryset(self):
qs = self.request.event.logentry_set.all().select_related('user', 'content_type').order_by('-datetime')
qs = self.request.event.logentry_set.all().select_related(
'user', 'content_type', 'api_token', 'oauth_application'
).order_by('-datetime')
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_orders',
request=self.request):
+140
View File
@@ -0,0 +1,140 @@
import logging
from django import forms
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from django.views.generic import DetailView, ListView
from oauth2_provider.generators import generate_client_secret
from oauth2_provider.models import get_application_model
from oauth2_provider.scopes import get_scopes_backend
from oauth2_provider.views import (
ApplicationDelete, ApplicationDetail, ApplicationList,
ApplicationRegistration, ApplicationUpdate,
)
from pretix.api.models import (
OAuthAccessToken, OAuthApplication, OAuthRefreshToken,
)
logger = logging.getLogger(__name__)
class OAuthApplicationListView(ApplicationList):
template_name = 'pretixcontrol/oauth/app_list.html'
def get_queryset(self):
return super().get_queryset().filter(active=True)
class OAuthApplicationRegistrationView(ApplicationRegistration):
template_name = 'pretixcontrol/oauth/app_register.html'
def get_form_class(self):
return forms.modelform_factory(
get_application_model(),
fields=(
"name", "redirect_uris"
)
)
def form_valid(self, form):
form.instance.client_type = 'confidential'
form.instance.authorization_grant_type = 'authorization-code'
return super().form_valid(form)
class ApplicationUpdateForm(forms.ModelForm):
class Meta:
model = OAuthApplication
fields = ("name", "client_id", "client_secret", "redirect_uris")
def clean_client_id(self):
return self.instance.client_id
def clean_client_secret(self):
return self.instance.client_secret
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['client_id'].widget.attrs['readonly'] = True
self.fields['client_secret'].widget.attrs['readonly'] = True
class OAuthApplicationUpdateView(ApplicationUpdate):
template_name = 'pretixcontrol/oauth/app_update.html'
def get_form_class(self):
return ApplicationUpdateForm
def get_queryset(self):
return super().get_queryset().filter(active=True)
class OAuthApplicationRollView(ApplicationDetail):
template_name = 'pretixcontrol/oauth/app_rollkeys.html'
def post(self, request, *args, **kwargs):
self.object = self.get_object()
messages.success(request, _('A new client secret has been generated and is now effective.'))
self.object.client_secret = generate_client_secret()
self.object.save()
return HttpResponseRedirect(self.object.get_absolute_url())
def get_queryset(self):
return super().get_queryset().filter(active=True)
class OAuthApplicationDeleteView(ApplicationDelete):
template_name = 'pretixcontrol/oauth/app_delete.html'
success_url = reverse_lazy("control:user.settings.oauth.apps")
def get_queryset(self):
return super().get_queryset().filter(active=True)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.active = False
self.object.save()
return HttpResponseRedirect(self.success_url)
class AuthorizationListView(ListView):
template_name = 'pretixcontrol/oauth/authorized.html'
context_object_name = 'tokens'
def get_queryset(self):
return OAuthAccessToken.objects.filter(
user=self.request.user
).select_related('application').prefetch_related('organizers')
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
all_scopes = get_scopes_backend().get_all_scopes()
for t in ctx['tokens']:
t.scopes_descriptions = [all_scopes[scope] for scope in t.scopes]
return ctx
class AuthorizationRevokeView(DetailView):
template_name = 'pretixcontrol/oauth/auth_revoke.html'
success_url = reverse_lazy("control:user.settings.oauth.list")
def get_queryset(self):
return OAuthAccessToken.objects.filter(user=self.request.user)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['application'] = self.get_object().application
return ctx
def post(self, request, *args, **kwargs):
o = self.get_object()
for rt in OAuthRefreshToken.objects.filter(access_token=o):
rt.revoke()
o.delete()
messages.success(request, _('Access for the selected application has been revoked.'))
return redirect(self.success_url)
+17
View File
@@ -250,6 +250,7 @@ INSTALLED_APPS = [
'django_countries',
'hijack',
'compat',
'oauth2_provider',
]
try:
@@ -275,6 +276,7 @@ REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'pretix.api.auth.token.TeamTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
@@ -590,3 +592,18 @@ AUTH_PASSWORD_VALIDATORS = [
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
OAUTH2_PROVIDER_APPLICATION_MODEL = 'pretixapi.OAuthApplication'
OAUTH2_PROVIDER_GRANT_MODEL = 'pretixapi.OAuthGrant'
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = 'pretixapi.OAuthAccessToken'
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = 'pretixapi.OAuthRefreshToken'
OAUTH2_PROVIDER = {
'SCOPES': {
'read': _('Read access'),
'write': _('Write access'),
},
'OAUTH2_VALIDATOR_CLASS': 'pretix.api.oauth.Validator',
'ALLOWED_REDIRECT_URI_SCHEMES': ['https'] if not DEBUG else ['http', 'https'],
'ACCESS_TOKEN_EXPIRE_SECONDS': 3600 * 24,
'ROTATE_REFRESH_TOKEN': False,
}
@@ -98,6 +98,7 @@ div[data-formset-body], div[data-formset-form], div[data-nested-formset-form], d
list-style: none;
margin-left: 0;
padding-left: 0;
margin-bottom: 0;
}
.question-option-row {
+1
View File
@@ -33,6 +33,7 @@ raven
babel
django-i18nfield>=1.2.1
django-hijack==2.1.*
django-oauth-toolkit==1.1.*
# Stripe
stripe==1.79.*
# PayPal
+2 -1
View File
@@ -110,7 +110,8 @@ setup(
'pyuca',
'defusedcsv',
'vat_moss==0.11.0',
'django-hijack==2.1.*'
'django-hijack==2.1.*',
'django-oauth-toolkit==1.1.*',
],
extras_require={
'dev': [
+586
View File
@@ -0,0 +1,586 @@
import base64
import json
import pytest
from django.utils.http import urlquote
from django.utils.timezone import now
from pretix.api.models import (
OAuthAccessToken, OAuthApplication, OAuthGrant, OAuthRefreshToken,
)
from pretix.base.models import Event, Organizer, Team, User
@pytest.fixture
def organizer():
return Organizer.objects.create(name='Dummy', slug='dummy')
@pytest.fixture
def event(organizer):
event = Event.objects.create(
organizer=organizer, name='Dummy', slug='dummy',
date_from=now()
)
return event
@pytest.fixture
def admin_team(organizer):
return Team.objects.create(organizer=organizer, can_change_teams=True, name='Admin team', all_events=True,
can_create_events=True)
@pytest.fixture
def admin_user(admin_team):
u = User.objects.create_user('dummy@dummy.dummy', 'dummy')
admin_team.members.add(u)
return u
@pytest.fixture
def application():
return OAuthApplication.objects.create(
name="pretalx",
redirect_uris="https://pretalx.com",
client_type='confidential',
authorization_grant_type='authorization-code'
)
@pytest.mark.django_db
def test_authorize_require_login(client, application: OAuthApplication):
resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s' % (
application.client_id, urlquote('https://example.org')
))
assert resp.status_code == 302
assert resp['Location'].startswith('/control/login')
@pytest.mark.django_db
def test_authorize_invalid_redirect_uri(client, admin_user, application: OAuthApplication):
client.login(email='dummy@dummy.dummy', password='dummy')
resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s' % (
application.client_id, urlquote('https://example.org')
))
assert resp.status_code == 400
@pytest.mark.django_db
def test_authorize_missing_response_type(client, admin_user, application: OAuthApplication):
client.login(email='dummy@dummy.dummy', password='dummy')
resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s' % (
application.client_id, urlquote(application.redirect_uris)
))
assert resp.status_code == 302
assert resp['Location'] == 'https://pretalx.com?error=invalid_request&error_description=Missing+response_type+parameter.'
@pytest.mark.django_db
def test_authorize_require_organizer(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' % (
application.client_id, urlquote(application.redirect_uris)
))
assert resp.status_code == 200
resp = client.post('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % (
application.client_id, urlquote(application.redirect_uris)
), data={
'redirect_uri': application.redirect_uris,
'scope': 'read write',
'client_id': application.client_id,
'response_type': 'code',
'allow': 'Authorize',
})
assert resp.status_code == 200
@pytest.mark.django_db
def test_authorize_denied(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' % (
application.client_id, urlquote(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': 'read write',
'client_id': application.client_id,
'response_type': 'code',
})
assert resp.status_code == 302
assert resp['Location'] == 'https://pretalx.com?error=access_denied'
@pytest.mark.django_db
def test_authorize_disallow_response_token(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=token' % (
application.client_id, urlquote(application.redirect_uris)
))
assert resp.status_code == 302
assert resp['Location'] == 'https://pretalx.com?error=unauthorized_client'
@pytest.mark.django_db
def test_authorize_read_scope(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' % (
application.client_id, urlquote(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': 'read',
'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]
grant = OAuthGrant.objects.get(code=code)
assert list(grant.organizers.all()) == [organizer]
assert grant.scope == "read"
@pytest.mark.django_db
def test_authorize_state(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&state=asdadf' % (
application.client_id, urlquote(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': 'read',
'client_id': application.client_id,
'response_type': 'code',
'allow': 'Authorize',
'state': 'asdadf'
})
assert resp.status_code == 302
assert 'state=asdadf' in resp['Location']
@pytest.mark.django_db
def test_authorize_default_scope(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' % (
application.client_id, urlquote(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': 'read write',
'client_id': application.client_id,
'response_type': 'code',
'allow': 'Authorize',
})
assert resp.status_code == 302
client.logout()
assert resp['Location'].startswith('https://pretalx.com?code=')
code = resp['Location'].split("=")[1]
grant = OAuthGrant.objects.get(code=code)
assert list(grant.organizers.all()) == [organizer]
assert grant.scope == "read write"
@pytest.mark.django_db
def test_token_from_code_without_auth(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' % (
application.client_id, urlquote(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': 'read write',
'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',
})
assert resp.status_code == 401
@pytest.mark.django_db
def test_token_from_code(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' % (
application.client_id, urlquote(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': 'read write',
'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())
assert data['expires_in'] == 86400
assert data['token_type'] == "Bearer"
assert data['scope'] == "read write"
access_token = data['access_token']
grant = OAuthAccessToken.objects.get(token=access_token)
assert list(grant.organizers.all()) == [organizer]
@pytest.mark.django_db
def test_use_token_for_access_one_organizer(client, admin_user, organizer, application: OAuthApplication):
o2 = Organizer.objects.create(name='A', slug='a')
t2 = Team.objects.create(organizer=o2, can_change_teams=True, name='Admin team', all_events=True)
t2.members.add(admin_user)
client.login(email='dummy@dummy.dummy', password='dummy')
resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % (
application.client_id, urlquote(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': 'read write',
'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 == 200
data = json.loads(resp.content.decode())
assert data == {'count': 1, 'next': None, 'previous': None, 'results': [{'name': 'Dummy', 'slug': 'dummy'}]}
resp = client.get('/api/v1/organizers/dummy/events/', HTTP_AUTHORIZATION='Bearer %s' % access_token)
assert resp.status_code == 200
resp = client.get('/api/v1/organizers/a/events/', HTTP_AUTHORIZATION='Bearer %s' % access_token)
assert resp.status_code == 403
@pytest.mark.django_db
def test_use_token_for_access_two_organizers(client, admin_user, organizer, application: OAuthApplication):
o2 = Organizer.objects.create(name='A', slug='a')
t2 = Team.objects.create(organizer=o2, can_change_teams=True, name='Admin team', all_events=True)
t2.members.add(admin_user)
client.login(email='dummy@dummy.dummy', password='dummy')
resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % (
application.client_id, urlquote(application.redirect_uris)
))
assert resp.status_code == 200
resp = client.post('/api/v1/oauth/authorize', data={
'organizers': [str(organizer.pk), str(o2.pk)],
'redirect_uri': application.redirect_uris,
'scope': 'read write',
'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 == 200
data = json.loads(resp.content.decode())
assert data == {'count': 2, 'next': None, 'previous': None, 'results': [
{'name': 'A', 'slug': 'a'},
{'name': 'Dummy', 'slug': 'dummy'},
]}
resp = client.get('/api/v1/organizers/dummy/events/', HTTP_AUTHORIZATION='Bearer %s' % access_token)
assert resp.status_code == 200
resp = client.get('/api/v1/organizers/a/events/', HTTP_AUTHORIZATION='Bearer %s' % access_token)
assert resp.status_code == 200
@pytest.mark.django_db
def test_token_refresh(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' % (
application.client_id, urlquote(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': 'read write',
'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())
refresh_token = data['refresh_token']
access_token = data['access_token']
resp = client.post('/api/v1/oauth/token', data={
'refresh_token': refresh_token,
'grant_type': 'refresh_token',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
assert resp.status_code == 200
assert not OAuthAccessToken.objects.filter(token=access_token).exists() # old token revoked
data = json.loads(resp.content.decode())
access_token = data['access_token']
grant = OAuthAccessToken.objects.get(token=access_token)
assert list(grant.organizers.all()) == [organizer]
@pytest.mark.django_db
def test_allow_write(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' % (
application.client_id, urlquote(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': 'read write',
'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.post('/api/v1/organizers/dummy/events/', HTTP_AUTHORIZATION='Bearer %s' % access_token)
assert resp.status_code == 400
@pytest.mark.django_db
def test_allow_read_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' % (
application.client_id, urlquote(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': 'read',
'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.post('/api/v1/organizers/dummy/events/', HTTP_AUTHORIZATION='Bearer %s' % access_token)
assert resp.status_code == 403
@pytest.mark.django_db
def test_token_revoke_refresh_token(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' % (
application.client_id, urlquote(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': 'read write',
'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())
refresh_token = data['refresh_token']
access_token = data['access_token']
resp = client.post('/api/v1/oauth/revoke_token', data={
'token': refresh_token,
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
assert resp.status_code == 200
assert not OAuthAccessToken.objects.get(token=access_token).is_valid()
assert not OAuthRefreshToken.objects.filter(token=refresh_token, revoked__isnull=True).exists()
resp = client.post('/api/v1/oauth/token', data={
'refresh_token': refresh_token,
'grant_type': 'refresh_token',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
assert resp.status_code == 401
@pytest.mark.django_db
def test_token_revoke_access_token(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' % (
application.client_id, urlquote(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': 'read write',
'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())
refresh_token = data['refresh_token']
access_token = data['access_token']
resp = client.post('/api/v1/oauth/revoke_token', data={
'token': access_token,
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
assert resp.status_code == 200
assert not OAuthAccessToken.objects.get(token=access_token).is_valid() # old token revoked
resp = client.post('/api/v1/oauth/token', data={
'refresh_token': refresh_token,
'grant_type': 'refresh_token',
}, 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']
grant = OAuthAccessToken.objects.get(token=access_token)
assert list(grant.organizers.all()) == [organizer]
@pytest.mark.django_db
def test_user_revoke(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' % (
application.client_id, urlquote(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': 'read write',
'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())
refresh_token = data['refresh_token']
access_token = data['access_token']
at = OAuthAccessToken.objects.get(token=access_token)
client.login(email='dummy@dummy.dummy', password='dummy')
resp = client.post('/control/settings/oauth/authorized/{}/revoke'.format(at.pk), data={
})
assert resp.status_code == 302
client.logout()
assert not OAuthAccessToken.objects.filter(token=access_token).exists()
assert OAuthRefreshToken.objects.get(token=refresh_token).revoked
resp = client.post('/api/v1/oauth/token', data={
'refresh_token': refresh_token,
'grant_type': 'refresh_token',
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
('%s:%s' % (application.client_id, application.client_secret)).encode()).decode())
assert resp.status_code == 401
+3
View File
@@ -73,6 +73,9 @@ def logged_in_client(client, event):
('/control/', 200),
('/control/settings/2fa/', 302),
('/control/settings/history/', 200),
('/control/settings/oauth/authorized/', 200),
('/control/settings/oauth/apps/', 200),
('/control/settings/oauth/apps/add', 200),
('/control/global/settings/', 200),
('/control/global/update/', 200),