core: add ability for users to create tokens
This commit is contained in:
parent
6a53069653
commit
c698ba37d9
|
@ -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:
|
||||
|
||||
|
|
22
passbook/core/forms/token.py
Normal file
22
passbook/core/forms/token.py
Normal 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(),
|
||||
}
|
|
@ -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 %}
|
||||
|
|
91
passbook/core/templates/user/token_list.html
Normal file
91
passbook/core/templates/user/token_list.html
Normal 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 %}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Reference in a new issue