web: migrate Token List to web

This commit is contained in:
Jens Langhammer 2021-02-19 18:59:24 +01:00
parent fd28f37c0d
commit 6597d5bd28
17 changed files with 285 additions and 194 deletions

View File

@ -1,102 +0,0 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load authentik_utils %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon pf-icon-security"></i>
{% trans 'Tokens' %}
</h1>
<p>{% trans "Tokens are used throughout authentik for Email validation stages, Recovery keys and API access." %}</p>
</div>
</section>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
{% 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 'Identifier' %}</th>
<th role="columnheader" scope="col">{% trans 'User' %}</th>
<th role="columnheader" scope="col">{% trans 'Expires?' %}</th>
<th role="columnheader" scope="col">{% trans 'Expiry Date' %}</th>
<th role="cell"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for token in object_list %}
<tr role="row">
<th role="columnheader">
<div>{{ token.identifier }}</div>
</th>
<td role="cell">
<span>
{{ token.user }}
</span>
</td>
<td role="cell">
<span>
{{ token.expiring|yesno:"Yes,No" }}
</span>
</td>
<td role="cell">
<span>
{% if not token.expiring %}
-
{% else %}
{{ token.expires }}
{% endif %}
</span>
</td>
<td>
<ak-modal-button href="{% url 'authentik_admin:token-delete' pk=token.pk %}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
{% trans 'Delete' %}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-token-copy-button identifier="{{ token.identifier }}">
{% trans 'Copy token' %}
</ak-token-copy-button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-key pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Tokens.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any token." %}
{% else %}
{% trans 'Currently no tokens exist.' %}
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
</section>
{% endblock %}

View File

