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 - Add System Task Overview to see all background tasks, their status, the log output, and retry them
- Alerts now disappear automatically - Alerts now disappear automatically
- Audit Logs are now searchable - Audit Logs are now searchable
- Users can now create their own Tokens to access the API
Fixes: 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' %}" <a href="{% url 'passbook_core:user-settings' %}"
class="pf-c-nav__link {% is_active 'passbook_core:user-settings' %}">{% trans 'User Details' %}</a> class="pf-c-nav__link {% is_active 'passbook_core:user-settings' %}">{% trans 'User Details' %}</a>
</li> </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> </ul>
</section> </section>
{% user_stages as user_stages_loc %} {% user_stages as user_stages_loc %}
@ -53,6 +57,7 @@
</div> </div>
</div> </div>
<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content"> <main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
{% block content %}
<section class="pf-c-page__main-section"> <section class="pf-c-page__main-section">
<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">
@ -61,5 +66,6 @@
</div> </div>
</div> </div>
</section> </section>
{% endblock %}
</main> </main>
{% endblock %} {% 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 = [ urlpatterns = [
# User views # User views
path("-/user/", user.UserSettingsView.as_view(), name="user-settings"), 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 # Overview
path("", overview.OverviewView.as_view(), name="overview"), path("", overview.OverviewView.as_view(), name="overview"),
# Impersonation # Impersonation

View File

@ -2,13 +2,28 @@
from typing import Any, Dict from typing import Any, Dict
from django.contrib.auth.mixins import LoginRequiredMixin 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.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.urls import reverse_lazy
from django.utils.translation import gettext as _ 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.forms.users import UserDetailForm
from passbook.core.models import Token, TokenIntents
from passbook.flows.models import Flow, FlowDesignation from passbook.flows.models import Flow, FlowDesignation
from passbook.lib.views import CreateAssignPermView
class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView): class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
@ -30,3 +45,93 @@ class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
) )
kwargs["unenrollment_enabled"] = bool(unenrollment_flow) kwargs["unenrollment_enabled"] = bool(unenrollment_flow)
return kwargs 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 in: path
required: true required: true
type: string type: string
format: slug
pattern: ^[-a-zA-Z0-9_]+$
/core/tokens/{identifier}/view_key/: /core/tokens/{identifier}/view_key/:
get: get:
operationId: core_tokens_view_key operationId: core_tokens_view_key
@ -546,6 +548,8 @@ paths:
in: path in: path
required: true required: true
type: string type: string
format: slug
pattern: ^[-a-zA-Z0-9_]+$
/core/users/: /core/users/:
get: get:
operationId: core_users_list operationId: core_users_list
@ -6227,6 +6231,7 @@ definitions:
type: object type: object
Token: Token:
required: required:
- identifier
- user - user
type: object type: object
properties: properties:
@ -6238,7 +6243,9 @@ definitions:
identifier: identifier:
title: Identifier title: Identifier
type: string type: string
readOnly: true format: slug
pattern: ^[-a-zA-Z0-9_]+$
maxLength: 255
minLength: 1 minLength: 1
intent: intent:
title: Intent title: Intent