Merge branch 'master' into version-2021.2
This commit is contained in:
commit
db113c5e8f
|
@ -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",
|
||||
]
|
|
@ -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 %}
|
|
@ -3,7 +3,6 @@
|
|||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load authentik_utils %}
|
||||
{% load admin_reflection %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
|
|
|
@ -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 %}
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
{% load admin_reflection %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
|
@ -63,7 +62,7 @@
|
|||
{% for source in object_list %}
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<a href="/sources/{{ source.slug }}/">
|
||||
<a href="/sources/{{ source.slug }}">
|
||||
<div>{{ source.name }}</div>
|
||||
{% if not source.enabled %}
|
||||
<small>{% trans 'Disabled' %}</small>
|
||||
|
@ -93,10 +92,6 @@
|
|||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
{% load admin_reflection %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
|
@ -88,10 +87,6 @@
|
|||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
{% load admin_reflection %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
|
@ -90,10 +89,6 @@
|
|||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
@ -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
|
|
@ -169,22 +169,22 @@ urlpatterns = [
|
|||
),
|
||||
# Stage Prompts
|
||||
path(
|
||||
"stages/prompts/",
|
||||
"stages_prompts/",
|
||||
stages_prompts.PromptListView.as_view(),
|
||||
name="stage-prompts",
|
||||
),
|
||||
path(
|
||||
"stages/prompts/create/",
|
||||
"stages_prompts/create/",
|
||||
stages_prompts.PromptCreateView.as_view(),
|
||||
name="stage-prompt-create",
|
||||
),
|
||||
path(
|
||||
"stages/prompts/<uuid:pk>/update/",
|
||||
"stages_prompts/<uuid:pk>/update/",
|
||||
stages_prompts.PromptUpdateView.as_view(),
|
||||
name="stage-prompt-update",
|
||||
),
|
||||
path(
|
||||
"stages/prompts/<uuid:pk>/delete/",
|
||||
"stages_prompts/<uuid:pk>/delete/",
|
||||
stages_prompts.PromptDeleteView.as_view(),
|
||||
name="stage-prompt-delete",
|
||||
),
|
||||
|
@ -311,11 +311,6 @@ urlpatterns = [
|
|||
name="certificatekeypair-delete",
|
||||
),
|
||||
# Outposts
|
||||
path(
|
||||
"outposts/",
|
||||
outposts.OutpostListView.as_view(),
|
||||
name="outposts",
|
||||
),
|
||||
path(
|
||||
"outposts/create/",
|
||||
outposts.OutpostCreateView.as_view(),
|
||||
|
|
|
@ -9,36 +9,15 @@ from django.contrib.auth.mixins import (
|
|||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
from django.views.generic import UpdateView
|
||||
from guardian.mixins import PermissionRequiredMixin
|
||||
|
||||
from authentik.admin.views.utils import (
|
||||
BackSuccessUrlMixin,
|
||||
DeleteMessageView,
|
||||
SearchListMixin,
|
||||
UserPaginateListMixin,
|
||||
)
|
||||
from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView
|
||||
from authentik.lib.views import CreateAssignPermView
|
||||
from authentik.outposts.forms import OutpostForm
|
||||
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(
|
||||
SuccessMessageMixin,
|
||||
BackSuccessUrlMixin,
|
||||
|
@ -53,7 +32,7 @@ class OutpostCreateView(
|
|||
permission_required = "authentik_outposts.add_outpost"
|
||||
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("authentik_admin:outposts")
|
||||
success_url = reverse_lazy("authentik_core:shell")
|
||||
success_message = _("Successfully created Outpost")
|
||||
|
||||
def get_initial(self) -> Dict[str, Any]:
|
||||
|
@ -78,7 +57,7 @@ class OutpostUpdateView(
|
|||
permission_required = "authentik_outposts.change_outpost"
|
||||
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("authentik_admin:outposts")
|
||||
success_url = reverse_lazy("authentik_core:shell")
|
||||
success_message = _("Successfully updated Outpost")
|
||||
|
||||
|
||||
|
@ -89,5 +68,5 @@ class OutpostDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessa
|
|||
permission_required = "authentik_outposts.delete_outpost"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("authentik_admin:outposts")
|
||||
success_url = reverse_lazy("authentik_core:shell")
|
||||
success_message = _("Successfully deleted Outpost")
|
||||
|
|
|
@ -29,11 +29,12 @@ from authentik.flows.api import (
|
|||
FlowViewSet,
|
||||
StageViewSet,
|
||||
)
|
||||
from authentik.outposts.api import (
|
||||
from authentik.outposts.api.outpost_service_connections import (
|
||||
DockerServiceConnectionViewSet,
|
||||
KubernetesServiceConnectionViewSet,
|
||||
OutpostViewSet,
|
||||
ServiceConnectionViewSet,
|
||||
)
|
||||
from authentik.outposts.api.outposts import OutpostViewSet
|
||||
from authentik.policies.api import (
|
||||
PolicyBindingViewSet,
|
||||
PolicyCacheViewSet,
|
||||
|
@ -88,6 +89,7 @@ router.register("core/users", UserViewSet)
|
|||
router.register("core/tokens", TokenViewSet)
|
||||
|
||||
router.register("outposts/outposts", OutpostViewSet)
|
||||
router.register("outposts/service_connections/all", ServiceConnectionViewSet)
|
||||
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)
|
||||
router.register(
|
||||
"outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
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.models import Source
|
||||
|
||||
|
@ -10,22 +9,26 @@ from authentik.core.models import Source
|
|||
class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"""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"""
|
||||
return obj._meta.object_name.lower().replace("source", "")
|
||||
|
||||
def to_representation(self, instance: Source):
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
if instance.__class__ == Source:
|
||||
return super().to_representation(instance)
|
||||
return instance.serializer(instance=instance).data
|
||||
return obj._meta.object_name.lower().replace("provider", "")
|
||||
|
||||
class Meta:
|
||||
|
||||
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):
|
||||
|
|
|
@ -1,30 +1,28 @@
|
|||
"""Outpost API Views"""
|
||||
from rest_framework.serializers import JSONField, ModelSerializer
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.outposts.models import (
|
||||
DockerServiceConnection,
|
||||
KubernetesServiceConnection,
|
||||
Outpost,
|
||||
OutpostServiceConnection,
|
||||
)
|
||||
|
||||
|
||||
class OutpostSerializer(ModelSerializer):
|
||||
"""Outpost Serializer"""
|
||||
|
||||
_config = JSONField()
|
||||
class ServiceConnectionSerializer(ModelSerializer):
|
||||
"""ServiceConnection Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Outpost
|
||||
fields = ["pk", "name", "providers", "service_connection", "_config"]
|
||||
model = OutpostServiceConnection
|
||||
fields = ["pk", "name"]
|
||||
|
||||
|
||||
class OutpostViewSet(ModelViewSet):
|
||||
"""Outpost Viewset"""
|
||||
class ServiceConnectionViewSet(ModelViewSet):
|
||||
"""ServiceConnection Viewset"""
|
||||
|
||||
queryset = Outpost.objects.all()
|
||||
serializer_class = OutpostSerializer
|
||||
queryset = OutpostServiceConnection.objects.all()
|
||||
serializer_class = ServiceConnectionSerializer
|
||||
|
||||
|
||||
class DockerServiceConnectionSerializer(ModelSerializer):
|
79
authentik/outposts/api/outposts.py
Normal file
79
authentik/outposts/api/outposts.py
Normal 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)
|
|
@ -9,7 +9,6 @@ from django.core.cache import cache
|
|||
from django.db import models, transaction
|
||||
from django.db.models.base import Model
|
||||
from django.forms.models import ModelForm
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from docker.client import DockerClient
|
||||
from docker.errors import DockerException
|
||||
|
@ -33,7 +32,6 @@ from authentik.crypto.models import CertificateKeyPair
|
|||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.models import InheritanceForeignKey
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.template import render_to_string
|
||||
from authentik.outposts.docker_tls import DockerInlineTLS
|
||||
|
||||
OUR_VERSION = parse(__version__)
|
||||
|
@ -378,13 +376,6 @@ class Outpost(models.Model):
|
|||
objects.append(provider)
|
||||
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:
|
||||
return f"Outpost {self.name}"
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -59,11 +59,11 @@ class OAuth2ProviderViewSet(ModelViewSet):
|
|||
queryset = OAuth2Provider.objects.all()
|
||||
serializer_class = OAuth2ProviderSerializer
|
||||
|
||||
@action(methods=["GET"], detail=True)
|
||||
@swagger_auto_schema(responses={200: OAuth2ProviderSetupURLs(many=False)})
|
||||
@action(methods=["GET"], detail=True)
|
||||
# pylint: disable=invalid-name
|
||||
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)
|
||||
data = {
|
||||
"issuer": provider.get_issuer(request),
|
||||
|
|
|
@ -23,6 +23,8 @@ return {
|
|||
"family_name": "",
|
||||
"preferred_username": 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()],
|
||||
}
|
||||
"""
|
||||
|
||||
|
|
|
@ -253,6 +253,7 @@ class OAuthFulfillmentStage(StageView):
|
|||
EventAction.AUTHORIZE_APPLICATION,
|
||||
authorized_application=application,
|
||||
flow=self.executor.plan.flow_pk,
|
||||
scopes=", ".join(self.params.scope),
|
||||
).from_http(self.request)
|
||||
return redirect(self.create_response_uri())
|
||||
except (ClientIdError, RedirectUriError) as error:
|
||||
|
|
|
@ -54,10 +54,10 @@ class SAMLProviderViewSet(ModelViewSet):
|
|||
queryset = SAMLProvider.objects.all()
|
||||
serializer_class = SAMLProviderSerializer
|
||||
|
||||
@action(methods=["GET"], detail=True)
|
||||
@swagger_auto_schema(responses={200: SAMLMetadataSerializer(many=False)})
|
||||
@action(methods=["GET"], detail=True)
|
||||
# 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"""
|
||||
provider = get_object_or_404(SAMLProvider, pk=pk)
|
||||
metadata = DescriptorDownloadView.get_metadata(request, provider)
|
||||
|
|
|
@ -2,17 +2,17 @@
|
|||
from rest_framework.serializers import ModelSerializer
|
||||
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.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
|
||||
|
||||
class LDAPSourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||
class LDAPSourceSerializer(SourceSerializer):
|
||||
"""LDAP Source Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = LDAPSource
|
||||
fields = SOURCE_SERIALIZER_FIELDS + [
|
||||
fields = SourceSerializer.Meta.fields + [
|
||||
"server_uri",
|
||||
"bind_cn",
|
||||
"bind_password",
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
"""OAuth Source Serializer"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.admin.forms.source import SOURCE_SERIALIZER_FIELDS
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
|
||||
|
||||
class OAuthSourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||
class OAuthSourceSerializer(SourceSerializer):
|
||||
"""OAuth Source Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = OAuthSource
|
||||
fields = SOURCE_SERIALIZER_FIELDS + [
|
||||
fields = SourceSerializer.Meta.fields + [
|
||||
"provider_type",
|
||||
"request_token_url",
|
||||
"authorization_url",
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
from django import forms
|
||||
|
||||
from authentik.admin.forms.source import SOURCE_FORM_FIELDS
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.sources.oauth.types.manager import MANAGER
|
||||
|
@ -27,7 +26,12 @@ class OAuthSourceForm(forms.ModelForm):
|
|||
class Meta:
|
||||
|
||||
model = OAuthSource
|
||||
fields = SOURCE_FORM_FIELDS + [
|
||||
fields = [
|
||||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"authentication_flow",
|
||||
"enrollment_flow",
|
||||
"provider_type",
|
||||
"request_token_url",
|
||||
"authorization_url",
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
"""SAMLSource API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.admin.forms.source import SOURCE_FORM_FIELDS
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
|
||||
|
||||
class SAMLSourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||
class SAMLSourceSerializer(SourceSerializer):
|
||||
"""SAMLSource Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = SAMLSource
|
||||
fields = SOURCE_FORM_FIELDS + [
|
||||
fields = SourceSerializer.Meta.fields + [
|
||||
"issuer",
|
||||
"sso_url",
|
||||
"slo_url",
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
from django import forms
|
||||
|
||||
from authentik.admin.forms.source import SOURCE_FORM_FIELDS
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
|
@ -28,7 +27,12 @@ class SAMLSourceForm(forms.ModelForm):
|
|||
class Meta:
|
||||
|
||||
model = SAMLSource
|
||||
fields = SOURCE_FORM_FIELDS + [
|
||||
fields = [
|
||||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"authentication_flow",
|
||||
"enrollment_flow",
|
||||
"issuer",
|
||||
"sso_url",
|
||||
"slo_url",
|
||||
|
|
|
@ -24,7 +24,7 @@ require (
|
|||
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
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/spf13/afero v1.5.1 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
|
|
|
@ -69,23 +69,10 @@ func (ac *APIController) Shutdown() {
|
|||
}
|
||||
|
||||
func (ac *APIController) startWSHandler() {
|
||||
notConnectedBackoff := 1
|
||||
logger := ac.logger.WithField("loop", "ws-handler")
|
||||
for {
|
||||
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
|
||||
} else {
|
||||
// When we're connected, reset backoff to 1
|
||||
notConnectedBackoff = 1
|
||||
}
|
||||
var wsMsg websocketMessage
|
||||
err := ac.wsConn.ReadJSON(&wsMsg)
|
||||
|
|
237
swagger.yaml
237
swagger.yaml
|
@ -1789,6 +1789,11 @@ paths:
|
|||
operationId: outposts_outposts_list
|
||||
description: Outpost Viewset
|
||||
parameters:
|
||||
- name: providers__isnull
|
||||
in: query
|
||||
description: ''
|
||||
required: false
|
||||
type: string
|
||||
- name: ordering
|
||||
in: query
|
||||
description: Which field to use when ordering the results.
|
||||
|
@ -1911,6 +1916,28 @@ paths:
|
|||
required: true
|
||||
type: string
|
||||
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/:
|
||||
get:
|
||||
operationId: outposts_proxy_list
|
||||
|
@ -2037,6 +2064,133 @@ paths:
|
|||
description: A unique integer value identifying this Proxy Provider.
|
||||
required: true
|
||||
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/:
|
||||
get:
|
||||
operationId: outposts_service_connections_docker_list
|
||||
|
@ -4308,7 +4462,7 @@ paths:
|
|||
/providers/oauth2/{id}/setup_urls/:
|
||||
get:
|
||||
operationId: providers_oauth2_setup_urls
|
||||
description: Return metadata as XML string
|
||||
description: Get Providers setup URLs
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
|
@ -8023,6 +8177,12 @@ definitions:
|
|||
items:
|
||||
type: integer
|
||||
uniqueItems: true
|
||||
providers_obj:
|
||||
description: ''
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Provider'
|
||||
readOnly: true
|
||||
service_connection:
|
||||
title: Service connection
|
||||
description: Select Service-Connection authentik should use to manage this
|
||||
|
@ -8030,9 +8190,36 @@ definitions:
|
|||
type: string
|
||||
format: uuid
|
||||
x-nullable: true
|
||||
token_identifier:
|
||||
title: Token identifier
|
||||
type: string
|
||||
readOnly: true
|
||||
_config:
|
||||
title: config
|
||||
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:
|
||||
title: 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
|
||||
Header. If not set, the user's Email address is used.
|
||||
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:
|
||||
description: DockerServiceConnection Serializer
|
||||
required:
|
||||
|
@ -9157,6 +9359,10 @@ definitions:
|
|||
type: string
|
||||
format: uuid
|
||||
x-nullable: true
|
||||
object_type:
|
||||
title: Object type
|
||||
type: string
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
title: Verbose name
|
||||
type: string
|
||||
|
@ -9165,10 +9371,6 @@ definitions:
|
|||
title: Verbose name plural
|
||||
type: string
|
||||
readOnly: true
|
||||
__type__:
|
||||
title: 'type '
|
||||
type: string
|
||||
readOnly: true
|
||||
LDAPSource:
|
||||
description: LDAP Source Serializer
|
||||
required:
|
||||
|
@ -9213,6 +9415,10 @@ definitions:
|
|||
type: string
|
||||
format: uuid
|
||||
x-nullable: true
|
||||
object_type:
|
||||
title: Object type
|
||||
type: string
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
title: Verbose name
|
||||
type: string
|
||||
|
@ -9344,6 +9550,10 @@ definitions:
|
|||
type: string
|
||||
format: uuid
|
||||
x-nullable: true
|
||||
object_type:
|
||||
title: Object type
|
||||
type: string
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
title: Verbose name
|
||||
type: string
|
||||
|
@ -9397,6 +9607,11 @@ definitions:
|
|||
- sso_url
|
||||
type: object
|
||||
properties:
|
||||
pk:
|
||||
title: Pbm uuid
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
name:
|
||||
title: Name
|
||||
description: Source's display Name.
|
||||
|
@ -9425,6 +9640,18 @@ definitions:
|
|||
type: string
|
||||
format: uuid
|
||||
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:
|
||||
title: Issuer
|
||||
description: Also known as Entity ID. Defaults the Metadata URL.
|
||||
|
|
6
web/package-lock.json
generated
6
web/package-lock.json
generated
|
@ -899,9 +899,9 @@
|
|||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"construct-style-sheets-polyfill": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/construct-style-sheets-polyfill/-/construct-style-sheets-polyfill-2.4.6.tgz",
|
||||
"integrity": "sha512-lU0to7dFDjKslMF+M5NUa4s0RQMBRVyZMXvD/vp7vmjdEPgziTkHSfZHQxfoIvVWajWRJUVJMLfrMwcx8fTh4A=="
|
||||
"version": "2.4.9",
|
||||
"resolved": "https://registry.npmjs.org/construct-style-sheets-polyfill/-/construct-style-sheets-polyfill-2.4.9.tgz",
|
||||
"integrity": "sha512-kPXZXxsp7CTr/Vs29+omUA29wTrFplkdY6jqxyv0DDWC5Ro79WmwpboH2M9KiOclbtn8r81GCFtc7+t7OjRnCw=="
|
||||
},
|
||||
"copy-descriptor": {
|
||||
"version": "0.1.1",
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
"@types/codemirror": "0.0.108",
|
||||
"chart.js": "^2.9.4",
|
||||
"codemirror": "^5.59.2",
|
||||
"construct-style-sheets-polyfill": "^2.4.6",
|
||||
"construct-style-sheets-polyfill": "^2.4.9",
|
||||
"flowchart.js": "^1.15.0",
|
||||
"lit-element": "^2.4.0",
|
||||
"lit-html": "^1.3.0",
|
||||
|
|
|
@ -7,6 +7,13 @@ export interface QueryArguments {
|
|||
[key: string]: number | string | boolean | null;
|
||||
}
|
||||
|
||||
export interface BaseInheritanceModel {
|
||||
|
||||
verbose_name: string;
|
||||
verbose_name_plural: string;
|
||||
|
||||
}
|
||||
|
||||
export class Client {
|
||||
makeUrl(url: string[], query?: QueryArguments): string {
|
||||
let builtUrl = `/api/${VERSION}/${url.join("/")}/`;
|
||||
|
|
40
web/src/api/Outposts.ts
Normal file
40
web/src/api/Outposts.ts
Normal 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}`;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
name: string;
|
||||
|
||||
constructor() {
|
||||
throw Error();
|
||||
}
|
||||
verbose_name: string;
|
||||
verbose_name_plural: string;
|
||||
|
||||
static get(pk: string): Promise<Policy> {
|
||||
return DefaultClient.fetch<Policy>(["policies", "all", pk]);
|
||||
|
|
|
@ -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;
|
||||
name: string;
|
||||
authorization_flow: string;
|
||||
|
|
|
@ -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;
|
||||
name: string;
|
||||
slug: string;
|
||||
|
@ -11,6 +11,8 @@ export class Source {
|
|||
constructor() {
|
||||
throw Error();
|
||||
}
|
||||
verbose_name: string;
|
||||
verbose_name_plural: string;
|
||||
|
||||
static get(slug: string): Promise<Source> {
|
||||
return DefaultClient.fetch<Source>(["sources", "all", slug]);
|
||||
|
@ -19,4 +21,8 @@ export class Source {
|
|||
static list(filter?: QueryArguments): Promise<PBResponse<Source>> {
|
||||
return DefaultClient.fetch<PBResponse<Source>>(["sources", "all"], filter);
|
||||
}
|
||||
|
||||
static adminUrl(rest: string): string {
|
||||
return `/administration/sources/${rest}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
|
|||
`^/sources/(?<slug>${SLUG_REGEX})$`,
|
||||
),
|
||||
new SidebarItem("Providers", "/providers"),
|
||||
new SidebarItem("Outposts", "/administration/outposts/"),
|
||||
new SidebarItem("Outposts", "/outposts"),
|
||||
new SidebarItem("Outpost Service Connections", "/administration/outpost_service_connections/"),
|
||||
).when((): Promise<boolean> => {
|
||||
return User.me().then(u => u.is_superuser);
|
||||
|
@ -41,7 +41,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
|
|||
new SidebarItem("Flows").children(
|
||||
new SidebarItem("Flows", "/administration/flows/").activeWhen(`^/flows/(?<slug>${SLUG_REGEX})$`),
|
||||
new SidebarItem("Stages", "/administration/stages/"),
|
||||
new SidebarItem("Prompts", "/administration/stages/prompts/"),
|
||||
new SidebarItem("Prompts", "/administration/stages_prompts/"),
|
||||
new SidebarItem("Invitations", "/administration/stages/invitations/"),
|
||||
).when((): Promise<boolean> => {
|
||||
return User.me().then(u => u.is_superuser);
|
||||
|
|
|
@ -65,13 +65,13 @@ export class EventInfo extends LitElement {
|
|||
case "model_updated":
|
||||
case "model_deleted":
|
||||
return html`
|
||||
<h3>${gettext("Affected model:")}</h3><hr>
|
||||
<h3>${gettext("Affected model:")}</h3>
|
||||
${this.getModelInfo(this.event.context.model as EventContext)}
|
||||
`;
|
||||
case "authorize_application":
|
||||
return html`<div class="pf-l-flex">
|
||||
<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)}
|
||||
</div>
|
||||
<div class="pf-l-flex__item">
|
||||
|
@ -83,14 +83,15 @@ export class EventInfo extends LitElement {
|
|||
}), html`<ak-spinner size=${SpinnerSize.Medium}></ak-spinner>`)}
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
</div>
|
||||
<ak-expand>${this.defaultResponse()}</ak-expand>`;
|
||||
case "login_failed":
|
||||
return html`
|
||||
<h3>${gettext(`Attempted to log in as ${this.event.context.username}`)}</h3>
|
||||
<ak-expand>${this.defaultResponse()}</ak-expand>`;
|
||||
case "token_view":
|
||||
return html`
|
||||
<h3>${gettext("Token:")}</h3><hr>
|
||||
<h3>${gettext("Token:")}</h3>
|
||||
${this.getModelInfo(this.event.context.token as EventContext)}`;
|
||||
case "property_mapping_exception":
|
||||
return html`<div class="pf-l-flex">
|
||||
|
|
49
web/src/pages/outposts/OutpostHealth.ts
Normal file
49
web/src/pages/outposts/OutpostHealth.ts
Normal 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> ${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> ${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>
|
||||
${gettext(`${h.version}, should be ${h.version_should}`)}` :
|
||||
html`<i class="fas fa-check pf-m-success"></i> ${gettext(`Version: ${h.version}`)}`}
|
||||
</li>
|
||||
</ul>
|
||||
</li>`;
|
||||
});
|
||||
}), html`<ak-spinner></ak-spinner>`)}</ul>`;
|
||||
}
|
||||
|
||||
}
|
121
web/src/pages/outposts/OutpostListPage.ts
Normal file
121
web/src/pages/outposts/OutpostListPage.ts
Normal 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()}
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ import "./pages/events/RuleListPage";
|
|||
import "./pages/providers/ProviderListPage";
|
||||
import "./pages/providers/ProviderViewPage";
|
||||
import "./pages/property-mappings/PropertyMappingListPage";
|
||||
import "./pages/outposts/OutpostListPage";
|
||||
|
||||
export const ROUTES: Route[] = [
|
||||
// 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/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("^/outposts$"), html`<ak-outpost-list></ak-outpost-list>`),
|
||||
];
|
||||
|
|
Reference in a new issue