mirror of
https://github.com/pretix/pretix.git
synced 2025-12-09 00:42:28 +00:00
Compare commits
3 Commits
api-create
...
oidc-pkce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5beda77f8 | ||
|
|
ef7f212063 | ||
|
|
26a5eab47f |
@@ -75,9 +75,8 @@ positions list of objects List of order p
|
||||
fees list of objects List of fees included in the order total. By default, only
|
||||
non-canceled fees are included.
|
||||
├ id integer Internal ID of the fee record
|
||||
├ fee_type string Type of fee (currently ``payment``, ``shipping``,
|
||||
``service``, ``cancellation``, ``insurance``, ``late``,
|
||||
``other``, ``giftcard``)
|
||||
├ fee_type string Type of fee (currently ``payment``, ``passbook``,
|
||||
``other``)
|
||||
├ value money (string) Fee amount
|
||||
├ description string Human-readable string with more details (can be empty)
|
||||
├ internal_type string Internal string (i.e. ID of the payment provider),
|
||||
@@ -110,7 +109,6 @@ cancellation_date datetime Time of order c
|
||||
Will not be set for partial cancellations and is not
|
||||
reliable for orders that have been cancelled,
|
||||
reactivated and cancelled again.
|
||||
plugin_data object Additional data added by plugins.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -166,10 +164,6 @@ plugin_data object Additional data
|
||||
|
||||
The ``tax_code`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2025.2
|
||||
|
||||
The ``plugin_data`` attribute has been added.
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
Order position resource
|
||||
@@ -257,7 +251,6 @@ seat objects The assigned se
|
||||
pdf_data object Data object required for ticket PDF generation. By default,
|
||||
this field is missing. It will be added only if you add the
|
||||
``pdf_data=true`` query parameter to your request.
|
||||
plugin_data object Additional data added by plugins.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 4.16
|
||||
@@ -272,10 +265,6 @@ plugin_data object Additional data
|
||||
|
||||
The ``tax_code`` attribute has been added.
|
||||
|
||||
.. versionchanged:: 2025.2
|
||||
|
||||
The ``plugin_data`` attribute has been added.
|
||||
|
||||
.. _order-payment-resource:
|
||||
|
||||
Order payment resource
|
||||
@@ -472,8 +461,7 @@ List of all orders
|
||||
"output": "pdf",
|
||||
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/"
|
||||
}
|
||||
],
|
||||
"plugin_data": {}
|
||||
]
|
||||
}
|
||||
],
|
||||
"downloads": [
|
||||
@@ -495,8 +483,7 @@ List of all orders
|
||||
}
|
||||
],
|
||||
"refunds": [],
|
||||
"cancellation_date": null,
|
||||
"plugin_data": {}
|
||||
"cancellation_date": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -715,8 +702,7 @@ Fetching individual orders
|
||||
"output": "pdf",
|
||||
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/"
|
||||
}
|
||||
],
|
||||
"plugin_data": {}
|
||||
]
|
||||
}
|
||||
],
|
||||
"downloads": [
|
||||
@@ -738,8 +724,7 @@ Fetching individual orders
|
||||
}
|
||||
],
|
||||
"refunds": [],
|
||||
"cancellation_date": null,
|
||||
"plugin_data": {}
|
||||
"cancellation_date": null
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -1686,8 +1671,7 @@ List of all order positions
|
||||
"output": "pdf",
|
||||
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/"
|
||||
}
|
||||
],
|
||||
"plugin_data": {}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1814,8 +1798,7 @@ Fetching individual positions
|
||||
"output": "pdf",
|
||||
"url": "https://pretix.eu/api/v1/organizers/bigevents/events/sampleconf/orderpositions/23442/download/pdf/"
|
||||
}
|
||||
],
|
||||
"plugin_data": {}
|
||||
]
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -2245,9 +2228,6 @@ otherwise, such as splitting an order or changing fees.
|
||||
|
||||
* ``cancel_fees``: A list of objects with the single key ``fee`` specifying an order fee ID.
|
||||
|
||||
* ``create_fees``: A list of objects describing new order fees with the fields ``fee_type``, ``value``, ``description``,
|
||||
``internal_type``, ``tax_rule``
|
||||
|
||||
* ``recalculate_taxes``: If set to ``"keep_net"``, all taxes will be recalculated based on the tax rule and invoice
|
||||
address, the net price will be kept. If set to ``"keep_gross"``, the gross price will be kept. If set to ``null``
|
||||
(the default) the taxes are not recalculated.
|
||||
@@ -2267,12 +2247,17 @@ otherwise, such as splitting an order or changing fees.
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"cancel_positions": [
|
||||
{
|
||||
"position": 12373
|
||||
}
|
||||
],
|
||||
"patch_positions": [
|
||||
{
|
||||
"position": 12374,
|
||||
"body": {
|
||||
"item": 12,
|
||||
"variation": null,
|
||||
"variation": None,
|
||||
"subevent": 562,
|
||||
"seat": "seat-guid-2",
|
||||
"price": "99.99",
|
||||
@@ -2280,11 +2265,6 @@ otherwise, such as splitting an order or changing fees.
|
||||
}
|
||||
}
|
||||
],
|
||||
"cancel_positions": [
|
||||
{
|
||||
"position": 12373
|
||||
}
|
||||
],
|
||||
"split_positions": [
|
||||
{
|
||||
"position": 12375
|
||||
@@ -2293,7 +2273,7 @@ otherwise, such as splitting an order or changing fees.
|
||||
"create_positions": [
|
||||
{
|
||||
"item": 12,
|
||||
"variation": null,
|
||||
"variation": None,
|
||||
"subevent": 562,
|
||||
"seat": "seat-guid-2",
|
||||
"price": "99.99",
|
||||
@@ -2301,26 +2281,17 @@ otherwise, such as splitting an order or changing fees.
|
||||
"attendee_name": "Peter",
|
||||
}
|
||||
],
|
||||
"patch_fees": [
|
||||
{
|
||||
"fee": 51,
|
||||
"body": {
|
||||
"value": "12.00"
|
||||
}
|
||||
}
|
||||
],
|
||||
"cancel_fees": [
|
||||
{
|
||||
"fee": 49
|
||||
}
|
||||
],
|
||||
"create_fees": [
|
||||
"change_fees": [
|
||||
{
|
||||
"fee_type": "other",
|
||||
"value": "1.50",
|
||||
"description": "Example Fee",
|
||||
"internal_type": "",
|
||||
"tax_rule": 15
|
||||
"fee": 51,
|
||||
"body": {
|
||||
"value": "12.00"
|
||||
}
|
||||
}
|
||||
],
|
||||
"reissue_invoice": true,
|
||||
|
||||
@@ -103,4 +103,4 @@ API
|
||||
|
||||
.. automodule:: pretix.api.signals
|
||||
:no-index:
|
||||
:members: register_device_security_profile, order_api_details, orderposition_api_details
|
||||
:members: register_device_security_profile
|
||||
|
||||
@@ -123,7 +123,7 @@ LANGUAGES_RTL = {
|
||||
'ar', 'hw'
|
||||
}
|
||||
LANGUAGES_INCUBATING = {
|
||||
'pt-br', 'gl',
|
||||
'fi', 'pt-br', 'gl',
|
||||
}
|
||||
LANGUAGES = ALL_LANGUAGES
|
||||
LOCALE_PATHS = [
|
||||
|
||||
@@ -46,7 +46,6 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.item import (
|
||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||
)
|
||||
from pretix.api.signals import order_api_details, orderposition_api_details
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
@@ -495,18 +494,6 @@ class OrderPositionListSerializer(serializers.ListSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class OrderPositionPluginDataField(serializers.Field):
|
||||
def to_representation(self, value: OrderPosition):
|
||||
d = {}
|
||||
if value and value.pk:
|
||||
for recv, resp in orderposition_api_details.send(
|
||||
sender=self.context.get("event") or value.order.event,
|
||||
orderposition=value
|
||||
):
|
||||
d.update(resp)
|
||||
return d
|
||||
|
||||
|
||||
class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
checkins = CheckinSerializer(many=True, read_only=True)
|
||||
print_logs = PrintLogSerializer(many=True, read_only=True)
|
||||
@@ -517,7 +504,6 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
seat = InlineSeatSerializer(read_only=True)
|
||||
country = CompatibleCountryField(source='*')
|
||||
attendee_name = serializers.CharField(required=False)
|
||||
plugin_data = OrderPositionPluginDataField(source='*', allow_null=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
list_serializer_class = OrderPositionListSerializer
|
||||
@@ -527,7 +513,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||
'print_logs', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled',
|
||||
'print_logs', 'downloads', 'answers', 'tax_rule', 'tax_code', 'pseudonymization_id', 'pdf_data', 'seat',
|
||||
'canceled', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use', 'plugin_data')
|
||||
'canceled', 'valid_from', 'valid_until', 'blocked', 'voucher_budget_use')
|
||||
read_only_fields = (
|
||||
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
|
||||
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'tax_code', 'pseudonymization_id',
|
||||
@@ -744,18 +730,6 @@ class OrderListSerializer(serializers.ListSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class OrderPluginDataField(serializers.Field):
|
||||
def to_representation(self, value: Order):
|
||||
d = {}
|
||||
if value and value.pk:
|
||||
for recv, resp in order_api_details.send(
|
||||
sender=self.context.get("event") or value.event,
|
||||
order=value
|
||||
):
|
||||
d.update(resp)
|
||||
return d
|
||||
|
||||
|
||||
class OrderSerializer(I18nAwareModelSerializer):
|
||||
event = SlugRelatedField(slug_field='slug', read_only=True)
|
||||
invoice_address = InvoiceAddressSerializer(allow_null=True)
|
||||
@@ -773,7 +747,6 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
)
|
||||
plugin_data = OrderPluginDataField(source='*', allow_null=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@@ -782,7 +755,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
||||
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date', 'plugin_data',
|
||||
'url', 'customer', 'valid_if_pending', 'api_meta', 'cancellation_date'
|
||||
)
|
||||
read_only_fields = (
|
||||
'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date',
|
||||
|
||||
@@ -30,7 +30,7 @@ from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.order import (
|
||||
AnswerCreateSerializer, AnswerSerializer, CompatibleCountryField,
|
||||
OrderFeeCreateSerializer, OrderPositionCreateSerializer,
|
||||
OrderPositionCreateSerializer,
|
||||
)
|
||||
from pretix.base.models import ItemVariation, Order, OrderFee, OrderPosition
|
||||
from pretix.base.services.orders import OrderError
|
||||
@@ -104,54 +104,6 @@ class OrderPositionCreateForExistingOrderSerializer(OrderPositionCreateSerialize
|
||||
raise ValidationError(str(e))
|
||||
|
||||
|
||||
class OrderFeeCreateForExistingOrderSerializer(OrderFeeCreateSerializer):
|
||||
order = serializers.SlugRelatedField(slug_field='code', queryset=Order.objects.none(), required=True, allow_null=False)
|
||||
value = serializers.DecimalField(required=True, allow_null=False, decimal_places=2,
|
||||
max_digits=13)
|
||||
internal_type = serializers.CharField(required=False, default="")
|
||||
|
||||
class Meta:
|
||||
model = OrderFee
|
||||
fields = ('order', 'fee_type', 'value', 'description', 'internal_type', 'tax_rule')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.context:
|
||||
return
|
||||
self.fields['order'].queryset = self.context['event'].orders.all()
|
||||
self.fields['tax_rule'].queryset = self.context['event'].tax_rules.all()
|
||||
if 'order' in self.context:
|
||||
del self.fields['order']
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
if 'order' in self.context:
|
||||
data['order'] = self.context['order']
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
ocm = self.context['ocm']
|
||||
|
||||
try:
|
||||
f = OrderFee(
|
||||
order=validated_data['order'],
|
||||
fee_type=validated_data['fee_type'],
|
||||
value=validated_data.get('value'),
|
||||
description=validated_data.get('description'),
|
||||
internal_type=validated_data.get('internal_type'),
|
||||
tax_rule=validated_data.get('tax_rule'),
|
||||
)
|
||||
f._calculate_tax()
|
||||
ocm.add_fee(f)
|
||||
if self.context.get('commit', True):
|
||||
ocm.commit()
|
||||
return validated_data['order'].fees.order_by('-pk').first()
|
||||
else:
|
||||
return OrderFee() # fake to appease DRF
|
||||
except OrderError as e:
|
||||
raise ValidationError(str(e))
|
||||
|
||||
|
||||
class OrderPositionInfoPatchSerializer(serializers.ModelSerializer):
|
||||
answers = AnswerSerializer(many=True)
|
||||
country = CompatibleCountryField(source='*')
|
||||
@@ -449,9 +401,6 @@ class OrderChangeOperationSerializer(serializers.Serializer):
|
||||
self.fields['split_positions'] = SelectPositionSerializer(
|
||||
many=True, required=False, context=self.context
|
||||
)
|
||||
self.fields['create_fees'] = OrderFeeCreateForExistingOrderSerializer(
|
||||
many=True, required=False, context=self.context
|
||||
)
|
||||
self.fields['patch_fees'] = PatchFeeSerializer(
|
||||
many=True, required=False, context=self.context
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.api.models import ApiCall, WebHookCall
|
||||
from pretix.base.signals import EventPluginSignal, periodic_task
|
||||
from pretix.base.signals import periodic_task
|
||||
from pretix.helpers.periodic import minimum_interval
|
||||
|
||||
register_webhook_events = Signal()
|
||||
@@ -43,28 +43,6 @@ return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurit
|
||||
or a list of such instances.
|
||||
"""
|
||||
|
||||
order_api_details = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``order``
|
||||
|
||||
This signal is sent out to fill the ``plugin_details`` field of the order API. Receivers
|
||||
should return a dictionary that is combined with the dictionaries of all other plugins.
|
||||
Note that doing database or network queries in receivers to this signal is discouraged
|
||||
and could cause serious performance issues. The main purpose is to provide information
|
||||
from e.g. ``meta_info`` to the API consumer,
|
||||
"""
|
||||
|
||||
orderposition_api_details = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``orderposition``
|
||||
|
||||
This signal is sent out to fill the ``plugin_details`` field of the order API. Receivers
|
||||
should return a dictionary that is combined with the dictionaries of all other plugins.
|
||||
Note that doing database or network queries in receivers to this signal is discouraged
|
||||
and could cause serious performance issues. The main purpose is to provide information
|
||||
from e.g. ``meta_info`` to the API consumer,
|
||||
"""
|
||||
|
||||
|
||||
@receiver(periodic_task)
|
||||
@scopes_disabled()
|
||||
|
||||
@@ -63,8 +63,7 @@ from pretix.api.serializers.order import (
|
||||
)
|
||||
from pretix.api.serializers.orderchange import (
|
||||
BlockNameSerializer, OrderChangeOperationSerializer,
|
||||
OrderFeeChangeSerializer, OrderFeeCreateForExistingOrderSerializer,
|
||||
OrderPositionChangeSerializer,
|
||||
OrderFeeChangeSerializer, OrderPositionChangeSerializer,
|
||||
OrderPositionCreateForExistingOrderSerializer,
|
||||
OrderPositionInfoPatchSerializer,
|
||||
)
|
||||
@@ -989,12 +988,6 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
ocm.cancel_fee(r['fee'])
|
||||
canceled_fees.add(r['fee'])
|
||||
|
||||
for r in serializer.validated_data.get('create_fees', []):
|
||||
pos_serializer = OrderFeeCreateForExistingOrderSerializer(
|
||||
context={'ocm': ocm, 'commit': False, 'event': request.event, **self.get_serializer_context()},
|
||||
)
|
||||
pos_serializer.create(r)
|
||||
|
||||
for r in serializer.validated_data.get('patch_fees', []):
|
||||
if r['fee'] in canceled_fees:
|
||||
continue
|
||||
|
||||
@@ -148,7 +148,7 @@ def oidc_validate_and_complete_config(config):
|
||||
return config
|
||||
|
||||
|
||||
def oidc_authorize_url(provider, state, redirect_uri):
|
||||
def oidc_authorize_url(provider, state, redirect_uri, pkce_code_verifier):
|
||||
endpoint = provider.configuration['provider_config']['authorization_endpoint']
|
||||
params = {
|
||||
# https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
|
||||
@@ -163,10 +163,14 @@ def oidc_authorize_url(provider, state, redirect_uri):
|
||||
if "query_parameters" in provider.configuration and provider.configuration["query_parameters"]:
|
||||
params.update(parse_qsl(provider.configuration["query_parameters"]))
|
||||
|
||||
if pkce_code_verifier and "S256" in provider.configuration['provider_config'].get('code_challenge_methods_supported', []):
|
||||
params["code_challenge"] = base64.urlsafe_b64encode(hashlib.sha256(pkce_code_verifier.encode()).digest()).decode().rstrip("=")
|
||||
params["code_challenge_method"] = "S256"
|
||||
|
||||
return endpoint + '?' + urlencode(params)
|
||||
|
||||
|
||||
def oidc_validate_authorization(provider, code, redirect_uri):
|
||||
def oidc_validate_authorization(provider, code, redirect_uri, pkce_code_verifier):
|
||||
endpoint = provider.configuration['provider_config']['token_endpoint']
|
||||
|
||||
# Wall of shame and RFC ignorant IDPs
|
||||
@@ -188,6 +192,9 @@ def oidc_validate_authorization(provider, code, redirect_uri):
|
||||
'redirect_uri': redirect_uri,
|
||||
}
|
||||
|
||||
if pkce_code_verifier and "S256" in provider.configuration['provider_config'].get('code_challenge_methods_supported', []):
|
||||
params["code_verifier"] = pkce_code_verifier
|
||||
|
||||
if token_endpoint_auth_method == 'client_secret_post':
|
||||
params['client_id'] = provider.configuration['client_id']
|
||||
params['client_secret'] = provider.configuration['client_secret']
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 4.2.17 on 2025-02-07 16:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0276_item_hidden_if_item_available_mode"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="customerssoclient",
|
||||
name="require_pkce",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="customerssogrant",
|
||||
name="code_challenge",
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="customerssogrant",
|
||||
name="code_challenge_method",
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -416,6 +416,10 @@ class CustomerSSOClient(LoggedModel):
|
||||
authorization_grant_type = models.CharField(
|
||||
max_length=32, choices=GRANT_TYPES, verbose_name=_("Grant type"), default=GRANT_AUTHORIZATION_CODE,
|
||||
)
|
||||
require_pkce = models.BooleanField(
|
||||
verbose_name=_("Require PKCE extension"),
|
||||
default=False,
|
||||
)
|
||||
redirect_uris = models.TextField(
|
||||
blank=False,
|
||||
verbose_name=_("Redirection URIs"),
|
||||
@@ -481,6 +485,8 @@ class CustomerSSOGrant(models.Model):
|
||||
expires = models.DateTimeField()
|
||||
redirect_uri = models.TextField()
|
||||
scope = models.TextField(blank=True)
|
||||
code_challenge = models.TextField(blank=True, null=True)
|
||||
code_challenge_method = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
|
||||
class CustomerSSOAccessToken(models.Model):
|
||||
|
||||
@@ -1113,7 +1113,7 @@ class SSOClientForm(I18nModelForm):
|
||||
class Meta:
|
||||
model = CustomerSSOClient
|
||||
fields = ['is_active', 'name', 'client_id', 'client_type', 'authorization_grant_type', 'redirect_uris',
|
||||
'allowed_scopes']
|
||||
'allowed_scopes', 'require_pkce']
|
||||
widgets = {
|
||||
'authorization_grant_type': forms.RadioSelect,
|
||||
'client_type': forms.RadioSelect,
|
||||
|
||||
@@ -325,7 +325,7 @@ class OrderChangedSplitFrom(OrderLogEntryType):
|
||||
})
|
||||
class CheckinErrorLogEntryType(OrderLogEntryType):
|
||||
def display(self, logentry: LogEntry, data):
|
||||
return self.display_plain(self.plain, logentry, data)
|
||||
self.display_plain(self.plain, logentry, data)
|
||||
|
||||
def display_plain(self, plain, logentry: LogEntry, data):
|
||||
if isinstance(plain, tuple):
|
||||
|
||||
@@ -125,15 +125,6 @@
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
{% if voucher.pk %}
|
||||
<div class="pull-left">
|
||||
<a href="{% url "control:event.voucher.delete" organizer=request.organizer.slug event=request.event.slug voucher=voucher.pk %}"
|
||||
class="btn btn-danger btn-lg">
|
||||
<span class="fa fa-trash"></span>
|
||||
{% trans "Delete voucher" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-01-29 13:18+0000\n"
|
||||
"PO-Revision-Date: 2025-02-06 18:00+0000\n"
|
||||
"PO-Revision-Date: 2025-02-05 20:00+0000\n"
|
||||
"Last-Translator: 조정화 <junghwa.jo@om.org>\n"
|
||||
"Language-Team: Korean <https://translate.pretix.eu/projects/pretix/pretix/ko/"
|
||||
">\n"
|
||||
@@ -516,53 +516,52 @@ msgstr "이벤트 생성"
|
||||
|
||||
#: pretix/api/webhooks.py:329
|
||||
msgid "Event details changed"
|
||||
msgstr "이벤트 세부 정보가 변경되었습니다"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/webhooks.py:333
|
||||
msgid "Event deleted"
|
||||
msgstr "이벤트 삭제"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/webhooks.py:337
|
||||
msgctxt "subevent"
|
||||
msgid "Event series date added"
|
||||
msgstr "이벤트 시리즈 날짜 추가"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/webhooks.py:341
|
||||
msgctxt "subevent"
|
||||
msgid "Event series date changed"
|
||||
msgstr "이벤트 시리즈 날짜 변경"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/webhooks.py:345
|
||||
msgctxt "subevent"
|
||||
msgid "Event series date deleted"
|
||||
msgstr "이벤트 시리즈 날짜 삭제"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/webhooks.py:349
|
||||
msgid ""
|
||||
"Product changed (including product added or deleted and including changes to "
|
||||
"nested objects like variations or bundles)"
|
||||
msgstr "제품 변경(제품 추가 또는 삭제, 변형 또는 번들과 같은 중첩된 객체에 대한 변경 "
|
||||
"포함)"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/webhooks.py:354
|
||||
msgid "Shop taken live"
|
||||
msgstr "라이브 촬영 상점"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/webhooks.py:358
|
||||
msgid "Shop taken offline"
|
||||
msgstr "오프라인 쇼핑"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/webhooks.py:362
|
||||
msgid "Test-Mode of shop has been activated"
|
||||
msgstr "상점의 테스트 모드가 활성화되었습니다"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/webhooks.py:366
|
||||
msgid "Test-Mode of shop has been deactivated"
|
||||
msgstr "상점의 테스트 모드가 비활성화되었습니다"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/webhooks.py:370
|
||||
msgid "Waiting list entry added"
|
||||
msgstr "대기자 명단 항목 추가"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/api/webhooks.py:374
|
||||
msgid "Waiting list entry changed"
|
||||
|
||||
@@ -292,11 +292,11 @@ def _handle_transaction(trans: BankTransaction, matches: tuple, event: Event = N
|
||||
trans.save()
|
||||
|
||||
|
||||
def parse_date(date_str, region=None):
|
||||
def parse_date(date_str):
|
||||
try:
|
||||
return dateutil.parser.parse(
|
||||
date_str,
|
||||
dayfirst="." in date_str or region in ["GB"],
|
||||
dayfirst="." in date_str,
|
||||
).date()
|
||||
except (ValueError, OverflowError):
|
||||
pass
|
||||
@@ -339,7 +339,7 @@ def _get_unknown_transactions(job: BankImportJob, data: list, event: Event = Non
|
||||
external_id=row.get('external_id'),
|
||||
currency=event.currency if event else job.currency)
|
||||
|
||||
trans.date_parsed = parse_date(trans.date, (event and event.settings.region) or (organizer and organizer.settings.region) or None)
|
||||
trans.date_parsed = parse_date(trans.date)
|
||||
|
||||
trans.checksum = trans.calculate_checksum()
|
||||
if trans.checksum not in known_checksums and (not trans.external_id or (trans.external_id, trans.date, trans.amount) not in known_by_external_id):
|
||||
|
||||
@@ -676,6 +676,8 @@ class SSOLoginView(RedirectBackMixin, View):
|
||||
popup_origin = None
|
||||
|
||||
nonce = get_random_string(32)
|
||||
pkce_code_verifier = get_random_string(64)
|
||||
request.session[f'pretix_customerauth_{self.provider.pk}_pkce_code_verifier'] = pkce_code_verifier
|
||||
request.session[f'pretix_customerauth_{self.provider.pk}_nonce'] = nonce
|
||||
request.session[f'pretix_customerauth_{self.provider.pk}_popup_origin'] = popup_origin
|
||||
request.session[f'pretix_customerauth_{self.provider.pk}_cross_domain_requested'] = self.request.GET.get("request_cross_domain_customer_auth") == "true"
|
||||
@@ -684,7 +686,7 @@ class SSOLoginView(RedirectBackMixin, View):
|
||||
})
|
||||
|
||||
if self.provider.method == "oidc":
|
||||
return redirect_to_url(oidc_authorize_url(self.provider, f'{nonce}%{next_url}', redirect_uri))
|
||||
return redirect_to_url(oidc_authorize_url(self.provider, f'{nonce}%{next_url}', redirect_uri, pkce_code_verifier))
|
||||
else:
|
||||
raise Http404("Unknown SSO method.")
|
||||
|
||||
@@ -718,6 +720,7 @@ class SSOLoginReturnView(RedirectBackMixin, View):
|
||||
)
|
||||
return HttpResponseRedirect(redirect_to)
|
||||
r = super().dispatch(request, *args, **kwargs)
|
||||
request.session.pop(f'pretix_customerauth_{self.provider.pk}_pkce_code_verifier', None)
|
||||
request.session.pop(f'pretix_customerauth_{self.provider.pk}_nonce', None)
|
||||
request.session.pop(f'pretix_customerauth_{self.provider.pk}_popup_origin', None)
|
||||
request.session.pop(f'pretix_customerauth_{self.provider.pk}_cross_domain_requested', None)
|
||||
@@ -763,6 +766,7 @@ class SSOLoginReturnView(RedirectBackMixin, View):
|
||||
self.provider,
|
||||
request.GET.get('code'),
|
||||
redirect_uri,
|
||||
request.session.get(f'pretix_customerauth_{self.provider.pk}_pkce_code_verifier'),
|
||||
)
|
||||
except ValidationError as e:
|
||||
for msg in e:
|
||||
|
||||
@@ -72,6 +72,9 @@ We currently do not implement the following optional parts of the spec:
|
||||
We also implement the Discovery extension (without issuer discovery)
|
||||
as per https://openid.net/specs/openid-connect-discovery-1_0.html
|
||||
|
||||
We also implement the PKCE extension for OAuth:
|
||||
https://www.rfc-editor.org/rfc/rfc7636
|
||||
|
||||
The implementation passed the certification tests against the following profiles, but we did not
|
||||
acquire formal certification:
|
||||
|
||||
@@ -136,19 +139,22 @@ class AuthorizeView(View):
|
||||
self._construct_redirect_uri(redirect_uri, response_mode, qs)
|
||||
)
|
||||
|
||||
def _require_login(self, request, client, scope, redirect_uri, response_type, response_mode, state, nonce):
|
||||
def _require_login(self, request, client, scope, redirect_uri, response_type, response_mode, state, nonce,
|
||||
code_challenge, code_challenge_method):
|
||||
form = AuthenticationForm(data=request.POST if "login-email" in request.POST else None, request=request,
|
||||
prefix="login")
|
||||
if "login-email" in request.POST and form.is_valid():
|
||||
customer_login(request, form.get_customer())
|
||||
return self._success(client, scope, redirect_uri, response_type, response_mode, state, nonce, form.get_customer())
|
||||
return self._success(client, scope, redirect_uri, response_type, response_mode, state, nonce,
|
||||
code_challenge, code_challenge_method, form.get_customer())
|
||||
else:
|
||||
return render(request, 'pretixpresale/organizers/customer_login.html', {
|
||||
'providers': [],
|
||||
'form': form,
|
||||
})
|
||||
|
||||
def _success(self, client, scope, redirect_uri, response_type, response_mode, state, nonce, customer):
|
||||
def _success(self, client, scope, redirect_uri, response_type, response_mode, state, nonce, code_challenge,
|
||||
code_challenge_method, customer):
|
||||
response_type = response_type.split(' ')
|
||||
qs = {}
|
||||
id_token_kwargs = {}
|
||||
@@ -162,6 +168,8 @@ class AuthorizeView(View):
|
||||
expires=now() + timedelta(minutes=10),
|
||||
auth_time=get_customer_auth_time(self.request),
|
||||
nonce=nonce,
|
||||
code_challenge=code_challenge,
|
||||
code_challenge_method=code_challenge_method,
|
||||
)
|
||||
qs['code'] = grant.code
|
||||
id_token_kwargs['with_code'] = grant.code
|
||||
@@ -209,6 +217,8 @@ class AuthorizeView(View):
|
||||
prompt = request_data.get("prompt")
|
||||
response_type = request_data.get("response_type")
|
||||
scope = request_data.get("scope", "").split(" ")
|
||||
code_challenge = request_data.get("code_challenge")
|
||||
code_challenge_method = request_data.get("code_challenge_method")
|
||||
|
||||
if not client_id:
|
||||
return self._final_error("invalid_request", "client_id missing")
|
||||
@@ -244,7 +254,17 @@ class AuthorizeView(View):
|
||||
response_mode, state)
|
||||
|
||||
if "id_token_hint" in request_data:
|
||||
return self._redirect_error("invalid_request", "id_token_hint currently not supported by this server",
|
||||
self._redirect_error("invalid_request", "id_token_hint currently not supported by this server",
|
||||
redirect_uri, response_mode, state)
|
||||
|
||||
if code_challenge and code_challenge_method != "S256":
|
||||
# "Clients re permitted to use "plain" only if they cannot support "S256" for some technical reason and
|
||||
# know via out-of-band configuration that the S256 MUST be implemented, plain is not mandatory."
|
||||
return self._redirect_error("invalid_request", "code_challenge transform algorithm not supported",
|
||||
redirect_uri, response_mode, state)
|
||||
|
||||
if client.require_pkce and not code_challenge:
|
||||
return self._redirect_error("invalid_request", "code_challenge (PKCE) required",
|
||||
redirect_uri, response_mode, state)
|
||||
|
||||
has_valid_session = bool(request.customer)
|
||||
@@ -252,8 +272,7 @@ class AuthorizeView(View):
|
||||
try:
|
||||
has_valid_session = int(time.time() - get_customer_auth_time(request)) < int(max_age)
|
||||
except ValueError:
|
||||
return self._redirect_error("invalid_request", "invalid max_age value", redirect_uri,
|
||||
response_mode, state)
|
||||
self._redirect_error("invalid_request", "invalid max_age value", redirect_uri, response_mode, state)
|
||||
|
||||
if not has_valid_session and prompt and prompt == "none":
|
||||
return self._redirect_error("interaction_required", "user is not logged in but no prompt is allowed",
|
||||
@@ -262,9 +281,11 @@ class AuthorizeView(View):
|
||||
has_valid_session = False
|
||||
|
||||
if has_valid_session:
|
||||
return self._success(client, scope, redirect_uri, response_type, response_mode, state, nonce, request.customer)
|
||||
return self._success(client, scope, redirect_uri, response_type, response_mode, state, nonce, code_challenge,
|
||||
code_challenge_method, request.customer)
|
||||
else:
|
||||
return self._require_login(request, client, scope, redirect_uri, response_type, response_mode, state, nonce)
|
||||
return self._require_login(request, client, scope, redirect_uri, response_type, response_mode, state, nonce,
|
||||
code_challenge, code_challenge_method)
|
||||
|
||||
|
||||
class TokenView(View):
|
||||
@@ -362,6 +383,24 @@ class TokenView(View):
|
||||
"error_description": "Mismatch of redirect_uri"
|
||||
}, status=400)
|
||||
|
||||
if grant.code_challenge:
|
||||
if not request.POST.get("code_verifier"):
|
||||
return JsonResponse({
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Missing of code_verifier"
|
||||
}, status=400)
|
||||
|
||||
if grant.code_challenge_method == "S256":
|
||||
expected_challenge = base64.urlsafe_b64encode(hashlib.sha256(request.POST["code_verifier"].encode()).digest()).decode().rstrip("=")
|
||||
print(grant.code_challenge, expected_challenge)
|
||||
if expected_challenge != grant.code_challenge:
|
||||
return JsonResponse({
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Mismatch of code_verifier with code_challenge"
|
||||
}, status=400)
|
||||
else:
|
||||
raise ValueError("Unsupported code_challenge_method in database")
|
||||
|
||||
with transaction.atomic():
|
||||
token = self.client.access_tokens.create(
|
||||
customer=grant.customer,
|
||||
@@ -503,6 +542,7 @@ class ConfigurationView(View):
|
||||
'token_endpoint_auth_methods_supported': [
|
||||
'client_secret_post', 'client_secret_basic'
|
||||
],
|
||||
'code_challenge_methods_supported': ['S256'],
|
||||
'claims_supported': [
|
||||
'iss',
|
||||
'aud',
|
||||
|
||||
@@ -162,7 +162,6 @@ def test_giftcard_detail_expand(token_client, organizer, event, giftcard):
|
||||
"tax_rule": None,
|
||||
"pseudonymization_id": op.pseudonymization_id,
|
||||
"pdf_data": {},
|
||||
"plugin_data": {},
|
||||
"seat": None,
|
||||
"canceled": False,
|
||||
"valid_from": None,
|
||||
|
||||
@@ -1797,13 +1797,6 @@ def test_order_change_cancel_and_create(token_client, organizer, event, order, q
|
||||
'price': '99.99'
|
||||
},
|
||||
],
|
||||
'create_fees': [
|
||||
{
|
||||
'value': '5.99',
|
||||
'fee_type': 'service',
|
||||
'description': 'Service fee',
|
||||
},
|
||||
],
|
||||
'cancel_fees': [
|
||||
{
|
||||
'fee': f.pk,
|
||||
@@ -1825,11 +1818,6 @@ def test_order_change_cancel_and_create(token_client, organizer, event, order, q
|
||||
assert p_new.price == Decimal('99.99')
|
||||
f.refresh_from_db()
|
||||
assert f.canceled
|
||||
f_new = order.all_fees.get(fee_type=OrderFee.FEE_TYPE_SERVICE)
|
||||
assert f_new.value == Decimal('5.99')
|
||||
assert f_new.description == "Service fee"
|
||||
assert f_new.internal_type == ""
|
||||
assert f_new.tax_value == Decimal('0.00')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -477,8 +477,7 @@ def test_order_create_simulate(token_client, organizer, event, item, quota, ques
|
||||
'zipcode': None,
|
||||
'state': None,
|
||||
'country': None,
|
||||
'canceled': False,
|
||||
'plugin_data': {},
|
||||
'canceled': False
|
||||
}
|
||||
],
|
||||
'downloads': [],
|
||||
@@ -488,8 +487,7 @@ def test_order_create_simulate(token_client, organizer, event, item, quota, ques
|
||||
'refunds': [],
|
||||
'require_approval': False,
|
||||
'sales_channel': 'web',
|
||||
'cancellation_date': None,
|
||||
'plugin_data': {},
|
||||
'cancellation_date': None
|
||||
}
|
||||
|
||||
|
||||
@@ -535,15 +533,13 @@ def test_order_create_positionids_addons_simulated(token_client, organizer, even
|
||||
'street': None, 'zipcode': None, 'city': None, 'country': None, 'state': None, 'attendee_email': None,
|
||||
'voucher': None, 'tax_rate': '19.00', 'tax_code': 'S/standard', 'tax_value': '3.67', 'discount': None, 'voucher_budget_use': None,
|
||||
'addon_to': None, 'subevent': None, 'checkins': [], 'print_logs': [], 'downloads': [], 'answers': [], 'tax_rule': item.tax_rule_id,
|
||||
'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None,
|
||||
'plugin_data': {}},
|
||||
'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None},
|
||||
{'id': 0, 'order': '', 'positionid': 2, 'item': item.pk, 'variation': None, 'price': '23.00',
|
||||
'attendee_name': 'Peter', 'attendee_name_parts': {'full_name': 'Peter', '_scheme': 'full'}, 'company': None,
|
||||
'street': None, 'zipcode': None, 'city': None, 'country': None, 'state': None, 'attendee_email': None,
|
||||
'voucher': None, 'tax_rate': '19.00', 'tax_code': 'S/standard', 'tax_value': '3.67', 'discount': None, 'voucher_budget_use': None,
|
||||
'addon_to': 1, 'subevent': None, 'checkins': [], 'print_logs': [], 'downloads': [], 'answers': [], 'tax_rule': item.tax_rule_id,
|
||||
'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None,
|
||||
'plugin_data': {}}
|
||||
'pseudonymization_id': 'PREVIEW', 'seat': None, 'canceled': False, 'valid_from': None, 'valid_until': None, 'blocked': None}
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -236,7 +236,6 @@ TEST_ORDERPOSITION_RES = {
|
||||
],
|
||||
"subevent": None,
|
||||
"canceled": False,
|
||||
"plugin_data": {},
|
||||
}
|
||||
TEST_PAYMENTS_RES = [
|
||||
{
|
||||
@@ -334,7 +333,6 @@ TEST_ORDER_RES = {
|
||||
"payments": TEST_PAYMENTS_RES,
|
||||
"refunds": TEST_REFUNDS_RES,
|
||||
"cancellation_date": None,
|
||||
"plugin_data": {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -203,8 +203,7 @@ def test_medium_detail(token_client, organizer, event, medium, giftcard, custome
|
||||
"canceled": False,
|
||||
"valid_from": None,
|
||||
"valid_until": None,
|
||||
"blocked": None,
|
||||
"plugin_data": {},
|
||||
"blocked": None
|
||||
}
|
||||
assert resp.data["linked_giftcard"] == {
|
||||
"id": giftcard.pk,
|
||||
|
||||
@@ -236,7 +236,8 @@ def provider(organizer):
|
||||
"response_modes_supported": ["query"],
|
||||
"grant_types_supported": ["authorization_code"],
|
||||
"scopes_supported": ["openid", "email", "profile"],
|
||||
"claims_supported": ["email", "sub"]
|
||||
"claims_supported": ["email", "sub"],
|
||||
"code_challenge_methods_supported": ["plain", "S256"],
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -244,6 +245,21 @@ def provider(organizer):
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_authorize_url(provider):
|
||||
assert (
|
||||
"https://example.com/authorize?"
|
||||
"response_type=code&"
|
||||
"client_id=abc123&"
|
||||
"scope=openid+email+profile&"
|
||||
"state=state_val&"
|
||||
"redirect_uri=https%3A%2F%2Fredirect%3Ffoo%3Dbar&"
|
||||
"code_challenge=S1ZnvzwMZHrWOO62nENdJ6jhODhf7VfyZFBIXQyrTKo&"
|
||||
"code_challenge_method=S256"
|
||||
) == oidc_authorize_url(provider, "state_val", "https://redirect?foo=bar", "pkce_value")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_authorize_url_no_pkce(provider):
|
||||
del provider.configuration["provider_config"]["code_challenge_methods_supported"]
|
||||
assert (
|
||||
"https://example.com/authorize?"
|
||||
"response_type=code&"
|
||||
@@ -251,7 +267,7 @@ def test_authorize_url(provider):
|
||||
"scope=openid+email+profile&"
|
||||
"state=state_val&"
|
||||
"redirect_uri=https%3A%2F%2Fredirect%3Ffoo%3Dbar"
|
||||
) == oidc_authorize_url(provider, "state_val", "https://redirect?foo=bar")
|
||||
) == oidc_authorize_url(provider, "state_val", "https://redirect?foo=bar", "pkce_value")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -264,7 +280,7 @@ def test_validate_authorization_invalid(provider):
|
||||
status=400,
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
oidc_validate_authorization(provider, "code_received", "https://redirect?foo=bar")
|
||||
oidc_validate_authorization(provider, "code_received", "https://redirect?foo=bar", "pkce_value")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -281,6 +297,7 @@ def test_validate_authorization_userinfo_invalid(provider):
|
||||
"grant_type": "authorization_code",
|
||||
"code": "code_received",
|
||||
"redirect_uri": "https://redirect?foo=bar",
|
||||
"code_verifier": "pkce_value",
|
||||
})
|
||||
],
|
||||
)
|
||||
@@ -296,7 +313,7 @@ def test_validate_authorization_userinfo_invalid(provider):
|
||||
],
|
||||
)
|
||||
with pytest.raises(ValidationError) as e:
|
||||
oidc_validate_authorization(provider, "code_received", "https://redirect?foo=bar")
|
||||
oidc_validate_authorization(provider, "code_received", "https://redirect?foo=bar", "pkce_value")
|
||||
assert 'could not fetch' in str(e.value)
|
||||
|
||||
|
||||
@@ -314,6 +331,7 @@ def test_validate_authorization_valid(provider):
|
||||
"grant_type": "authorization_code",
|
||||
"code": "code_received",
|
||||
"redirect_uri": "https://redirect?foo=bar",
|
||||
"code_verifier": "pkce_value",
|
||||
})
|
||||
],
|
||||
)
|
||||
@@ -328,4 +346,4 @@ def test_validate_authorization_valid(provider):
|
||||
matchers.header_matcher({"Authorization": "Bearer test_access_token"})
|
||||
],
|
||||
)
|
||||
oidc_validate_authorization(provider, "code_received", "https://redirect?foo=bar")
|
||||
oidc_validate_authorization(provider, "code_received", "https://redirect?foo=bar", "pkce_value")
|
||||
|
||||
@@ -790,33 +790,3 @@ def test_ignore_by_external_id(env, job):
|
||||
}])
|
||||
with scopes_disabled():
|
||||
assert BankTransaction.objects.count() == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_ambigious_date_without_region(env, job):
|
||||
process_banktransfers(job, [{
|
||||
'payer': 'Karla Kundin',
|
||||
'reference': 'Bestellung DUMMY1Z3AS',
|
||||
'date': '03/05/2016',
|
||||
'amount': '23.00'
|
||||
}])
|
||||
env[2].refresh_from_db()
|
||||
|
||||
with scopes_disabled():
|
||||
assert env[2].payments.last().info_data["date"] == "2016-03-05"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_ambigious_date_with_region(env, job):
|
||||
env[0].settings.region = "GB"
|
||||
|
||||
process_banktransfers(job, [{
|
||||
'payer': 'Karla Kundin',
|
||||
'reference': 'Bestellung DUMMY1Z3AS',
|
||||
'date': '03/05/2016',
|
||||
'amount': '23.00'
|
||||
}])
|
||||
env[2].refresh_from_db()
|
||||
|
||||
with scopes_disabled():
|
||||
assert env[2].payments.last().info_data["date"] == "2016-05-03"
|
||||
|
||||
@@ -40,5 +40,3 @@ def test_date_formats():
|
||||
|
||||
assert dt == parse_date("2020-07-01")
|
||||
assert dt == parse_date("2020-7-1")
|
||||
|
||||
assert dt == parse_date("01/07/2020", "GB")
|
||||
|
||||
@@ -208,6 +208,37 @@ def test_authorize_with_prompt_none(env, client, ssoclient):
|
||||
assert re.match(r'https://example.net\?code=([a-z0-9A-Z]{64})&state=STATE', r.headers['Location'])
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_authorize_with_invalid_pkce_method(env, client, ssoclient):
|
||||
url = f'/bigevents/oauth2/v1/authorize?' \
|
||||
f'client_id={ssoclient[0].client_id}&' \
|
||||
f'redirect_uri=https://example.net&' \
|
||||
f'response_type=code&state=STATE&scope=openid+profile&' \
|
||||
f'code_challenge=pkce_value&code_challenge_method=plain'
|
||||
r = client.get(url)
|
||||
assert r.status_code == 302
|
||||
assert r.headers['Location'] == 'https://example.net?' \
|
||||
'error=invalid_request&' \
|
||||
'error_description=code_challenge+transform+algorithm+not+supported&' \
|
||||
'state=STATE'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_authorize_with_missing_pkce_if_required(env, client, ssoclient):
|
||||
ssoclient[0].require_pkce = True
|
||||
ssoclient[0].save()
|
||||
url = f'/bigevents/oauth2/v1/authorize?' \
|
||||
f'client_id={ssoclient[0].client_id}&' \
|
||||
f'redirect_uri=https://example.net&' \
|
||||
f'response_type=code&state=STATE&scope=openid+profile'
|
||||
r = client.get(url)
|
||||
assert r.status_code == 302
|
||||
assert r.headers['Location'] == 'https://example.net?' \
|
||||
'error=invalid_request&' \
|
||||
'error_description=code_challenge+%28PKCE%29+required&' \
|
||||
'state=STATE'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_authorize_require_login_if_prompt_requires_it_or_is_expired(env, client, ssoclient):
|
||||
with freeze_time("2021-04-10T11:00:00+02:00"):
|
||||
@@ -286,7 +317,7 @@ def test_token_require_client_id(env, client, ssoclient):
|
||||
assert b'unsupported_grant_type' in r.content
|
||||
|
||||
|
||||
def _authorization_step(client, ssoclient):
|
||||
def _authorization_step(client, ssoclient, code_challenge=None):
|
||||
r = client.post('/bigevents/account/login', {
|
||||
'email': 'john@example.org',
|
||||
'password': 'foo',
|
||||
@@ -299,6 +330,8 @@ def _authorization_step(client, ssoclient):
|
||||
f'client_id={ssoclient[0].client_id}&' \
|
||||
f'redirect_uri=https://example.net&' \
|
||||
f'response_type=code&state=STATE&scope=openid+profile+email+phone'
|
||||
if code_challenge:
|
||||
url += f'&code_challenge={code_challenge}&code_challenge_method=S256'
|
||||
r = client.get(url)
|
||||
assert r.status_code == 302
|
||||
m = re.match(r'https://example.net\?code=([a-z0-9A-Z]{64})&state=STATE', r.headers['Location'])
|
||||
@@ -373,6 +406,55 @@ def test_token_success(env, client, ssoclient):
|
||||
CustomerSSOAccessToken.objects.get(token=d['access_token']).expires < now()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_token_pkce_required_if_used_in_authorization(env, client, ssoclient):
|
||||
code = _authorization_step(client, ssoclient, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM")
|
||||
|
||||
r = client.post('/bigevents/oauth2/v1/token', {
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': 'https://example.net',
|
||||
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(f'{ssoclient[0].client_id}:{ssoclient[1]}'.encode()).decode())
|
||||
assert r.status_code == 400
|
||||
d = json.loads(r.content)
|
||||
assert d['error'] == 'invalid_grant'
|
||||
assert d['error_description'] == 'Missing of code_verifier'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_token_pkce_incorrect(env, client, ssoclient):
|
||||
code = _authorization_step(client, ssoclient, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM")
|
||||
|
||||
r = client.post('/bigevents/oauth2/v1/token', {
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': 'https://example.net',
|
||||
'code_verifier': "WRONG",
|
||||
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(f'{ssoclient[0].client_id}:{ssoclient[1]}'.encode()).decode())
|
||||
assert r.status_code == 400
|
||||
d = json.loads(r.content)
|
||||
assert d['error'] == 'invalid_grant'
|
||||
assert d['error_description'] == 'Mismatch of code_verifier with code_challenge'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_token_success_pkce(env, client, ssoclient):
|
||||
# this is the sample from the actual RFC
|
||||
code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
|
||||
code = _authorization_step(client, ssoclient, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM")
|
||||
|
||||
r = client.post('/bigevents/oauth2/v1/token', {
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': 'https://example.net',
|
||||
'code_verifier': code_verifier,
|
||||
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(f'{ssoclient[0].client_id}:{ssoclient[1]}'.encode()).decode())
|
||||
print(r.content)
|
||||
assert r.status_code == 200
|
||||
d = json.loads(r.content)
|
||||
assert d['access_token']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_scope_enforcement(env, client, ssoclient):
|
||||
ssoclient[0].allowed_scopes = ['openid', 'profile']
|
||||
|
||||
Reference in New Issue
Block a user