Merge branch 'master' into version-0.14

This commit is contained in:
Jens Langhammer 2020-12-28 14:33:28 +01:00
commit 65355372ce
25 changed files with 310 additions and 146 deletions

View file

@ -48,6 +48,15 @@ class EventViewSet(ReadOnlyModelViewSet):
queryset = Event.objects.all()
serializer_class = EventSerializer
ordering = ["-created"]
search_fields = [
"user",
"action",
"app",
"context",
"client_ip",
]
filterset_fields = ["action"]
@swagger_auto_schema(
method="GET", responses={200: EventTopPerUserSerialier(many=True)}

View file

@ -10,7 +10,6 @@ class AuthentikEventsConfig(AppConfig):
name = "authentik.events"
label = "authentik_events"
verbose_name = "authentik Events"
mountpoint = "events/"
def ready(self):
import_module("authentik.events.signals")

View file

@ -1,90 +0,0 @@
{% extends "base/page.html" %}
{% load i18n %}
{% load authentik_utils %}
{% block page_content %}
<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon pf-icon-catalog"></i>
{% trans 'Event Log' %}
</h1>
</div>
</section>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<button role="ak-refresh" class="pf-c-button pf-m-primary">
{% trans 'Refresh' %}
</button>
{% include 'partials/pagination.html' %}
</div>
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Action' %}</th>
<th role="columnheader" scope="col">{% trans 'Context' %}</th>
<th role="columnheader" scope="col">{% trans 'User' %}</th>
<th role="columnheader" scope="col">{% trans 'Creation Date' %}</th>
<th role="columnheader" scope="col">{% trans 'Client IP' %}</th>
</tr>
</thead>
<tbody role="rowgroup">
{% for entry in object_list %}
<tr role="row">
<th role="columnheader">
<div>
<div>{{ entry.action }}</div>
<small>{{ entry.app|default:'-' }}</small>
</div>
</th>
<td role="cell">
<div>
<div>
<code>{{ entry.context }}</code>
</div>
{% if entry.user.on_behalf_of %}
<small>
{% blocktrans with username=entry.user.on_behalf_of.username %}
On behalf of {{ username }}
{% endblocktrans %}
</small>
{% endif %}
</div>
</td>
<td role="cell">
<div>
<div>{{ entry.user.username }}</div>
<small>
{% blocktrans with pk=entry.user.pk %}
ID: {{ pk }}
{% endblocktrans %}
</small>
</div>
</td>
<td role="cell">
<span>
{{ entry.created }}
</span>
</td>
<td role="cell">
<span>
{{ entry.client_ip }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
</div>
</section>
</main>
{% endblock %}

View file

@ -1,9 +0,0 @@
"""authentik events urls"""
from django.urls import path
from authentik.events.views import EventListView
urlpatterns = [
# Event Log
path("log/", EventListView.as_view(), name="log"),
]

View file

@ -1,30 +0,0 @@
"""authentik Event administration"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView
from guardian.mixins import PermissionListMixin
from authentik.admin.views.utils import SearchListMixin, UserPaginateListMixin
from authentik.events.models import Event
class EventListView(
PermissionListMixin,
LoginRequiredMixin,
SearchListMixin,
UserPaginateListMixin,
ListView,
):
"""Show list of all invitations"""
model = Event
template_name = "events/list.html"
permission_required = "authentik_events.view_event"
ordering = "-created"
search_fields = [
"user",
"action",
"app",
"context",
"client_ip",
]

View file

@ -78,6 +78,8 @@ class FlowViewSet(ModelViewSet):
queryset = Flow.objects.all()
serializer_class = FlowSerializer
lookup_field = "slug"
search_fields = ["name", "slug", "designation", "title"]
filterset_fields = ["flow_uuid", "name", "slug", "designation"]
@swagger_auto_schema(responses={200: FlowDiagramSerializer()})
@action(detail=True, methods=["get"])

View file

@ -154,6 +154,7 @@ class DockerController(BaseController):
),
"AUTHENTIK_TOKEN": self.outpost.token.key,
},
"labels": self._get_labels(),
}
},
}

View file

@ -398,7 +398,7 @@ class AuthorizationCode(ExpiringModel, BaseGrantModel):
verbose_name_plural = _("Authorization Codes")
def __str__(self):
return "{0} - {1}".format(self.provider, self.code)
return f"Authorization code for {self.provider} for user {self.user}"
@dataclass
@ -461,7 +461,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
self._id_token = json.dumps(asdict(value))
def __str__(self):
return f"{self.provider} - {self.access_token}"
return f"Refresh Token for {self.provider} for user {self.access_token.user}"
@property
def at_hash(self):

View file

@ -74,11 +74,13 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
# goes to the same pod
"nginx.ingress.kubernetes.io/affinity": "cookie",
"traefik.ingress.kubernetes.io/affinity": "true",
"nginx.ingress.kubernetes.io/proxy-buffers-number": "4",
"nginx.ingress.kubernetes.io/proxy-buffer-size": "16k",
}
annotations.update(
self.controller.outpost.config.kubernetes_ingress_annotations
)
return dict()
return annotations
def get_reference_object(self) -> NetworkingV1beta1Ingress:
"""Get deployment object for outpost"""

View file

@ -7,7 +7,7 @@ from authentik.providers.proxy.models import ProxyProvider
class ProxyProviderForm(forms.ModelForm):
"""Security Gateway Provider form"""
"""Proxy Provider form"""
instance: ProxyProvider

View file

@ -142,6 +142,7 @@ SWAGGER_SETTINGS = {
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "authentik.api.pagination.Pagination",
"PAGE_SIZE": 100,
"DATETIME_FORMAT": "%s",
"DEFAULT_FILTER_BACKENDS": [
"rest_framework_guardian.filters.ObjectPermissionsFilter",
"django_filters.rest_framework.DjangoFilterBackend",

View file

@ -868,6 +868,11 @@ paths:
operationId: events_events_list
description: Event Read-Only Viewset
parameters:
- name: action
in: query
description: ''
required: false
type: string
- name: ordering
in: query
description: Which field to use when ordering the results.
@ -919,6 +924,11 @@ paths:
operationId: events_events_top_per_user
description: Get the top_n events grouped by user count
parameters:
- name: action
in: query
description: ''
required: false
type: string
- name: ordering
in: query
description: Which field to use when ordering the results.
@ -1194,6 +1204,26 @@ paths:
operationId: flows_instances_list
description: Flow Viewset
parameters:
- name: flow_uuid
in: query
description: ''
required: false
type: string
- name: name
in: query
description: ''
required: false
type: string
- name: slug
in: query
description: ''
required: false
type: string
- name: designation
in: query
description: ''
required: false
type: string
- name: ordering
in: query
description: Which field to use when ordering the results.
@ -8133,6 +8163,14 @@ definitions:
type: string
format: uuid
x-nullable: true
verbose_name:
title: Verbose name
type: string
readOnly: true
verbose_name_plural:
title: Verbose name plural
type: string
readOnly: true
__type__:
title: 'type '
type: string
@ -8181,6 +8219,14 @@ definitions:
type: string
format: uuid
x-nullable: true
verbose_name:
title: Verbose name
type: string
readOnly: true
verbose_name_plural:
title: Verbose name plural
type: string
readOnly: true
server_uri:
title: Server URI
type: string
@ -8296,6 +8342,14 @@ definitions:
type: string
format: uuid
x-nullable: true
verbose_name:
title: Verbose name
type: string
readOnly: true
verbose_name_plural:
title: Verbose name plural
type: string
readOnly: true
provider_type:
title: Provider type
type: string

6
web/package-lock.json generated
View file

@ -304,9 +304,9 @@
}
},
"@types/codemirror": {
"version": "0.0.102",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.102.tgz",
"integrity": "sha512-WZZW8VL8BAzzAZWkiYnM5VsYz1qWYieRqHPPtC/BB015QXd3LPXtBlbRYA8lauKgM10qWYeLH8p5LsIn2SLXfA==",
"version": "0.0.103",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.103.tgz",
"integrity": "sha512-dYQTrIcZal0pnYz/ODjpJB+yadKJhGHywylAlHKjE8VSzGiw2A+6S+hD6jfyXw02ToFR9DO52X+O1pvHn31sbg==",
"requires": {
"@types/tern": "*"
}

View file

@ -12,7 +12,7 @@
"@sentry/browser": "^5.29.2",
"@sentry/tracing": "^5.29.2",
"@types/chart.js": "^2.9.29",
"@types/codemirror": "0.0.102",
"@types/codemirror": "0.0.103",
"chart.js": "^2.9.4",
"codemirror": "^5.59.0",
"construct-style-sheets-polyfill": "^2.4.3",

View file

@ -1,6 +1,37 @@
import { DefaultClient } from "./Client";
import { DefaultClient, PBResponse, QueryArguments } from "./Client";
export interface EventUser {
pk: number;
email?: string;
username: string;
on_behalf_of?: EventUser;
}
export interface EventContext {
[key: string]: EventContext | string | number;
}
export class Event {
pk: string;
user: EventUser;
action: string;
app: string;
context: EventContext;
client_ip: string;
created: string;
constructor() {
throw Error();
}
static get(pk: string): Promise<Event> {
return DefaultClient.fetch<Event>(["events", "events", pk]);
}
static list(filter?: QueryArguments): Promise<PBResponse<Event>> {
return DefaultClient.fetch<PBResponse<Event>>(["events", "events"], filter);
}
// events/events/top_per_user/?filter_action=authorize_application
static topForUser(action: string): Promise<TopNEvent[]> {
return DefaultClient.fetch<TopNEvent[]>(["events", "events", "top_per_user"], {

View file

@ -115,6 +115,7 @@ export abstract class Table<T> extends LitElement {
this.apiEndpoint(this.page).then((r) => {
this.data = r;
this.page = r.pagination.current;
this.expandedRows = [];
});
}
@ -144,7 +145,7 @@ export abstract class Table<T> extends LitElement {
<tr role="row">
<td role="cell" colspan="8">
<div class="pf-l-bullseye">
${inner ? inner : html`<ak-empty-state header="none"></ak-empty-state>`}
${inner ? inner : html`<ak-empty-state header="${gettext("No elements found.")}"></ak-empty-state>`}
</div>
</td>
</tr>
@ -178,7 +179,7 @@ export abstract class Table<T> extends LitElement {
</tr>
<tr class="pf-c-table__expandable-row ${this.expandedRows[idx] ? "pf-m-expanded" : ""}" role="row">
<td></td>
${this.renderExpanded(item)}
${this.expandedRows[idx] ? this.renderExpanded(item) : html``}
</tr>
</tbody>`;
});

View file

@ -26,9 +26,9 @@ export class TableSearch extends LitElement {
if (el.value === "") return;
this.onSearch(el?.value);
}}>
<input class="pf-c-form-control" name="search" type="search" placeholder="Search..." value="${ifDefined(this.value)}" @search=${() => {
<input class="pf-c-form-control" name="search" type="search" placeholder="Search..." value="${ifDefined(this.value)}" @search=${(ev: Event) => {
if (!this.onSearch) return;
this.onSearch("");
this.onSearch((ev.target as HTMLInputElement).value);
}}>
<button class="pf-c-button pf-m-control" type="submit">
<i class="fas fa-search" aria-hidden="true"></i>

View file

@ -36,7 +36,7 @@
class="pf-c-page__main"
tabindex="-1"
id="main-content"
defaultUrl="/library/"
defaultUrl="/library"
>
</ak-router-outlet>
</div>

View file

@ -9,7 +9,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Monitor").children(
new SidebarItem("Overview", "/administration/overview"),
new SidebarItem("System Tasks", "/administration/tasks/"),
new SidebarItem("Events", "/events/log/"),
new SidebarItem("Events", "/events"),
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
}),

View file

@ -36,7 +36,7 @@ export abstract class Interface extends LitElement {
<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/">
<ak-router-outlet role="main" class="pf-c-page__main" tabindex="-1" id="main-content" defaultUrl="/library">
</ak-router-outlet>
</main>
</div>`;

View file

@ -0,0 +1,116 @@
import { gettext } from "django";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { until } from "lit-html/directives/until";
import { Event, EventContext } from "../../api/Events";
import { Flow } from "../../api/Flows";
import { COMMON_STYLES } from "../../common/styles";
import "../../elements/Spinner";
import { SpinnerSize } from "../../elements/Spinner";
@customElement("ak-event-info")
export class EventInfo extends LitElement {
@property({attribute: false})
event?: Event;
static get styles(): CSSResult[] {
return COMMON_STYLES.concat(
css`
code {
display: block;
white-space: pre-wrap;
}
`
);
}
getModelInfo(context: EventContext): TemplateResult {
return html`<ul class="pf-c-list">
<li>${gettext("UID")}: ${context.pk as string}</li>
<li>${gettext("Name")}: ${context.name as string}</li>
<li>${gettext("App")}: ${context.app as string}</li>
<li>${gettext("Model Name")}: ${context.model_name as string}</li>
</ul>`;
}
defaultResponse(): TemplateResult {
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${gettext("Context")}</h3>
<code>${JSON.stringify(this.event?.context)}</code>
</div>
<div class="pf-l-flex__item">
<h3>${gettext("User")}</h3>
<code>${JSON.stringify(this.event?.user)}</code>
</div>
</div>`;
}
render(): TemplateResult {
if (!this.event) {
return html`<ak-spinner size=${SpinnerSize.Medium}></ak-spinner>`;
}
switch (this.event?.action) {
case "model_created":
case "model_updated":
case "model_deleted":
return html`
<h3>${gettext("Affected model:")}</h3><hr>
${this.getModelInfo(this.event.context.model as EventContext)}
`;
case "authorize_application":
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${gettext("Authorized application:")}</h3><hr>
${this.getModelInfo(this.event.context.authorized_application as EventContext)}
</div>
<div class="pf-l-flex__item">
<h3>${gettext("Using flow")}</h3>
<span>${until(Flow.list({
flow_uuid: this.event.context.flow as string,
}).then(resp => {
return html`<a href="#/flows/${resp.results[0].slug}">${resp.results[0].name}</a>`;
}), html`<ak-spinner size=${SpinnerSize.Medium}></ak-spinner>`)}</span>
</div>
</div>`;
case "login_failed":
return html`
<h3>${gettext(`Attempted to log in as ${this.event.context.username}`)}</h3>
`;
case "token_view":
return html`
<h3>${gettext("Token:")}</h3><hr>
${this.getModelInfo(this.event.context.token as EventContext)}
`;
case "property_mapping_exception":
case "policy_exception":
return html`<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${gettext("Exception")}</h3>
<code>${this.event.context.error}</code>
</div>
<div class="pf-l-flex__item">
<h3>${gettext("Expression")}</h3>
<code>${this.event.context.expression}</code>
</div>
</div>`;
case "configuration_error":
return html`<h3>${this.event.context.message}</h3>`;
case "update_available":
return html`<h3>${gettext("New version available!")}</h3>
<a target="_blank" href="https://github.com/BeryJu/authentik/releases/tag/version%2F${this.event.context.new_version}">${this.event.context.new_version}</a>
`;
// Action types which typically don't record any extra context.
// If context is not empty, we fall to the default response.
case "login":
case "logout":
if (this.event.context === {}) {
return html`<span>${gettext("No additional data available.")}</span>`;
}
return this.defaultResponse();
default:
return this.defaultResponse();
}
}
}

View file

@ -0,0 +1,71 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { PBResponse } from "../../api/Client";
import { Event } from "../../api/Events";
import { TableColumn } from "../../elements/table/Table";
import { TablePage } from "../../elements/table/TablePage";
import { time } from "../../utils";
import "./EventInfo";
@customElement("ak-event-list")
export class EventListPage extends TablePage<Event> {
expandable = true;
pageTitle(): string {
return "Event Log";
}
pageDescription(): string | undefined {
return;
}
pageIcon(): string {
return "pf-icon pf-icon-catalog";
}
searchEnabled(): boolean {
return true;
}
@property()
order = "-created";
apiEndpoint(page: number): Promise<PBResponse<Event>> {
return Event.list({
ordering: this.order,
page: page,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn("Action", "action"),
new TableColumn("User", "user"),
new TableColumn("Creation Date", "created"),
new TableColumn("Client IP", "client_ip"),
];
}
row(item: Event): TemplateResult[] {
return [
html`<div>${item.action}</div>
<small>${item.app}</small>`,
html`<div>${item.user.username}</div>
${item.user.on_behalf_of ? html`<small>
${gettext(`On behalf of ${item.user.on_behalf_of.username}`)}
</small>` : html``}`,
html`<span>${time(item.created).toLocaleString()}</span>`,
html`<span>${item.client_ip}</span>`,
];
}
renderExpanded(item: Event): TemplateResult {
return html`
<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<ak-event-info .event=${item}></ak-event-info>
</div>
</td>
<td></td>
<td></td>
<td></td>`;
}
}

View file

@ -7,6 +7,7 @@ import "./pages/applications/ApplicationListPage";
import "./pages/applications/ApplicationViewPage";
import "./pages/sources/SourceViewPage";
import "./pages/flows/FlowViewPage";
import "./pages/events/EventListPage";
export const ROUTES: Route[] = [
// Prevent infinite Shell loops
@ -24,4 +25,5 @@ export const ROUTES: Route[] = [
new Route(new RegExp(`^/flows/(?<slug>${SLUG_REGEX})$`)).then((args) => {
return html`<ak-flow-view .args=${args}></ak-flow-view>`;
}),
new Route(new RegExp("^/events$"), html`<ak-event-list></ak-event-list>`),
];

View file

@ -50,3 +50,7 @@ export function loading<T>(v: T, actual: TemplateResult): TemplateResult {
}
return actual;
}
export function time(t: string): Date {
return new Date(parseInt(t, 10) * 1000);
}