Compare commits

...

57 Commits

Author SHA1 Message Date
897e0f90fe core: fix token update/delete not working 2020-12-26 01:28:45 +01:00
ecbcd86f05 core: fix User's token creation not working 2020-12-26 01:28:45 +01:00
65d9f690cd release: 0.13.5-stable 2020-12-26 00:52:26 +01:00
f96c2db5df core: show multi-select notice for SelectMultiple Widgets 2020-12-26 00:43:29 +01:00
5647f53140 core: fix anonymous user being included in User API
# Conflicts:
#	authentik/admin/views/applications.py
2020-12-26 00:26:24 +01:00
4e20cd0fee core: fix error during migrations 2020-12-25 23:51:51 +01:00
49636f8fa0 stages/invitation: fix optional field being required 2020-12-25 23:42:21 +01:00
cd8157ea08 stages/password: fix PasswordStageForm not showing backends 2020-12-25 23:42:21 +01:00
2a94ad7782 release: 0.13.4-stable 2020-12-24 15:36:39 +01:00
07eb5ffb4b core: fix missing import for application api 2020-12-24 15:35:57 +01:00
8cc68928b8 outposts: validate kubeconfig before saving 2020-12-24 13:25:08 +01:00
221db12f85 outposts: allow blank kubeconfig 2020-12-24 13:25:04 +01:00
34166d3c20 core: make application's provider not required
# Conflicts:
#	authentik/core/api/applications.py
2020-12-24 13:24:52 +01:00
94972d64e6 web: fix sidebar being overlayed over modal backdrop 2020-12-22 20:38:30 +01:00
253eaa382c admin: fix policy test button in dark theme 2020-12-20 22:32:53 +01:00
fc4f9733d1 policies/expression: fix missing ak_logger 2020-12-20 22:32:45 +01:00
8d784afcd1 web: expand sidebar by default on desktop, auto collapse 2020-12-20 22:32:37 +01:00
e23afd18e4 release: 0.13.3-stable 2020-12-19 16:55:07 +01:00
c2a30b760a web: allow Sidebar to be opened on mobile (#417)
* web: initial sidebar trigger on mobile

* web: render hamburger button as overlay top right
2020-12-19 16:54:25 +01:00
6e24856d45 flows: fix redirect when un-authenticated user uses external authentication (#416)
* flows: add PLAN_CONTEXT_REDIRECT so final redirect can be set from within flow

* sources/*: use PLAN_CONTEXT_REDIRECT

* flows: fallback when flow plan is empty
2020-12-19 16:42:39 +01:00
98a58b74e3 core: ensure generic error template fills screen 2020-12-19 14:28:20 +01:00
5f3ab22bea providers/oauth2: fix incorrect background set on end session screen 2020-12-19 14:24:28 +01:00
1ed5d5da35 build(deps): bump @sentry/browser from 5.29.1 to 5.29.2 in /web (#413)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 5.29.1 to 5.29.2.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/5.29.1...5.29.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-18 09:58:34 +01:00
76193e0031 build(deps): bump boto3 from 1.16.38 to 1.16.39 (#412)
Bumps [boto3](https://github.com/boto/boto3) from 1.16.38 to 1.16.39.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.16.38...1.16.39)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-18 09:44:54 +01:00
50109ca7ad build(deps): bump @sentry/tracing from 5.29.1 to 5.29.2 in /web (#414)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 5.29.1 to 5.29.2.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/5.29.1...5.29.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-18 09:44:33 +01:00
e4b66d991c release: 0.13.2-stable 2020-12-17 20:20:47 +01:00
68adc2d5a5 admin: fix warning during swagger generation 2020-12-17 19:49:35 +01:00
349a3a67d5 flows: use to_stage_response in _flow_done() 2020-12-17 19:34:15 +01:00
e1394207e7 flows: fix inconsistent behaviour when flow is empty 2020-12-17 19:22:24 +01:00
f265c1f10b admin: fix cache clean views erroring 2020-12-17 19:03:32 +01:00
1aecdc7f8f web: fix css for policy tertiary buttons and text on flow card 2020-12-17 14:31:45 +01:00
a18edaf62b build(deps): bump @sentry/tracing from 5.29.0 to 5.29.1 in /web (#411)
Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 5.29.0 to 5.29.1.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/5.29.0...5.29.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-17 11:41:19 +01:00
c91abe448c build(deps): bump celery from 5.0.4 to 5.0.5 (#407)
Bumps [celery](https://github.com/celery/celery) from 5.0.4 to 5.0.5.
- [Release notes](https://github.com/celery/celery/releases)
- [Changelog](https://github.com/celery/celery/blob/master/Changelog.rst)
- [Commits](https://github.com/celery/celery/compare/v5.0.4...v5.0.5)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-17 09:46:11 +01:00
e531e52403 build(deps): bump django-storages from 1.10.1 to 1.11 (#408)
Bumps [django-storages](https://github.com/jschneier/django-storages) from 1.10.1 to 1.11.
- [Release notes](https://github.com/jschneier/django-storages/releases)
- [Changelog](https://github.com/jschneier/django-storages/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jschneier/django-storages/compare/1.10.1...1.11)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-17 09:45:58 +01:00
cae536fa65 build(deps): bump boto3 from 1.16.37 to 1.16.38 (#409)
Bumps [boto3](https://github.com/boto/boto3) from 1.16.37 to 1.16.38.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.16.37...1.16.38)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-17 09:45:45 +01:00
316b15b8a9 build(deps): bump @sentry/browser from 5.29.0 to 5.29.1 in /web (#410)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 5.29.0 to 5.29.1.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/5.29.0...5.29.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-17 09:45:29 +01:00
e6ccd4fa76 web: fix file name casing 2020-12-17 00:18:24 +01:00
86aabba3ed web: fix file name casing 2020-12-17 00:18:03 +01:00
0b36aad5c8 admin: ensure clean_expired_models is called during tests 2020-12-17 00:17:20 +01:00
64d2a216f0 web: fix linting 2020-12-16 23:50:23 +01:00
a5e5e140d6 admin: add full api tests 2020-12-16 23:42:44 +01:00
29f98abd00 root: update swagger 2020-12-16 23:32:14 +01:00
7b5ce4e98a web: use colours for icons, move users to separate card 2020-12-16 23:28:04 +01:00
d7fa52ebf3 admin: remove old admin overview 2020-12-16 23:21:38 +01:00
2ffaa94825 web: fix typo 2020-12-16 23:08:40 +01:00
b80b2626a6 web: fix rendering of version 2020-12-16 23:08:35 +01:00
3b7bba5a62 web: make sure naming matches backend 2020-12-16 23:03:06 +01:00
2d9efe035e web: migrate admin overview cards to separate files 2020-12-16 23:00:32 +01:00
48438e28fd admin: separate overview API into WorkerAPI and VersionAPI 2020-12-16 22:53:53 +01:00
885a2f0a58 web: add flow and policy cache card 2020-12-16 22:30:37 +01:00
cf46ee06b7 api: create dedicated api for cached flows and policies 2020-12-16 22:18:36 +01:00
9e33b49d29 web: rewrite aggregate cards to separate components 2020-12-16 22:00:40 +01:00
1179ba4ef2 api: remove counters from overview api and allow filtering on object apis 2020-12-16 22:00:29 +01:00
3c12c8b3ff core: make Provider SerializerModel 2020-12-16 21:38:40 +01:00
4d22659b6e web: re-organise sidebar 2020-12-16 16:04:11 +01:00
2c0709eeee web: render SidebarItem from the item 2020-12-16 16:04:02 +01:00
c24d1b6b84 outposts: fix incorrect timeout for state cache 2020-12-16 12:14:34 +01:00
98 changed files with 1293 additions and 786 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.13.1-stable
current_version = 0.13.5-stable
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)

View File

@ -18,11 +18,11 @@ jobs:
- name: Building Docker Image
run: docker build
--no-cache
-t beryju/authentik:0.13.1-stable
-t beryju/authentik:0.13.5-stable
-t beryju/authentik:latest
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/authentik:0.13.1-stable
run: docker push beryju/authentik:0.13.5-stable
- name: Push Docker Container to Registry (latest)
run: docker push beryju/authentik:latest
build-proxy:
@ -48,11 +48,11 @@ jobs:
cd proxy/
docker build \
--no-cache \
-t beryju/authentik-proxy:0.13.1-stable \
-t beryju/authentik-proxy:0.13.5-stable \
-t beryju/authentik-proxy:latest \
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/authentik-proxy:0.13.1-stable
run: docker push beryju/authentik-proxy:0.13.5-stable
- name: Push Docker Container to Registry (latest)
run: docker push beryju/authentik-proxy:latest
build-static:
@ -69,11 +69,11 @@ jobs:
cd web/
docker build \
--no-cache \
-t beryju/authentik-static:0.13.1-stable \
-t beryju/authentik-static:0.13.5-stable \
-t beryju/authentik-static:latest \
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/authentik-static:0.13.1-stable
run: docker push beryju/authentik-static:0.13.5-stable
- name: Push Docker Container to Registry (latest)
run: docker push beryju/authentik-static:latest
test-release:
@ -107,5 +107,5 @@ jobs:
SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org
with:
tagName: 0.13.1-stable
tagName: 0.13.5-stable
environment: beryjuorg-prod

36
Pipfile.lock generated
View File

@ -74,18 +74,18 @@
},
"boto3": {
"hashes": [
"sha256:ad0e8dbd934d97b5228252785c0236c3ee4d464c14138f568e371bf43c6ea584",
"sha256:ee86c26b3d457aa4d0256d0535d13107c32aa33bb5eb2a0b2dac9d81c3aca405"
"sha256:a05614300fd404c7952a55ae92e106b9400ae65886425aaab3104527be833848",
"sha256:c7556b0861d982b71043fbc0df024644320c817ad796391c442d0c2f15a77223"
],
"index": "pypi",
"version": "==1.16.37"
"version": "==1.16.39"
},
"botocore": {
"hashes": [
"sha256:5605c250f6f7c72ca50e45eab6186dfda03cb84296ca5b05f7416defcd3fcbc5",
"sha256:67bf1285455d79336ce7061da1768206b78f7a0efc13c8b4033fd348a74e7491"
"sha256:449e4196160ff58ee27d2a626a7ce4cfff2640fe1806d7a279e73a30ad286347",
"sha256:e0d0386098a072abd7b6c087e6149d997377c969a823ebe01b3f5bfabe9bfac0"
],
"version": "==1.19.37"
"version": "==1.19.39"
},
"cachetools": {
"hashes": [
@ -96,11 +96,11 @@
},
"celery": {
"hashes": [
"sha256:45bb7909061862305cefec94289fabc1b89ac004680f4dc7d9dea642a2507e53",
"sha256:533f3635065b7ed362ffc04228635b4c82d53a9ab812118ccdedb5eae281fb97"
"sha256:5e8d364e058554e83bbb116e8377d90c79be254785f357cb2cec026e79febe13",
"sha256:f4efebe6f8629b0da2b8e529424de376494f5b7a743c321c8a2ddc2b1414921c"
],
"index": "pypi",
"version": "==5.0.4"
"version": "==5.0.5"
},
"certifi": {
"hashes": [
@ -168,10 +168,10 @@
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
],
"version": "==3.0.4"
"version": "==4.0.0"
},
"click": {
"hashes": [
@ -343,11 +343,11 @@
},
"django-storages": {
"hashes": [
"sha256:12de8fb2605b9b57bfaf54b075280d7cbb3b3ee1ca4bc9b9add147af87fe3a2c",
"sha256:652275ab7844538c462b62810276c0244866f345878256a9e0e86f5b1283ae18"
"sha256:056ec3e9e2b0c6f363913976072ffba2923e79e4859578047da139ba1637497e",
"sha256:7af56611c62a1c174aab4e862efb7fdd98296dccf76f42135f5b6851fc313c97"
],
"index": "pypi",
"version": "==1.10.1"
"version": "==1.11"
},
"djangorestframework": {
"hashes": [
@ -950,10 +950,10 @@
},
"requests": {
"hashes": [
"sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
"sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
],
"version": "==2.25.0"
"version": "==2.25.1"
},
"requests-oauthlib": {
"hashes": [

View File

@ -1,2 +1,2 @@
"""authentik"""
__version__ = "0.13.1-stable"
__version__ = "0.13.5-stable"

View File

@ -1,4 +1,4 @@
"""authentik administration overview"""
"""authentik administration metrics"""
import time
from collections import Counter
from datetime import timedelta
@ -47,7 +47,7 @@ def get_events_per_1h(**filter_kwargs) -> List[Dict[str, int]]:
class AdministrationMetricsSerializer(Serializer):
"""Overview View"""
"""Login Metrics per 1h"""
logins_per_1h = SerializerMethodField()
logins_failed_per_1h = SerializerMethodField()
@ -68,12 +68,12 @@ class AdministrationMetricsSerializer(Serializer):
class AdministrationMetricsViewSet(ViewSet):
"""Return single instance of AdministrationMetricsSerializer"""
"""Login Metrics per 1h"""
permission_classes = [IsAdminUser]
@swagger_auto_schema(responses={200: AdministrationMetricsSerializer(many=True)})
def list(self, request: Request) -> Response:
"""Return single instance of AdministrationMetricsSerializer"""
"""Login Metrics per 1h"""
serializer = AdministrationMetricsSerializer(True)
return Response(serializer.data)

View File

@ -1,79 +0,0 @@
"""authentik administration overview"""
from django.core.cache import cache
from drf_yasg2.utils import swagger_auto_schema
from rest_framework.fields import SerializerMethodField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.viewsets import ViewSet
from authentik import __version__
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
from authentik.core.models import Provider
from authentik.policies.models import Policy
from authentik.root.celery import CELERY_APP
class AdministrationOverviewSerializer(Serializer):
"""Overview View"""
version = SerializerMethodField()
version_latest = SerializerMethodField()
worker_count = SerializerMethodField()
providers_without_application = SerializerMethodField()
policies_without_binding = SerializerMethodField()
cached_policies = SerializerMethodField()
cached_flows = SerializerMethodField()
def get_version(self, _) -> str:
"""Get current version"""
return __version__
def get_version_latest(self, _) -> str:
"""Get latest version from cache"""
version_in_cache = cache.get(VERSION_CACHE_KEY)
if not version_in_cache:
update_latest_version.delay()
return __version__
return version_in_cache
def get_worker_count(self, _) -> int:
"""Ping workers"""
return len(CELERY_APP.control.ping(timeout=0.5))
def get_providers_without_application(self, _) -> int:
"""Count of providers without application"""
return len(Provider.objects.filter(application=None))
def get_policies_without_binding(self, _) -> int:
"""Count of policies not bound or use in prompt stages"""
return len(
Policy.objects.filter(bindings__isnull=True, promptstage__isnull=True)
)
def get_cached_policies(self, _) -> int:
"""Get cached policy count"""
return len(cache.keys("policy_*"))
def get_cached_flows(self, _) -> int:
"""Get cached flow count"""
return len(cache.keys("flow_*"))
def create(self, request: Request) -> Response:
raise NotImplementedError
def update(self, request: Request) -> Response:
raise NotImplementedError
class AdministrationOverviewViewSet(ViewSet):
"""Return single instance of AdministrationOverviewSerializer"""
permission_classes = [IsAdminUser]
@swagger_auto_schema(responses={200: AdministrationOverviewSerializer(many=True)})
def list(self, request: Request) -> Response:
"""Return single instance of AdministrationOverviewSerializer"""
serializer = AdministrationOverviewSerializer(True)
return Response(serializer.data)

View File

@ -66,7 +66,7 @@ class TaskViewSet(ViewSet):
"successful": True,
}
)
except ImportError:
except ImportError: # pragma: no cover
# if we get an import error, the module path has probably changed
task.delete()
return Response({"successful": False})

View File

@ -0,0 +1,60 @@
"""authentik administration overview"""
from django.core.cache import cache
from drf_yasg2.utils import swagger_auto_schema
from packaging.version import parse
from rest_framework.fields import SerializerMethodField
from rest_framework.mixins import ListModelMixin
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.viewsets import GenericViewSet
from authentik import __version__
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
class VersionSerializer(Serializer):
"""Get running and latest version."""
version_current = SerializerMethodField()
version_latest = SerializerMethodField()
outdated = SerializerMethodField()
def get_version_current(self, _) -> str:
"""Get current version"""
return __version__
def get_version_latest(self, _) -> str:
"""Get latest version from cache"""
version_in_cache = cache.get(VERSION_CACHE_KEY)
if not version_in_cache: # pragma: no cover
update_latest_version.delay()
return __version__
return version_in_cache
def get_outdated(self, instance) -> bool:
"""Check if we're running the latest version"""
return parse(self.get_version_current(instance)) < parse(
self.get_version_latest(instance)
)
def create(self, request: Request) -> Response:
raise NotImplementedError
def update(self, request: Request) -> Response:
raise NotImplementedError
class VersionViewSet(ListModelMixin, GenericViewSet):
"""Get running and latest version."""
permission_classes = [IsAdminUser]
def get_queryset(self):
return None
@swagger_auto_schema(responses={200: VersionSerializer(many=True)})
def list(self, request: Request) -> Response:
"""Get running and latest version."""
return Response(VersionSerializer(True).data)

View File

@ -0,0 +1,25 @@
"""authentik administration overview"""
from rest_framework.mixins import ListModelMixin
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.viewsets import GenericViewSet
from authentik.root.celery import CELERY_APP
class WorkerViewSet(ListModelMixin, GenericViewSet):
"""Get currently connected worker count."""
serializer_class = Serializer
permission_classes = [IsAdminUser]
def get_queryset(self):
return None
def list(self, request: Request) -> Response:
"""Get currently connected worker count."""
return Response(
{"pagination": {"count": len(CELERY_APP.control.ping(timeout=0.5))}}
)

View File

@ -1,230 +0,0 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load static %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>{% trans 'System Overview' %}</h1>
</div>
</section>
<section class="pf-c-page__main-section">
<div class="pf-l-gallery pf-m-gutter">
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 3;grid-row-end: span 2;">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Logins over the last 24 hours' %}
</div>
</div>
<div class="pf-c-card__body">
<ak-admin-logins-chart url="{% url 'authentik_api:admin_metrics-list' %}"></ak-admin-logins-chart>
</div>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 2;grid-row-end: span 3;">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Apps with most usage' %}
</div>
</div>
<div class="pf-c-card__body">
<table class="pf-c-table pf-m-compact" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Application' %}</th>
<th role="columnheader" scope="col">{% trans 'Logins' %}</th>
<th role="columnheader" scope="col"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for app in most_used_applications %}
<tr role="row">
<td role="cell">
{{ app.application.name }}
</td>
<td role="cell">
{{ app.total_logins }}
</td>
<td role="cell">
<progress value="{{ app.total_logins }}" max="{{ most_used_applications.0.total_logins }}"></progress>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %}
</div>
<a href="{% url 'authentik_admin:providers' %}">
<i class="fa fa-external-link-alt"> </i>
</a>
</div>
<div class="pf-c-card__body">
{% if providers_without_application.exists %}
<p class="ak-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> {{ provider_count }}
</p>
<p>{% trans 'Warning: At least one Provider has no application assigned.' %}</p>
{% else %}
<p class="ak-aggregate-card">
<i class="fa fa-check-circle"></i> {{ provider_count }}
</p>
{% endif %}
</div>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %}
</div>
<a href="{% url 'authentik_admin:policies' %}">
<i class="fa fa-external-link-alt"> </i>
</a>
</div>
<div class="pf-c-card__body">
{% if policies_without_binding %}
<p class="ak-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> {{ policy_count }}
</p>
<p>{% trans 'Policies without binding exist.' %}</p>
{% else %}
<p class="ak-aggregate-card">
<i class="fa fa-check-circle"></i> {{ policy_count }}
</p>
{% endif %}
</div>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-user"></i> {% trans 'Users' %}
</div>
<a href="{% url 'authentik_admin:users' %}">
<i class="fa fa-external-link-alt"> </i>
</a>
</div>
<div class="pf-c-card__body">
<p class="ak-aggregate-card">
<i class="fa fa-check-circle"></i> {{ user_count }}
</p>
</div>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %}
</div>
<a href="https://github.com/BeryJu/authentik/releases" target="_blank">
<i class="fa fa-external-link-alt"> </i>
</a>
</div>
<div class="pf-c-card__body">
<p class="ak-aggregate-card">
{% if version >= version_latest %}
<i class="fa fa-check-circle"></i> {{ version }}
{% else %}
<i class="fa fa-exclamation-triangle"></i> {{ version }}
{% endif %}
</p>
{% if version >= version_latest %}
{% blocktrans %}
Up-to-date!
{% endblocktrans %}
{% else %}
{% blocktrans with latest=version_latest %}
{{ latest }} is available!
{% endblocktrans %}
{% endif %}
</div>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %}
</div>
</div>
<fetch-fill-slot class="pf-c-card__body" url="{% url 'authentik_api:admin_overview-list' %}" key="worker_count">
<div slot="value < 1">
<p class="ak-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> <span data-value></span>
</p>
<p>{% trans 'No workers connected.' %}</p>
</div>
<div slot="value >= 1">
<p class="ak-aggregate-card">
<i class="fa fa-check-circle"></i> <span data-value></span>
</p>
</div>
<div>
<span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>
</fetch-fill-slot>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Policies' %}
</div>
<ak-modal-button href="{% url 'authentik_admin:overview-clear-policy-cache' %}">
<a slot="trigger">
<i class="fa fa-trash"> </i>
</a>
<div slot="modal"></div>
</ak-modal-button>
</div>
<div class="pf-c-card__body">
{% if cached_policies < 1 %}
<p class="ak-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> {{ cached_policies }}
</p>
<p>{% trans 'No policies cached. Users may experience slow response times.' %}</p>
{% else %}
<p class="ak-aggregate-card">
<i class="fa fa-check-circle"></i> {{ cached_policies }}
</p>
{% endif %}
</div>
</div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Flows' %}
</div>
<ak-modal-button href="{% url 'authentik_admin:overview-clear-flow-cache' %}">
<a slot="trigger">
<i class="fa fa-trash"> </i>
</a>
<div slot="modal"></div>
</ak-modal-button>
</div>
<div class="pf-c-card__body">
{% if cached_flows < 1 %}
<p class="ak-aggregate-card">
<span class="fa fa-exclamation-triangle"></span> {{ cached_flows }}
</p>
<p>{% trans 'No flows cached.' %}</p>
{% else %}
<p class="ak-aggregate-card">
<i class="fa fa-check-circle"></i> {{ cached_flows }}
</p>
{% endif %}
</div>
</div>
</div>
</section>
{% endblock %}

View File

@ -81,7 +81,7 @@
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="{% url 'authentik_admin:policy-test' pk=policy.pk %}">
<ak-spinner-button slot="trigger" class="pf-m-tertiary">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
{% trans 'Test' %}
</ak-spinner-button>
<div slot="modal"></div>

View File

@ -37,8 +37,9 @@
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'ID' %}</th>
<th role="columnheader" scope="col">{% trans 'Created by' %}</th>
<th role="columnheader" scope="col">{% trans 'Expiry' %}</th>
<th role="columnheader" scope="col">{% trans 'Link' %}</th>
<th role="cell"></th>
</tr>
</thead>
@ -47,12 +48,17 @@
<tr role="row">
<td role="cell">
<span>
{{ invitation.expiry }}
{{ invitation.invite_uuid }}
</span>
</td>
<td role="cell">
<span>
{{ invitation.Link }}
{{ invitation.created_by }}
</span>
</td>
<td role="cell">
<span>
{{ invitation.expiry|default:"-" }}
</span>
</td>
<td>

View File

@ -6,6 +6,7 @@ from django.test import TestCase
from authentik import __version__
from authentik.core.models import Group, User
from authentik.core.tasks import clean_expired_models
class TestAdminAPI(TestCase):
@ -19,19 +20,54 @@ class TestAdminAPI(TestCase):
self.group.save()
self.client.force_login(self.user)
def test_overview(self):
"""Test Overview API"""
response = self.client.get(reverse("authentik_api:admin_overview-list"))
def test_tasks(self):
"""Test Task API"""
clean_expired_models.delay()
response = self.client.get(reverse("authentik_api:admin_system_tasks-list"))
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(body["version"], __version__)
self.assertTrue(
any([task["task_name"] == "clean_expired_models" for task in body])
)
def test_tasks_retry(self):
"""Test Task API (retry)"""
clean_expired_models.delay()
response = self.client.post(
reverse(
"authentik_api:admin_system_tasks-retry",
kwargs={"pk": "clean_expired_models"},
)
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertTrue(body["successful"])
def test_tasks_retry_404(self):
"""Test Task API (retry, 404)"""
response = self.client.post(
reverse(
"authentik_api:admin_system_tasks-retry",
kwargs={"pk": "qwerqewrqrqewrqewr"},
)
)
self.assertEqual(response.status_code, 404)
def test_version(self):
"""Test Version API"""
response = self.client.get(reverse("authentik_api:admin_version-list"))
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(body["version_current"], __version__)
def test_workers(self):
"""Test Workers API"""
response = self.client.get(reverse("authentik_api:admin_workers-list"))
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(body["pagination"]["count"], 0)
def test_metrics(self):
"""Test metrics API"""
response = self.client.get(reverse("authentik_api:admin_metrics-list"))
self.assertEqual(response.status_code, 200)
def test_tasks(self):
"""Test tasks metrics API"""
response = self.client.get(reverse("authentik_api:admin_system_tasks-list"))
self.assertEqual(response.status_code, 200)

View File

@ -34,7 +34,6 @@ urlpatterns = [
overview.PolicyCacheClearView.as_view(),
name="overview-clear-policy-cache",
),
path("overview/", overview.AdministrationOverviewView.as_view(), name="overview"),
# Applications
path(
"applications/", applications.ApplicationListView.as_view(), name="applications"

View File

@ -1,65 +1,25 @@
"""authentik administration overview"""
from typing import Union
from django.conf import settings
from django.contrib.messages.views import SuccessMessageMixin
from django.core.cache import cache
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import FormView, TemplateView
from packaging.version import LegacyVersion, Version, parse
from django.views.generic import FormView
from structlog import get_logger
from authentik import __version__
from authentik.admin.forms.overview import FlowCacheClearForm, PolicyCacheClearForm
from authentik.admin.mixins import AdminRequiredMixin
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
from authentik.core.models import Provider, User
from authentik.policies.models import Policy
LOGGER = get_logger()
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
"""Overview View"""
template_name = "administration/overview.html"
def get_latest_version(self) -> Union[LegacyVersion, Version]:
"""Get latest version from cache"""
version_in_cache = cache.get(VERSION_CACHE_KEY)
if not version_in_cache:
if not settings.DEBUG:
update_latest_version.delay()
return parse(__version__)
return parse(version_in_cache)
def get_context_data(self, **kwargs):
kwargs["policy_count"] = len(Policy.objects.all())
kwargs["user_count"] = len(User.objects.all()) - 1 # Remove anonymous user
kwargs["provider_count"] = len(Provider.objects.all())
kwargs["version"] = parse(__version__)
kwargs["version_latest"] = self.get_latest_version()
kwargs["providers_without_application"] = Provider.objects.filter(
application=None
)
kwargs["policies_without_binding"] = len(
Policy.objects.filter(bindings__isnull=True, promptstage__isnull=True)
)
kwargs["cached_policies"] = len(cache.keys("policy_*"))
kwargs["cached_flows"] = len(cache.keys("flow_*"))
return super().get_context_data(**kwargs)
class PolicyCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
"""View to clear Policy cache"""
form_class = PolicyCacheClearForm
template_name = "generic/form_non_model.html"
success_url = reverse_lazy("authentik_admin:overview")
success_url = "/"
success_message = _("Successfully cleared Policy cache")
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
@ -75,7 +35,7 @@ class FlowCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
form_class = FlowCacheClearForm
template_name = "generic/form_non_model.html"
success_url = reverse_lazy("authentik_admin:overview")
success_url = "/"
success_message = _("Successfully cleared Flow cache")
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:

View File

@ -5,9 +5,10 @@ from drf_yasg2.views import get_schema_view
from rest_framework import routers
from rest_framework.permissions import AllowAny
from authentik.admin.api.overview import AdministrationOverviewViewSet
from authentik.admin.api.overview_metrics import AdministrationMetricsViewSet
from authentik.admin.api.metrics import AdministrationMetricsViewSet
from authentik.admin.api.tasks import TaskViewSet
from authentik.admin.api.version import VersionViewSet
from authentik.admin.api.workers import WorkerViewSet
from authentik.api.v2.config import ConfigsViewSet
from authentik.api.v2.messages import MessagesViewSet
from authentik.audit.api import EventViewSet
@ -19,13 +20,22 @@ from authentik.core.api.sources import SourceViewSet
from authentik.core.api.tokens import TokenViewSet
from authentik.core.api.users import UserViewSet
from authentik.crypto.api import CertificateKeyPairViewSet
from authentik.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
from authentik.flows.api import (
FlowCacheViewSet,
FlowStageBindingViewSet,
FlowViewSet,
StageViewSet,
)
from authentik.outposts.api import (
DockerServiceConnectionViewSet,
KubernetesServiceConnectionViewSet,
OutpostViewSet,
)
from authentik.policies.api import PolicyBindingViewSet, PolicyViewSet
from authentik.policies.api import (
PolicyBindingViewSet,
PolicyCacheViewSet,
PolicyViewSet,
)
from authentik.policies.dummy.api import DummyPolicyViewSet
from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet
from authentik.policies.expression.api import ExpressionPolicyViewSet
@ -63,9 +73,8 @@ router = routers.DefaultRouter()
router.register("root/messages", MessagesViewSet, basename="messages")
router.register("root/config", ConfigsViewSet, basename="configs")
router.register(
"admin/overview", AdministrationOverviewViewSet, basename="admin_overview"
)
router.register("admin/version", VersionViewSet, basename="admin_version")
router.register("admin/workers", WorkerViewSet, basename="admin_workers")
router.register("admin/metrics", AdministrationMetricsViewSet, basename="admin_metrics")
router.register("admin/system_tasks", TaskViewSet, basename="admin_system_tasks")
@ -82,6 +91,7 @@ router.register(
router.register("outposts/proxy", ProxyOutpostConfigViewSet)
router.register("flows/instances", FlowViewSet)
router.register("flows/cached", FlowCacheViewSet, basename="flows_cache")
router.register("flows/bindings", FlowStageBindingViewSet)
router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet)
@ -94,6 +104,7 @@ router.register("sources/saml", SAMLSourceViewSet)
router.register("sources/oauth", OAuthSourceViewSet)
router.register("policies/all", PolicyViewSet)
router.register("policies/cached", PolicyCacheViewSet, basename="policies_cache")
router.register("policies/bindings", PolicyBindingViewSet)
router.register("policies/expression", ExpressionPolicyViewSet)
router.register("policies/group_membership", GroupMembershipPolicyViewSet)

View File

@ -11,8 +11,9 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.admin.api.overview_metrics import get_events_per_1h
from authentik.admin.api.metrics import get_events_per_1h
from authentik.audit.models import EventAction
from authentik.core.api.providers import ProviderSerializer
from authentik.core.models import Application
from authentik.policies.engine import PolicyEngine
@ -21,6 +22,7 @@ class ApplicationSerializer(ModelSerializer):
"""Application Serializer"""
launch_url = SerializerMethodField()
provider = ProviderSerializer(source="get_provider", required=False)
def get_launch_url(self, instance: Application) -> str:
"""Get generated launch URL"""

View File

@ -1,6 +1,6 @@
"""Provider API Views"""
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.viewsets import ModelViewSet
from authentik.core.models import Provider
@ -14,17 +14,33 @@ class ProviderSerializer(ModelSerializer):
"""Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace("provider", "")
def to_representation(self, instance: Provider):
# pyright: reportGeneralTypeIssues=false
if instance.__class__ == Provider:
return super().to_representation(instance)
return instance.serializer(instance=instance).data
class Meta:
model = Provider
fields = ["pk", "name", "authorization_flow", "property_mappings", "__type__"]
fields = [
"pk",
"name",
"application",
"authorization_flow",
"property_mappings",
"__type__",
]
class ProviderViewSet(ReadOnlyModelViewSet):
class ProviderViewSet(ModelViewSet):
"""Provider Viewset"""
queryset = Provider.objects.all()
serializer_class = ProviderSerializer
filterset_fields = {
"application": ["isnull"],
}
def get_queryset(self):
return Provider.objects.select_subclasses()

View File

@ -1,5 +1,6 @@
"""User API Views"""
from drf_yasg2.utils import swagger_auto_schema
from guardian.utils import get_anonymous_user
from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.response import Response
@ -33,9 +34,12 @@ class UserSerializer(ModelSerializer):
class UserViewSet(ModelViewSet):
"""User Viewset"""
queryset = User.objects.all()
queryset = User.objects.all().exclude(pk=get_anonymous_user().pk)
serializer_class = UserSerializer
def get_queryset(self):
return User.objects.all().exclude(pk=get_anonymous_user().pk)
@swagger_auto_schema(responses={200: UserSerializer(many=False)})
@action(detail=False)
# pylint: disable=invalid-name

View File

@ -14,6 +14,7 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from guardian.mixins import GuardianUserMixin
from model_utils.managers import InheritanceManager
from rest_framework.serializers import Serializer
from structlog import get_logger
from authentik.core.exceptions import PropertyMappingExpressionException
@ -127,7 +128,7 @@ class User(GuardianUserMixin, AbstractUser):
verbose_name_plural = _("Users")
class Provider(models.Model):
class Provider(SerializerModel):
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
name = models.TextField()
@ -156,6 +157,11 @@ class Provider(models.Model):
"""Return Form class used to edit this object"""
raise NotImplementedError
@property
def serializer(self) -> Type[Serializer]:
"""Get serializer for this model"""
raise NotImplementedError
def __str__(self):
return self.name

View File

@ -3,6 +3,15 @@
{% load i18n %}
{% load authentik_utils %}
{% block head %}
{{ block.super }}
<style>
.pf-c-empty-state {
height: 100vh;
}
</style>
{% endblock %}
{% block body %}
<section class="pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
<div class="pf-c-empty-state">

View File

@ -31,7 +31,7 @@
<p class="pf-c-form__helper-text">{{ field.help_text }}</p>
{% endif %}
</div>
{% elif field.field.widget|fieldtype == 'Select' %}
{% elif field.field.widget|fieldtype == 'Select' or field.field.widget|fieldtype == "SelectMultiple" %}
<div class="pf-c-form__group-label">
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
<span class="pf-c-form__label-text">{{ field.label }}</span>
@ -46,6 +46,9 @@
{% if field.help_text %}
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
{% endif %}
{% if field.field.widget|fieldtype == 'SelectMultiple' %}
<p class="pf-c-form__helper-text">{% trans 'Hold control/command to select multiple items.' %}</p>
{% endif %}
</div>
</div>
{% elif field.field.widget|fieldtype == 'CheckboxInput' %}

View File

@ -94,11 +94,6 @@ class TokenCreateView(
success_url = reverse_lazy("authentik_core:user-tokens")
success_message = _("Successfully created Token")
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
kwargs["container_template"] = "user/base.html"
return kwargs
def form_valid(self, form: UserTokenForm) -> HttpResponse:
form.instance.user = self.request.user
form.instance.intent = TokenIntents.INTENT_API
@ -112,21 +107,16 @@ class TokenUpdateView(
model = Token
form_class = UserTokenForm
permission_required = "authentik_core.update_token"
permission_required = "authentik_core.change_token"
template_name = "generic/update.html"
success_url = reverse_lazy("authentik_core:user-tokens")
success_message = _("Successfully updated Token")
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
kwargs["container_template"] = "user/base.html"
return kwargs
def get_object(self) -> Token:
identifier = self.kwargs.get("identifier")
return get_objects_for_user(
self.request.user, "authentik_core.update_token", self.model
).filter(intent=TokenIntents.INTENT_API, identifier=identifier)
self.request.user, self.permission_required, self.model
).filter(intent=TokenIntents.INTENT_API, identifier=identifier).first()
class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
@ -138,7 +128,8 @@ class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessage
success_url = reverse_lazy("authentik_core:user-tokens")
success_message = _("Successfully deleted Token")
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
kwargs["container_template"] = "user/base.html"
return kwargs
def get_object(self) -> Token:
identifier = self.kwargs.get("identifier")
return get_objects_for_user(
self.request.user, self.permission_required, self.model
).filter(intent=TokenIntents.INTENT_API, identifier=identifier).first()

View File

@ -1,7 +1,14 @@
"""Flow API Views"""
from django.core.cache import cache
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework.mixins import ListModelMixin
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import (
ModelSerializer,
Serializer,
SerializerMethodField,
)
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from authentik.flows.models import Flow, FlowStageBinding, Stage
from authentik.flows.planner import cache_key
@ -98,3 +105,14 @@ class FlowStageBindingViewSet(ModelViewSet):
queryset = FlowStageBinding.objects.all()
serializer_class = FlowStageBindingSerializer
filterset_fields = "__all__"
class FlowCacheViewSet(ListModelMixin, GenericViewSet):
"""Info about cached flows"""
queryset = Flow.objects.none()
serializer_class = Serializer
def list(self, request: Request) -> Response:
"""Info about cached flows"""
return Response(data={"pagination": {"count": len(cache.keys("flow_*"))}})

View File

@ -19,6 +19,7 @@ LOGGER = get_logger()
PLAN_CONTEXT_PENDING_USER = "pending_user"
PLAN_CONTEXT_SSO = "is_sso"
PLAN_CONTEXT_REDIRECT = "redirect"
PLAN_CONTEXT_APPLICATION = "application"

View File

@ -8,7 +8,7 @@ from django.test.client import RequestFactory
from django.utils.encoding import force_str
from authentik.core.models import User
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import FlowPlan, FlowPlanner
@ -40,6 +40,10 @@ class TestFlowExecutor(TestCase):
def setUp(self):
self.request_factory = RequestFactory()
@patch(
"authentik.flows.views.to_stage_response",
TO_STAGE_RESPONSE_MOCK,
)
def test_existing_plan_diff_flow(self):
"""Check that a plan for a different flow cancels the current plan"""
flow = Flow.objects.create(
@ -62,7 +66,7 @@ class TestFlowExecutor(TestCase):
"authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}
),
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, 302)
self.assertEqual(cancel_mock.call_count, 2)
@patch(
@ -105,10 +109,13 @@ class TestFlowExecutor(TestCase):
response = self.client.get(
reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
)
self.assertEqual(response.status_code, 200)
self.assertIsInstance(response, AccessDeniedResponse)
self.assertInHTML(EmptyFlowException.__doc__, response.rendered_content)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("authentik_core:shell"))
@patch(
"authentik.flows.views.to_stage_response",
TO_STAGE_RESPONSE_MOCK,
)
def test_invalid_flow_redirect(self):
"""Tests that an invalid flow still redirects"""
flow = Flow.objects.create(
@ -121,11 +128,8 @@ class TestFlowExecutor(TestCase):
dest = "/unique-string"
url = reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug})
response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}")
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{"type": "redirect", "to": dest},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("authentik_core:shell"))
def test_multi_stage_flow(self):
"""Test a full flow with multiple stages"""
@ -161,6 +165,10 @@ class TestFlowExecutor(TestCase):
plan: FlowPlan = session[SESSION_KEY_PLAN]
self.assertEqual(len(plan.stages), 1)
@patch(
"authentik.flows.views.to_stage_response",
TO_STAGE_RESPONSE_MOCK,
)
def test_reevaluate_remove_last(self):
"""Test planner with re-evaluate (last stage is removed)"""
flow = Flow.objects.create(

View File

@ -21,7 +21,12 @@ from authentik.audit.models import cleanse_dict
from authentik.core.models import USER_ATTRIBUTE_DEBUG
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan, FlowPlanner
from authentik.flows.planner import (
PLAN_CONTEXT_PENDING_USER,
PLAN_CONTEXT_REDIRECT,
FlowPlan,
FlowPlanner,
)
from authentik.lib.utils.reflection import class_to_path
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
from authentik.policies.http import AccessDeniedResponse
@ -83,7 +88,9 @@ class FlowExecutorView(View):
return to_stage_response(self.request, self.handle_invalid_flow(exc))
except EmptyFlowException as exc:
LOGGER.warning("f(exec): Flow is empty", exc=exc)
return to_stage_response(self.request, self.handle_invalid_flow(exc))
# To match behaviour with loading an empty flow plan from cache,
# we don't show an error message here, but rather call _flow_done()
return self._flow_done()
# We don't save the Plan after getting the next stage
# as it hasn't been successfully passed yet
next_stage = self.plan.next(self.request)
@ -143,11 +150,15 @@ class FlowExecutorView(View):
"""User Successfully passed all stages"""
# Since this is wrapped by the ExecutorShell, the next argument is saved in the session
# extract the next param before cancel as that cleans it
next_param = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:shell"
)
next_param = None
if self.plan:
next_param = self.plan.context.get(PLAN_CONTEXT_REDIRECT)
if not next_param:
next_param = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:shell"
)
self.cancel()
return redirect_with_qs(next_param)
return to_stage_response(self.request, redirect_with_qs(next_param))
def stage_ok(self) -> HttpResponse:
"""Callback called by stages upon successful completion.

View File

@ -1,7 +1,11 @@
"""Outpost forms"""
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from kubernetes.client.configuration import Configuration
from kubernetes.config.config_exception import ConfigException
from kubernetes.config.kube_config import load_kube_config_from_dict
from authentik.admin.fields import CodeMirrorWidget, YAMLField
from authentik.crypto.models import CertificateKeyPair
@ -71,6 +75,23 @@ class DockerServiceConnectionForm(forms.ModelForm):
class KubernetesServiceConnectionForm(forms.ModelForm):
"""Kubernetes service-connection form"""
def clean_kubeconfig(self):
"""Validate kubeconfig by attempting to load it"""
kubeconfig = self.cleaned_data["kubeconfig"]
if kubeconfig == {}:
if not self.cleaned_data["local"]:
raise ValidationError(
_("You can only use an empty kubeconfig when local is enabled.")
)
# Empty kubeconfig is valid
return kubeconfig
config = Configuration()
try:
load_kube_config_from_dict(kubeconfig, client_configuration=config)
except ConfigException:
raise ValidationError(_("Invalid kubeconfig"))
return kubeconfig
class Meta:
model = KubernetesServiceConnection

View File

@ -0,0 +1,21 @@
# Generated by Django 3.1.4 on 2020-12-24 12:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_outposts", "0014_auto_20201213_1407"),
]
operations = [
migrations.AlterField(
model_name="kubernetesserviceconnection",
name="kubeconfig",
field=models.JSONField(
blank=True,
help_text="Paste your kubeconfig here. authentik will automatically use the currently selected context.",
),
),
]

View File

@ -234,7 +234,8 @@ class KubernetesServiceConnection(OutpostServiceConnection):
"Paste your kubeconfig here. authentik will automatically use "
"the currently selected context."
)
)
),
blank=True,
)
@property

View File

@ -42,9 +42,8 @@ def outpost_service_connection_state(connection_pk: Any):
.select_subclasses()
.first()
)
cache.delete(f"outpost_service_connection_{connection.pk.hex}")
state = connection.fetch_state()
cache.set(connection.state_key, state, timeout=0)
cache.set(connection.state_key, state, timeout=None)
@CELERY_APP.task(bind=True, base=MonitoredTask)

View File

@ -1,11 +1,16 @@
"""policy API Views"""
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.mixins import ListModelMixin
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import (
ModelSerializer,
PrimaryKeyRelatedField,
Serializer,
SerializerMethodField,
)
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
from authentik.policies.forms import GENERAL_FIELDS
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
@ -68,6 +73,10 @@ class PolicyViewSet(ReadOnlyModelViewSet):
queryset = Policy.objects.all()
serializer_class = PolicySerializer
filterset_fields = {
"bindings": ["isnull"],
"promptstage": ["isnull"],
}
def get_queryset(self):
return Policy.objects.select_subclasses()
@ -98,3 +107,14 @@ class PolicyBindingViewSet(ModelViewSet):
serializer_class = PolicyBindingSerializer
filterset_fields = ["policy", "target", "enabled", "order", "timeout"]
search_fields = ["policy__name"]
class PolicyCacheViewSet(ListModelMixin, GenericViewSet):
"""Info about cached policies"""
queryset = Policy.objects.none()
serializer_class = Serializer
def list(self, request: Request) -> Response:
"""Info about cached policies"""
return Response(data={"pagination": {"count": len(cache.keys("policy_*"))}})

View File

@ -21,6 +21,7 @@ class PolicyEvaluator(BaseEvaluator):
def __init__(self, policy_name: str):
super().__init__()
self._messages = []
self._context["ak_logger"] = get_logger(policy_name)
self._context["ak_message"] = self.expr_func_message
self._context["ip_address"] = ip_address
self._context["ip_network"] = ip_network

View File

@ -18,6 +18,7 @@ from django.utils import dateformat, timezone
from django.utils.translation import gettext_lazy as _
from jwkest.jwk import Key, RSAKey, SYMKey, import_rsa_key
from jwkest.jws import JWS
from rest_framework.serializers import Serializer
from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User
from authentik.crypto.models import CertificateKeyPair
@ -263,6 +264,12 @@ class OAuth2Provider(Provider):
launch_url = urlparse(main_url)
return main_url.replace(launch_url.path, "")
@property
def serializer(self) -> Type[Serializer]:
from authentik.providers.oauth2.api import OAuth2ProviderSerializer
return OAuth2ProviderSerializer
@property
def form(self) -> Type[ModelForm]:
from authentik.providers.oauth2.forms import OAuth2ProviderForm

View File

@ -4,6 +4,16 @@
{% load i18n %}
{% load authentik_utils %}
{% block head %}
{{ block.super }}
<style>
.pf-c-background-image::before {
background-image: url("{% static 'dist/assets/images/flow_background.jpg' %}");
background-position: center;
}
</style>
{% endblock %}
{% block title %}
{% trans 'End session' %}
{% endblock %}

View File

@ -8,6 +8,7 @@ from django.db import models
from django.forms import ModelForm
from django.http import HttpRequest
from django.utils.translation import gettext as _
from rest_framework.serializers import Serializer
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.models import DomainlessURLValidator
@ -108,6 +109,12 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
return ProxyProviderForm
@property
def serializer(self) -> Type[Serializer]:
from authentik.providers.proxy.api import ProxyProviderSerializer
return ProxyProviderSerializer
@property
def launch_url(self) -> Optional[str]:
"""Use external_host as launch URL"""

View File

@ -7,6 +7,7 @@ from django.forms import ModelForm
from django.http import HttpRequest
from django.shortcuts import reverse
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from structlog import get_logger
from authentik.core.models import PropertyMapping, Provider
@ -145,6 +146,12 @@ class SAMLProvider(Provider):
launch_url = urlparse(self.acs_url)
return self.acs_url.replace(launch_url.path, "")
@property
def serializer(self) -> Type[Serializer]:
from authentik.providers.saml.api import SAMLPropertyMappingSerializer
return SAMLPropertyMappingSerializer
@property
def form(self) -> Type[ModelForm]:
from authentik.providers.saml.forms import SAMLProviderForm

View File

@ -15,10 +15,11 @@ from authentik.core.models import User
from authentik.flows.models import Flow, in_memory_stage
from authentik.flows.planner import (
PLAN_CONTEXT_PENDING_USER,
PLAN_CONTEXT_REDIRECT,
PLAN_CONTEXT_SSO,
FlowPlanner,
)
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.policies.utils import delete_none_keys
from authentik.sources.oauth.auth import AuthorizedServiceBackend
@ -135,11 +136,17 @@ class OAuthCallback(OAuthClientMixin, View):
def handle_login_flow(self, flow: Flow, **kwargs) -> HttpResponse:
"""Prepare Authentication Plan, redirect user FlowExecutor"""
# Ensure redirect is carried through when user was trying to
# authorize application
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:shell"
)
kwargs.update(
{
# Since we authenticate the user by their token, they have no backend set
PLAN_CONTEXT_AUTHENTICATION_BACKEND: "django.contrib.auth.backends.ModelBackend",
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_REDIRECT: final_redirect,
}
)
# We run the Flow planner here so we can pass the Pending user in the context

View File

@ -13,10 +13,11 @@ from authentik.core.models import User
from authentik.flows.models import Flow
from authentik.flows.planner import (
PLAN_CONTEXT_PENDING_USER,
PLAN_CONTEXT_REDIRECT,
PLAN_CONTEXT_SSO,
FlowPlanner,
)
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.policies.utils import delete_none_keys
from authentik.sources.saml.exceptions import (
@ -54,11 +55,14 @@ class ResponseProcessor:
_root: Any
_root_xml: str
_http_request: HttpRequest
def __init__(self, source: SAMLSource):
self._source = source
def parse(self, request: HttpRequest):
"""Check if `request` contains SAML Response data, parse and validate it."""
self._http_request = request
# First off, check if we have any SAML Data at all.
raw_response = request.POST.get("SAMLResponse", None)
if not raw_response:
@ -187,6 +191,11 @@ class ResponseProcessor:
name_id_filter = self._get_name_id_filter()
matching_users = User.objects.filter(**name_id_filter)
# Ensure redirect is carried through when user was trying to
# authorize application
final_redirect = self._http_request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:shell"
)
if matching_users.exists():
# User exists already, switch to authentication flow
return self._flow_response(
@ -195,6 +204,7 @@ class ResponseProcessor:
**{
PLAN_CONTEXT_PENDING_USER: matching_users.first(),
PLAN_CONTEXT_AUTHENTICATION_BACKEND: DEFAULT_BACKEND,
PLAN_CONTEXT_REDIRECT: final_redirect,
},
)
return self._flow_response(

View File

@ -1,6 +1,5 @@
"""authentik flows invitation forms"""
from django import forms
from django.utils.translation import gettext as _
from authentik.admin.fields import CodeMirrorWidget, YAMLField
from authentik.stages.invitation.models import Invitation, InvitationStage
@ -25,8 +24,5 @@ class InvitationForm(forms.ModelForm):
model = Invitation
fields = ["expires", "fixed_data"]
labels = {
"fixed_data": _("Optional fixed data to enforce on user enrollment."),
}
widgets = {"fixed_data": CodeMirrorWidget()}
field_classes = {"fixed_data": YAMLField}

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2020-12-25 21:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_invitation", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="invitation",
name="fixed_data",
field=models.JSONField(blank=True, default=dict),
),
]

View File

@ -61,7 +61,11 @@ class Invitation(models.Model):
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
expires = models.DateTimeField(default=None, blank=True, null=True)
fixed_data = models.JSONField(default=dict)
fixed_data = models.JSONField(
default=dict,
blank=True,
help_text=_("Optional fixed data to enforce on user enrollment."),
)
def __str__(self):
return f"Invitation {self.invite_uuid.hex} created by {self.created_by}"

View File

@ -53,5 +53,5 @@ class PasswordStageForm(forms.ModelForm):
fields = ["name", "backends", "configure_flow", "failed_attempts_before_cancel"]
widgets = {
"name": forms.TextInput(),
"backends": forms.SelectMultiple(get_authentication_backends()),
"backends": forms.SelectMultiple(choices=get_authentication_backends()),
}

View File

@ -19,7 +19,7 @@ services:
networks:
- internal
server:
image: beryju/authentik:${AUTHENTIK_TAG:-0.13.1-stable}
image: beryju/authentik:${AUTHENTIK_TAG:-0.13.5-stable}
command: server
environment:
AUTHENTIK_REDIS__HOST: redis
@ -44,7 +44,7 @@ services:
env_file:
- .env
worker:
image: beryju/authentik:${AUTHENTIK_TAG:-0.13.1-stable}
image: beryju/authentik:${AUTHENTIK_TAG:-0.13.5-stable}
command: worker
networks:
- internal
@ -60,7 +60,7 @@ services:
env_file:
- .env
static:
image: beryju/authentik-static:${AUTHENTIK_TAG:-0.13.1-stable}
image: beryju/authentik-static:${AUTHENTIK_TAG:-0.13.5-stable}
networks:
- internal
labels:

View File

@ -4,7 +4,7 @@ name: authentik
home: https://goauthentik.io
sources:
- https://github.com/BeryJu/authentik
version: "0.13.1-stable"
version: "0.13.5-stable"
icon: https://raw.githubusercontent.com/BeryJu/authentik/master/web/icons/icon.svg
dependencies:
- name: postgresql

View File

@ -4,7 +4,7 @@
|-----------------------------------|-------------------------|-------------|
| image.name | beryju/authentik | Image used to run the authentik server and worker |
| image.name_static | beryju/authentik-static | Image used to run the authentik static server (CSS and JS Files) |
| image.tag | 0.13.1-stable | Image tag |
| image.tag | 0.13.5-stable | Image tag |
| image.pullPolicy | IfNotPresent | Image Pull Policy used for all deployments |
| serverReplicas | 1 | Replicas for the Server deployment |
| workerReplicas | 1 | Replicas for the Worker deployment |

View File

@ -5,7 +5,7 @@ image:
name: beryju/authentik
name_static: beryju/authentik-static
name_outposts: beryju/authentik # Prefix used for Outpost deployments, Outpost type and version is appended
tag: 0.13.1-stable
tag: 0.13.5-stable
pullPolicy: IfNotPresent
serverReplicas: 1

View File

@ -1,3 +1,3 @@
package pkg
const VERSION = "0.13.1-stable"
const VERSION = "0.13.5-stable"

View File

@ -22,11 +22,11 @@ paths:
/admin/metrics/:
get:
operationId: admin_metrics_list
description: Return single instance of AdministrationMetricsSerializer
description: Login Metrics per 1h
parameters: []
responses:
'200':
description: Overview View
description: Login Metrics per 1h
schema:
description: ''
type: array
@ -35,22 +35,6 @@ paths:
tags:
- admin
parameters: []
/admin/overview/:
get:
operationId: admin_overview_list
description: Return single instance of AdministrationOverviewSerializer
parameters: []
responses:
'200':
description: Overview View
schema:
description: ''
type: array
items:
$ref: '#/definitions/AdministrationOverview'
tags:
- admin
parameters: []
/admin/system_tasks/:
get:
operationId: admin_system_tasks_list
@ -82,6 +66,95 @@ paths:
in: path
required: true
type: string
/admin/version/:
get:
operationId: admin_version_list
description: Get running and latest version.
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: Get running and latest version.
schema:
description: ''
type: array
items:
$ref: '#/definitions/Version'
tags:
- admin
parameters: []
/admin/workers/:
get:
operationId: admin_workers_list
description: Get currently connected worker count.
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:
description: ''
type: object
properties: {}
tags:
- admin
parameters: []
/audit/events/:
get:
operationId: audit_events_list
@ -1062,6 +1135,59 @@ paths:
required: true
type: string
format: uuid
/flows/cached/:
get:
operationId: flows_cached_list
description: Info about cached flows
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:
description: ''
type: object
properties: {}
tags:
- flows
parameters: []
/flows/instances/:
get:
operationId: flows_instances_list
@ -1702,6 +1828,16 @@ paths:
operationId: policies_all_list
description: Policy Viewset
parameters:
- name: bindings__isnull
in: query
description: ''
required: false
type: string
- name: promptstage__isnull
in: query
description: ''
required: false
type: string
- name: ordering
in: query
description: Which field to use when ordering the results.
@ -1919,6 +2055,59 @@ paths:
required: true
type: string
format: uuid
/policies/cached/:
get:
operationId: policies_cached_list
description: Info about cached policies
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:
description: ''
type: object
properties: {}
tags:
- policies
parameters: []
/policies/dummy/:
get:
operationId: policies_dummy_list
@ -3264,6 +3453,11 @@ paths:
operationId: providers_all_list
description: Provider Viewset
parameters:
- name: application__isnull
in: query
description: ''
required: false
type: string
- name: ordering
in: query
description: Which field to use when ordering the results.
@ -3309,6 +3503,22 @@ paths:
$ref: '#/definitions/Provider'
tags:
- providers
post:
operationId: providers_all_create
description: Provider Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/Provider'
responses:
'201':
description: ''
schema:
$ref: '#/definitions/Provider'
tags:
- providers
parameters: []
/providers/all/{id}/:
get:
@ -3322,6 +3532,47 @@ paths:
$ref: '#/definitions/Provider'
tags:
- providers
put:
operationId: providers_all_update
description: Provider Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/Provider'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Provider'
tags:
- providers
patch:
operationId: providers_all_partial_update
description: Provider Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/Provider'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Provider'
tags:
- providers
delete:
operationId: providers_all_delete
description: Provider Viewset
parameters: []
responses:
'204':
description: ''
tags:
- providers
parameters:
- name: id
in: path
@ -6421,7 +6672,7 @@ paths:
format: uuid
definitions:
AdministrationMetrics:
description: Overview View
description: Login Metrics per 1h
type: object
properties:
logins_per_1h:
@ -6432,38 +6683,6 @@ definitions:
title: Logins failed per 1h
type: string
readOnly: true
AdministrationOverview:
description: Overview View
type: object
properties:
version:
title: Version
type: string
readOnly: true
version_latest:
title: Version latest
type: string
readOnly: true
worker_count:
title: Worker count
type: integer
readOnly: true
providers_without_application:
title: Providers without application
type: integer
readOnly: true
policies_without_binding:
title: Policies without binding
type: integer
readOnly: true
cached_policies:
title: Cached policies
type: integer
readOnly: true
cached_flows:
title: Cached flows
type: integer
readOnly: true
Task:
description: Serialize TaskInfo and TaskResult
required:
@ -6494,6 +6713,22 @@ definitions:
type: array
items:
type: string
Version:
description: Get running and latest version.
type: object
properties:
version_current:
title: Version current
type: string
readOnly: true
version_latest:
title: Version latest
type: string
readOnly: true
outdated:
title: Outdated
type: boolean
readOnly: true
Event:
description: Event Serializer
required:
@ -7110,7 +7345,6 @@ definitions:
description: KubernetesServiceConnection Serializer
required:
- name
- kubeconfig
type: object
properties:
pk:
@ -7480,6 +7714,7 @@ definitions:
description: Provider Serializer
required:
- name
- application
- authorization_flow
type: object
properties:
@ -7491,6 +7726,9 @@ definitions:
title: Name
type: string
minLength: 1
application:
title: Application
type: string
authorization_flow:
title: Authorization flow
description: Flow used when authorizing this provider.
@ -8357,6 +8595,7 @@ definitions:
x-nullable: true
fixed_data:
title: Fixed data
description: Optional fixed data to enforce on user enrollment.
type: object
OTPStaticStage:
description: OTPStaticStage Serializer

108
web/package-lock.json generated
View File

@ -142,13 +142,13 @@
}
},
"@sentry/browser": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.29.0.tgz",
"integrity": "sha512-kRlt1mE2wrYjspnIupNnPxqsUrRuy02SuXhbpP7J6uu8QasoEmJ78hk0hHz4jOZRmuWwfs2zIXD4tLGgWOKq8A==",
"version": "5.29.2",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.29.2.tgz",
"integrity": "sha512-uxZ7y7rp85tJll+RZtXRhXPbnFnOaxZqJEv05vJlXBtBNLQtlczV5iCtU9mZRLVHDtmZ5VVKUV8IKXntEqqDpQ==",
"requires": {
"@sentry/core": "5.29.0",
"@sentry/types": "5.29.0",
"@sentry/utils": "5.29.0",
"@sentry/core": "5.29.2",
"@sentry/types": "5.29.2",
"@sentry/utils": "5.29.2",
"tslib": "^1.9.3"
},
"dependencies": {
@ -160,14 +160,14 @@
}
},
"@sentry/core": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.29.0.tgz",
"integrity": "sha512-a1sZBJ2u3NG0YDlGvOTwUCWiNjhfmDtAQiKK1o6RIIbcrWy9TlSps7CYDkBP239Y3A4pnvohjEEKEP3v3L3LZQ==",
"version": "5.29.2",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.29.2.tgz",
"integrity": "sha512-7WYkoxB5IdlNEbwOwqSU64erUKH4laavPsM0/yQ+jojM76ErxlgEF0u//p5WaLPRzh3iDSt6BH+9TL45oNZeZw==",
"requires": {
"@sentry/hub": "5.29.0",
"@sentry/minimal": "5.29.0",
"@sentry/types": "5.29.0",
"@sentry/utils": "5.29.0",
"@sentry/hub": "5.29.2",
"@sentry/minimal": "5.29.2",
"@sentry/types": "5.29.2",
"@sentry/utils": "5.29.2",
"tslib": "^1.9.3"
},
"dependencies": {
@ -179,12 +179,12 @@
}
},
"@sentry/hub": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.29.0.tgz",
"integrity": "sha512-kcDPQsRG4cFdmqDh+TzjeO7lWYxU8s1dZYAbbl1J4uGKmhNB0J7I4ak4SGwTsXLY6fhbierxr6PRaoNojCxjPw==",
"version": "5.29.2",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.29.2.tgz",
"integrity": "sha512-LaAIo2hwUk9ykeh9RF0cwLy6IRw+DjEee8l1HfEaDFUM6TPGlNNGObMJNXb9/95jzWp7jWwOpQjoIE3jepdQJQ==",
"requires": {
"@sentry/types": "5.29.0",
"@sentry/utils": "5.29.0",
"@sentry/types": "5.29.2",
"@sentry/utils": "5.29.2",
"tslib": "^1.9.3"
},
"dependencies": {
@ -196,12 +196,12 @@
}
},
"@sentry/minimal": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.29.0.tgz",
"integrity": "sha512-nhXofdjtO41/caiF1wk1oT3p/QuhOZDYdF/b29DoD2MiAMK9IjhhOXI/gqaRpDKkXlDvd95fDTcx4t/MqqcKXA==",
"version": "5.29.2",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.29.2.tgz",
"integrity": "sha512-0aINSm8fGA1KyM7PavOBe1GDZDxrvnKt+oFnU0L+bTcw8Lr+of+v6Kwd97rkLRNOLw621xP076dL/7LSIzMuhw==",
"requires": {
"@sentry/hub": "5.29.0",
"@sentry/types": "5.29.0",
"@sentry/hub": "5.29.2",
"@sentry/types": "5.29.2",
"tslib": "^1.9.3"
},
"dependencies": {
@ -213,48 +213,48 @@
}
},
"@sentry/tracing": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.29.0.tgz",
"integrity": "sha512-2ZITUH7Eur7IkmRAd5gw8Xt2Sfc28btCnT7o2P2J8ZPD65e99ATqjxXPokx0+6zEkTsstIDD3mbyuwkpbuvuTA==",
"version": "5.29.2",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.29.2.tgz",
"integrity": "sha512-iumYbVRpvoU3BUuIooxibydeaOOjl5ysc+mzsqhRs2NGW/C3uKAsFXdvyNfqt3bxtRQwJEhwJByLP2u3pLThpw==",
"requires": {
"@sentry/hub": "5.29.0",
"@sentry/minimal": "5.29.0",
"@sentry/types": "5.29.0",
"@sentry/utils": "5.29.0",
"@sentry/hub": "5.29.2",
"@sentry/minimal": "5.29.2",
"@sentry/types": "5.29.2",
"@sentry/utils": "5.29.2",
"tslib": "^1.9.3"
},
"dependencies": {
"@sentry/hub": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.29.0.tgz",
"integrity": "sha512-kcDPQsRG4cFdmqDh+TzjeO7lWYxU8s1dZYAbbl1J4uGKmhNB0J7I4ak4SGwTsXLY6fhbierxr6PRaoNojCxjPw==",
"version": "5.29.2",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.29.2.tgz",
"integrity": "sha512-LaAIo2hwUk9ykeh9RF0cwLy6IRw+DjEee8l1HfEaDFUM6TPGlNNGObMJNXb9/95jzWp7jWwOpQjoIE3jepdQJQ==",
"requires": {
"@sentry/types": "5.29.0",
"@sentry/utils": "5.29.0",
"@sentry/types": "5.29.2",
"@sentry/utils": "5.29.2",
"tslib": "^1.9.3"
}
},
"@sentry/minimal": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.29.0.tgz",
"integrity": "sha512-nhXofdjtO41/caiF1wk1oT3p/QuhOZDYdF/b29DoD2MiAMK9IjhhOXI/gqaRpDKkXlDvd95fDTcx4t/MqqcKXA==",
"version": "5.29.2",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.29.2.tgz",
"integrity": "sha512-0aINSm8fGA1KyM7PavOBe1GDZDxrvnKt+oFnU0L+bTcw8Lr+of+v6Kwd97rkLRNOLw621xP076dL/7LSIzMuhw==",
"requires": {
"@sentry/hub": "5.29.0",
"@sentry/types": "5.29.0",
"@sentry/hub": "5.29.2",
"@sentry/types": "5.29.2",
"tslib": "^1.9.3"
}
},
"@sentry/types": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.29.0.tgz",
"integrity": "sha512-iDkxT/9sT3UF+Xb+JyLjZ5caMXsgLfRyV9VXQEiR2J6mgpMielj184d9jeF3bm/VMuAf/VFFqrHlcVsVgmrrMw=="
"version": "5.29.2",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.29.2.tgz",
"integrity": "sha512-dM9wgt8wy4WRty75QkqQgrw9FV9F+BOMfmc0iaX13Qos7i6Qs2Q0dxtJ83SoR4YGtW8URaHzlDtWlGs5egBiMA=="
},
"@sentry/utils": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.29.0.tgz",
"integrity": "sha512-b2B1gshw2u3EHlAi84PuI5sfmLKXW1z9enMMhNuuNT/CoRp+g5kMAcUv/qYTws7UNnYSvTuVGuZG30v1e0hP9A==",
"version": "5.29.2",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.29.2.tgz",
"integrity": "sha512-nEwQIDjtFkeE4k6yIk4Ka5XjGRklNLThWLs2xfXlL7uwrYOH2B9UBBOOIRUraBm/g/Xrra3xsam/kRxuiwtXZQ==",
"requires": {
"@sentry/types": "5.29.0",
"@sentry/types": "5.29.2",
"tslib": "^1.9.3"
}
},
@ -266,16 +266,16 @@
}
},
"@sentry/types": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.29.0.tgz",
"integrity": "sha512-iDkxT/9sT3UF+Xb+JyLjZ5caMXsgLfRyV9VXQEiR2J6mgpMielj184d9jeF3bm/VMuAf/VFFqrHlcVsVgmrrMw=="
"version": "5.29.2",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.29.2.tgz",
"integrity": "sha512-dM9wgt8wy4WRty75QkqQgrw9FV9F+BOMfmc0iaX13Qos7i6Qs2Q0dxtJ83SoR4YGtW8URaHzlDtWlGs5egBiMA=="
},
"@sentry/utils": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.29.0.tgz",
"integrity": "sha512-b2B1gshw2u3EHlAi84PuI5sfmLKXW1z9enMMhNuuNT/CoRp+g5kMAcUv/qYTws7UNnYSvTuVGuZG30v1e0hP9A==",
"version": "5.29.2",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.29.2.tgz",
"integrity": "sha512-nEwQIDjtFkeE4k6yIk4Ka5XjGRklNLThWLs2xfXlL7uwrYOH2B9UBBOOIRUraBm/g/Xrra3xsam/kRxuiwtXZQ==",
"requires": {
"@sentry/types": "5.29.0",
"@sentry/types": "5.29.2",
"tslib": "^1.9.3"
},
"dependencies": {

View File

@ -9,8 +9,8 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"@patternfly/patternfly": "^4.70.2",
"@sentry/browser": "^5.29.0",
"@sentry/tracing": "^5.29.0",
"@sentry/browser": "^5.29.2",
"@sentry/tracing": "^5.29.2",
"@types/chart.js": "^2.9.29",
"@types/codemirror": "0.0.102",
"chart.js": "^2.9.4",

View File

@ -1,4 +1,4 @@
import { DefaultClient, PBResponse, QueryArguments } from "./client";
import { DefaultClient, PBResponse, QueryArguments } from "./Client";
export class Application {
pk: string;

View File

@ -1,4 +1,4 @@
import { NotFoundError, RequestError } from "./errors";
import { NotFoundError, RequestError } from "./Error";
export const VERSION = "v2beta";

View File

@ -1,4 +1,4 @@
import { DefaultClient } from "./client";
import { DefaultClient } from "./Client";
import * as Sentry from "@sentry/browser";
import { Integrations } from "@sentry/tracing";
import { VERSION } from "../constants";

View File

@ -1,4 +1,4 @@
import { DefaultClient } from "./client";
import { DefaultClient } from "./Client";
export class AuditEvent {
//audit/events/top_per_user/?filter_action=authorize_application

View File

@ -1,4 +1,4 @@
import { DefaultClient, PBResponse, QueryArguments } from "./client";
import { DefaultClient, PBResponse, QueryArguments } from "./Client";
export enum FlowDesignation {
Authentication = "authentication",
@ -33,6 +33,12 @@ export class Flow {
static list(filter?: QueryArguments): Promise<PBResponse<Flow>> {
return DefaultClient.fetch<PBResponse<Flow>>(["flows", "instances"], filter);
}
static cached(): Promise<number> {
return DefaultClient.fetch<PBResponse<Flow>>(["flows", "cached"]).then(r => {
return r.pagination.count;
});
}
}
export class Stage {

24
web/src/api/Policies.ts Normal file
View File

@ -0,0 +1,24 @@
import { DefaultClient, PBResponse, QueryArguments } from "./Client";
export class Policy {
pk: string;
name: string;
constructor() {
throw Error();
}
static get(pk: string): Promise<Policy> {
return DefaultClient.fetch<Policy>(["policies", "all", pk]);
}
static list(filter?: QueryArguments): Promise<PBResponse<Policy>> {
return DefaultClient.fetch<PBResponse<Policy>>(["policies", "all"], filter);
}
static cached(): Promise<number> {
return DefaultClient.fetch<PBResponse<Policy>>(["policies", "cached"]).then(r => {
return r.pagination.count;
});
}
}

View File

@ -1,10 +1,5 @@
import { DefaultClient, PBResponse, QueryArguments } from "./client";
export interface Policy {
pk: string;
name: string;
[key: string]: unknown;
}
import { DefaultClient, PBResponse, QueryArguments } from "./Client";
import { Policy } from "./Policies";
export class PolicyBinding {
pk: string;

19
web/src/api/Providers.ts Normal file
View File

@ -0,0 +1,19 @@
import { DefaultClient, PBResponse, QueryArguments } from "./Client";
export class Provider {
pk: number;
name: string;
authorization_flow: string;
constructor() {
throw Error();
}
static get(slug: string): Promise<Provider> {
return DefaultClient.fetch<Provider>(["providers", "all", slug]);
}
static list(filter?: QueryArguments): Promise<PBResponse<Provider>> {
return DefaultClient.fetch<PBResponse<Provider>>(["providers", "all"], filter);
}
}

View File

@ -1,4 +1,4 @@
import { DefaultClient, PBResponse, QueryArguments } from "./client";
import { DefaultClient, PBResponse, QueryArguments } from "./Client";
export class Source {
pk: string;

View File

@ -1,4 +1,4 @@
import { DefaultClient } from "./client";
import { DefaultClient } from "./Client";
interface TokenResponse {
key: string;

View File

@ -1,4 +1,4 @@
import { DefaultClient, PBResponse } from "./client";
import { DefaultClient, PBResponse } from "./Client";
let _globalMePromise: Promise<User>;

17
web/src/api/Versions.ts Normal file
View File

@ -0,0 +1,17 @@
import { DefaultClient } from "./Client";
export class Version {
version_current: string;
version_latest: string;
outdated: boolean;
constructor() {
throw Error();
}
static get(): Promise<Version> {
return DefaultClient.fetch<Version>(["admin", "version"]);
}
}

View File

@ -1,20 +0,0 @@
import { DefaultClient } from "./client";
export class AdminOverview {
version: string;
version_latest: string;
worker_count: number;
providers_without_application: number;
policies_without_binding: number;
cached_policies: number;
cached_flows: number;
constructor() {
throw Error();
}
static get(): Promise<AdminOverview> {
return DefaultClient.fetch<AdminOverview>(["admin", "overview"]);
}
}

View File

@ -5,11 +5,6 @@ html {
--pf-c-nav__link--PaddingLeft: 0.5rem;
}
/* Fix patternfly sidebar and header with open Modal */
.pf-c-page__sidebar {
z-index: 0;
}
.pf-c-page__header {
z-index: 0;
}
@ -86,6 +81,10 @@ select[multiple] {
font-size: var(--pf-global--FontSize--sm);
}
.pf-c-page__main {
z-index: auto !important;
}
@media (prefers-color-scheme: dark) {
:root {
--ak-dark-foreground: #fafafa;
@ -162,10 +161,12 @@ select[multiple] {
background-color: var(--ak-dark-background-light);
color: var(--ak-dark-foreground);
}
.pf-m-tertiary,
.pf-c-button.pf-m-tertiary {
--pf-c-button--after--BorderColor: var(--ak-dark-foreground-darker);
color: var(--ak-dark-foreground-darker);
}
.pf-m-tertiary:hover,
.pf-c-button.pf-m-tertiary:hover {
--pf-c-button--after--BorderColor: var(--ak-dark-background-lighter);
}
@ -197,6 +198,9 @@ select[multiple] {
.pf-c-login__main {
background-color: var(--ak-dark-background);
}
.pf-c-login__main-body {
color: var(--ak-dark-foreground);
}
.pf-c-login__main-footer-links-item-link > img {
filter: invert(1);
}

View File

@ -28,4 +28,4 @@ export const ColorStyles = css`
background-color: var(--pf-global--danger-color--100);
}
`;
export const VERSION = "0.13.1-stable";
export const VERSION = "0.13.5-stable";

View File

@ -3,7 +3,7 @@ import { css, CSSResult, customElement, html, LitElement, property, TemplateResu
import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css";
// @ts-ignore
import ButtonStyle from "@patternfly/patternfly/components/Button/button.css";
import { tokenByIdentifier } from "../../api/token";
import { tokenByIdentifier } from "../../api/Tokens";
import { ColorStyles, ERROR_CLASS, PRIMARY_CLASS, SUCCESS_CLASS } from "../../constants";
@customElement("ak-token-copy-button")

View File

@ -2,6 +2,7 @@ import { gettext } from "django";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
import { COMMON_STYLES } from "../../common/styles";
import { ColorStyles } from "../../constants";
@customElement("ak-aggregate-card")
export class AggregateCard extends LitElement {
@ -24,22 +25,26 @@ export class AggregateCard extends LitElement {
text-align: center;
color: var(--pf-global--Color--100);
}
`]);
`, ColorStyles]);
}
renderInner(): TemplateResult {
return html`<slot></slot>`;
}
renderHeaderLink(): TemplateResult {
return html`${this.headerLink ? html`<a href="${this.headerLink}">
<i class="fa fa-external-link-alt"> </i>
</a>` : ""}`;
}
render(): TemplateResult {
return html`<div class="pf-c-card pf-c-card-aggregate">
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
<div class="pf-c-card__header-main">
<i class="${ifDefined(this.icon)}"></i> ${this.header ? gettext(this.header) : ""}
</div>
${this.headerLink ? html`<a href="${this.headerLink}">
<i class="fa fa-external-link-alt"> </i>
</a>` : ""}
${this.renderHeaderLink()}
</div>
<div class="pf-c-card__body center-value">
${this.renderInner()}

View File

@ -1,6 +1,6 @@
import { gettext } from "django";
import { LitElement, html, customElement, TemplateResult, property } from "lit-element";
import { DefaultClient } from "../../api/client";
import { DefaultClient } from "../../api/Client";
import "./Message";
import { APIMessage } from "./Message";

View File

@ -1,8 +1,8 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { PBResponse } from "../../api/client";
import { PolicyBinding } from "../../api/policy_binding";
import { PBResponse } from "../../api/Client";
import { Table } from "../../elements/table/Table";
import { PolicyBinding } from "../../api/PolicyBindings";
import "../../elements/Tabs";
import "../../elements/AdminLoginsChart";

View File

@ -65,6 +65,32 @@ export class SidebarItem {
}
});
}
async render(activePath: string): Promise<TemplateResult> {
if (this.condition) {
const result = await this.condition();
if (!result) {
return html``;
}
}
return html` <li class="pf-c-nav__item ${this.hasChildren() ? "pf-m-expandable pf-m-expanded" : ""}">
${this.path ?
html`<a href="#${this.path}" class="pf-c-nav__link ${this.isActive(activePath) ? "pf-m-current" : ""}">
${this.name}
</a>` :
html`<a class="pf-c-nav__link" aria-expanded="true">
${this.name}
<span class="pf-c-nav__toggle">
<i class="fas fa-angle-right" aria-hidden="true"></i>
</span>
</a>
<section class="pf-c-nav__subnav">
<ul class="pf-c-nav__simple-list">
${this._children.map((i) => until(i.render(activePath), html``))}
</ul>
</section>`}
</li>`;
}
}
@customElement("ak-sidebar")
@ -118,37 +144,11 @@ export class Sidebar extends LitElement {
});
}
async renderItem(item: SidebarItem): Promise<TemplateResult> {
if (item.condition) {
const result = await item.condition();
if (!result) {
return html``;
}
}
return html` <li class="pf-c-nav__item ${item.hasChildren() ? "pf-m-expandable pf-m-expanded" : ""}">
${item.path ?
html`<a href="#${item.path}" class="pf-c-nav__link ${item.isActive(this.activePath) ? "pf-m-current": ""}">
${item.name}
</a>` :
html`<a class="pf-c-nav__link" aria-expanded="true">
${item.name}
<span class="pf-c-nav__toggle">
<i class="fas fa-angle-right" aria-hidden="true"></i>
</span>
</a>
<section class="pf-c-nav__subnav">
<ul class="pf-c-nav__simple-list">
${item._children.map((i) => until(this.renderItem(i), html``))}
</ul>
</section>`}
</li>`;
}
render(): TemplateResult {
return html`<nav class="pf-c-nav" aria-label="Global">
<ak-sidebar-brand></ak-sidebar-brand>
<ul class="pf-c-nav__list">
${this.items.map((i) => until(this.renderItem(i), html``))}
${this.items.map((i) => until(i.render(this.activePath), html``))}
</ul>
<ak-sidebar-user></ak-sidebar-user>
</nav>`;

View File

@ -3,7 +3,7 @@ import { css, CSSResult, customElement, html, LitElement, property, TemplateResu
import PageStyle from "@patternfly/patternfly/components/Page/page.css";
// @ts-ignore
import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css";
import { Config } from "../../api/config";
import { Config } from "../../api/Config";
export const DefaultConfig: Config = {
branding_logo: " /static/dist/assets/icons/icon_left_brand.svg",

View File

@ -0,0 +1,35 @@
import { css, CSSResult, customElement, html, LitElement, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../../common/styles";
@customElement("ak-sidebar-hamburger")
export class SidebarHamburger extends LitElement {
static get styles(): CSSResult[] {
return COMMON_STYLES.concat(
css`
:host {
position: absolute;
top: var(--pf-c-page__main-section--PaddingTop);
right: var(--pf-c-page__main-section--PaddingRight);
z-index: 250;
}
`
);
}
onClick(): void {
this.dispatchEvent(
new CustomEvent("ak-sidebar-toggle", {
bubbles: true,
composed: true,
})
);
}
render(): TemplateResult {
return html`<button @click=${() => (this.onClick())} class="pf-c-button pf-m-plain" type="button">
<i class="fas fa-bars" aria-hidden="true"></i>
</button>`;
}
}

View File

@ -5,7 +5,7 @@ import NavStyle from "@patternfly/patternfly/components/Nav/nav.css";
import fa from "@fortawesome/fontawesome-free/css/all.css";
// @ts-ignore
import AvatarStyle from "@patternfly/patternfly/components/Avatar/avatar.css";
import { User } from "../../api/user";
import { User } from "../../api/Users";
import { until } from "lit-html/directives/until";
@customElement("ak-sidebar-user")

View File

@ -1,6 +1,6 @@
import { gettext } from "django";
import { CSSResult, html, LitElement, property, TemplateResult } from "lit-element";
import { PBResponse } from "../../api/client";
import { PBResponse } from "../../api/Client";
import { COMMON_STYLES } from "../../common/styles";
import "./TablePagination";

View File

@ -1,6 +1,6 @@
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../../common/styles";
import { PBPagination } from "../../api/client";
import { PBPagination } from "../../api/Client";
@customElement("ak-table-pagination")
export class TablePagination extends LitElement {

View File

@ -1,17 +1,19 @@
import { customElement } from "lit-element";
import { User } from "../api/user";
import { User } from "../api/Users";
import { SidebarItem } from "../elements/sidebar/Sidebar";
import { SLUG_REGEX } from "../elements/router/Route";
import { Interface } from "./Interface";
export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Library", "/library/"),
new SidebarItem("Monitor", "/audit/audit").when((): Promise<boolean> => {
new SidebarItem("Monitor").children(
new SidebarItem("Overview", "/administration/overview/"),
new SidebarItem("System Tasks", "/administration/tasks/"),
new SidebarItem("Events", "/audit/audit"),
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
}),
new SidebarItem("Administration").children(
new SidebarItem("Overview", "/administration/overview-ng/"),
new SidebarItem("System Tasks", "/administration/tasks/"),
new SidebarItem("Applications", "/administration/applications/").activeWhen(
`^/applications/(?<slug>${SLUG_REGEX})/$`
),
@ -19,27 +21,29 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
`^/sources/(?<slug>${SLUG_REGEX})/$`,
),
new SidebarItem("Providers", "/administration/providers/"),
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("Invitations", "/administration/stages/invitations/"),
),
new SidebarItem("User Management").children(
new SidebarItem("User", "/administration/users/"),
new SidebarItem("Groups", "/administration/groups/")
),
new SidebarItem("Outposts").children(
new SidebarItem("Outposts", "/administration/outposts/"),
new SidebarItem("Service Connections", "/administration/outposts/service_connections/")
),
new SidebarItem("Outposts", "/administration/outposts/"),
new SidebarItem("Outpost Service Connections", "/administration/outposts/service_connections/"),
new SidebarItem("Policies", "/administration/policies/"),
new SidebarItem("Property Mappings", "/administration/property-mappings"),
new SidebarItem("Certificates", "/administration/crypto/certificates"),
new SidebarItem("Tokens", "/administration/tokens/"),
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
})
}),
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("Invitations", "/administration/stages/invitations/"),
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
}),
new SidebarItem("User Management").children(
new SidebarItem("User", "/administration/users/"),
new SidebarItem("Groups", "/administration/groups/")
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
}),
];
@customElement("ak-interface-admin")

View File

@ -1,11 +1,14 @@
import { gettext } from "django";
import { html, LitElement, TemplateResult } from "lit-element";
import { html, LitElement, property, TemplateResult } from "lit-element";
import { SidebarItem } from "../elements/sidebar/Sidebar";
import "../elements/router/RouterOutlet";
import "../elements/messages/MessageContainer";
import "../elements/sidebar/SidebarHamburger";
export abstract class Interface extends LitElement {
@property({type: Boolean})
sidebarOpen?: boolean;
abstract get sidebar(): SidebarItem[];
@ -13,11 +16,24 @@ export abstract class Interface extends LitElement {
return this;
}
constructor() {
super();
this.sidebarOpen = window.outerWidth >= 1280;
window.addEventListener("resize", () => {
this.sidebarOpen = window.outerWidth >= 1280;
});
window.addEventListener("ak-sidebar-toggle", () => {
this.sidebarOpen = !this.sidebarOpen;
});
}
render(): TemplateResult {
return html`<ak-message-container></ak-message-container>
<div class="pf-c-page">
<a class="pf-c-skip-to-content pf-c-button pf-m-primary" href="#main-content">${gettext("Skip to content")}</a>
<ak-sidebar class="pf-c-page__sidebar" .items=${this.sidebar}>
<ak-sidebar-hamburger>
</ak-sidebar-hamburger>
<ak-sidebar class="pf-c-page__sidebar ${this.sidebarOpen ? "pf-m-expanded" : "pf-m-collapsed"}" .items=${this.sidebar}>
</ak-sidebar>
<main class="pf-c-page__main">
<ak-router-outlet role="main" class="pf-c-page__main" tabindex="-1" id="main-content" defaultUrl="/library/">

View File

@ -1,8 +1,8 @@
import { gettext } from "django";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
import { Application } from "../api/application";
import { PBResponse } from "../api/client";
import { Application } from "../api/Applications";
import { PBResponse } from "../api/Client";
import { COMMON_STYLES } from "../common/styles";
import { loading, truncate } from "../utils";

View File

@ -1,71 +1,27 @@
import { gettext } from "django";
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { AdminOverview } from "../../api/admin_overview";
import { DefaultClient } from "../../api/client";
import { User } from "../../api/user";
import { CSSResult, customElement, html, LitElement, TemplateResult } from "lit-element";
import { DefaultClient } from "../../api/Client";
import { COMMON_STYLES } from "../../common/styles";
import { AggregatePromiseCard } from "../../elements/cards/AggregatePromiseCard";
import { SpinnerSize } from "../../elements/Spinner";
import "../../elements/AdminLoginsChart";
import "../../elements/cards/AggregatePromiseCard";
import "./TopApplicationsTable";
@customElement("ak-admin-status-card")
export class AdminStatusCard extends AggregatePromiseCard {
@property({type: Number})
value?: number;
@property()
warningText?: string;
@property({type: Number})
lessThanThreshold?: number;
renderNone(): TemplateResult {
return html`<ak-spinner size=${SpinnerSize.Large}></ak-spinner>`;
}
renderGood(): TemplateResult {
return html`<p class="ak-aggregate-card">
<i class="fa fa-check-circle"></i> ${this.value}
</p>`;
}
renderBad(): TemplateResult {
return html`<p class="ak-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> ${this.value}
</p>
<p class="subtext">${this.warningText ? gettext(this.warningText) : ""}</p>`;
}
renderInner(): TemplateResult {
if (!this.value) {
return this.renderNone();
}
return html``;
}
}
import "./cards/AdminStatusCard";
import "./cards/FlowCacheStatusCard";
import "./cards/PolicyCacheStatusCard";
import "./cards/PolicyUnboundStatusCard";
import "./cards/ProviderStatusCard";
import "./cards/UserCountStatusCard";
import "./cards/VersionStatusCard";
import "./cards/WorkerStatusCard";
@customElement("ak-admin-overview")
export class AdminOverviewPage extends LitElement {
@property({attribute: false})
data?: AdminOverview;
@property({attribute: false})
users?: Promise<number>;
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
firstUpdated(): void {
AdminOverview.get().then(value => this.data = value);
this.users = User.count();
}
render(): TemplateResult {
return html`<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
@ -80,48 +36,20 @@ export class AdminOverviewPage extends LitElement {
<ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Apps with most usage" style="grid-column-end: span 2;grid-row-end: span 3;">
<ak-top-applications-table></ak-top-applications-table>
</ak-aggregate-card>
<ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Workers">
${this.data ?
this.data?.worker_count < 1 ?
html`<p class="ak-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> ${this.data?.worker_count}
</p>
<p class="subtext">${gettext("No workers connected.")}</p>` :
html`<p class="ak-aggregate-card">
<i class="fa fa-check-circle"></i> ${this.data?.worker_count}
</p>`
: html`<ak-spinner size=${SpinnerSize.Large}></ak-spinner>`}
</ak-aggregate-card>
<ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-plugged" header="Providers" headerLink="#/administration/providers/">
${this.data ?
this.data?.providers_without_application > 1 ?
html`<p class="ak-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> 0
</p>
<p class="subtext">${gettext("At least one Provider has no application assigned.")}</p>` :
html`<p class="ak-aggregate-card">
<i class="fa fa-check-circle"></i> 0
</p>`
: html`<ak-spinner size=${SpinnerSize.Large}></ak-spinner>`}
</ak-aggregate-card>
<ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-plugged" header="Policies" headerLink="#/administration/policies/">
${this.data ?
this.data?.policies_without_binding > 1 ?
html`<p class="ak-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> 0
</p>
<p class="subtext">${gettext("Policies without binding exist.")}</p>` :
html`<p class="ak-aggregate-card">
<i class="fa fa-check-circle"></i> 0
</p>`
: html`<ak-spinner size=${SpinnerSize.Large}></ak-spinner>`}
</ak-aggregate-card>
<ak-aggregate-card-promise
icon="pf-icon pf-icon-user"
header="Users"
headerLink="#/administration/users/"
.promise=${this.users}>
</ak-aggregate-card-promise>
<ak-admin-status-card-provider class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-plugged" header="Providers" headerLink="#/administration/providers/">
</ak-admin-status-card-provider>
<ak-admin-status-card-policy-unbound class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-infrastructure" header="Policies" headerLink="#/administration/policies/">
</ak-admin-status-card-policy-unbound>
<ak-admin-status-card-user-count class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-user" header="Users" headerLink="#/administration/users/">
</ak-admin-status-card-user-count>
<ak-admin-status-version class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-bundle" header="Version" headerLink="https://github.com/BeryJu/authentik/releases">
</ak-admin-status-version>
<ak-admin-status-card-workers class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Workers">
</ak-admin-status-card-workers>
<ak-admin-status-card-policy-cache class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Cached Policies">
</ak-admin-status-card-policy-cache>
<ak-admin-status-card-flow-cache class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Cached Flows">
</ak-admin-status-card-flow-cache>
</div>
</section>`;
}

View File

@ -1,6 +1,6 @@
import { gettext } from "django";
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { AuditEvent, TopNEvent } from "../../api/events";
import { AuditEvent, TopNEvent } from "../../api/Events";
import { COMMON_STYLES } from "../../common/styles";
import "../../elements/Spinner";

View File

@ -0,0 +1,37 @@
import { html, TemplateResult } from "lit-html";
import { until } from "lit-html/directives/until";
import { AggregateCard } from "../../../elements/cards/AggregateCard";
import { SpinnerSize } from "../../../elements/Spinner";
export interface AdminStatus {
icon: string;
message?: string;
}
export abstract class AdminStatusCard<T> extends AggregateCard {
abstract getPrimaryValue(): Promise<T>;
abstract getStatus(value: T): Promise<AdminStatus>;
value?: T;
renderValue(): TemplateResult {
return html`${this.value}`;
}
renderInner(): TemplateResult {
return html`<p class="center-value">
${until(this.getPrimaryValue().then((v) => {
this.value = v;
return this.getStatus(v);
}).then((status) => {
return html`<p class="ak-aggregate-card">
<i class="${status.icon}"></i> ${this.renderValue()}
</p>
${status.message ? html`<p class="subtext">${status.message}</p>` : html``}`;
}), html`<ak-spinner size="${SpinnerSize.Large}"></ak-spinner>`)}
</p>`;
}
}

View File

@ -0,0 +1,36 @@
import { gettext } from "django";
import { customElement, html, TemplateResult } from "lit-element";
import { Flow } from "../../../api/Flows";
import { AdminStatus, AdminStatusCard } from "./AdminStatusCard";
import "../../../elements/buttons/ModalButton";
@customElement("ak-admin-status-card-flow-cache")
export class FlowCacheStatusCard extends AdminStatusCard<number> {
getPrimaryValue(): Promise<number> {
return Flow.cached();
}
getStatus(value: number): Promise<AdminStatus> {
if (value < 1) {
return Promise.resolve<AdminStatus>({
icon: "fa fa-exclamation-triangle pf-m-warning",
message: gettext("No flows cached."),
});
} else {
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success"
});
}
}
renderHeaderLink(): TemplateResult {
return html`<ak-modal-button href="/administration/overview/cache/flow/">
<a slot="trigger">
<i class="fa fa-trash"> </i>
</a>
<div slot="modal"></div>
</ak-modal-button>`;
}
}

View File

@ -0,0 +1,37 @@
import { gettext } from "django";
import { customElement } from "lit-element";
import { TemplateResult, html } from "lit-html";
import { Policy } from "../../../api/Policies";
import { AdminStatusCard, AdminStatus } from "./AdminStatusCard";
import "../../../elements/buttons/ModalButton";
@customElement("ak-admin-status-card-policy-cache")
export class PolicyCacheStatusCard extends AdminStatusCard<number> {
getPrimaryValue(): Promise<number> {
return Policy.cached();
}
getStatus(value: number): Promise<AdminStatus> {
if (value < 1) {
return Promise.resolve<AdminStatus>({
icon: "fa fa-exclamation-triangle pf-m-warning",
message: gettext("No policies cached. Users may experience slow response times."),
});
} else {
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success"
});
}
}
renderHeaderLink(): TemplateResult {
return html`<ak-modal-button href="/administration/overview/cache/policy/">
<a slot="trigger">
<i class="fa fa-trash"> </i>
</a>
<div slot="modal"></div>
</ak-modal-button>`;
}
}

View File

@ -0,0 +1,31 @@
import { gettext } from "django";
import { customElement } from "lit-element";
import { Policy } from "../../../api/Policies";
import { AdminStatusCard, AdminStatus } from "./AdminStatusCard";
@customElement("ak-admin-status-card-policy-unbound")
export class PolicyUnboundStatusCard extends AdminStatusCard<number> {
getPrimaryValue(): Promise<number> {
return Policy.list({
"bindings__isnull": true,
"promptstage__isnull": true,
}).then((response) => {
return response.pagination.count;
});
}
getStatus(value: number): Promise<AdminStatus> {
if (value > 0) {
return Promise.resolve<AdminStatus>({
icon: "fa fa-exclamation-triangle pf-m-warning",
message: gettext("Policies without binding exist."),
});
} else {
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success"
});
}
}
}

