core: add ability for users to create tokens

This commit is contained in:
Jens Langhammer 2020-10-18 15:38:28 +02:00
parent 6a53069653
commit c698ba37d9
7 changed files with 250 additions and 2 deletions

View file

@ -7,6 +7,7 @@ This update brings these headline features:
- Add System Task Overview to see all background tasks, their status, the log output, and retry them
- Alerts now disappear automatically
- Audit Logs are now searchable
- Users can now create their own Tokens to access the API
Fixes:

View file

@ -0,0 +1,22 @@
"""Core user token form"""
from django import forms
from passbook.core.models import Token
class UserTokenForm(forms.ModelForm):
"""Token form, for tokens created by endusers"""
class Meta:
model = Token
fields = [
"identifier",
"expires",
"expiring",
"description",
]
widgets = {
"identifier": forms.TextInput(),
"description": forms.TextInput(),
}

View file

@ -16,6 +16,10 @@
<a href="{% url 'passbook_core:user-settings' %}"
class="pf-c-nav__link {% is_active 'passbook_core:user-settings' %}">{% trans 'User Details' %}</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_core:user-tokens' %}"
class="pf-c-nav__link {% is_active 'passbook_core:user-tokens' 'passbook_core:user-tokens-create' 'passbook_core:user-tokens-update' 'passbook_core:user-tokens-delete' %}">{% trans 'Tokens' %}</a>
</li>
</ul>
</section>
{% user_stages as user_stages_loc %}
@ -53,6 +57,7 @@
</div>
</div>
<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
{% block content %}
<section class="pf-c-page__main-section">
<div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75">
@ -61,5 +66,6 @@
</div>
</div>
</section>
{% endblock %}
</main>
{% endblock %}

View file

@ -0,0 +1,91 @@
{% extends "user/base.html" %}
{% load i18n %}
{% load passbook_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-users"></i>
{% trans 'Tokens' %}
</h1>
<p>{% trans "Tokens can be used to access passbook's API." %}
</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' %}
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_core:user-tokens-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% 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 'Expires?' %}</th>
<th role="columnheader" scope="col">{% trans 'Expiry Date' %}</th>
<th role="columnheader" scope="col">{% trans 'Description' %}</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.expiring|yesno:"Yes,No" }}
</span>
</td>
<td role="cell">
<span>
{% if not token.expiring %}
-
{% else %}
{{ token.expires }}
{% endif %}
</span>
</td>
<td role="cell">
<span>
{{ token.description }}
</span>
</td>
<td>
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_core:user-tokens-update' identifier=token.identifier %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_core:user-tokens-delete' identifier=token.identifier %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-cubes 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">
{% trans 'Currently no tokens exist. Click the button below to create one.' %}
</div>
<a href="{% url 'passbook_core:user-tokens-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
</div>
{% endif %}
</div>
</section>
{% endblock %}

View file

@ -6,6 +6,22 @@ from passbook.core.views import impersonate, overview, user
urlpatterns = [
# User views
path("-/user/", user.UserSettingsView.as_view(), name="user-settings"),
path("-/user/tokens/", user.TokenListView.as_view(), name="user-tokens"),
path(
"-/user/tokens/create/",
user.TokenCreateView.as_view(),
name="user-tokens-create",
),
path(
"-/user/tokens/<slug:identifier>/update/",
user.TokenUpdateView.as_view(),
name="user-tokens-update",
),
path(
"-/user/tokens/<slug:identifier>/delete/",
user.TokenDeleteView.as_view(),
name="user-tokens-delete",
),
# Overview
path("", overview.OverviewView.as_view(), name="overview"),
# Impersonation

View file

@ -2,13 +2,28 @@
from typing import Any, Dict
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.db.models.query import QuerySet
from django.http.response import HttpResponse
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import UpdateView
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from guardian.shortcuts import get_objects_for_user
from passbook.admin.views.utils import (
DeleteMessageView,
SearchListMixin,
UserPaginateListMixin,
)
from passbook.core.forms.token import UserTokenForm
from passbook.core.forms.users import UserDetailForm
from passbook.core.models import Token, TokenIntents
from passbook.flows.models import Flow, FlowDesignation
from passbook.lib.views import CreateAssignPermView
class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
@ -30,3 +45,93 @@ class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
)
kwargs["unenrollment_enabled"] = bool(unenrollment_flow)
return kwargs
class TokenListView(
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
ListView,
):
"""Show list of all tokens"""
model = Token
ordering = "expires"
permission_required = "passbook_core.view_token"
template_name = "user/token_list.html"
search_fields = [
"identifier",
"intent",
"description",
]
def get_queryset(self) -> QuerySet:
return super().get_queryset().filter(intent=TokenIntents.INTENT_API)
class TokenCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
"""Create new Token"""
model = Token
form_class = UserTokenForm
permission_required = "passbook_core.add_token"
template_name = "generic/create.html"
success_url = reverse_lazy("passbook_core:user-tokens")
success_message = _("Successfully created Token")
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
kwargs["container_template"] = "user/base.html"
return kwargs
def form_valid(self, form: UserTokenForm) -> HttpResponse:
form.instance.user = self.request.user
form.instance.intent = TokenIntents.INTENT_API
return super().form_valid(form)
class TokenUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
"""Update token"""
model = Token
form_class = UserTokenForm
permission_required = "passbook_core.update_token"
template_name = "generic/update.html"
success_url = reverse_lazy("passbook_core:user-tokens")
success_message = _("Successfully updated Token")
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
kwargs["container_template"] = "user/base.html"
return kwargs
def get_object(self) -> Token:
identifier = self.kwargs.get("identifier")
return get_objects_for_user(
self.request.user, "passbook_core.update_token", self.model
).filter(intent=TokenIntents.INTENT_API, identifier=identifier)
class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete token"""
model = Token
permission_required = "passbook_core.delete_token"
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_core:user-tokens")
success_message = _("Successfully deleted Token")
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
kwargs["container_template"] = "user/base.html"
return kwargs

View file

@ -529,6 +529,8 @@ paths:
in: path
required: true
type: string
format: slug
pattern: ^[-a-zA-Z0-9_]+$
/core/tokens/{identifier}/view_key/:
get:
operationId: core_tokens_view_key
@ -546,6 +548,8 @@ paths:
in: path
required: true
type: string
format: slug
pattern: ^[-a-zA-Z0-9_]+$
/core/users/:
get:
operationId: core_users_list
@ -6227,6 +6231,7 @@ definitions:
type: object
Token:
required:
- identifier
- user
type: object
properties:
@ -6238,7 +6243,9 @@ definitions:
identifier:
title: Identifier
type: string
readOnly: true
format: slug
pattern: ^[-a-zA-Z0-9_]+$
maxLength: 255
minLength: 1
intent:
title: Intent