Compare commits

...

17 Commits

Author SHA1 Message Date
Jens Langhammer
36b1f8ba36 new release: 0.8.3-beta 2020-02-20 15:14:49 +01:00
Jens Langhammer
6c889eff27 core: fix application icons not loading, fix with_sources being broken 2020-02-20 14:30:06 +01:00
Jens Langhammer
9d8675e54b new release: 0.8.2-beta 2020-02-20 13:57:46 +01:00
Jens Langhammer
22ae986c0b root: add logger name to log output 2020-02-20 13:52:14 +01:00
Jens Langhammer
2bef5f3911 policies: struct -> types to match core 2020-02-20 13:52:05 +01:00
Jens Langhammer
3c2b8e5ee1 all: prefix all UI related methods with ui_, switch to property and return dataclass 2020-02-20 13:51:41 +01:00
Jens Langhammer
c96571bdba core: fix discord logo being hard to see 2020-02-20 13:50:05 +01:00
Jens Langhammer
2dfd93afb1 core: add more fields for metadata of applications 2020-02-20 13:45:22 +01:00
Jens Langhammer
1d22e30c70 lib: sentry ignore Redis and OSError 2020-02-19 17:13:44 +01:00
Jens Langhammer
07b7951390 sources/ldap: handle user_sync errors better, show warning when user exists already 2020-02-19 16:20:33 +01:00
Jens Langhammer
995615d0a0 policies/expression: Return False if Policy returns Undefined and log warning 2020-02-19 16:19:02 +01:00
Jens Langhammer
ac273aab75 core: raise PropertyMappingExpressionException when PropertyMapping returns Undefined 2020-02-19 16:18:31 +01:00
Jens Langhammer
44cd03654d core: base set maximum-scale to 1 2020-02-19 15:11:25 +01:00
Jens Langhammer
3e2375f970 new release: 0.8.1-beta 2020-02-19 11:31:05 +01:00
Jens Langhammer
38ad8e5fd3 policies/expression: fix pb_is_sso_flow 2020-02-19 11:01:20 +01:00
Jens Langhammer
c481558a46 helm: fix error that FLUSHDB Command is not available 2020-02-19 10:57:57 +01:00
Jens Langhammer
e27a05a7fc lib/sentry: ignore django validation error 2020-02-19 10:54:29 +01:00
33 changed files with 288 additions and 162 deletions

View File

@@ -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>.*)

View File

@@ -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:

View File

@@ -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

View File

@@ -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: []

View File

@@ -1,2 +1,2 @@
"""passbook"""
__version__ = "0.8.0-beta"
__version__ = "0.8.3-beta"

View File

@@ -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>

View File

@@ -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",
]

View File

@@ -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.")}

View 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),
),
]

View File

@@ -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

View File

@@ -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

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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
View 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

View File

@@ -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)

View File

@@ -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):

View File

@@ -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:

View File

@@ -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"]

View File

@@ -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:

View File

@@ -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()

View File

@@ -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:

View File

@@ -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):

View File

@@ -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()

View File

@@ -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()

View File

@@ -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):

View File

@@ -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):

View File

@@ -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(),

View File

@@ -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(

View File

@@ -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:

View File

@@ -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}
)