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
|
- 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:
|
||||||
|
|
||||||
|
|
|
@ -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' %}"
|
<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 %}
|
||||||
|
|
|
@ -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 = [
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Reference in New Issue