View File

@ -0,0 +1,30 @@
import { gettext } from "django";
import { customElement } from "lit-element";
import { Provider } from "../../../api/Providers";
import { AdminStatusCard, AdminStatus } from "./AdminStatusCard";
@customElement("ak-admin-status-card-provider")
export class ProviderStatusCard extends AdminStatusCard<number> {
getPrimaryValue(): Promise<number> {
return Provider.list({
"application__isnull": true
}).then((response) => {
return response.pagination.count;
});
}
getStatus(value: number): Promise<AdminStatus> {
if (value > 0) {
return Promise.resolve<AdminStatus>({
icon: "fa fa-exclamation-triangle pf-m-warning",
message: gettext("Warning: At least one Provider has no application assigned."),
});
} else {
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success"
});
}
}
}

View File

@ -0,0 +1,19 @@
import { customElement } from "lit-element";
import { User } from "../../../api/Users";
import { AdminStatusCard, AdminStatus } from "./AdminStatusCard";
@customElement("ak-admin-status-card-user-count")
export class UserCountStatusCard extends AdminStatusCard<number> {
getPrimaryValue(): Promise<number> {
return User.count();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getStatus(value: number): Promise<AdminStatus> {
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success"
});
}
}

View File

@ -0,0 +1,31 @@
import { gettext } from "django";
import { customElement, html, TemplateResult } from "lit-element";
import { Version } from "../../../api/Versions";
import { AdminStatusCard, AdminStatus } from "./AdminStatusCard";
@customElement("ak-admin-status-version")
export class VersionStatusCard extends AdminStatusCard<Version> {
getPrimaryValue(): Promise<Version> {
return Version.get();
}
getStatus(value: Version): Promise<AdminStatus> {
if (value.outdated) {
return Promise.resolve<AdminStatus>({
icon: "fa fa-exclamation-triangle pf-m-warning",
message: gettext(`${value.version_latest} is available!`),
});
} else {
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success",
message: gettext("Up-to-date!")
});
}
}
renderValue(): TemplateResult {
return html`${this.value?.version_current}`;
}
}

