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

View File

@@ -21,7 +21,6 @@ def set_pids(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0092_auto_20180511_1224'),
]

View File

@@ -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 = [
]

View File

@@ -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'),
),
]

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")

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')

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='{}')

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()

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):