Merge branch 'master' into e2e

# Conflicts:
#	Pipfile.lock
#	docs/installation/docker-compose.md
This commit is contained in:
Jens Langhammer
2020-06-19 09:00:46 +02:00
141 changed files with 1773 additions and 1105 deletions

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = "0.8.15-beta"
__version__ = "0.9.0-pre2"

View File

@ -16,11 +16,13 @@
<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" id="page-layout-table-simple-toolbar-top">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% include 'partials/pagination.html' %}
</div>
{% include 'partials/pagination.html' %}
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
@ -65,14 +67,16 @@
</div>
{% else %}
<div class="pf-c-empty-state">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Applications.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no applications exist. Click the button below to create one.' %}
<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 Applications.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no applications exist. Click the button below to create one.' %}
</div>
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
<a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% endif %}
</div>

View File

@ -15,8 +15,10 @@
</section>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
{% include 'partials/pagination.html' %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/pagination.html' %}
</div>
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>

View File

@ -16,11 +16,13 @@
<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" id="page-layout-table-simple-toolbar-top">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% include 'partials/pagination.html' %}
</div>
{% include 'partials/pagination.html' %}
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
@ -67,14 +69,16 @@
</div>
{% else %}
<div class="pf-c-empty-state">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Certificates.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no certificates exist. Click the button below to create one.' %}
<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 Certificates.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no certificates exist. Click the button below to create one.' %}
</div>
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% endif %}
</div>

View File

@ -16,11 +16,13 @@
<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" id="page-layout-table-simple-toolbar-top">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% include 'partials/pagination.html' %}
</div>
{% include 'partials/pagination.html' %}
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
@ -69,15 +71,16 @@
</div>
{% else %}
<div class="pf-c-empty-state">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Flows.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no flows exist. Click the button below to create one.' %}
<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 Flows.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no flows exist. Click the button below to create one.' %}
</div>
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}"
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% endif %}
</div>

View File

@ -17,12 +17,14 @@
<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" id="page-layout-table-simple-toolbar-top">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}"
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}"
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% include 'partials/pagination.html' %}
</div>
{% include 'partials/pagination.html' %}
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
@ -64,14 +66,16 @@
</div>
{% else %}
<div class="pf-c-empty-state">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Groups.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no group exist. Click the button below to create one.' %}
<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 Groups.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no group exist. Click the button below to create one.' %}
</div>
<a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
<a href="{% url 'passbook_admin:group-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% endif %}
</div>

View File

@ -11,8 +11,8 @@
<section class="pf-c-page__main-section">
<div class="pf-l-gallery pf-m-gutter">
<a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card__head">
<div class="pf-c-card__head-main">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-applications"></i> {% trans 'Applications' %}
</div>
</div>
@ -22,8 +22,8 @@
</a>
<a href="{% url 'passbook_admin:sources' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card__head">
<div class="pf-c-card__head-main">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-middleware"></i> {% trans 'Sources' %}
</div>
</div>
@ -33,8 +33,8 @@
</a>
<a href="{% url 'passbook_admin:providers' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card__head">
<div class="pf-c-card__head-main">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %}
</div>
</div>
@ -49,8 +49,8 @@
</a>
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card__head">
<div class="pf-c-card__head-main">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Stages' %}
</div>
</div>
@ -65,8 +65,8 @@
</a>
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card__head">
<div class="pf-c-card__head-main">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-topology"></i> {% trans 'Flows' %}
</div>
</div>
@ -76,8 +76,8 @@
</a>
<a href="{% url 'passbook_admin:policies' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card__head">
<div class="pf-c-card__head-main">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %}
</div>
</div>
@ -92,8 +92,8 @@
</a>
<a href="{% url 'passbook_admin:stage-invitations' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card__head">
<div class="pf-c-card__head-main">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-migration"></i> {% trans 'Invitation' %}
</div>
</div>
@ -103,8 +103,8 @@
</a>
<a href="{% url 'passbook_admin:users' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card__head">
<div class="pf-c-card__head-main">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-user"></i> {% trans 'Users' %}
</div>
</div>
@ -114,8 +114,8 @@
</a>
<div class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card__head">
<div class="pf-c-card__head-main">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %}
</div>
</div>
@ -125,8 +125,8 @@
</div>
<div class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card__head">
<div class="pf-c-card__head-main">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %}
</div>
</div>
@ -141,8 +141,8 @@
</div>
<a class="pf-c-card pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
<div class="pf-c-card__head">
<div class="pf-c-card__head-main">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Policies' %}
</div>
</div>

View File

@ -16,28 +16,30 @@
<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" id="page-layout-table-simple-toolbar-top">
<div class="pf-c-toolbar__bulk-select">
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:policy-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
<div class="pf-c-toolbar__bulk-select">
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:policy-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
{% include 'partials/pagination.html' %}
</div>
{% include 'partials/pagination.html' %}
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
@ -81,31 +83,33 @@
</div>
{% else %}
<div class="pf-c-empty-state">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Policies.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no policies exist. Click the button below to create one.' %}
</div>
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item"
href="{% url 'passbook_admin:policy-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
<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 Policies.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no policies exist. Click the button below to create one.' %}
</div>
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item"
href="{% url 'passbook_admin:policy-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endif %}

View File

@ -16,12 +16,14 @@
<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" id="page-layout-table-simple-toolbar-top">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:policy-binding-create' %}?back={{ request.get_full_path }}"
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:policy-binding-create' %}?back={{ request.get_full_path }}"
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% include 'partials/pagination.html' %}
</div>
{% include 'partials/pagination.html' %}
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
@ -57,14 +59,16 @@
</div>
{% else %}
<div class="pf-c-empty-state">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Policy Bindings.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no policy bindings exist. Click the button below to create one.' %}
<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 Policy Bindings.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no policy bindings exist. Click the button below to create one.' %}
</div>
<a href="{% url 'passbook_admin:policy-binding-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
<a href="{% url 'passbook_admin:policy-binding-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% endif %}
</div>

View File

@ -17,29 +17,31 @@
<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" id="page-layout-table-simple-toolbar-top">
<div class="pf-c-toolbar__bulk-select">
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item"
href="{% url 'passbook_admin:property-mapping-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
<div class="pf-c-toolbar__bulk-select">
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item"
href="{% url 'passbook_admin:property-mapping-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
{% include 'partials/pagination.html' %}
</div>
{% include 'partials/pagination.html' %}
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
@ -75,31 +77,33 @@
</div>
{% else %}
<div class="pf-c-empty-state">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Property Mappings.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no property mappings exist. Click the button below to create one.' %}
</div>
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item"
href="{% url 'passbook_admin:property-mapping-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
<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 Property Mappings.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no property mappings exist. Click the button below to create one.' %}
</div>
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item"
href="{% url 'passbook_admin:property-mapping-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endif %}

View File

@ -18,28 +18,30 @@
<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" id="page-layout-table-simple-toolbar-top">
<div class="pf-c-toolbar__bulk-select">
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:provider-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
<div class="pf-c-toolbar__bulk-select">
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:provider-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
{% include 'partials/pagination.html' %}
</div>
{% include 'partials/pagination.html' %}
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
@ -94,30 +96,32 @@
</div>
{% else %}
<div class="pf-c-empty-state">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Providers.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no providers exist. Click the button below to create one.' %}
</div>
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:provider-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
<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 Providers.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no providers exist. Click the button below to create one.' %}
</div>
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:provider-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endif %}

View File

@ -18,28 +18,30 @@
<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" id="page-layout-table-simple-toolbar-top">
<div class="pf-c-toolbar__bulk-select">
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:source-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
<div class="pf-c-toolbar__bulk-select">
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:source-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
{% include 'partials/pagination.html' %}
</div>
{% include 'partials/pagination.html' %}
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
@ -88,30 +90,32 @@
</div>
{% else %}
<div class="pf-c-empty-state">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Sources.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no sources exist. Click the button below to create one.' %}
</div>
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:source-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
<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 Sources.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no sources exist. Click the button below to create one.' %}
</div>
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:source-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endif %}

View File

@ -18,28 +18,30 @@
<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" id="page-layout-table-simple-toolbar-top">
<div class="pf-c-toolbar__bulk-select">
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:stage-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
<div class="pf-c-toolbar__bulk-select">
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:stage-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
{% include 'partials/pagination.html' %}
</div>
{% include 'partials/pagination.html' %}
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
@ -62,6 +64,8 @@
<ul>
{% for flow in stage.flow_set.all %}
<li><a href="{% url 'passbook_admin:flow-update' pk=flow.pk %}">{{ flow.slug }}</a></li>
{% empty %}
<li>-</li>
{% endfor %}
</ul>
</td>
@ -82,31 +86,33 @@
</div>
{% else %}
<div class="pf-c-empty-state">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Stages.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no stages exist. Click the button below to create one.' %}
</div>
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item"
href="{% url 'passbook_admin:stage-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
<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 Stages.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no stages exist. Click the button below to create one.' %}
</div>
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item"
href="{% url 'passbook_admin:stage-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endif %}

View File

@ -16,12 +16,14 @@
<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" id="page-layout-table-simple-toolbar-top">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:stage-binding-create' %}?back={{ request.get_full_path }}"
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:stage-binding-create' %}?back={{ request.get_full_path }}"
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% include 'partials/pagination.html' %}
</div>
{% include 'partials/pagination.html' %}
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
@ -84,14 +86,16 @@
</div>
{% else %}
<div class="pf-c-empty-state">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Flow-Stage Bindings.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no flow-stage bindings exist. Click the button below to create one.' %}
<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 Flow-Stage Bindings.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no flow-stage bindings exist. Click the button below to create one.' %}
</div>
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
<a href="{% url 'passbook_admin:certificatekeypair-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% endif %}
</div>

View File

