mirror of
https://github.com/goauthentik/authentik
synced 2026-05-05 22:52:42 +02:00
Compare commits
17 Commits
version/0.
...
version/0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36b1f8ba36 | ||
|
|
6c889eff27 | ||
|
|
9d8675e54b | ||
|
|
22ae986c0b | ||
|
|
2bef5f3911 | ||
|
|
3c2b8e5ee1 | ||
|
|
c96571bdba | ||
|
|
2dfd93afb1 | ||
|
|
1d22e30c70 | ||
|
|
07b7951390 | ||
|
|
995615d0a0 | ||
|
|
ac273aab75 | ||
|
|
44cd03654d | ||
|
|
3e2375f970 | ||
|
|
38ad8e5fd3 | ||
|
|
c481558a46 | ||
|
|
e27a05a7fc |
@@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.8.0-beta
|
||||
current_version = 0.8.3-beta
|
||||
tag = True
|
||||
commit = True
|
||||
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
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/passbook:0.8.0-beta
|
||||
-t beryju/passbook:0.8.3-beta
|
||||
-t beryju/passbook:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook:0.8.0-beta
|
||||
run: docker push beryju/passbook:0.8.3-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.0-beta \
|
||||
-t beryju/passbook-gatekeeper:0.8.3-beta \
|
||||
-t beryju/passbook-gatekeeper:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-gatekeeper:0.8.0-beta
|
||||
run: docker push beryju/passbook-gatekeeper:0.8.3-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.0-beta
|
||||
-t beryju/passbook-static:0.8.3-beta
|
||||
-t beryju/passbook-static:latest
|
||||
-f static.Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-static:0.8.0-beta
|
||||
run: docker push beryju/passbook-static:0.8.3-beta
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-static:latest
|
||||
test-release:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
apiVersion: v1
|
||||
appVersion: "0.8.0-beta"
|
||||
appVersion: "0.8.3-beta"
|
||||
description: A Helm chart for passbook.
|
||||
name: passbook
|
||||
version: "0.8.0-beta"
|
||||
version: "0.8.3-beta"
|
||||
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
image:
|
||||
tag: 0.8.0-beta
|
||||
tag: 0.8.3-beta
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
@@ -42,3 +42,5 @@ redis:
|
||||
master:
|
||||
persistence:
|
||||
enabled: false
|
||||
# https://stackoverflow.com/a/59189742
|
||||
disableCommands: []
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = "0.8.0-beta"
|
||||
__version__ = "0.8.3-beta"
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<tr>
|
||||
<td>{{ source.name }}</td>
|
||||
<td>{{ source|fieldtype }}</td>
|
||||
<td>{{ source.additional_info|safe }}</td>
|
||||
<td>{{ source.ui_additional_info|safe|default:"" }}</td>
|
||||
<td>
|
||||
<a class="btn btn-default btn-sm"
|
||||
href="{% url 'passbook_admin:source-update' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
|
||||
@@ -15,11 +15,13 @@ class ApplicationSerializer(ModelSerializer):
|
||||
"pk",
|
||||
"name",
|
||||
"slug",
|
||||
"launch_url",
|
||||
"icon_url",
|
||||
"provider",
|
||||
"policies",
|
||||
"skip_authorization",
|
||||
"provider",
|
||||
"meta_launch_url",
|
||||
"meta_icon_url",
|
||||
"meta_description",
|
||||
"meta_publisher",
|
||||
"policies",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -19,19 +19,25 @@ class ApplicationForm(forms.ModelForm):
|
||||
fields = [
|
||||
"name",
|
||||
"slug",
|
||||
"launch_url",
|
||||
"icon_url",
|
||||
"provider",
|
||||
"policies",
|
||||
"skip_authorization",
|
||||
"provider",
|
||||
"meta_launch_url",
|
||||
"meta_icon_url",
|
||||
"meta_description",
|
||||
"meta_publisher",
|
||||
"policies",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"launch_url": forms.TextInput(),
|
||||
"icon_url": forms.TextInput(),
|
||||
"meta_launch_url": forms.TextInput(),
|
||||
"meta_icon_url": forms.TextInput(),
|
||||
"meta_publisher": forms.TextInput(),
|
||||
"policies": FilteredSelectMultiple(_("policies"), False),
|
||||
}
|
||||
labels = {
|
||||
"launch_url": _("Launch URL"),
|
||||
"icon_url": _("Icon URL"),
|
||||
"meta_launch_url": _("Launch URL"),
|
||||
"meta_icon_url": _("Icon URL"),
|
||||
"meta_description": _("Description"),
|
||||
"meta_publisher": _("Publisher"),
|
||||
}
|
||||
help_texts = {"policies": _("Policies required to access this Application.")}
|
||||
|
||||
29
passbook/core/migrations/0008_auto_20200220_1242.py
Normal file
29
passbook/core/migrations/0008_auto_20200220_1242.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.0.3 on 2020-02-20 12:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_core", "0007_auto_20200217_1934"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="application", old_name="icon_url", new_name="meta_icon_url",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="application", old_name="launch_url", new_name="meta_launch_url",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="application",
|
||||
name="meta_description",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="application",
|
||||
name="meta_publisher",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -15,16 +15,18 @@ from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_prometheus.models import ExportModelOperationsMixin
|
||||
from guardian.mixins import GuardianUserMixin
|
||||
from jinja2 import Undefined
|
||||
from jinja2.exceptions import TemplateSyntaxError, UndefinedError
|
||||
from jinja2.nativetypes import NativeEnvironment
|
||||
from model_utils.managers import InheritanceManager
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.types import UIUserSettings, UILoginButton
|
||||
from passbook.core.exceptions import PropertyMappingExpressionException
|
||||
from passbook.core.signals import password_changed
|
||||
from passbook.lib.models import CreatedUpdatedModel, UUIDModel
|
||||
from passbook.policies.exceptions import PolicyException
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
NATIVE_ENVIRONMENT = NativeEnvironment()
|
||||
@@ -101,19 +103,6 @@ class PolicyModel(UUIDModel, CreatedUpdatedModel):
|
||||
policies = models.ManyToManyField("Policy", blank=True)
|
||||
|
||||
|
||||
class UserSettings:
|
||||
"""Dataclass for Factor and Source's user_settings"""
|
||||
|
||||
name: str
|
||||
icon: str
|
||||
view_name: str
|
||||
|
||||
def __init__(self, name: str, icon: str, view_name: str):
|
||||
self.name = name
|
||||
self.icon = icon
|
||||
self.view_name = view_name
|
||||
|
||||
|
||||
class Factor(ExportModelOperationsMixin("factor"), PolicyModel):
|
||||
"""Authentication factor, multiple instances of the same Factor can be used"""
|
||||
|
||||
@@ -126,9 +115,10 @@ class Factor(ExportModelOperationsMixin("factor"), PolicyModel):
|
||||
type = ""
|
||||
form = ""
|
||||
|
||||
def user_settings(self) -> Optional[UserSettings]:
|
||||
@property
|
||||
def ui_user_settings(self) -> Optional[UIUserSettings]:
|
||||
"""Entrypoint to integrate with User settings. Can either return None if no
|
||||
user settings are available, or an instanace of UserSettings."""
|
||||
user settings are available, or an instanace of UIUserSettings."""
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
@@ -142,16 +132,19 @@ class Application(ExportModelOperationsMixin("application"), PolicyModel):
|
||||
|
||||
name = models.TextField()
|
||||
slug = models.SlugField()
|
||||
launch_url = models.URLField(null=True, blank=True)
|
||||
icon_url = models.TextField(null=True, blank=True)
|
||||
skip_authorization = models.BooleanField(default=False)
|
||||
provider = models.OneToOneField(
|
||||
"Provider", null=True, blank=True, default=None, on_delete=models.SET_DEFAULT
|
||||
)
|
||||
skip_authorization = models.BooleanField(default=False)
|
||||
|
||||
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)
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
def get_provider(self):
|
||||
def get_provider(self) -> Optional[Provider]:
|
||||
"""Get casted provider instance"""
|
||||
if not self.provider:
|
||||
return None
|
||||
@@ -166,6 +159,7 @@ class Source(ExportModelOperationsMixin("source"), PolicyModel):
|
||||
|
||||
name = models.TextField()
|
||||
slug = models.SlugField()
|
||||
|
||||
enabled = models.BooleanField(default=True)
|
||||
property_mappings = models.ManyToManyField(
|
||||
"PropertyMapping", default=None, blank=True
|
||||
@@ -176,19 +170,20 @@ class Source(ExportModelOperationsMixin("source"), PolicyModel):
|
||||
objects = InheritanceManager()
|
||||
|
||||
@property
|
||||
def login_button(self):
|
||||
"""Return a tuple of URL, Icon name and Name
|
||||
if Source should get a link on the login page"""
|
||||
def ui_login_button(self) -> Optional[UILoginButton]:
|
||||
"""If source uses a http-based flow, return UI Information about the login
|
||||
button. If source doesn't use http-based flow, return None."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def additional_info(self):
|
||||
def ui_additional_info(self) -> Optional[str]:
|
||||
"""Return additional Info, such as a callback URL. Show in the administration interface."""
|
||||
return None
|
||||
|
||||
def user_settings(self) -> Optional[UserSettings]:
|
||||
@property
|
||||
def ui_user_settings(self) -> Optional[UIUserSettings]:
|
||||
"""Entrypoint to integrate with User settings. Can either return None if no
|
||||
user settings are available, or an instanace of UserSettings."""
|
||||
user settings are available, or an instanace of UIUserSettings."""
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
@@ -313,7 +308,10 @@ class PropertyMapping(UUIDModel):
|
||||
except TemplateSyntaxError as exc:
|
||||
raise PropertyMappingExpressionException from exc
|
||||
try:
|
||||
return expression.render(user=user, request=request, **kwargs)
|
||||
response = expression.render(user=user, request=request, **kwargs)
|
||||
if isinstance(response, Undefined):
|
||||
raise PropertyMappingExpressionException("Response was 'Undefined'")
|
||||
return response
|
||||
except UndefinedError as exc:
|
||||
raise PropertyMappingExpressionException from exc
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><style>.st0{fill:#FFFFFF;}</style><path class="st0" 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 class="st0" 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>
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -7,7 +7,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>
|
||||
{% block title %}
|
||||
{% title %}
|
||||
|
||||
@@ -48,13 +48,16 @@
|
||||
<!--login-pf-section-->
|
||||
<section class="login-pf-social-section" role="contentinfo" aria-label="Log in with third party account">
|
||||
<ul class="login-pf-social login-pf-social-double-col list-unstyled">
|
||||
{% for url, icon, name in sources %}
|
||||
{% for source in sources %}
|
||||
<li class="login-pf-social-link">
|
||||
<a href="{{ url }}">
|
||||
{% if icon %}
|
||||
<img src="{% static 'img/logos/' %}{{ icon }}.svg" alt="{{ name }}">
|
||||
<a href="{{ source.url }}">
|
||||
{% if source.icon_path %}
|
||||
<img src="{% static source.icon_path %}" alt="{{ source.name }}">
|
||||
{% endif %}
|
||||
{{ name }}
|
||||
{% if source.icon_url %}
|
||||
<img src="icon_url" alt="{{ source.name }}">
|
||||
{% endif %}
|
||||
{{ source.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -2,42 +2,44 @@
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
img.app-icon {
|
||||
max-height: 72px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row row-cards-pf">
|
||||
{% for app in applications %}
|
||||
<div class="col-xs-12 col-sm-6 col-md-3">
|
||||
<div class="card-pf card-pf-accented card-pf-aggregate-status">
|
||||
<h2 class="card-pf-title">
|
||||
<span class="fa fa-shield"></span> {{ app.name }}
|
||||
</h2>
|
||||
<div class="card-pf-body">
|
||||
<p class="card-pf-aggregate-status-notifications">
|
||||
<span class="card-pf-aggregate-status-notification">
|
||||
<a href="{{ app.launch_url }}" class="add" data-toggle="tooltip" data-placement="top" title="{% trans 'Open App...' %}">
|
||||
{% if not app.icon_url %}
|
||||
<span class="pficon pficon-arrow"></span>
|
||||
{% else %}
|
||||
<img class="app-icon" src="{{ app.icon_url }}" alt="{% trans 'Application Icon' %}">
|
||||
{% endif %}
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
<div class="row row-cards-pf">
|
||||
{% for app in applications %}
|
||||
<div class="col-xs-12 col-sm-6 col-md-3">
|
||||
<div class="card-pf card-pf-view card-pf-view-select card-pf-view-single-select">
|
||||
<div class="card-pf-body">
|
||||
<div class="card-pf-top-element">
|
||||
<a href="{{ app.meta_launch_url }}">
|
||||
{% if not app.meta_icon_url %}
|
||||
<span class="pficon pficon-arrow card-pf-icon-circle"></span>
|
||||
{% else %}
|
||||
<img class="app-icon card-pf-icon-circle" src="{{ app.meta_icon_url }}" alt="{% trans 'Application Icon' %}">
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
<h2 class="card-pf-title text-center">
|
||||
<a href="{{ app.meta_launch_url }}">
|
||||
{{ app.name }}
|
||||
</a>
|
||||
</h2>
|
||||
{% if app.meta_publisher %}
|
||||
<div class="card-pf-items text-center">
|
||||
<small>{{ app.meta_publisher }}</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if app.meta_description %}
|
||||
<div class="card-pf-items text-center">
|
||||
<p>{{ app.meta_description }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-pf-view-checkbox">
|
||||
<a href="{{ app.meta_launch_url }}"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<h1>{% trans 'No Applications available.' %}</h1>
|
||||
{% endfor %}
|
||||
</div><!-- /row -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,46 +1,53 @@
|
||||
"""passbook user settings template tags"""
|
||||
from typing import List
|
||||
from typing import List, Iterable
|
||||
|
||||
from django import template
|
||||
from django.template.context import RequestContext
|
||||
|
||||
from passbook.core.models import Factor, Source, UserSettings
|
||||
from passbook.core.types import UIUserSettings
|
||||
from passbook.core.models import Factor, Source
|
||||
from passbook.policies.engine import PolicyEngine
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def user_factors(context: RequestContext) -> List[UserSettings]:
|
||||
def user_factors(context: RequestContext) -> List[UIUserSettings]:
|
||||
"""Return list of all factors which apply to user"""
|
||||
user = context.get("request").user
|
||||
_all_factors = (
|
||||
_all_factors: Iterable[Factor] = (
|
||||
Factor.objects.filter(enabled=True).order_by("order").select_subclasses()
|
||||
)
|
||||
matching_factors: List[UserSettings] = []
|
||||
matching_factors: List[UIUserSettings] = []
|
||||
for factor in _all_factors:
|
||||
user_settings = factor.user_settings()
|
||||
user_settings = factor.ui_user_settings
|
||||
if not user_settings:
|
||||
continue
|
||||
policy_engine = PolicyEngine(
|
||||
factor.policies.all(), user, context.get("request")
|
||||
)
|
||||
policy_engine.build()
|
||||
if policy_engine.passing and user_settings:
|
||||
if policy_engine.passing:
|
||||
matching_factors.append(user_settings)
|
||||
return matching_factors
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def user_sources(context: RequestContext) -> List[UserSettings]:
|
||||
def user_sources(context: RequestContext) -> List[UIUserSettings]:
|
||||
"""Return a list of all sources which are enabled for the user"""
|
||||
user = context.get("request").user
|
||||
_all_sources = Source.objects.filter(enabled=True).select_subclasses()
|
||||
matching_sources: List[UserSettings] = []
|
||||
_all_sources: Iterable[Source] = (
|
||||
Source.objects.filter(enabled=True).select_subclasses()
|
||||
)
|
||||
matching_sources: List[UIUserSettings] = []
|
||||
for factor in _all_sources:
|
||||
user_settings = factor.user_settings()
|
||||
user_settings = factor.ui_user_settings
|
||||
if not user_settings:
|
||||
continue
|
||||
policy_engine = PolicyEngine(
|
||||
factor.policies.all(), user, context.get("request")
|
||||
)
|
||||
policy_engine.build()
|
||||
if policy_engine.passing and user_settings:
|
||||
if policy_engine.passing:
|
||||
matching_sources.append(user_settings)
|
||||
return matching_sources
|
||||
|
||||
29
passbook/core/types.py
Normal file
29
passbook/core/types.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""passbook core dataclasses"""
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class UIUserSettings:
|
||||
"""Dataclass for Factor and Source's user_settings"""
|
||||
|
||||
name: str
|
||||
icon: str
|
||||
view_name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class UILoginButton:
|
||||
"""Dataclass for Source's ui_ui_login_button"""
|
||||
|
||||
# Name, ran through i18n
|
||||
name: str
|
||||
|
||||
# URL Which Button points to
|
||||
url: str
|
||||
|
||||
# Icon name, ran through django's static
|
||||
icon_path: Optional[str] = None
|
||||
|
||||
# Icon URL, used as-is
|
||||
icon_url: Optional[str] = None
|
||||
@@ -47,9 +47,9 @@ class LoginView(UserPassesTestMixin, FormView):
|
||||
kwargs["sources"] = []
|
||||
sources = Source.objects.filter(enabled=True).select_subclasses()
|
||||
for source in sources:
|
||||
login_button = source.login_button
|
||||
if login_button:
|
||||
kwargs["sources"].append(login_button)
|
||||
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)
|
||||
@@ -72,7 +72,6 @@ class LoginView(UserPassesTestMixin, FormView):
|
||||
if not pre_user:
|
||||
# No user found
|
||||
return self.invalid_login(self.request)
|
||||
# self.request.session.flush()
|
||||
self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk
|
||||
return _redirect_with_qs("passbook_core:auth-process", self.request.GET)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""OTP Factor"""
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.core.models import Factor, UserSettings
|
||||
from passbook.core.types import UIUserSettings
|
||||
from passbook.core.models import Factor
|
||||
|
||||
|
||||
class OTPFactor(Factor):
|
||||
@@ -17,9 +17,12 @@ class OTPFactor(Factor):
|
||||
type = "passbook.factors.otp.factors.OTPFactor"
|
||||
form = "passbook.factors.otp.forms.OTPFactorForm"
|
||||
|
||||
def user_settings(self) -> UserSettings:
|
||||
return UserSettings(
|
||||
_("OTP"), "pficon-locked", "passbook_factors_otp:otp-user-settings"
|
||||
@property
|
||||
def ui_user_settings(self) -> UIUserSettings:
|
||||
return UIUserSettings(
|
||||
name="OTP",
|
||||
icon="pficon-locked",
|
||||
view_name="passbook_factors_otp:otp-user-settings",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -3,7 +3,8 @@ from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.core.models import Factor, Policy, User, UserSettings
|
||||
from passbook.core.types import UIUserSettings
|
||||
from passbook.core.models import Factor, Policy, User
|
||||
|
||||
|
||||
class PasswordFactor(Factor):
|
||||
@@ -18,9 +19,12 @@ class PasswordFactor(Factor):
|
||||
type = "passbook.factors.password.factor.PasswordFactor"
|
||||
form = "passbook.factors.password.forms.PasswordFactorForm"
|
||||
|
||||
def user_settings(self):
|
||||
return UserSettings(
|
||||
_("Change Password"), "pficon-key", "passbook_core:user-change-password"
|
||||
@property
|
||||
def ui_user_settings(self) -> UIUserSettings:
|
||||
return UIUserSettings(
|
||||
name="Change Password",
|
||||
icon="pficon-key",
|
||||
view_name="passbook_core:user-change-password",
|
||||
)
|
||||
|
||||
def password_passes(self, user: User) -> bool:
|
||||
|
||||
@@ -8,10 +8,12 @@ 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,
|
||||
@@ -24,6 +26,9 @@ def before_send(event, hint):
|
||||
ConnectionResetError,
|
||||
KeyboardInterrupt,
|
||||
ClientError,
|
||||
ValidationError,
|
||||
OSError,
|
||||
RedisError,
|
||||
)
|
||||
if "exc_info" in hint:
|
||||
_exc_type, exc_value, _ = hint["exc_info"]
|
||||
|
||||
@@ -9,7 +9,7 @@ from structlog import get_logger
|
||||
|
||||
from passbook.core.models import Policy, User
|
||||
from passbook.policies.process import PolicyProcess, cache_key
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
# This is only really needed for macOS, because Python 3.8 changed the default to spawn
|
||||
@@ -63,13 +63,13 @@ class PolicyEngine:
|
||||
for policy in self._select_subclasses():
|
||||
cached_policy = cache.get(cache_key(policy, self.request.user), None)
|
||||
if cached_policy and self.use_cache:
|
||||
LOGGER.debug("Taking result from cache", policy=policy)
|
||||
LOGGER.debug("P_ENG: Taking result from cache", policy=policy)
|
||||
self.__cached_policies.append(cached_policy)
|
||||
continue
|
||||
LOGGER.debug("Evaluating policy", policy=policy)
|
||||
LOGGER.debug("P_ENG: Evaluating policy", policy=policy)
|
||||
our_end, task_end = Pipe(False)
|
||||
task = PolicyProcess(policy, self.request, task_end)
|
||||
LOGGER.debug("Starting Process", policy=policy)
|
||||
LOGGER.debug("P_ENG: Starting Process", policy=policy)
|
||||
task.start()
|
||||
self.__processes.append(
|
||||
PolicyProcessInfo(process=task, connection=our_end, policy=policy)
|
||||
@@ -90,7 +90,7 @@ class PolicyEngine:
|
||||
x.result for x in self.__processes if x.result
|
||||
]
|
||||
for result in process_results + self.__cached_policies:
|
||||
LOGGER.debug("result", passing=result.passing)
|
||||
LOGGER.debug("P_ENG: result", passing=result.passing)
|
||||
if result.messages:
|
||||
messages += result.messages
|
||||
if not result.passing:
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.utils.translation import gettext as _
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import Policy
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@@ -3,16 +3,19 @@ import re
|
||||
from typing import TYPE_CHECKING, Any, Dict
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from jinja2 import Undefined
|
||||
from jinja2.exceptions import TemplateSyntaxError, UndefinedError
|
||||
from jinja2.nativetypes import NativeEnvironment
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.factors.view import AuthenticationView
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from passbook.core.models import User
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class Evaluator:
|
||||
"""Validate and evaulate jinja2-based expressions"""
|
||||
@@ -47,7 +50,7 @@ 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.user.session.get(
|
||||
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
|
||||
@@ -67,6 +70,13 @@ class Evaluator:
|
||||
result = expression.render(
|
||||
request=request, **self._get_expression_context(request)
|
||||
)
|
||||
if isinstance(result, Undefined):
|
||||
LOGGER.warning(
|
||||
"Expression policy returned undefined",
|
||||
src=expression_source,
|
||||
req=request,
|
||||
)
|
||||
return PolicyResult(False)
|
||||
if isinstance(result, list) and len(result) == 2:
|
||||
return PolicyResult(*result)
|
||||
if result:
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.core.models import Policy
|
||||
from passbook.policies.expression.evaluator import Evaluator
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
|
||||
class ExpressionPolicy(Policy):
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.utils.translation import gettext as _
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import Policy
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from structlog import get_logger
|
||||
|
||||
from passbook.core.models import Policy
|
||||
from passbook.policies.exceptions import PolicyException
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.core.models import Policy, User
|
||||
from passbook.lib.utils.http import get_client_ip
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
|
||||
class ReputationPolicy(Policy):
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.core.models import Policy
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
|
||||
class WebhookPolicy(Policy):
|
||||
|
||||
@@ -271,6 +271,7 @@ STATIC_URL = "/static/"
|
||||
structlog.configure_once(
|
||||
processors=[
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.stdlib.add_logger_name,
|
||||
structlog.stdlib.PositionalArgumentsFormatter(),
|
||||
structlog.processors.TimeStamper(),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any, Dict, Optional
|
||||
import ldap3
|
||||
import ldap3.core.exceptions
|
||||
from structlog import get_logger
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
from passbook.core.exceptions import PropertyMappingExpressionException
|
||||
from passbook.core.models import Group, User
|
||||
@@ -94,18 +95,31 @@ class Connector:
|
||||
)
|
||||
for user in users:
|
||||
attributes = user.get("attributes", {})
|
||||
user, created = User.objects.update_or_create(
|
||||
attributes__ldap_uniq=attributes.get(
|
||||
self._source.object_uniqueness_field, ""
|
||||
),
|
||||
defaults=self._build_object_properties(attributes),
|
||||
)
|
||||
if created:
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
LOGGER.debug(
|
||||
"Synced User", user=attributes.get("name", ""), created=created
|
||||
)
|
||||
try:
|
||||
uniq = attributes[self._source.object_uniqueness_field]
|
||||
except KeyError:
|
||||
LOGGER.warning("Cannot find uniqueness Field in attributes")
|
||||
continue
|
||||
try:
|
||||
user, created = User.objects.update_or_create(
|
||||
attributes__ldap_uniq=uniq,
|
||||
defaults=self._build_object_properties(attributes),
|
||||
)
|
||||
except IntegrityError as exc:
|
||||
LOGGER.warning("Failed to create user", exc=exc)
|
||||
LOGGER.warning(
|
||||
(
|
||||
"To merge new User with existing user, set the User's "
|
||||
f"Attribute 'ldap_uniq' to '{uniq}'"
|
||||
)
|
||||
)
|
||||
else:
|
||||
if created:
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
LOGGER.debug(
|
||||
"Synced User", user=attributes.get("name", ""), created=created
|
||||
)
|
||||
|
||||
def sync_membership(self):
|
||||
"""Iterate over all Users and assign Groups using memberOf Field"""
|
||||
@@ -155,13 +169,15 @@ class Connector:
|
||||
) -> Dict[str, Dict[Any, Any]]:
|
||||
properties = {"attributes": {}}
|
||||
for mapping in self._source.property_mappings.all().select_subclasses():
|
||||
if not isinstance(mapping, LDAPPropertyMapping):
|
||||
continue
|
||||
mapping: LDAPPropertyMapping
|
||||
try:
|
||||
properties[mapping.object_field] = mapping.evaluate(
|
||||
user=None, request=None, ldap=attributes
|
||||
)
|
||||
except PropertyMappingExpressionException as exc:
|
||||
LOGGER.warning(exc)
|
||||
LOGGER.warning("Mapping failed to evaluate", exc=exc, mapping=mapping)
|
||||
continue
|
||||
if self._source.object_uniqueness_field in attributes:
|
||||
properties["attributes"]["ldap_uniq"] = attributes.get(
|
||||
|
||||
@@ -4,7 +4,8 @@ from django.db import models
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.core.models import Source, UserSettings, UserSourceConnection
|
||||
from passbook.core.types import UILoginButton, UIUserSettings
|
||||
from passbook.core.models import Source, UserSourceConnection
|
||||
from passbook.sources.oauth.clients import get_client
|
||||
|
||||
|
||||
@@ -28,30 +29,35 @@ class OAuthSource(Source):
|
||||
form = "passbook.sources.oauth.forms.OAuthSourceForm"
|
||||
|
||||
@property
|
||||
def login_button(self):
|
||||
url = reverse_lazy(
|
||||
"passbook_sources_oauth:oauth-client-login",
|
||||
kwargs={"source_slug": self.slug},
|
||||
def ui_login_button(self) -> UILoginButton:
|
||||
return UILoginButton(
|
||||
url=reverse_lazy(
|
||||
"passbook_sources_oauth:oauth-client-login",
|
||||
kwargs={"source_slug": self.slug},
|
||||
),
|
||||
icon_path=f"img/logos/{self.provider_type}.svg",
|
||||
name=self.name,
|
||||
)
|
||||
return url, self.provider_type, self.name
|
||||
|
||||
@property
|
||||
def additional_info(self):
|
||||
return "Callback URL: <pre>%s</pre>" % reverse_lazy(
|
||||
def ui_additional_info(self) -> str:
|
||||
url = reverse_lazy(
|
||||
"passbook_sources_oauth:oauth-client-callback",
|
||||
kwargs={"source_slug": self.slug},
|
||||
)
|
||||
return f"Callback URL: <pre>{url}</pre>"
|
||||
|
||||
def user_settings(self) -> UserSettings:
|
||||
@property
|
||||
def ui_user_settings(self) -> UIUserSettings:
|
||||
icon_type = self.provider_type
|
||||
if icon_type == "azure ad":
|
||||
icon_type = "windows"
|
||||
icon_class = "fa fa-%s" % icon_type
|
||||
icon_class = f"fa fa-{icon_type}"
|
||||
view_name = "passbook_sources_oauth:oauth-client-user"
|
||||
return UserSettings(
|
||||
self.name,
|
||||
icon_class,
|
||||
reverse((view_name), kwargs={"source_slug": self.slug}),
|
||||
return UIUserSettings(
|
||||
name=self.name,
|
||||
icon=icon_class,
|
||||
view_name=reverse((view_name), kwargs={"source_slug": self.slug}),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -3,11 +3,12 @@ from django.db import models
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.core.types import UILoginButton
|
||||
from passbook.core.models import Source
|
||||
|
||||
|
||||
class SAMLSource(Source):
|
||||
"""SAML2 Source"""
|
||||
"""SAML Source"""
|
||||
|
||||
entity_id = models.TextField(blank=True, default=None, verbose_name=_("Entity ID"))
|
||||
idp_url = models.URLField(verbose_name=_("IDP URL"))
|
||||
@@ -20,14 +21,17 @@ class SAMLSource(Source):
|
||||
form = "passbook.sources.saml.forms.SAMLSourceForm"
|
||||
|
||||
@property
|
||||
def login_button(self):
|
||||
url = reverse_lazy(
|
||||
"passbook_sources_saml:login", kwargs={"source_slug": self.slug}
|
||||
def ui_login_button(self) -> UILoginButton:
|
||||
return UILoginButton(
|
||||
name=self.name,
|
||||
url=reverse_lazy(
|
||||
"passbook_sources_saml:login", kwargs={"source_slug": self.slug}
|
||||
),
|
||||
icon_path="",
|
||||
)
|
||||
return url, "", self.name
|
||||
|
||||
@property
|
||||
def additional_info(self):
|
||||
def ui_additional_info(self) -> str:
|
||||
metadata_url = reverse_lazy(
|
||||
"passbook_sources_saml:metadata", kwargs={"source_slug": self}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user