Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • hansegucker/AlekSIS-Core
  • pinguin/AlekSIS-Core
  • AlekSIS/official/AlekSIS-Core
  • sunweaver/AlekSIS-Core
  • sggua/AlekSIS-Core
  • edward/AlekSIS-Core
  • magicfelix/AlekSIS-Core
7 results
Show changes
Commits on Source (18)
Showing
with 284 additions and 61 deletions
......@@ -18,6 +18,9 @@ Added
~~~~~
* OpenID Connect RSA keys can now be passed as string in config files
* Views filtering for person names now also search the username of a linked user
* OAuth2 applications now take an icon which is shown in the authorization progress.
* Add support for hiding the main side nav in ``base.html``.
Fixed
~~~~~
......@@ -25,6 +28,8 @@ Fixed
* GroupManager.get_queryset() returned an incomplete QuerySet
* OAuth was broken by a non-semver-adhering django-oauth-toolkit update
* Too long texts in chips didn't result in a larger chip.
* The data check results list view didn't work if a related object had been deleted in the meanwhile.
* Socialaccount login template was not overriden
Changed
~~~~~~~
......@@ -39,6 +44,8 @@ Changed
* [Docker] Base image now contains curl, grep, less, sed, and pspg
* Views raising a 404 error can now customise the message that is displayed on the error page
* OpenID Connect is enabled by default now, without RSA support
* Login and authorization pages for OAuth2/OpenID Connect now indicate that the user is in progress
to authorize an external application.
`2.5`_ – 2022-01-02
-------------------
......
......@@ -54,6 +54,7 @@ class PersonFilter(FilterSet):
"additional_name__icontains",
"last_name__icontains",
"short_name__icontains",
"user__username__icontains",
],
label=_("Search by name"),
)
......
......@@ -775,6 +775,7 @@ class OAuthApplicationForm(forms.ModelForm):
model = OAuthApplication
fields = (
"name",
"icon",
"client_id",
"client_secret",
"client_type",
......
# Generated by Django 3.2.11 on 2022-01-08 13:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0030_user_attributes'),
]
operations = [
migrations.AddField(
model_name='oauthapplication',
name='icon',
field=models.ImageField(blank=True, help_text='This image will be shown as icon in the authorization flow. It should be squared.', null=True, upload_to='', verbose_name='Icon'),
),
]
......@@ -1248,6 +1248,15 @@ class OAuthApplication(AbstractApplication):
blank=True,
)
icon = models.ImageField(
verbose_name=_("Icon"),
blank=True,
null=True,
help_text=_(
"This image will be shown as icon in the authorization flow. It should be squared."
),
)
def allows_grant_type(self, *grant_types: set[str]) -> bool:
allowed_grants = get_site_preferences()["auth__oauth_allowed_grants"]
......
......@@ -64,6 +64,10 @@ header, main, footer {
margin-left: 300px;
}
.without-menu header, .without-menu main, .without-menu footer {
margin-left: 0;
}
@media only screen and (max-width: 992px) {
header, main, footer {
margin-left: 0;
......@@ -800,3 +804,15 @@ main figure.alert {
height: 24px;
margin-bottom: 8px;
}
.application-circle {
border-radius: 50%;
width: 20vh;
height: 20vh;
}
.application-circle img {
@extend .application-circle;
object-fit: cover;
}
......@@ -58,7 +58,7 @@
{% block extra_head %}{% endblock %}
</head>
<body>
<body {% if no_menu %}class="without-menu"{% endif %}>
<header>
<!-- Menu button (sidenav) -->
......@@ -88,32 +88,36 @@
</nav>
<!-- Main nav (sidenav) -->
<ul id="slide-out" class="sidenav sidenav-fixed">
<li class="logo">
{% static "img/aleksis-banner.svg" as aleksis_banner %}
<a id="logo-container" href="/" class="brand-logo">
<img src="{% firstof request.site.preferences.theme__logo.url aleksis_banner %}"
alt="{{ request.site.preferences.general__title }} – Logo">
</a>
</li>
{% has_perm 'core.search_rule' user as search %}
{% if search %}
<li class="search">
<form method="get" action="{% url "haystack_search" %}" id="search-form" class="autocomplete">
<div class="search-wrapper">
<input id="search" name="q" type="search" enterkeyhint="search" placeholder="{% trans "Search" %}">
<button class="btn btn-flat search-button" type="submit" aria-label="{% trans "Search" %}">
<i class="material-icons">search</i>
</button>
<div class="progress" id="search-loader"><div class="indeterminate"></div></div>
</div>
</form>
{% if not no_menu %}
<ul id="slide-out" class="sidenav sidenav-fixed">
<li class="logo">
{% static "img/aleksis-banner.svg" as aleksis_banner %}
<a id="logo-container" href="/" class="brand-logo">
<img src="{% firstof request.site.preferences.theme__logo.url aleksis_banner %}"
alt="{{ request.site.preferences.general__title }} – Logo">
</a>
</li>
{% endif %}
<li class="no-padding">
{% include "core/partials/sidenav.html" %}
</li>
</ul>
{% has_perm 'core.search_rule' user as search %}
{% if search %}
<li class="search">
<form method="get" action="{% url "haystack_search" %}" id="search-form" class="autocomplete">
<div class="search-wrapper">
<input id="search" name="q" type="search" enterkeyhint="search" placeholder="{% trans "Search" %}">
<button class="btn btn-flat search-button" type="submit" aria-label="{% trans "Search" %}">
<i class="material-icons">search</i>
</button>
<div class="progress" id="search-loader">
<div class="indeterminate"></div>
</div>
</div>
</form>
</li>
{% endif %}
<li class="no-padding">
{% include "core/partials/sidenav.html" %}
</li>
</ul>
{% endif %}
</header>
......
......@@ -50,27 +50,29 @@
</thead>
<tbody>
{% for result in results %}
<tr>
<td>
<code>{{ result.id }}</code>
</td>
<td>{% verbose_name_object result.related_object %}</td>
<td>{{ result.related_object }}</td>
<td>{{ result.related_check.problem_name }}</td>
<td>
<a class="btn-flat waves-effect waves-light" href="{{ result.related_object.get_absolute_url }}">
{% trans "Show object" %}
</a>
</td>
<td>
{% for option_name, option in result.related_check.solve_options.items %}
<a class="btn btn-margin waves-effect waves-light"
href="{% url "data_check_solve" result.pk option_name %}">
{{ option.verbose_name }}
{% if result.related_object %}
<tr>
<td>
<code>{{ result.id }}</code>
</td>
<td>{% verbose_name_object result.related_object %}</td>
<td>{{ result.related_object }}</td>
<td>{{ result.related_check.problem_name }}</td>
<td>
<a class="btn-flat waves-effect waves-light" href="{{ result.related_object.get_absolute_url }}">
{% trans "Show object" %}
</a>
{% endfor %}
</td>
</tr>
</td>
<td>
{% for option_name, option in result.related_check.solve_options.items %}
<a class="btn btn-margin waves-effect waves-light"
href="{% url "data_check_solve" result.pk option_name %}">
{{ option.verbose_name }}
</a>
{% endfor %}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
......
......@@ -6,7 +6,7 @@
{% block page_title %}{% blocktrans %}Register OAuth2 Application{% endblocktrans %}{% endblock %}
{% block content %}
<form method="post">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% form form=form %}{% endform %}
{% include "core/partials/save_button.html" %}
......
......@@ -22,6 +22,18 @@
</a>
<table class="responsive-table">
<tbody>
<tr>
<th>{% trans "Icon" %}</th>
<td>
{% if application.icon %}
<div class="application-circle materialboxed z-depth-2">
<img src="{{ application.icon.url }}" alt="{{ oauth_application.name }}" class="hundred-percent">
</div>
{% else %}
{% endif %}
</td>
</tr>
<tr>
<th>
{% trans "Client id" %}
......
......@@ -6,7 +6,7 @@
{% block page_title %}{% blocktrans %}Edit OAuth2 Application{% endblocktrans %}{% endblock %}
{% block content %}
<form method="post">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% form form=form %}{% endform %}
{% include "core/partials/save_button.html" %}
......
......@@ -12,8 +12,13 @@
</a>
<div class="collection">
{% for application in applications %}
<a class="collection-item" href="{% url "oauth2_application" application.id %}">
{{ application.name }}
<a class="collection-item avatar" href="{% url "oauth2_application" application.id %}">
{% if application.icon %}
<img src="{{ application.icon.url }}" alt="{{ application.name }}" class="circle">
{% endif %}
<span class="title">
{{ application.name }}
</span>
</a>
{% empty %}
<div class="collection-item flow-text">
......
......@@ -12,8 +12,15 @@
<div class="col s12 m10 l8 xl6">
<div class="card">
<div class="card-content">
<div class="card-title">
{% trans "Authorize" %} {{ application.name }}
{% if application.icon %}
<div class="center-via-flex margin-bottom">
<div class="application-circle materialboxed z-depth-2">
<img src="{{ application.icon.url }}" alt="{{ application.name }}" class="hundred-percent">
</div>
</div>
{% endif %}
<div class="card-title {% if application.icon %}center{% endif %}">
{% blocktrans with name=application.name %}Authorize {{ name }}{% endblocktrans %}
</div>
<p class="margin-bottom">{% trans "The application requests access to the following scopes:" %}</p>
{% for scope in scopes_descriptions %}
......
{% extends "core/base.html" %}
{% load i18n material_form account %}
{% block browser_title %}{% trans "Authorize" %}{% endblock %}
{% block page_title %}{% trans "Authorize" %}{% endblock %}
{% block content %}
{% if process == "connect" %}
<p class="flow-text">
<i class="material-icons left">info</i>
{% blocktrans with provider.name as provider %}You are about to connect a new third party account from {{ provider }}.{% endblocktrans %}
</p>
<form method="post">
{% csrf_token %}
{% form form=form %}{% endform %}
{% trans "Confirm" as caption %}
{% include "core/partials/save_button.html" with caption=caption icon="how_to_reg" %}
</form>
{% else %}
<p class="flow-text">
<i class="material-icons left small">info</i>
{% blocktrans with provider.name as provider %}You are about to sign in using a third party account from {{ provider }}.{% endblocktrans %}
</p>
<form method="post">
{% csrf_token %}
{% form form=form %}{% endform %}
{% trans "Continue" as caption %}
{% include "core/partials/save_button.html" with caption=caption icon="how_to_reg" %}
</form>
{% endif %}
{% endblock %}
......@@ -16,7 +16,17 @@
<div class="col s12 m10 l8 xl6">
<div class="card">
<div class="card-content">
{% if wizard.steps.current == 'auth' and socialaccount_providers %}
{% if oauth and oauth_application.icon %}
<div class="center-via-flex margin-bottom">
<div class="application-circle materialboxed z-depth-2">
<img src="{{ oauth_application.icon.url }}" alt="{{ oauth_application.name }}"
class="hundred-percent">
</div>
</div>
<div class="card-title center">
{% blocktrans with name=oauth_application.name %}Login for {{ name }}{% endblocktrans %}
</div>
{% elif wizard.steps.current == 'auth' and socialaccount_providers %}
<div class="card-title">{% trans "Login with username and password" %}</div>
{% else %}
<div class="card-title">{% trans "Login" %}</div>
......@@ -30,12 +40,21 @@
</p>
</div>
{% elif wizard.steps.current == 'auth' %}
<div class="alert primary">
<p>
<i class="material-icons left">info</i>
{% blocktrans %}Please login to see this page.{% endblocktrans %}
</p>
</div>
{% if oauth %}
<div class="alert primary">
<p>
<i class="material-icons left">info</i>
{% blocktrans %}Please login with your account to use the external application.{% endblocktrans %}
</p>
</div>
{% else %}
<div class="alert primary">
<p>
<i class="material-icons left">info</i>
{% blocktrans %}Please login to see this page.{% endblocktrans %}
</p>
</div>
{% endif %}
{% endif %}
{% if not wizard.steps.current == "auth" %}
<div class="alert primary">
......
......@@ -134,6 +134,11 @@ urlpatterns = [
views.OAuth2EditView.as_view(),
name="edit_oauth2_application",
),
path(
"oauth/authorize/",
views.CustomAuthorizationView.as_view(),
name="oauth2_provider:authorize",
),
path("oauth/", include("oauth2_provider.urls", namespace="oauth2_provider")),
path("__i18n__/", include("django.conf.urls.i18n")),
path(
......
from textwrap import wrap
from typing import Any, Dict, Optional, Type
from urllib.parse import urlencode
from urllib.parse import urlencode, urlparse, urlunparse
from django.apps import apps
from django.conf import settings
......@@ -18,6 +18,7 @@ from django.http import (
HttpResponseRedirect,
HttpResponseServerError,
JsonResponse,
QueryDict,
)
from django.shortcuts import get_object_or_404, redirect, render
from django.template import loader
......@@ -50,6 +51,9 @@ from haystack.query import SearchQuerySet
from haystack.utils.loading import UnifiedIndex
from health_check.views import MainView
from invitations.views import SendInvite, accept_invitation
from oauth2_provider.exceptions import OAuthToolkitError
from oauth2_provider.models import get_application_model
from oauth2_provider.views import AuthorizationView
from reversion import set_user
from reversion.views import RevisionMixin
from rules import test_rule
......@@ -1460,3 +1464,42 @@ class LoginView(AllAuthLoginView):
return render(self.request, "account/verification_sent.html")
return super().done(form_list, **kwargs)
def get_context_data(self, form, **kwargs):
"""Override context data to hide side menu and include OAuth2 application if given."""
context = super().get_context_data(form, **kwargs)
if self.request.GET.get("oauth"):
context["no_menu"] = True
if self.request.GET.get("client_id"):
application = get_application_model().objects.get(
client_id=self.request.GET["client_id"]
)
context["oauth_application"] = application
return context
class CustomAuthorizationView(AuthorizationView):
def handle_no_permission(self):
"""Override handle_no_permission to provide OAuth2 information to login page."""
redirect_obj = super().handle_no_permission()
try:
scopes, credentials = self.validate_authorization_request(self.request)
except OAuthToolkitError as error:
# Application is not available at this time.
return self.error_response(error, application=None)
login_url_parts = list(urlparse(redirect_obj.url))
querystring = QueryDict(login_url_parts[4], mutable=True)
querystring["oauth"] = "yes"
querystring["client_id"] = credentials["client_id"]
login_url_parts[4] = querystring.urlencode(safe="/")
return HttpResponseRedirect(urlunparse(login_url_parts))
def get_context_data(self, **kwargs):
"""Override context data to hide side menu."""
context = super().get_context_data(**kwargs)
context["no_menu"] = True
return context
docs/_static/create_social_application.png

41 KiB

Social accounts
===============
AlekSIS can authenticate users against third party applications using OAuth2
or OpenID.
.. warning::
Social accounts are **not** working with two factor authentication! If a user
authenticates with a social account, the two factor authentication is
ignored on login (but enforced for views that require two factor authentication later).
Configuring social account provider
-----------------------------------
For available providers, see documentation of `django-allauth
<https://django-allauth.readthedocs.io/en/latest/providers.html>`_.
A new social account provider can be configured in your configuration file
(located in ``/etc/aleksis/``).
Configuration example::
[auth.providers.gitlab]
GITLAB_URL = "https://gitlab.exmaple.com"
After configuring a new auth provider, you have to restart AlekSIS and configure client id and secret in the Backend Admin interface.
Click "Social applications" and add a new application. Choose your
provider and enter client id and secret from your application and choose
your site:
.. image:: ../_static/create_social_application.png
:width: 400
:alt: Create social application
......@@ -5,7 +5,14 @@ packages = [
{ include = "aleksis" }
]
readme = "README.rst"
include = ["CHANGELOG.rst", "LICENCE.rst", "docs/*", "docs/**/*", "aleksis/**/*.mo", "conftest.py", "tox.ini"]
include = [
{ path = "aleksis/**/*.mo", format = ["sdist", "wheel"] },
{ path = "*.rst", format = "sdist" },
{ path = "docs/*", format = "sdist" },
{ path = "docs/**/*", format = "sdist" },
{ path = "conftest.py", format = "sdist" },
{ path = "tox.ini", format = "sdist" }
]
description = "AlekSIS (School Information System) — Core"
authors = [
......