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

customizable OIDC #1088

Merged
merged 23 commits into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ephios/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
WorkingHours,
)
from ephios.core.models.events import PlaceholderParticipation
from ephios.core.models.users import EphiosOIDCClient

admin.site.register(UserProfile)
admin.site.register(Qualification)
Expand All @@ -29,3 +30,4 @@
admin.site.register(LocalParticipation)
admin.site.register(PlaceholderParticipation)
admin.site.register(Notification)
admin.site.register(EphiosOIDCClient)
13 changes: 13 additions & 0 deletions ephios/core/dynamic_preferences_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
global_preferences_registry,
)
from dynamic_preferences.types import (
BooleanPreference,
DateTimePreference,
ModelMultipleChoicePreference,
MultipleChoicePreference,
Expand Down Expand Up @@ -101,6 +102,18 @@ def set_last_call(cls, value):
preferences[f"{cls.section.name}__{cls.name}"] = value


@global_preferences_registry.register
class HideLoginForm(BooleanPreference):
name = "hide_login_form"
verbose_name = _("Hide login form")
help_text = _(
"Hide the login form on the login page. This only takes effect if you configured at least one identity provider."
)
default = False
section = general_global_section
required = False


@user_preferences_registry.register
class NotificationPreference(JSONPreference):
name = "notifications"
Expand Down
6 changes: 6 additions & 0 deletions ephios/core/forms/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,3 +470,9 @@ class UserOwnDataForm(ModelForm):
class Meta:
model = UserProfile
fields = ["preferred_language"]


class OIDCDiscoveryForm(Form):
url = forms.URLField(
label=_("OIDC Provider URL"), help_text=_("The base URL of the OIDC provider.")
)
103 changes: 103 additions & 0 deletions ephios/core/migrations/0022_ephiosoidcclient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Generated by Django 4.2.4 on 2023-10-08 18:50

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("core", "0021_userprofile_preferred_language_and_more"),
]

operations = [
migrations.CreateModel(
name="EphiosOIDCClient",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"label",
models.CharField(
help_text="The label displayed to users attempting to log in with this provider.",
max_length=255,
verbose_name="label",
),
),
(
"client_id",
models.CharField(
help_text="Your client id provided by the OIDC provider.",
max_length=255,
verbose_name="client id",
),
),
(
"client_secret",
models.CharField(
help_text="Your client secret provided by the OIDC provider.",
max_length=255,
verbose_name="client secret",
),
),
(
"scopes",
models.CharField(
default="openid profile email",
help_text="The OIDC scopes to request from the provider. Separate multiple scopes with spaces. Use the default value if you are unsure.",
max_length=255,
verbose_name="scopes",
),
),
(
"auth_endpoint",
models.URLField(
help_text="The OIDC authorization endpoint.",
verbose_name="authorization endpoint",
),
),
(
"token_endpoint",
models.URLField(
help_text="The OIDC token endpoint.", verbose_name="token endpoint"
),
),
(
"user_endpoint",
models.URLField(
help_text="The OIDC user endpoint.", verbose_name="user endpoint"
),
),
(
"jwks_endpoint",
models.URLField(
blank=True,
help_text="The OIDC JWKS endpoint. A less secure signing method will be used if this is not provided.",
null=True,
verbose_name="JWKS endpoint",
),
),
(
"end_session_endpoint",
models.URLField(
blank=True,
help_text="The OIDC end session endpoint, if supported by your provider.",
null=True,
verbose_name="end session endpoint",
),
),
(
"default_groups",
models.ManyToManyField(
blank=True,
help_text="The groups that users logging in with this provider will be added to.",
to="auth.group",
verbose_name="default groups",
),
),
],
),
]
58 changes: 58 additions & 0 deletions ephios/core/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,3 +532,61 @@ def as_plaintext(self):

def get_actions(self):
return self.notification_type.get_actions(self)


