Compare commits

..

54 Commits

Author SHA1 Message Date
6ed2e137a2 new release: 0.8.12-beta 2020-02-28 11:54:03 +01:00
45bd63c720 api: update old field names 2020-02-28 11:48:55 +01:00
736e13fc35 ui: add template for csrf errors 2020-02-28 11:41:28 +01:00
966fff008c ui: re-enable branding on navbar 2020-02-28 11:37:07 +01:00
64f15eadbd providers/saml: fix CSRF errors with POST binding 2020-02-28 10:50:16 +01:00
81b66ecdcd core: remove some more dead code, add more help texts for factors 2020-02-27 16:39:30 +01:00
53e5cf7826 admin: fix some models not being paginated 2020-02-27 15:30:28 +01:00
82654b3fd9 ui: re-organize some of the navigation to make it cleaner for end-users 2020-02-27 14:59:34 +01:00
9b72c604dd docs: fix some typos 2020-02-27 13:00:55 +01:00
5fb1b8044c new release: 0.8.11-beta 2020-02-25 11:38:50 +01:00
b8daab4377 providers/saml: fix AccessRequiredView.dispatch not being called 2020-02-25 11:38:26 +01:00
c5b91bdae8 providers/saml: fix CannotHandleAssertion Error still being sent to sentry 2020-02-24 19:14:43 +01:00
39a208c55f providers/saml: fix wrong key being used for params 2020-02-24 17:48:03 +01:00
a5bfef9b6b providers/saml: fix leftover data in session, fix IdP initiated login
move can_handle calls to binding endpoints (/login/ and /login/initiate/), so that /login/authorize/ works either way, can clean up the session and audit
2020-02-24 17:34:52 +01:00
f1f4cbef9b lib/sentry: fix SentryIgnoredException not being ignored correctly 2020-02-24 17:01:31 +01:00
8388120b06 new release: 0.8.10-beta 2020-02-24 15:30:57 +01:00
2bf96828f1 root: fix logging.basicConfig being called by pyjwkest 2020-02-24 15:30:28 +01:00
22838e66fe providers/saml: fix users being able to authenticate without audit logs being created 2020-02-24 14:40:12 +01:00
484dd6de09 providers/oidc: add error template 2020-02-24 14:19:02 +01:00
b743736c26 lib/logging: fix typo 2020-02-24 14:10:58 +01:00
af91e2079b core: sort provider by pk when selection application provider 2020-02-24 14:10:51 +01:00
cad1c17f14 helm: fix inconsistent labels 2020-02-24 13:49:42 +01:00
120d32e4dc new release: 0.8.9-beta 2020-02-24 13:23:20 +01:00
238b489e07 root: add process ID to logging output 2020-02-24 13:20:32 +01:00
4daa70c894 core: fix saving of policy not correctly clearing it's cache 2020-02-24 13:15:52 +01:00
f8599438df ui: fix lists not being rendered correctly 2020-02-24 13:13:42 +01:00
155c9a4c3f ui: update remaining forms, completely remove jQuery 2020-02-24 13:13:28 +01:00
8433b5e583 ui: fix automatic slug generation 2020-02-24 12:40:16 +01:00
dc5ba144f1 ui: fix height of multiple select input 2020-02-24 12:40:06 +01:00
521a8b5356 ui: update more remaining templates 2020-02-23 22:49:56 +01:00
3453077d7b root: set SameSite to None when debugging 2020-02-23 22:49:33 +01:00
70ede8581a core: sort sources on login view 2020-02-23 20:19:01 +01:00
6e9d297f02 deploy: use new bootstrap command 2020-02-23 20:12:48 +01:00
6a7545fd43 lib: add bootstrap command 2020-02-23 19:52:41 +01:00
a8926cbd07 lib: add more errors to sentry ignore 2020-02-23 19:48:14 +01:00
64d7b009ab sources/oauth: fix invalid headers, fix invalid function signature 2020-02-23 19:42:57 +01:00
2b5fddb7bf policies: add unittests for evaluator 2020-02-23 15:54:26 +01:00
b99d23c119 all: remove dead code 2020-02-23 15:32:20 +01:00
03905b74ff admin: exclude anonymous user from listing 2020-02-23 15:27:28 +01:00
6b8a59cfbd admin: show prettified yaml 2020-02-23 15:27:11 +01:00
d6fdcd3ef9 ui: re-add automatic slug generation 2020-02-23 15:20:41 +01:00
53ebc551d2 ui: fix icon sizing on login 2020-02-23 15:13:18 +01:00
3d4f43d6e3 ui: show default icon for source without icon 2020-02-23 15:09:58 +01:00
074cde7cd5 audit: save model's name or string representation 2020-02-23 15:04:30 +01:00
382e563590 new release: 0.8.8-beta 2020-02-23 14:45:45 +01:00
ca61a7cc21 audit: don't pop password as its censored already 2020-02-23 14:43:33 +01:00
fa2870afe0 sources: remove policies as they are not used currently 2020-02-23 14:40:06 +01:00
0f46207ea4 admin: fix provider list not having pagination 2020-02-23 14:29:21 +01:00
1e7d912144 actions: build :<branch-name> on push 2020-02-23 13:54:19 +01:00
f4a676e2fb sources/oauth: slugify provider type instead of just lowercase 2020-02-23 13:53:16 +01:00
b2c10e2387 ui: add missing discord and twitter icons 2020-02-23 13:47:21 +01:00
8c329dca7d core: add migration to fix null fields in core.application 2020-02-22 19:26:34 +01:00
83da175749 policies/expression: add pb_client_ip field 2020-02-22 19:26:16 +01:00
995c87938f core: fix default Null causing issues in translation 2020-02-21 23:10:00 +01:00
90 changed files with 1091 additions and 693 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.8.7-beta
current_version = 0.8.12-beta
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)

View File

