Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement a dedicated plugin for SSO functionalities #475

Merged
merged 15 commits into from
Dec 17, 2024
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ dependencies = [
# Access is required to the private repositories, if you don't have access, you can remove the dependencies
'eventyay-paypal @ git+https://[email protected]/fossasia/eventyay-tickets-paypal.git@master',
'django_celery_beat==2.7.0',
'cron-descriptor==1.4.5'
'cron-descriptor==1.4.5',
'django-allauth[socialaccount]==65.3.0'
]

[project.optional-dependencies]
Expand Down
12 changes: 12 additions & 0 deletions src/pretix/control/forms/global_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,15 @@ class UpdateSettingsForm(SettingsForm):
def __init__(self, *args, **kwargs):
self.obj = GlobalSettingsObject()
super().__init__(*args, obj=self.obj, **kwargs)


class SSOConfigForm(SettingsForm):
redirect_url = forms.URLField(
required=True,
label=_("Redirect URL"),
help_text=_("e.g. {sample}").format(sample="https://app-test.eventyay.com/talk/")
)

def __init__(self, *args, **kwargs):
self.obj = GlobalSettingsObject()
super().__init__(*args, obj=self.obj, **kwargs)
5 changes: 5 additions & 0 deletions src/pretix/control/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,11 @@ def get_admin_navigation(request):
'url': reverse('control:admin.global.update'),
'active': (url.url_name == 'admin.global.update'),
},
{
'label': _('Generate keys for SSO'),
'url': reverse('control:admin.global.sso'),
'active': (url.url_name == 'admin.global.sso'),
},
]
},
]
Expand Down
68 changes: 68 additions & 0 deletions src/pretix/control/templates/pretixcontrol/global_sso.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{% extends "pretixcontrol/admin/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load rich_text %}


{% block title %}{% trans "Generate keys for SSO" %}{% endblock %}
{% block content %}
<h1>{% trans "Generate keys for SSO" %}</h1>
{{ global_settings.banner_message_detail|rich_text }}
{% block inner %}
<form role="form" action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_form form layout='control' %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Submit" %}
</button>
</div>
</form>
{% endblock %}

{% if result.error_message %}
<h2>Error:</h2>
<pre>{{ result.error_message }}</pre>
{% elif result.success_message %}
<h2>{{ result.success_message }}</h2>
<div class="row">
<div class="col-md-12">
<label>Client ID:</label>
<input type="text" value="{{ result.client_id }}" disabled class="form-control" style="width: 300px;">
</div>
<div class="col-md-12">
<label>Client Secret:</label>
<input type="text" value="{{ result.client_secret }}" disabled class="form-control" style="width: 300px;">
</div>
</div>
{% endif %}

{% if oauth_applications %}
<h2>OAuth Applications</h2>
<ul>
{% for application in oauth_applications %}
<li class="list-group-item">
<h5>OAuth Application</h5>
<p><strong>URL:</strong> {{ application.redirect_uris }}</p>
<div class="row">
<div class="col-md-12">
<label>Client ID:</label>
<input type="text" value="{{ application.client_id }}" disabled class="form-control" style="width: 300px;">
</div>
<div class="col-md-12">
<label>Client Secret:</label>
<input type="text" value="{{ application.client_secret }}" disabled class="form-control" style="width: 300px;">
</div>
</div>
<br>
<form action="{% url 'control:admin.global.sso.delete' application.pk %}" method="post" class="text-right">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</li>
{% endfor %}
</ul>
{% endif %}

{% endblock %}
2 changes: 2 additions & 0 deletions src/pretix/control/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,8 @@
url(r'^global/settings/$', global_settings.GlobalSettingsView.as_view(), name='admin.global.settings'),
url(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='admin.global.update'),
url(r'^global/message/$', global_settings.MessageView.as_view(), name='admin.global.message'),
url(r'^global/sso/$', global_settings.SSOView.as_view(), name='admin.global.sso'),
url(r'^global/sso/(?P<pk>\d+)/delete/$', global_settings.DeleteOAuthApplicationView.as_view(), name='admin.global.sso.delete'),
url(r'^pages/$', pages.PageList.as_view(), name="admin.pages"),
url(r'^pages/add$', pages.PageCreate.as_view(), name="admin.pages.add"),
url(r'^pages/(?P<id>\d+)/edit$', pages.PageUpdate.as_view(), name="admin.pages.edit"),
Expand Down
88 changes: 84 additions & 4 deletions src/pretix/control/views/global_settings.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import logging
import secrets

from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import IntegrityError
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, reverse
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.generic import FormView, TemplateView
from django.views.generic import DeleteView, FormView, TemplateView

from pretix.api.models import OAuthApplication
from pretix.base.models import LogEntry, OrderPayment, OrderRefund
from pretix.base.services.update_check import check_result_table, update_check
from pretix.base.settings import GlobalSettingsObject
from pretix.control.forms.global_settings import (
GlobalSettingsForm, UpdateSettingsForm,
GlobalSettingsForm, SSOConfigForm, UpdateSettingsForm,
)
from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin,
)