class EphiosOIDCClient(Model):
label = models.CharField(
max_length=255,
verbose_name=_("label"),
help_text=_("The label displayed to users attempting to log in with this provider."),
)
client_id = models.CharField(
max_length=255,
verbose_name=_("client id"),
help_text=_("Your client id provided by the OIDC provider."),
)
client_secret = models.CharField(
max_length=255,
verbose_name=_("client secret"),
help_text=_("Your client secret provided by the OIDC provider."),
)
scopes = models.CharField(
max_length=255,
default="openid profile email",
verbose_name=_("scopes"),
help_text=_(
"The OIDC scopes to request from the provider. Separate multiple scopes with spaces. Use the default value if you are unsure."
),
)
auth_endpoint = models.URLField(
verbose_name=_("authorization endpoint"), help_text=_("The OIDC authorization endpoint.")
)
token_endpoint = models.URLField(
verbose_name=_("token endpoint"), help_text=_("The OIDC token endpoint.")
)
user_endpoint = models.URLField(
verbose_name=_("user endpoint"), help_text=_("The OIDC user endpoint.")
)
jwks_endpoint = models.URLField(
blank=True,
null=True,
verbose_name=_("JWKS endpoint"),
help_text=_(
"The OIDC JWKS endpoint. A less secure signing method will be used if this is not provided."
),
)
end_session_endpoint = models.URLField(
blank=True,
null=True,
verbose_name=_("end session endpoint"),
help_text=_("The OIDC end session endpoint, if supported by your provider."),
)
default_groups = models.ManyToManyField(
Group,
blank=True,
verbose_name=_("default groups"),
help_text=_("The groups that users logging in with this provider will be added to."),
)

def __str__(self):
return _("OIDC client {label}").format(label=self.label)
15 changes: 15 additions & 0 deletions ephios/core/templates/core/ephiosoidcclient_confirm_delete.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% extends "core/settings/settings_base.html" %}
{% load i18n %}

{% block settings_content %}
<form method="post">
{% csrf_token %}
<p>{% blocktrans trimmed with label=ephiosoidcclient.label %}
Are you sure you want to delete the identity provider "{{ label }}"?
Users will no longer be able to log in with this identity provider.
Please make sure that they have an alternative means of logging in.
{% endblocktrans %}</p>
<a role="button" class="btn btn-secondary" href="{% url "core:settings_oidc_list" %}">{% translate "Back" %}</a>
<button type="submit" class="btn btn-danger">{% translate "Delete" %}</button>
</form>
{% endblock %}
17 changes: 17 additions & 0 deletions ephios/core/templates/core/ephiosoidcclient_discovery.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% extends "core/settings/settings_base.html" %}
{% load i18n %}
{% load crispy_forms_filters %}

{% block settings_content %}
<div class="alert alert-info" role="alert">
{% blocktranslate trimmed %}
We can try to auto-discover certain values for your OpenID Connect Provider. To start the discovery, please enter the base URL of your OpenID Connect Provider below.
{% endblocktranslate %}
</div>
<form method="POST">
{% csrf_token %}
{{ form|crispy }}
<input class="btn btn-primary" type="submit" value="{% translate "Start auto-discovery" %}">
<a class="btn btn-secondary" href="{% url "core:settings_oidc_create" %}">{% translate "Continue without auto-discovery" %}</a>
</form>
{% endblock %}
11 changes: 11 additions & 0 deletions ephios/core/templates/core/ephiosoidcclient_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{% extends "core/settings/settings_base.html" %}
{% load i18n %}
{% load crispy_forms_filters %}

{% block settings_content %}
<form method="POST">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn btn-primary">{% translate "Save" %}</button>
</form>
{% endblock %}
35 changes: 35 additions & 0 deletions ephios/core/templates/core/ephiosoidcclient_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{% extends "core/settings/settings_base.html" %}
{% load i18n %}