@ -17,12 +17,14 @@
<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" id="page-layout-table-simple-toolbar-top">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:stage-invitation-create' %}?back={{ request.get_full_path }}"
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:stage-invitation-create' %}?back={{ request.get_full_path }}"
class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% include 'partials/pagination.html' %}
</div>
{% include 'partials/pagination.html' %}
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
@ -57,14 +59,16 @@
</div>
{% else %}
<div class="pf-c-empty-state">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Invitations.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no invitations exist. Click the button below to create one.' %}
<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 Invitations.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no invitations exist. Click the button below to create one.' %}
</div>
<a href="{% url 'passbook_admin:stage-invitation-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
<a href="{% url 'passbook_admin:stage-invitation-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% endif %}
</div>

View File

@ -17,11 +17,13 @@
<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" id="page-layout-table-simple-toolbar-top">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% include 'partials/pagination.html' %}
</div>
{% include 'partials/pagination.html' %}
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
@ -83,14 +85,16 @@
</div>
{% else %}
<div class="pf-c-empty-state">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Stage Prompts.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no stage prompts exist. Click the button below to create one.' %}
<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 Stage Prompts.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no stage prompts exist. Click the button below to create one.' %}
</div>
<a href="{% url 'passbook_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
<a href="{% url 'passbook_admin:stage-prompt-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% endif %}
</div>

View File

@ -15,11 +15,13 @@
<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" id="page-layout-table-simple-toolbar-top">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
<div class="pf-c-toolbar__bulk-select">
<a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% include 'partials/pagination.html' %}
</div>
{% include 'partials/pagination.html' %}
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
@ -64,14 +66,16 @@
</div>
{% else %}
<div class="pf-c-empty-state">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Users.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no users exist. How did you even get here.' %}
<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 Users.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no users exist. How did you even get here.' %}
</div>
<a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
<a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% endif %}
</div>

View File

@ -35,10 +35,12 @@
{% block beneath_form %}
{% endblock %}
<div class="pf-c-form__group pf-m-action">
<div class="pf-c-form__horizontal-group">
<div class="pf-c-form__actions">
<input class="pf-c-button pf-m-primary" type="submit" value="{% block action %}{% endblock %}" />
<a class="pf-c-button pf-m-secondary" href="{% back %}">{% trans "Cancel" %}</a>
<div class="pf-c-form__group-control">
<div class="pf-c-form__horizontal-group">
<div class="pf-c-form__actions">
<input class="pf-c-button pf-m-primary" type="submit" value="{% block action %}{% endblock %}" />
<a class="pf-c-button pf-m-secondary" href="{% back %}">{% trans "Cancel" %}</a>
</div>
</div>
</div>
</div>

View File

@ -6,7 +6,7 @@
{% block above_form %}
<h1>
{% blocktrans with type=form|form_verbose_name|title inst=form.instance %}
Update {{ type }}: {{ inst }}
Update {{ inst }}
{% endblocktrans %}
</h1>
{% endblock %}

View File

@ -6,10 +6,10 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
# User = apps.get_model("passbook_core", "User")
# We have to use a direct import here, otherwise we get an object manager error
from passbook.core.models import User
pbadmin = User.objects.create(
pbadmin, _ = User.objects.get_or_create(
username="pbadmin", email="root@localhost", name="passbook Default Admin"
)
pbadmin.set_password("pbadmin") # noqa # nosec

View File

@ -25,8 +25,8 @@
</a>
</div>
<div class="pf-c-page__header-nav">
<nav class="pf-c-nav" aria-label="Nav">
<ul class="pf-c-nav__horizontal-list ws-top-nav">
<nav class="pf-c-nav pf-m-horizontal" aria-label="Nav">
<ul class="pf-c-nav__list ws-top-nav">
<li class="pf-c-nav__item"><a class="pf-c-nav__link {% is_active_url 'passbook_core:overview' %}"
href="{% url 'passbook_core:overview' %}">{% trans 'Access' %}</a></li>
{% if user.is_superuser %}

View File

@ -43,17 +43,19 @@
{% endfor %}
</div>
{% else %}
<div class="pf-c-empty-state">
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">{% trans 'No Applications available.' %}</h1>
<div class="pf-c-empty-state__body">
{% trans "Either no applications are defined, or you don't have access to any." %}
<div class="pf-c-empty-state pf-m-full-height">
<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 Applications available.' %}</h1>
<div class="pf-c-empty-state__body">
{% trans "Either no applications are defined, or you don't have access to any." %}
</div>
{% if user.is_superuser %} {# todo: use guardian permissions instead #}
<a href="{% url 'passbook_admin:application-create' %}" class="pf-c-button pf-m-primary" type="button">
{% trans 'Create Application' %}
</a>
{% endif %}
</div>
{% if user.is_superuser %} {# todo: use guardian permissions instead #}
<a href="{% url 'passbook_admin:application-create' %}" class="pf-c-button pf-m-primary" type="button">
{% trans 'Create Application' %}
</a>
{% endif %}
</div>
{% endif %}
</section>

View File

@ -10,6 +10,9 @@
</div>
{% endif %}
{% for field in form %}
{% if field.field.widget|fieldtype == 'HiddenInput' %}
{{ field }}
{% else %}
<div class="pf-c-form__group {% if field.errors %} has-error {% endif %}">
{% if field.field.widget|fieldtype == 'RadioSelect' %}
<label class="pf-c-form__label" {% if field.field.required %}class="required" {% endif %}
@ -66,4 +69,5 @@
</p>
{% endfor %}
</div>
{% endif %}
{% endfor %}

View File

@ -5,55 +5,72 @@
{% for field in form %}
<div class="pf-c-form__group {% if field.errors %} has-error {% endif %}">
{% if field.field.widget|fieldtype == 'RadioSelect' %}
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
<span class="pf-c-form__label-text">{{ field.label }}</span>
{% if field.field.required %}
<span class="pf-c-form__label-required" aria-hidden="true">&#42;</span>
{% endif %}
</label>
{% for c in field %}
<div class="radio col-sm-10">
<input type="radio" id="{{ field.name }}-{{ forloop.counter0 }}"
name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}" value="{{ c.data.value }}"
{% if c.data.selected %} checked {% endif %}>
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
<div class="pf-c-form__group-label">
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
<span class="pf-c-form__label-text">{{ field.label }}</span>
{% if field.field.required %}
<span class="pf-c-form__label-required" aria-hidden="true">&#42;</span>
{% endif %}
</label>
</div>
{% endfor %}
{% elif field.field.widget|fieldtype == 'Select' %}
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
<span class="pf-c-form__label-text">{{ field.label }}</span>
{% if field.field.required %}
<span class="pf-c-form__label-required" aria-hidden="true">&#42;</span>
{% endif %}
</label>
<div class="pf-c-form__horizontal-group">
{{ field|css_class:"pf-c-form-control" }}
{% if field.help_text %}
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
{% endif %}
</div>
{% elif field.field.widget|fieldtype == 'CheckboxInput' %}
<div class="pf-c-form__horizontal-group">
<div class="pf-c-check">
{{ field|css_class:"pf-c-check__input" }}
<label class="pf-c-check__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ field.label }}</label>
<div class="pf-c-form__group-control">
{% for c in field %}
<div class="radio col-sm-10">
<input type="radio" id="{{ field.name }}-{{ forloop.counter0 }}"
name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}" value="{{ c.data.value }}"
{% if c.data.selected %} checked {% endif %}>
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
</div>
{% if field.help_text %}
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
<p class="pf-c-form__helper-text">{{ field.help_text }}</p>
{% endif %}
{% endfor %}
</div>
{% elif field.field.widget|fieldtype == 'Select' %}
<div class="pf-c-form__group-label">
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
<span class="pf-c-form__label-text">{{ field.label }}</span>
{% if field.field.required %}
<span class="pf-c-form__label-required" aria-hidden="true">&#42;</span>
{% endif %}
</label>
</div>
<div class="pf-c-form__group-control">
<div class="pf-c-form__horizontal-group">
{{ field|css_class:"pf-c-form-control" }}
{% if field.help_text %}
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
{% endif %}
</div>
</div>
{% elif field.field.widget|fieldtype == 'CheckboxInput' %}
<div class="pf-c-form__group-control">
<div class="pf-c-form__horizontal-group">
<div class="pf-c-check">
{{ field|css_class:"pf-c-check__input" }}
<label class="pf-c-check__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ field.label }}</label>
</div>
{% if field.help_text %}
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
{% endif %}
</div>
</div>
{% else %}
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
<span class="pf-c-form__label-text">{{ field.label }}</span>
{% if field.field.required %}
<span class="pf-c-form__label-required" aria-hidden="true">&#42;</span>
{% endif %}
</label>
<div class="c-form__horizontal-group">
{{ field|css_class:'pf-c-form-control' }}
{% if field.help_text %}
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
{% endif %}
<div class="pf-c-form__group-label">
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
<span class="pf-c-form__label-text">{{ field.label }}</span>
{% if field.field.required %}
<span class="pf-c-form__label-required" aria-hidden="true">&#42;</span>
{% endif %}
</label>
</div>
<div class="pf-c-form__group-control">
<div class="c-form__horizontal-group">
{{ field|css_class:'pf-c-form-control' }}
{% if field.help_text %}
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
{% endif %}
</div>
</div>
{% endif %}
{% for error in field.errors %}

View File

@ -1,43 +1,43 @@
{% load i18n %}
<div class="pf-c-pagination">
<div class="pf-c-pagination__total-items">
<b>{{ page_obj.start_index }} - {{ page_obj.end_index }}</b>of
<b>{{ page_obj.count }}</b>
</div>
{% with param=get_param|default:'page' %}
<nav class="pf-c-pagination__nav" aria-label="Pagination">
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to first page"
href="?{{ param }}=1">
<i class="fas fa-angle-double-left" aria-hidden="true"></i>
</a>
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to previous page"
{% if page_obj.has_previous %}
href="?{{ param }}={{ page_obj.previous_page_number }}"
{% else %}
disabled
{% endif %}>
<i class="fas fa-angle-left" aria-hidden="true"></i>
</a>
<div class="pf-c-pagination__nav-page-select">
<span>
{% blocktrans with current=page_obj.number total=page_obj.paginator.num_pages %}
{{ current }} of {{ total }}
{% endblocktrans %}
</span>
<div class="pf-c-toolbar__item pf-m-pagination">
<div class="pf-c-pagination">
<div class="pf-c-pagination__total-items">
<b>{{ page_obj.start_index }} - {{ page_obj.end_index }}</b>of
<b>{{ page_obj.count }}</b>
</div>
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to next page"
{% if page_obj.has_next %}
href="?{{ param }}={{ page_obj.next_page_number }}"
{% else %}
disabled
{% endif %}>
<i class="fas fa-angle-right" aria-hidden="true"></i>
</a>
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to last page"
href="?{{ param }}={{ page_obj.num_pages }}">
<i class="fas fa-angle-double-right" aria-hidden="true"></i>
</a>
</nav>
{% endwith %}
{% with param=get_param|default:'page' %}
<nav class="pf-c-pagination__nav" aria-label="Pagination">
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to first page" href="?{{ param }}=1">
<i class="fas fa-angle-double-left" aria-hidden="true"></i>
</a>
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to previous page"
{% if page_obj.has_previous %}
href="?{{ param }}={{ page_obj.previous_page_number }}"
{% else %}
disabled
{% endif %}>
<i class="fas fa-angle-left" aria-hidden="true"></i>
</a>
<div class="pf-c-pagination__nav-page-select">
<span>
{% blocktrans with current=page_obj.number total=page_obj.paginator.num_pages %}
{{ current }} of {{ total }}
{% endblocktrans %}
</span>
</div>
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to next page"
{% if page_obj.has_next %}
href="?{{ param }}={{ page_obj.next_page_number }}"
{% else %}
disabled
{% endif %}>
<i class="fas fa-angle-right" aria-hidden="true"></i>
</a>
<a class="pf-c-button pf-m-plain" type="button" aria-label="Go to last page" href="?{{ param }}={{ page_obj.num_pages }}">
<i class="fas fa-angle-double-right" aria-hidden="true"></i>
</a>
</nav>
{% endwith %}
</div>
</div>

View File

@ -44,6 +44,9 @@ class FlowStageBindingForm(forms.ModelForm):
"re_evaluate_policies",
"order",
]
labels = {
"re_evaluate_policies": _("Re-evaluate Policies"),
}
widgets = {
"name": forms.TextInput(),
}

