Merge branch 'master' into pf4

# Conflicts:
#	passbook/core/static/img/logos/discord.svg
#	passbook/core/static/js/passbook.js
#	passbook/core/templates/login/with_sources.html
#	passbook/core/templates/overview/index.html
#	passbook/core/views/authentication.py
This commit is contained in:
Jens Langhammer
2020-02-21 09:05:40 +01:00
52 changed files with 446 additions and 329 deletions

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

@ -1,5 +1,6 @@
"""passbook core exceptions"""
from passbook.lib.sentry import SentryIgnoredException
class PropertyMappingExpressionException(Exception):
class PropertyMappingExpressionException(SentryIgnoredException):
"""Error when a PropertyMapping Exception expression could not be parsed or evaluated."""

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

@ -23,9 +23,10 @@ from structlog import get_logger
from passbook.core.exceptions import PropertyMappingExpressionException
from passbook.core.signals import password_changed
from passbook.core.types import UILoginButton, UIUserSettings
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()
@ -102,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"""
@ -127,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):
@ -143,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
@ -167,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
@ -177,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):

View File

@ -1,46 +1,53 @@
"""passbook user settings template tags"""
from typing import List
from typing import Iterable, List
from django import template
from django.template.context import RequestContext
from passbook.core.models import Factor, Source, UserSettings
from passbook.core.models import Factor, Source
from passbook.core.types import UIUserSettings
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 dataclasses import dataclass
from typing import Optional
@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)
if ui_login_button:
kwargs["sources"].append(ui_login_button)
ui_login_button = source.ui_login_button
# if kwargs["sources"]:
# self.template_name = "login/with_sources.html"
return super().get_context_data(**kwargs)