{% block settings_content %}
<div class="alert alert-info" role="alert">
{% blocktranslate trimmed %}
ephios can use other services (identity providers) to authenticate users via OpenID Connect (OIDC), e.g. a Nextcloud instance. You can set up multiple identity providers and users can choose which one to use when logging in.
{% endblocktranslate %}
</div>
<a class="btn btn-secondary" href="{% url "core:settings_oidc_discovery" %}"><span
class="fa fa-plus"></span> {% translate "Add identity provider" %}</a>
<table id="event_table" class="table table-striped display mt-2">
<thead>
<tr>
<th>{% translate "Label" %}</th>
<th>{% translate "Action" %}</th>
</tr>
</thead>
<tbody>
{% for client in ephiosoidcclient_list %}
<tr>
<td class="break-word">{{ client.label }}</td>
<td class="d-flex">
<a class="btn btn-secondary btn-sm text-nowrap"
href="{% url "core:settings_oidc_edit" client.pk %}"><span
class="fa fa-edit"></span> <span class="d-none d-md-inline">{% translate "Edit" %}</span></a>
<a class="btn btn-secondary btn-sm text-nowrap ms-1"
href="{% url "core:settings_oidc_delete" client.pk %}"><span
class="fa fa-trash-alt"></span> <span class="d-none d-md-inline">{% translate "Delete" %}</span></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
6 changes: 4 additions & 2 deletions ephios/core/templatetags/settings_extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from django.conf import settings
from dynamic_preferences.registries import global_preferences_registry

from ephios.core.models.users import EphiosOIDCClient

register = template.Library()


Expand All @@ -15,8 +17,8 @@ def available_management_settings_sections(request):


@register.simple_tag
def oidc_client_enabled():
return settings.ENABLE_OIDC_CLIENT
def oidc_clients():
return EphiosOIDCClient.objects.all()


@register.simple_tag
Expand Down
26 changes: 26 additions & 0 deletions ephios/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@
UserProfilePasswordTokenRevokationView,
UserProfileUpdateView,
)
from ephios.core.views.auth import (
OIDCCallbackView,
OIDCClientCreateView,
OIDCClientDeleteView,
OIDCClientDiscoveryView,
OIDCClientListView,
OIDCClientUpdateView,
OIDCInitiateView,
OIDCLogoutView,
)
from ephios.core.views.bulk import EventBulkDeleteView
from ephios.core.views.consequences import ConsequenceUpdateView
from ephios.core.views.event import (
Expand Down Expand Up @@ -190,6 +200,19 @@
EventTypeDeleteView.as_view(),
name="settings_eventtype_delete",
),
path("settings/oidc/", OIDCClientListView.as_view(), name="settings_oidc_list"),
path("settings/oidc/create/", OIDCClientCreateView.as_view(), name="settings_oidc_create"),
path(
"settings/oidc/discovery/",
OIDCClientDiscoveryView.as_view(),
name="settings_oidc_discovery",
),
path("settings/oidc/<int:pk>/edit/", OIDCClientUpdateView.as_view(), name="settings_oidc_edit"),
path(
"settings/oidc/<int:pk>/delete/",
OIDCClientDeleteView.as_view(),
name="settings_oidc_delete",
),
path("groups/", GroupListView.as_view(), name="group_list"),
path("groups/<int:pk>/edit/", GroupUpdateView.as_view(), name="group_edit"),
path("groups/<int:pk>/delete/", GroupDeleteView.as_view(), name="group_delete"),
Expand Down Expand Up @@ -251,4 +274,7 @@
WorkingHourCreateView.as_view(),
name="workinghours_add",
),
path("oidc/initiate/<int:client>/", OIDCInitiateView.as_view(), name="oidc_initiate"),
path("oidc/callback/", OIDCCallbackView.as_view(), name="oidc_callback"),
path("oidc/logout/", OIDCLogoutView.as_view(), name="oidc_logout"),
]
Loading