logger = logging.getLogger(__name__)


class GlobalSettingsView(AdministratorPermissionRequiredMixin, FormView):
template_name = 'pretixcontrol/global_settings.html'
Expand All @@ -26,13 +35,82 @@ def form_valid(self, form):
return super().form_valid(form)

def form_invalid(self, form):
messages.error(self.request, _('Your changes have not been saved, see below for errors.'))
messages.error(
self.request, _('Your changes have not been saved, see below for errors.')
)
return super().form_invalid(form)

def get_success_url(self):
return reverse('control:admin.global.settings')


class SSOView(AdministratorPermissionRequiredMixin, FormView):
template_name = 'pretixcontrol/global_sso.html'
form_class = SSOConfigForm

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
oauth_applications = OAuthApplication.objects.all()
context['oauth_applications'] = oauth_applications
return context

def post(self, request, *args, **kwargs):
HungNgien marked this conversation as resolved.
Show resolved Hide resolved
form = self.get_form()
return self.form_valid(form) if form.is_valid() else self.form_invalid(form)

def form_valid(self, form):
HungNgien marked this conversation as resolved.
Show resolved Hide resolved
url = form.cleaned_data['redirect_url']

try:
result = self.create_oauth_application(url)
except (IntegrityError, ValidationError, ObjectDoesNotExist, Exception) as e:
HungNgien marked this conversation as resolved.
Show resolved Hide resolved
error_type = type(e).__name__
error_message = str(e)
HungNgien marked this conversation as resolved.
Show resolved Hide resolved
logger.error(f"Error while creating OAuth2 application: {error_type} - {error_message}")
HungNgien marked this conversation as resolved.
Show resolved Hide resolved
return self.render_to_response({"error_message": f"{error_type}: {error_message}"})

return self.render_to_response(self.get_context_data(form=form, result=result))

def form_invalid(self, form):
messages.error(
self.request, _('Your changes have not been saved, see below for errors.')
)
return super().form_invalid(form)

def get_success_url(self):
return reverse('control:admin.global.sso')

def create_oauth_application(self, redirect_uris):
application, created = OAuthApplication.objects.get_or_create(
redirect_uris=redirect_uris,
defaults={
'name': "Talk SSO Client",
'client_type': OAuthApplication.CLIENT_CONFIDENTIAL,
'authorization_grant_type': OAuthApplication.GRANT_AUTHORIZATION_CODE,
'user': None,
'client_id': secrets.token_urlsafe(32),
'client_secret': secrets.token_urlsafe(64),
'hash_client_secret': False,
'skip_authorization': True,
},
)

return {
"success_message": (
"Successfully created OAuth2 Application"
if created
else "OAuth2 Application with this redirect URI already exists"
),
"client_id": application.client_id,
"client_secret": application.client_secret,
}


class DeleteOAuthApplicationView(AdministratorPermissionRequiredMixin, DeleteView):
model = OAuthApplication
success_url = reverse_lazy('control:admin.global.sso')


