*(minor): switch has_user_settings to return Optional dataclass instead of tuple

This commit is contained in:
Langhammer, Jens 2019-10-09 12:47:14 +02:00
parent 088b9592cd
commit 2e15b24f0a
8 changed files with 62 additions and 43 deletions

View File

@ -2,6 +2,7 @@
from datetime import timedelta from datetime import timedelta
from random import SystemRandom from random import SystemRandom
from time import sleep from time import sleep
from typing import Optional
from uuid import uuid4 from uuid import uuid4
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
@ -74,6 +75,20 @@ class PolicyModel(UUIDModel, CreatedUpdatedModel):
policies = models.ManyToManyField('Policy', blank=True) 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(PolicyModel): class Factor(PolicyModel):
"""Authentication factor, multiple instances of the same Factor can be used""" """Authentication factor, multiple instances of the same Factor can be used"""
@ -86,11 +101,10 @@ class Factor(PolicyModel):
type = '' type = ''
form = '' form = ''
def has_user_settings(self): def user_settings(self) -> Optional[UserSettings]:
"""Entrypoint to integrate with User settings. Can either return False if no """Entrypoint to integrate with User settings. Can either return None if no
user settings are available, or a tuple or string, string, string where the first string user settings are available, or an instanace of UserSettings."""
is the name the item has, the second string is the icon and the third is the view-name.""" return None
return False
def __str__(self): def __str__(self):
return f"Factor {self.slug}" return f"Factor {self.slug}"
@ -147,11 +161,10 @@ class Source(PolicyModel):
"""Return additional Info, such as a callback URL. Show in the administration interface.""" """Return additional Info, such as a callback URL. Show in the administration interface."""
return None return None
def has_user_settings(self): def user_settings(self) -> Optional[UserSettings]:
"""Entrypoint to integrate with User settings. Can either return False if no """Entrypoint to integrate with User settings. Can either return None if no
user settings are available, or a tuple or string, string, string where the first string user settings are available, or an instanace of UserSettings."""
is the name the item has, the second string is the icon and the third is the view-name.""" return None
return False
def __str__(self): def __str__(self):
return self.name return self.name

View File

@ -18,19 +18,19 @@
</li> </li>
<li class="nav-divider"></li> <li class="nav-divider"></li>
{% user_factors as uf %} {% user_factors as uf %}
{% for name, icon, link in uf %} {% for user_settings in uf %}
<li class="{% is_active link %}"> <li class="{% is_active user_settings.view_name %}">
<a href="{% url link %}"> <a href="{% url user_settings.view_name %}">
<i class="{{ icon }}"></i> {{ name }} <i class="{{ user_settings.icon }}"></i> {{ user_settings.name }}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}
<li class="nav-divider"></li> <li class="nav-divider"></li>
{% user_sources as us %} {% user_sources as us %}
{% for name, icon, link in us %} {% for user_settings in us %}
<li class="{% if link == request.get_full_path %} active {% endif %}"> <li class="{% if user_settings.view_name == request.get_full_path %} active {% endif %}">
<a href="{{ link }}"> <a href="{{ user_settings.view_name }}">
<i class="{{ icon }}"></i> {{ name }} <i class="{{ user_settings.icon }}"></i> {{ user_settings.name }}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}

View File

@ -1,37 +1,37 @@
"""passbook user settings template tags""" """passbook user settings template tags"""
from typing import List
from django import template from django import template
from django.template.context import RequestContext from django.template.context import RequestContext
from passbook.core.models import Factor, Source from passbook.core.models import Factor, Source, UserSettings
from passbook.policies.engine import PolicyEngine from passbook.policies.engine import PolicyEngine
register = template.Library() register = template.Library()
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def user_factors(context: RequestContext): def user_factors(context: RequestContext) -> List[UserSettings]:
"""Return list of all factors which apply to user""" """Return list of all factors which apply to user"""
user = context.get('request').user user = context.get('request').user
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses() _all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
matching_factors = [] matching_factors: List[UserSettings] = []
for factor in _all_factors: for factor in _all_factors:
_link = factor.has_user_settings() user_settings = factor.user_settings()
policy_engine = PolicyEngine(factor.policies.all()) policy_engine = PolicyEngine(factor.policies.all())
policy_engine.for_user(user).with_request(context.get('request')).build() policy_engine.for_user(user).with_request(context.get('request')).build()
if policy_engine.passing and _link: if policy_engine.passing and user_settings:
matching_factors.append(_link) matching_factors.append(user_settings)
return matching_factors return matching_factors
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def user_sources(context: RequestContext): def user_sources(context: RequestContext) -> List[UserSettings]:
"""Return a list of all sources which are enabled for the user""" """Return a list of all sources which are enabled for the user"""
user = context.get('request').user user = context.get('request').user
_all_sources = Source.objects.filter(enabled=True).select_subclasses() _all_sources = Source.objects.filter(enabled=True).select_subclasses()
matching_sources = [] matching_sources: List[UserSettings] = []
for factor in _all_sources: for factor in _all_sources:
_link = factor.has_user_settings() user_settings = factor.user_settings()
policy_engine = PolicyEngine(factor.policies.all()) policy_engine = PolicyEngine(factor.policies.all())
policy_engine.for_user(user).with_request(context.get('request')).build() policy_engine.for_user(user).with_request(context.get('request')).build()
if policy_engine.passing and _link: if policy_engine.passing and user_settings:
matching_sources.append(_link) matching_sources.append(user_settings)
return matching_sources return matching_sources

