*(minor): switch has_user_settings to return Optional dataclass instead of tuple
This commit is contained in:
parent
088b9592cd
commit
2e15b24f0a
|
@ -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
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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
|
||||||
|
|
5
passbook/factors/captcha/admin.py
Normal file
5
passbook/factors/captcha/admin.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
"""captcha factor admin"""
|
||||||
|
|
||||||
|
from passbook.lib.admin import admin_autoregister
|
||||||
|
|
||||||
|
admin_autoregister('passbook_factors_captcha')
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}"
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
Reference in a new issue