@ -53,7 +53,6 @@ urlpatterns = [
name="application-delete", name="application-delete",
), ),
# Tokens # Tokens
path("tokens/", tokens.TokenListView.as_view(), name="tokens"),
path( path(
"tokens/<uuid:pk>/delete/", "tokens/<uuid:pk>/delete/",
tokens.TokenDeleteView.as_view(), tokens.TokenDeleteView.as_view(),

View File

@ -1,39 +1,12 @@
"""authentik Token administration""" """authentik Token administration"""
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import ListView from guardian.mixins import PermissionRequiredMixin
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from authentik.admin.views.utils import ( from authentik.admin.views.utils import DeleteMessageView
DeleteMessageView,
SearchListMixin,
UserPaginateListMixin,
)
from authentik.core.models import Token from authentik.core.models import Token
class TokenListView(
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
ListView,
):
"""Show list of all tokens"""
model = Token
permission_required = "authentik_core.view_token"
ordering = "expires"
template_name = "administration/token/list.html"
search_fields = [
"identifier",
"intent",
"user__username",
"description",
]
class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView): class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete token""" """Delete token"""
@ -41,5 +14,5 @@ class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessage
permission_required = "authentik_core.delete_token" permission_required = "authentik_core.delete_token"
template_name = "generic/delete.html" template_name = "generic/delete.html"
success_url = reverse_lazy("authentik_admin:tokens") success_url = "/"
success_message = _("Successfully deleted Token") success_message = _("Successfully deleted Token")

View File

@ -7,19 +7,14 @@ from django.contrib.auth.mixins import (
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.http.response import HttpResponseRedirect from django.http.response import HttpResponseRedirect
from django.shortcuts import redirect from django.shortcuts import redirect, reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import DetailView, UpdateView from django.views.generic import DetailView, UpdateView
from guardian.mixins import ( from guardian.mixins import PermissionRequiredMixin
PermissionRequiredMixin,
)
from authentik.admin.forms.users import UserForm from authentik.admin.forms.users import UserForm
from authentik.admin.views.utils import ( from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView
BackSuccessUrlMixin,
DeleteMessageView,
)
from authentik.core.models import Token, User from authentik.core.models import Token, User
from authentik.lib.views import CreateAssignPermView from authentik.lib.views import CreateAssignPermView

View File

@ -19,3 +19,5 @@ class GroupViewSet(ModelViewSet):
queryset = Group.objects.all() queryset = Group.objects.all()
serializer_class = GroupSerializer serializer_class = GroupSerializer
search_fields = ["name", "is_superuser"]
filterset_fields = ["name", "is_superuser"]

View File

@ -9,6 +9,7 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, Serializer from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.users import UserSerializer
from authentik.core.models import Token from authentik.core.models import Token
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
@ -16,10 +17,21 @@ from authentik.events.models import Event, EventAction
class TokenSerializer(ModelSerializer): class TokenSerializer(ModelSerializer):
"""Token Serializer""" """Token Serializer"""
user = UserSerializer()
class Meta: class Meta:
model = Token model = Token
fields = ["pk", "identifier", "intent", "user", "description"] fields = [
"pk",
"identifier",
"intent",
"user",
"description",
"expires",
"expiring",
]
depth = 2
class TokenViewSerializer(Serializer): class TokenViewSerializer(Serializer):
@ -40,6 +52,19 @@ class TokenViewSet(ModelViewSet):
lookup_field = "identifier" lookup_field = "identifier"
queryset = Token.filter_not_expired() queryset = Token.filter_not_expired()
serializer_class = TokenSerializer serializer_class = TokenSerializer
search_fields = [
"identifier",
"intent",
"user__username",
"description",
]
filterset_fields = [
"identifier",
"intent",
"user__username",
"description",
]
ordering = ["expires"]
@swagger_auto_schema(responses={200: TokenViewSerializer(many=False)}) @swagger_auto_schema(responses={200: TokenViewSerializer(many=False)})
@action(detail=True) @action(detail=True)

View File

@ -37,6 +37,7 @@ class UserSerializer(ModelSerializer):
"is_superuser", "is_superuser",
"email", "email",
"avatar", "avatar",
"attributes",
] ]
@ -45,6 +46,8 @@ class UserViewSet(ModelViewSet):
queryset = User.objects.none() queryset = User.objects.none()
serializer_class = UserSerializer serializer_class = UserSerializer
search_fields = ["username", "name", "is_active"]
filterset_fields = ["username", "name", "is_active"]
def get_queryset(self): def get_queryset(self):
return User.objects.all().exclude(pk=get_anonymous_user().pk) return User.objects.all().exclude(pk=get_anonymous_user().pk)

View File

@ -186,6 +186,8 @@ class StageViewSet(ReadOnlyModelViewSet):
queryset = Stage.objects.all() queryset = Stage.objects.all()
serializer_class = StageSerializer serializer_class = StageSerializer
search_fields = ["name"]
filterset_fields = ["name"]
def get_queryset(self): def get_queryset(self):
return Stage.objects.select_subclasses() return Stage.objects.select_subclasses()

View File

@ -61,6 +61,8 @@ class ServiceConnectionViewSet(ModelViewSet):
queryset = OutpostServiceConnection.objects.select_subclasses() queryset = OutpostServiceConnection.objects.select_subclasses()
serializer_class = ServiceConnectionSerializer serializer_class = ServiceConnectionSerializer
search_fields = ["name"]
filterset_fields = ["name"]
@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)}) @swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False) @action(detail=False)

View File

@ -107,6 +107,7 @@ class PolicyViewSet(ReadOnlyModelViewSet):
"bindings": ["isnull"], "bindings": ["isnull"],
"promptstage": ["isnull"], "promptstage": ["isnull"],
} }
search_fields = ["name"]
def get_queryset(self): def get_queryset(self):
return Policy.objects.select_subclasses().prefetch_related( return Policy.objects.select_subclasses().prefetch_related(

View File

@ -309,6 +309,16 @@ paths:
operationId: core_groups_list operationId: core_groups_list
description: Group Viewset description: Group Viewset
parameters: parameters:
- name: name
in: query
description: ''
required: false
type: string
- name: is_superuser
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.
@ -436,6 +446,26 @@ paths:
operationId: core_tokens_list operationId: core_tokens_list
description: Token Viewset description: Token Viewset
parameters: parameters:
- name: identifier
in: query
description: ''
required: false
type: string
- name: intent
in: query
description: ''
required: false
type: string
- name: user__username
in: query
description: ''
required: false
type: string
- name: description
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.
@ -582,6 +612,21 @@ paths:
operationId: core_users_list operationId: core_users_list
description: User Viewset description: User Viewset
parameters: parameters:
- name: username
in: query
description: ''
required: false
type: string
- name: name
in: query
description: ''
required: false
type: string
- name: is_active
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.
@ -649,6 +694,21 @@ paths:
operationId: core_users_me operationId: core_users_me
description: Get information about current user description: Get information about current user
parameters: parameters:
- name: username
in: query
description: ''
required: false
type: string
- name: name
in: query
description: ''
required: false
type: string
- name: is_active
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.
@ -2107,6 +2167,11 @@ paths:
operationId: outposts_service_connections_all_list operationId: outposts_service_connections_all_list
description: ServiceConnection Viewset description: ServiceConnection Viewset
parameters: parameters:
- name: name
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.
@ -2174,6 +2239,11 @@ paths:
operationId: outposts_service_connections_all_types operationId: outposts_service_connections_all_types
description: Get all creatable service connection types description: Get all creatable service connection types
parameters: parameters:
- name: name
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.
@ -5506,6 +5576,11 @@ paths:
operationId: stages_all_list operationId: stages_all_list
description: Stage Viewset description: Stage Viewset
parameters: parameters:
- name: name
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.
@ -5557,6 +5632,11 @@ paths:
operationId: stages_all_types operationId: stages_all_types
description: Get all creatable stage types description: Get all creatable stage types
parameters: parameters:
- name: name
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.
@ -8091,48 +8171,8 @@ definitions:
attributes: attributes:
title: Attributes title: Attributes
type: object type: object
Token:
description: Token Serializer
required:
- identifier
- user
type: object
properties:
pk:
title: Token uuid
type: string
format: uuid
readOnly: true
identifier:
title: Identifier
type: string
format: slug
pattern: ^[-a-zA-Z0-9_]+$
maxLength: 255
minLength: 1
intent:
title: Intent
type: string
enum:
- verification
- api
- recovery
user:
title: User
type: integer
description:
title: Description
type: string
TokenView:
description: Show token's current key
type: object
properties:
key:
title: Key
type: string
readOnly: true
minLength: 1
User: User:
title: User
description: User Serializer description: User Serializer
required: required:
- username - username
@ -8179,6 +8219,56 @@ definitions:
title: Avatar title: Avatar
type: string type: string
readOnly: true readOnly: true
attributes:
title: Attributes
type: object
Token:
description: Token Serializer
required:
- identifier
- user
type: object
properties:
pk:
title: Token uuid
type: string
format: uuid
readOnly: true
identifier:
title: Identifier
type: string
format: slug
pattern: ^[-a-zA-Z0-9_]+$
maxLength: 255
minLength: 1
intent:
title: Intent
type: string
enum:
- verification
- api
- recovery
user:
$ref: '#/definitions/User'
description:
title: Description
type: string
expires:
title: Expires
type: string
format: date-time
expiring:
title: Expiring
type: boolean
TokenView:
description: Show token's current key
type: object
properties:
key:
title: Key
type: string
readOnly: true
minLength: 1
CertificateKeyPair: CertificateKeyPair:
description: CertificateKeyPair Serializer description: CertificateKeyPair Serializer
required: required:

View File

@ -1,11 +1,43 @@
import { DefaultClient } from "./Client"; import { AKResponse, DefaultClient, QueryArguments } from "./Client";
import { User } from "./Users";
interface TokenResponse { export enum TokenIntent {
key: string; INTENT_VERIFICATION = "verification",
INTENT_API = "api",
INTENT_RECOVERY = "recovery",
} }
export function tokenByIdentifier(identifier: string): Promise<string> { export class Token {
return DefaultClient.fetch<TokenResponse>(["core", "tokens", identifier, "view_key"]).then(
pk: string;
identifier: string;
intent: TokenIntent;
user: User;
description: string;
expires: number;
expiring: boolean;
constructor() {
throw Error();
}
static get(pk: string): Promise<User> {
return DefaultClient.fetch<User>(["core", "tokens", pk]);
}
static list(filter?: QueryArguments): Promise<AKResponse<Token>> {
return DefaultClient.fetch<AKResponse<Token>>(["core", "tokens"], filter);
}
static adminUrl(rest: string): string {
return `/administration/tokens/${rest}`;
}
static getKey(identifier: string): Promise<string> {
return DefaultClient.fetch<{ key: string }>(["core", "tokens", identifier, "view_key"]).then(
(r) => r.key (r) => r.key
); );
} }
}

View File

@ -3,7 +3,7 @@ import { css, CSSResult, customElement, html, LitElement, property, TemplateResu
import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css"; import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css";
// @ts-ignore // @ts-ignore
import ButtonStyle from "@patternfly/patternfly/components/Button/button.css"; import ButtonStyle from "@patternfly/patternfly/components/Button/button.css";
import { tokenByIdentifier } from "../../api/Tokens"; import { Token } from "../../api/Tokens";
import { ColorStyles, ERROR_CLASS, PRIMARY_CLASS, SUCCESS_CLASS } from "../../constants"; import { ColorStyles, ERROR_CLASS, PRIMARY_CLASS, SUCCESS_CLASS } from "../../constants";
@customElement("ak-token-copy-button") @customElement("ak-token-copy-button")
@ -35,7 +35,7 @@ export class TokenCopyButton extends LitElement {
}, 1500); }, 1500);
return; return;
} }
tokenByIdentifier(this.identifier).then((token) => { Token.getKey(this.identifier).then((token) => {
navigator.clipboard.writeText(token).then(() => { navigator.clipboard.writeText(token).then(() => {
this.buttonClass = SUCCESS_CLASS; this.buttonClass = SUCCESS_CLASS;
setTimeout(() => { setTimeout(() => {

View File

@ -43,7 +43,7 @@ export class TablePagination extends LitElement {
<button <button
class="pf-c-button pf-m-plain" class="pf-c-button pf-m-plain"
@click=${() => { this.pageChangeHandler(this.pages?.next || 0); }} @click=${() => { this.pageChangeHandler(this.pages?.next || 0); }}
?disabled="${(this.pages?.next || 0) < 0}" ?disabled="${(this.pages?.next || 0) <= 0}"
aria-label="${gettext("Go to next page")}" aria-label="${gettext("Go to next page")}"
> >
<i class="fas fa-angle-right" aria-hidden="true"></i> <i class="fas fa-angle-right" aria-hidden="true"></i>

View File

@ -50,7 +50,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
new SidebarItem("User", "/users"), new SidebarItem("User", "/users"),
new SidebarItem("Groups", "/groups"), new SidebarItem("Groups", "/groups"),
new SidebarItem("Certificates", "/crypto/certificates"), new SidebarItem("Certificates", "/crypto/certificates"),
new SidebarItem("Tokens", "/administration/tokens/"), new SidebarItem("Tokens", "/tokens"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser); return User.me().then(u => u.is_superuser);
}), }),

View File

@ -0,0 +1,67 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { AKResponse } from "../../api/Client";
import { TablePage } from "../../elements/table/TablePage";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/Dropdown";
import { TableColumn } from "../../elements/table/Table";
import { Token } from "../../api/Tokens";
@customElement("ak-token-list")
export class TokenListPage extends TablePage<Token> {
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return gettext("Tokens");
}
pageDescription(): string {
return gettext("Tokens are used throughout authentik for Email validation stages, Recovery keys and API access.");
}
pageIcon(): string {
return gettext("pf-icon pf-icon-security");
}
@property()
order = "expires";
apiEndpoint(page: number): Promise<AKResponse<Token>> {
return Token.list({
ordering: this.order,
page: page,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn("Identifier", "identifier"),
new TableColumn("User", "user"),
new TableColumn("Expires?", "expiring"),
new TableColumn("Expiry date", "expires"),
new TableColumn(""),
];
}
row(item: Token): TemplateResult[] {
return [
html`${item.identifier}`,
html`${item.user.username}`,
html`${item.expiring ? "Yes" : "No"}`,
html`${item.expiring ? new Date(item.expires * 1000).toLocaleString() : '-'}`,
html`
<ak-modal-button href="${Token.adminUrl(`${item.pk}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
${gettext('Delete')}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-token-copy-button identifier="${item.identifier}">
${gettext('Copy Key')}
</ak-token-copy-button>
`,
];
}
}

View File

@ -22,6 +22,7 @@ import "./pages/sources/SourcesListPage";
import "./pages/sources/SourceViewPage"; import "./pages/sources/SourceViewPage";
import "./pages/groups/GroupListPage"; import "./pages/groups/GroupListPage";
import "./pages/users/UserListPage"; import "./pages/users/UserListPage";
import "./pages/tokens/TokenListPage";
import "./pages/system-tasks/SystemTaskListPage"; import "./pages/system-tasks/SystemTaskListPage";
export const ROUTES: Route[] = [ export const ROUTES: Route[] = [
@ -47,6 +48,7 @@ export const ROUTES: Route[] = [
new Route(new RegExp("^/groups$"), html`<ak-group-list></ak-group-list>`), new Route(new RegExp("^/groups$"), html`<ak-group-list></ak-group-list>`),
new Route(new RegExp("^/users$"), html`<ak-user-list></ak-user-list>`), new Route(new RegExp("^/users$"), html`<ak-user-list></ak-user-list>`),
new Route(new RegExp("^/flows$"), html`<ak-flow-list></ak-flow-list>`), new Route(new RegExp("^/flows$"), html`<ak-flow-list></ak-flow-list>`),
new Route(new RegExp("^/tokens$"), html`<ak-token-list></ak-token-list>`),
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 .flowSlug=${args.slug}></ak-flow-view>`; return html`<ak-flow-view .flowSlug=${args.slug}></ak-flow-view>`;
}), }),