sources/oauth: migrate to webcomponents

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-03-24 20:19:10 +01:00
parent a085632b8e
commit 533a719914
24 changed files with 286 additions and 522 deletions

View file

@ -1,42 +0,0 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load authentik_utils %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
{% block above_form %}
<h1>
{% blocktrans with object_type=object|verbose_name %}
Disable {{ object_type }}
{% endblocktrans %}
</h1>
{% endblock %}
</div>
</section>
<section class="pf-c-page__main-section">
<div class="pf-l-stack">
<div class="pf-l-stack__item">
<div class="pf-c-card">
<div class="pf-c-card__body">
<form action="" method="post" class="pf-c-form">
{% csrf_token %}
<p>
{% blocktrans with object_type=object|verbose_name name=object %}
Are you sure you want to disable {{ object_type }} "{{ object }}"?
{% endblocktrans %}
</p>
<div class="pf-c-form__group pf-m-action">
<div class="pf-c-form__actions">
<input class="pf-c-button pf-m-danger" type="submit" value="{% trans 'Disable' %}" />
<a class="pf-c-button pf-m-secondary" href="{% back %}">{% trans "Back" %}</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
{% endblock %}

View file

@ -30,7 +30,7 @@
<ak-spinner-button form="main-form"> <ak-spinner-button form="main-form">
{% block action %}{% endblock %} {% block action %}{% endblock %}
</ak-spinner-button>&nbsp; </ak-spinner-button>&nbsp;
<a class="pf-c-button pf-m-secondary" href="{% back %}">{% trans "Cancel" %}</a> <a class="pf-c-button pf-m-secondary" href="#/">{% trans "Cancel" %}</a>
</footer> </footer>
{% endblock %} {% endblock %}

View file

@ -2,6 +2,10 @@
{% load static %} {% load static %}
{% block title %}
authentik API Browser
{% endblock %}
{% block head %} {% block head %}
<script type="module" src="{% static 'dist/rapidoc-min.js' %}"></script> <script type="module" src="{% static 'dist/rapidoc-min.js' %}"></script>
{% endblock %} {% endblock %}

View file

@ -107,7 +107,6 @@ router.register("core/applications", ApplicationViewSet)
router.register("core/groups", GroupViewSet) router.register("core/groups", GroupViewSet)
router.register("core/users", UserViewSet) router.register("core/users", UserViewSet)
router.register("core/user_consent", UserConsentViewSet) router.register("core/user_consent", UserConsentViewSet)
router.register("core/source_user_connections_oauth", UserOAuthSourceConnectionViewSet)
router.register("core/tokens", TokenViewSet) router.register("core/tokens", TokenViewSet)
router.register("outposts/outposts", OutpostViewSet) router.register("outposts/outposts", OutpostViewSet)
@ -129,6 +128,7 @@ router.register("events/transports", NotificationTransportViewSet)
router.register("events/rules", NotificationRuleViewSet) router.register("events/rules", NotificationRuleViewSet)
router.register("sources/all", SourceViewSet) router.register("sources/all", SourceViewSet)
router.register("sources/oauth_user_connections", UserOAuthSourceConnectionViewSet)
router.register("sources/ldap", LDAPSourceViewSet) router.register("sources/ldap", LDAPSourceViewSet)
router.register("sources/saml", SAMLSourceViewSet) router.register("sources/saml", SAMLSourceViewSet)
router.register("sources/oauth", OAuthSourceViewSet) router.register("sources/oauth", OAuthSourceViewSet)

View file