50
passbook/flows/markers.py Normal file
View File

@ -0,0 +1,50 @@
"""Stage Markers"""
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional
from structlog import get_logger
from passbook.core.models import User
from passbook.flows.models import Stage
from passbook.policies.engine import PolicyEngine
from passbook.policies.models import PolicyBinding
if TYPE_CHECKING:
from passbook.flows.planner import FlowPlan
LOGGER = get_logger()
@dataclass
class StageMarker:
"""Base stage marker class, no extra attributes, and has no special handler."""
# pylint: disable=unused-argument
def process(self, plan: "FlowPlan", stage: Stage) -> Optional[Stage]:
"""Process callback for this marker. This should be overridden by sub-classes.
If a stage should be removed, return None."""
return stage
@dataclass
class ReevaluateMarker(StageMarker):
"""Reevaluate Marker, forces stage's policies to be evaluated again."""
binding: PolicyBinding
user: User
def process(self, plan: "FlowPlan", stage: Stage) -> Optional[Stage]:
"""Re-evaluate policies bound to stage, and if they fail, remove from plan"""
engine = PolicyEngine(self.binding, self.user)
engine.use_cache = False
engine.request.context = plan.context
engine.build()
result = engine.result
if result.passing:
return stage
LOGGER.warning(
"f(plan_inst)[re-eval marker]: stage failed re-evaluation",
stage=stage,
messages=result.messages,
)
return None

View File