@ -145,3 +145,73 @@ jobs:
run: pip install -U pip pipenv && pipenv install --dev
- name: Run coverage
run: pipenv run ./scripts/coverage.sh
# Build
build-server:
needs:
- migrations
- coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Docker Login Registry
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image
run: docker build
--no-cache
-t beryju/passbook:${GITHUB_REF##*/}
-f Dockerfile .
- name: Push Docker Container to Registry
run: docker push beryju/passbook:${GITHUB_REF##*/}
build-gatekeeper:
needs:
- migrations
- coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Docker Login Registry
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image
run: |
cd gatekeeper
docker build \
--no-cache \
-t beryju/passbook-gatekeeper:${GITHUB_REF##*/} \
-f Dockerfile .
- name: Push Docker Container to Registry
run: docker push beryju/passbook-gatekeeper:${GITHUB_REF##*/}
build-static:
needs:
- migrations
- coverage
runs-on: ubuntu-latest
services:
postgres:
image: postgres:latest
env:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
redis:
image: redis:latest
steps:
- uses: actions/checkout@v1
- name: Docker Login Registry
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image
run: docker build
--no-cache
--network=$(docker network ls | grep github | awk '{print $1}')
-t beryju/passbook-static:${GITHUB_REF##*/}
-f static.Dockerfile .
- name: Push Docker Container to Registry
run: docker push beryju/passbook-static:${GITHUB_REF##*/}

View File

@ -16,11 +16,11 @@ jobs:
- name: Building Docker Image
run: docker build
--no-cache
-t beryju/passbook:0.8.7-beta
-t beryju/passbook:0.8.12-beta
-t beryju/passbook:latest
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook:0.8.7-beta
run: docker push beryju/passbook:0.8.12-beta
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook:latest
build-gatekeeper:
@ -37,11 +37,11 @@ jobs:
cd gatekeeper
docker build \
--no-cache \
-t beryju/passbook-gatekeeper:0.8.7-beta \
-t beryju/passbook-gatekeeper:0.8.12-beta \
-t beryju/passbook-gatekeeper:latest \
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-gatekeeper:0.8.7-beta
run: docker push beryju/passbook-gatekeeper:0.8.12-beta
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-gatekeeper:latest
build-static:
@ -66,11 +66,11 @@ jobs:
run: docker build
--no-cache
--network=$(docker network ls | grep github | awk '{print $1}')
-t beryju/passbook-static:0.8.7-beta
-t beryju/passbook-static:0.8.12-beta
-t beryju/passbook-static:latest
-f static.Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-static:0.8.7-beta
run: docker push beryju/passbook-static:0.8.12-beta
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-static:latest
test-release:

6
.isort.cfg Normal file
View File

@ -0,0 +1,6 @@
[settings]
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88

View File

@ -7,7 +7,3 @@ const-rgx=[a-zA-Z0-9_]{1,40}$
ignored-modules=django-otp
jobs=4
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=20

View File

@ -23,6 +23,8 @@ services:
server:
image: beryju/passbook:${SERVER_TAG:-latest}
command:
- ./manage.py
- bootstrap
- uwsgi
- uwsgi.ini
environment:
@ -42,6 +44,8 @@ services:
worker:
image: beryju/passbook:${SERVER_TAG:-latest}
command:
- ./manage.py
- bootstrap
- celery
- worker
- --autoscale=10,3

View File

@ -12,6 +12,8 @@ The following objects are passed into the variable:
- `request.obj`: A Django Model instance. This is only set if the Policy is ran against an object.
- `pb_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external Provider.
- `pb_is_group_member(user, group_name)`: Function which checks if `user` is member of a Group with Name `gorup_name`.
- `pb_logger`: Standard Python Logger Object, which can be used to debug expressions.
- `pb_client_ip`: Client's IP Address.
There are also the following custom filters available:

View File

@ -1,6 +1,6 @@
apiVersion: v1
appVersion: "0.8.7-beta"
appVersion: "0.8.12-beta"
description: A Helm chart for passbook.
name: passbook
version: "0.8.7-beta"
version: "0.8.12-beta"
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png

View File

@ -18,7 +18,7 @@ spec:
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
passbook.io/component: web
k8s.passbook.io/component: web
spec:
volumes:
- name: config-volume
@ -27,9 +27,12 @@ spec:
initContainers:
- name: passbook-database-migrations
image: "beryju/passbook:{{ .Values.image.tag }}"
imagePullPolicy: Always
command:
- ./manage.py
args:
- bootstrap
- ./manage.py
- migrate
volumeMounts:
- mountPath: /etc/passbook
@ -57,10 +60,12 @@ spec:
containers:
- name: {{ .Chart.Name }}
image: "beryju/passbook:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
imagePullPolicy: Always
command:
- uwsgi
- ./manage.py
args:
- bootstrap
- uwsgi
- uwsgi.ini
volumeMounts:
- mountPath: /etc/passbook

View File

@ -18,4 +18,4 @@ spec:
selector:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
passbook.io/component: web
k8s.passbook.io/component: web

View File

@ -18,7 +18,7 @@ spec:
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
passbook.io/component: worker
k8s.passbook.io/component: worker
spec:
volumes:
- name: config-volume
@ -29,8 +29,10 @@ spec:
image: "beryju/passbook:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
command:
- celery
- ./manage.py
args:
- bootstrap
- celery
- worker
- --autoscale=10,3
- -E

View File

@ -2,7 +2,7 @@
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
image:
tag: 0.8.7-beta
tag: 0.8.12-beta
nameOverride: ""

View File

@ -27,7 +27,7 @@ nav:
- Sentry: integrations/services/sentry/index.md
- Ansible Tower/AWX: integrations/services/tower-awx/index.md
repo_name: "BeryJu.org/passbook"
repo_name: "BeryJu/passbook"
repo_url: https://github.com/BeryJu/passbook
theme:
name: "material"

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = "0.8.7-beta"
__version__ = "0.8.12-beta"

View File

@ -48,7 +48,7 @@ class YAMLField(forms.CharField):
def prepare_value(self, value):
if isinstance(value, InvalidYAMLInput):
return value
return yaml.dump(value, explicit_start=True)
return yaml.dump(value, explicit_start=True, default_flow_style=False)
def has_changed(self, initial, data):
if super().has_changed(initial, data):

View File

@ -1,40 +0,0 @@
"""passbook form helpers"""
from django import forms
from passbook.admin.fields import YAMLField
class TagModelForm(forms.ModelForm):
"""Base form for models that have attributes"""
def __init__(self, *args, **kwargs):
# Check if we have an instance, load tags otherwise use an empty dict
instance = kwargs.get("instance", None)
tags = instance.tags if instance else {}
# Make sure all predefined tags exist in tags, and set default if they don't
predefined_tags = (
self._meta.model().get_predefined_tags() # pylint: disable=no-member
)
for key, value in predefined_tags.items():
if key not in tags:
tags[key] = value
# Format JSON
kwargs["initial"]["tags"] = tags
super().__init__(*args, **kwargs)
def clean_tags(self):
"""Make sure all required tags are set"""
if hasattr(self.instance, "get_required_keys") and hasattr(
self.instance, "tags"
):
for key in self.instance.get_required_keys():
if key not in self.cleaned_data.get("tags"):
raise forms.ValidationError("Tag %s missing." % key)
return self.cleaned_data.get("tags")
# pylint: disable=too-few-public-methods
class TagModelFormMeta:
"""Base Meta class that uses the YAMLField"""
field_classes = {"tags": YAMLField}

View File

@ -1,7 +1,4 @@
"""passbook core source form fields"""
# from django import forms
SOURCE_FORM_FIELDS = ["name", "slug", "enabled", "policies"]
SOURCE_SERIALIZER_FIELDS = ["pk", "name", "slug", "enabled", "policies"]
# class SourceForm(forms.Form)
SOURCE_FORM_FIELDS = ["name", "slug", "enabled"]
SOURCE_SERIALIZER_FIELDS = ["pk", "name", "slug", "enabled"]

View File

@ -1,66 +1,68 @@
{% extends "administration/base.html" %}
{% extends "base/page.html" %}
{% load i18n %}
{% load utils %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon pf-icon-catalog"></i>
{% trans 'Audit Log' %}
</h1>
</div>
</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' %}
{% block page_content %}
<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon pf-icon-catalog"></i>
{% trans 'Audit Log' %}
</h1>
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Action' %}</th>
<th role="columnheader" scope="col">{% trans 'Context' %}</th>
<th role="columnheader" scope="col">{% trans 'User' %}</th>
<th role="columnheader" scope="col">{% trans 'Creation Date' %}</th>
<th role="columnheader" scope="col">{% trans 'Client IP' %}</th>
</tr>
</thead>
<tbody role="rowgroup">
{% for entry in object_list %}
<tr role="row">
<th role="columnheader">
<div>
<div>{{ entry.action }}</div>
<small>{{ entry.app|default:'-' }}</small>
</div>
</th>
<td role="cell">
<code>{{ entry.context }}</code>
</td>
<td role="cell">
<span>
{{ entry.user }}
</span>
</td>
<td role="cell">
<span>
{{ entry.created }}
</span>
</td>
<td role="cell">
<span>
{{ entry.client_ip }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
{% include 'partials/pagination.html' %}
</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>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Action' %}</th>
<th role="columnheader" scope="col">{% trans 'Context' %}</th>
<th role="columnheader" scope="col">{% trans 'User' %}</th>
<th role="columnheader" scope="col">{% trans 'Creation Date' %}</th>
<th role="columnheader" scope="col">{% trans 'Client IP' %}</th>
</tr>
</thead>
<tbody role="rowgroup">
{% for entry in object_list %}
<tr role="row">
<th role="columnheader">
<div>
<div>{{ entry.action }}</div>
<small>{{ entry.app|default:'-' }}</small>
</div>
</th>
<td role="cell">
<code>{{ entry.context }}</code>
</td>
<td role="cell">
<span>
{{ entry.user }}
</span>
</td>
<td role="cell">
<span>
{{ entry.created }}
</span>
</td>
<td role="cell">
<span>
{{ entry.client_ip }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
{% include 'partials/pagination.html' %}
</div>
</div>
</div>
</section>
</section>
</main>
{% endblock %}

View File

@ -1,6 +1,10 @@
{% extends "overview/base.html" %}
{% extends "base/page.html" %}
{% load static %}
{% load i18n %}
{% load is_active %}
{% load utils %}
{% block head %}
{{ block.super }}
@ -12,3 +16,78 @@
<script src="{% static 'node_modules/codemirror/mode/yaml/yaml.js' %}"></script>
<script src="{% static 'node_modules/codemirror/mode/jinja2/jinja2.js' %}"></script>
{% endblock %}
{% block page_content %}
<div class="pf-c-page__sidebar">
<div class="pf-c-page__sidebar-body">
<nav class="pf-c-nav" id="page-default-nav-example-primary-nav" aria-label="Global">
<ul class="pf-c-nav__list">
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:overview' %}"
class="pf-c-nav__link {% is_active_url 'passbook_admin:overview' %}">
{% trans 'System Status' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:applications' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:applications' 'passbook_admin:application-create' 'passbook_admin:application-update' 'passbook_admin:application-delete' %}">
{% trans 'Applications' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:sources' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:sources' 'passbook_admin:source-create' 'passbook_admin:source-update' 'passbook_admin:source-delete' %}">
{% trans 'Sources' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:providers' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:providers' 'passbook_admin:provider-create' 'passbook_admin:provider-update' 'passbook_admin:provider-delete' %}">
{% trans 'Providers' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:property-mappings' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:property-mappings' 'passbook_admin:property-mapping-create' 'passbook_admin:property-mapping-update' 'passbook_admin:property-mapping-delete' %}">
{% trans 'Property Mappings' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:factors' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:factors' 'passbook_admin:factor-create' 'passbook_admin:factor-update' 'passbook_admin:factor-delete' %}">
{% trans 'Factors' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:policies' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:policies' 'passbook_admin:policy-create' 'passbook_admin:policy-update' 'passbook_admin:policy-delete' 'passbook_admin:policy-test' %}">
{% trans 'Policies' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:invitations' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:invitations' 'passbook_admin:invitation-create' 'passbook_admin:invitation-update' 'passbook_admin:invitation-delete' 'passbook_admin:invitation-test' %}">
{% trans 'Invitations' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:users' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:users' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
{% trans 'Users' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:groups' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:groups' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
{% trans 'Groups' %}
</a>
</li>
</ul>
</nav>
</div>
</div>
<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
{% block content %}
{% endblock %}
</main>
{% endblock %}

View File

@ -11,16 +11,22 @@
{% endblock %}
{% block beneath_form %}
<p class="loading" style="display: none;">
<span class="spinner spinner-xs spinner-inline"></span> {% trans 'Processing, please wait...' %}
</p>
<div class="pf-c-form__group pf-m-action" style="display: none;" id="loading">
<div class="pf-c-form__horizontal-group">
<span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ block.super }}
<script>
$('form').on('submit', function () {
$('p.loading').show();
})
document.querySelector("form").addEventListener("submit", (e) => {
document.getElementById("loading").removeAttribute("style");
});
</script>
{% endblock %}

View File

@ -12,4 +12,4 @@ class EventListView(PermissionListMixin, ListView):
template_name = "administration/audit/list.html"
permission_required = "passbook_audit.view_event"
ordering = "-created"
paginate_by = 10
paginate_by = 20

View File

@ -23,6 +23,8 @@ class InvitationListView(LoginRequiredMixin, PermissionListMixin, ListView):
model = Invitation
permission_required = "passbook_core.view_invitation"
template_name = "administration/invitation/list.html"
paginate_by = 10
ordering = "-expires"
class InvitationCreateView(

View File

@ -24,6 +24,7 @@ class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):
model = Policy
permission_required = "passbook_core.view_policy"
paginate_by = 10
ordering = "order"
template_name = "administration/policy/list.html"

View File

@ -22,6 +22,8 @@ class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView):
model = Provider
permission_required = "passbook_core.add_provider"
template_name = "administration/provider/list.html"
paginate_by = 10
ordering = "id"
def get_context_data(self, **kwargs):
kwargs["types"] = {

View File

@ -9,7 +9,11 @@ from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, DetailView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from guardian.mixins import (
PermissionListMixin,
PermissionRequiredMixin,
get_anonymous_user,
)
from passbook.admin.forms.users import UserForm
from passbook.core.models import Nonce, User
@ -25,6 +29,9 @@ class UserListView(LoginRequiredMixin, PermissionListMixin, ListView):
paginate_by = 40
template_name = "administration/user/list.html"
def get_queryset(self):
return super().get_queryset().exclude(pk=get_anonymous_user().pk)
class UserCreateView(
SuccessMessageMixin,

View File

@ -7,7 +7,7 @@ from rest_framework import routers
from structlog import get_logger
from passbook.api.permissions import CustomObjectPermissions
from passbook.audit.api.events import EventViewSet
from passbook.audit.api import EventViewSet
from passbook.core.api.applications import ApplicationViewSet
from passbook.core.api.factors import FactorViewSet
from passbook.core.api.groups import GroupViewSet

View File

@ -18,7 +18,7 @@ class EventSerializer(ModelSerializer):
"date",
"app",
"context",
"request_ip",
"client_ip",
"created",
]

View File

@ -33,11 +33,15 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
source[key] = sanitize_dict(value)
elif isinstance(value, models.Model):
model_content_type = ContentType.objects.get_for_model(value)
name = str(value)
if hasattr(value, "name"):
name = value.name
source[key] = sanitize_dict(
{
"app": model_content_type.app_label,
"name": model_content_type.model,
"model_name": model_content_type.model,
"pk": value.pk,
"name": name,
}
)
elif isinstance(value, UUID):
@ -133,6 +137,7 @@ class Event(UUIDModel):
action=self.action,
context=self.context,
client_ip=self.client_ip,
user=self.user,
)
return super().save(*args, **kwargs)

View File

@ -34,7 +34,6 @@ def on_user_login_failed(
sender, credentials: Dict[str, str], request: HttpRequest, **_
):
"""Failed Login"""
credentials.pop("password")
Event.new(EventAction.LOGIN_FAILED, **credentials).from_http(request)

View File

@ -17,7 +17,7 @@ class PropertyMappingSerializer(ModelSerializer):
class Meta:
model = PropertyMapping
fields = ["pk", "name", "__type__"]
fields = ["pk", "name", "expression", "__type__"]
class PropertyMappingViewSet(ReadOnlyModelViewSet):

View File

@ -10,7 +10,8 @@ class ApplicationForm(forms.ModelForm):
"""Application Form"""
provider = forms.ModelChoiceField(
queryset=Provider.objects.all().select_subclasses(), required=False
queryset=Provider.objects.all().order_by("pk").select_subclasses(),
required=False,
)
class Meta:

View File

@ -0,0 +1,33 @@
# Generated by Django 3.0.3 on 2020-02-21 22:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_core", "0009_auto_20200221_1410"),
]
operations = [
migrations.AlterField(
model_name="application",
name="meta_description",
field=models.TextField(blank=True, default=""),
),
migrations.AlterField(
model_name="application",
name="meta_icon_url",
field=models.TextField(blank=True, default=""),
),
migrations.AlterField(
model_name="application",
name="meta_launch_url",
field=models.URLField(blank=True, default=""),
),
migrations.AlterField(
model_name="application",
name="meta_publisher",
field=models.TextField(blank=True, default=""),
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 3.0.3 on 2020-02-22 18:22
from django.db import migrations
def fix_application_null(apps, schema_editor):
"""Fix Application meta_fields being null"""
Application = apps.get_model("passbook_core", "Application")
for app in Application.objects.all():
if app.meta_launch_url is None:
app.meta_launch_url = ""
if app.meta_icon_url is None:
app.meta_icon_url = ""
if app.meta_description is None:
app.meta_description = ""
if app.meta_publisher is None:
app.meta_publisher = ""
app.save()
class Migration(migrations.Migration):
dependencies = [
("passbook_core", "0010_auto_20200221_2208"),
]
operations = [migrations.RunPython(fix_application_null)]

View File

@ -139,10 +139,10 @@ class Application(ExportModelOperationsMixin("application"), PolicyModel):
"Provider", null=True, blank=True, default=None, on_delete=models.SET_DEFAULT
)
meta_launch_url = models.URLField(null=True, blank=True)
meta_icon_url = models.TextField(null=True, blank=True)
meta_description = models.TextField(null=True, blank=True)
meta_publisher = models.TextField(null=True, blank=True)
meta_launch_url = models.URLField(default="", blank=True)
meta_icon_url = models.TextField(default="", blank=True)
meta_description = models.TextField(default="", blank=True)
meta_publisher = models.TextField(default="", blank=True)
objects = InheritanceManager()

View File

@ -18,9 +18,11 @@ password_changed = Signal(providing_args=["user", "password"])
def invalidate_policy_cache(sender, instance, **_):
"""Invalidate Policy cache when policy is updated"""
from passbook.core.models import Policy
from passbook.policies.process import cache_key
if isinstance(instance, Policy):
LOGGER.debug("Invalidating policy cache", policy=instance)
keys = cache.keys("%s#*" % instance.pk)
prefix = cache_key(instance) + "*"
keys = cache.keys(prefix)
cache.delete_many(keys)
LOGGER.debug("Deleted %d keys", len(keys))

View File

@ -0,0 +1,27 @@
{% extends 'login/base.html' %}
{% load static %}
{% load i18n %}
{% load utils %}
{% block card_title %}
{{ title }} <span>(403)</span>
{% endblock %}
{% block card %}
<form>
<h3>{{ main }}</h3>
{% if no_referer %}
<p>{{ no_referer1 }}</p>
<p>{{ no_referer2 }}</p>
<p>{{ no_referer3 }}</p>
{% endif %}
{% if no_cookie %}
<p>{{ no_cookie1 }}</p>
<p>{{ no_cookie2 }}</p>
{% endif %}
{% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %}
</form>
{% endblock %}

View File

@ -0,0 +1,58 @@
{% extends "base/skeleton.html" %}
{% load static %}
{% load i18n %}
{% load is_active %}
{% load utils %}
{% block body %}
{% include 'partials/messages.html' %}
<div class="pf-c-page" id="page-default-nav-example">
<a class="pf-c-skip-to-content pf-c-button pf-m-primary" href="#main-content">{% trans 'Skip to content' %}</a>
<header role="banner" class="pf-c-page__header ws-page-header">
<div class="pf-c-page__header-brand">
<div class="pf-c-page__header-brand-toggle">
<button class="pf-c-button pf-m-plain" type="button" id="page-default-nav-example-nav-toggle"
aria-label="Global navigation" aria-expanded="true"
aria-controls="page-default-nav-example-primary-nav">
<i class="fas fa-bars" aria-hidden="true"></i>
</button>
</div>
<a class="pf-c-page__header-brand-link">
<img class="pf-c-brand" src="{% static 'passbook/logo.png' %}" alt="" />
<img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" alt="passbook" />
</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">
<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 %}
<li class="pf-c-nav__item"><a class="pf-c-nav__link {% is_active_url 'passbook_admin:overview' %}"
href="{% url 'passbook_admin:overview' %}">{% trans 'Administrate' %}</a></li>
<li class="pf-c-nav__item"><a class="pf-c-nav__link {% is_active_url 'passbook_admin:audit-log' %}"
href="{% url 'passbook_admin:audit-log' %}">{% trans 'Monitor' %}</a></li>
{% endif %}
</ul>
</nav>
</div>
<div class="pf-c-page__header-tools">
<div class="pf-c-page__header-tools-group pf-m-icons">
<a href="{% url 'passbook_core:auth-logout' %}" class="pf-c-button pf-m-plain" type="button">
<i class="fas fa-sign-out-alt" aria-hidden="true"></i>
</a>
</div>
<div class="pf-c-page__header-tools-group">
<a href="{% url 'passbook_core:user-settings' %}" class="pf-c-button">
{{ user.username }}
</a>
</div>
<img class="pf-c-avatar" src="{% gravatar user.email %}" alt="">
</div>
</header>
{% block page_content %}
{% endblock %}
</div>
{% endblock %}

View File

@ -8,7 +8,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{% block title %}{% title %}{% endblock %}</title>
<title>{% block title %}{% trans title %}{% endblock %}</title>
<link rel="icon" type="image/png" href="{% static 'passbook/logo.png' %}">
<link rel="shortcut icon" type="image/png" href="{% static 'passbook/logo.png' %}">
<link rel="stylesheet" type="text/css" href="{% static 'node_modules/@patternfly/patternfly/patternfly.css' %}">

View File

@ -8,7 +8,6 @@
{% endblock %}
{% block content %}
{% config 'passbook.branding' as branding %}
<!-- HERO -->
<tr>
<td bgcolor="#7c72dc" align="center" style="padding: 0px 10px 0px 10px;">

View File

@ -5,7 +5,7 @@
<!DOCTYPE html>
<html>
<head>
<title>{% config passbook.branding %}</title>
<title>passbook</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
@ -118,7 +118,7 @@
<!-- ADDRESS -->
<tr>
<td bgcolor="#1b2a32" align="left" style="padding: 0px 30px 30px 30px; color: #E9ECEF; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;">
<p style="margin: 0;"><a href="{% config 'passbook.branding' %}">{% config 'passbook.branding' %}</a></p>
<p style="margin: 0;"><a href="passbook">passbook</a></p>
</td>
</tr>
</table>

View File

@ -23,7 +23,7 @@
<header class="pf-c-login__header">
<img class="pf-c-brand" src="{% static 'passbook/logo.svg' %}" style="height: 60px;"
alt="passbook icon" />
<img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" style="height: 80px;"
<img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" style="height: 60px;"
alt="passbook branding" />
</header>
<main class="pf-c-login__main">
@ -54,9 +54,10 @@
<a href="{{ source.url }}" class="pf-c-login__main-footer-links-item-link">
{% if source.icon_path %}
<img src="{% static source.icon_path %}" alt="{{ source.name }}">
{% endif %}
{% if source.icon_url %}
{% elif source.icon_url %}
<img src="icon_url" alt="{{ source.name }}">
{% else %}
<i class="pf-icon pf-icon-arrow" title="{{ source.name }}"></i>
{% endif %}
</a>
</li>

View File

@ -4,27 +4,18 @@
{% load i18n %}
{% load utils %}
{% block head %}
{{ block.super }}
<style>
.pf-icon {
font-size: 48px;
text-align: center;
}
</style>
{% endblock %}
{% block card %}
<header class="login-pf-header">
<h1>{% trans title %}</h1>
</header>
<form method="POST">
{% csrf_token %}
{% include 'partials/form.html' %}
<span class="pf-icon pficon-error-circle-o btn-block"></span>
Access denied
{% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %}
<form method="POST" class="pf-c-form">
{% csrf_token %}
{% include 'partials/form.html' %}
<div class="pf-c-form__group">
<p>
<i class="pf-icon pf-icon-error-circle-o"></i>
{% trans 'Access denied' %}
</p>
</div>
{% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %}
</form>
{% endblock %}

View File

@ -5,7 +5,7 @@
{% load utils %}
{% block title %}
{% title title %}
{% trans title %}
{% endblock %}
{% block head %}

View File

@ -1,4 +1,4 @@
{% extends "base/skeleton.html" %}
{% extends "base/page.html" %}
{% load static %}
{% load i18n %}
@ -6,120 +6,10 @@
{% load is_active %}
{% load utils %}
{% block body %}
{% block page_content %}
{% include 'partials/messages.html' %}
<div class="pf-c-page" id="page-default-nav-example">
<a class="pf-c-skip-to-content pf-c-button pf-m-primary" href="#main-content">{% trans 'Skip to content' %}</a>
<header role="banner" class="pf-c-page__header">
<div class="pf-c-page__header-brand">
<div class="pf-c-page__header-brand-toggle">
<button class="pf-c-button pf-m-plain" type="button" id="page-default-nav-example-nav-toggle"
aria-label="Global navigation" aria-expanded="true"
aria-controls="page-default-nav-example-primary-nav">
<i class="fas fa-bars" aria-hidden="true"></i>
</button>
</div>
<a class="pf-c-page__header-brand-link">
<img class="pf-c-brand" src="{% static 'passbook/logo.png' %}" alt="" />
<img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" alt="passbook" />
</a>
</div>
<div class="pf-c-page__header-tools">
<div class="pf-c-page__header-tools-group pf-m-icons">
<a href="{% url 'passbook_core:auth-logout' %}" class="pf-c-button pf-m-plain" type="button">
<i class="fas fa-sign-out-alt" aria-hidden="true"></i>
</a>
</div>
<div class="pf-c-page__header-tools-group">
<a href="{% url 'passbook_core:user-settings' %}" class="pf-c-button">
{{ user.username }}
</a>
</div>
<img class="pf-c-avatar" src="{% gravatar user.email %}" alt="">
</div>
</header>
<div class="pf-c-page__sidebar pf-m-dark">
<div class="pf-c-page__sidebar-body">
<nav class="pf-c-nav pf-m-dark" id="page-default-nav-example-primary-nav" aria-label="Global">
<ul class="pf-c-nav__list">
<li class="pf-c-nav__item">
<a href="{% url 'passbook_core:overview' %}" class="pf-c-nav__link {% is_active_url 'passbook_core:overview' %}">
{% trans 'Overview' %}
</a>
</li>
{% if user.is_superuser %}
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:overview' %}" class="pf-c-nav__link {% is_active_url 'passbook_admin:overview' %}">
{% trans 'System Status' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:applications' %}" class="pf-c-nav__link {% is_active 'passbook_admin:applications' 'passbook_admin:application-create' 'passbook_admin:application-update' 'passbook_admin:application-delete' %}">
{% trans 'Applications' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:sources' %}" class="pf-c-nav__link {% is_active 'passbook_admin:sources' 'passbook_admin:source-create' 'passbook_admin:source-update' 'passbook_admin:source-delete' %}">
{% trans 'Sources' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:providers' %}" class="pf-c-nav__link {% is_active 'passbook_admin:providers' 'passbook_admin:provider-create' 'passbook_admin:provider-update' 'passbook_admin:provider-delete' %}">
{% trans 'Providers' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:property-mappings' %}" class="pf-c-nav__link {% is_active 'passbook_admin:property-mappings' 'passbook_admin:property-mapping-create' 'passbook_admin:property-mapping-update' 'passbook_admin:property-mapping-delete' %}">
{% trans 'Property Mappings' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:factors' %}" class="pf-c-nav__link {% is_active 'passbook_admin:factors' 'passbook_admin:factor-create' 'passbook_admin:factor-update' 'passbook_admin:factor-delete' %}">
{% trans 'Factors' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:policies' %}" class="pf-c-nav__link {% is_active 'passbook_admin:policies' 'passbook_admin:policy-create' 'passbook_admin:policy-update' 'passbook_admin:policy-delete' 'passbook_admin:policy-test' %}">
{% trans 'Policies' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:invitations' %}" class="pf-c-nav__link {% is_active 'passbook_admin:invitations' 'passbook_admin:invitation-create' 'passbook_admin:invitation-update' 'passbook_admin:invitation-delete' 'passbook_admin:invitation-test' %}">
{% trans 'Invitations' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:users' %}" class="pf-c-nav__link {% is_active 'passbook_admin:users' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
{% trans 'Users' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:groups' %}" class="pf-c-nav__link {% is_active 'passbook_admin:groups' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
{% trans 'Groups' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:audit-log' %}" class="pf-c-nav__link {% is_active 'passbook_admin:audit-log' %}">
{% trans 'Audit Log' %}
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>Main title</h1>
<p>This is a demo of the Page component.</p>
</div>
</section>
<section class="pf-c-page__main-section">
</section>
{% endblock %}
</main>
</div>
<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
{% block content %}
{% endblock %}
</main>
{% endblock %}

View File

@ -44,13 +44,13 @@ class LoginView(UserPassesTestMixin, FormView):
kwargs["primary_action"] = _("Log in")
kwargs["show_sign_up_notice"] = CONFIG.y("passbook.sign_up.enabled")
kwargs["sources"] = []
sources = Source.objects.filter(enabled=True).select_subclasses()
sources = (
Source.objects.filter(enabled=True).order_by("name").select_subclasses()
)
for source in sources:
ui_login_button = source.ui_login_button
if ui_login_button:
kwargs["sources"].append(ui_login_button)
# if kwargs["sources"]:
# self.template_name = "login/with_sources.html"
return super().get_context_data(**kwargs)
def get_user(self, uid_value) -> Optional[User]:
@ -231,7 +231,6 @@ class PasswordResetView(View):
login(request, nonce.user)
nonce.delete()
messages.success(
request,
_(("Temporarily authenticated with Nonce, " "please change your password")),
request, _(("Temporarily authenticated, please change your password")),
)
return redirect("passbook_core:user-change-password")

View File

@ -2,7 +2,7 @@
from captcha.fields import ReCaptchaField
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from passbook.factors.captcha.models import CaptchaFactor
from passbook.factors.forms import GENERAL_FIELDS
@ -28,3 +28,8 @@ class CaptchaFactorForm(forms.ModelForm):
"public_key": forms.TextInput(),
"private_key": forms.TextInput(),
}
help_texts = {
"policies": _(
"Policies which determine if this factor applies to the current user."
)
}

View File

@ -1,7 +1,7 @@
"""passbook administration forms"""
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from passbook.factors.email.models import EmailFactor
from passbook.factors.forms import GENERAL_FIELDS
@ -41,3 +41,8 @@ class EmailFactorForm(forms.ModelForm):
"ssl_keyfile": _("SSL Keyfile (optional)"),
"ssl_certfile": _("SSL Certfile (optional)"),
}
help_texts = {
"policies": _(
"Policies which determine if this factor applies to the current user."
)
}

View File

@ -4,7 +4,7 @@ from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.core.validators import RegexValidator
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django_otp.models import Device
from passbook.factors.forms import GENERAL_FIELDS
@ -80,3 +80,8 @@ class OTPFactorForm(forms.ModelForm):
"order": forms.NumberInput(),
"policies": FilteredSelectMultiple(_("policies"), False),
}
help_texts = {
"policies": _(
"Policies which determine if this factor applies to the current user."
)
}

View File

@ -2,7 +2,7 @@
from django import forms
from django.conf import settings
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from passbook.factors.forms import GENERAL_FIELDS
from passbook.factors.password.models import PasswordFactor
@ -49,3 +49,8 @@ class PasswordFactorForm(forms.ModelForm):
"password_policies": FilteredSelectMultiple(_("password policies"), False),
"reset_factors": FilteredSelectMultiple(_("reset factors"), False),
}
help_texts = {
"policies": _(
"Policies which determine if this factor applies to the current user."
)
}

View File

@ -25,18 +25,6 @@ passbook:
password_reset:
# Enable password reset, passwords are reset in internal Database and in LDAP if ldap.reset_password is true
enabled: true
# Verification the user has to provide in order to be able to reset passwords. Can be any combination of `email`, `2fa`, `security_questions`
verification:
- email
# Text used in title, on login page and multiple other places
branding: passbook
login:
# Override URL used for logo
logo_url: null
# Override URL used for Background on Login page
bg_url: null
# Optionally add a subtext, placed below logo on the login page
subtext: null
footer:
links:
# Optionally add links to the footer on the login page
@ -46,14 +34,3 @@ passbook:
uid_fields:
- username
- email
# Provider-specific settings
ldap:
# Which field from `uid_fields` maps to which LDAP Attribute
login_field_map:
username: sAMAccountName
email: mail # or userPrincipalName
user_attribute_map:
active_directory:
username: "%(sAMAccountName)s"
email: "%(mail)s"
name: "%(displayName)"

View File

@ -1,48 +0,0 @@
"""passbook lib fields"""
from itertools import chain
from django import forms
from django.contrib.postgres.utils import prefix_validation_error
from passbook.lib.widgets import DynamicArrayWidget
class DynamicArrayField(forms.Field):
"""Show array field as a dynamic amount of textboxes"""
default_error_messages = {
"item_invalid": "Item %(nth)s in the array did not validate: "
}
def __init__(self, base_field, **kwargs):
self.base_field = base_field
self.max_length = kwargs.pop("max_length", None)
kwargs.setdefault("widget", DynamicArrayWidget)
super().__init__(**kwargs)
def clean(self, value):
cleaned_data = []
errors = []
value = [x for x in value if x]
for index, item in enumerate(value):
try:
cleaned_data.append(self.base_field.clean(item))
except forms.ValidationError as error:
errors.append(
prefix_validation_error(
error,
self.error_messages["item_invalid"],
code="item_invalid",
params={"nth": index},
)
)
if errors:
raise forms.ValidationError(list(chain.from_iterable(errors)))
if not cleaned_data and self.required:
raise forms.ValidationError(self.error_messages["required"])
return cleaned_data
def has_changed(self, initial, data):
if not data and not initial:
return False
return super().has_changed(initial, data)

9
passbook/lib/logging.py Normal file
View File

@ -0,0 +1,9 @@
"""logging helpers"""
from os import getpid
# pylint: disable=unused-argument
def add_process_id(logger, method_name, event_dict):
"""Add the current process ID"""
event_dict["pid"] = getpid()
return event_dict

View File

@ -0,0 +1,66 @@
"""passbook management command to bootstrap"""
from argparse import REMAINDER
from subprocess import Popen # nosec
# pylint: disable=redefined-builtin
from sys import exit, stderr, stdin, stdout
from time import sleep
from django.core.management.base import BaseCommand
from django.db import connection
from django.db.utils import OperationalError
from django_redis import get_redis_connection
from redis.exceptions import ConnectionError as RedisConnectionError
from structlog import get_logger
LOGGER = get_logger()
class Command(BaseCommand):
"""Bootstrap passbook, ensure Database and Cache are
reachable, and directories are writeable"""
help = """Bootstrap passbook, ensure Database and Cache are
reachable, and directories are writeable"""
def add_arguments(self, parser):
parser.add_argument("command", nargs=REMAINDER)
def check_database(self) -> bool:
"""Return true if database is reachable, false otherwise"""
try:
connection.cursor()
LOGGER.info("Database reachable")
return True
except OperationalError:
LOGGER.info("Database unreachable")
return False
def check_cache(self) -> bool:
"""Return true if cache is reachable, false otherwise"""
try:
con = get_redis_connection("default")
con.ping()
LOGGER.info("Cache reachable")
return True
except RedisConnectionError:
LOGGER.info("Cache unreachable")
return False
def handle(self, *args, **options):
LOGGER.info("passbook bootstrapping...")
should_check = True
while should_check:
should_check = not (self.check_database() and self.check_cache())
sleep(1)
LOGGER.info("Dependencies are up, starting command...")
proc = Popen(
args=options.get("command"), stdout=stdout, stderr=stderr, stdin=stdin
) # nosec
try:
proc.wait()
exit(proc.returncode)
except KeyboardInterrupt:
LOGGER.info("Killing process")
proc.kill()
exit(254)

View File

@ -1,29 +1,28 @@
"""passbook sentry integration"""
from billiard.exceptions import WorkerLostError
from botocore.client import ClientError
from django.core.exceptions import DisallowedHost, ValidationError
from django.db import InternalError, OperationalError, ProgrammingError
from django_redis.exceptions import ConnectionInterrupted
from redis.exceptions import RedisError
from rest_framework.exceptions import APIException
from structlog import get_logger
LOGGER = get_logger()
class SentryIgnoredException(Exception):
"""Base Class for all errors that are supressed, and not sent to sentry."""
"""Base Class for all errors that are suppressed, and not sent to sentry."""
def before_send(event, hint):
"""Check if error is database error, and ignore if so"""
from django_redis.exceptions import ConnectionInterrupted
from django.db import OperationalError, InternalError
from django.core.exceptions import ValidationError
from rest_framework.exceptions import APIException
from billiard.exceptions import WorkerLostError
from django.core.exceptions import DisallowedHost
from botocore.client import ClientError
from redis.exceptions import RedisError
ignored_classes = (
OperationalError,
InternalError,
ProgrammingError,
ConnectionInterrupted,
APIException,
InternalError,
ConnectionResetError,
WorkerLostError,
DisallowedHost,

View File

@ -3,11 +3,9 @@ from hashlib import md5
from urllib.parse import urlencode
from django import template
from django.apps import apps
from django.db.models import Model
from django.template import Context
from django.utils.html import escape
from django.utils.translation import ugettext as _
from passbook.lib.config import CONFIG
from passbook.lib.utils.urls import is_url_absolute
@ -40,38 +38,6 @@ def fieldtype(field):
return field.__class__.__name__
@register.simple_tag(takes_context=True)
def title(context: Context, *title) -> str:
"""Return either just branding or title - branding"""
branding = CONFIG.y("passbook.branding", "passbook")
if not title:
return branding
if "request" not in context:
return ""
resolver_match = context.request.resolver_match
if not resolver_match:
return ""
# Include App Title in title
app = ""
if resolver_match.namespace != "":
dj_app = None
namespace = context.request.resolver_match.namespace.split(":")[0]
# New label (App URL Namespace == App Label)
dj_app = apps.get_app_config(namespace)
title_modifier = getattr(dj_app, "title_modifier", None)
if title_modifier:
app_title = dj_app.title_modifier(context.request)
app = app_title + " -"
return _(
"%(title)s - %(app)s %(branding)s"
% {
"title": " - ".join([str(x) for x in title]),
"branding": branding,
"app": app,
}
)
@register.simple_tag
def config(path, default=""):
"""Get a setting from the database. Returns default is setting doesn't exist."""

View File

@ -9,6 +9,7 @@ from jinja2.nativetypes import NativeEnvironment
from structlog import get_logger
from passbook.factors.view import AuthenticationView
from passbook.lib.utils.http import get_client_ip
from passbook.policies.types import PolicyRequest, PolicyResult
if TYPE_CHECKING:
@ -18,7 +19,7 @@ LOGGER = get_logger()
class Evaluator:
"""Validate and evaulate jinja2-based expressions"""
"""Validate and evaluate jinja2-based expressions"""
_env: NativeEnvironment
@ -50,11 +51,15 @@ class Evaluator:
"""Return dictionary with additional global variables passed to expression"""
# update passbook/policies/expression/templates/policy/expression/form.html
# update docs/policies/expression/index.md
kwargs["pb_is_sso_flow"] = request.http_request.session.get(
AuthenticationView.SESSION_IS_SSO_LOGIN, False
)
kwargs["pb_is_group_member"] = Evaluator.jinja2_func_is_group_member
kwargs["pb_logger"] = get_logger()
if request.http_request:
kwargs["pb_is_sso_flow"] = request.http_request.session.get(
AuthenticationView.SESSION_IS_SSO_LOGIN, False
)
kwargs["pb_client_ip"] = (
get_client_ip(request.http_request) or "255.255.255.255"
)
return kwargs
def evaluate(self, expression_source: str, request: PolicyRequest) -> PolicyResult:
@ -77,7 +82,7 @@ class Evaluator:
req=request,
)
return PolicyResult(False)
if isinstance(result, list) and len(result) == 2:
if isinstance(result, (list, tuple)) and len(result) == 2:
return PolicyResult(*result)
if result:
return PolicyResult(result)

View File

@ -9,15 +9,17 @@
<p>
Expression using <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/">Jinja</a>. Following variables are available:
</p>
<ul>
<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_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>
<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>

View File

@ -0,0 +1,58 @@
"""evaluator tests"""
from django.core.exceptions import ValidationError
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from passbook.policies.expression.evaluator import Evaluator
from passbook.policies.types import PolicyRequest
class TestEvaluator(TestCase):
"""Evaluator tests"""
def setUp(self):
self.request = PolicyRequest(user=get_anonymous_user())
def test_valid(self):
"""test simple value expression"""
template = "True"
evaluator = Evaluator()
self.assertEqual(evaluator.evaluate(template, self.request).passing, True)
def test_messages(self):
"""test expression with message return"""
template = "False, 'some message'"
evaluator = Evaluator()
result = evaluator.evaluate(template, self.request)
self.assertEqual(result.passing, False)
self.assertEqual(result.messages, ("some message",))
def test_invalid_syntax(self):
"""test invalid syntax"""
template = "{%"
evaluator = Evaluator()
result = evaluator.evaluate(template, self.request)
self.assertEqual(result.passing, False)
self.assertEqual(result.messages, ("tag name expected",))
def test_undefined(self):
"""test undefined result"""
template = "{{ foo.bar }}"
evaluator = Evaluator()
result = evaluator.evaluate(template, self.request)
self.assertEqual(result.passing, False)
self.assertEqual(result.messages, ("'foo' is undefined",))
def test_validate(self):
"""test validate"""
template = "True"
evaluator = Evaluator()
result = evaluator.validate(template)
self.assertEqual(result, True)
def test_validate_invalid(self):
"""test validate"""
template = "{%"
evaluator = Evaluator()
with self.assertRaises(ValidationError):
evaluator.validate(template)

View File

@ -5,16 +5,19 @@ from multiprocessing.connection import Connection
from django.core.cache import cache
from structlog import get_logger
from passbook.core.models import Policy
from passbook.core.models import Policy, User
from passbook.policies.exceptions import PolicyException
from passbook.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger()
def cache_key(policy, user):
def cache_key(policy: Policy, user: User = None) -> str:
"""Generate Cache key for policy"""
return f"policy_{policy.pk}#{user.pk}"
prefix = f"policy_{policy.pk}"
if user:
prefix += f"#{user.pk}"
return prefix
class PolicyProcess(Process):
@ -33,7 +36,7 @@ class PolicyProcess(Process):
def run(self):
"""Task wrapper to run policy checking"""
LOGGER.debug(
"Running policy",
"P_ENG(proc): Running policy",
policy=self.policy,
user=self.request.user,
process="PolicyProcess",
@ -41,13 +44,13 @@ class PolicyProcess(Process):
try:
policy_result = self.policy.passes(self.request)
except PolicyException as exc:
LOGGER.debug(exc)
LOGGER.debug("P_ENG(proc): error", exc=exc)
policy_result = PolicyResult(False, str(exc))
# Invert result if policy.negate is set
if self.policy.negate:
policy_result.passing = not policy_result.passing
LOGGER.debug(
"Got result",
"P_ENG(proc): Finished",
policy=self.policy,
result=policy_result,
process="PolicyProcess",
@ -56,5 +59,5 @@ class PolicyProcess(Process):
)
key = cache_key(self.policy, self.request.user)
cache.set(key, policy_result)
LOGGER.debug("Cached policy evaluation", key=key)
LOGGER.debug("P_ENG(proc): Cached policy evaluation", key=key)
self.connection.send(policy_result)

View File

@ -1,7 +1,7 @@
"""policy structures"""
from __future__ import annotations
from typing import TYPE_CHECKING, Tuple
from typing import TYPE_CHECKING, Optional, Tuple
from django.db.models import Model
from django.http import HttpRequest
@ -14,11 +14,13 @@ class PolicyRequest:
"""Data-class to hold policy request data"""
user: User
http_request: HttpRequest
obj: Model
http_request: Optional[HttpRequest]
obj: Optional[Model]
def __init__(self, user: User):
self.user = user
self.http_request = None
self.obj = None
def __str__(self):
return f"<PolicyRequest user={self.user}>"

View File

@ -34,7 +34,7 @@ class ApplicationGatewayProviderSerializer(ModelSerializer):
class Meta:
model = ApplicationGatewayProvider
fields = ["pk", "name", "host", "client"]
fields = ["pk", "name", "internal_host", "external_host", "client"]
read_only_fields = ["client"]

View File

@ -3,15 +3,12 @@
{% load utils %}
{% load i18n %}
{% block title %}
{% title 'Authorize Application' %}
{% block card_title %}
{% trans 'Authorize Application' %}
{% endblock %}
{% block card %}
<header class="login-pf-header">
<h1>{% trans 'Authorize Application' %}</h1>
</header>
<form method="POST">
<form method="POST" class="pf-c-form">
{% csrf_token %}
{% if not error %}
{% csrf_token %}
@ -20,32 +17,40 @@
{{ field }}
{% endif %}
{% endfor %}
<div class="form-group">
<div class="pf-c-form__group">
<p class="subtitle">
{% blocktrans with remote=application.name %}
You're about to sign into {{ remote }}
You're about to sign into {{ remote }}.
{% endblocktrans %}
</p>
<p>{% trans "Application requires following permissions" %}</p>
<ul>
<ul class="pf-c-list">
{% for scope in scopes_descriptions %}
<li>{{ scope }}</li>
{% endfor %}
</ul>
{{ form.errors }}
{{ form.non_field_errors }}
</div>
<div class="pf-c-form__group">
<p>
{% blocktrans with user=user %}
You are logged in as {{ user }}. Not you?
{% endblocktrans %}
<a href="{% url 'passbook_core:auth-logout' %}">{% trans 'Logout' %}</a>
</p>
<div class="form-group">
<input type="submit" class="btn btn-success btn-disabled btn-lg click-spinner" name="allow" value="{% trans 'Continue' %}">
<a href="{% back %}" class="btn btn-default btn-lg">{% trans "Cancel" %}</a>
</div>
<div class="form-group spinner-hidden hidden">
<div class="spinner"></div>
</div>
<div class="pf-c-form__group pf-m-action">
<input type="submit" class="pf-c-button pf-m-primary" name="allow" value="{% trans 'Continue' %}">
<a href="{% back %}" class="pf-c-button pf-m-secondary">{% trans "Cancel" %}</a>
</div>
<div class="pf-c-form__group" style="display: none;" id="loading">
<div class="pf-c-form__horizontal-group">
<span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>
</div>
{% else %}
@ -61,9 +66,8 @@
{% block scripts %}
<script>
$('.click-spinner').on('click', function (e) {
$('.spinner-hidden').removeClass('hidden');
$(e.target).addClass('disabled');
})
document.querySelector("form").addEventListener("submit", (e) => {
document.getElementById("loading").removeAttribute("style");
});
</script>
{% endblock %}

View File

@ -19,6 +19,8 @@ class OIDCProviderForm(forms.ModelForm):
self.fields["client_secret"].initial = generate_client_secret()
def save(self, *args, **kwargs):
self.instance.reuse_consent = False # This is managed by passbook
self.instance.require_consent = True # This is managed by passbook
response = super().save(*args, **kwargs)
# Check if openidprovider class instance exists
if not OpenIDProvider.objects.filter(oidc_client=self.instance).exists():

View File

@ -3,15 +3,12 @@
{% load utils %}
{% load i18n %}
{% block title %}
{% title 'Authorize Application' %}
{% block card_title %}
{% trans 'Authorize Application' %}
{% endblock %}
{% block card %}
<header class="login-pf-header">
<h1>{% trans 'Authorize Application' %}</h1>
</header>
<form method="POST">
<form method="POST" class="pf-c-form">
{% csrf_token %}
{% if not error %}
{% csrf_token %}
@ -20,14 +17,14 @@
{{ field }}
{% endif %}
{% endfor %}
<div class="form-group">
<div class="pf-c-form__group">
<p class="subtitle">
{% blocktrans with remote=client.name %}
You're about to sign into {{ remote }}
You're about to sign into {{ remote }}.
{% endblocktrans %}
</p>
<p>{% trans "Application requires following permissions" %}</p>
<ul>
<ul class="pf-c-list">
{% for scope in scopes %}
<li>{{ scope.name }}</li>
{% endfor %}
@ -35,18 +32,26 @@
{{ hidden_inputs }}
{{ form.errors }}
{{ form.non_field_errors }}
</div>
<div class="pf-c-form__group">
<p>
{% blocktrans with user=user %}
You are logged in as {{ user }}. Not you?
{% endblocktrans %}
<a href="{% url 'passbook_core:auth-logout' %}">{% trans 'Logout' %}</a>
</p>
<div class="form-group">
<input type="submit" class="btn btn-success btn-disabled btn-lg click-spinner" name="allow" value="{% trans 'Continue' %}">
<a href="{% back %}" class="btn btn-default btn-lg">{% trans "Cancel" %}</a>
</div>
<div class="form-group spinner-hidden hidden">
<div class="spinner"></div>
</div>
<div class="pf-c-form__group pf-m-action">
<input type="submit" class="pf-c-button pf-m-primary" name="allow" value="{% trans 'Continue' %}">
<a href="{% back %}" class="pf-c-button pf-m-secondary">{% trans "Cancel" %}</a>
</div>
<div class="pf-c-form__group" style="display: none;" id="loading">
<div class="pf-c-form__horizontal-group">
<span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>
</div>
{% else %}
@ -62,9 +67,8 @@
{% block scripts %}
<script>
$('.click-spinner').on('click', function (e) {
$('.spinner-hidden').removeClass('hidden');
$(e.target).addClass('disabled');
})
document.querySelector("form").addEventListener("submit", (e) => {
document.getElementById("loading").removeAttribute("style");
});
</script>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends 'login/base.html' %}
{% load static %}
{% load i18n %}
{% load utils %}
{% block card_title %}
{% trans error %}
{% endblock %}
{% block card %}
<form>
<h3>{% trans description %}</h3>
{% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %}
</form>
{% endblock %}

View File

@ -184,7 +184,7 @@ class Processor:
try:
self._extract_saml_request()
except KeyError:
raise CannotHandleAssertion(f"Couldn't find SAML request in user session:")
raise CannotHandleAssertion(f"Couldn't find SAML request in user session")
try:
self._decode_and_parse_request()

View File

@ -3,23 +3,17 @@
{% load utils %}
{% load i18n %}
{% block title %}
{% title 'Redirecting...' %}
{% block card_title %}
{% trans 'Redirecting...' %}
{% endblock %}
{% block card %}
<header class="login-pf-header">
<h1>{% trans 'Redirecting...' %}</h1>
</header>
<form method="POST" action="{{ url }}">
{% csrf_token %}
{% for key, value in attrs.items %}
<input type="hidden" name="{{ key }}" value="{{ value }}">
{% endfor %}
<div class="login-group">
<h3>
{% trans "Redirecting..." %}
</h3>
<p>
{% blocktrans with user=user %}
You are logged in as {{ user }}.
@ -34,6 +28,6 @@
{% block scripts %}
{{ block.super }}
<script>
$('form').submit();
document.querySelector("form").submit();
</script>
{% endblock %}

View File

@ -4,11 +4,8 @@
{% load i18n %}
{% block card %}
<form method="POST" class="pf-c-form" action="{{ saml_params.acs_url }}">
<form method="POST" class="pf-c-form">
{% csrf_token %}
<input type="hidden" name="ACSUrl" value="{{ saml_params.acs_url }}">
<input type="hidden" name="RelayState" value="{{ saml_params.relay_state }}" />
<input type="hidden" name="SAMLResponse" value="{{ saml_params.saml_response }}" />
<div class="pf-c-form__group">
<h3>
{% blocktrans with provider=provider.application.name %}

View File

@ -8,7 +8,7 @@
<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>
<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>

View File

@ -1,7 +1,5 @@
<saml:Subject>
<saml:NameID Format="{{ SUBJECT_FORMAT }}">
{{ SUBJECT }}
</saml:NameID>
<saml:NameID Format="{{ SUBJECT_FORMAT }}">{{ SUBJECT }}</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData {{ IN_RESPONSE_TO|safe }} NotOnOrAfter="{{ NOT_ON_OR_AFTER }}" Recipient="{{ ACS_URL }}" />
</saml:SubjectConfirmation>

View File

@ -16,9 +16,9 @@ urlpatterns = [
"<slug:application>/login/", views.LoginBeginView.as_view(), name="saml-login"
),
path(
"<slug:application>/login/process/",
views.LoginProcessView.as_view(),
name="saml-login-process",
"<slug:application>/login/authorize/",
views.AuthorizeView.as_view(),
name="saml-login-authorize",
),
path("<slug:application>/logout/", views.LogoutView.as_view(), name="saml-logout"),
path(

View File

@ -21,12 +21,16 @@ from passbook.core.models import Application, Provider
from passbook.lib.utils.template import render_to_string
from passbook.lib.views import bad_request_message
from passbook.policies.engine import PolicyEngine
from passbook.providers.saml import exceptions
from passbook.providers.saml.exceptions import CannotHandleAssertion
from passbook.providers.saml.models import SAMLProvider
from passbook.providers.saml.processors.types import SAMLResponseParams
LOGGER = get_logger()
URL_VALIDATOR = URLValidator(schemes=("http", "https"))
SESSION_KEY_SAML_REQUEST = "SAMLRequest"
SESSION_KEY_SAML_RESPONSE = "SAMLResponse"
SESSION_KEY_RELAY_STATE = "RelayState"
SESSION_KEY_PARAMS = "SAMLParams"
class AccessRequiredView(AccessMixin, View):
@ -50,14 +54,18 @@ class AccessRequiredView(AccessMixin, View):
def _has_access(self) -> bool:
"""Check if user has access to application"""
LOGGER.debug(
"_has_access", user=self.request.user, app=self.provider.application
)
policy_engine = PolicyEngine(
self.provider.application.policies.all(), self.request.user, self.request
)
policy_engine.build()
return policy_engine.passing
passing = policy_engine.passing
LOGGER.debug(
"saml_has_access",
user=self.request.user,
app=self.provider.application,
passing=passing,
)
return passing
def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
if not request.user.is_authenticated:
@ -75,80 +83,29 @@ class LoginBeginView(AccessRequiredView):
"""Receives a SAML 2.0 AuthnRequest from a Service Provider and
stores it in the session prior to enforcing login."""
@method_decorator(csrf_exempt)
def dispatch(self, request: HttpRequest, application: str) -> HttpResponse:
if request.method == "POST":
source = request.POST
else:
source = request.GET
def handler(self, source, application: str) -> HttpResponse:
"""Handle SAML Request whether its a POST or a Redirect binding"""
# Store these values now, because Django's login cycle won't preserve them.
try:
request.session["SAMLRequest"] = source["SAMLRequest"]
self.request.session[SESSION_KEY_SAML_REQUEST] = source[
SESSION_KEY_SAML_REQUEST
]
except (KeyError, MultiValueDictKeyError):
return bad_request_message(request, "The SAML request payload is missing.")
request.session["RelayState"] = source.get("RelayState", "")
return redirect(
reverse(
"passbook_providers_saml:saml-login-process",
kwargs={"application": application},
return bad_request_message(
self.request, "The SAML request payload is missing."
)
self.request.session[SESSION_KEY_RELAY_STATE] = source.get(
SESSION_KEY_RELAY_STATE, ""
)
class LoginProcessView(AccessRequiredView):
"""Processor-based login continuation.
Presents a SAML 2.0 Assertion for POSTing back to the Service Provider."""
def handle_redirect(
self, params: SAMLResponseParams, skipped_authorization: bool
) -> HttpResponse:
"""Handle direct redirect to SP"""
# Log Application Authorization
Event.new(
EventAction.AUTHORIZE_APPLICATION,
authorized_application=self.provider.application,
skipped_authorization=skipped_authorization,
).from_http(self.request)
return render(
self.request,
"saml/idp/autosubmit_form.html",
{
"url": params.acs_url,
"attrs": {
"SAMLResponse": params.saml_response,
"RelayState": params.relay_state,
},
},
)
def get(self, request: HttpRequest, application: str) -> HttpResponse:
"""Handle get request, i.e. render form"""
# User access gets checked in dispatch
# Otherwise we generate the IdP initiated session
try:
# application.skip_authorization is set so we directly redirect the user
if self.provider.application.skip_authorization:
return self.post(request, application)
self.provider.processor.init_deep_link(request)
self.provider.processor.can_handle(self.request)
params = self.provider.processor.generate_response()
return render(
request,
"saml/idp/login.html",
{
"saml_params": params,
"provider": self.provider,
"title": "Authorize Application",
},
)
except exceptions.CannotHandleAssertion as exc:
LOGGER.error(exc)
did_you_mean_link = request.build_absolute_uri(
self.request.session[SESSION_KEY_PARAMS] = params
except CannotHandleAssertion as exc:
LOGGER.info(exc)
did_you_mean_link = self.request.build_absolute_uri(
reverse(
"passbook_providers_saml:saml-login-initiate",
kwargs={"application": application},
@ -158,19 +115,101 @@ class LoginProcessView(AccessRequiredView):
f" Did you mean to go <a href='{did_you_mean_link}'>here</a>?"
)
return bad_request_message(
request, mark_safe(str(exc) + did_you_mean_message)
self.request, mark_safe(str(exc) + did_you_mean_message)
)
return redirect(
reverse(
"passbook_providers_saml:saml-login-authorize",
kwargs={"application": application},
)
)
@method_decorator(csrf_exempt)
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
@method_decorator(csrf_exempt)
def get(self, request: HttpRequest, application: str) -> HttpResponse:
"""Handle REDIRECT bindings"""
return self.handler(request.GET, application)
@method_decorator(csrf_exempt)
def post(self, request: HttpRequest, application: str) -> HttpResponse:
"""Handle POST Bindings"""
return self.handler(request.POST, application)
class InitiateLoginView(AccessRequiredView):
"""IdP-initiated Login"""
def get(self, request: HttpRequest, application: str) -> HttpResponse:
"""Initiates an IdP-initiated link to a simple SP resource/target URL."""
self.provider.processor.is_idp_initiated = True
self.provider.processor.init_deep_link(request)
params = self.provider.processor.generate_response()
request.session[SESSION_KEY_PARAMS] = params
return redirect(
reverse(
"passbook_providers_saml:saml-login-authorize",
kwargs={"application": application},
)
)
class AuthorizeView(AccessRequiredView):
"""Ask the user for authorization to continue to the SP.
Presents a SAML 2.0 Assertion for POSTing back to the Service Provider."""
def get(self, request: HttpRequest, application: str) -> HttpResponse:
"""Handle get request, i.e. render form"""
# User access gets checked in dispatch
# Otherwise we generate the IdP initiated session
try:
# application.skip_authorization is set so we directly redirect the user
if self.provider.application.skip_authorization:
LOGGER.debug("skipping authz", application=self.provider.application)
return self.post(request, application)
return render(
request,
"saml/idp/login.html",
{"provider": self.provider, "title": "Authorize Application",},
)
except KeyError:
return bad_request_message(request, "Missing SAML Payload")
# pylint: disable=unused-argument
def post(self, request: HttpRequest, application: str) -> HttpResponse:
"""Handle post request, return back to ACS"""
# User access gets checked in dispatch
# we get here when skip_authorization is False, and after the user accepted
# we get here when skip_authorization is True, and after the user accepted
# the authorization form
self.provider.processor.can_handle(request)
saml_params = self.provider.processor.generate_response()
return self.handle_redirect(saml_params, True)
# Log Application Authorization
Event.new(
EventAction.AUTHORIZE_APPLICATION,
authorized_application=self.provider.application,
skipped_authorization=self.provider.application.skip_authorization,
).from_http(self.request)
self.request.session.pop(SESSION_KEY_SAML_REQUEST, None)
self.request.session.pop(SESSION_KEY_SAML_RESPONSE, None)
self.request.session.pop(SESSION_KEY_RELAY_STATE, None)
response: SAMLResponseParams = self.request.session.pop(SESSION_KEY_PARAMS)
return render(
self.request,
"saml/idp/autosubmit_form.html",
{
"url": response.acs_url,
"attrs": {
"ACSUrl": response.acs_url,
SESSION_KEY_SAML_RESPONSE: response.saml_response,
SESSION_KEY_RELAY_STATE: response.relay_state,
},
},
)
@method_decorator(csrf_exempt, name="dispatch")
@ -204,7 +243,9 @@ class SLOLogout(AccessRequiredView):
# pylint: disable=unused-argument
def post(self, request: HttpRequest, application: str) -> HttpResponse:
"""Perform logout"""
request.session["SAMLRequest"] = request.POST["SAMLRequest"]
request.session[SESSION_KEY_SAML_REQUEST] = request.POST[
SESSION_KEY_SAML_REQUEST
]
# TODO: Parse SAML LogoutRequest from POST data, similar to login_process().
# TODO: Modify the base processor to handle logouts?
# TODO: Combine this with login_process(), since they are so very similar?
@ -259,54 +300,7 @@ class DescriptorDownloadView(AccessRequiredView):
)
else:
response = HttpResponse(metadata, content_type="application/xml")
response["Content-Disposition"] = (
'attachment; filename="' '%s_passbook_meta.xml"' % self.provider.name
)
response[
"Content-Disposition"
] = f'attachment; filename="{self.provider.name}_passbook_meta.xml"'
return response
class InitiateLoginView(AccessRequiredView):
"""IdP-initiated Login"""
def handle_redirect(
self, params: SAMLResponseParams, skipped_authorization: bool
) -> HttpResponse:
"""Handle direct redirect to SP"""
# Log Application Authorization
Event.new(
EventAction.AUTHORIZE_APPLICATION,
authorized_application=self.provider.application,
skipped_authorization=skipped_authorization,
).from_http(self.request)
return render(
self.request,
"saml/idp/autosubmit_form.html",
{
"url": params.acs_url,
"attrs": {
"SAMLResponse": params.saml_response,
"RelayState": params.relay_state,
},
},
)
# pylint: disable=unused-argument
def get(self, request: HttpRequest, application: str) -> HttpResponse:
"""Initiates an IdP-initiated link to a simple SP resource/target URL."""
self.provider.processor.is_idp_initiated = True
self.provider.processor.init_deep_link(request)
params = self.provider.processor.generate_response()
# IdP-initiated Login Flow
if self.provider.application.skip_authorization:
return self.handle_redirect(params, True)
return render(
request,
"saml/idp/login.html",
{
"saml_params": params,
"provider": self.provider,
"title": "Authorize Application",
},
)

View File

@ -22,6 +22,7 @@ from sentry_sdk.integrations.django import DjangoIntegration
from passbook import __version__
from passbook.lib.config import CONFIG
from passbook.lib.logging import add_process_id
from passbook.lib.sentry import before_send
LOGGER = structlog.get_logger()
@ -53,6 +54,7 @@ if DEBUG:
CSRF_COOKIE_NAME = "passbook_csrf_debug"
LANGUAGE_COOKIE_NAME = "passbook_language_debug"
SESSION_COOKIE_NAME = "passbook_session_debug"
SESSION_COOKIE_SAMESITE = None
else:
CSRF_COOKIE_NAME = "passbook_csrf"
LANGUAGE_COOKIE_NAME = "passbook_language"
@ -278,6 +280,7 @@ structlog.configure_once(
processors=[
structlog.stdlib.add_log_level,
structlog.stdlib.add_logger_name,
add_process_id,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(),
structlog.processors.StackInfoRenderer(),
@ -314,7 +317,7 @@ LOGGING = {
},
"handlers": {
"console": {
"level": DEBUG,
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "colored" if DEBUG else "plain",
},
@ -322,6 +325,7 @@ LOGGING = {
"loggers": {},
}
_LOGGING_HANDLER_MAP = {
"": "DEBUG",
"passbook": "DEBUG",
"django": "WARNING",
"celery": "WARNING",
@ -334,7 +338,7 @@ for handler_name, level in _LOGGING_HANDLER_MAP.items():
LOGGING["loggers"][handler_name] = {
"handlers": ["console"],
"level": level,
"propagate": True,
"propagate": False,
}
TEST = False

View File

@ -42,7 +42,6 @@ class LDAPSourceForm(forms.ModelForm):
"group_object_filter": forms.TextInput(),
"user_group_membership_field": forms.TextInput(),
"object_uniqueness_field": forms.TextInput(),
"policies": FilteredSelectMultiple(_("policies"), False),
"property_mappings": FilteredSelectMultiple(_("Property Mappings"), False),
}

View File

@ -8,7 +8,7 @@
<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>
<ul class="pf-c-list">
<li><code>ldap</code>: A Dictionary of all values retrieved from LDAP.</li>
</ul>
</p>

View File

@ -21,6 +21,5 @@ class PassbookSourceOAuthConfig(AppConfig):
for source_type in settings.PASSBOOK_SOURCES_OAUTH_TYPES:
try:
import_module(source_type)
LOGGER.info("Loaded source_type", source_class=source_type)
except ImportError as exc:
LOGGER.debug(exc)

View File

@ -1,8 +1,9 @@
"""OAuth Clients"""
import json
from typing import Dict
from typing import Dict, Optional
from urllib.parse import parse_qs, urlencode
from django.http import HttpRequest
from django.utils.crypto import constant_time_compare, get_random_string
from django.utils.encoding import force_text
from requests import Session
@ -18,30 +19,26 @@ LOGGER = get_logger()
class BaseOAuthClient:
"""Base OAuth Client"""
_session: Session = None
session: Session = None
def __init__(self, source, token=""): # nosec
self.source = source
self.token = token
self._session = Session()
self._session.headers.update({"User-Agent": "passbook %s" % __version__})
self.session = Session()
self.session.headers.update({"User-Agent": "passbook %s" % __version__})
def get_access_token(self, request, callback=None):
"Fetch access token from callback request."
raise NotImplementedError("Defined in a sub-class") # pragma: no cover
def get_profile_info(self, raw_token):
def get_profile_info(self, token: Dict[str, str]):
"Fetch user profile information."
try:
token = json.loads(raw_token)
headers = {
"Authorization": f"{token['token_type']} {token['access_token']}"
}
response = self.request(
"get",
self.source.profile_url,
token=token["access_token"],
headers=headers,
response = self.session.request(
"get", self.source.profile_url, headers=headers,
)
response.raise_for_status()
except RequestException as exc:
@ -67,10 +64,6 @@ class BaseOAuthClient:
"Parse token and secret from raw token response."
raise NotImplementedError("Defined in a sub-class") # pragma: no cover
def request(self, method, url, **kwargs):
"Build remote url request."
return self._session.request(method, url, **kwargs)
@property
def session_key(self):
"""Return Session Key"""
@ -80,36 +73,48 @@ class BaseOAuthClient:
class OAuthClient(BaseOAuthClient):
"""OAuth1 Client"""
def get_access_token(self, request, callback=None):
_default_headers = {
"Accept": "application/json",
}
def get_access_token(
self, request: HttpRequest, callback=None
) -> Optional[Dict[str, str]]:
"Fetch access token from callback request."
raw_token = request.session.get(self.session_key, None)
verifier = request.GET.get("oauth_verifier", None)
if raw_token is not None and verifier is not None:
data = {"oauth_verifier": verifier}
data = {
"oauth_verifier": verifier,
"oauth_callback": callback,
"token": raw_token,
}
callback = request.build_absolute_uri(callback or request.path)
callback = force_text(callback)
try:
response = self.request(
response = self.session.request(
"post",
self.source.access_token_url,
token=raw_token,
data=data,
oauth_callback=callback,
headers=self._default_headers,
)
response.raise_for_status()
except RequestException as exc:
LOGGER.warning("Unable to fetch access token", exc=exc)
return None
else:
return response.text
return response.json()
return None
def get_request_token(self, request, callback):
"Fetch the OAuth request token. Only required for OAuth 1.0."
callback = force_text(request.build_absolute_uri(callback))
try:
response = self.request(
"post", self.source.request_token_url, oauth_callback=callback
response = self.session.request(
"post",
self.source.request_token_url,
data={"oauth_callback": callback},
headers=self._default_headers,
)
response.raise_for_status()
except RequestException as exc:
@ -154,7 +159,7 @@ class OAuthClient(BaseOAuthClient):
callback_uri=callback,
)
kwargs["auth"] = oauth
return super(OAuthClient, self).request(method, url, **kwargs)
return super(OAuthClient, self).session.request(method, url, **kwargs)
@property
def session_key(self):
@ -164,6 +169,10 @@ class OAuthClient(BaseOAuthClient):
class OAuth2Client(BaseOAuthClient):
"""OAuth2 Client"""
_default_headers = {
"Accept": "application/json",
}
# pylint: disable=unused-argument
def check_application_state(self, request, callback):
"Check optional state parameter."
@ -197,15 +206,19 @@ class OAuth2Client(BaseOAuthClient):
LOGGER.warning("No code returned by the source")
return None
try:
response = self.request(
"post", self.source.access_token_url, data=args, **request_kwargs
response = self.session.request(
"post",
self.source.access_token_url,
data=args,
headers=self._default_headers,
**request_kwargs,
)
response.raise_for_status()
except RequestException as exc:
LOGGER.warning("Unable to fetch access token", exc=exc)
return None
else:
return response.text
return response.json()
# pylint: disable=unused-argument
def get_application_state(self, request, callback):
@ -247,7 +260,7 @@ class OAuth2Client(BaseOAuthClient):
params = kwargs.get("params", {})
params["access_token"] = token
kwargs["params"] = params
return super(OAuth2Client, self).request(method, url, **kwargs)
return super(OAuth2Client, self).session.request(method, url, **kwargs)
@property
def session_key(self):

View File

@ -1,8 +1,6 @@
"""passbook oauth_client forms"""
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _
from passbook.admin.forms.source import SOURCE_FORM_FIELDS
from passbook.sources.oauth.models import OAuthSource
@ -36,7 +34,6 @@ class OAuthSourceForm(forms.ModelForm):
"consumer_key": forms.TextInput(),
"consumer_secret": forms.TextInput(),
"provider_type": forms.Select(choices=MANAGER.get_name_tuple()),
"policies": FilteredSelectMultiple(_("policies"), False),
}
@ -119,7 +116,7 @@ class AzureADOAuthSourceForm(OAuthSourceForm):
class Meta(OAuthSourceForm.Meta):
overrides = {
"provider_type": "azure_ad",
"provider_type": "azure-ad",
"request_token_url": "",
"authorization_url": "https://login.microsoftonline.com/common/oauth2/authorize",
"access_token_url": "https://login.microsoftonline.com/common/oauth2/token",

View File

@ -35,7 +35,7 @@ class OAuthSource(Source):
"passbook_sources_oauth:oauth-client-login",
kwargs={"source_slug": self.slug},
),
icon_path=f"passbook/sources/{self.provider_type.replace(' ', '-')}.svg",
icon_path=f"passbook/sources/{self.provider_type}.svg",
name=self.name,
)

View File

@ -1,6 +1,7 @@
"""Source type manager"""
from enum import Enum
from django.utils.text import slugify
from structlog import get_logger
from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect
@ -36,7 +37,7 @@ class SourceTypeManager:
def get_name_tuple(self):
"""Get list of tuples of all registered names"""
return [(x.lower(), x) for x in set(self.__names)]
return [(slugify(x), x) for x in set(self.__names)]
def find(self, source, kind):
"""Find fitting Source Type"""

View File

@ -89,13 +89,15 @@ class OAuthCallback(OAuthClientMixin, View):
client = self.get_client(self.source)
callback = self.get_callback_url(self.source)
# Fetch access token
raw_token = client.get_access_token(self.request, callback=callback)
if raw_token is None:
token = client.get_access_token(self.request, callback=callback)
if token is None:
return self.handle_login_failure(
self.source, "Could not retrieve token."
)
if "error" in token:
return self.handle_login_failure(self.source, token["error"])
# Fetch profile info
info = client.get_profile_info(raw_token)
info = client.get_profile_info(token)
if info is None:
return self.handle_login_failure(
self.source, "Could not retrieve profile."
@ -105,7 +107,7 @@ class OAuthCallback(OAuthClientMixin, View):
return self.handle_login_failure(self.source, "Could not determine id.")
# Get or create access record
defaults = {
"access_token": raw_token,
"access_token": token.get("access_token"),
}
existing = UserOAuthSourceConnection.objects.filter(
source=self.source, identifier=identifier
@ -113,13 +115,15 @@ class OAuthCallback(OAuthClientMixin, View):
if existing.exists():
connection = existing.first()
connection.access_token = raw_token
connection.access_token = token.get("access_token")
UserOAuthSourceConnection.objects.filter(pk=connection.pk).update(
**defaults
)
else:
connection = UserOAuthSourceConnection(
source=self.source, identifier=identifier, access_token=raw_token
source=self.source,
identifier=identifier,
access_token=token.get("access_token"),
)
user = authenticate(
source=self.source, identifier=identifier, request=request

View File

@ -4,7 +4,7 @@
{% load i18n %}
{% block title %}
{% title 'Authorize Application' %}
{% trans 'Authorize Application' %}
{% endblock %}
{% block card %}

View File

@ -1,2 +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="270px" height="20px" viewBox="0 0 150 10" enable-background="new 0 0 270 10" xml:space="preserve"><defs><style>.cls-1{isolation:isolate;}.cls-2{fill:#fff;}</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>
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:#fff;}</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>

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -7,33 +7,18 @@
z-index: 0;
}
@@ -1,204 +0,0 @@
.navbar-brand-name {
height: 35px;
/* login page's icons */
.pf-c-login__main-footer-links-item-link img {
fill: var(--pf-c-login__main-footer-links-item-link-svg--Fill);
width: 100%;
max-width: var(--pf-c-login__main-footer-links-item-link-svg--Width);
height: 100%;
max-height: var(--pf-c-login__main-footer-links-item-link-svg--Height);
}
.dynamic-array-widget .array-item {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.dynamic-array-widget .remove_sign {
width: 10px;
height: 2px;
background: #a41515;
border-radius: 1px;
}
.dynamic-array-widget .remove {
height: 15px;
display: flex;
align-items: center;
margin-left: 5px;
}
.dynamic-array-widget .remove:hover {
cursor: pointer;
/* fix multiple selects height */
select[multiple] {
height: initial;
}
/* Selector */
@ -212,3 +197,117 @@ form .form-row p.datetime {
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
}
}

View File

@ -36,3 +36,19 @@ document.querySelectorAll(".codemirror").forEach((cm) => {
autoRefresh: true,
});
});
// Automatic slug fields
const convertToSlug = (text) => {
return text
.toLowerCase()
.replace(/[^\w ]+/g, '')
.replace(/ +/g, '-');
};
document.querySelectorAll("input[name=name]").forEach((input) => {
input.addEventListener("input", (e) => {
const form = e.target.closest("form");
const slugField = form.querySelector("input[name=slug]");
slugField.value = convertToSlug(e.target.value);
});
});

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><path d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
<style type="text/css">
.st0{fill:#1DA1F2;}
.st1{fill:#FFFFFF;}
</style>
<g id="Dark_Blue">
<rect class="st0" width="400" height="400"/>
</g>
<g id="Logo__x2014__FIXED">
<path class="st1" d="M153.6,301.6c94.3,0,145.9-78.2,145.9-145.9c0-2.2,0-4.4-0.1-6.6c10-7.2,18.7-16.3,25.6-26.6
c-9.2,4.1-19.1,6.8-29.5,8.1c10.6-6.3,18.7-16.4,22.6-28.4c-9.9,5.9-20.9,10.1-32.6,12.4c-9.4-10-22.7-16.2-37.4-16.2
c-28.3,0-51.3,23-51.3,51.3c0,4,0.5,7.9,1.3,11.7c-42.6-2.1-80.4-22.6-105.7-53.6c-4.4,7.6-6.9,16.4-6.9,25.8
c0,17.8,9.1,33.5,22.8,42.7c-8.4-0.3-16.3-2.6-23.2-6.4c0,0.2,0,0.4,0,0.7c0,24.8,17.7,45.6,41.1,50.3c-4.3,1.2-8.8,1.8-13.5,1.8
c-3.3,0-6.5-0.3-9.6-0.9c6.5,20.4,25.5,35.2,47.9,35.6c-17.6,13.8-39.7,22-63.7,22c-4.1,0-8.2-0.2-12.2-0.7
C97.7,293.1,124.7,301.6,153.6,301.6"/>
</g>
<g id="Annotations">
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB