Compare commits
35 Commits
version/0.
...
version/0.
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.8.8-beta
|
current_version = 0.8.11-beta
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||||
|
|||||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@ -16,11 +16,11 @@ jobs:
|
|||||||
- name: Building Docker Image
|
- name: Building Docker Image
|
||||||
run: docker build
|
run: docker build
|
||||||
--no-cache
|
--no-cache
|
||||||
-t beryju/passbook:0.8.8-beta
|
-t beryju/passbook:0.8.11-beta
|
||||||
-t beryju/passbook:latest
|
-t beryju/passbook:latest
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/passbook:0.8.8-beta
|
run: docker push beryju/passbook:0.8.11-beta
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/passbook:latest
|
run: docker push beryju/passbook:latest
|
||||||
build-gatekeeper:
|
build-gatekeeper:
|
||||||
@ -37,11 +37,11 @@ jobs:
|
|||||||
cd gatekeeper
|
cd gatekeeper
|
||||||
docker build \
|
docker build \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
-t beryju/passbook-gatekeeper:0.8.8-beta \
|
-t beryju/passbook-gatekeeper:0.8.11-beta \
|
||||||
-t beryju/passbook-gatekeeper:latest \
|
-t beryju/passbook-gatekeeper:latest \
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/passbook-gatekeeper:0.8.8-beta
|
run: docker push beryju/passbook-gatekeeper:0.8.11-beta
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/passbook-gatekeeper:latest
|
run: docker push beryju/passbook-gatekeeper:latest
|
||||||
build-static:
|
build-static:
|
||||||
@ -66,11 +66,11 @@ jobs:
|
|||||||
run: docker build
|
run: docker build
|
||||||
--no-cache
|
--no-cache
|
||||||
--network=$(docker network ls | grep github | awk '{print $1}')
|
--network=$(docker network ls | grep github | awk '{print $1}')
|
||||||
-t beryju/passbook-static:0.8.8-beta
|
-t beryju/passbook-static:0.8.11-beta
|
||||||
-t beryju/passbook-static:latest
|
-t beryju/passbook-static:latest
|
||||||
-f static.Dockerfile .
|
-f static.Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/passbook-static:0.8.8-beta
|
run: docker push beryju/passbook-static:0.8.11-beta
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/passbook-static:latest
|
run: docker push beryju/passbook-static:latest
|
||||||
test-release:
|
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
|
ignored-modules=django-otp
|
||||||
jobs=4
|
jobs=4
|
||||||
|
|
||||||
[SIMILARITIES]
|
|
||||||
|
|
||||||
# Minimum lines number of a similarity.
|
|
||||||
min-similarity-lines=20
|
|
||||||
|
|||||||
@ -23,6 +23,8 @@ services:
|
|||||||
server:
|
server:
|
||||||
image: beryju/passbook:${SERVER_TAG:-latest}
|
image: beryju/passbook:${SERVER_TAG:-latest}
|
||||||
command:
|
command:
|
||||||
|
- ./manage.py
|
||||||
|
- bootstrap
|
||||||
- uwsgi
|
- uwsgi
|
||||||
- uwsgi.ini
|
- uwsgi.ini
|
||||||
environment:
|
environment:
|
||||||
@ -42,6 +44,8 @@ services:
|
|||||||
worker:
|
worker:
|
||||||
image: beryju/passbook:${SERVER_TAG:-latest}
|
image: beryju/passbook:${SERVER_TAG:-latest}
|
||||||
command:
|
command:
|
||||||
|
- ./manage.py
|
||||||
|
- bootstrap
|
||||||
- celery
|
- celery
|
||||||
- worker
|
- worker
|
||||||
- --autoscale=10,3
|
- --autoscale=10,3
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
appVersion: "0.8.8-beta"
|
appVersion: "0.8.11-beta"
|
||||||
description: A Helm chart for passbook.
|
description: A Helm chart for passbook.
|
||||||
name: passbook
|
name: passbook
|
||||||
version: "0.8.8-beta"
|
version: "0.8.11-beta"
|
||||||
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png
|
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png
|
||||||
|
|||||||
@ -18,7 +18,7 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
passbook.io/component: web
|
k8s.passbook.io/component: web
|
||||||
spec:
|
spec:
|
||||||
volumes:
|
volumes:
|
||||||
- name: config-volume
|
- name: config-volume
|
||||||
@ -27,9 +27,12 @@ spec:
|
|||||||
initContainers:
|
initContainers:
|
||||||
- name: passbook-database-migrations
|
- name: passbook-database-migrations
|
||||||
image: "beryju/passbook:{{ .Values.image.tag }}"
|
image: "beryju/passbook:{{ .Values.image.tag }}"
|
||||||
|
imagePullPolicy: Always
|
||||||
command:
|
command:
|
||||||
- ./manage.py
|
- ./manage.py
|
||||||
args:
|
args:
|
||||||
|
- bootstrap
|
||||||
|
- ./manage.py
|
||||||
- migrate
|
- migrate
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: /etc/passbook
|
- mountPath: /etc/passbook
|
||||||
@ -57,10 +60,12 @@ spec:
|
|||||||
containers:
|
containers:
|
||||||
- name: {{ .Chart.Name }}
|
- name: {{ .Chart.Name }}
|
||||||
image: "beryju/passbook:{{ .Values.image.tag }}"
|
image: "beryju/passbook:{{ .Values.image.tag }}"
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: Always
|
||||||
command:
|
command:
|
||||||
- uwsgi
|
- ./manage.py
|
||||||
args:
|
args:
|
||||||
|
- bootstrap
|
||||||
|
- uwsgi
|
||||||
- uwsgi.ini
|
- uwsgi.ini
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: /etc/passbook
|
- mountPath: /etc/passbook
|
||||||
|
|||||||
@ -18,4 +18,4 @@ spec:
|
|||||||
selector:
|
selector:
|
||||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
passbook.io/component: web
|
k8s.passbook.io/component: web
|
||||||
|
|||||||
@ -18,7 +18,7 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
passbook.io/component: worker
|
k8s.passbook.io/component: worker
|
||||||
spec:
|
spec:
|
||||||
volumes:
|
volumes:
|
||||||
- name: config-volume
|
- name: config-volume
|
||||||
@ -29,8 +29,10 @@ spec:
|
|||||||
image: "beryju/passbook:{{ .Values.image.tag }}"
|
image: "beryju/passbook:{{ .Values.image.tag }}"
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- celery
|
- ./manage.py
|
||||||
args:
|
args:
|
||||||
|
- bootstrap
|
||||||
|
- celery
|
||||||
- worker
|
- worker
|
||||||
- --autoscale=10,3
|
- --autoscale=10,3
|
||||||
- -E
|
- -E
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
# This is a YAML-formatted file.
|
# This is a YAML-formatted file.
|
||||||
# Declare variables to be passed into your templates.
|
# Declare variables to be passed into your templates.
|
||||||
image:
|
image:
|
||||||
tag: 0.8.8-beta
|
tag: 0.8.11-beta
|
||||||
|
|
||||||
nameOverride: ""
|
nameOverride: ""
|
||||||
|
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
"""passbook"""
|
"""passbook"""
|
||||||
__version__ = "0.8.8-beta"
|
__version__ = "0.8.11-beta"
|
||||||
|
|||||||
@ -48,7 +48,7 @@ class YAMLField(forms.CharField):
|
|||||||
def prepare_value(self, value):
|
def prepare_value(self, value):
|
||||||
if isinstance(value, InvalidYAMLInput):
|
if isinstance(value, InvalidYAMLInput):
|
||||||
return value
|
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):
|
def has_changed(self, initial, data):
|
||||||
if super().has_changed(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}
|
|
||||||
@ -11,16 +11,22 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block beneath_form %}
|
{% block beneath_form %}
|
||||||
<p class="loading" style="display: none;">
|
<div class="pf-c-form__group pf-m-action" style="display: none;" id="loading">
|
||||||
<span class="spinner spinner-xs spinner-inline"></span> {% trans 'Processing, please wait...' %}
|
<div class="pf-c-form__horizontal-group">
|
||||||
</p>
|
<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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<script>
|
<script>
|
||||||
$('form').on('submit', function () {
|
document.querySelector("form").addEventListener("submit", (e) => {
|
||||||
$('p.loading').show();
|
document.getElementById("loading").removeAttribute("style");
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -9,7 +9,11 @@ from django.shortcuts import redirect
|
|||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.views.generic import DeleteView, DetailView, ListView, UpdateView
|
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.admin.forms.users import UserForm
|
||||||
from passbook.core.models import Nonce, User
|
from passbook.core.models import Nonce, User
|
||||||
@ -25,6 +29,9 @@ class UserListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
|||||||
paginate_by = 40
|
paginate_by = 40
|
||||||
template_name = "administration/user/list.html"
|
template_name = "administration/user/list.html"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().exclude(pk=get_anonymous_user().pk)
|
||||||
|
|
||||||
|
|
||||||
class UserCreateView(
|
class UserCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
|||||||
@ -33,11 +33,15 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
|||||||
source[key] = sanitize_dict(value)
|
source[key] = sanitize_dict(value)
|
||||||
elif isinstance(value, models.Model):
|
elif isinstance(value, models.Model):
|
||||||
model_content_type = ContentType.objects.get_for_model(value)
|
model_content_type = ContentType.objects.get_for_model(value)
|
||||||
|
name = str(value)
|
||||||
|
if hasattr(value, "name"):
|
||||||
|
name = value.name
|
||||||
source[key] = sanitize_dict(
|
source[key] = sanitize_dict(
|
||||||
{
|
{
|
||||||
"app": model_content_type.app_label,
|
"app": model_content_type.app_label,
|
||||||
"name": model_content_type.model,
|
"model_name": model_content_type.model,
|
||||||
"pk": value.pk,
|
"pk": value.pk,
|
||||||
|
"name": name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif isinstance(value, UUID):
|
elif isinstance(value, UUID):
|
||||||
@ -133,6 +137,7 @@ class Event(UUIDModel):
|
|||||||
action=self.action,
|
action=self.action,
|
||||||
context=self.context,
|
context=self.context,
|
||||||
client_ip=self.client_ip,
|
client_ip=self.client_ip,
|
||||||
|
user=self.user,
|
||||||
)
|
)
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ class PropertyMappingSerializer(ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = PropertyMapping
|
model = PropertyMapping
|
||||||
fields = ["pk", "name", "__type__"]
|
fields = ["pk", "name", "expression", "__type__"]
|
||||||
|
|
||||||
|
|
||||||
class PropertyMappingViewSet(ReadOnlyModelViewSet):
|
class PropertyMappingViewSet(ReadOnlyModelViewSet):
|
||||||
|
|||||||
@ -10,7 +10,8 @@ class ApplicationForm(forms.ModelForm):
|
|||||||
"""Application Form"""
|
"""Application Form"""
|
||||||
|
|
||||||
provider = forms.ModelChoiceField(
|
provider = forms.ModelChoiceField(
|
||||||
queryset=Provider.objects.all().select_subclasses(), required=False
|
queryset=Provider.objects.all().order_by("pk").select_subclasses(),
|
||||||
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@ -18,9 +18,11 @@ password_changed = Signal(providing_args=["user", "password"])
|
|||||||
def invalidate_policy_cache(sender, instance, **_):
|
def invalidate_policy_cache(sender, instance, **_):
|
||||||
"""Invalidate Policy cache when policy is updated"""
|
"""Invalidate Policy cache when policy is updated"""
|
||||||
from passbook.core.models import Policy
|
from passbook.core.models import Policy
|
||||||
|
from passbook.policies.process import cache_key
|
||||||
|
|
||||||
if isinstance(instance, Policy):
|
if isinstance(instance, Policy):
|
||||||
LOGGER.debug("Invalidating policy cache", policy=instance)
|
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)
|
cache.delete_many(keys)
|
||||||
LOGGER.debug("Deleted %d keys", len(keys))
|
LOGGER.debug("Deleted %d keys", len(keys))
|
||||||
|
|||||||
@ -54,9 +54,10 @@
|
|||||||
<a href="{{ source.url }}" class="pf-c-login__main-footer-links-item-link">
|
<a href="{{ source.url }}" class="pf-c-login__main-footer-links-item-link">
|
||||||
{% if source.icon_path %}
|
{% if source.icon_path %}
|
||||||
<img src="{% static source.icon_path %}" alt="{{ source.name }}">
|
<img src="{% static source.icon_path %}" alt="{{ source.name }}">
|
||||||
{% endif %}
|
{% elif source.icon_url %}
|
||||||
{% if source.icon_url %}
|
|
||||||
<img src="icon_url" alt="{{ source.name }}">
|
<img src="icon_url" alt="{{ source.name }}">
|
||||||
|
{% else %}
|
||||||
|
<i class="pf-icon pf-icon-arrow" title="{{ source.name }}"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -4,25 +4,16 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load utils %}
|
{% load utils %}
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
{{ block.super }}
|
|
||||||
<style>
|
|
||||||
.pf-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block card %}
|
{% block card %}
|
||||||
<header class="login-pf-header">
|
<form method="POST" class="pf-c-form">
|
||||||
<h1>{% trans title %}</h1>
|
|
||||||
</header>
|
|
||||||
<form method="POST">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include 'partials/form.html' %}
|
{% include 'partials/form.html' %}
|
||||||
<span class="pf-icon pficon-error-circle-o btn-block"></span>
|
<div class="pf-c-form__group">
|
||||||
Access denied
|
<p>
|
||||||
|
<i class="pf-icon pf-icon-error-circle-o"></i>
|
||||||
|
{% trans 'Access denied' %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{% if 'back' in request.GET %}
|
{% if 'back' in request.GET %}
|
||||||
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
|
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -44,13 +44,13 @@ class LoginView(UserPassesTestMixin, FormView):
|
|||||||
kwargs["primary_action"] = _("Log in")
|
kwargs["primary_action"] = _("Log in")
|
||||||
kwargs["show_sign_up_notice"] = CONFIG.y("passbook.sign_up.enabled")
|
kwargs["show_sign_up_notice"] = CONFIG.y("passbook.sign_up.enabled")
|
||||||
kwargs["sources"] = []
|
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:
|
for source in sources:
|
||||||
ui_login_button = source.ui_login_button
|
ui_login_button = source.ui_login_button
|
||||||
if ui_login_button:
|
if ui_login_button:
|
||||||
kwargs["sources"].append(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)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
def get_user(self, uid_value) -> Optional[User]:
|
def get_user(self, uid_value) -> Optional[User]:
|
||||||
@ -231,7 +231,6 @@ class PasswordResetView(View):
|
|||||||
login(request, nonce.user)
|
login(request, nonce.user)
|
||||||
nonce.delete()
|
nonce.delete()
|
||||||
messages.success(
|
messages.success(
|
||||||
request,
|
request, _(("Temporarily authenticated, please change your password")),
|
||||||
_(("Temporarily authenticated with Nonce, " "please change your password")),
|
|
||||||
)
|
)
|
||||||
return redirect("passbook_core:user-change-password")
|
return redirect("passbook_core:user-change-password")
|
||||||
|
|||||||
@ -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/__init__.py
Normal file
0
passbook/lib/management/__init__.py
Normal file
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"""
|
"""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
|
from structlog import get_logger
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
class SentryIgnoredException(Exception):
|
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):
|
def before_send(event, hint):
|
||||||
"""Check if error is database error, and ignore if so"""
|
"""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 = (
|
ignored_classes = (
|
||||||
OperationalError,
|
OperationalError,
|
||||||
|
InternalError,
|
||||||
|
ProgrammingError,
|
||||||
ConnectionInterrupted,
|
ConnectionInterrupted,
|
||||||
APIException,
|
APIException,
|
||||||
InternalError,
|
|
||||||
ConnectionResetError,
|
ConnectionResetError,
|
||||||
WorkerLostError,
|
WorkerLostError,
|
||||||
DisallowedHost,
|
DisallowedHost,
|
||||||
|
|||||||
@ -19,7 +19,7 @@ LOGGER = get_logger()
|
|||||||
|
|
||||||
|
|
||||||
class Evaluator:
|
class Evaluator:
|
||||||
"""Validate and evaulate jinja2-based expressions"""
|
"""Validate and evaluate jinja2-based expressions"""
|
||||||
|
|
||||||
_env: NativeEnvironment
|
_env: NativeEnvironment
|
||||||
|
|
||||||
@ -51,11 +51,12 @@ class Evaluator:
|
|||||||
"""Return dictionary with additional global variables passed to expression"""
|
"""Return dictionary with additional global variables passed to expression"""
|
||||||
# update passbook/policies/expression/templates/policy/expression/form.html
|
# update passbook/policies/expression/templates/policy/expression/form.html
|
||||||
# update docs/policies/expression/index.md
|
# update docs/policies/expression/index.md
|
||||||
|
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(
|
kwargs["pb_is_sso_flow"] = request.http_request.session.get(
|
||||||
AuthenticationView.SESSION_IS_SSO_LOGIN, False
|
AuthenticationView.SESSION_IS_SSO_LOGIN, False
|
||||||
)
|
)
|
||||||
kwargs["pb_is_group_member"] = Evaluator.jinja2_func_is_group_member
|
|
||||||
kwargs["pb_logger"] = get_logger()
|
|
||||||
kwargs["pb_client_ip"] = (
|
kwargs["pb_client_ip"] = (
|
||||||
get_client_ip(request.http_request) or "255.255.255.255"
|
get_client_ip(request.http_request) or "255.255.255.255"
|
||||||
)
|
)
|
||||||
@ -81,7 +82,7 @@ class Evaluator:
|
|||||||
req=request,
|
req=request,
|
||||||
)
|
)
|
||||||
return PolicyResult(False)
|
return PolicyResult(False)
|
||||||
if isinstance(result, list) and len(result) == 2:
|
if isinstance(result, (list, tuple)) and len(result) == 2:
|
||||||
return PolicyResult(*result)
|
return PolicyResult(*result)
|
||||||
if result:
|
if result:
|
||||||
return PolicyResult(result)
|
return PolicyResult(result)
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<p>
|
<p>
|
||||||
Expression using <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/">Jinja</a>. Following variables are available:
|
Expression using <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/">Jinja</a>. Following variables are available:
|
||||||
</p>
|
</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.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.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>request.obj</code>: Model the Policy is run against. </li>
|
||||||
@ -19,7 +19,7 @@
|
|||||||
<li><code>pb_client_ip</code>: Client's IP Address.</li>
|
<li><code>pb_client_ip</code>: Client's IP Address.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>Custom Filters:</p>
|
<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_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>
|
<li><code>regex_replace(regex, repl)</code>: Replace string matched by <code>regex</code> with <code>repl</code></li>
|
||||||
</ul>
|
</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 django.core.cache import cache
|
||||||
from structlog import get_logger
|
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.exceptions import PolicyException
|
||||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
def cache_key(policy, user):
|
def cache_key(policy: Policy, user: User = None) -> str:
|
||||||
"""Generate Cache key for policy"""
|
"""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):
|
class PolicyProcess(Process):
|
||||||
@ -33,7 +36,7 @@ class PolicyProcess(Process):
|
|||||||
def run(self):
|
def run(self):
|
||||||
"""Task wrapper to run policy checking"""
|
"""Task wrapper to run policy checking"""
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Running policy",
|
"P_ENG(proc): Running policy",
|
||||||
policy=self.policy,
|
policy=self.policy,
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
process="PolicyProcess",
|
process="PolicyProcess",
|
||||||
@ -41,13 +44,13 @@ class PolicyProcess(Process):
|
|||||||
try:
|
try:
|
||||||
policy_result = self.policy.passes(self.request)
|
policy_result = self.policy.passes(self.request)
|
||||||
except PolicyException as exc:
|
except PolicyException as exc:
|
||||||
LOGGER.debug(exc)
|
LOGGER.debug("P_ENG(proc): error", exc=exc)
|
||||||
policy_result = PolicyResult(False, str(exc))
|
policy_result = PolicyResult(False, str(exc))
|
||||||
# Invert result if policy.negate is set
|
# Invert result if policy.negate is set
|
||||||
if self.policy.negate:
|
if self.policy.negate:
|
||||||
policy_result.passing = not policy_result.passing
|
policy_result.passing = not policy_result.passing
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Got result",
|
"P_ENG(proc): Finished",
|
||||||
policy=self.policy,
|
policy=self.policy,
|
||||||
result=policy_result,
|
result=policy_result,
|
||||||
process="PolicyProcess",
|
process="PolicyProcess",
|
||||||
@ -56,5 +59,5 @@ class PolicyProcess(Process):
|
|||||||
)
|
)
|
||||||
key = cache_key(self.policy, self.request.user)
|
key = cache_key(self.policy, self.request.user)
|
||||||
cache.set(key, policy_result)
|
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)
|
self.connection.send(policy_result)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"""policy structures"""
|
"""policy structures"""
|
||||||
from __future__ import annotations
|
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.db.models import Model
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
@ -14,11 +14,13 @@ class PolicyRequest:
|
|||||||
"""Data-class to hold policy request data"""
|
"""Data-class to hold policy request data"""
|
||||||
|
|
||||||
user: User
|
user: User
|
||||||
http_request: HttpRequest
|
http_request: Optional[HttpRequest]
|
||||||
obj: Model
|
obj: Optional[Model]
|
||||||
|
|
||||||
def __init__(self, user: User):
|
def __init__(self, user: User):
|
||||||
self.user = user
|
self.user = user
|
||||||
|
self.http_request = None
|
||||||
|
self.obj = None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"<PolicyRequest user={self.user}>"
|
return f"<PolicyRequest user={self.user}>"
|
||||||
|
|||||||
@ -3,15 +3,12 @@
|
|||||||
{% load utils %}
|
{% load utils %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}
|
{% block card_title %}
|
||||||
{% title 'Authorize Application' %}
|
{% title 'Authorize Application' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block card %}
|
{% block card %}
|
||||||
<header class="login-pf-header">
|
<form method="POST" class="pf-c-form">
|
||||||
<h1>{% trans 'Authorize Application' %}</h1>
|
|
||||||
</header>
|
|
||||||
<form method="POST">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if not error %}
|
{% if not error %}
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@ -20,32 +17,40 @@
|
|||||||
{{ field }}
|
{{ field }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="form-group">
|
<div class="pf-c-form__group">
|
||||||
<p class="subtitle">
|
<p class="subtitle">
|
||||||
{% blocktrans with remote=application.name %}
|
{% blocktrans with remote=application.name %}
|
||||||
You're about to sign into {{ remote }}
|
You're about to sign into {{ remote }}.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
<p>{% trans "Application requires following permissions" %}</p>
|
<p>{% trans "Application requires following permissions" %}</p>
|
||||||
<ul>
|
<ul class="pf-c-list">
|
||||||
{% for scope in scopes_descriptions %}
|
{% for scope in scopes_descriptions %}
|
||||||
<li>{{ scope }}</li>
|
<li>{{ scope }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{{ form.errors }}
|
{{ form.errors }}
|
||||||
{{ form.non_field_errors }}
|
{{ form.non_field_errors }}
|
||||||
|
</div>
|
||||||
|
<div class="pf-c-form__group">
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans with user=user %}
|
{% blocktrans with user=user %}
|
||||||
You are logged in as {{ user }}. Not you?
|
You are logged in as {{ user }}. Not you?
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
<a href="{% url 'passbook_core:auth-logout' %}">{% trans 'Logout' %}</a>
|
<a href="{% url 'passbook_core:auth-logout' %}">{% trans 'Logout' %}</a>
|
||||||
</p>
|
</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>
|
||||||
<div class="form-group spinner-hidden hidden">
|
<div class="pf-c-form__group pf-m-action">
|
||||||
<div class="spinner"></div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -61,9 +66,8 @@
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
$('.click-spinner').on('click', function (e) {
|
document.querySelector("form").addEventListener("submit", (e) => {
|
||||||
$('.spinner-hidden').removeClass('hidden');
|
document.getElementById("loading").removeAttribute("style");
|
||||||
$(e.target).addClass('disabled');
|
});
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -19,6 +19,8 @@ class OIDCProviderForm(forms.ModelForm):
|
|||||||
self.fields["client_secret"].initial = generate_client_secret()
|
self.fields["client_secret"].initial = generate_client_secret()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
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)
|
response = super().save(*args, **kwargs)
|
||||||
# Check if openidprovider class instance exists
|
# Check if openidprovider class instance exists
|
||||||
if not OpenIDProvider.objects.filter(oidc_client=self.instance).exists():
|
if not OpenIDProvider.objects.filter(oidc_client=self.instance).exists():
|
||||||
|
|||||||
@ -3,15 +3,12 @@
|
|||||||
{% load utils %}
|
{% load utils %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}
|
{% block card_title %}
|
||||||
{% title 'Authorize Application' %}
|
{% trans 'Authorize Application' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block card %}
|
{% block card %}
|
||||||
<header class="login-pf-header">
|
<form method="POST" class="pf-c-form">
|
||||||
<h1>{% trans 'Authorize Application' %}</h1>
|
|
||||||
</header>
|
|
||||||
<form method="POST">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if not error %}
|
{% if not error %}
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@ -20,14 +17,14 @@
|
|||||||
{{ field }}
|
{{ field }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="form-group">
|
<div class="pf-c-form__group">
|
||||||
<p class="subtitle">
|
<p class="subtitle">
|
||||||
{% blocktrans with remote=client.name %}
|
{% blocktrans with remote=client.name %}
|
||||||
You're about to sign into {{ remote }}
|
You're about to sign into {{ remote }}.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
<p>{% trans "Application requires following permissions" %}</p>
|
<p>{% trans "Application requires following permissions" %}</p>
|
||||||
<ul>
|
<ul class="pf-c-list">
|
||||||
{% for scope in scopes %}
|
{% for scope in scopes %}
|
||||||
<li>{{ scope.name }}</li>
|
<li>{{ scope.name }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -35,18 +32,26 @@
|
|||||||
{{ hidden_inputs }}
|
{{ hidden_inputs }}
|
||||||
{{ form.errors }}
|
{{ form.errors }}
|
||||||
{{ form.non_field_errors }}
|
{{ form.non_field_errors }}
|
||||||
|
</div>
|
||||||
|
<div class="pf-c-form__group">
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans with user=user %}
|
{% blocktrans with user=user %}
|
||||||
You are logged in as {{ user }}. Not you?
|
You are logged in as {{ user }}. Not you?
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
<a href="{% url 'passbook_core:auth-logout' %}">{% trans 'Logout' %}</a>
|
<a href="{% url 'passbook_core:auth-logout' %}">{% trans 'Logout' %}</a>
|
||||||
</p>
|
</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>
|
||||||
<div class="form-group spinner-hidden hidden">
|
<div class="pf-c-form__group pf-m-action">
|
||||||
<div class="spinner"></div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -62,9 +67,8 @@
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
$('.click-spinner').on('click', function (e) {
|
document.querySelector("form").addEventListener("submit", (e) => {
|
||||||
$('.spinner-hidden').removeClass('hidden');
|
document.getElementById("loading").removeAttribute("style");
|
||||||
$(e.target).addClass('disabled');
|
});
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% 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:
|
try:
|
||||||
self._extract_saml_request()
|
self._extract_saml_request()
|
||||||
except KeyError:
|
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:
|
try:
|
||||||
self._decode_and_parse_request()
|
self._decode_and_parse_request()
|
||||||
|
|||||||
@ -3,23 +3,17 @@
|
|||||||
{% load utils %}
|
{% load utils %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}
|
{% block card_title %}
|
||||||
{% title 'Redirecting...' %}
|
{% title 'Redirecting...' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block card %}
|
{% block card %}
|
||||||
<header class="login-pf-header">
|
|
||||||
<h1>{% trans 'Redirecting...' %}</h1>
|
|
||||||
</header>
|
|
||||||
<form method="POST" action="{{ url }}">
|
<form method="POST" action="{{ url }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% for key, value in attrs.items %}
|
{% for key, value in attrs.items %}
|
||||||
<input type="hidden" name="{{ key }}" value="{{ value }}">
|
<input type="hidden" name="{{ key }}" value="{{ value }}">
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="login-group">
|
<div class="login-group">
|
||||||
<h3>
|
|
||||||
{% trans "Redirecting..." %}
|
|
||||||
</h3>
|
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans with user=user %}
|
{% blocktrans with user=user %}
|
||||||
You are logged in as {{ user }}.
|
You are logged in as {{ user }}.
|
||||||
@ -34,6 +28,6 @@
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<script>
|
<script>
|
||||||
$('form').submit();
|
document.querySelector("form").submit();
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -4,11 +4,8 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block card %}
|
{% block card %}
|
||||||
<form method="POST" class="pf-c-form" action="{{ saml_params.acs_url }}">
|
<form method="POST" class="pf-c-form">
|
||||||
{% csrf_token %}
|
{% 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">
|
<div class="pf-c-form__group">
|
||||||
<h3>
|
<h3>
|
||||||
{% blocktrans with provider=provider.application.name %}
|
{% blocktrans with provider=provider.application.name %}
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<div class="c-form__horizontal-group">
|
<div class="c-form__horizontal-group">
|
||||||
<p>
|
<p>
|
||||||
Expression using <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/">Jinja</a>. Following variables are available:
|
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>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>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>
|
<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:Subject>
|
||||||
<saml:NameID Format="{{ SUBJECT_FORMAT }}">
|
<saml:NameID Format="{{ SUBJECT_FORMAT }}">{{ SUBJECT }}</saml:NameID>
|
||||||
{{ SUBJECT }}
|
|
||||||
</saml:NameID>
|
|
||||||
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
|
<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:SubjectConfirmationData {{ IN_RESPONSE_TO|safe }} NotOnOrAfter="{{ NOT_ON_OR_AFTER }}" Recipient="{{ ACS_URL }}" />
|
||||||
</saml:SubjectConfirmation>
|
</saml:SubjectConfirmation>
|
||||||
|
|||||||
@ -16,9 +16,9 @@ urlpatterns = [
|
|||||||
"<slug:application>/login/", views.LoginBeginView.as_view(), name="saml-login"
|
"<slug:application>/login/", views.LoginBeginView.as_view(), name="saml-login"
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"<slug:application>/login/process/",
|
"<slug:application>/login/authorize/",
|
||||||
views.LoginProcessView.as_view(),
|
views.AuthorizeView.as_view(),
|
||||||
name="saml-login-process",
|
name="saml-login-authorize",
|
||||||
),
|
),
|
||||||
path("<slug:application>/logout/", views.LogoutView.as_view(), name="saml-logout"),
|
path("<slug:application>/logout/", views.LogoutView.as_view(), name="saml-logout"),
|
||||||
path(
|
path(
|
||||||
|
|||||||
@ -21,12 +21,16 @@ from passbook.core.models import Application, Provider
|
|||||||
from passbook.lib.utils.template import render_to_string
|
from passbook.lib.utils.template import render_to_string
|
||||||
from passbook.lib.views import bad_request_message
|
from passbook.lib.views import bad_request_message
|
||||||
from passbook.policies.engine import PolicyEngine
|
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.models import SAMLProvider
|
||||||
from passbook.providers.saml.processors.types import SAMLResponseParams
|
from passbook.providers.saml.processors.types import SAMLResponseParams
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
URL_VALIDATOR = URLValidator(schemes=("http", "https"))
|
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):
|
class AccessRequiredView(AccessMixin, View):
|
||||||
@ -50,14 +54,18 @@ class AccessRequiredView(AccessMixin, View):
|
|||||||
|
|
||||||
def _has_access(self) -> bool:
|
def _has_access(self) -> bool:
|
||||||
"""Check if user has access to application"""
|
"""Check if user has access to application"""
|
||||||
LOGGER.debug(
|
|
||||||
"_has_access", user=self.request.user, app=self.provider.application
|
|
||||||
)
|
|
||||||
policy_engine = PolicyEngine(
|
policy_engine = PolicyEngine(
|
||||||
self.provider.application.policies.all(), self.request.user, self.request
|
self.provider.application.policies.all(), self.request.user, self.request
|
||||||
)
|
)
|
||||||
policy_engine.build()
|
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:
|
def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
@ -75,80 +83,29 @@ class LoginBeginView(AccessRequiredView):
|
|||||||
"""Receives a SAML 2.0 AuthnRequest from a Service Provider and
|
"""Receives a SAML 2.0 AuthnRequest from a Service Provider and
|
||||||
stores it in the session prior to enforcing login."""
|
stores it in the session prior to enforcing login."""
|
||||||
|
|
||||||
@method_decorator(csrf_exempt)
|
def handler(self, source, application: str) -> HttpResponse:
|
||||||
def dispatch(self, request: HttpRequest, application: str) -> HttpResponse:
|
"""Handle SAML Request whether its a POST or a Redirect binding"""
|
||||||
if request.method == "POST":
|
|
||||||
source = request.POST
|
|
||||||
else:
|
|
||||||
source = request.GET
|
|
||||||
|
|
||||||
# Store these values now, because Django's login cycle won't preserve them.
|
# Store these values now, because Django's login cycle won't preserve them.
|
||||||
try:
|
try:
|
||||||
request.session["SAMLRequest"] = source["SAMLRequest"]
|
self.request.session[SESSION_KEY_SAML_REQUEST] = source[
|
||||||
|
SESSION_KEY_SAML_REQUEST
|
||||||
|
]
|
||||||
except (KeyError, MultiValueDictKeyError):
|
except (KeyError, MultiValueDictKeyError):
|
||||||
return bad_request_message(request, "The SAML request payload is missing.")
|
return bad_request_message(
|
||||||
|
self.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},
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.request.session[SESSION_KEY_RELAY_STATE] = source.get(
|
||||||
class LoginProcessView(AccessRequiredView):
|
SESSION_KEY_RELAY_STATE, ""
|
||||||
"""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:
|
try:
|
||||||
# application.skip_authorization is set so we directly redirect the user
|
self.provider.processor.can_handle(self.request)
|
||||||
if self.provider.application.skip_authorization:
|
|
||||||
return self.post(request, application)
|
|
||||||
|
|
||||||
self.provider.processor.init_deep_link(request)
|
|
||||||
params = self.provider.processor.generate_response()
|
params = self.provider.processor.generate_response()
|
||||||
|
self.request.session[SESSION_KEY_PARAMS] = params
|
||||||
return render(
|
except CannotHandleAssertion as exc:
|
||||||
request,
|
LOGGER.info(exc)
|
||||||
"saml/idp/login.html",
|
did_you_mean_link = self.request.build_absolute_uri(
|
||||||
{
|
|
||||||
"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(
|
|
||||||
reverse(
|
reverse(
|
||||||
"passbook_providers_saml:saml-login-initiate",
|
"passbook_providers_saml:saml-login-initiate",
|
||||||
kwargs={"application": application},
|
kwargs={"application": application},
|
||||||
@ -158,19 +115,97 @@ class LoginProcessView(AccessRequiredView):
|
|||||||
f" Did you mean to go <a href='{did_you_mean_link}'>here</a>?"
|
f" Did you mean to go <a href='{did_you_mean_link}'>here</a>?"
|
||||||
)
|
)
|
||||||
return bad_request_message(
|
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 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
|
# pylint: disable=unused-argument
|
||||||
def post(self, request: HttpRequest, application: str) -> HttpResponse:
|
def post(self, request: HttpRequest, application: str) -> HttpResponse:
|
||||||
"""Handle post request, return back to ACS"""
|
"""Handle post request, return back to ACS"""
|
||||||
# User access gets checked in dispatch
|
# 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
|
# the authorization form
|
||||||
self.provider.processor.can_handle(request)
|
# Log Application Authorization
|
||||||
saml_params = self.provider.processor.generate_response()
|
Event.new(
|
||||||
return self.handle_redirect(saml_params, True)
|
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")
|
@method_decorator(csrf_exempt, name="dispatch")
|
||||||
@ -204,7 +239,9 @@ class SLOLogout(AccessRequiredView):
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def post(self, request: HttpRequest, application: str) -> HttpResponse:
|
def post(self, request: HttpRequest, application: str) -> HttpResponse:
|
||||||
"""Perform logout"""
|
"""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: Parse SAML LogoutRequest from POST data, similar to login_process().
|
||||||
# TODO: Modify the base processor to handle logouts?
|
# TODO: Modify the base processor to handle logouts?
|
||||||
# TODO: Combine this with login_process(), since they are so very similar?
|
# TODO: Combine this with login_process(), since they are so very similar?
|
||||||
@ -259,54 +296,7 @@ class DescriptorDownloadView(AccessRequiredView):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
response = HttpResponse(metadata, content_type="application/xml")
|
response = HttpResponse(metadata, content_type="application/xml")
|
||||||
response["Content-Disposition"] = (
|
response[
|
||||||
'attachment; filename="' '%s_passbook_meta.xml"' % self.provider.name
|
"Content-Disposition"
|
||||||
)
|
] = f'attachment; filename="{self.provider.name}_passbook_meta.xml"'
|
||||||
return response
|
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 import __version__
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
|
from passbook.lib.logging import add_process_id
|
||||||
from passbook.lib.sentry import before_send
|
from passbook.lib.sentry import before_send
|
||||||
|
|
||||||
LOGGER = structlog.get_logger()
|
LOGGER = structlog.get_logger()
|
||||||
@ -53,6 +54,7 @@ if DEBUG:
|
|||||||
CSRF_COOKIE_NAME = "passbook_csrf_debug"
|
CSRF_COOKIE_NAME = "passbook_csrf_debug"
|
||||||
LANGUAGE_COOKIE_NAME = "passbook_language_debug"
|
LANGUAGE_COOKIE_NAME = "passbook_language_debug"
|
||||||
SESSION_COOKIE_NAME = "passbook_session_debug"
|
SESSION_COOKIE_NAME = "passbook_session_debug"
|
||||||
|
SESSION_COOKIE_SAMESITE = None
|
||||||
else:
|
else:
|
||||||
CSRF_COOKIE_NAME = "passbook_csrf"
|
CSRF_COOKIE_NAME = "passbook_csrf"
|
||||||
LANGUAGE_COOKIE_NAME = "passbook_language"
|
LANGUAGE_COOKIE_NAME = "passbook_language"
|
||||||
@ -278,6 +280,7 @@ structlog.configure_once(
|
|||||||
processors=[
|
processors=[
|
||||||
structlog.stdlib.add_log_level,
|
structlog.stdlib.add_log_level,
|
||||||
structlog.stdlib.add_logger_name,
|
structlog.stdlib.add_logger_name,
|
||||||
|
add_process_id,
|
||||||
structlog.stdlib.PositionalArgumentsFormatter(),
|
structlog.stdlib.PositionalArgumentsFormatter(),
|
||||||
structlog.processors.TimeStamper(),
|
structlog.processors.TimeStamper(),
|
||||||
structlog.processors.StackInfoRenderer(),
|
structlog.processors.StackInfoRenderer(),
|
||||||
@ -314,7 +317,7 @@ LOGGING = {
|
|||||||
},
|
},
|
||||||
"handlers": {
|
"handlers": {
|
||||||
"console": {
|
"console": {
|
||||||
"level": DEBUG,
|
"level": "DEBUG",
|
||||||
"class": "logging.StreamHandler",
|
"class": "logging.StreamHandler",
|
||||||
"formatter": "colored" if DEBUG else "plain",
|
"formatter": "colored" if DEBUG else "plain",
|
||||||
},
|
},
|
||||||
@ -322,6 +325,7 @@ LOGGING = {
|
|||||||
"loggers": {},
|
"loggers": {},
|
||||||
}
|
}
|
||||||
_LOGGING_HANDLER_MAP = {
|
_LOGGING_HANDLER_MAP = {
|
||||||
|
"": "DEBUG",
|
||||||
"passbook": "DEBUG",
|
"passbook": "DEBUG",
|
||||||
"django": "WARNING",
|
"django": "WARNING",
|
||||||
"celery": "WARNING",
|
"celery": "WARNING",
|
||||||
@ -334,7 +338,7 @@ for handler_name, level in _LOGGING_HANDLER_MAP.items():
|
|||||||
LOGGING["loggers"][handler_name] = {
|
LOGGING["loggers"][handler_name] = {
|
||||||
"handlers": ["console"],
|
"handlers": ["console"],
|
||||||
"level": level,
|
"level": level,
|
||||||
"propagate": True,
|
"propagate": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST = False
|
TEST = False
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<div class="c-form__horizontal-group">
|
<div class="c-form__horizontal-group">
|
||||||
<p>
|
<p>
|
||||||
Expression using <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/">Jinja</a>. Following variables are available:
|
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>
|
<li><code>ldap</code>: A Dictionary of all values retrieved from LDAP.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
"""OAuth Clients"""
|
"""OAuth Clients"""
|
||||||
import json
|
import json
|
||||||
from typing import Dict
|
from typing import Dict, Optional
|
||||||
from urllib.parse import parse_qs, urlencode
|
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.crypto import constant_time_compare, get_random_string
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from requests import Session
|
from requests import Session
|
||||||
@ -18,30 +19,26 @@ LOGGER = get_logger()
|
|||||||
class BaseOAuthClient:
|
class BaseOAuthClient:
|
||||||
"""Base OAuth Client"""
|
"""Base OAuth Client"""
|
||||||
|
|
||||||
_session: Session = None
|
session: Session = None
|
||||||
|
|
||||||
def __init__(self, source, token=""): # nosec
|
def __init__(self, source, token=""): # nosec
|
||||||
self.source = source
|
self.source = source
|
||||||
self.token = token
|
self.token = token
|
||||||
self._session = Session()
|
self.session = Session()
|
||||||
self._session.headers.update({"User-Agent": "passbook %s" % __version__})
|
self.session.headers.update({"User-Agent": "passbook %s" % __version__})
|
||||||
|
|
||||||
def get_access_token(self, request, callback=None):
|
def get_access_token(self, request, callback=None):
|
||||||
"Fetch access token from callback request."
|
"Fetch access token from callback request."
|
||||||
raise NotImplementedError("Defined in a sub-class") # pragma: no cover
|
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."
|
"Fetch user profile information."
|
||||||
try:
|
try:
|
||||||
token = json.loads(raw_token)
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"{token['token_type']} {token['access_token']}"
|
"Authorization": f"{token['token_type']} {token['access_token']}"
|
||||||
}
|
}
|
||||||
response = self.request(
|
response = self.session.request(
|
||||||
"get",
|
"get", self.source.profile_url, headers=headers,
|
||||||
self.source.profile_url,
|
|
||||||
token=token["access_token"],
|
|
||||||
headers=headers,
|
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except RequestException as exc:
|
except RequestException as exc:
|
||||||
@ -67,10 +64,6 @@ class BaseOAuthClient:
|
|||||||
"Parse token and secret from raw token response."
|
"Parse token and secret from raw token response."
|
||||||
raise NotImplementedError("Defined in a sub-class") # pragma: no cover
|
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
|
@property
|
||||||
def session_key(self):
|
def session_key(self):
|
||||||
"""Return Session Key"""
|
"""Return Session Key"""
|
||||||
@ -80,36 +73,48 @@ class BaseOAuthClient:
|
|||||||
class OAuthClient(BaseOAuthClient):
|
class OAuthClient(BaseOAuthClient):
|
||||||
"""OAuth1 Client"""
|
"""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."
|
"Fetch access token from callback request."
|
||||||
raw_token = request.session.get(self.session_key, None)
|
raw_token = request.session.get(self.session_key, None)
|
||||||
verifier = request.GET.get("oauth_verifier", None)
|
verifier = request.GET.get("oauth_verifier", None)
|
||||||
if raw_token is not None and verifier is not 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 = request.build_absolute_uri(callback or request.path)
|
||||||
callback = force_text(callback)
|
callback = force_text(callback)
|
||||||
try:
|
try:
|
||||||
response = self.request(
|
response = self.session.request(
|
||||||
"post",
|
"post",
|
||||||
self.source.access_token_url,
|
self.source.access_token_url,
|
||||||
token=raw_token,
|
|
||||||
data=data,
|
data=data,
|
||||||
oauth_callback=callback,
|
headers=self._default_headers,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except RequestException as exc:
|
except RequestException as exc:
|
||||||
LOGGER.warning("Unable to fetch access token", exc=exc)
|
LOGGER.warning("Unable to fetch access token", exc=exc)
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return response.text
|
return response.json()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_request_token(self, request, callback):
|
def get_request_token(self, request, callback):
|
||||||
"Fetch the OAuth request token. Only required for OAuth 1.0."
|
"Fetch the OAuth request token. Only required for OAuth 1.0."
|
||||||
callback = force_text(request.build_absolute_uri(callback))
|
callback = force_text(request.build_absolute_uri(callback))
|
||||||
try:
|
try:
|
||||||
response = self.request(
|
response = self.session.request(
|
||||||
"post", self.source.request_token_url, oauth_callback=callback
|
"post",
|
||||||
|
self.source.request_token_url,
|
||||||
|
data={"oauth_callback": callback},
|
||||||
|
headers=self._default_headers,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except RequestException as exc:
|
except RequestException as exc:
|
||||||
@ -154,7 +159,7 @@ class OAuthClient(BaseOAuthClient):
|
|||||||
callback_uri=callback,
|
callback_uri=callback,
|
||||||
)
|
)
|
||||||
kwargs["auth"] = oauth
|
kwargs["auth"] = oauth
|
||||||
return super(OAuthClient, self).request(method, url, **kwargs)
|
return super(OAuthClient, self).session.request(method, url, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def session_key(self):
|
def session_key(self):
|
||||||
@ -164,6 +169,10 @@ class OAuthClient(BaseOAuthClient):
|
|||||||
class OAuth2Client(BaseOAuthClient):
|
class OAuth2Client(BaseOAuthClient):
|
||||||
"""OAuth2 Client"""
|
"""OAuth2 Client"""
|
||||||
|
|
||||||
|
_default_headers = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def check_application_state(self, request, callback):
|
def check_application_state(self, request, callback):
|
||||||
"Check optional state parameter."
|
"Check optional state parameter."
|
||||||
@ -197,15 +206,19 @@ class OAuth2Client(BaseOAuthClient):
|
|||||||
LOGGER.warning("No code returned by the source")
|
LOGGER.warning("No code returned by the source")
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
response = self.request(
|
response = self.session.request(
|
||||||
"post", self.source.access_token_url, data=args, **request_kwargs
|
"post",
|
||||||
|
self.source.access_token_url,
|
||||||
|
data=args,
|
||||||
|
headers=self._default_headers,
|
||||||
|
**request_kwargs,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except RequestException as exc:
|
except RequestException as exc:
|
||||||
LOGGER.warning("Unable to fetch access token", exc=exc)
|
LOGGER.warning("Unable to fetch access token", exc=exc)
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return response.text
|
return response.json()
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def get_application_state(self, request, callback):
|
def get_application_state(self, request, callback):
|
||||||
@ -247,7 +260,7 @@ class OAuth2Client(BaseOAuthClient):
|
|||||||
params = kwargs.get("params", {})
|
params = kwargs.get("params", {})
|
||||||
params["access_token"] = token
|
params["access_token"] = token
|
||||||
kwargs["params"] = params
|
kwargs["params"] = params
|
||||||
return super(OAuth2Client, self).request(method, url, **kwargs)
|
return super(OAuth2Client, self).session.request(method, url, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def session_key(self):
|
def session_key(self):
|
||||||
|
|||||||
@ -116,7 +116,7 @@ class AzureADOAuthSourceForm(OAuthSourceForm):
|
|||||||
class Meta(OAuthSourceForm.Meta):
|
class Meta(OAuthSourceForm.Meta):
|
||||||
|
|
||||||
overrides = {
|
overrides = {
|
||||||
"provider_type": "azure_ad",
|
"provider_type": "azure-ad",
|
||||||
"request_token_url": "",
|
"request_token_url": "",
|
||||||
"authorization_url": "https://login.microsoftonline.com/common/oauth2/authorize",
|
"authorization_url": "https://login.microsoftonline.com/common/oauth2/authorize",
|
||||||
"access_token_url": "https://login.microsoftonline.com/common/oauth2/token",
|
"access_token_url": "https://login.microsoftonline.com/common/oauth2/token",
|
||||||
|
|||||||
@ -89,13 +89,15 @@ class OAuthCallback(OAuthClientMixin, View):
|
|||||||
client = self.get_client(self.source)
|
client = self.get_client(self.source)
|
||||||
callback = self.get_callback_url(self.source)
|
callback = self.get_callback_url(self.source)
|
||||||
# Fetch access token
|
# Fetch access token
|
||||||
raw_token = client.get_access_token(self.request, callback=callback)
|
token = client.get_access_token(self.request, callback=callback)
|
||||||
if raw_token is None:
|
if token is None:
|
||||||
return self.handle_login_failure(
|
return self.handle_login_failure(
|
||||||
self.source, "Could not retrieve token."
|
self.source, "Could not retrieve token."
|
||||||
)
|
)
|
||||||
|
if "error" in token:
|
||||||
|
return self.handle_login_failure(self.source, token["error"])
|
||||||
# Fetch profile info
|
# Fetch profile info
|
||||||
info = client.get_profile_info(raw_token)
|
info = client.get_profile_info(token)
|
||||||
if info is None:
|
if info is None:
|
||||||
return self.handle_login_failure(
|
return self.handle_login_failure(
|
||||||
self.source, "Could not retrieve profile."
|
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.")
|
return self.handle_login_failure(self.source, "Could not determine id.")
|
||||||
# Get or create access record
|
# Get or create access record
|
||||||
defaults = {
|
defaults = {
|
||||||
"access_token": raw_token,
|
"access_token": token.get("access_token"),
|
||||||
}
|
}
|
||||||
existing = UserOAuthSourceConnection.objects.filter(
|
existing = UserOAuthSourceConnection.objects.filter(
|
||||||
source=self.source, identifier=identifier
|
source=self.source, identifier=identifier
|
||||||
@ -113,13 +115,15 @@ class OAuthCallback(OAuthClientMixin, View):
|
|||||||
|
|
||||||
if existing.exists():
|
if existing.exists():
|
||||||
connection = existing.first()
|
connection = existing.first()
|
||||||
connection.access_token = raw_token
|
connection.access_token = token.get("access_token")
|
||||||
UserOAuthSourceConnection.objects.filter(pk=connection.pk).update(
|
UserOAuthSourceConnection.objects.filter(pk=connection.pk).update(
|
||||||
**defaults
|
**defaults
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
connection = UserOAuthSourceConnection(
|
connection = UserOAuthSourceConnection(
|
||||||
source=self.source, identifier=identifier, access_token=raw_token
|
source=self.source,
|
||||||
|
identifier=identifier,
|
||||||
|
access_token=token.get("access_token"),
|
||||||
)
|
)
|
||||||
user = authenticate(
|
user = authenticate(
|
||||||
source=self.source, identifier=identifier, request=request
|
source=self.source, identifier=identifier, request=request
|
||||||
|
|||||||
@ -7,33 +7,18 @@
|
|||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,204 +0,0 @@
|
/* login page's icons */
|
||||||
.navbar-brand-name {
|
.pf-c-login__main-footer-links-item-link img {
|
||||||
height: 35px;
|
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 {
|
/* fix multiple selects height */
|
||||||
display: flex;
|
select[multiple] {
|
||||||
align-items: center;
|
height: initial;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Selector */
|
/* Selector */
|
||||||
|
|||||||
@ -36,3 +36,19 @@ document.querySelectorAll(".codemirror").forEach((cm) => {
|
|||||||
autoRefresh: true,
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user