@ -9,7 +9,8 @@ from structlog import get_logger
from passbook.core.models import User
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from passbook.flows.models import Flow, Stage
from passbook.flows.markers import ReevaluateMarker, StageMarker
from passbook.flows.models import Flow, FlowStageBinding, Stage
from passbook.policies.engine import PolicyEngine
LOGGER = get_logger()
@ -33,12 +34,39 @@ class FlowPlan:
of all Stages that should be run."""
flow_pk: str
stages: List[Stage] = field(default_factory=list)
context: Dict[str, Any] = field(default_factory=dict)
markers: List[StageMarker] = field(default_factory=list)
def next(self) -> Stage:
def next(self) -> Optional[Stage]:
"""Return next pending stage from the bottom of the list"""
return self.stages[0]
if not self.has_stages:
return None
stage = self.stages[0]
marker = self.markers[0]
LOGGER.debug("f(plan_inst): stage has marker", stage=stage, marker=marker)
marked_stage = marker.process(self, stage)
if not marked_stage:
LOGGER.debug("f(plan_inst): marker returned none, next stage", stage=stage)
self.stages.remove(stage)
self.markers.remove(marker)
if not self.has_stages:
return None
# pylint: disable=not-callable
return self.next()
return marked_stage
def pop(self):
"""Pop next pending stage from bottom of list"""
self.markers.pop(0)
self.stages.pop(0)
@property
def has_stages(self) -> bool:
"""Check if there are any stages left in this plan"""
return len(self.markers) + len(self.stages) > 0
class FlowPlanner:
@ -100,7 +128,8 @@ class FlowPlanner:
request: HttpRequest,
default_context: Optional[Dict[str, Any]],
) -> FlowPlan:
"""Actually build flow plan"""
"""Build flow plan by checking each stage in their respective
order and checking the applied policies"""
start_time = time()
plan = FlowPlan(flow_pk=self.flow.pk.hex)
if default_context:
@ -111,13 +140,24 @@ class FlowPlanner:
.select_subclasses()
.select_related()
):
binding = stage.flowstagebinding_set.get(flow__pk=self.flow.pk)
binding: FlowStageBinding = stage.flowstagebinding_set.get(
flow__pk=self.flow.pk
)
engine = PolicyEngine(binding, user, request)
engine.request.context = plan.context
engine.build()
if engine.passing:
LOGGER.debug("f(plan): Stage passing", stage=stage, flow=self.flow)
plan.stages.append(stage)
marker = StageMarker()
if binding.re_evaluate_policies:
LOGGER.debug(
"f(plan): Stage has re-evaluate marker",
stage=stage,
flow=self.flow,
)
marker = ReevaluateMarker(binding=binding, user=user)
plan.markers.append(marker)
end_time = time()
LOGGER.debug(
"f(plan): Finished building",

View File

@ -57,7 +57,7 @@
<p></p>
<ul class="pf-c-list pf-m-inline">
<li>
<a href="https://beryju.github.io/passbook/">{% trans 'Documentation' %}</a>
<a href="https://passbook.beryju.org/">{% trans 'Documentation' %}</a>
</li>
<!-- todo: load config.passbook.footer.links -->
</ul>
@ -120,6 +120,7 @@ const updateCard = (data) => {
break;
case "template":
flowBody.innerHTML = data.body;
checkAutofocus();
updateMessages();
loadFormCode();
setFormSubmitHandlers();
@ -138,6 +139,12 @@ const loadFormCode = () => {
document.head.appendChild(newScript);
});
};
const checkAutofocus = () => {
const autofocusElement = document.querySelector("#flow-body [autofocus]");
if (autofocusElement !== null) {
autofocusElement.focus();
}
};
const updateFormAction = (form) => {
for (let index = 0; index < form.elements.length; index++) {
const element = form.elements[index];

View File

@ -1,6 +1,7 @@
"""flow planner tests"""
from unittest.mock import MagicMock, PropertyMock, patch
from django.contrib.sessions.middleware import SessionMiddleware
from django.core.cache import cache
from django.shortcuts import reverse
from django.test import RequestFactory, TestCase
@ -8,14 +9,19 @@ from guardian.shortcuts import get_anonymous_user
from passbook.core.models import User
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from passbook.flows.markers import ReevaluateMarker, StageMarker
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
from passbook.policies.dummy.models import DummyPolicy
from passbook.policies.models import PolicyBinding
from passbook.policies.types import PolicyResult
from passbook.stages.dummy.models import DummyStage
POLICY_RESULT_MOCK = PropertyMock(return_value=PolicyResult(False))
POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False))
TIME_NOW_MOCK = MagicMock(return_value=3)
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
class TestFlowPlanner(TestCase):
"""Test planner logic"""
@ -40,7 +46,7 @@ class TestFlowPlanner(TestCase):
planner.plan(request)
@patch(
"passbook.policies.engine.PolicyEngine.result", POLICY_RESULT_MOCK,
"passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE,
)
def test_non_applicable_plan(self):
"""Test that empty plan raises exception"""
@ -103,3 +109,71 @@ class TestFlowPlanner(TestCase):
planner.plan(request, default_context={PLAN_CONTEXT_PENDING_USER: user})
key = cache_key(flow, user)
self.assertTrue(cache.get(key) is not None)
def test_planner_marker_reevaluate(self):
"""Test that the planner creates the proper marker"""
flow = Flow.objects.create(
name="test-default-context",
slug="test-default-context",
designation=FlowDesignation.AUTHENTICATION,
)
FlowStageBinding.objects.create(
flow=flow,
stage=DummyStage.objects.create(name="dummy1"),
order=0,
re_evaluate_policies=True,
)
request = self.request_factory.get(
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
)
request.user = get_anonymous_user()
planner = FlowPlanner(flow)
plan = planner.plan(request)
self.assertIsInstance(plan.markers[0], ReevaluateMarker)
def test_planner_reevaluate_actual(self):
"""Test planner with re-evaluate"""
flow = Flow.objects.create(
name="test-default-context",
slug="test-default-context",
designation=FlowDesignation.AUTHENTICATION,
)
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
binding = FlowStageBinding.objects.create(
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
)
binding2 = FlowStageBinding.objects.create(
flow=flow,
stage=DummyStage.objects.create(name="dummy2"),
order=1,
re_evaluate_policies=True,
)
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
request = self.request_factory.get(
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
)
request.user = get_anonymous_user()
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
# Here we patch the dummy policy to evaluate to true so the stage is included
with patch(
"passbook.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE
):
planner = FlowPlanner(flow)
plan = planner.plan(request)
self.assertEqual(plan.stages[0], binding.stage)
self.assertEqual(plan.stages[1], binding2.stage)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker)

View File

@ -3,16 +3,21 @@ from unittest.mock import MagicMock, PropertyMock, patch
from django.shortcuts import reverse
from django.test import Client, TestCase
from django.utils.encoding import force_text
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from passbook.flows.markers import ReevaluateMarker, StageMarker
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.flows.planner import FlowPlan
from passbook.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN
from passbook.lib.config import CONFIG
from passbook.policies.dummy.models import DummyPolicy
from passbook.policies.models import PolicyBinding
from passbook.policies.types import PolicyResult
from passbook.stages.dummy.models import DummyStage
POLICY_RESULT_MOCK = PropertyMock(return_value=PolicyResult(False))
POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False))
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
class TestFlowExecutor(TestCase):
@ -29,7 +34,9 @@ class TestFlowExecutor(TestCase):
designation=FlowDesignation.AUTHENTICATION,
)
stage = DummyStage.objects.create(name="dummy")
plan = FlowPlan(flow_pk=flow.pk.hex + "a", stages=[stage])
plan = FlowPlan(
flow_pk=flow.pk.hex + "a", stages=[stage], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
@ -45,7 +52,7 @@ class TestFlowExecutor(TestCase):
self.assertEqual(cancel_mock.call_count, 1)
@patch(
"passbook.policies.engine.PolicyEngine.result", POLICY_RESULT_MOCK,
"passbook.policies.engine.PolicyEngine.result", POLICY_RETURN_FALSE,
)
def test_invalid_non_applicable_flow(self):
"""Tests that a non-applicable flow returns the correct error message"""
@ -125,3 +132,197 @@ class TestFlowExecutor(TestCase):
session = self.client.session
plan: FlowPlan = session[SESSION_KEY_PLAN]
self.assertEqual(len(plan.stages), 1)
def test_reevaluate_remove_last(self):
"""Test planner with re-evaluate (last stage is removed)"""
flow = Flow.objects.create(
name="test-default-context",
slug="test-default-context",
designation=FlowDesignation.AUTHENTICATION,
)
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
binding = FlowStageBinding.objects.create(
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
)
binding2 = FlowStageBinding.objects.create(
flow=flow,
stage=DummyStage.objects.create(name="dummy2"),
order=1,
re_evaluate_policies=True,
)
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
# Here we patch the dummy policy to evaluate to true so the stage is included
with patch(
"passbook.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE
):
exec_url = reverse(
"passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}
)
# First request, run the planner
response = self.client.get(exec_url)
self.assertEqual(response.status_code, 200)
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding.stage)
self.assertEqual(plan.stages[1], binding2.stage)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
# Second request, this passes the first dummy stage
response = self.client.post(exec_url)
self.assertEqual(response.status_code, 302)
# third request, this should trigger the re-evaluate
# We do this request without the patch, so the policy results in false
response = self.client.post(exec_url)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("passbook_core:overview"))
def test_reevaluate_remove_middle(self):
"""Test planner with re-evaluate (middle stage is removed)"""
flow = Flow.objects.create(
name="test-default-context",
slug="test-default-context",
designation=FlowDesignation.AUTHENTICATION,
)
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
binding = FlowStageBinding.objects.create(
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
)
binding2 = FlowStageBinding.objects.create(
flow=flow,
stage=DummyStage.objects.create(name="dummy2"),
order=1,
re_evaluate_policies=True,
)
binding3 = FlowStageBinding.objects.create(
flow=flow, stage=DummyStage.objects.create(name="dummy3"), order=2
)
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
# Here we patch the dummy policy to evaluate to true so the stage is included
with patch(
"passbook.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE
):
exec_url = reverse(
"passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}
)
# First request, run the planner
response = self.client.get(exec_url)
self.assertEqual(response.status_code, 200)
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding.stage)
self.assertEqual(plan.stages[1], binding2.stage)
self.assertEqual(plan.stages[2], binding3.stage)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
self.assertIsInstance(plan.markers[2], StageMarker)
# Second request, this passes the first dummy stage
response = self.client.post(exec_url)
self.assertEqual(response.status_code, 302)
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding2.stage)
self.assertEqual(plan.stages[1], binding3.stage)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], StageMarker)
# third request, this should trigger the re-evaluate
# We do this request without the patch, so the policy results in false
response = self.client.post(exec_url)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_text(response.content),
{"type": "redirect", "to": reverse("passbook_core:overview")},
)
def test_reevaluate_remove_consecutive(self):
"""Test planner with re-evaluate (consecutive stages are removed)"""
flow = Flow.objects.create(
name="test-default-context",
slug="test-default-context",
designation=FlowDesignation.AUTHENTICATION,
)
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
binding = FlowStageBinding.objects.create(
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
)
binding2 = FlowStageBinding.objects.create(
flow=flow,
stage=DummyStage.objects.create(name="dummy2"),
order=1,
re_evaluate_policies=True,
)
binding3 = FlowStageBinding.objects.create(
flow=flow,
stage=DummyStage.objects.create(name="dummy3"),
order=2,
re_evaluate_policies=True,
)
binding4 = FlowStageBinding.objects.create(
flow=flow, stage=DummyStage.objects.create(name="dummy4"), order=2
)
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
PolicyBinding.objects.create(policy=false_policy, target=binding3, order=0)
# Here we patch the dummy policy to evaluate to true so the stage is included
with patch(
"passbook.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE
):
exec_url = reverse(
"passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}
)
# First request, run the planner
response = self.client.get(exec_url)
self.assertEqual(response.status_code, 200)
self.assertIn("dummy1", force_text(response.content))
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
self.assertEqual(plan.stages[0], binding.stage)
self.assertEqual(plan.stages[1], binding2.stage)
self.assertEqual(plan.stages[2], binding3.stage)
self.assertEqual(plan.stages[3], binding4.stage)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
self.assertIsInstance(plan.markers[2], ReevaluateMarker)
self.assertIsInstance(plan.markers[3], StageMarker)
# Second request, this passes the first dummy stage
response = self.client.post(exec_url)
self.assertEqual(response.status_code, 302)
# third request, this should trigger the re-evaluate
# A get request will evaluate the policies and this will return stage 4
# but it won't save it, hence we cant' check the plan
response = self.client.get(exec_url)
self.assertEqual(response.status_code, 200)
self.assertIn("dummy4", force_text(response.content))
# fourth request, this confirms the last stage (dummy4)
# We do this request without the patch, so the policy results in false
response = self.client.post(exec_url)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_text(response.content),
{"type": "redirect", "to": reverse("passbook_core:overview")},
)

View File

@ -26,7 +26,7 @@ class TestHelperView(TestCase):
def test_default_view_invalid_plan(self):
"""Test that ToDefaultFlow returns the expected URL (with an invalid plan)"""
flow = Flow.objects.filter(designation=FlowDesignation.INVALIDATION,).first()
plan = FlowPlan(flow_pk=flow.pk.hex + "aa", stages=[])
plan = FlowPlan(flow_pk=flow.pk.hex + "aa")
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()

View File

@ -86,6 +86,9 @@ class FlowExecutorView(View):
current_stage=self.current_stage,
flow_slug=self.flow.slug,
)
if not self.current_stage:
LOGGER.debug("f(exec): no more stages, flow is done.")
return self._flow_done()
stage_cls = path_to_class(self.current_stage.type)
self.current_stage_view = stage_cls(self)
self.current_stage_view.args = self.args
@ -98,6 +101,7 @@ class FlowExecutorView(View):
LOGGER.debug(
"f(exec): Passing GET",
view_class=class_to_path(self.current_stage_view.__class__),
stage=self.current_stage,
flow_slug=self.flow.slug,
)
stage_response = self.current_stage_view.get(request, *args, **kwargs)
@ -108,6 +112,7 @@ class FlowExecutorView(View):
LOGGER.debug(
"f(exec): Passing POST",
view_class=class_to_path(self.current_stage_view.__class__),
stage=self.current_stage,
flow_slug=self.flow.slug,
)
stage_response = self.current_stage_view.post(request, *args, **kwargs)
@ -133,7 +138,11 @@ class FlowExecutorView(View):
stage_class=class_to_path(self.current_stage_view.__class__),
flow_slug=self.flow.slug,
)
self.plan.stages.pop(0)
# We call plan.next here to check for re-evaluate markers
# this is important so we can save the result
# and we don't have to re-evaluate the policies each request
self.plan.next()
self.plan.pop()
self.request.session[SESSION_KEY_PLAN] = self.plan
if self.plan.stages:
LOGGER.debug(

View File

@ -7,23 +7,8 @@
<label for="" class="pf-c-form__label"></label>
<div class="c-form__horizontal-group">
<p>
Expression using <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/">Jinja</a>. Following variables are available:
Expression using Python. See <a href="https://passbook.beryju.org/policies/expression/">here</a> for a list of all variables.
</p>
<ul class="pf-c-list">
<li><code>request.user</code>: Passbook User Object (<a href="https://beryju.github.io/passbook/property-mappings/reference/user-object/">Reference</a>)</li>
<li><code>request.http_request</code>: Django HTTP Request Object (<a href="https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects">Reference</a>) </li>
<li><code>request.obj</code>: Model the Policy is run against. </li>
<li><code>pb_flow_plan</code>: Current Plan if Policy is called while a flow is active.</li>
<li><code>pb_is_sso_flow</code>: Boolean which is true if request was initiated by authenticating through an external Provider.</li>
<li><code>pb_is_group_member(user, group_name)</code>: Function which checks if <code>user</code> is member of a Group with Name <code>group_name</code>.</li>
<li><code>pb_logger</code>: Standard Python Logger Object, which can be used to debug expressions.</li>
<li><code>pb_client_ip</code>: Client's IP Address.</li>
</ul>
<p>Custom Filters:</p>
<ul class="pf-c-list">
<li><code>regex_match(regex)</code>: Checks if value matches <code>regex</code></li>
<li><code>regex_replace(regex, repl)</code>: Replace string matched by <code>regex</code> with <code>repl</code></li>
</ul>
</div>
</div>
{% endblock %}

View File

@ -2,7 +2,6 @@ version: "3.5"
services:
passbook_gatekeeper:
container_name: gatekeeper
image: beryju/passbook-gatekeeper:{{ version }}
ports:
- 4180:4180

View File

@ -23,11 +23,9 @@ OAUTH2_PROVIDER = {
# this is the list of available scopes
"SCOPES": {
"openid": "Access OpenID Userinfo",
"openid:userinfo": "Access OpenID Userinfo",
"email": "Access OpenID E-Mail",
# 'write': 'Write scope',
# 'groups': 'Access to your groups',
"user:email": "GitHub Compatibility: User E-Mail",
"userinfo": "Access OpenID Userinfo",
"email": "Access OpenID Email",
"user:email": "GitHub Compatibility: User Email",
"read:org": "GitHub Compatibility: User Groups",
}
}

View File

@ -2,8 +2,10 @@
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.html import mark_safe
from django.utils.translation import gettext as _
from passbook.admin.fields import CodeMirrorWidget
from passbook.core.expression import PropertyMappingEvaluator
from passbook.flows.models import Flow, FlowDesignation
from passbook.providers.saml.models import (
@ -74,4 +76,13 @@ class SAMLPropertyMappingForm(forms.ModelForm):
"name": forms.TextInput(),
"saml_name": forms.TextInput(),
"friendly_name": forms.TextInput(),
"expression": CodeMirrorWidget(mode="python"),
}
help_texts = {
"saml_name": mark_safe(
_(
"URN OID used by SAML. This is optional. "
'<a href="https://www.rfc-editor.org/rfc/rfc2798.html#section-2">Reference</a>'
)
),
}

View File

@ -7,12 +7,7 @@
<label for="" class="pf-c-form__label"></label>
<div class="c-form__horizontal-group">
<p>
Expression using <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/">Jinja</a>. Following variables are available:
<ul class="pf-c-list">
<li><code>user</code>: Passbook User Object (<a href="https://beryju.github.io/passbook/reference/property-mappings/user-object/">Reference</a>)</li>
<li><code>request</code>: Django HTTP Request Object (<a href="https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects">Reference</a>) </li>
<li><code>provider</code>: Passbook SAML Provider Object (<a href="https://github.com/BeryJu/passbook/blob/master/passbook/providers/saml/models.py#L16">Reference</a>) </li>
</ul>
Expression using Python. See <a href="https://passbook.beryju.org/property-mappings/expression/">here</a> for a list of all variables.
</p>
</div>
</div>

View File

@ -102,7 +102,7 @@ class SAMLSSOBindingRedirectView(SAMLSSOView):
"""SAML Handler for SSO/Redirect bindings, which are sent via GET"""
# pylint: disable=unused-argument
def get(
def get( # lgtm [py/similar-function]
self, request: HttpRequest, application_slug: str
) -> Optional[HttpResponse]:
"""Handle REDIRECT bindings"""

View File

@ -188,7 +188,7 @@ WSGI_APPLICATION = "passbook.root.wsgi.application"
DATABASES = {
"default": {
"ENGINE": "django_prometheus.db.backends.postgresql",
"ENGINE": "django.db.backends.postgresql",
"HOST": CONFIG.y("postgresql.host"),
"NAME": CONFIG.y("postgresql.name"),
"USER": CONFIG.y("postgresql.user"),

View File

@ -4,6 +4,7 @@ from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext_lazy as _
from passbook.admin.fields import CodeMirrorWidget
from passbook.admin.forms.source import SOURCE_FORM_FIELDS
from passbook.core.expression import PropertyMappingEvaluator
from passbook.sources.ldap.models import LDAPPropertyMapping, LDAPSource
@ -68,4 +69,8 @@ class LDAPPropertyMappingForm(forms.ModelForm):
"name": forms.TextInput(),
"ldap_property": forms.TextInput(),
"object_field": forms.TextInput(),
"expression": CodeMirrorWidget(mode="python"),
}
help_texts = {
"object_field": _("Field of the user object this value is written to.")
}

View File

@ -55,7 +55,7 @@ class LDAPSource(Source):
form = "passbook.sources.ldap.forms.LDAPSourceForm"
_connection: Optional[Connection]
_connection: Optional[Connection] = None
@property
def connection(self) -> Connection:

View File

@ -7,10 +7,7 @@
<label for="" class="pf-c-form__label"></label>
<div class="c-form__horizontal-group">
<p>
Expression using <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/">Jinja</a>. Following variables are available:
<ul class="pf-c-list">
<li><code>ldap</code>: A Dictionary of all values retrieved from LDAP.</li>
</ul>
Expression using Python. See <a href="https://passbook.beryju.org/property-mappings/expression/">here</a> for a list of all variables.
</p>
</div>
</div>

View File

@ -94,8 +94,6 @@ class OAuthClient(BaseOAuthClient):
"oauth_callback": callback,
"token": raw_token,
}
callback = request.build_absolute_uri(callback or request.path)
callback = force_text(callback)
try:
response = self.session.request(
"post",

View File

@ -5,6 +5,7 @@ from django.test import Client, TestCase
from django.utils.encoding import force_text
from passbook.core.models import User
from passbook.flows.markers import StageMarker
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.flows.planner import FlowPlan
from passbook.flows.views import SESSION_KEY_PLAN
@ -35,7 +36,9 @@ class TestCaptchaStage(TestCase):
def test_valid(self):
"""Test valid captcha"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()

