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() queryset = Event.objects.all()
serializer_class = EventSerializer serializer_class = EventSerializer
ordering = ["-created"]
search_fields = [
"user",
"action",
"app",
"context",
"client_ip",
]
filterset_fields = ["action"]
@swagger_auto_schema( @swagger_auto_schema(
method="GET", responses={200: EventTopPerUserSerialier(many=True)} method="GET", responses={200: EventTopPerUserSerialier(many=True)}

View File

@ -10,7 +10,6 @@ class AuthentikEventsConfig(AppConfig):
name = "authentik.events" name = "authentik.events"
label = "authentik_events" label = "authentik_events"
verbose_name = "authentik Events" verbose_name = "authentik Events"
mountpoint = "events/"
def ready(self): def ready(self):
import_module("authentik.events.signals") 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() queryset = Flow.objects.all()
serializer_class = FlowSerializer serializer_class = FlowSerializer
lookup_field = "slug" lookup_field = "slug"
search_fields = ["name", "slug", "designation", "title"]
filterset_fields = ["flow_uuid", "name", "slug", "designation"]
@swagger_auto_schema(responses={200: FlowDiagramSerializer()}) @swagger_auto_schema(responses={200: FlowDiagramSerializer()})
@action(detail=True, methods=["get"]) @action(detail=True, methods=["get"])

View File

@ -154,6 +154,7 @@ class DockerController(BaseController):
), ),
"AUTHENTIK_TOKEN": self.outpost.token.key, "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") verbose_name_plural = _("Authorization Codes")
def __str__(self): def __str__(self):
return "{0} - {1}".format(self.provider, self.code) return f"Authorization code for {self.provider} for user {self.user}"
@dataclass @dataclass
@ -461,7 +461,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
self._id_token = json.dumps(asdict(value)) self._id_token = json.dumps(asdict(value))
def __str__(self): def __str__(self):
return f"{self.provider} - {self.access_token}" return f"Refresh Token for {self.provider} for user {self.access_token.user}"
@property @property
def at_hash(self): def at_hash(self):

View File

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

View File

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

View File

@ -142,6 +142,7 @@ 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

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

6
web/package-lock.json generated
View File

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

View File

@ -12,7 +12,7 @@
"@sentry/browser": "^5.29.2", "@sentry/browser": "^5.29.2",
"@sentry/tracing": "^5.29.2", "@sentry/tracing": "^5.29.2",
"@types/chart.js": "^2.9.29", "@types/chart.js": "^2.9.29",
"@types/codemirror": "0.0.102", "@types/codemirror": "0.0.103",
"chart.js": "^2.9.4", "chart.js": "^2.9.4",
"codemirror": "^5.59.0", "codemirror": "^5.59.0",
"construct-style-sheets-polyfill": "^2.4.3", "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 { 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 // events/events/top_per_user/?filter_action=authorize_application
static topForUser(action: string): Promise<TopNEvent[]> { static topForUser(action: string): Promise<TopNEvent[]> {
return DefaultClient.fetch<TopNEvent[]>(["events", "events", "top_per_user"], { 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.apiEndpoint(this.page).then((r) => {
this.data = r; this.data = r;
this.page = r.pagination.current; this.page = r.pagination.current;
this.expandedRows = [];
}); });
} }
@ -144,7 +145,7 @@ export abstract class Table<T> extends LitElement {
<tr role="row"> <tr role="row">
<td role="cell" colspan="8"> <td role="cell" colspan="8">
<div class="pf-l-bullseye"> <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> </div>
</td> </td>
</tr> </tr>
@ -178,7 +179,7 @@ export abstract class Table<T> extends LitElement {
</tr> </tr>
<tr class="pf-c-table__expandable-row ${this.expandedRows[idx] ? "pf-m-expanded" : ""}" role="row"> <tr class="pf-c-table__expandable-row ${this.expandedRows[idx] ? "pf-m-expanded" : ""}" role="row">
<td></td> <td></td>
${this.renderExpanded(item)} ${this.expandedRows[idx] ? this.renderExpanded(item) : html``}
</tr> </tr>
</tbody>`; </tbody>`;
}); });

View File

@ -26,9 +26,9 @@ export class TableSearch extends LitElement {
if (el.value === "") return; if (el.value === "") return;
this.onSearch(el?.value); 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; if (!this.onSearch) return;
this.onSearch(""); this.onSearch((ev.target as HTMLInputElement).value);
}}> }}>
<button class="pf-c-button pf-m-control" type="submit"> <button class="pf-c-button pf-m-control" type="submit">
<i class="fas fa-search" aria-hidden="true"></i> <i class="fas fa-search" aria-hidden="true"></i>

View File

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

View File

@ -9,7 +9,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("Monitor").children( new SidebarItem("Monitor").children(
new SidebarItem("Overview", "/administration/overview"), new SidebarItem("Overview", "/administration/overview"),
new SidebarItem("System Tasks", "/administration/tasks/"), new SidebarItem("System Tasks", "/administration/tasks/"),
new SidebarItem("Events", "/events/log/"), new SidebarItem("Events", "/events"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser); 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 class="pf-c-page__sidebar ${this.sidebarOpen ? "pf-m-expanded" : "pf-m-collapsed"}" .items=${this.sidebar}>
</ak-sidebar> </ak-sidebar>
<main class="pf-c-page__main"> <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> </ak-router-outlet>
</main> </main>
</div>`; </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/applications/ApplicationViewPage";
import "./pages/sources/SourceViewPage"; import "./pages/sources/SourceViewPage";
import "./pages/flows/FlowViewPage"; import "./pages/flows/FlowViewPage";
import "./pages/events/EventListPage";
export const ROUTES: Route[] = [ export const ROUTES: Route[] = [
// Prevent infinite Shell loops // Prevent infinite Shell loops
@ -24,4 +25,5 @@ export const ROUTES: Route[] = [
new Route(new RegExp(`^/flows/(?<slug>${SLUG_REGEX})$`)).then((args) => { new Route(new RegExp(`^/flows/(?<slug>${SLUG_REGEX})$`)).then((args) => {
return html`<ak-flow-view .args=${args}></ak-flow-view>`; 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; return actual;
} }
export function time(t: string): Date {
return new Date(parseInt(t, 10) * 1000);
}