Merge branch 'master' into version-2021.2

This commit is contained in:
Jens Langhammer 2021-02-08 21:33:58 +01:00
commit db113c5e8f
41 changed files with 623 additions and 555 deletions

View file

@ -1,19 +0,0 @@
"""authentik core source form fields"""
SOURCE_FORM_FIELDS = [
"name",
"slug",
"enabled",
"authentication_flow",
"enrollment_flow",
]
SOURCE_SERIALIZER_FIELDS = [
"pk",
"name",
"slug",
"enabled",
"authentication_flow",
"enrollment_flow",
"verbose_name",
"verbose_name_plural",
]

View file

@ -1,149 +0,0 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load humanize %}
{% load authentik_utils %}
{% load admin_reflection %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon pf-icon-zone"></i>
{% trans 'Outposts' %}
</h1>
<p>{% trans "Outposts are deployments of authentik components to support different environments and protocols, like reverse proxies." %}</p>
</div>
</section>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<ak-modal-button href="{% url 'authentik_admin:outpost-create' %}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
{% trans 'Create' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<button role="ak-refresh" class="pf-c-button pf-m-primary">
{% trans 'Refresh' %}
</button>
</div>
{% include 'partials/pagination.html' %}
</div>
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
<th role="columnheader" scope="col">{% trans 'Providers' %}</th>
<th role="columnheader" scope="col">{% trans 'Health' %}</th>
<th role="columnheader" scope="col">{% trans 'Version' %}</th>
<th role="cell"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for outpost in object_list %}
<tr role="row">
<th role="columnheader">
<span>{{ outpost.name }}</span>
</th>
<td role="cell">
<span>
{{ outpost.providers.all.select_subclasses|join:", " }}
</span>
</td>
{% with states=outpost.state %}
{% if states|length > 0 %}
<td role="cell">
{% for state in states %}
<div>
{% if state.last_seen %}
<i class="fas fa-check pf-m-success"></i> {{ state.last_seen|naturaltime }}
{% else %}
<i class="fas fa-times pf-m-danger"></i> {% trans 'Unhealthy' %}
{% endif %}
</div>
{% endfor %}
</td>
<td role="cell">
{% for state in states %}
<div>
{% if not state.version %}
<i class="fas fa-question-circle"></i>
{% elif state.version_outdated %}
<i class="fas fa-times pf-m-danger"></i> {% blocktrans with is=state.version should=state.version_should %}{{ is }}, should be {{ should }}{% endblocktrans %}
{% else %}
<i class="fas fa-check pf-m-success"></i> {{ state.version }}
{% endif %}
</div>
{% endfor %}
</td>
{% else %}
<td role="cell">
<i class="fas fa-question-circle"></i>
</td>
<td role="cell">
<i class="fas fa-question-circle"></i>
</td>
{% endif %}
{% endwith %}
<td>
<ak-modal-button href="{% url 'authentik_admin:outpost-update' pk=outpost.pk %}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
{% trans 'Edit' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="{% url 'authentik_admin:outpost-delete' pk=outpost.pk %}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
{% trans 'Delete' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
{% get_htmls outpost as htmls %}
{% for html in htmls %}
{{ html|safe }}
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-map-marker pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Outposts.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any outposts." %}
{% else %}
{% trans 'Currently no outposts exist. Click the button below to create one.' %}
{% endif %}
</div>
<ak-modal-button href="{% url 'authentik_admin:outpost-create' %}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
{% trans 'Create' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
</div>
</div>
{% endif %}
</div>
</section>
{% endblock %}

View file

@ -3,7 +3,6 @@
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}
{% load authentik_utils %} {% load authentik_utils %}
{% load admin_reflection %}
{% block content %} {% block content %}
<section class="pf-c-page__main-section pf-m-light"> <section class="pf-c-page__main-section pf-m-light">

View file

@ -1,139 +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">
<h1>
<i class="pf-icon pf-icon-blueprint"></i>
{% trans 'Property Mappings' %}
</h1>
<p>{% trans "Control how authentik exposes and interprets information." %}
</p>
</div>
</section>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<ak-dropdown class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<ak-modal-button href="{% url 'authentik_admin:property-mapping-create' %}?type={{ type }}">
<button slot="trigger" class="pf-c-dropdown__menu-item">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>
{% endfor %}
</ul>
</ak-dropdown>
<button role="ak-refresh" class="pf-c-button pf-m-primary">
{% trans 'Refresh' %}
</button>
</div>
{% include 'partials/pagination.html' %}
</div>
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
<th role="columnheader" scope="col">{% trans 'Type' %}</th>
<th role="cell"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for property_mapping in object_list %}
<tr role="row">
<td role="cell">
<span>
{{ property_mapping.name }}
</span>
</td>
<td role="cell">
<span>
{{ property_mapping|verbose_name }}
</span>
</td>
<td>
<ak-modal-button href="{% url 'authentik_admin:property-mapping-update' pk=property_mapping.pk %}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
{% trans 'Edit' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="{% url 'authentik_admin:property-mapping-delete' pk=property_mapping.pk %}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
{% trans 'Delete' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="pf-icon pf-icon-blueprint pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Property Mappings.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any property mappings." %}
{% else %}
{% trans 'Currently no property mappings exist. Click the button below to create one.' %}
{% endif %}
</div>
<ak-dropdown class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<ak-modal-button href="{% url 'authentik_admin:property-mapping-create' %}?type={{ type }}">
<button slot="trigger" class="pf-c-dropdown__menu-item">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</button>
<div slot="modal"></div>
</ak-modal-button>
</li>
{% endfor %}
</ul>
</ak-dropdown>
</div>
</div>
{% endif %}
</div>
</section>
{% endblock %}

View file

@ -2,7 +2,6 @@
{% load i18n %} {% load i18n %}
{% load authentik_utils %} {% load authentik_utils %}
{% load admin_reflection %}
{% block content %} {% block content %}
<section class="pf-c-page__main-section pf-m-light"> <section class="pf-c-page__main-section pf-m-light">
@ -63,7 +62,7 @@
{% for source in object_list %} {% for source in object_list %}
<tr role="row"> <tr role="row">
<th role="columnheader"> <th role="columnheader">
<a href="/sources/{{ source.slug }}/"> <a href="/sources/{{ source.slug }}">
<div>{{ source.name }}</div> <div>{{ source.name }}</div>
{% if not source.enabled %} {% if not source.enabled %}
<small>{% trans 'Disabled' %}</small> <small>{% trans 'Disabled' %}</small>
@ -93,10 +92,6 @@
</ak-spinner-button> </ak-spinner-button>
<div slot="modal"></div> <div slot="modal"></div>
</ak-modal-button> </ak-modal-button>
{% get_links source as links %}
{% for name, href in links %}
<a class="pf-c-button pf-m-tertiary ak-root-link" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
{% endfor %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -2,7 +2,6 @@
{% load i18n %} {% load i18n %}
{% load authentik_utils %} {% load authentik_utils %}
{% load admin_reflection %}
{% block content %} {% block content %}
<section class="pf-c-page__main-section pf-m-light"> <section class="pf-c-page__main-section pf-m-light">
@ -88,10 +87,6 @@
</ak-spinner-button> </ak-spinner-button>
<div slot="modal"></div> <div slot="modal"></div>
</ak-modal-button> </ak-modal-button>
{% get_links stage as links %}
{% for name, href in links.items %}
<a class="pf-c-button pf-m-tertiary ak-root-link" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
{% endfor %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -2,7 +2,6 @@
{% load i18n %} {% load i18n %}
{% load authentik_utils %} {% load authentik_utils %}
{% load admin_reflection %}
{% block content %} {% block content %}
<section class="pf-c-page__main-section pf-m-light"> <section class="pf-c-page__main-section pf-m-light">
@ -90,10 +89,6 @@
</ak-spinner-button> </ak-spinner-button>
<div slot="modal"></div> <div slot="modal"></div>
</ak-modal-button> </ak-modal-button>
{% get_links prompt as links %}
{% for name, href in links.items %}
<a class="pf-c-button pf-m-tertiary ak-root-link" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
{% endfor %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -1,62 +0,0 @@
"""authentik admin templatetags"""
from django import template
from django.db.models import Model
from django.utils.html import mark_safe
from structlog.stdlib import get_logger
register = template.Library()
LOGGER = get_logger()
@register.simple_tag()
def get_links(model_instance):
"""Find all link_ methods on an object instance, run them and return as dict"""
prefix = "link_"
links = {}
if not isinstance(model_instance, Model):
LOGGER.warning("Model is not instance of Model", model_instance=model_instance)
return links
try:
for name in dir(model_instance):
if not name.startswith(prefix):
continue
value = getattr(model_instance, name)
if not callable(value):
continue
human_name = name.replace(prefix, "").replace("_", " ").capitalize()
link = value()
if link:
links[human_name] = link
except NotImplementedError:
pass
return links
@register.simple_tag(takes_context=True)
def get_htmls(context, model_instance):
"""Find all html_ methods on an object instance, run them and return as dict"""
prefix = "html_"
htmls = []
if not isinstance(model_instance, Model):
LOGGER.warning("Model is not instance of Model", model_instance=model_instance)
return htmls
try:
for name in dir(model_instance):
if not name.startswith(prefix):
continue
value = getattr(model_instance, name)
if not callable(value):
continue
if name.startswith(prefix):
html = value(context.get("request"))
if html:
htmls.append(mark_safe(html))
except NotImplementedError:
pass
return htmls

View file

@ -169,22 +169,22 @@ urlpatterns = [
), ),
# Stage Prompts # Stage Prompts
path( path(
"stages/prompts/", "stages_prompts/",
stages_prompts.PromptListView.as_view(), stages_prompts.PromptListView.as_view(),
name="stage-prompts", name="stage-prompts",
), ),
path( path(
"stages/prompts/create/", "stages_prompts/create/",
stages_prompts.PromptCreateView.as_view(), stages_prompts.PromptCreateView.as_view(),
name="stage-prompt-create", name="stage-prompt-create",
), ),
path( path(
"stages/prompts/<uuid:pk>/update/", "stages_prompts/<uuid:pk>/update/",
stages_prompts.PromptUpdateView.as_view(), stages_prompts.PromptUpdateView.as_view(),
name="stage-prompt-update", name="stage-prompt-update",
), ),
path( path(
"stages/prompts/<uuid:pk>/delete/", "stages_prompts/<uuid:pk>/delete/",
stages_prompts.PromptDeleteView.as_view(), stages_prompts.PromptDeleteView.as_view(),
name="stage-prompt-delete", name="stage-prompt-delete",
), ),
@ -311,11 +311,6 @@ urlpatterns = [
name="certificatekeypair-delete", name="certificatekeypair-delete",
), ),
# Outposts # Outposts
path(
"outposts/",
outposts.OutpostListView.as_view(),
name="outposts",
),
path( path(
"outposts/create/", "outposts/create/",
outposts.OutpostCreateView.as_view(), outposts.OutpostCreateView.as_view(),

View file

@ -9,36 +9,15 @@ from django.contrib.auth.mixins import (
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import ListView, UpdateView from django.views.generic import UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin from guardian.mixins import PermissionRequiredMixin
from authentik.admin.views.utils import ( from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView
BackSuccessUrlMixin,
DeleteMessageView,
SearchListMixin,
UserPaginateListMixin,
)
from authentik.lib.views import CreateAssignPermView from authentik.lib.views import CreateAssignPermView
from authentik.outposts.forms import OutpostForm from authentik.outposts.forms import OutpostForm
from authentik.outposts.models import Outpost, OutpostConfig from authentik.outposts.models import Outpost, OutpostConfig
class OutpostListView(
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
ListView,
):
"""Show list of all outposts"""
model = Outpost
permission_required = "authentik_outposts.view_outpost"
ordering = "name"
template_name = "administration/outpost/list.html"
search_fields = ["name", "_config"]
class OutpostCreateView( class OutpostCreateView(
SuccessMessageMixin, SuccessMessageMixin,
BackSuccessUrlMixin, BackSuccessUrlMixin,
@ -53,7 +32,7 @@ class OutpostCreateView(
permission_required = "authentik_outposts.add_outpost" permission_required = "authentik_outposts.add_outpost"
template_name = "generic/create.html" template_name = "generic/create.html"
success_url = reverse_lazy("authentik_admin:outposts") success_url = reverse_lazy("authentik_core:shell")
success_message = _("Successfully created Outpost") success_message = _("Successfully created Outpost")
def get_initial(self) -> Dict[str, Any]: def get_initial(self) -> Dict[str, Any]:
@ -78,7 +57,7 @@ class OutpostUpdateView(
permission_required = "authentik_outposts.change_outpost" permission_required = "authentik_outposts.change_outpost"
template_name = "generic/update.html" template_name = "generic/update.html"
success_url = reverse_lazy("authentik_admin:outposts") success_url = reverse_lazy("authentik_core:shell")
success_message = _("Successfully updated Outpost") success_message = _("Successfully updated Outpost")
@ -89,5 +68,5 @@ class OutpostDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessa
permission_required = "authentik_outposts.delete_outpost" permission_required = "authentik_outposts.delete_outpost"
template_name = "generic/delete.html" template_name = "generic/delete.html"
success_url = reverse_lazy("authentik_admin:outposts") success_url = reverse_lazy("authentik_core:shell")
success_message = _("Successfully deleted Outpost") success_message = _("Successfully deleted Outpost")

View file

@ -29,11 +29,12 @@ from authentik.flows.api import (
FlowViewSet, FlowViewSet,
StageViewSet, StageViewSet,
) )
from authentik.outposts.api import ( from authentik.outposts.api.outpost_service_connections import (
DockerServiceConnectionViewSet, DockerServiceConnectionViewSet,
KubernetesServiceConnectionViewSet, KubernetesServiceConnectionViewSet,
OutpostViewSet, ServiceConnectionViewSet,
) )
from authentik.outposts.api.outposts import OutpostViewSet
from authentik.policies.api import ( from authentik.policies.api import (
PolicyBindingViewSet, PolicyBindingViewSet,
PolicyCacheViewSet, PolicyCacheViewSet,
@ -88,6 +89,7 @@ router.register("core/users", UserViewSet)
router.register("core/tokens", TokenViewSet) router.register("core/tokens", TokenViewSet)
router.register("outposts/outposts", OutpostViewSet) router.register("outposts/outposts", OutpostViewSet)
router.register("outposts/service_connections/all", ServiceConnectionViewSet)
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet) router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)
router.register( router.register(
"outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet "outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet

View file

@ -2,7 +2,6 @@
from rest_framework.serializers import ModelSerializer, SerializerMethodField from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet
from authentik.admin.forms.source import SOURCE_SERIALIZER_FIELDS
from authentik.core.api.utils import MetaNameSerializer from authentik.core.api.utils import MetaNameSerializer
from authentik.core.models import Source from authentik.core.models import Source
@ -10,22 +9,26 @@ from authentik.core.models import Source
class SourceSerializer(ModelSerializer, MetaNameSerializer): class SourceSerializer(ModelSerializer, MetaNameSerializer):
"""Source Serializer""" """Source Serializer"""
__type__ = SerializerMethodField(method_name="get_type") object_type = SerializerMethodField()
def get_type(self, obj): def get_object_type(self, obj):
"""Get object type so that we know which API Endpoint to use to get the full object""" """Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace("source", "") return obj._meta.object_name.lower().replace("provider", "")
def to_representation(self, instance: Source):
# pyright: reportGeneralTypeIssues=false
if instance.__class__ == Source:
return super().to_representation(instance)
return instance.serializer(instance=instance).data
class Meta: class Meta:
model = Source model = Source
fields = SOURCE_SERIALIZER_FIELDS + ["__type__"] fields = SOURCE_SERIALIZER_FIELDS = [
"pk",
"name",
"slug",
"enabled",
"authentication_flow",
"enrollment_flow",
"object_type",
"verbose_name",
"verbose_name_plural",
]
class SourceViewSet(ReadOnlyModelViewSet): class SourceViewSet(ReadOnlyModelViewSet):

View file

@ -1,30 +1,28 @@
"""Outpost API Views""" """Outpost API Views"""
from rest_framework.serializers import JSONField, ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.outposts.models import ( from authentik.outposts.models import (
DockerServiceConnection, DockerServiceConnection,
KubernetesServiceConnection, KubernetesServiceConnection,
Outpost, OutpostServiceConnection,
) )
class OutpostSerializer(ModelSerializer): class ServiceConnectionSerializer(ModelSerializer):
"""Outpost Serializer""" """ServiceConnection Serializer"""
_config = JSONField()
class Meta: class Meta:
model = Outpost model = OutpostServiceConnection
fields = ["pk", "name", "providers", "service_connection", "_config"] fields = ["pk", "name"]
class OutpostViewSet(ModelViewSet): class ServiceConnectionViewSet(ModelViewSet):
"""Outpost Viewset""" """ServiceConnection Viewset"""
queryset = Outpost.objects.all() queryset = OutpostServiceConnection.objects.all()
serializer_class = OutpostSerializer serializer_class = ServiceConnectionSerializer
class DockerServiceConnectionSerializer(ModelSerializer): class DockerServiceConnectionSerializer(ModelSerializer):

View file

@ -0,0 +1,79 @@
"""Outpost API Views"""
from django.db.models import Model
from drf_yasg2.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.fields import BooleanField, CharField, DateTimeField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import JSONField, ModelSerializer, Serializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.outposts.models import Outpost
class OutpostSerializer(ModelSerializer):
"""Outpost Serializer"""
_config = JSONField()
providers_obj = ProviderSerializer(source="providers", many=True, read_only=True)
class Meta:
model = Outpost
fields = [
"pk",
"name",
"providers",
"providers_obj",
"service_connection",
"token_identifier",
"_config",
]
class OutpostHealthSerializer(Serializer):
"""Outpost health status"""
last_seen = DateTimeField(read_only=True)
version = CharField(read_only=True)
version_should = CharField(read_only=True)
version_outdated = BooleanField(read_only=True)
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
class OutpostViewSet(ModelViewSet):
"""Outpost Viewset"""
queryset = Outpost.objects.all()
serializer_class = OutpostSerializer
filterset_fields = {
"providers": ["isnull"],
}
search_fields = [
"name",
"providers__name",
]
@swagger_auto_schema(responses={200: OutpostHealthSerializer(many=True)})
@action(methods=["GET"], detail=True)
# pylint: disable=invalid-name, unused-argument
def health(self, request: Request, pk: int) -> Response:
"""Get outposts current health"""
outpost: Outpost = self.get_object()
states = []
for state in outpost.state:
states.append(
{
"last_seen": state.last_seen,
"version": state.version,
"version_should": state.version_should,
"version_outdated": state.version_outdated,
}
)
return Response(OutpostHealthSerializer(states, many=True).data)

View file

@ -9,7 +9,6 @@ from django.core.cache import cache
from django.db import models, transaction from django.db import models, transaction
from django.db.models.base import Model from django.db.models.base import Model
from django.forms.models import ModelForm from django.forms.models import ModelForm
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from docker.client import DockerClient from docker.client import DockerClient
from docker.errors import DockerException from docker.errors import DockerException
@ -33,7 +32,6 @@ from authentik.crypto.models import CertificateKeyPair
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.models import InheritanceForeignKey from authentik.lib.models import InheritanceForeignKey
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.template import render_to_string
from authentik.outposts.docker_tls import DockerInlineTLS from authentik.outposts.docker_tls import DockerInlineTLS
OUR_VERSION = parse(__version__) OUR_VERSION = parse(__version__)
@ -378,13 +376,6 @@ class Outpost(models.Model):
objects.append(provider) objects.append(provider)
return objects return objects
def html_deployment_view(self, request: HttpRequest) -> Optional[str]:
"""return template and context modal to view token and other config info"""
return render_to_string(
"outposts/deployment_modal.html",
{"outpost": self, "full_url": request.build_absolute_uri("/")},
)
def __str__(self) -> str: def __str__(self) -> str:
return f"Outpost {self.name}" return f"Outpost {self.name}"

View file

@ -1,43 +0,0 @@
{% load i18n %}
<ak-modal-button>
<button slot="trigger" class="pf-c-button pf-m-tertiary">
{% trans 'View Deployment Info' %}
</button>
<div slot="modal">
<div class="pf-c-modal-box__header">
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Outpost Deployment Info' %}</h1>
</div>
<div class="pf-c-modal-box__body" id="modal-description">
<p><a href="https://goauthentik.io/docs/outposts/outposts/#deploy">{% trans 'View deployment documentation' %}</a></p>
<form class="pf-c-form">
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">AUTHENTIK_HOST</span>
</label>
<input class="pf-c-form-control" readonly type="text" value="{{ full_url }}" />
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">AUTHENTIK_TOKEN</span>
</label>
<div>
<ak-token-copy-button identifier="{{ outpost.token_identifier }}">
{% trans 'Click to copy token' %}
</ak-token-copy-button>
</div>
</div>
<h3>{% trans 'If your authentik Instance is using a self-signed certificate, set this value.' %}</h3>
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">AUTHENTIK_INSECURE</span>
</label>
<input class="pf-c-form-control" readonly type="text" value="true" />
</div>
</form>
</div>
<footer class="pf-c-modal-box__footer pf-m-align-left">
<a class="pf-c-button pf-m-primary">{% trans 'Close' %}</a>
</footer>
</div>
</ak-modal-button>

View file

@ -59,11 +59,11 @@ class OAuth2ProviderViewSet(ModelViewSet):
queryset = OAuth2Provider.objects.all() queryset = OAuth2Provider.objects.all()
serializer_class = OAuth2ProviderSerializer serializer_class = OAuth2ProviderSerializer
@action(methods=["GET"], detail=True)
@swagger_auto_schema(responses={200: OAuth2ProviderSetupURLs(many=False)}) @swagger_auto_schema(responses={200: OAuth2ProviderSetupURLs(many=False)})
@action(methods=["GET"], detail=True)
# pylint: disable=invalid-name # pylint: disable=invalid-name
def setup_urls(self, request: Request, pk: int) -> str: def setup_urls(self, request: Request, pk: int) -> str:
"""Return metadata as XML string""" """Get Providers setup URLs"""
provider = get_object_or_404(OAuth2Provider, pk=pk) provider = get_object_or_404(OAuth2Provider, pk=pk)
data = { data = {
"issuer": provider.get_issuer(request), "issuer": provider.get_issuer(request),

View file

@ -23,6 +23,8 @@ return {
"family_name": "", "family_name": "",
"preferred_username": user.username, "preferred_username": user.username,
"nickname": user.username, "nickname": user.username,
# groups is not part of the official userinfo schema, but is a quasi-standard
"groups": [group.name for group in user.ak_groups.all()],
} }
""" """

View file

@ -253,6 +253,7 @@ class OAuthFulfillmentStage(StageView):
EventAction.AUTHORIZE_APPLICATION, EventAction.AUTHORIZE_APPLICATION,
authorized_application=application, authorized_application=application,
flow=self.executor.plan.flow_pk, flow=self.executor.plan.flow_pk,
scopes=", ".join(self.params.scope),
).from_http(self.request) ).from_http(self.request)
return redirect(self.create_response_uri()) return redirect(self.create_response_uri())
except (ClientIdError, RedirectUriError) as error: except (ClientIdError, RedirectUriError) as error:

View file

@ -54,10 +54,10 @@ class SAMLProviderViewSet(ModelViewSet):
queryset = SAMLProvider.objects.all() queryset = SAMLProvider.objects.all()
serializer_class = SAMLProviderSerializer serializer_class = SAMLProviderSerializer
@action(methods=["GET"], detail=True)
@swagger_auto_schema(responses={200: SAMLMetadataSerializer(many=False)}) @swagger_auto_schema(responses={200: SAMLMetadataSerializer(many=False)})
@action(methods=["GET"], detail=True)
# pylint: disable=invalid-name # pylint: disable=invalid-name
def metadata(self, request: Request, pk: int) -> str: def metadata(self, request: Request, pk: int) -> Response:
"""Return metadata as XML string""" """Return metadata as XML string"""
provider = get_object_or_404(SAMLProvider, pk=pk) provider = get_object_or_404(SAMLProvider, pk=pk)
metadata = DescriptorDownloadView.get_metadata(request, provider) metadata = DescriptorDownloadView.get_metadata(request, provider)

View file

@ -2,17 +2,17 @@
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.admin.forms.source import SOURCE_SERIALIZER_FIELDS from authentik.core.api.sources import SourceSerializer
from authentik.core.api.utils import MetaNameSerializer from authentik.core.api.utils import MetaNameSerializer
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
class LDAPSourceSerializer(ModelSerializer, MetaNameSerializer): class LDAPSourceSerializer(SourceSerializer):
"""LDAP Source Serializer""" """LDAP Source Serializer"""
class Meta: class Meta:
model = LDAPSource model = LDAPSource
fields = SOURCE_SERIALIZER_FIELDS + [ fields = SourceSerializer.Meta.fields + [
"server_uri", "server_uri",
"bind_cn", "bind_cn",
"bind_password", "bind_password",

View file

@ -1,18 +1,16 @@
"""OAuth Source Serializer""" """OAuth Source Serializer"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.admin.forms.source import SOURCE_SERIALIZER_FIELDS from authentik.core.api.sources import SourceSerializer
from authentik.core.api.utils import MetaNameSerializer
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource
class OAuthSourceSerializer(ModelSerializer, MetaNameSerializer): class OAuthSourceSerializer(SourceSerializer):
"""OAuth Source Serializer""" """OAuth Source Serializer"""
class Meta: class Meta:
model = OAuthSource model = OAuthSource
fields = SOURCE_SERIALIZER_FIELDS + [ fields = SourceSerializer.Meta.fields + [
"provider_type", "provider_type",
"request_token_url", "request_token_url",
"authorization_url", "authorization_url",

View file

@ -2,7 +2,6 @@
from django import forms from django import forms
from authentik.admin.forms.source import SOURCE_FORM_FIELDS
from authentik.flows.models import Flow, FlowDesignation from authentik.flows.models import Flow, FlowDesignation
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.manager import MANAGER from authentik.sources.oauth.types.manager import MANAGER
@ -27,7 +26,12 @@ class OAuthSourceForm(forms.ModelForm):
class Meta: class Meta:
model = OAuthSource model = OAuthSource
fields = SOURCE_FORM_FIELDS + [ fields = [
"name",
"slug",
"enabled",
"authentication_flow",
"enrollment_flow",
"provider_type", "provider_type",
"request_token_url", "request_token_url",
"authorization_url", "authorization_url",

View file

@ -1,19 +1,17 @@
"""SAMLSource API Views""" """SAMLSource API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.admin.forms.source import SOURCE_FORM_FIELDS from authentik.core.api.sources import SourceSerializer
from authentik.core.api.utils import MetaNameSerializer
from authentik.sources.saml.models import SAMLSource from authentik.sources.saml.models import SAMLSource
class SAMLSourceSerializer(ModelSerializer, MetaNameSerializer): class SAMLSourceSerializer(SourceSerializer):
"""SAMLSource Serializer""" """SAMLSource Serializer"""
class Meta: class Meta:
model = SAMLSource model = SAMLSource
fields = SOURCE_FORM_FIELDS + [ fields = SourceSerializer.Meta.fields + [
"issuer", "issuer",
"sso_url", "sso_url",
"slo_url", "slo_url",

View file

@ -2,7 +2,6 @@
from django import forms from django import forms
from authentik.admin.forms.source import SOURCE_FORM_FIELDS
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow, FlowDesignation from authentik.flows.models import Flow, FlowDesignation
from authentik.sources.saml.models import SAMLSource from authentik.sources.saml.models import SAMLSource
@ -28,7 +27,12 @@ class SAMLSourceForm(forms.ModelForm):
class Meta: class Meta:
model = SAMLSource model = SAMLSource
fields = SOURCE_FORM_FIELDS + [ fields = [
"name",
"slug",
"enabled",
"authentication_flow",
"enrollment_flow",
"issuer", "issuer",
"sso_url", "sso_url",
"slo_url", "slo_url",

View file

@ -24,7 +24,7 @@ require (
github.com/pelletier/go-toml v1.8.1 // indirect github.com/pelletier/go-toml v1.8.1 // indirect
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pquerna/cachecontrol v0.0.0-20200819021114-67c6ae64274f // indirect github.com/pquerna/cachecontrol v0.0.0-20200819021114-67c6ae64274f // indirect
github.com/recws-org/recws v1.2.2 github.com/recws-org/recws v1.2.1
github.com/sirupsen/logrus v1.7.0 github.com/sirupsen/logrus v1.7.0
github.com/spf13/afero v1.5.1 // indirect github.com/spf13/afero v1.5.1 // indirect
github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cast v1.3.1 // indirect

View file

@ -69,23 +69,10 @@ func (ac *APIController) Shutdown() {
} }
func (ac *APIController) startWSHandler() { func (ac *APIController) startWSHandler() {
notConnectedBackoff := 1
logger := ac.logger.WithField("loop", "ws-handler") logger := ac.logger.WithField("loop", "ws-handler")
for { for {
if !ac.wsConn.IsConnected() { if !ac.wsConn.IsConnected() {
notConnectedWait := time.Duration(notConnectedBackoff) * time.Second
logger.WithField("wait", notConnectedWait).Info("Not connected, trying again...")
time.Sleep(notConnectedWait)
notConnectedBackoff += notConnectedBackoff
// Limit backoff to max 60 seconds
if notConnectedBackoff >= 60 {
notConnectedBackoff = 60
}
ac.wsConn.CloseAndReconnect()
continue continue
} else {
// When we're connected, reset backoff to 1
notConnectedBackoff = 1
} }
var wsMsg websocketMessage var wsMsg websocketMessage
err := ac.wsConn.ReadJSON(&wsMsg) err := ac.wsConn.ReadJSON(&wsMsg)

View file

@ -1789,6 +1789,11 @@ paths:
operationId: outposts_outposts_list operationId: outposts_outposts_list
description: Outpost Viewset description: Outpost Viewset
parameters: parameters:
- name: providers__isnull
in: query
description: ''
required: false
type: string
- name: ordering - name: ordering
in: query in: query
description: Which field to use when ordering the results. description: Which field to use when ordering the results.
@ -1911,6 +1916,28 @@ paths:
required: true required: true
type: string type: string
format: uuid format: uuid
/outposts/outposts/{uuid}/health/:
get:
operationId: outposts_outposts_health
description: Get outposts current health
parameters: []
responses:
'200':
description: Outpost health status
schema:
description: ''
type: array
items:
$ref: '#/definitions/OutpostHealth'
tags:
- outposts
parameters:
- name: uuid
in: path
description: A UUID string identifying this outpost.
required: true
type: string
format: uuid
/outposts/proxy/: /outposts/proxy/:
get: get:
operationId: outposts_proxy_list operationId: outposts_proxy_list
@ -2037,6 +2064,133 @@ paths:
description: A unique integer value identifying this Proxy Provider. description: A unique integer value identifying this Proxy Provider.
required: true required: true
type: integer type: integer
/outposts/service_connections/all/:
get:
operationId: outposts_service_connections_all_list
description: ServiceConnection 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: A page number within the paginated result set.
required: false
type: integer
- name: page_size
in: query
description: Number of results to return per page.
required: false
type: integer
responses:
'200':
description: ''
schema:
required:
- count
- results
type: object
properties:
count:
type: integer
next:
type: string
format: uri
x-nullable: true
previous:
type: string
format: uri
x-nullable: true
results:
type: array
items:
$ref: '#/definitions/ServiceConnection'
tags:
- outposts
post:
operationId: outposts_service_connections_all_create
description: ServiceConnection Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/ServiceConnection'
responses:
'201':
description: ''
schema:
$ref: '#/definitions/ServiceConnection'
tags:
- outposts
parameters: []
/outposts/service_connections/all/{uuid}/:
get:
operationId: outposts_service_connections_all_read
description: ServiceConnection Viewset
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/ServiceConnection'
tags:
- outposts
put:
operationId: outposts_service_connections_all_update
description: ServiceConnection Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/ServiceConnection'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/ServiceConnection'
tags:
- outposts
patch:
operationId: outposts_service_connections_all_partial_update
description: ServiceConnection Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/ServiceConnection'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/ServiceConnection'
tags:
- outposts
delete:
operationId: outposts_service_connections_all_delete
description: ServiceConnection Viewset
parameters: []
responses:
'204':
description: ''
tags:
- outposts
parameters:
- name: uuid
in: path
description: A UUID string identifying this Outpost Service-Connection.
required: true
type: string
format: uuid
/outposts/service_connections/docker/: /outposts/service_connections/docker/:
get: get:
operationId: outposts_service_connections_docker_list operationId: outposts_service_connections_docker_list
@ -4308,7 +4462,7 @@ paths:
/providers/oauth2/{id}/setup_urls/: /providers/oauth2/{id}/setup_urls/:
get: get:
operationId: providers_oauth2_setup_urls operationId: providers_oauth2_setup_urls
description: Return metadata as XML string description: Get Providers setup URLs
parameters: [] parameters: []
responses: responses:
'200': '200':
@ -8023,6 +8177,12 @@ definitions:
items: items:
type: integer type: integer
uniqueItems: true uniqueItems: true
providers_obj:
description: ''
type: array
items:
$ref: '#/definitions/Provider'
readOnly: true
service_connection: service_connection:
title: Service connection title: Service connection
description: Select Service-Connection authentik should use to manage this description: Select Service-Connection authentik should use to manage this
@ -8030,9 +8190,36 @@ definitions:
type: string type: string
format: uuid format: uuid
x-nullable: true x-nullable: true
token_identifier:
title: Token identifier
type: string
readOnly: true
_config: _config:
title: config title: config
type: object type: object
OutpostHealth:
description: Outpost health status
type: object
properties:
last_seen:
title: Last seen
type: string
format: date-time
readOnly: true
version:
title: Version
type: string
readOnly: true
minLength: 1
version_should:
title: Version should
type: string
readOnly: true
minLength: 1
version_outdated:
title: Version outdated
type: boolean
readOnly: true
OpenIDConnectConfiguration: OpenIDConnectConfiguration:
title: Oidc configuration title: Oidc configuration
description: rest_framework Serializer for OIDC Configuration description: rest_framework Serializer for OIDC Configuration
@ -8170,6 +8357,21 @@ definitions:
description: User/Group Attribute used for the user part of the HTTP-Basic description: User/Group Attribute used for the user part of the HTTP-Basic
Header. If not set, the user's Email address is used. Header. If not set, the user's Email address is used.
type: string type: string
ServiceConnection:
description: ServiceConnection Serializer
required:
- name
type: object
properties:
pk:
title: Uuid
type: string
format: uuid
readOnly: true
name:
title: Name
type: string
minLength: 1
DockerServiceConnection: DockerServiceConnection:
description: DockerServiceConnection Serializer description: DockerServiceConnection Serializer
required: required:
@ -9157,6 +9359,10 @@ definitions:
type: string type: string
format: uuid format: uuid
x-nullable: true x-nullable: true
object_type:
title: Object type
type: string
readOnly: true
verbose_name: verbose_name:
title: Verbose name title: Verbose name
type: string type: string
@ -9165,10 +9371,6 @@ definitions:
title: Verbose name plural title: Verbose name plural
type: string type: string
readOnly: true readOnly: true
__type__:
title: 'type '
type: string
readOnly: true
LDAPSource: LDAPSource:
description: LDAP Source Serializer description: LDAP Source Serializer
required: required:
@ -9213,6 +9415,10 @@ definitions:
type: string type: string
format: uuid format: uuid
x-nullable: true x-nullable: true
object_type:
title: Object type
type: string
readOnly: true
verbose_name: verbose_name:
title: Verbose name title: Verbose name
type: string type: string
@ -9344,6 +9550,10 @@ definitions:
type: string type: string
format: uuid format: uuid
x-nullable: true x-nullable: true
object_type:
title: Object type
type: string
readOnly: true
verbose_name: verbose_name:
title: Verbose name title: Verbose name
type: string type: string
@ -9397,6 +9607,11 @@ definitions:
- sso_url - sso_url
type: object type: object
properties: properties:
pk:
title: Pbm uuid
type: string
format: uuid
readOnly: true
name: name:
title: Name title: Name
description: Source's display Name. description: Source's display Name.
@ -9425,6 +9640,18 @@ definitions:
type: string type: string
format: uuid format: uuid
x-nullable: true x-nullable: true
object_type:
title: Object type
type: string
readOnly: true
verbose_name:
title: Verbose name
type: string
readOnly: true
verbose_name_plural:
title: Verbose name plural
type: string
readOnly: true
issuer: issuer:
title: Issuer title: Issuer
description: Also known as Entity ID. Defaults the Metadata URL. description: Also known as Entity ID. Defaults the Metadata URL.

6
web/package-lock.json generated
View file

@ -899,9 +899,9 @@
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
}, },
"construct-style-sheets-polyfill": { "construct-style-sheets-polyfill": {
"version": "2.4.6", "version": "2.4.9",
"resolved": "https://registry.npmjs.org/construct-style-sheets-polyfill/-/construct-style-sheets-polyfill-2.4.6.tgz", "resolved": "https://registry.npmjs.org/construct-style-sheets-polyfill/-/construct-style-sheets-polyfill-2.4.9.tgz",
"integrity": "sha512-lU0to7dFDjKslMF+M5NUa4s0RQMBRVyZMXvD/vp7vmjdEPgziTkHSfZHQxfoIvVWajWRJUVJMLfrMwcx8fTh4A==" "integrity": "sha512-kPXZXxsp7CTr/Vs29+omUA29wTrFplkdY6jqxyv0DDWC5Ro79WmwpboH2M9KiOclbtn8r81GCFtc7+t7OjRnCw=="
}, },
"copy-descriptor": { "copy-descriptor": {
"version": "0.1.1", "version": "0.1.1",

View file

@ -18,7 +18,7 @@
"@types/codemirror": "0.0.108", "@types/codemirror": "0.0.108",
"chart.js": "^2.9.4", "chart.js": "^2.9.4",
"codemirror": "^5.59.2", "codemirror": "^5.59.2",
"construct-style-sheets-polyfill": "^2.4.6", "construct-style-sheets-polyfill": "^2.4.9",
"flowchart.js": "^1.15.0", "flowchart.js": "^1.15.0",
"lit-element": "^2.4.0", "lit-element": "^2.4.0",
"lit-html": "^1.3.0", "lit-html": "^1.3.0",

View file

@ -7,6 +7,13 @@ export interface QueryArguments {
[key: string]: number | string | boolean | null; [key: string]: number | string | boolean | null;
} }
export interface BaseInheritanceModel {
verbose_name: string;
verbose_name_plural: string;
}
export class Client { export class Client {
makeUrl(url: string[], query?: QueryArguments): string { makeUrl(url: string[], query?: QueryArguments): string {
let builtUrl = `/api/${VERSION}/${url.join("/")}/`; let builtUrl = `/api/${VERSION}/${url.join("/")}/`;

40
web/src/api/Outposts.ts Normal file
View file

@ -0,0 +1,40 @@
import { DefaultClient, PBResponse, QueryArguments } from "./Client";
import { Provider } from "./Providers";
export interface OutpostHealth {
last_seen: number;
version: string;
version_should: string;
version_outdated: boolean;
}
export class Outpost {
pk: string;
name: string;
providers: number[];
providers_obj: Provider[];
service_connection?: string;
_config: QueryArguments;
token_identifier: string;
constructor() {
throw Error();
}
static get(pk: string): Promise<Outpost> {
return DefaultClient.fetch<Outpost>(["outposts", "outposts", pk]);
}
static list(filter?: QueryArguments): Promise<PBResponse<Outpost>> {
return DefaultClient.fetch<PBResponse<Outpost>>(["outposts", "outposts"], filter);
}
static health(pk: string): Promise<OutpostHealth[]> {
return DefaultClient.fetch<OutpostHealth[]>(["outposts", "outposts", pk, "health"]);
}
static adminUrl(rest: string): string {
return `/administration/outposts/${rest}`;
}
}

View file

@ -1,12 +1,14 @@
import { DefaultClient, PBResponse, QueryArguments } from "./Client"; import { DefaultClient, BaseInheritanceModel, PBResponse, QueryArguments } from "./Client";
export class Policy { export class Policy implements BaseInheritanceModel {
pk: string; pk: string;
name: string; name: string;
constructor() { constructor() {
throw Error(); throw Error();
} }
verbose_name: string;
verbose_name_plural: string;
static get(pk: string): Promise<Policy> { static get(pk: string): Promise<Policy> {
return DefaultClient.fetch<Policy>(["policies", "all", pk]); return DefaultClient.fetch<Policy>(["policies", "all", pk]);

View file

@ -1,6 +1,6 @@
import { DefaultClient, PBResponse, QueryArguments } from "./Client"; import { BaseInheritanceModel, DefaultClient, PBResponse, QueryArguments } from "./Client";
export class Provider { export class Provider implements BaseInheritanceModel {
pk: number; pk: number;
name: string; name: string;
authorization_flow: string; authorization_flow: string;

View file

@ -1,6 +1,6 @@
import { DefaultClient, PBResponse, QueryArguments } from "./Client"; import { BaseInheritanceModel, DefaultClient, PBResponse, QueryArguments } from "./Client";
export class Source { export class Source implements BaseInheritanceModel {
pk: string; pk: string;
name: string; name: string;
slug: string; slug: string;
@ -11,6 +11,8 @@ export class Source {
constructor() { constructor() {
throw Error(); throw Error();
} }
verbose_name: string;
verbose_name_plural: string;
static get(slug: string): Promise<Source> { static get(slug: string): Promise<Source> {
return DefaultClient.fetch<Source>(["sources", "all", slug]); return DefaultClient.fetch<Source>(["sources", "all", slug]);
@ -19,4 +21,8 @@ export class Source {
static list(filter?: QueryArguments): Promise<PBResponse<Source>> { static list(filter?: QueryArguments): Promise<PBResponse<Source>> {
return DefaultClient.fetch<PBResponse<Source>>(["sources", "all"], filter); return DefaultClient.fetch<PBResponse<Source>>(["sources", "all"], filter);
} }
static adminUrl(rest: string): string {
return `/administration/sources/${rest}`;
}
} }

View file

@ -27,7 +27,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
`^/sources/(?<slug>${SLUG_REGEX})$`, `^/sources/(?<slug>${SLUG_REGEX})$`,
), ),
new SidebarItem("Providers", "/providers"), new SidebarItem("Providers", "/providers"),
new SidebarItem("Outposts", "/administration/outposts/"), new SidebarItem("Outposts", "/outposts"),
new SidebarItem("Outpost Service Connections", "/administration/outpost_service_connections/"), new SidebarItem("Outpost Service Connections", "/administration/outpost_service_connections/"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser); return User.me().then(u => u.is_superuser);
@ -41,7 +41,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Flows").children( new SidebarItem("Flows").children(
new SidebarItem("Flows", "/administration/flows/").activeWhen(`^/flows/(?<slug>${SLUG_REGEX})$`), new SidebarItem("Flows", "/administration/flows/").activeWhen(`^/flows/(?<slug>${SLUG_REGEX})$`),
new SidebarItem("Stages", "/administration/stages/"), new SidebarItem("Stages", "/administration/stages/"),
new SidebarItem("Prompts", "/administration/stages/prompts/"), new SidebarItem("Prompts", "/administration/stages_prompts/"),
new SidebarItem("Invitations", "/administration/stages/invitations/"), new SidebarItem("Invitations", "/administration/stages/invitations/"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser); return User.me().then(u => u.is_superuser);

View file

@ -65,13 +65,13 @@ export class EventInfo extends LitElement {
case "model_updated": case "model_updated":
case "model_deleted": case "model_deleted":
return html` return html`
<h3>${gettext("Affected model:")}</h3><hr> <h3>${gettext("Affected model:")}</h3>
${this.getModelInfo(this.event.context.model as EventContext)} ${this.getModelInfo(this.event.context.model as EventContext)}
`; `;
case "authorize_application": case "authorize_application":
return html`<div class="pf-l-flex"> return html`<div class="pf-l-flex">
<div class="pf-l-flex__item"> <div class="pf-l-flex__item">
<h3>${gettext("Authorized application:")}</h3><hr> <h3>${gettext("Authorized application:")}</h3>
${this.getModelInfo(this.event.context.authorized_application as EventContext)} ${this.getModelInfo(this.event.context.authorized_application as EventContext)}
</div> </div>
<div class="pf-l-flex__item"> <div class="pf-l-flex__item">
@ -83,14 +83,15 @@ export class EventInfo extends LitElement {
}), html`<ak-spinner size=${SpinnerSize.Medium}></ak-spinner>`)} }), html`<ak-spinner size=${SpinnerSize.Medium}></ak-spinner>`)}
</span> </span>
</div> </div>
</div>`; </div>
<ak-expand>${this.defaultResponse()}</ak-expand>`;
case "login_failed": case "login_failed":
return html` return html`
<h3>${gettext(`Attempted to log in as ${this.event.context.username}`)}</h3> <h3>${gettext(`Attempted to log in as ${this.event.context.username}`)}</h3>
<ak-expand>${this.defaultResponse()}</ak-expand>`; <ak-expand>${this.defaultResponse()}</ak-expand>`;
case "token_view": case "token_view":
return html` return html`
<h3>${gettext("Token:")}</h3><hr> <h3>${gettext("Token:")}</h3>
${this.getModelInfo(this.event.context.token as EventContext)}`; ${this.getModelInfo(this.event.context.token as EventContext)}`;
case "property_mapping_exception": case "property_mapping_exception":
return html`<div class="pf-l-flex"> return html`<div class="pf-l-flex">

View file

@ -0,0 +1,49 @@
import { gettext } from "django";
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { until } from "lit-html/directives/until";
import { Outpost } from "../../api/Outposts";
import { COMMON_STYLES } from "../../common/styles";
@customElement("ak-outpost-health")
export class OutpostHealth extends LitElement {
@property()
outpostId?: string;
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
render(): TemplateResult {
if (!this.outpostId) {
return html`<ak-spinner></ak-spinner>`;
}
return html`<ul>${until(Outpost.health(this.outpostId).then((oh) => {
if (oh.length === 0) {
return html`<li>
<ul>
<li role="cell">
<i class="fas fa-question-circle"></i>&nbsp;${gettext("Not available")}
</li>
</ul>
</li>`;
}
return oh.map((h) => {
return html`<li>
<ul>
<li role="cell">
<i class="fas fa-check pf-m-success"></i>&nbsp;${gettext(`Last seen: ${new Date(h.last_seen * 1000).toLocaleTimeString()}`)}
</li>
<li role="cell">
${h.version_outdated ?
html`<i class="fas fa-times pf-m-danger"></i>&nbsp;
${gettext(`${h.version}, should be ${h.version_should}`)}` :
html`<i class="fas fa-check pf-m-success"></i>&nbsp;${gettext(`Version: ${h.version}`)}`}
</li>
</ul>
</li>`;
});
}), html`<ak-spinner></ak-spinner>`)}</ul>`;
}
}

View file

@ -0,0 +1,121 @@
import { gettext } from "django";
import { customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html";
import { PBResponse } from "../../api/Client";
import { Outpost } from "../../api/Outposts";
import { TableColumn } from "../../elements/table/Table";
import { TablePage } from "../../elements/table/TablePage";
import "./OutpostHealth";
import "../../elements/buttons/SpinnerButton";
import "../../elements/buttons/ModalButton";
@customElement("ak-outpost-list")
export class OutpostListPage extends TablePage<Outpost> {
pageTitle(): string {
return "Outposts";
}
pageDescription(): string | undefined {
return "Outposts are deployments of authentik components to support different environments and protocols, like reverse proxies.";
}
pageIcon(): string {
return "pf-icon pf-icon-zone";
}
searchEnabled(): boolean {
return true;
}
apiEndpoint(page: number): Promise<PBResponse<Outpost>> {
return Outpost.list({
ordering: this.order,
page: page,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn("Name", "name"),
new TableColumn("Providers"),
new TableColumn("Health and Version"),
new TableColumn(""),
];
}
@property()
order = "name";
row(item: Outpost): TemplateResult[] {
return [
html`${item.name}`,
html`<ul>${item.providers_obj.map((p) => {
return html`<li><a href="#/providers/${p.pk}">${p.name}</a></li>`;
})}</ul>`,
html`<ak-outpost-health outpostId=${item.pk}></ak-outpost-health>`,
html`
<ak-modal-button href="${Outpost.adminUrl(`${item.pk}/update`)}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="${Outpost.adminUrl(`${item.pk}/delete`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
${gettext("Delete")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button>
<button slot="trigger" class="pf-c-button pf-m-tertiary">
${gettext("View Deployment Info")}
</button>
<div slot="modal">
<div class="pf-c-modal-box__header">
<h1 class="pf-c-title pf-m-2xl" id="modal-title">${gettext("Outpost Deployment Info")}</h1>
</div>
<div class="pf-c-modal-box__body" id="modal-description">
<p><a href="https://goauthentik.io/docs/outposts/outposts/#deploy">${gettext("View deployment documentation")}</a></p>
<form class="pf-c-form">
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">AUTHENTIK_HOST</span>
</label>
<input class="pf-c-form-control" readonly type="text" value="${document.location.toString()}" />
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">AUTHENTIK_TOKEN</span>
</label>
<div>
<ak-token-copy-button identifier="${item.token_identifier}">
${gettext("Click to copy token")}
</ak-token-copy-button>
</div>
</div>
<h3>${gettext("If your authentik Instance is using a self-signed certificate, set this value.")}</h3>
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">AUTHENTIK_INSECURE</span>
</label>
<input class="pf-c-form-control" readonly type="text" value="true" />
</div>
</form>
</div>
<footer class="pf-c-modal-box__footer pf-m-align-left">
<a class="pf-c-button pf-m-primary">${gettext("Close")}</a>
</footer>
</div>
</ak-modal-button>`,
];
}
renderToolbar(): TemplateResult {
return html`
<ak-modal-button href=${Outpost.adminUrl("create/")}>
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Create")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
${super.renderToolbar()}
`;
}
}

View file

@ -14,6 +14,7 @@ import "./pages/events/RuleListPage";
import "./pages/providers/ProviderListPage"; import "./pages/providers/ProviderListPage";
import "./pages/providers/ProviderViewPage"; import "./pages/providers/ProviderViewPage";
import "./pages/property-mappings/PropertyMappingListPage"; import "./pages/property-mappings/PropertyMappingListPage";
import "./pages/outposts/OutpostListPage";
export const ROUTES: Route[] = [ export const ROUTES: Route[] = [
// Prevent infinite Shell loops // Prevent infinite Shell loops
@ -42,4 +43,5 @@ export const ROUTES: Route[] = [
new Route(new RegExp("^/events/transports$"), html`<ak-event-transport-list></ak-event-transport-list>`), new Route(new RegExp("^/events/transports$"), html`<ak-event-transport-list></ak-event-transport-list>`),
new Route(new RegExp("^/events/rules$"), html`<ak-event-rule-list></ak-event-rule-list>`), new Route(new RegExp("^/events/rules$"), html`<ak-event-rule-list></ak-event-rule-list>`),
new Route(new RegExp("^/property-mappings$"), html`<ak-property-mapping-list></ak-property-mapping-list>`), new Route(new RegExp("^/property-mappings$"), html`<ak-property-mapping-list></ak-property-mapping-list>`),
new Route(new RegExp("^/outposts$"), html`<ak-outpost-list></ak-outpost-list>`),
]; ];