View File

@ -4,6 +4,7 @@ from django.test import Client, TestCase
from django.utils.encoding import force_text
from passbook.core.models import User
from passbook.flows.markers import StageMarker
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.flows.planner import FlowPlan
from passbook.flows.views import SESSION_KEY_PLAN
@ -30,7 +31,9 @@ class TestConsentStage(TestCase):
def test_valid(self):
"""Test valid consent"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()

View File

@ -1,4 +1,6 @@
"""passbook multi-stage authentication engine"""
from typing import Any, Dict
from django.http import HttpRequest
from passbook.flows.stage import StageView
@ -10,3 +12,8 @@ class DummyStage(StageView):
def post(self, request: HttpRequest):
"""Just redirect to next stage"""
return self.executor.stage_ok()
def get_context_data(self, **kwargs: Dict[str, Any]) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
kwargs["title"] = self.executor.current_stage.name
return kwargs

View File

@ -6,13 +6,13 @@ from passbook.stages.email.models import EmailStage
class EmailStageSendForm(forms.Form):
"""Form used when sending the e-mail to prevent multiple emails being sent"""
"""Form used when sending the email to prevent multiple emails being sent"""
invalid = forms.CharField(widget=forms.HiddenInput, required=True)
class EmailStageForm(forms.ModelForm):
"""Form to create/edit E-Mail Stage"""
"""Form to create/edit Email Stage"""
class Meta:
@ -34,6 +34,7 @@ class EmailStageForm(forms.ModelForm):
widgets = {
"name": forms.TextInput(),
"host": forms.TextInput(),
"subject": forms.TextInput(),
"username": forms.TextInput(),
"password": forms.TextInput(),
}

View File

@ -8,7 +8,7 @@ from passbook.flows.models import Stage
class EmailTemplates(models.TextChoices):
"""Templates used for rendering the E-Mail"""
"""Templates used for rendering the Email"""
PASSWORD_RESET = (
"stages/email/for_email/password_reset.html",

View File

@ -22,7 +22,7 @@ QS_KEY_TOKEN = "token"
class EmailStageView(FormView, StageView):
"""E-Mail stage which sends E-Mail for verification"""
"""Email stage which sends Email for verification"""
form_class = EmailStageSendForm
template_name = "stages/email/waiting_message.html"
@ -41,11 +41,14 @@ class EmailStageView(FormView, StageView):
token = get_object_or_404(Token, pk=request.GET[QS_KEY_TOKEN])
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = token.user
token.delete()
messages.success(request, _("Successfully verified E-Mail."))
messages.success(request, _("Successfully verified Email."))
return self.executor.stage_ok()
return super().get(request, *args, **kwargs)
def form_invalid(self, form: EmailStageSendForm) -> HttpResponse:
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
messages.error(self.request, _("No pending user."))
return super().form_invalid(form)
pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
valid_delta = timedelta(
minutes=self.executor.current_stage.token_expiry + 1

View File

@ -27,7 +27,7 @@ def send_mails(stage: EmailStage, *messages: List[EmailMultiAlternatives]):
)
# pylint: disable=unused-argument
def _send_mail_task(self, email_stage_pk: int, message: Dict[Any, Any]):
"""Send E-Mail according to EmailStage parameters from background worker.
"""Send Email according to EmailStage parameters from background worker.
Automatically retries if message couldn't be sent."""
stage: EmailStage = EmailStage.objects.get(pk=email_stage_pk)
backend = stage.backend