class UpdateCheckView(StaffMemberRequiredMixin, FormView):
template_name = 'pretixcontrol/global_update.html'
form_class = UpdateSettingsForm
Expand All @@ -49,7 +127,9 @@ def form_valid(self, form):
return super().form_valid(form)

def form_invalid(self, form):
messages.error(self.request, _('Your changes have not been saved, see below for errors.'))
messages.error(
self.request, _('Your changes have not been saved, see below for errors.')
)
return super().form_invalid(form)

def get_context_data(self, **kwargs):
Expand Down
Empty file.
11 changes: 11 additions & 0 deletions src/pretix/plugins/socialauth/adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from allauth.core.exceptions import ImmediateHttpResponse
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from django.http import HttpResponseRedirect
from django.urls import reverse


class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
def on_authentication_error(
self, request, provider, error=None, exception=None, extra_context=None
):
raise ImmediateHttpResponse(HttpResponseRedirect(reverse("control:index")))
19 changes: 19 additions & 0 deletions src/pretix/plugins/socialauth/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _

from pretix import __version__ as version


class SocialAuthApp(AppConfig):
name = 'pretix.plugins.socialauth'
verbose_name = _("SocialAuth")

class PretixPluginMeta:
name = _("SocialAuth")
author = _("the pretix team")
version = version
featured = True
description = _("This plugin allows you to login via social networks")


default_app_config = 'pretix.plugins.socialauth.PaypalApp'
51 changes: 51 additions & 0 deletions src/pretix/plugins/socialauth/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from allauth.socialaccount.adapter import get_adapter

from pretix.base.auth import BaseAuthBackend
from pretix.helpers.urls import build_absolute_uri

adapter = get_adapter()


class MediaWikiBackend(BaseAuthBackend):
identifier = 'mediawiki'

@property
def verbose_name(self):
return "Login with MediaWiki"

def authentication_url(self, request):
return (
adapter.get_provider(request, 'mediawiki').get_login_url(request)
+ "?next="
HungNgien marked this conversation as resolved.
Show resolved Hide resolved
+ build_absolute_uri("plugins:socialauth:mediawiki.oauth.return")
)


class GoogleBackend(BaseAuthBackend):
identifier = 'google'

@property
def verbose_name(self):
return "Login with Google"

def authentication_url(self, request):
return (
adapter.get_provider(request, 'google').get_login_url(request)
+ "?next="
+ build_absolute_uri("plugins:socialauth:mediawiki.oauth.return")
)


class GithubBackend(BaseAuthBackend):
identifier = 'github'

@property
def verbose_name(self):
return "Login with Github"

def authentication_url(self, request):
return (
adapter.get_provider(request, 'github').get_login_url(request)
+ "?next="
+ build_absolute_uri("plugins:socialauth:mediawiki.oauth.return")
)
7 changes: 7 additions & 0 deletions src/pretix/plugins/socialauth/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.urls import re_path as url

from . import views

urlpatterns = [
url(r'^oauth_return$', views.oauth_return, name='mediawiki.oauth.return')
]
30 changes: 30 additions & 0 deletions src/pretix/plugins/socialauth/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import logging

from django.conf import settings
from django.contrib import messages
from django.shortcuts import redirect

from pretix.base.models import User
from pretix.control.views.auth import process_login_and_set_cookie

logger = logging.getLogger(__name__)


def oauth_return(request):
try:
user, _ = User.objects.get_or_create(
email=request.user.email,
defaults={
'locale': getattr(request, 'LANGUAGE_CODE', settings.LANGUAGE_CODE),
'timezone': getattr(request, 'timezone', settings.TIME_ZONE),
'auth_backend': 'native',
'password': '',
},
)
return process_login_and_set_cookie(request, user, False)
except AttributeError:
messages.error(
request, _('Error while authorizing: no email address available.')
)
logger.error('Error while authorizing: user has no email address.')
return redirect('control:auth.login')
Loading
Loading