@ -94,7 +94,7 @@ class SourceViewSet(
if not policy_engine.passing: if not policy_engine.passing:
continue continue
source_settings = source.ui_user_settings source_settings = source.ui_user_settings
source_settings.initial_data["object_uid"] = str(source.pk) source_settings.initial_data["object_uid"] = source.slug
if not source_settings.is_valid(): if not source_settings.is_valid():
LOGGER.warning(source_settings.errors) LOGGER.warning(source_settings.errors)
matching_sources.append(source_settings.validated_data) matching_sources.append(source_settings.validated_data)

View file

@ -25,9 +25,6 @@
<h3>{% trans message %}</h3> <h3>{% trans message %}</h3>
{% endif %} {% endif %}
</div> </div>
{% if 'back' in request.GET %}
<a href="{% back %}" class="pf-c-button pf-m-primary pf-m-block">{% trans 'Back' %}</a>
{% endif %}
<a href="/" class="pf-c-button pf-m-primary pf-m-block">{% trans 'Go to home' %}</a> <a href="/" class="pf-c-button pf-m-primary pf-m-block">{% trans 'Go to home' %}</a>
</div> </div>
</div> </div>

View file

@ -1,37 +0,0 @@
{% load i18n %}
{% load authentik_utils %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
{% block above_form %}
<h1>
{% blocktrans with object_type=object|verbose_name %}
Delete {{ object_type }}
{% endblocktrans %}
</h1>
{% endblock %}
</div>
</section>
<section class="pf-c-page__main-section">
<div class="pf-l-stack">
<div class="pf-l-stack__item">
<div class="pf-c-card">
<div class="pf-c-card__body">
<form id="delete-form" action="" method="post" class="pf-c-form">
{% csrf_token %}
<p>
{% blocktrans with object_type=object|verbose_name name=object %}
Are you sure you want to delete {{ object_type }} "{{ object }}"?
{% endblocktrans %}
</p>
<input type="hidden" name="confirmdelete" value="yes">
</form>
</div>
</div>
</div>
</div>
</section>
<footer class="pf-c-modal-box__footer">
<input class="pf-c-button pf-m-danger" type="submit" form="delete-form" value="{% trans 'Delete' %}" />
<a class="pf-c-button pf-m-secondary" href="{% back %}">{% trans "Back" %}</a>
</footer>

View file

@ -1,26 +0,0 @@
{% load static %}
{% load i18n %}
<ak-message-container></ak-message-container>
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
{% block card_title %}
{% trans title %}
{% endblock %}
</h1>
</header>
<div class="pf-c-login__main-body">
{% block card %}
<form method="POST" class="pf-c-form">
{% include 'partials/form.html' %}
<div class="pf-c-form__group pf-m-action">
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">Log in</button>
</div>
</form>
{% endblock %}
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
</footer>

View file

@ -1,73 +0,0 @@
{% load authentik_utils %}
{% load i18n %}
{% csrf_token %}
{% if form.non_field_errors %}
<div class="pf-c-form__group">
<p class="pf-c-form__helper-text pf-m-error">
{{ form.non_field_errors }}
</p>
</div>
{% endif %}
{% for field in form %}
{% if field.field.widget|fieldtype == 'HiddenInput' %}
{{ field }}
{% else %}
<div class="pf-c-form__group">
{% if field.field.widget|fieldtype == 'RadioSelect' %}
<label class="pf-c-form__label" {% if field.field.required %}class="required" {% endif %}
for="{{ field.name }}-{{ forloop.counter0 }}">
{{ field.label }}
</label>
{% for c in field %}
<div class="radio col-sm-10">
<input type="radio" id="{{ field.name }}-{{ forloop.counter0 }}"
name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}" value="{{ c.data.value }}"
{% if c.data.selected %} checked {% endif %}>
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
</div>
{% endfor %}
{% elif field.field.widget|fieldtype == 'Select' %}
<label class="pf-c-form__label" {% if field.field.required %}class="required" {% endif %}
for="{{ field.name }}-{{ forloop.counter0 }}">
{{ field.label }}
</label>
<div class="select col-sm-10">
{{ field }}
</div>
{% if field.help_text %}
<span>
{{ field.help_text }}
</span>
{% endif %}
{% elif field.field.widget|fieldtype == 'CheckboxInput' %}
<label class="checkbox-label">
{{ field }} {{ field.label }}
</label>
{% if field.help_text %}
<span>
{{ field.help_text }}
</span>
{% endif %}
{% else %}
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
<span class="pf-c-form__label-text">{{ field.label }}</span>
{% if field.field.required %}
<span class="pf-c-form__label-required" aria-hidden="true">&#42;</span>
{% endif %}
</label>
{{ field|css_class:'pf-c-form-control' }}
{% if field.help_text %}
<span>
{{ field.help_text }}
</span>
{% endif %}
{% endif %}
{% for error in field.errors %}
<p class="pf-c-form__helper-text pf-m-error">
{{ error }}
</p>
{% endfor %}
</div>
{% endif %}
{% endfor %}

View file

@ -87,7 +87,7 @@ class StageViewSet(
user_settings = stage.ui_user_settings user_settings = stage.ui_user_settings
if not user_settings: if not user_settings:
continue continue
user_settings.initial_data["object_uid"] = stage.pk user_settings.initial_data["object_uid"] = str(stage.pk)
if not user_settings.is_valid(): if not user_settings.is_valid():
LOGGER.warning(user_settings.errors) LOGGER.warning(user_settings.errors)
matching_stages.append(user_settings.initial_data) matching_stages.append(user_settings.initial_data)

View file

@ -2,32 +2,12 @@
from django import template from django import template
from django.db.models import Model from django.db.models import Model
from django.template import Context
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.lib.utils.urls import is_url_absolute
register = template.Library() register = template.Library()
LOGGER = get_logger() LOGGER = get_logger()
@register.simple_tag(takes_context=True)
def back(context: Context) -> str:
"""Return a link back (either from GET parameter or referer."""
if "request" not in context:
return ""
request = context.get("request")
url = ""
if "HTTP_REFERER" in request.META:
url = request.META.get("HTTP_REFERER")
if "back" in request.GET:
url = request.GET.get("back")
if not is_url_absolute(url):
return url
return ""
@register.filter("fieldtype") @register.filter("fieldtype")
def fieldtype(field): def fieldtype(field):
"""Return classname""" """Return classname"""

View file

@ -15,7 +15,6 @@
{% block card %} {% block card %}
<form method="POST" class="pf-c-form"> <form method="POST" class="pf-c-form">
{% csrf_token %} {% csrf_token %}
{% include 'partials/form.html' %}
<div class="pf-c-form__group"> <div class="pf-c-form__group">
<p> <p>
<i class="pf-icon pf-icon-error-circle-o"></i> <i class="pf-icon pf-icon-error-circle-o"></i>
@ -37,29 +36,26 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% if policy_result.source_results %} {% if policy_result.source_results %}
<em>{% trans 'Explanation:' %}</em> <em>{% trans 'Explanation:' %}</em>
<ul class="pf-c-list"> <ul class="pf-c-list">
{% for source_result in policy_result.source_results %} {% for source_result in policy_result.source_results %}
<li> <li>
{% blocktrans with name=source_result.source_policy.name result=source_result.passing %} {% blocktrans with name=source_result.source_policy.name result=source_result.passing %}
Policy '{{ name }}' returned result '{{ result }}' Policy '{{ name }}' returned result '{{ result }}'
{% endblocktrans %} {% endblocktrans %}
{% if source_result.messages %} {% if source_result.messages %}
<ul class="pf-c-list"> <ul class="pf-c-list">
{% for message in source_result.messages %} {% for message in source_result.messages %}
<li>{{ message }}</li> <li>{{ message }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
{% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %}
</form> </form>
{% endblock %} {% endblock %}

View file

@ -11,10 +11,10 @@ class UserOAuthSourceConnectionSerializer(SourceSerializer):
class Meta: class Meta:
model = UserOAuthSourceConnection model = UserOAuthSourceConnection
fields = [ fields = [
"pk",
"user", "user",
"source", "source",
"identifier", "identifier",
"access_token",
] ]
@ -23,6 +23,7 @@ class UserOAuthSourceConnectionViewSet(ModelViewSet):
queryset = UserOAuthSourceConnection.objects.all() queryset = UserOAuthSourceConnection.objects.all()
serializer_class = UserOAuthSourceConnectionSerializer serializer_class = UserOAuthSourceConnectionSerializer
filterset_fields = ["source"]
def get_queryset(self): def get_queryset(self):
if not self.request: if not self.request:

View file

@ -9,8 +9,7 @@ from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
from authentik.core.models import Source, UserSourceConnection from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UILoginButton from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.flows.challenge import Challenge, ChallengeTypes
class OAuthSource(Source): class OAuthSource(Source):
@ -67,13 +66,11 @@ class OAuthSource(Source):
) )
@property @property
def ui_user_settings(self) -> Optional[Challenge]: def ui_user_settings(self) -> Optional[UserSettingSerializer]:
view_name = "authentik_sources_oauth:oauth-client-user" return UserSettingSerializer(
return Challenge(
data={ data={
"type": ChallengeTypes.shell.value, "title": f"OAuth {self.name}",
"title": self.name, "component": "ak-user-settings-source-oauth",
"component": reverse(view_name, kwargs={"source_slug": self.slug}),
} }
) )

View file

@ -1,24 +0,0 @@
{% load i18n %}
<div class="pf-c-card">
<div class="pf-pf-c-card__title">
{% blocktrans with source_name=source.name %}
Source {{ source_name }}
{% endblocktrans %}
</div>
<div class="pf-c-card__body">
{% if connections.exists %}
<p>{% trans 'Connected.' %}</p>
<a class="pf-c-button pf-m-danger ak-root-link"
href="{% url 'authentik_sources_oauth:oauth-client-disconnect' source_slug=source.slug %}">
{% trans 'Disconnect' %}
</a>
{% else %}
<p>Not connected.</p>
<a class="pf-c-button pf-m-primary ak-root-link"
href="{% url 'authentik_sources_oauth:oauth-client-login' source_slug=source.slug %}">
{% trans 'Connect' %}
</a>
{% endif %}
</div>
</div>

View file

@ -4,7 +4,6 @@ from django.urls import path
from authentik.sources.oauth.types.manager import RequestKind from authentik.sources.oauth.types.manager import RequestKind
from authentik.sources.oauth.views.dispatcher import DispatcherView from authentik.sources.oauth.views.dispatcher import DispatcherView
from authentik.sources.oauth.views.user import DisconnectView, UserSettingsView
urlpatterns = [ urlpatterns = [
path( path(
@ -17,14 +16,4 @@ urlpatterns = [
DispatcherView.as_view(kind=RequestKind.callback), DispatcherView.as_view(kind=RequestKind.callback),
name="oauth-client-callback", name="oauth-client-callback",
), ),
path(
"user/<slug:source_slug>/",
UserSettingsView.as_view(),
name="oauth-client-user",
),
path(
"user/<slug:source_slug>/disconnect/",
DisconnectView.as_view(),
name="oauth-client-disconnect",
),
] ]

View file

@ -1,70 +0,0 @@
"""authentik oauth_client user views"""
from typing import Optional
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import gettext as _
from django.views.generic import TemplateView, View
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
class UserSettingsView(LoginRequiredMixin, TemplateView):
"""Show user current connection state"""
template_name = "oauth_client/user.html"
def get_context_data(self, **kwargs):
source = get_object_or_404(OAuthSource, slug=self.kwargs.get("source_slug"))
connections = UserOAuthSourceConnection.objects.filter(
user=self.request.user, source=source
)
kwargs["source"] = source
kwargs["connections"] = connections
return super().get_context_data(**kwargs)
class DisconnectView(LoginRequiredMixin, View):
"""Delete connection with source"""
source: Optional[OAuthSource] = None
aas: Optional[UserOAuthSourceConnection] = None
def dispatch(self, request: HttpRequest, source_slug: str) -> HttpResponse:
self.source = get_object_or_404(OAuthSource, slug=source_slug)
self.aas = get_object_or_404(
UserOAuthSourceConnection, source=self.source, user=request.user
)
return super().dispatch(request, source_slug)
def post(self, request: HttpRequest, source_slug: str) -> HttpResponse:
"""Delete connection object"""
if "confirmdelete" in request.POST:
# User confirmed deletion
self.aas.delete()
messages.success(request, _("Connection successfully deleted"))
return redirect(
reverse(
"authentik_sources_oauth:oauth-client-user",
kwargs={"source_slug": self.source.slug},
)
)
return self.get(request, source_slug)
# pylint: disable=unused-argument
def get(self, request: HttpRequest, source_slug: str) -> HttpResponse:
"""Show delete form"""
return render(
request,
"generic/delete.html",
{
"object": self.source,
"delete_url": reverse(
"authentik_sources_oauth:oauth-client-disconnect",
kwargs={"source_slug": self.source.slug},
),
},
)

View file

@ -9,6 +9,7 @@ from authentik.events.models import Event
@receiver(pre_delete, sender=StaticDevice) @receiver(pre_delete, sender=StaticDevice)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def pre_delete_event(sender, instance: StaticDevice, **_): def pre_delete_event(sender, instance: StaticDevice, **_):
"""Create event before deleting Static Devices"""
# Create event with email notification # Create event with email notification
event = Event.new( event = Event.new(
"static_authenticator_disable", message="User disabled Static OTP Tokens." "static_authenticator_disable", message="User disabled Static OTP Tokens."

View file

@ -9,6 +9,7 @@ from authentik.events.models import Event
@receiver(pre_delete, sender=TOTPDevice) @receiver(pre_delete, sender=TOTPDevice)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def pre_delete_event(sender, instance: TOTPDevice, **_): def pre_delete_event(sender, instance: TOTPDevice, **_):
"""Create event before deleting TOTP Devices"""
# Create event with email notification # Create event with email notification
event = Event.new("totp_disable", message="User disabled Time-based OTP.") event = Event.new("totp_disable", message="User disabled Time-based OTP.")
event.set_user(instance.user) event.set_user(instance.user)

View file

@ -1188,147 +1188,6 @@ paths:
required: true required: true
type: string type: string
format: uuid format: uuid
/core/source_user_connections_oauth/:
get:
operationId: core_source_user_connections_oauth_list
description: Source Viewset
parameters:
- name: ordering
in: query
description: Which field to use when ordering the results.
required: false
type: string
- name: search
in: query
description: A search term.
required: false
type: string
- name: page
in: query
description: Page Index
required: false
type: integer
- name: page_size
in: query
description: Page Size
required: false
type: integer
responses:
'200':
description: ''
schema:
required:
- results
- pagination
type: object
properties:
pagination:
required:
- next
- previous
- count
- current
- total_pages
- start_index
- end_index
type: object
properties:
next:
type: number
previous:
type: number
count:
type: number
current:
type: number
total_pages:
type: number
start_index:
type: number
end_index:
type: number
results:
type: array
items:
$ref: '#/definitions/UserOAuthSourceConnection'
tags:
- core
post:
operationId: core_source_user_connections_oauth_create
description: Source Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/UserOAuthSourceConnection'
responses:
'201':
description: ''
schema:
$ref: '#/definitions/UserOAuthSourceConnection'
tags:
- core
parameters: []
/core/source_user_connections_oauth/{id}/:
get:
operationId: core_source_user_connections_oauth_read
description: Source Viewset
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/UserOAuthSourceConnection'
tags:
- core
put:
operationId: core_source_user_connections_oauth_update
description: Source Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/UserOAuthSourceConnection'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/UserOAuthSourceConnection'
tags:
- core
patch:
operationId: core_source_user_connections_oauth_partial_update
description: Source Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/UserOAuthSourceConnection'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/UserOAuthSourceConnection'
tags:
- core
delete:
operationId: core_source_user_connections_oauth_delete
description: Source Viewset
parameters: []
responses:
'204':
description: ''
tags:
- core
parameters:
- name: id
in: path
description: A unique integer value identifying this User OAuth Source Connection.
required: true
type: integer
/core/tokens/: /core/tokens/:
get: get:
operationId: core_tokens_list operationId: core_tokens_list
@ -7551,6 +7410,152 @@ paths:
type: string type: string
format: slug format: slug
pattern: ^[-a-zA-Z0-9_]+$ pattern: ^[-a-zA-Z0-9_]+$
/sources/oauth_user_connections/:
get:
operationId: sources_oauth_user_connections_list
description: Source Viewset
parameters:
- name: source
in: query
description: ''
required: false
type: string
- name: ordering
in: query
description: Which field to use when ordering the results.
required: false
type: string
- name: search
in: query
description: A search term.
required: false
type: string
- name: page
in: query
description: Page Index
required: false
type: integer
- name: page_size
in: query
description: Page Size
required: false
type: integer
responses:
'200':
description: ''
schema:
required:
- results
- pagination
type: object
properties:
pagination:
required:
- next
- previous
- count
- current
- total_pages
- start_index
- end_index
type: object
properties:
next:
type: number
previous:
type: number
count:
type: number
current:
type: number
total_pages:
type: number
start_index:
type: number
end_index:
type: number
results:
type: array
items:
$ref: '#/definitions/UserOAuthSourceConnection'
tags:
- sources
post:
operationId: sources_oauth_user_connections_create
description: Source Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/UserOAuthSourceConnection'
responses:
'201':
description: ''
schema:
$ref: '#/definitions/UserOAuthSourceConnection'
tags:
- sources
parameters: []
/sources/oauth_user_connections/{id}/:
get:
operationId: sources_oauth_user_connections_read
description: Source Viewset
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/UserOAuthSourceConnection'
tags:
- sources
put:
operationId: sources_oauth_user_connections_update
description: Source Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/UserOAuthSourceConnection'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/UserOAuthSourceConnection'
tags:
- sources
patch:
operationId: sources_oauth_user_connections_partial_update
description: Source Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/UserOAuthSourceConnection'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/UserOAuthSourceConnection'
tags:
- sources
delete:
operationId: sources_oauth_user_connections_delete
description: Source Viewset
parameters: []
responses:
'204':
description: ''
tags:
- sources
parameters:
- name: id
in: path
description: A unique integer value identifying this User OAuth Source Connection.
required: true
type: integer
/sources/saml/: /sources/saml/:
get: get:
operationId: sources_saml_list operationId: sources_saml_list
@ -10963,29 +10968,6 @@ definitions:
attributes: attributes:
title: Attributes title: Attributes
type: object type: object
UserOAuthSourceConnection:
description: OAuth Source Serializer
required:
- user
- source
- identifier
type: object
properties:
user:
title: User
type: integer
source:
title: Source
type: string
identifier:
title: Identifier
type: string
maxLength: 255
minLength: 1
access_token:
title: Access token
type: string
x-nullable: true
User: User:
title: User title: User
description: User Serializer description: User Serializer
@ -13742,6 +13724,29 @@ definitions:
title: Callback url title: Callback url
type: string type: string
readOnly: true readOnly: true
UserOAuthSourceConnection:
description: OAuth Source Serializer
required:
- user
- source
- identifier
type: object
properties:
pk:
title: ID
type: integer
readOnly: true
user:
title: User
type: integer
source:
title: Source
type: string
identifier:
title: Identifier
type: string
maxLength: 255
minLength: 1
SAMLSource: SAMLSource:
description: SAMLSource Serializer description: SAMLSource Serializer
required: required:

View file

@ -1,5 +1,6 @@
"""Test Enroll flow""" """Test Enroll flow"""
from sys import platform from sys import platform
from time import sleep
from typing import Any, Optional from typing import Any, Optional
from unittest.case import skipUnless from unittest.case import skipUnless
@ -190,6 +191,7 @@ class TestFlowsEnroll(SeleniumTestCase):
self.driver.close() self.driver.close()
self.driver.switch_to.window(self.driver.window_handles[0]) self.driver.switch_to.window(self.driver.window_handles[0])
sleep(2)
# We're now logged in # We're now logged in
wait = WebDriverWait( wait = WebDriverWait(
self.get_shadow_root("ak-interface-admin"), self.wait_timeout self.get_shadow_root("ak-interface-admin"), self.wait_timeout

View file

@ -94,6 +94,9 @@ export class AppURLManager {
static sourceSAML(slug: string, rest: string): string { static sourceSAML(slug: string, rest: string): string {
return `/source/saml/${slug}/${rest}`; return `/source/saml/${slug}/${rest}`;
} }
static sourceOAuth(slug: string, action: string): string {
return `/source/oauth/${action}/${slug}/`;
}
static providerSAML(rest: string): string { static providerSAML(rest: string): string {
return `/application/saml/${rest}`; return `/application/saml/${rest}`;
} }

View file

@ -24,6 +24,7 @@ import "./settings/UserSettingsAuthenticatorTOTP";
import "./settings/UserSettingsAuthenticatorStatic"; import "./settings/UserSettingsAuthenticatorStatic";
import "./settings/UserSettingsAuthenticatorWebAuthnDevices"; import "./settings/UserSettingsAuthenticatorWebAuthnDevices";
import "./settings/UserSettingsPassword"; import "./settings/UserSettingsPassword";
import "./settings/SourceSettingsOAuth";
@customElement("ak-user-settings") @customElement("ak-user-settings")
export class UserSettingsPage extends LitElement { export class UserSettingsPage extends LitElement {
@ -35,16 +36,16 @@ export class UserSettingsPage extends LitElement {
renderStageSettings(stage: UserSetting): TemplateResult { renderStageSettings(stage: UserSetting): TemplateResult {
switch (stage.component) { switch (stage.component) {
case "ak-user-settings-authenticator-webauthn": case "ak-user-settings-authenticator-webauthn":
return html`<ak-user-settings-authenticator-webauthn stageId=${stage.objectUid}> return html`<ak-user-settings-authenticator-webauthn objectId=${stage.objectUid}>
</ak-user-settings-authenticator-webauthn>`; </ak-user-settings-authenticator-webauthn>`;
case "ak-user-settings-password": case "ak-user-settings-password":
return html`<ak-user-settings-password stageId=${stage.objectUid}> return html`<ak-user-settings-password objectId=${stage.objectUid}>
</ak-user-settings-password>`; </ak-user-settings-password>`;
case "ak-user-settings-authenticator-totp": case "ak-user-settings-authenticator-totp":
return html`<ak-user-settings-authenticator-totp stageId=${stage.objectUid}> return html`<ak-user-settings-authenticator-totp objectId=${stage.objectUid}>
</ak-user-settings-authenticator-totp>`; </ak-user-settings-authenticator-totp>`;
case "ak-user-settings-authenticator-static": case "ak-user-settings-authenticator-static":
return html`<ak-user-settings-authenticator-static stageId=${stage.objectUid}> return html`<ak-user-settings-authenticator-static objectId=${stage.objectUid}>
</ak-user-settings-authenticator-static>`; </ak-user-settings-authenticator-static>`;
default: default:
return html`<div class="pf-u-display-flex pf-u-justify-content-center"> return html`<div class="pf-u-display-flex pf-u-justify-content-center">
@ -57,6 +58,22 @@ export class UserSettingsPage extends LitElement {
} }
} }
renderSourceSettings(source: UserSetting): TemplateResult {
switch (source.component) {
case "ak-user-settings-source-oauth":
return html`<ak-user-settings-source-oauth objectId=${source.objectUid}>
</ak-user-settings-source-oauth>`;
default:
return html`<div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75">
<ak-site-shell url="${ifDefined(source.component)}">
<div slot="body"></div>
</ak-site-shell>
</div>
</div>`;
}
}
render(): TemplateResult { render(): TemplateResult {
return html`<div class="pf-c-page"> return html`<div class="pf-c-page">
<main role="main" class="pf-c-page__main" tabindex="-1"> <main role="main" class="pf-c-page__main" tabindex="-1">
@ -89,18 +106,11 @@ export class UserSettingsPage extends LitElement {
</section>`; </section>`;
}); });
}))} }))}
${until(new SourcesApi(DEFAULT_CONFIG).sourcesAllUserSettings({}).then((sources) => { ${until(new SourcesApi(DEFAULT_CONFIG).sourcesAllUserSettings({}).then((source) => {
return sources.map((source) => { return source.map((stage) => {
// TODO: Check for non-shell sources return html`<section slot="page-${stage.objectUid}" data-tab-title="${ifDefined(stage.title)}" class="pf-c-page__main-section pf-m-no-padding-mobile">
return html`<section slot="page-${source.objectUid}" data-tab-title="${ifDefined(source.title)}" class="pf-c-page__main-section pf-m-no-padding-mobile"> ${this.renderSourceSettings(stage)}
<div class="pf-u-display-flex pf-u-justify-content-center"> </section>`;
<div class="pf-u-w-75">
<ak-site-shell url="${ifDefined(source.component)}">
<div slot="body"></div>
</ak-site-shell>
</div>
</div>
</section>`;
}); });
}))} }))}
</ak-tabs> </ak-tabs>

View file

@ -0,0 +1,50 @@
import { customElement, html, TemplateResult } from "lit-element";
import { BaseUserSettings } from "./BaseUserSettings";
import { OAuthSource, SourcesApi } from "authentik-api";
import { until } from "lit-html/directives/until";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { gettext } from "django";
import { AppURLManager } from "../../../api/legacy";
@customElement("ak-user-settings-source-oauth")
export class SourceSettingsOAuth extends BaseUserSettings {
render(): TemplateResult {
return html`${until(new SourcesApi(DEFAULT_CONFIG).sourcesOauthRead({
slug: this.objectId
}).then((source) => {
return html`<div class="pf-c-card">
<div class="pf-pf-c-card__title">
${gettext(`Source ${source.name}`)}
</div>
<div class="pf-c-card__body">
${this.renderInner(source)}
</div>
</div>`;
}))}`;
}
renderInner(source: OAuthSource): TemplateResult {
return html`${until(new SourcesApi(DEFAULT_CONFIG).sourcesOauthUserConnectionsList({
source: this.objectId
}).then((connection) => {
if (connection.results.length > 0) {
return html`<p>${gettext("Connected.")}</p>
<button class="pf-c-button pf-m-danger"
@click=${() => {
return new SourcesApi(DEFAULT_CONFIG).sourcesOauthUserConnectionsDelete({
id: connection.results[0].pk || 0
});
}}>
${gettext("Disconnect")}
</button>`;
}
return html`<p>${gettext("Not connected.")}</p>
<a class="pf-c-button pf-m-primary"
href=${AppURLManager.sourceOAuth(source.slug, "login")}>
${gettext("Connect")}
</a>`;
}))}`;
}
}