View File

@ -0,0 +1,5 @@
"""captcha factor admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister('passbook_factors_captcha')

View File

@ -1,6 +1,8 @@
"""passbook captcha factor forms""" """passbook captcha factor forms"""
from captcha.fields import ReCaptchaField from captcha.fields import ReCaptchaField
from django import forms from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _
from passbook.factors.captcha.models import CaptchaFactor from passbook.factors.captcha.models import CaptchaFactor
from passbook.factors.forms import GENERAL_FIELDS from passbook.factors.forms import GENERAL_FIELDS
@ -21,6 +23,7 @@ class CaptchaFactorForm(forms.ModelForm):
widgets = { widgets = {
'name': forms.TextInput(), 'name': forms.TextInput(),
'order': forms.NumberInput(), 'order': forms.NumberInput(),
'policies': FilteredSelectMultiple(_('policies'), False),
'public_key': forms.TextInput(), 'public_key': forms.TextInput(),
'private_key': forms.TextInput(), 'private_key': forms.TextInput(),
} }

View File

@ -3,7 +3,7 @@
from django.db import models from django.db import models
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.core.models import Factor from passbook.core.models import Factor, UserSettings
class OTPFactor(Factor): class OTPFactor(Factor):
@ -15,8 +15,8 @@ class OTPFactor(Factor):
type = 'passbook.factors.otp.factors.OTPFactor' type = 'passbook.factors.otp.factors.OTPFactor'
form = 'passbook.factors.otp.forms.OTPFactorForm' form = 'passbook.factors.otp.forms.OTPFactorForm'
def has_user_settings(self): def user_settings(self) -> UserSettings:
return _('OTP'), 'pficon-locked', 'passbook_factors_otp:otp-user-settings' return UserSettings(_('OTP'), 'pficon-locked', 'passbook_factors_otp:otp-user-settings')
def __str__(self): def __str__(self):
return f"OTP Factor {self.slug}" return f"OTP Factor {self.slug}"

View File

@ -3,7 +3,7 @@ from django.contrib.postgres.fields import ArrayField
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from passbook.core.models import Factor, Policy, User from passbook.core.models import Factor, Policy, User, UserSettings
class PasswordFactor(Factor): class PasswordFactor(Factor):
@ -16,8 +16,9 @@ class PasswordFactor(Factor):
type = 'passbook.factors.password.factor.PasswordFactor' type = 'passbook.factors.password.factor.PasswordFactor'
form = 'passbook.factors.password.forms.PasswordFactorForm' form = 'passbook.factors.password.forms.PasswordFactorForm'
def has_user_settings(self): def user_settings(self):
return _('Change Password'), 'pficon-key', 'passbook_core:user-change-password' return UserSettings(_('Change Password'), 'pficon-key',
'passbook_core:user-change-password')
def password_passes(self, user: User) -> bool: def password_passes(self, user: User) -> bool:
"""Return true if user's password passes, otherwise False or raise Exception""" """Return true if user's password passes, otherwise False or raise Exception"""

View File

@ -4,7 +4,7 @@ from django.db import models
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.core.models import Source, UserSourceConnection from passbook.core.models import Source, UserSourceConnection, UserSettings
from passbook.sources.oauth.clients import get_client from passbook.sources.oauth.clients import get_client
@ -37,18 +37,15 @@ class OAuthSource(Source):
reverse_lazy('passbook_sources_oauth:oauth-client-callback', reverse_lazy('passbook_sources_oauth:oauth-client-callback',
kwargs={'source_slug': self.slug}) kwargs={'source_slug': self.slug})
def has_user_settings(self): def user_settings(self) -> UserSettings:
"""Entrypoint to integrate with User settings. Can either return False if no
user settings are available, or a tuple or string, string, string where the first string
is the name the item has, the second string is the icon and the third is the view-name."""
icon_type = self.provider_type icon_type = self.provider_type
if icon_type == 'azure ad': if icon_type == 'azure ad':
icon_type = 'windows' icon_type = 'windows'
icon_class = 'fa fa-%s' % icon_type icon_class = 'fa fa-%s' % icon_type
view_name = 'passbook_sources_oauth:oauth-client-user' view_name = 'passbook_sources_oauth:oauth-client-user'
return self.name, icon_class, reverse((view_name), kwargs={ return UserSettings(self.name, icon_class, reverse((view_name), kwargs={
'source_slug': self.slug 'source_slug': self.slug
}) }))
class Meta: class Meta: