OpenID Connect OP support for customer accounts

This commit is contained in:
Raphael Michel
2022-08-10 14:22:30 +02:00
committed by Raphael Michel
parent 7f5518dbf6
commit a4171ef819
20 changed files with 1735 additions and 23 deletions

View File

@@ -62,7 +62,7 @@ from pretix.base.models import (
Customer, Device, EventMetaProperty, Gate, GiftCard, Membership,
MembershipType, Organizer, Team,
)
from pretix.base.models.customers import CustomerSSOProvider
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
from pretix.base.models.organizer import OrganizerFooterLink
from pretix.base.settings import PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS
from pretix.control.forms import ExtFileField, SplitDateTimeField
@@ -797,3 +797,36 @@ class SSOProviderForm(I18nModelForm):
oidc_validate_and_complete_config(config)
self.instance.configuration = config
class SSOClientForm(I18nModelForm):
regenerate_client_secret = forms.BooleanField(
label=_('Invalidate old client secret and generate a new one'),
required=False,
)
class Meta:
model = CustomerSSOClient
fields = ['is_active', 'name', 'client_id', 'client_type', 'authorization_grant_type', 'redirect_uris',
'allowed_scopes']
widgets = {
'authorization_grant_type': forms.RadioSelect,
'client_type': forms.RadioSelect,
'allowed_scopes': forms.CheckboxSelectMultiple,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['allowed_scopes'] = forms.MultipleChoiceField(
label=self.fields['allowed_scopes'].label,
help_text=self.fields['allowed_scopes'].help_text,
required=self.fields['allowed_scopes'].required,
initial=self.fields['allowed_scopes'].initial,
choices=CustomerSSOClient.SCOPE_CHOICES,
widget=forms.CheckboxSelectMultiple
)
if self.instance and self.instance.pk:
self.fields['client_id'].disabled = True
else:
del self.fields['client_id']
del self.fields['regenerate_client_secret']

View File

@@ -324,6 +324,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.ssoprovider.created': _('The SSO provider has been created.'),
'pretix.ssoprovider.changed': _('The SSO provider has been changed.'),
'pretix.ssoprovider.deleted': _('The SSO provider has been deleted.'),
'pretix.ssoclient.created': _('The SSO client has been created.'),
'pretix.ssoclient.changed': _('The SSO client has been changed.'),
'pretix.ssoclient.deleted': _('The SSO client has been deleted.'),
'pretix.membershiptype.created': _('The membership type has been created.'),
'pretix.membershiptype.changed': _('The membership type has been changed.'),
'pretix.membershiptype.deleted': _('The membership type has been deleted.'),

View File

@@ -550,6 +550,15 @@ def get_organizer_navigation(request):
'active': 'organizer.membershiptype' in url.url_name,
}
)
children.append(
{
'label': _('SSO clients'),
'url': reverse('control:organizer.ssoclients', kwargs={
'organizer': request.organizer.slug
}),
'active': 'organizer.ssoclient' in url.url_name,
}
)
children.append(
{
'label': _('SSO providers'),

View File

@@ -0,0 +1,25 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<h1>{% trans "Delete SSO client:" %} {{ client.name }}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% if is_allowed %}
<p>{% blocktrans %}Are you sure you want to delete this SSO client?{% endblocktrans %}
{% else %}
<p>{% blocktrans %}This SSO client cannot be deleted since it has already been used.{% endblocktrans %}
{% endif %}
</p>
<div class="form-group submit-group">
<a href="{% url "control:organizer.ssoclients" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
{% if is_allowed %}
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
{% endif %}
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,20 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
{% if client %}
<h1>{% trans "SSO client:" %} {{ client.name }}</h1>
{% else %}
<h1>{% trans "Create a new SSO client" %}</h1>
{% endif %}
<form class="form-horizontal" action="" method="post">
{% 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 %}

View File

@@ -0,0 +1,44 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "SSO clients" %}{% endblock %}
{% block inner %}
<h1>{% trans "SSO clients" %}</h1>
<p>
{% blocktrans trimmed %}
You can allow your customers to log into other systems using their customer account credentials by setting up
your other systems as a Single-Sign-On (SSO) client based on OpenID Connect.
{% endblocktrans %}
</p>
<a href="{% url "control:organizer.ssoclient.add" organizer=request.organizer.slug %}" class="btn btn-default">
<span class="fa fa-plus"></span>
{% trans "Create a new SSO client" %}
</a>
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for c in clients %}
<tr>
<td><strong>
<a href="{% url "control:organizer.ssoclient.edit" organizer=request.organizer.slug client=c.id %}">
{% if not c.is_active %}<del>{% endif %}
{{ c.name }}
{% if not c.is_active %}</del>{% endif %}
</a>
</strong></td>
<td class="text-right flip">
<a href="{% url "control:organizer.ssoclient.edit" organizer=request.organizer.slug client=c.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:organizer.ssoclient.delete" organizer=request.organizer.slug client=c.id %}"
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@@ -140,6 +140,13 @@ urlpatterns = [
name='organizer.ssoprovider.edit'),
re_path(r'^organizer/(?P<organizer>[^/]+)/ssoprovider/(?P<provider>[^/]+)/delete$', organizer.SSOProviderDeleteView.as_view(),
name='organizer.ssoprovider.delete'),
re_path(r'^organizer/(?P<organizer>[^/]+)/ssoclients$', organizer.SSOClientListView.as_view(), name='organizer.ssoclients'),
re_path(r'^organizer/(?P<organizer>[^/]+)/ssoclient/add$', organizer.SSOClientCreateView.as_view(),
name='organizer.ssoclient.add'),
re_path(r'^organizer/(?P<organizer>[^/]+)/ssoclient/(?P<client>[^/]+)/edit$', organizer.SSOClientUpdateView.as_view(),
name='organizer.ssoclient.edit'),
re_path(r'^organizer/(?P<organizer>[^/]+)/ssoclient/(?P<client>[^/]+)/delete$', organizer.SSOClientDeleteView.as_view(),
name='organizer.ssoclient.delete'),
re_path(r'^organizer/(?P<organizer>[^/]+)/customers$', organizer.CustomerListView.as_view(), name='organizer.customers'),
re_path(r'^organizer/(?P<organizer>[^/]+)/customers/select2$', typeahead.customer_select2, name='organizer.customers.select2'),
re_path(r'^organizer/(?P<organizer>[^/]+)/customer/add$',

View File

@@ -71,7 +71,7 @@ from pretix.base.models import (
Membership, MembershipType, Order, OrderPayment, OrderPosition, Organizer,
Team, TeamInvite, User,
)
from pretix.base.models.customers import CustomerSSOProvider
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue
from pretix.base.models.giftcards import (
GiftCardTransaction, gen_giftcard_secret,
@@ -95,8 +95,8 @@ from pretix.control.forms.organizer import (
EventMetaPropertyForm, GateForm, GiftCardCreateForm, GiftCardUpdateForm,
MailSettingsForm, MembershipTypeForm, MembershipUpdateForm,
OrganizerDeleteForm, OrganizerFooterLinkFormset, OrganizerForm,
OrganizerSettingsForm, OrganizerUpdateForm, SSOProviderForm, TeamForm,
WebHookForm,
OrganizerSettingsForm, OrganizerUpdateForm, SSOClientForm, SSOProviderForm,
TeamForm, WebHookForm,
)
from pretix.control.logdisplay import OVERVIEW_BANLIST
from pretix.control.permissions import (
@@ -2039,6 +2039,134 @@ class SSOProviderDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequire
return redirect(success_url)
class SSOClientListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = CustomerSSOClient
template_name = 'pretixcontrol/organizers/ssoclients.html'
permission = 'can_change_organizer_settings'
context_object_name = 'clients'
def get_queryset(self):
return self.request.organizer.sso_clients.all()
class SSOClientCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = CustomerSSOClient
template_name = 'pretixcontrol/organizers/ssoclient_edit.html'
permission = 'can_change_organizer_settings'
form_class = SSOClientForm
def get_object(self, queryset=None):
return get_object_or_404(CustomerSSOClient, organizer=self.request.organizer, pk=self.kwargs.get('client'))
def get_success_url(self):
return reverse('control:organizer.ssoclient.edit', kwargs={
'organizer': self.request.organizer.slug,
'client': self.object.pk
})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['event'] = self.request.organizer
return kwargs
def form_valid(self, form):
secret = form.instance.set_client_secret()
messages.success(
self.request,
_('The SSO client has been created. Please note down the following client secret, it will never be shown '
'again: {secret}').format(secret=secret)
)
form.instance.organizer = self.request.organizer
ret = super().form_valid(form)
form.instance.log_action('pretix.ssoclient.created', user=self.request.user, data={
k: getattr(self.object, k, form.cleaned_data.get(k)) for k in form.changed_data
})
return ret
def form_invalid(self, form):
messages.error(self.request, _('Your changes could not be saved.'))
return super().form_invalid(form)
class SSOClientUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = CustomerSSOClient
template_name = 'pretixcontrol/organizers/ssoclient_edit.html'
permission = 'can_change_organizer_settings'
context_object_name = 'client'
form_class = SSOClientForm
def get_object(self, queryset=None):
return get_object_or_404(CustomerSSOClient, organizer=self.request.organizer, pk=self.kwargs.get('client'))
def get_success_url(self):
return reverse('control:organizer.ssoclient.edit', kwargs={
'organizer': self.request.organizer.slug,
'client': self.object.pk
})
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
return ctx
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['event'] = self.request.organizer
return kwargs
def form_valid(self, form):
if form.has_changed():
self.object.log_action('pretix.ssoclient.changed', user=self.request.user, data={
k: getattr(self.object, k, form.cleaned_data.get(k)) for k in form.changed_data
})
if form.cleaned_data.get('regenerate_client_secret'):
secret = form.instance.set_client_secret()
messages.success(
self.request,
_('Your changes have been saved. Please note down the following client secret, it will never be shown '
'again: {secret}').format(secret=secret)
)
else:
messages.success(
self.request,
_('Your changes have been saved.')
)
return super().form_valid(form)
def form_invalid(self, form):
messages.error(self.request, _('Your changes could not be saved.'))
return super().form_invalid(form)
class SSOClientDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
model = CustomerSSOClient
template_name = 'pretixcontrol/organizers/ssoclient_delete.html'
permission = 'can_change_organizer_settings'
context_object_name = 'client'
def get_object(self, queryset=None):
return get_object_or_404(CustomerSSOClient, organizer=self.request.organizer, pk=self.kwargs.get('client'))
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['is_allowed'] = self.object.allow_delete()
return ctx
def get_success_url(self):
return reverse('control:organizer.ssoclients', kwargs={
'organizer': self.request.organizer.slug,
})
@transaction.atomic
def delete(self, request, *args, **kwargs):
success_url = self.get_success_url()
self.object = self.get_object()
if self.object.allow_delete():
self.object.log_action('pretix.ssoclient.deleted', user=self.request.user)
self.object.delete()
messages.success(request, _('The selected object has been deleted.'))
return redirect(success_url)
class CustomerListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
model = Customer
template_name = 'pretixcontrol/organizers/customers.html'