Compare commits
54 Commits
version/0.
...
version/0.
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ed2e137a2 | |||
| 45bd63c720 | |||
| 736e13fc35 | |||
| 966fff008c | |||
| 64f15eadbd | |||
| 81b66ecdcd | |||
| 53e5cf7826 | |||
| 82654b3fd9 | |||
| 9b72c604dd | |||
| 5fb1b8044c | |||
| b8daab4377 | |||
| c5b91bdae8 | |||
| 39a208c55f | |||
| a5bfef9b6b | |||
| f1f4cbef9b | |||
| 8388120b06 | |||
| 2bf96828f1 | |||
| 22838e66fe | |||
| 484dd6de09 | |||
| b743736c26 | |||
| af91e2079b | |||
| cad1c17f14 | |||
| 120d32e4dc | |||
| 238b489e07 | |||
| 4daa70c894 | |||
| f8599438df | |||
| 155c9a4c3f | |||
| 8433b5e583 | |||
| dc5ba144f1 | |||
| 521a8b5356 | |||
| 3453077d7b | |||
| 70ede8581a | |||
| 6e9d297f02 | |||
| 6a7545fd43 | |||
| a8926cbd07 | |||
| 64d7b009ab | |||
| 2b5fddb7bf | |||
| b99d23c119 | |||
| 03905b74ff | |||
| 6b8a59cfbd | |||
| d6fdcd3ef9 | |||
| 53ebc551d2 | |||
| 3d4f43d6e3 | |||
| 074cde7cd5 | |||
| 382e563590 | |||
| ca61a7cc21 | |||
| fa2870afe0 | |||
| 0f46207ea4 | |||
| 1e7d912144 | |||
| f4a676e2fb | |||
| b2c10e2387 | |||
| 8c329dca7d | |||
| 83da175749 | |||
| 995c87938f |
@ -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>.*)
|
||||
|
||||
70
.github/workflows/ci.yml
vendored
70
.github/workflows/ci.yml
vendored
@ -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##*/}
|
||||
|
||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@ -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
6
.isort.cfg
Normal file
@ -0,0 +1,6 @@
|
||||
[settings]
|
||||
multi_line_output=3
|
||||
include_trailing_comma=True
|
||||
force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: ""
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = "0.8.7-beta"
|
||||
__version__ = "0.8.12-beta"
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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}
|
||||
@ -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"]
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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"] = {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -18,7 +18,7 @@ class EventSerializer(ModelSerializer):
|
||||
"date",
|
||||
"app",
|
||||
"context",
|
||||
"request_ip",
|
||||
"client_ip",
|
||||
"created",
|
||||
]
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ class PropertyMappingSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
|
||||
model = PropertyMapping
|
||||
fields = ["pk", "name", "__type__"]
|
||||
fields = ["pk", "name", "expression", "__type__"]
|
||||
|
||||
|
||||
class PropertyMappingViewSet(ReadOnlyModelViewSet):
|
||||
|
||||
@ -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:
|
||||
|
||||
33
passbook/core/migrations/0010_auto_20200221_2208.py
Normal file
33
passbook/core/migrations/0010_auto_20200221_2208.py
Normal 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=""),
|
||||
),
|
||||
]
|
||||
27
passbook/core/migrations/0011_auto_20200222_1822.py
Normal file
27
passbook/core/migrations/0011_auto_20200222_1822.py
Normal 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)]
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
27
passbook/core/templates/403_csrf.html
Normal file
27
passbook/core/templates/403_csrf.html
Normal 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 %}
|
||||
58
passbook/core/templates/base/page.html
Normal file
58
passbook/core/templates/base/page.html
Normal 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 %}
|
||||
@ -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' %}">
|
||||
|
||||
@ -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;">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
{% load utils %}
|
||||
|
||||
{% block title %}
|
||||
{% title title %}
|
||||
{% trans title %}
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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."
|
||||
)
|
||||
}
|
||||
|
||||
@ -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."
|
||||
)
|
||||
}
|
||||
|
||||
@ -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."
|
||||
)
|
||||
}
|
||||
|
||||
@ -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."
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)"
|
||||
|
||||
@ -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
9
passbook/lib/logging.py
Normal 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
|
||||
0
passbook/lib/management/commands/__init__.py
Normal file
0
passbook/lib/management/commands/__init__.py
Normal file
66
passbook/lib/management/commands/bootstrap.py
Normal file
66
passbook/lib/management/commands/bootstrap.py
Normal 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)
|
||||
@ -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,
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
0
passbook/policies/expression/tests/__init__.py
Normal file
0
passbook/policies/expression/tests/__init__.py
Normal file
58
passbook/policies/expression/tests/test_evaluator.py
Normal file
58
passbook/policies/expression/tests/test_evaluator.py
Normal 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)
|
||||
@ -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)
|
||||
|
||||
@ -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}>"
|
||||
|
||||
@ -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"]
|
||||
|
||||
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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 %}
|
||||
|
||||
18
passbook/providers/oidc/templates/oidc_provider/error.html
Normal file
18
passbook/providers/oidc/templates/oidc_provider/error.html
Normal 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 %}
|
||||
@ -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()
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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",
|
||||
},
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
@ -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"""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
{% title 'Authorize Application' %}
|
||||
{% trans 'Authorize Application' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
|
||||
@ -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 |
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
1
passbook/static/static/passbook/sources/discord.svg
Normal file
1
passbook/static/static/passbook/sources/discord.svg
Normal 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 |
22
passbook/static/static/passbook/sources/twitter.svg
Executable file
22
passbook/static/static/passbook/sources/twitter.svg
Executable 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 |
Reference in New Issue
Block a user