View File

@ -18,7 +18,7 @@
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td> <a href="{{ url }}" target="_blank">{% trans 'Confirm Account' %}</a> </td>
<td> <a href="{{ url }}" rel="noopener noreferrer" target="_blank">{% trans 'Confirm Account' %}</a> </td>
</tr>
</tbody>
</table>

View File

@ -23,7 +23,7 @@
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td> <a href="{{ url }}" target="_blank">{% trans 'Reset Password' %}</a> </td>
<td> <a href="{{ url }}" rel="noopener noreferrer" target="_blank">{% trans 'Reset Password' %}</a> </td>
</tr>
</tbody>
</table>
@ -33,7 +33,7 @@
</table>
<p>
{% blocktrans with expires=expires|naturaltime %}
If you did not request a password change, please ignore this E-Mail. The link above is valid for {{ expires }}.
If you did not request a password change, please ignore this Email. The link above is valid for {{ expires }}.
{% endblocktrans %}
</p>
</td>

View File

@ -7,7 +7,7 @@
<form method="POST" class="pf-c-form">
<p>
{% blocktrans %}
Check your E-Mails for a password reset link.
Check your Emails for a password reset link.
{% endblocktrans %}
</p>
{% csrf_token %}
@ -15,7 +15,7 @@
{% block beneath_form %}
{% endblock %}
<div class="pf-c-form__group pf-m-action">
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans "Send Recovery E-Mail." %}</button>
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans "Send Recovery Email." %}</button>
</div>
</form>
{% endblock %}

View File

@ -7,6 +7,7 @@ from django.test import Client, TestCase
from django.utils.encoding import force_text
from passbook.core.models import Token, User
from passbook.flows.markers import StageMarker
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from passbook.flows.views import SESSION_KEY_PLAN
@ -34,7 +35,9 @@ class TestEmailStage(TestCase):
def test_rendering(self):
"""Test with pending user"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
@ -48,7 +51,9 @@ class TestEmailStage(TestCase):
def test_without_user(self):
"""Test without pending user"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
@ -61,7 +66,9 @@ class TestEmailStage(TestCase):
def test_pending_user(self):
"""Test with pending user"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
@ -82,7 +89,9 @@ class TestEmailStage(TestCase):
"""Test with token"""
# Make sure token exists
self.test_pending_user()
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()

View File

@ -1,5 +1,6 @@
"""passbook flows identification forms"""
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.core.validators import validate_email
from django.utils.translation import gettext_lazy as _
from structlog import get_logger
@ -19,6 +20,9 @@ class IdentificationStageForm(forms.ModelForm):
fields = ["name", "user_fields", "template", "enrollment_flow", "recovery_flow"]
widgets = {
"name": forms.TextInput(),
"user_fields": FilteredSelectMultiple(
_("fields"), False, choices=UserFields.choices
),
}
@ -35,8 +39,16 @@ class IdentificationForm(forms.Form):
super().__init__(*args, **kwargs)
if self.stage.user_fields == [UserFields.E_MAIL]:
self.fields["uid_field"] = forms.EmailField()
self.fields["uid_field"].label = human_list(
[x.title() for x in self.stage.user_fields]
label = human_list([x.title() for x in self.stage.user_fields])
self.fields["uid_field"].label = label
self.fields["uid_field"].widget.attrs.update(
{
"placeholder": _(label),
"autofocus": "autofocus",
# Autocomplete according to
# https://www.chromium.org/developers/design-documents/form-styles-that-chromium-understands
"autocomplete": "username",
}
)
def clean_uid_field(self):

View File

@ -0,0 +1,28 @@
# Generated by Django 3.0.7 on 2020-06-15 16:41
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_flows", "0005_provider_flows"),
("passbook_stages_identification", "0002_auto_20200530_2204"),
]
operations = [
migrations.AlterField(
model_name="identificationstage",
name="recovery_flow",
field=models.ForeignKey(
blank=True,
default=None,
help_text="Optional recovery flow, which is linked at the bottom of the page.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
related_name="+",
to="passbook_flows.Flow",
),
),
]

View File

@ -48,7 +48,7 @@ class IdentificationStage(Stage):
related_name="+",
default=None,
help_text=_(
"Optional enrollment flow, which is linked at the bottom of the page."
"Optional recovery flow, which is linked at the bottom of the page."
),
)

View File

@ -61,7 +61,7 @@ class TestIdentificationStage(TestCase):
)
def test_invalid_with_username(self):
"""Test invalid with username (user exists but stage only allows e-mail)"""
"""Test invalid with username (user exists but stage only allows email)"""
form_data = {"uid_field": self.user.username}
response = self.client.post(
reverse(
@ -72,7 +72,7 @@ class TestIdentificationStage(TestCase):
self.assertEqual(response.status_code, 200)
def test_invalid_with_invalid_email(self):
"""Test with invalid e-mail (user doesn't exist) -> Will return to login form"""
"""Test with invalid email (user doesn't exist) -> Will return to login form"""
form_data = {"uid_field": self.user.email + "test"}
response = self.client.post(
reverse(

View File

@ -7,6 +7,7 @@ from passbook.stages.invitation.models import Invitation, InvitationStage
from passbook.stages.prompt.stage import PLAN_CONTEXT_PROMPT
INVITATION_TOKEN_KEY = "token"
INVITATION_IN_EFFECT = "invitation_in_effect"
class InvitationStageView(StageView):
@ -23,4 +24,5 @@ class InvitationStageView(StageView):
token = request.GET[INVITATION_TOKEN_KEY]
invite: Invitation = get_object_or_404(Invitation, pk=token)
self.executor.plan.context[PLAN_CONTEXT_PROMPT] = invite.fixed_data
self.executor.plan.context[INVITATION_IN_EFFECT] = True
return self.executor.stage_ok()

View File

@ -7,6 +7,7 @@ from django.utils.encoding import force_text
from guardian.shortcuts import get_anonymous_user
from passbook.core.models import User
from passbook.flows.markers import StageMarker
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from passbook.flows.views import SESSION_KEY_PLAN
@ -39,7 +40,9 @@ class TestUserLoginStage(TestCase):
def test_without_invitation_fail(self):
"""Test without any invitation, continue_flow_without_invitation not set."""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
plan.context[
PLAN_CONTEXT_AUTHENTICATION_BACKEND
@ -64,7 +67,9 @@ class TestUserLoginStage(TestCase):
"""Test without any invitation, continue_flow_without_invitation is set."""
self.stage.continue_flow_without_invitation = True
self.stage.save()
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
plan.context[
PLAN_CONTEXT_AUTHENTICATION_BACKEND
@ -90,7 +95,9 @@ class TestUserLoginStage(TestCase):
def test_with_invitation(self):
"""Test with invitation, check data in session"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
plan.context[
PLAN_CONTEXT_AUTHENTICATION_BACKEND

View File

@ -110,7 +110,7 @@ class EnableView(LoginRequiredMixin, FormView):
self.static_device = StaticDevice(user=request.user, confirmed=False)
self.static_device.save()
# Create 9 tokens and save them
# TODO: Send static tokens via E-Mail
# TODO: Send static tokens via Email
for _counter in range(0, 9):
token = StaticToken(
device=self.static_device, token=StaticToken.random_token()

View File

@ -1,25 +1,31 @@
"""passbook administration forms"""
from django import forms
from django.conf import settings
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext_lazy as _
from passbook.lib.utils.reflection import path_to_class
from passbook.stages.password.models import PasswordStage
def get_authentication_backends():
"""Return all available authentication backends as tuple set"""
for backend in settings.AUTHENTICATION_BACKENDS:
klass = path_to_class(backend)
yield backend, getattr(
klass(), "name", "%s (%s)" % (klass.__name__, klass.__module__)
)
return [
(
"django.contrib.auth.backends.ModelBackend",
_("passbook-internal Userdatabase"),
),
(
"passbook.sources.ldap.auth.LDAPBackend",
_("passbook LDAP (Only needed when User-Sync is not enabled."),
),
]
class PasswordForm(forms.Form):
"""Password authentication form"""
username = forms.CharField(
widget=forms.HiddenInput(attrs={"autocomplete": "username"}), required=False
)
password = forms.CharField(
widget=forms.PasswordInput(
attrs={

View File

@ -52,9 +52,20 @@ class PasswordStage(FormView, StageView):
form_class = PasswordForm
template_name = "stages/password/backend.html"
def get_form(self, form_class=None) -> PasswordForm:
form = super().get_form(form_class=form_class)
# If there's a pending user, update the `username` field
# this field is only used by password managers.
# If there's no user set, an error is raised later.
if PLAN_CONTEXT_PENDING_USER in self.executor.plan.context:
pending_user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
form.fields["username"].initial = pending_user.username
return form
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
kwargs["primary_action"] = _("Log in")
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)
if recovery_flow.exists():
kwargs["recovery_flow"] = recovery_flow.first()

View File

@ -9,6 +9,7 @@ from django.test import Client, TestCase
from django.utils.encoding import force_text
from passbook.core.models import User
from passbook.flows.markers import StageMarker
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from passbook.flows.views import SESSION_KEY_PLAN
@ -43,7 +44,9 @@ class TestPasswordStage(TestCase):
def test_without_user(self):
"""Test without user"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
@ -68,7 +71,9 @@ class TestPasswordStage(TestCase):
designation=FlowDesignation.RECOVERY, slug="qewrqerqr"
)
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
@ -83,7 +88,9 @@ class TestPasswordStage(TestCase):
def test_valid_password(self):
"""Test with a valid pending user and valid password"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
@ -105,7 +112,9 @@ class TestPasswordStage(TestCase):
def test_invalid_password(self):
"""Test with a valid pending user and invalid password"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
@ -127,7 +136,9 @@ class TestPasswordStage(TestCase):
def test_permission_denied(self):
"""Test with a valid pending user and valid password.
Backend is patched to return PermissionError"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan

View File

@ -0,0 +1,33 @@
# Generated by Django 3.0.7 on 2020-06-15 16:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_stages_prompt", "0002_auto_20200528_2059"),
]
operations = [
migrations.AlterField(
model_name="prompt",
name="type",
field=models.CharField(
choices=[
("text", "Text"),
("username", "Username"),
("e-mail", "Email"),
("password", "Password"),
("number", "Number"),
("checkbox", "Checkbox"),
("data", "Date"),
("data-time", "Date Time"),
("separator", "Separator"),
("hidden", "Hidden"),
("static", "Static"),
],
max_length=100,
),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 3.0.7 on 2020-06-18 17:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_stages_prompt", "0003_auto_20200615_1641"),
]
operations = [
migrations.AlterField(
model_name="prompt",
name="type",
field=models.CharField(
choices=[
("text", "Text"),
("username", "Username"),
("email", "Email"),
("password", "Password"),
("number", "Number"),
("checkbox", "Checkbox"),
("data", "Date"),
("data-time", "Date Time"),
("separator", "Separator"),
("hidden", "Hidden"),
("static", "Static"),
],
max_length=100,
),
),
]

View File

@ -12,8 +12,11 @@ from passbook.policies.models import PolicyBindingModel
class FieldTypes(models.TextChoices):
"""Field types an Prompt can be"""
# Simple text field
TEXT = "text"
EMAIL = "e-mail"
# Same as text, but has autocomplete for password managers
USERNAME = "username"
EMAIL = "email"
PASSWORD = "password" # noqa # nosec
NUMBER = "number"
CHECKBOX = "checkbox"
@ -52,8 +55,11 @@ class Prompt(models.Model):
}
if self.type == FieldTypes.EMAIL:
field_class = forms.EmailField
if self.type == FieldTypes.USERNAME:
attrs["autocomplete"] = "username"
if self.type == FieldTypes.PASSWORD:
widget = forms.PasswordInput(attrs=attrs)
attrs["autocomplete"] = "new-password"
if self.type == FieldTypes.NUMBER:
field_class = forms.IntegerField
widget = forms.NumberInput(attrs=attrs)
@ -64,6 +70,10 @@ class Prompt(models.Model):
if self.type == FieldTypes.CHECKBOX:
field_class = forms.CheckboxInput
kwargs["required"] = False
if self.type == FieldTypes.DATE:
field_class = forms.DateInput
if self.type == FieldTypes.DATE_TIME:
field_class = forms.DateTimeInput
# TODO: Implement static
# TODO: Implement separator

View File

@ -6,6 +6,7 @@ from django.test import Client, TestCase
from django.utils.encoding import force_text
from passbook.core.models import User
from passbook.flows.markers import StageMarker
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.flows.planner import FlowPlan
from passbook.flows.views import SESSION_KEY_PLAN
@ -96,7 +97,9 @@ class TestPromptStage(TestCase):
def test_render(self):
"""Test render of form, check if all prompts are rendered correctly"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
@ -114,7 +117,9 @@ class TestPromptStage(TestCase):
def test_valid_form_with_policy(self) -> PromptForm:
"""Test form validation"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
expr = "return request.context['password_prompt'] == request.context['password2_prompt']"
expr_policy = ExpressionPolicy.objects.create(
name="validate-form", expression=expr
@ -126,7 +131,9 @@ class TestPromptStage(TestCase):
def test_invalid_form(self) -> PromptForm:
"""Test form validation"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
expr = "False"
expr_policy = ExpressionPolicy.objects.create(
name="validate-form", expression=expr
@ -138,7 +145,9 @@ class TestPromptStage(TestCase):
def test_valid_form_request(self):
"""Test a request with valid form data"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()

View File

@ -4,6 +4,7 @@ from django.test import Client, TestCase
from django.utils.encoding import force_text
from passbook.core.models import User
from passbook.flows.markers import StageMarker
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from passbook.flows.views import SESSION_KEY_PLAN
@ -29,7 +30,9 @@ class TestUserDeleteStage(TestCase):
def test_no_user(self):
"""Test without user set"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
@ -47,7 +50,9 @@ class TestUserDeleteStage(TestCase):
def test_user_delete_get(self):
"""Test Form render"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
@ -62,7 +67,9 @@ class TestUserDeleteStage(TestCase):
def test_user_delete_post(self):
"""Test User delete (actual)"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan

View File

@ -4,6 +4,7 @@ from django.test import Client, TestCase
from django.utils.encoding import force_text
from passbook.core.models import User
from passbook.flows.markers import StageMarker
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from passbook.flows.views import SESSION_KEY_PLAN
@ -30,7 +31,9 @@ class TestUserLoginStage(TestCase):
def test_valid_password(self):
"""Test with a valid pending user and backend"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
plan.context[
PLAN_CONTEXT_AUTHENTICATION_BACKEND
@ -53,7 +56,9 @@ class TestUserLoginStage(TestCase):
def test_without_user(self):
"""Test a plan without any pending user, resulting in a denied"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
@ -72,7 +77,9 @@ class TestUserLoginStage(TestCase):
def test_without_backend(self):
"""Test a plan with pending user, without backend, resulting in a denied"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan

View File

@ -4,6 +4,7 @@ from django.test import Client, TestCase
from django.utils.encoding import force_text
from passbook.core.models import User
from passbook.flows.markers import StageMarker
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from passbook.flows.views import SESSION_KEY_PLAN
@ -30,7 +31,9 @@ class TestUserLogoutStage(TestCase):
def test_valid_password(self):
"""Test with a valid pending user and backend"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
plan.context[
PLAN_CONTEXT_AUTHENTICATION_BACKEND

View File

@ -7,6 +7,7 @@ from django.test import Client, TestCase
from django.utils.encoding import force_text
from passbook.core.models import User
from passbook.flows.markers import StageMarker
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from passbook.flows.views import SESSION_KEY_PLAN
@ -37,7 +38,9 @@ class TestUserWriteStage(TestCase):
for _ in range(8)
)
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PROMPT] = {
"username": "test-user",
"name": "name",
@ -71,7 +74,9 @@ class TestUserWriteStage(TestCase):
SystemRandom().choice(string.ascii_uppercase + string.digits)
for _ in range(8)
)
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = User.objects.create(
username="unittest", email="test@beryju.org"
)
@ -104,7 +109,9 @@ class TestUserWriteStage(TestCase):
def test_without_data(self):
"""Test without data results in error"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
plan = FlowPlan(
flow_pk=self.flow.pk.hex, stages=[self.stage], markers=[StageMarker()]
)
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()

View File

@ -1,8 +1,8 @@
{
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",
"@patternfly/patternfly": "^2.71.6",
"@fortawesome/fontawesome-free": "^5.13.1",
"@patternfly/patternfly": "^4.10.31",
"codemirror": "^5.54.0"
}
}

View File

@ -0,0 +1,2 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="120px" height="20px" viewBox="15 0 10 10" enable-background="new 0 0 270 10" xml:space="preserve"><defs><style>.cls-1{isolation:isolate;}.cls-2{fill:#000;}</style></defs><g class="cls-1"><path class="cls-2" d="M1.65,11V2.45H2.87V3a2.81,2.81,0,0,1,.47-.45A1.13,1.13,0,0,1,4,2.38,1.11,1.11,0,0,1,5.1,3a1.55,1.55,0,0,1,.16.5,5.61,5.61,0,0,1,0,.81V6.58c0,.45,0,.77,0,1a1.17,1.17,0,0,1-.55.9,1.23,1.23,0,0,1-.7.16,1.35,1.35,0,0,1-.64-.16A1.53,1.53,0,0,1,2.89,8h0v3ZM4.08,4.43a1.21,1.21,0,0,0-.14-.6.51.51,0,0,0-.46-.22A.54.54,0,0,0,3,3.82a.8.8,0,0,0-.17.54V6.73A.68.68,0,0,0,3,7.2a.6.6,0,0,0,.44.18A.53.53,0,0,0,4,7.17a1,1,0,0,0,.12-.5Z"/><path class="cls-2" d="M8.63,8.54V7.91h0a2.24,2.24,0,0,1-.48.52,1.13,1.13,0,0,1-.69.18A1.39,1.39,0,0,1,7,8.54a1.09,1.09,0,0,1-.43-.24,1.32,1.32,0,0,1-.33-.49A2.33,2.33,0,0,1,6.11,7a4.89,4.89,0,0,1,.08-.91,1.51,1.51,0,0,1,.31-.65,1.44,1.44,0,0,1,.59-.38A3.19,3.19,0,0,1,8,4.93h.59V4.33a1,1,0,0,0-.13-.52A.52.52,0,0,0,8,3.61a.71.71,0,0,0-.44.15.78.78,0,0,0-.26.46H6.13A2,2,0,0,1,6.69,2.9a1.73,1.73,0,0,1,.57-.38A2,2,0,0,1,8,2.38a2.18,2.18,0,0,1,.72.12,1.71,1.71,0,0,1,.59.36,2,2,0,0,1,.38.6,2.18,2.18,0,0,1,.14.84V8.54Zm0-2.62-.34,0a1.2,1.2,0,0,0-.67.18.76.76,0,0,0-.29.68.89.89,0,0,0,.17.56A.55.55,0,0,0,8,7.53a.63.63,0,0,0,.49-.2.91.91,0,0,0,.17-.58Z"/><path class="cls-2" d="M13,4.16a.59.59,0,0,0-.2-.47.65.65,0,0,0-.42-.16.59.59,0,0,0-.45.19.66.66,0,0,0-.15.43.8.8,0,0,0,.08.33.85.85,0,0,0,.44.29l.71.29a1.73,1.73,0,0,1,.95.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.56,1.56,0,0,1-.58.39,1.88,1.88,0,0,1-2-.32,1.58,1.58,0,0,1-.4-.57,1.81,1.81,0,0,1-.17-.8h1.15a1.11,1.11,0,0,0,.17.47.56.56,0,0,0,.49.22.71.71,0,0,0,.47-.18A.59.59,0,0,0,13,6.8a.69.69,0,0,0-.13-.43,1.08,1.08,0,0,0-.48-.32l-.59-.21a2.08,2.08,0,0,1-.9-.64,1.66,1.66,0,0,1-.33-1,1.89,1.89,0,0,1,.14-.72,1.78,1.78,0,0,1,.4-.57,1.5,1.5,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.6,1.6,0,0,1,.54.38,1.85,1.85,0,0,1,.36.57,1.82,1.82,0,0,1,.13.7Z"/><path class="cls-2" d="M17.2,4.16a.63.63,0,0,0-.2-.47.69.69,0,0,0-.43-.16.55.55,0,0,0-.44.19.62.62,0,0,0-.16.43.68.68,0,0,0,.09.33.81.81,0,0,0,.43.29l.72.29a1.7,1.7,0,0,1,.94.72,2,2,0,0,1,.26,1,1.85,1.85,0,0,1-.52,1.3,1.61,1.61,0,0,1-.57.39,1.81,1.81,0,0,1-.74.15,1.76,1.76,0,0,1-1.24-.47,1.61,1.61,0,0,1-.41-.57,2,2,0,0,1-.17-.8h1.15a1.12,1.12,0,0,0,.18.47.53.53,0,0,0,.48.22.72.72,0,0,0,.48-.18.59.59,0,0,0,.21-.48.69.69,0,0,0-.14-.43,1,1,0,0,0-.48-.32l-.58-.21a2.06,2.06,0,0,1-.91-.64,1.66,1.66,0,0,1-.33-1A1.89,1.89,0,0,1,15,3.44a1.78,1.78,0,0,1,.4-.57,1.58,1.58,0,0,1,.56-.36,1.82,1.82,0,0,1,.7-.13,1.93,1.93,0,0,1,.69.13,1.75,1.75,0,0,1,.55.38,1.85,1.85,0,0,1,.36.57,2,2,0,0,1,.13.7Z"/><path class="cls-2" d="M19.2,8.54V0h1.22V3h0a1.53,1.53,0,0,1,.48-.47,1.39,1.39,0,0,1,.65-.16,1.26,1.26,0,0,1,.69.16,1.35,1.35,0,0,1,.4.39,1.18,1.18,0,0,1,.15.51,7.72,7.72,0,0,1,0,1V6.73a5.56,5.56,0,0,1-.05.8,1.56,1.56,0,0,1-.15.5,1.12,1.12,0,0,1-1.07.58,1.15,1.15,0,0,1-.7-.18A3.79,3.79,0,0,1,20.42,8v.55Zm2.44-4.21a1,1,0,0,0-.13-.51A.5.5,0,0,0,21,3.61a.57.57,0,0,0-.44.18.66.66,0,0,0-.18.48V6.63a.83.83,0,0,0,.17.54.52.52,0,0,0,.45.21.49.49,0,0,0,.45-.22,1.11,1.11,0,0,0,.15-.6Z"/><path class="cls-2" d="M23.76,4.49a4.83,4.83,0,0,1,0-.68A1.55,1.55,0,0,1,24,3.26a1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24A1.59,1.59,0,0,1,24,7.73a1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1,0-.68ZM25,6.69a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17A.55.55,0,0,0,26,7.21a.72.72,0,0,0,.16-.52V4.3A.74.74,0,0,0,26,3.78a.55.55,0,0,0-.44-.17.53.53,0,0,0-.43.17A.74.74,0,0,0,25,4.3Z"/><path class="cls-2" d="M28.2,4.49a4.83,4.83,0,0,1,.05-.68,1.55,1.55,0,0,1,.18-.55,1.59,1.59,0,0,1,.62-.64,1.84,1.84,0,0,1,1-.24,1.87,1.87,0,0,1,1,.24,1.59,1.59,0,0,1,.62.64,1.55,1.55,0,0,1,.18.55,4.83,4.83,0,0,1,.05.68v2a4.72,4.72,0,0,1-.05.68,1.55,1.55,0,0,1-.18.55,1.59,1.59,0,0,1-.62.64,1.87,1.87,0,0,1-1,.24,1.84,1.84,0,0,1-1-.24,1.59,1.59,0,0,1-.62-.64,1.55,1.55,0,0,1-.18-.55,4.72,4.72,0,0,1-.05-.68Zm1.22,2.2a.72.72,0,0,0,.17.52.53.53,0,0,0,.43.17.55.55,0,0,0,.44-.17.72.72,0,0,0,.16-.52V4.3a.74.74,0,0,0-.16-.52A.55.55,0,0,0,30,3.61a.53.53,0,0,0-.43.17.74.74,0,0,0-.17.52Z"/><path class="cls-2" d="M32.75,8.54V0H34V5.11h0l1.47-2.66H36.7L35.24,4.93,37,8.54H35.66l-1.1-2.63L34,6.83V8.54Z"/></g></svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -198,120 +198,6 @@ input[data-is-monospace] {
font-family: monospace;
}
.ws-page-header {
background-color: #151515;
min-height: auto
}
@media (min-width: 992px) {
.ws-page-header .pf-c-page__header-nav {
margin-left:12px
}
}
.ws-page-header .pf-c-nav__scroll-button {
outline-offset: -4px;
height: 100%;
top: 0
}
.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__item {
margin-right: 0
}
.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link {
padding-top: 22px;
padding-right: var(--pf-global--spacer--md);
padding-left: var(--pf-global--spacer--md);
color: var(--pf-global--Color--light-100)
}
@media (max-width: 991px) {
.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link {
padding-top:10px
}
}
.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link:after {
top: 0!important;
height: 4px
}
.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link:active,.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link:hover {
-webkit-transition: .5s;
transition: .5s
}
.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link.pf-m-current,.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link:active,.ws-page-header .pf-c-nav__horizontal-list .pf-c-nav__link:hover {
background-color: var(--pf-global--BackgroundColor--light-100);
color: #151515!important;
font-weight: var(--pf-global--FontWeight--normal)
}
.ws-page-header li a:after {
content: "";
position: absolute;
left: 50%!important;
bottom: 0;
-webkit-transform: translateX(-50%) scaleX(0);
transform: translateX(-50%) scaleX(0);
-webkit-transform-origin: 50% 50%;
transform-origin: 50% 50%;
width: 100%;
height: 1px;
background-color: var(--pf-global--BackgroundColor--light-100);
color: #151515!important;
-webkit-transition: -webkit-transform .25s;
transition: -webkit-transform .25s;
transition: transform .25s;
transition: transform .25s,-webkit-transform .25s
}
.ws-page-header li a:hover:after {
-webkit-transform: translateX(-50%) scaleX(1);
transform: translateX(-50%) scaleX(1)
}
.ws-page-header li a.pf-m-current:after {
left: 0!important;
-webkit-transform: none;
transform: none
}
.ws-page-sidebar#page-sidebar {
color: #fff;
box-shadow: none
}
.ws-page-sidebar .pf-c-nav {
margin-top: 16px
}
.pf-site-search {
padding: 0 0 2px;
width: 150px;
background: transparent;
-webkit-transition: .25s;
transition: .25s
}
.ws-page-header .pf-c-page__header-brand-toggle {
display: none;
visibility: hidden
}
@media (max-width: 768px) {
.pf-site-search {
width:100px
}
.ws-page-header .pf-c-page__header-brand-toggle {
display: block;
visibility: visible
}
}
/* Form with user */
.form-control-static {
display: flex;

View File

@ -2,15 +2,15 @@
# yarn lockfile v1
"@fortawesome/fontawesome-free@^5.13.0":
version "5.13.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz#fcb113d1aca4b471b709e8c9c168674fbd6e06d9"
integrity sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg==
"@fortawesome/fontawesome-free@^5.13.1":
version "5.13.1"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.1.tgz#c53b4066edae16cd1fd669f687baf031b45fb9d6"
integrity sha512-D819f34FLHeBN/4xvw0HR0u7U2G7RqjPSggXqf7LktsxWQ48VAfGwvMrhcVuaZV2fF069c/619RdgCCms0DHhw==
"@patternfly/patternfly@^2.71.6":
version "2.71.6"
resolved "https://registry.yarnpkg.com/@patternfly/patternfly/-/patternfly-2.71.6.tgz#6385cbd5aaca2f59bf65496e0189c541a7f00a82"
integrity sha512-mqqtuCVa+/FbyyK8hSAcfEIwNX73+zbnzHpmC4NrW0kyMzSszPtBqev/ZO79ZxGqZUpLOyUBTVaH7oKn8cL35Q==
"@patternfly/patternfly@^4.10.31":
version "4.10.31"
resolved "https://registry.yarnpkg.com/@patternfly/patternfly/-/patternfly-4.10.31.tgz#742852b69d90bb2efe304130f7226d2e356306cf"
integrity sha512-UxdZ/apWRowXYZ5qPz5LPfXwyB4YGpomrCJPX7c36+Zg8jFpYyVqgVYainL8Yf/GrChtC2LKyoHg7UUTtMtp4A==
codemirror@^5.54.0:
version "5.54.0"