View File

@ -0,0 +1,28 @@
import { gettext } from "django";
import { customElement } from "lit-element";
import { DefaultClient, PBResponse } from "../../../api/Client";
import { AdminStatus, AdminStatusCard } from "./AdminStatusCard";
@customElement("ak-admin-status-card-workers")
export class WorkersStatusCard extends AdminStatusCard<number> {
getPrimaryValue(): Promise<number> {
return DefaultClient.fetch<PBResponse<number>>(["admin", "workers"]).then((r) => {
return r.pagination.count;
});
}
getStatus(value: number): Promise<AdminStatus> {
if (value < 1) {
return Promise.resolve<AdminStatus>({
icon: "fa fa-exclamation-triangle pf-m-warning",
message: gettext("No workers connected. Background tasks will not run."),
});
} else {
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success"
});
}
}
}

View File

@ -1,7 +1,7 @@
import { gettext } from "django";
import { customElement, html, TemplateResult } from "lit-element";
import { Application } from "../../api/application";
import { PBResponse } from "../../api/client";
import { Application } from "../../api/Applications";
import { PBResponse } from "../../api/Client";
import { TablePage } from "../../elements/table/TablePage";
import "../../elements/buttons/ModalButton";

View File

@ -1,7 +1,7 @@
import { gettext } from "django";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { Application } from "../../api/application";
import { DefaultClient } from "../../api/client";
import { Application } from "../../api/Applications";
import { DefaultClient } from "../../api/Client";
import { COMMON_STYLES } from "../../common/styles";
import "../../elements/Tabs";

