Merge branch 'master' into version-2021.3

This commit is contained in:
Jens Langhammer 2021-03-03 20:48:02 +01:00
commit dd31191845
65 changed files with 2287 additions and 1200 deletions

24
Pipfile.lock generated
View file

@ -95,10 +95,10 @@
}, },
"autobahn": { "autobahn": {
"hashes": [ "hashes": [
"sha256:884f79c50fdc55ade2c315946a9caa145e8b10075eee9d2c2594ea5e8f5226aa", "sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac",
"sha256:bf7a9d302a34d0f719d43c57f65ca1f2f5c982dd6ea0c11e1e190ef6f43710fe" "sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03"
], ],
"version": "==21.2.2" "version": "==21.3.1"
}, },
"automat": { "automat": {
"hashes": [ "hashes": [
@ -116,18 +116,18 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:3570a3c0fbd80bcb30449f87cf9d2f7abb67fac2a5e317d002f9921c59be9b17", "sha256:c9513a9ea00f8d17ecdc02c391ae956bf0f990aa07deec11c421607c09b294e1",
"sha256:ceff2f32ba05acc9ee35a6dd82e29ea285d63e889bed39a6ba7a700146f43749" "sha256:f84ca60e9605af69022f039c035b33d519531eeaac52724b9223a5465f4a8b6b"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.17.18" "version": "==1.17.19"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:51900b10da4ae45be4b16045e5b2ff7d1158a7955d9d7cc5e5a9ba3170f10586", "sha256:135b5f30e6662b46d804f993bf31d9c7769c6c0848321ed0aa0393f5b9c19a94",
"sha256:b181f32d9075e5419a89fa9636ce95946c15459c9bfadfabb53ca902fc8072b8" "sha256:8e42c78d2eb888551635309158c04ef2648a96d8c2c70dbce7712c6ce8629759"
], ],
"version": "==1.20.18" "version": "==1.20.19"
}, },
"cachetools": { "cachetools": {
"hashes": [ "hashes": [
@ -1249,10 +1249,10 @@
}, },
"websocket-client": { "websocket-client": {
"hashes": [ "hashes": [
"sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549", "sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663",
"sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010" "sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f"
], ],
"version": "==0.57.0" "version": "==0.58.0"
}, },
"websockets": { "websockets": {
"hashes": [ "hashes": [

View file

@ -7,14 +7,14 @@ from django.http.response import Http404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_yasg2.utils import swagger_auto_schema from drf_yasg2.utils import swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, DateTimeField, IntegerField, ListField from rest_framework.fields import CharField, ChoiceField, DateTimeField, ListField
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
from rest_framework.viewsets import ViewSet from rest_framework.viewsets import ViewSet
from authentik.events.monitored_tasks import TaskInfo from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus
class TaskSerializer(Serializer): class TaskSerializer(Serializer):
@ -24,7 +24,10 @@ class TaskSerializer(Serializer):
task_description = CharField() task_description = CharField()
task_finish_timestamp = DateTimeField(source="finish_timestamp") task_finish_timestamp = DateTimeField(source="finish_timestamp")
status = IntegerField(source="result.status.value") status = ChoiceField(
source="result.status.name",
choices=[(x.name, x.name) for x in TaskResultStatus],
)
messages = ListField(source="result.messages") messages = ListField(source="result.messages")
def create(self, validated_data: dict) -> Model: def create(self, validated_data: dict) -> Model:

View file

@ -55,7 +55,7 @@ class VersionViewSet(ListModelMixin, GenericViewSet):
def get_queryset(self): # pragma: no cover def get_queryset(self): # pragma: no cover
return None return None
@swagger_auto_schema(responses={200: VersionSerializer(many=True)}) @swagger_auto_schema(responses={200: VersionSerializer(many=False)})
def list(self, request: Request) -> Response: def list(self, request: Request) -> Response:
"""Get running and latest version.""" """Get running and latest version."""
return Response(VersionSerializer(True).data) return Response(VersionSerializer(True).data)

View file

@ -0,0 +1,97 @@
"""Swagger Pagination Schema class"""
from typing import OrderedDict
from drf_yasg2 import openapi
from drf_yasg2.inspectors import PaginatorInspector
class PaginationInspector(PaginatorInspector):
"""Swagger Pagination Schema class"""
def get_paginated_response(self, paginator, response_schema):
"""
:param BasePagination paginator: the paginator
:param openapi.Schema response_schema: the response schema that must be paged.
:rtype: openapi.Schema
"""
return openapi.Schema(
type=openapi.TYPE_OBJECT,
properties=OrderedDict(
(
(
"pagination",
openapi.Schema(
type=openapi.TYPE_OBJECT,
properties=OrderedDict(
(
("next", openapi.Schema(type=openapi.TYPE_NUMBER)),
(
"previous",
openapi.Schema(type=openapi.TYPE_NUMBER),
),
("count", openapi.Schema(type=openapi.TYPE_NUMBER)),
(
"current",
openapi.Schema(type=openapi.TYPE_NUMBER),
),
(
"total_pages",
openapi.Schema(type=openapi.TYPE_NUMBER),
),
(
"start_index",
openapi.Schema(type=openapi.TYPE_NUMBER),
),
(
"end_index",
openapi.Schema(type=openapi.TYPE_NUMBER),
),
)
),
required=[
"next",
"previous",
"count",
"current",
"total_pages",
"start_index",
"end_index",
],
),
),
("results", response_schema),
)
),
required=["results", "pagination"],
)
def get_paginator_parameters(self, paginator):
"""
Get the pagination parameters for a single paginator **instance**.
Should return :data:`.NotHandled` if this inspector
does not know how to handle the given `paginator`.
:param BasePagination paginator: the paginator
:rtype: list[openapi.Parameter]
"""
return [
openapi.Parameter(
"page",
openapi.IN_QUERY,
"Page Index",
False,
None,
openapi.TYPE_INTEGER,
),
openapi.Parameter(
"page_size",
openapi.IN_QUERY,
"Page Size",
False,
None,
openapi.TYPE_INTEGER,
),
]

View file

@ -1,10 +1,11 @@
"""core Configs API""" """core Configs API"""
from django.db.models import Model from django.db.models import Model
from drf_yasg2.utils import swagger_auto_schema from drf_yasg2.utils import swagger_auto_schema
from rest_framework.fields import BooleanField, CharField
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ReadOnlyField, Serializer from rest_framework.serializers import Serializer
from rest_framework.viewsets import ViewSet from rest_framework.viewsets import ViewSet
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
@ -13,12 +14,12 @@ from authentik.lib.config import CONFIG
class ConfigSerializer(Serializer): class ConfigSerializer(Serializer):
"""Serialize authentik Config into DRF Object""" """Serialize authentik Config into DRF Object"""
branding_logo = ReadOnlyField() branding_logo = CharField(read_only=True)
branding_title = ReadOnlyField() branding_title = CharField(read_only=True)
error_reporting_enabled = ReadOnlyField() error_reporting_enabled = BooleanField(read_only=True)
error_reporting_environment = ReadOnlyField() error_reporting_environment = CharField(read_only=True)
error_reporting_send_pii = ReadOnlyField() error_reporting_send_pii = BooleanField(read_only=True)
def create(self, validated_data: dict) -> Model: def create(self, validated_data: dict) -> Model:
raise NotImplementedError raise NotImplementedError
@ -32,7 +33,7 @@ class ConfigsViewSet(ViewSet):
permission_classes = [AllowAny] permission_classes = [AllowAny]
@swagger_auto_schema(responses={200: ConfigSerializer(many=True)}) @swagger_auto_schema(responses={200: ConfigSerializer(many=False)})
def list(self, request: Request) -> Response: def list(self, request: Request) -> Response:
"""Retrive public configuration options""" """Retrive public configuration options"""
config = ConfigSerializer( config = ConfigSerializer(

View file

@ -1,37 +0,0 @@
"""core messages API"""
from django.contrib.messages import get_messages
from django.db.models import Model
from drf_yasg2.utils import swagger_auto_schema
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ReadOnlyField, Serializer
from rest_framework.viewsets import ViewSet
class MessageSerializer(Serializer):
"""Serialize Django Message into DRF Object"""
message = ReadOnlyField()
level = ReadOnlyField()
tags = ReadOnlyField()
extra_tags = ReadOnlyField()
level_tag = ReadOnlyField()
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
class MessagesViewSet(ViewSet):
"""Read-only view set that returns the current session's messages"""
permission_classes = [AllowAny]
@swagger_auto_schema(responses={200: MessageSerializer(many=True)})
def list(self, request: Request) -> Response:
"""List current messages and pass into Serializer"""
all_messages = list(get_messages(request))
return Response(MessageSerializer(all_messages, many=True).data)

View file

@ -10,7 +10,6 @@ from authentik.admin.api.tasks import TaskViewSet
from authentik.admin.api.version import VersionViewSet from authentik.admin.api.version import VersionViewSet
from authentik.admin.api.workers import WorkerViewSet from authentik.admin.api.workers import WorkerViewSet
from authentik.api.v2.config import ConfigsViewSet from authentik.api.v2.config import ConfigsViewSet
from authentik.api.v2.messages import MessagesViewSet
from authentik.core.api.applications import ApplicationViewSet from authentik.core.api.applications import ApplicationViewSet
from authentik.core.api.groups import GroupViewSet from authentik.core.api.groups import GroupViewSet
from authentik.core.api.propertymappings import PropertyMappingViewSet from authentik.core.api.propertymappings import PropertyMappingViewSet
@ -77,7 +76,6 @@ from authentik.stages.user_write.api import UserWriteStageViewSet
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register("root/messages", MessagesViewSet, basename="messages")
router.register("root/config", ConfigsViewSet, basename="configs") router.register("root/config", ConfigsViewSet, basename="configs")
router.register("admin/version", VersionViewSet, basename="admin_version") router.register("admin/version", VersionViewSet, basename="admin_version")

View file

@ -46,8 +46,7 @@ def backup_database(self: MonitoredTask): # pragma: no cover
TaskResult( TaskResult(
TaskResultStatus.SUCCESSFUL, TaskResultStatus.SUCCESSFUL,
[ [
f"Successfully finished database backup {naturaltime(start)}", f"Successfully finished database backup {naturaltime(start)} {out.getvalue()}",
out.getvalue(),
], ],
) )
) )

View file

@ -13,7 +13,7 @@
<p>{% trans "Configure settings relevant to your user profile." %}</p> <p>{% trans "Configure settings relevant to your user profile." %}</p>
</div> </div>
</section> </section>
<ak-tabs> <ak-tabs vertical="true">
<section slot="page-1" data-tab-title="{% trans 'User details' %}" class="pf-c-page__main-section pf-m-no-padding-mobile"> <section slot="page-1" data-tab-title="{% trans 'User details' %}" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-u-display-flex pf-u-justify-content-center"> <div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75"> <div class="pf-u-w-75">

View file

@ -29,7 +29,7 @@ class EventSerializer(ModelSerializer):
] ]
class EventTopPerUserSerialier(Serializer): class EventTopPerUserSerializer(Serializer):
"""Response object of Event's top_per_user""" """Response object of Event's top_per_user"""
application = DictField() application = DictField()
@ -60,7 +60,7 @@ class EventViewSet(ReadOnlyModelViewSet):
filterset_fields = ["action"] filterset_fields = ["action"]
@swagger_auto_schema( @swagger_auto_schema(
method="GET", responses={200: EventTopPerUserSerialier(many=True)} method="GET", responses={200: EventTopPerUserSerializer(many=True)}
) )
@action(detail=False, methods=["GET"]) @action(detail=False, methods=["GET"])
def top_per_user(self, request: Request): def top_per_user(self, request: Request):

View file

@ -5,6 +5,7 @@ from typing import Type
from kubernetes.client import OpenApiException from kubernetes.client import OpenApiException
from kubernetes.client.api_client import ApiClient from kubernetes.client.api_client import ApiClient
from structlog.testing import capture_logs from structlog.testing import capture_logs
from urllib3.exceptions import HTTPError
from yaml import dump_all from yaml import dump_all
from authentik.outposts.controllers.base import BaseController, ControllerException from authentik.outposts.controllers.base import BaseController, ControllerException
@ -42,7 +43,7 @@ class KubernetesController(BaseController):
reconciler = self.reconcilers[reconcile_key](self) reconciler = self.reconcilers[reconcile_key](self)
reconciler.up() reconciler.up()
except OpenApiException as exc: except (OpenApiException, HTTPError) as exc:
raise ControllerException from exc raise ControllerException from exc
def up_with_logs(self) -> list[str]: def up_with_logs(self) -> list[str]:
@ -54,7 +55,7 @@ class KubernetesController(BaseController):
reconciler.up() reconciler.up()
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs] all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
return all_logs return all_logs
except OpenApiException as exc: except (OpenApiException, HTTPError) as exc:
raise ControllerException from exc raise ControllerException from exc
def down(self): def down(self):

View file

@ -139,6 +139,9 @@ GUARDIAN_MONKEY_PATCH = False
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
"DEFAULT_INFO": "authentik.api.v2.urls.info", "DEFAULT_INFO": "authentik.api.v2.urls.info",
"DEFAULT_PAGINATOR_INSPECTORS": [
"authentik.api.pagination_schema.PaginationInspector",
],
"SECURITY_DEFINITIONS": { "SECURITY_DEFINITIONS": {
"token": {"type": "apiKey", "name": "Authorization", "in": "header"} "token": {"type": "apiKey", "name": "Authorization", "in": "header"}
}, },
@ -147,7 +150,6 @@ SWAGGER_SETTINGS = {
REST_FRAMEWORK = { REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "authentik.api.pagination.Pagination", "DEFAULT_PAGINATION_CLASS": "authentik.api.pagination.Pagination",
"PAGE_SIZE": 100, "PAGE_SIZE": 100,
"DATETIME_FORMAT": "%s",
"DEFAULT_FILTER_BACKENDS": [ "DEFAULT_FILTER_BACKENDS": [
"rest_framework_guardian.filters.ObjectPermissionsFilter", "rest_framework_guardian.filters.ObjectPermissionsFilter",
"django_filters.rest_framework.DjangoFilterBackend", "django_filters.rest_framework.DjangoFilterBackend",

View file

@ -1,9 +1,6 @@
"""OTP Validate stage forms""" """OTP Validate stage forms"""
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _
from django_otp import match_token
from authentik.core.models import User
from authentik.flows.models import NotConfiguredAction from authentik.flows.models import NotConfiguredAction
from authentik.stages.authenticator_validate.models import ( from authentik.stages.authenticator_validate.models import (
AuthenticatorValidateStage, AuthenticatorValidateStage,
@ -11,35 +8,6 @@ from authentik.stages.authenticator_validate.models import (
) )
class ValidationForm(forms.Form):
"""OTP Validate stage forms"""
user: User
code = forms.CharField(
label=_("Please enter the token from your device."),
widget=forms.TextInput(
attrs={
"autocomplete": "one-time-code",
"placeholder": "123456",
"autofocus": "autofocus",
}
),
)
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
def clean_code(self):
"""Validate code against all confirmed devices"""
code = self.cleaned_data.get("code")
device = match_token(self.user, code)
if not device:
raise forms.ValidationError(_("Invalid Token"))
return code
class AuthenticatorValidateStageForm(forms.ModelForm): class AuthenticatorValidateStageForm(forms.ModelForm):
"""OTP Validate stage forms""" """OTP Validate stage forms"""

View file

@ -378,8 +378,8 @@ stages:
python ./scripts/az_do_set_branch.py python ./scripts/az_do_set_branch.py
- task: Docker@2 - task: Docker@2
inputs: inputs:
containerRegistry: 'GHCR' containerRegistry: 'beryjuorg-harbor'
repository: 'beryju/authentik' repository: 'authentik/server'
command: 'buildAndPush' command: 'buildAndPush'
Dockerfile: 'Dockerfile' Dockerfile: 'Dockerfile'
tags: "gh-$(branchName)" tags: "gh-$(branchName)"

View file

@ -98,8 +98,8 @@ stages:
python ./scripts/az_do_set_branch.py python ./scripts/az_do_set_branch.py
- task: Docker@2 - task: Docker@2
inputs: inputs:
containerRegistry: 'GHCR' containerRegistry: 'beryjuorg-harbor'
repository: 'beryju/authentik-proxy' repository: 'authentik/proxy'
command: 'buildAndPush' command: 'buildAndPush'
Dockerfile: 'outpost/proxy.Dockerfile' Dockerfile: 'outpost/proxy.Dockerfile'
buildContext: 'outpost/' buildContext: 'outpost/'

File diff suppressed because it is too large Load diff

View file

@ -78,8 +78,8 @@ stages:
python ./scripts/az_do_set_branch.py python ./scripts/az_do_set_branch.py
- task: Docker@2 - task: Docker@2
inputs: inputs:
containerRegistry: 'GHCR' containerRegistry: 'beryjuorg-harbor'
repository: 'beryju/authentik-static' repository: 'authentik/static'
command: 'buildAndPush' command: 'buildAndPush'
Dockerfile: 'web/Dockerfile' Dockerfile: 'web/Dockerfile'
tags: "gh-$(branchName)" tags: "gh-$(branchName)"

View file

@ -1,3 +1,5 @@
import { gettext } from "django";
import { showMessage } from "../elements/messages/MessageContainer";
import { getCookie } from "../utils"; import { getCookie } from "../utils";
import { NotFoundError, RequestError } from "./Error"; import { NotFoundError, RequestError } from "./Error";
@ -47,6 +49,13 @@ export class Client {
} }
return r; return r;
}) })
.catch((e) => {
showMessage({
level_tag: "error",
message: gettext(`Unexpected error while fetching: ${e.toString()}`),
});
return e;
})
.then((r) => r.json()) .then((r) => r.json())
.then((r) => <T>r); .then((r) => <T>r);
} }

View file

@ -1,16 +1,21 @@
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import Chart from "chart.js"; import Chart from "chart.js";
import { showMessage } from "./messages/MessageContainer"; import { DefaultClient } from "../api/Client";
interface TickValue { interface TickValue {
value: number; value: number;
major: boolean; major: boolean;
} }
export interface LoginMetrics {
logins_failed_per_1h: { x: number, y: number }[];
logins_per_1h: { x: number, y: number }[];
}
@customElement("ak-admin-logins-chart") @customElement("ak-admin-logins-chart")
export class AdminLoginsChart extends LitElement { export class AdminLoginsChart extends LitElement {
@property() @property({type: Array})
url = ""; url: string[] = [];
chart?: Chart; chart?: Chart;
@ -40,15 +45,7 @@ export class AdminLoginsChart extends LitElement {
} }
firstUpdated(): void { firstUpdated(): void {
fetch(this.url) DefaultClient.fetch<LoginMetrics>(this.url)
.then((r) => r.json())
.catch((e) => {
showMessage({
level_tag: "error",
message: "Unexpected error"
});
console.error(e);
})
.then((r) => { .then((r) => {
const canvas = <HTMLCanvasElement>this.shadowRoot?.querySelector("canvas"); const canvas = <HTMLCanvasElement>this.shadowRoot?.querySelector("canvas");
if (!canvas) { if (!canvas) {

View file

@ -12,10 +12,17 @@ export class Tabs extends LitElement {
@property() @property()
currentPage?: string; currentPage?: string;
@property({type: Boolean})
vertical = false;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [GlobalsStyle, TabsStyle, css` return [GlobalsStyle, TabsStyle, css`
::slotted(*) { ::slotted(*) {
height: 100%; height: 100%;
flex-grow: 2;
}
:host([vertical]) {
display: flex;
} }
`]; `];
} }
@ -39,7 +46,7 @@ export class Tabs extends LitElement {
} }
this.currentPage = pages[0].attributes.getNamedItem("slot")?.value; this.currentPage = pages[0].attributes.getNamedItem("slot")?.value;
} }
return html`<div class="pf-c-tabs"> return html`<div class="pf-c-tabs ${this.vertical ? "pf-m-vertical pf-m-box" : ""}">
<ul class="pf-c-tabs__list"> <ul class="pf-c-tabs__list">
${pages.map((page) => this.renderTab(page))} ${pages.map((page) => this.renderTab(page))}
</ul> </ul>

View file

@ -1,6 +1,5 @@
import { gettext } from "django"; import { gettext } from "django";
import { LitElement, html, customElement, TemplateResult, property } from "lit-element"; import { LitElement, html, customElement, TemplateResult, property } from "lit-element";
import { DefaultClient } from "../../api/Client";
import "./Message"; import "./Message";
import { APIMessage } from "./Message"; import { APIMessage } from "./Message";
@ -15,7 +14,6 @@ export function showMessage(message: APIMessage): void {
@customElement("ak-message-container") @customElement("ak-message-container")
export class MessageContainer extends LitElement { export class MessageContainer extends LitElement {
url = DefaultClient.makeUrl(["root", "messages"]);
@property({attribute: false}) @property({attribute: false})
messages: APIMessage[] = []; messages: APIMessage[] = [];
@ -36,10 +34,6 @@ export class MessageContainer extends LitElement {
} }
} }
firstUpdated(): void {
this.fetchMessages();
}
connect(): void { connect(): void {
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${ const wsUrl = `${window.location.protocol.replace("http", "ws")}//${
window.location.host window.location.host
@ -74,21 +68,6 @@ export class MessageContainer extends LitElement {
}); });
} }
/* Fetch messages which were stored in the session.
* This mostly gets messages which were created when the user arrives/leaves the site
* and especially the login flow */
fetchMessages(): Promise<void> {
console.debug("authentik/messages: fetching messages over direct api");
return fetch(this.url)
.then((r) => r.json())
.then((r: APIMessage[]) => {
r.forEach((m: APIMessage) => {
this.messages.push(m);
this.requestUpdate();
});
});
}
render(): TemplateResult { render(): TemplateResult {
return html`<ul class="pf-c-alert-group pf-m-toast"> return html`<ul class="pf-c-alert-group pf-m-toast">
${this.messages.map((m) => { ${this.messages.map((m) => {

View file

@ -119,13 +119,15 @@ export class AuthenticatorValidateStage extends BaseStage implements StageHost {
return html`<ak-stage-authenticator-validate-code return html`<ak-stage-authenticator-validate-code
.host=${this} .host=${this}
.challenge=${this.challenge} .challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}> .deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.device_challenges.length || []) > 1}>
</ak-stage-authenticator-validate-code>`; </ak-stage-authenticator-validate-code>`;
case DeviceClasses.WEBAUTHN: case DeviceClasses.WEBAUTHN:
return html`<ak-stage-authenticator-validate-webauthn return html`<ak-stage-authenticator-validate-webauthn
.host=${this} .host=${this}
.challenge=${this.challenge} .challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}> .deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.device_challenges.length || []) > 1}>
</ak-stage-authenticator-validate-webauthn>`; </ak-stage-authenticator-validate-webauthn>`;
} }
} }

View file

@ -14,6 +14,9 @@ export class AuthenticatorValidateStageWebCode extends BaseStage {
@property({ attribute: false }) @property({ attribute: false })
deviceChallenge?: DeviceChallenge; deviceChallenge?: DeviceChallenge;
@property({ type: Boolean })
showBackButton = false;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return COMMON_STYLES; return COMMON_STYLES;
} }
@ -61,14 +64,16 @@ export class AuthenticatorValidateStageWebCode extends BaseStage {
</div> </div>
<footer class="pf-c-login__main-footer"> <footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"> <ul class="pf-c-login__main-footer-links">
<li class="pf-c-login__main-footer-links-item"> ${this.showBackButton ?
html`<li class="pf-c-login__main-footer-links-item">
<button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => { <button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => {
if (!this.host) return; if (!this.host) return;
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined; (this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
}}> }}>
${gettext("Return to device picker")} ${gettext("Return to device picker")}
</button> </button>
</li> </li>`:
html``}
</ul> </ul>
</footer>`; </footer>`;
} }

View file

@ -21,6 +21,9 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage {
@property() @property()
authenticateMessage = ""; authenticateMessage = "";
@property({type: Boolean})
showBackButton = false;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return COMMON_STYLES; return COMMON_STYLES;
} }
@ -98,14 +101,16 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage {
</div> </div>
<footer class="pf-c-login__main-footer"> <footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"> <ul class="pf-c-login__main-footer-links">
<li class="pf-c-login__main-footer-links-item"> ${this.showBackButton ?
html`<li class="pf-c-login__main-footer-links-item">
<button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => { <button class="pf-c-button pf-m-secondary pf-m-block" @click=${() => {
if (!this.host) return; if (!this.host) return;
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined; (this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
}}> }}>
${gettext("Return to device picker")} ${gettext("Return to device picker")}
</button> </button>
</li> </li>`:
html``}
</ul> </ul>
</footer>`; </footer>`;
} }

View file

@ -1,6 +1,5 @@
import { gettext } from "django"; import { gettext } from "django";
import { CSSResult, customElement, html, LitElement, TemplateResult } from "lit-element"; import { CSSResult, customElement, html, LitElement, TemplateResult } from "lit-element";
import { DefaultClient } from "../../api/Client";
import { COMMON_STYLES } from "../../common/styles"; import { COMMON_STYLES } from "../../common/styles";
import "../../elements/AdminLoginsChart"; import "../../elements/AdminLoginsChart";
@ -31,7 +30,7 @@ export class AdminOverviewPage extends LitElement {
<section class="pf-c-page__main-section"> <section class="pf-c-page__main-section">
<div class="pf-l-gallery pf-m-gutter"> <div class="pf-l-gallery pf-m-gutter">
<ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Logins over the last 24 hours" style="grid-column-end: span 3;grid-row-end: span 2;"> <ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Logins over the last 24 hours" style="grid-column-end: span 3;grid-row-end: span 2;">
<ak-admin-logins-chart url="${DefaultClient.makeUrl(["admin", "metrics"])}"></ak-admin-logins-chart> <ak-admin-logins-chart .url="${["admin", "metrics"]}"></ak-admin-logins-chart>
</ak-aggregate-card> </ak-aggregate-card>
<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-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-top-applications-table></ak-top-applications-table>

View file

@ -1,7 +1,6 @@
import { gettext } from "django"; import { gettext } from "django";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { Application } from "../../api/Applications"; import { Application } from "../../api/Applications";
import { DefaultClient } from "../../api/Client";
import { COMMON_STYLES } from "../../common/styles"; import { COMMON_STYLES } from "../../common/styles";
import "../../elements/Tabs"; import "../../elements/Tabs";
@ -71,7 +70,7 @@ export class ApplicationViewPage extends LitElement {
<div class="pf-c-card__body"> <div class="pf-c-card__body">
${this.application ? html` ${this.application ? html`
<ak-admin-logins-chart <ak-admin-logins-chart
url="${DefaultClient.makeUrl(["core", "applications", this.application?.slug, "metrics"])}"> .url="${["core", "applications", this.application?.slug, "metrics"]}">
</ak-admin-logins-chart>`: ""} </ak-admin-logins-chart>`: ""}
</div> </div>
</div> </div>

View file

@ -1,5 +1,5 @@
--- ---
title: Static Authenticator stage title: Static Authentication Setup stage
--- ---
This stage configures static OTP Tokens, which can be used as a backup method to time-based OTP tokens. This stage configures static OTP Tokens, which can be used as a backup method to time-based OTP tokens.

View file

@ -1,7 +1,7 @@
--- ---
title: TOTP stage title: TOTP Authentication Setup stage
--- ---
This stage configures a time-based OTP Device, such as Google Authenticator or Authy. This stage configures a time-based OTP Device, such as Google Authenticator or Authy.
You can configure how many digest should be used for the OTP Token. You can configure how many digits should be used for the OTP Token.

View file

@ -2,7 +2,16 @@
title: Authenticator Validation Stage title: Authenticator Validation Stage
--- ---
This stage validates an already configured OTP Device. This device has to be configured using any of the other authenticator stages: This stage validates an already configured Authenticator Device. This device has to be configured using any of the other authenticator stages:
- [TOTP authenticator stage](../authenticator_totp/index.md) - [TOTP authenticator stage](../authenticator_totp/index.md)
- [Static authenticator stage](../authenticator_static/index.md). - [Static authenticator stage](../authenticator_static/index.md).
- [WebAuth authenticator stage](../authenticator_webauthn/index.md).
You can select which type of device classes are allowed.
Using the `Not configured action`, you can choose what happens when a user does not have any matching devices.
- Skip: Validation is skipped and the flow continues
- Deny: Access is denied, the flow execution ends
- Configure: This option requires a *Configuration stage* to be set. The validation stage will be marked as successful, and the configuration stage will be injected into the flow.

View file

@ -0,0 +1,7 @@
---
title: WebAuthn Authentication Setup stage
---
This stage configures a WebAuthn-based Authenticator. This can either be a browser, biometrics or a Security stick like a YubiKey.
There are no stage-specific settings.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 115 KiB

View file

@ -0,0 +1,10 @@
---
title: Deny stage
---
This stage stops the execution of a flow. This can be used to conditionally deny users access to a flow,
even if they are not signed in (and permissions can't be checked via groups).
:::caution
To effectively use this stage, make sure to **disable** *Evaluate on plan* on the Stage binding.
:::

View file

@ -0,0 +1,62 @@
---
title: Apache Guacamole™
---
## What is Apache Guacamole™
From https://guacamole.apache.org/
:::note
Apache Guacamole is a clientless remote desktop gateway. It supports standard protocols like VNC, RDP, and SSH.
:::
## Preparation
The following placeholders will be used:
- `guacamole.company` is the FQDN of the Guacamole install.
- `authentik.company` is the FQDN of the authentik install.
Create an OAuth2/OpenID provider with the following parameters:
- Client Type: `Confidential`
- JWT Algorithm: `RS256`
- Redirect URIs: `https://guacamole.company/` (depending on your Tomcat setup, you might have to add `/guacamole/` if the application runs in a subfolder)
- Scopes: OpenID, Email and Profile
Note the Client ID value. Create an application, using the provider you've created above.
## Guacamole
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
<Tabs
defaultValue="docker"
values={[
{label: 'Docker', value: 'docker'},
{label: 'Standalone', value: 'standalone'},
]}>
<TabItem value="docker">
The docker containers are configured via environment variables. The following variables are required:
```yaml
OPENID_AUTHORIZATION_ENDPOINT: https://authentik.company/application/o/authorize/
OPENID_CLIENT_ID: # client ID from above
OPENID_ISSUER: https://authentik.company/application/o/apache-guacamole/
OPENID_JWKS_ENDPOINT: https://authentik.company/application/o/apache-guacamole/jwks/
OPENID_REDIRECT_URI: https://guacamole.company/ # This must match the redirect URI above
```
</TabItem>
<TabItem value="standalone">
Standalone Guacamole is configured using the `guacamole.properties` file. Add the following settings:
```
openid-authorization-endpoint=https://authentik.company/application/o/authorize/
openid-client-id=# client ID from above
openid-issuer=https://authentik.company/application/o/apache-guacamole/
openid-jwks-endpoint=https://authentik.company/application/o/apache-guacamole/jwks/
openid-redirect-uri=https://guacamole.company/ # This must match the redirect URI above
```
</TabItem>
</Tabs>

View file

@ -29,6 +29,16 @@ Note the Client ID and Client Secret values. Create an application, using the pr
## Grafana ## Grafana
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
<Tabs
defaultValue="docker"
values={[
{label: 'Docker', value: 'docker'},
{label: 'Standalone', value: 'standalone'},
]}>
<TabItem value="docker">
If your Grafana is running in docker, set the following environment variables: If your Grafana is running in docker, set the following environment variables:
```yaml ```yaml
@ -45,7 +55,8 @@ environment:
# Optionally enable auto-login # Optionally enable auto-login
GF_AUTH_OAUTH_AUTO_LOGIN: "true" GF_AUTH_OAUTH_AUTO_LOGIN: "true"
``` ```
</TabItem>
<TabItem value="standalone">
If you are using a config-file instead, you have to set these options: If you are using a config-file instead, you have to set these options:
```ini ```ini
@ -64,3 +75,5 @@ auth_url = https://authentik.company/application/o/authorize/
token_url = https://authentik.company/application/o/token/ token_url = https://authentik.company/application/o/token/
api_url = https://authentik.company/application/o/userinfo/ api_url = https://authentik.company/application/o/userinfo/
``` ```
</TabItem>
</Tabs>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

View file

@ -0,0 +1,72 @@
---
title: Wiki.js
---
## What is Wiki.js
From https://en.wikipedia.org/wiki/Wiki.js
:::note
Wiki.js is a wiki engine running on Node.js and written in JavaScript. It is free software released under the Affero GNU General Public License. It is available as a self-hosted solution or using "single-click" install on the DigitalOcean and AWS marketplace.
:::
:::note
This is based on authentik 2021.3 and Wiki.js 2.5. Instructions may differ between versions.
:::
## Preparation
The following placeholders will be used:
- `wiki.company` is the FQDN of Wiki.js.
- `authentik.company` is the FQDN of authentik.
### Step 1
In Wiki.js, navigate to the _Authentication_ section in the _Administration_ interface.
Add a _Generic OpenID Connect / OAuth2_ strategy and note the _Callback URL / Redirect URI_ in the _Configuration Reference_ section at the bottom.
### Step 2
In authentik, under _Providers_, create an _OAuth2/OpenID Provider_ with these settings:
- Client Type: Confidential
- JWT Algorithm: RS256
- Redirect URI: The _Callback URL / Redirect URI_ you noted from the previous step.
- Scopes: Default OAUth mappings for: OpenID, email, profile.
- RSA Key: Choose a certificate.
- Sub Mode: Based on username.
Note the _client ID_ and _client secret_, then save the provider. If you need to retrieve these values, you can do so by editing the provider.
![](./authentik_provider.png)
### Step 3
In Wiki.js, configure the authentication strategy with these settings:
- Client ID: Client ID from the authentik provider.
- Client Secret: Client Secret from the authentik provider.
- Authorization Endpoint URL: https://authentik.company/application/o/authorize/
- Token Endpoint URL: https://authentik.company/application/o/token/
- User Info Endpont URL: https://authentik.company/application/o/userinfo/
- Issuer: https://authentik.company/application/o/wikijs/
- Logout URL: https://authentik.company/application/o/wikijs/end-session/
- Allow self-registration: Enabled
- Assign to group: The group to which new users logging in from authentik should be assigned.
![](./wiki-js_strategy.png)
:::note
You do not have to enable "Allow self-registration" and select a group to which new users should be assigned, but if you don't you will have to manually provision users in Wiki.js and ensure that their usernames match the username they have in authentik.
:::
### Step 5
In authentik, create an application which uses this provider. Optionally apply access restrictions to the application using policy bindings.
Set the Launch URL to the _Callback URL / Redirect URI_ without the `/callback` at the end, as shown below. This will skip Wiki.js' login prompt and log you in directly.
![](./authentik_application.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

View file

@ -12,7 +12,6 @@ Scopes can be configured using Scope Mappings, a type of [Property Mappings](../
| Token | `/application/o/token/` | | Token | `/application/o/token/` |
| User Info | `/application/o/userinfo/` | | User Info | `/application/o/userinfo/` |
| End Session | `/application/o/end-session/` | | End Session | `/application/o/end-session/` |
| Introspect | `/application/o/end-session/` |
| JWKS | `/application/o/<application slug>/jwks/` | | JWKS | `/application/o/<application slug>/jwks/` |
| OpenID Configuration | `/application/o/<application slug>/.well-known/openid-configuration` | | OpenID Configuration | `/application/o/<application slug>/.well-known/openid-configuration` |

View file

@ -49,7 +49,9 @@ module.exports = {
"flow/stages/authenticator_static/index", "flow/stages/authenticator_static/index",
"flow/stages/authenticator_totp/index", "flow/stages/authenticator_totp/index",
"flow/stages/authenticator_validate/index", "flow/stages/authenticator_validate/index",
"flow/stages/authenticator_webauthn/index",
"flow/stages/captcha/index", "flow/stages/captcha/index",
"flow/stages/deny",
"flow/stages/email/index", "flow/stages/email/index",
"flow/stages/identification/index", "flow/stages/identification/index",
"flow/stages/invitation/index", "flow/stages/invitation/index",
@ -106,6 +108,7 @@ module.exports = {
type: "category", type: "category",
label: "as Provider", label: "as Provider",
items: [ items: [
"integrations/services/apache-guacamole/index",
"integrations/services/aws/index", "integrations/services/aws/index",
"integrations/services/awx-tower/index", "integrations/services/awx-tower/index",
"integrations/services/gitlab/index", "integrations/services/gitlab/index",
@ -120,6 +123,7 @@ module.exports = {
"integrations/services/ubuntu-landscape/index", "integrations/services/ubuntu-landscape/index",
"integrations/services/veeam-enterprise-manager/index", "integrations/services/veeam-enterprise-manager/index",
"integrations/services/vmware-vcenter/index", "integrations/services/vmware-vcenter/index",
"integrations/services/wiki-js/index",
], ],
}, },
], ],
@ -141,6 +145,7 @@ module.exports = {
"releases/0.14", "releases/0.14",
"releases/2021.1", "releases/2021.1",
"releases/2021.2", "releases/2021.2",
"releases/2021.3",
], ],
}, },
{ {