View File

@ -1,6 +1,6 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { PBResponse } from "../../api/client";
import { PBResponse } from "../../api/Client";
import { Table } from "../../elements/table/Table";
import "../../elements/Tabs";
@ -8,7 +8,7 @@ import "../../elements/AdminLoginsChart";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import "../../elements/policies/BoundPoliciesList";
import { FlowStageBinding } from "../../api/flow";
import { FlowStageBinding } from "../../api/Flows";
@customElement("ak-bound-stages-list")
export class BoundStagesList extends Table<FlowStageBinding> {

View File

@ -1,7 +1,7 @@
import { gettext } from "django";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../../common/styles";
import { Flow } from "../../api/flow";
import { Flow } from "../../api/Flows";
import "../../elements/Tabs";
import "../../elements/AdminLoginsChart";

View File

@ -7,7 +7,7 @@ import "../../elements/AdminLoginsChart";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import "../../elements/policies/BoundPoliciesList";
import { Source } from "../../api/source";
import { Source } from "../../api/Sources";
@customElement("ak-source-view")
export class SourceViewPage extends LitElement {

View File

@ -13,7 +13,7 @@ export const ROUTES: Route[] = [
new Route(new RegExp("^/$")).redirect("/library/"),
new Route(new RegExp("^#.*")).redirect("/library/"),
new Route(new RegExp("^/library/$"), html`<ak-library></ak-library>`),
new Route(new RegExp("^/administration/overview-ng/$"), html`<ak-admin-overview></ak-admin-overview>`),
new Route(new RegExp("^/administration/overview/$"), html`<ak-admin-overview></ak-admin-overview>`),
new Route(new RegExp("^/applications/$"), html`<ak-application-list></ak-application-list>`),
new Route(new RegExp(`^/applications/(?<slug>${SLUG_REGEX})/$`)).then((args) => {
return html`<ak-application-view .args=${args}></ak-application-view>`;

View File

@ -15,7 +15,7 @@ Download the latest `docker-compose.yml` from [here](https://raw.githubuserconte
To optionally enable error-reporting, run `echo AUTHENTIK_ERROR_REPORTING__ENABLED=true >> .env`
To optionally deploy a different version run `echo AUTHENTIK_TAG=0.13.1-stable >> .env`
To optionally deploy a different version run `echo AUTHENTIK_TAG=0.13.5-stable >> .env`
If this is a fresh authentik install run the following commands to generate a password:

View File

@ -22,7 +22,7 @@ image:
name: beryju/authentik
name_static: beryju/authentik-static
name_outposts: beryju/authentik # Prefix used for Outpost deployments, Outpost type and version is appended
tag: 0.13.1-stable
tag: 0.13.5-stable
serverReplicas: 1
workerReplicas: 1