0002-authenticators-migrate-login-password-authenticator-.patch
src/authentic2/apps/authenticators/forms.py | ||
---|---|---|
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | 17 |
from django import forms |
18 |
from django.core.exceptions import ValidationError |
|
19 |
from django.template import Template, TemplateSyntaxError, VariableDoesNotExist |
|
20 |
from django.utils.translation import ugettext as _ |
|
18 | 21 | |
19 | 22 |
from authentic2.forms.mixins import SlugMixin |
20 | 23 | |
21 |
from .models import BaseAuthenticator |
|
24 |
from .models import BaseAuthenticator, LoginPasswordAuthenticator |
|
25 | ||
26 | ||
27 |
class AuthenticatorFormMixin: |
|
28 |
def clean_show_condition(self): |
|
29 |
condition = self.cleaned_data['show_condition'] |
|
30 |
if condition: |
|
31 |
try: |
|
32 |
Template('{%% if %s %%}OK{%% endif %%}' % condition) |
|
33 |
except (TemplateSyntaxError, VariableDoesNotExist) as e: |
|
34 |
raise ValidationError(_('template syntax error: %s') % e) |
|
35 |
return condition |
|
22 | 36 | |
23 | 37 | |
24 | 38 |
class AuthenticatorAddForm(SlugMixin, forms.ModelForm): |
25 | 39 |
field_order = ('authenticator', 'name', 'ou') |
26 |
authenticators = {x.type: x for x in BaseAuthenticator.__subclasses__()} |
|
40 |
authenticators = {x.type: x for x in BaseAuthenticator.__subclasses__() if not x.internal}
|
|
27 | 41 | |
28 | 42 |
authenticator = forms.ChoiceField(choices=[(k, v._meta.verbose_name) for k, v in authenticators.items()]) |
29 | 43 | |
... | ... | |
35 | 49 |
Authenticator = self.authenticators[self.cleaned_data['authenticator']] |
36 | 50 |
self.instance = Authenticator(name=self.cleaned_data['name'], ou=self.cleaned_data['ou']) |
37 | 51 |
return super().save() |
52 | ||
53 | ||
54 |
class LoginPasswordAuthenticatorEditForm(AuthenticatorFormMixin, forms.ModelForm): |
|
55 |
class Meta: |
|
56 |
model = LoginPasswordAuthenticator |
|
57 |
exclude = ('name', 'slug', 'ou') |
src/authentic2/apps/authenticators/migrations/0002_loginpasswordauthenticator.py | ||
---|---|---|
1 |
# Generated by Django 2.2.28 on 2022-04-13 12:56 |
|
2 | ||
3 |
import django.db.models.deletion |
|
4 |
from django.db import migrations, models |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('authenticators', '0001_initial'), |
|
11 |
] |
|
12 | ||
13 |
operations = [ |
|
14 |
migrations.CreateModel( |
|
15 |
name='LoginPasswordAuthenticator', |
|
16 |
fields=[ |
|
17 |
( |
|
18 |
'baseauthenticator_ptr', |
|
19 |
models.OneToOneField( |
|
20 |
auto_created=True, |
|
21 |
on_delete=django.db.models.deletion.CASCADE, |
|
22 |
parent_link=True, |
|
23 |
primary_key=True, |
|
24 |
serialize=False, |
|
25 |
to='authenticators.BaseAuthenticator', |
|
26 |
), |
|
27 |
), |
|
28 |
( |
|
29 |
'remember_me', |
|
30 |
models.PositiveIntegerField( |
|
31 |
blank=True, |
|
32 |
help_text='Session duration as seconds when using the remember me checkbox. Leave blank to hide the checkbox.', |
|
33 |
null=True, |
|
34 |
verbose_name='Remember me duration', |
|
35 |
), |
|
36 |
), |
|
37 |
( |
|
38 |
'include_ou_selector', |
|
39 |
models.BooleanField(default=False, verbose_name='Include OU selector in login form'), |
|
40 |
), |
|
41 |
], |
|
42 |
options={ |
|
43 |
'verbose_name': 'Password', |
|
44 |
}, |
|
45 |
bases=('authenticators.baseauthenticator',), |
|
46 |
), |
|
47 |
] |
src/authentic2/apps/authenticators/migrations/0003_auto_20220413_1504.py | ||
---|---|---|
1 |
# Generated by Django 2.2.28 on 2022-04-13 13:04 |
|
2 | ||
3 |
from django.db import migrations |
|
4 | ||
5 |
from authentic2 import app_settings |
|
6 | ||
7 | ||
8 |
def create_login_password_authenticator(apps, schema_editor): |
|
9 |
kwargs_settings = getattr(app_settings, 'AUTH_FRONTENDS_KWARGS', {}) |
|
10 |
password_settings = kwargs_settings.get('password', {}) |
|
11 | ||
12 |
LoginPasswordAuthenticator = apps.get_model('authenticators', 'LoginPasswordAuthenticator') |
|
13 |
LoginPasswordAuthenticator.objects.get_or_create( |
|
14 |
slug='password-authenticator', |
|
15 |
defaults={ |
|
16 |
'order': password_settings.get('priority', 0), |
|
17 |
'show_condition': password_settings.get('show_condition', ''), |
|
18 |
'enabled': app_settings.A2_AUTH_PASSWORD_ENABLE, |
|
19 |
'remember_me': app_settings.A2_USER_REMEMBER_ME, |
|
20 |
'include_ou_selector': app_settings.A2_LOGIN_FORM_OU_SELECTOR, |
|
21 |
}, |
|
22 |
) |
|
23 | ||
24 | ||
25 |
class Migration(migrations.Migration): |
|
26 | ||
27 |
dependencies = [ |
|
28 |
('authenticators', '0002_loginpasswordauthenticator'), |
|
29 |
] |
|
30 | ||
31 |
operations = [ |
|
32 |
migrations.RunPython(create_login_password_authenticator, reverse_code=migrations.RunPython.noop), |
|
33 |
] |
src/authentic2/apps/authenticators/models.py | ||
---|---|---|
23 | 23 |
from django.utils.formats import date_format |
24 | 24 |
from django.utils.translation import ugettext_lazy as _ |
25 | 25 | |
26 |
from authentic2 import views |
|
26 | 27 |
from authentic2.utils.evaluate import evaluate_condition |
27 | 28 | |
28 | 29 |
from .query import AuthenticatorManager |
... | ... | |
60 | 61 | |
61 | 62 |
type = '' |
62 | 63 |
manager_form_class = None |
64 |
internal = False |
|
63 | 65 |
description_fields = ['show_condition'] |
64 | 66 | |
65 | 67 |
class Meta: |
... | ... | |
106 | 108 |
except Exception as e: |
107 | 109 |
logger.error(e) |
108 | 110 |
return False |
111 | ||
112 | ||
113 |
class LoginPasswordAuthenticator(BaseAuthenticator): |
|
114 |
remember_me = models.PositiveIntegerField( |
|
115 |
_('Remember me duration'), |
|
116 |
blank=True, |
|
117 |
null=True, |
|
118 |
help_text=_( |
|
119 |
'Session duration as seconds when using the remember me checkbox. Leave blank to hide the checkbox.' |
|
120 |
), |
|
121 |
) |
|
122 |
include_ou_selector = models.BooleanField(_('Include OU selector in login form'), default=False) |
|
123 | ||
124 |
type = 'password' |
|
125 |
how = ['password', 'password-on-https'] |
|
126 |
internal = True |
|
127 | ||
128 |
class Meta: |
|
129 |
verbose_name = _('Password') |
|
130 | ||
131 |
@property |
|
132 |
def manager_form_class(self): |
|
133 |
from .forms import LoginPasswordAuthenticatorEditForm |
|
134 | ||
135 |
return LoginPasswordAuthenticatorEditForm |
|
136 | ||
137 |
def login(self, request, *args, **kwargs): |
|
138 |
return views.login_password_login(request, self, *args, **kwargs) |
|
139 | ||
140 |
def profile(self, request, *args, **kwargs): |
|
141 |
return views.login_password_profile(request, *args, **kwargs) |
|
142 | ||
143 |
def registration(self, request, *args, **kwargs): |
|
144 |
context = kwargs.get('context', {}) |
|
145 |
return render(request, 'authentic2/login_password_registration_form.html', context) |
src/authentic2/apps/authenticators/templates/authentic2/authenticators/authenticator_detail.html | ||
---|---|---|
9 | 9 |
<a href="{% url 'a2-manager-authenticator-toggle' pk=object.pk %}">{{ object.enabled|yesno:_("Disable,Enable") }}</a> |
10 | 10 |
<a href="{% url 'a2-manager-authenticator-edit' pk=object.pk %}">{% trans "Edit" %}</a> |
11 | 11 |
<ul class="extra-actions-menu"> |
12 |
<li><a rel="popup" href="{% url 'a2-manager-authenticator-delete' pk=object.pk %}">{% trans "Delete" %}</a></li> |
|
12 |
{% if not object.internal %} |
|
13 |
<li><a rel="popup" href="{% url 'a2-manager-authenticator-delete' pk=object.pk %}">{% trans "Delete" %}</a></li> |
|
14 |
{% endif %} |
|
13 | 15 |
</ul> |
14 | 16 |
</span> |
15 | 17 |
{% endblock %} |
src/authentic2/apps/authenticators/views.py | ||
---|---|---|
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | 17 |
from django.contrib import messages |
18 |
from django.core.exceptions import PermissionDenied |
|
18 | 19 |
from django.http import HttpResponseRedirect |
19 | 20 |
from django.urls import reverse_lazy |
20 | 21 |
from django.utils.translation import ugettext as _ |
... | ... | |
79 | 80 |
model = BaseAuthenticator |
80 | 81 |
success_url = reverse_lazy('a2-manager-authenticators') |
81 | 82 | |
83 |
def dispatch(self, *args, **kwargs): |
|
84 |
if self.get_object().internal: |
|
85 |
raise PermissionDenied |
|
86 |
return super().dispatch(*args, **kwargs) |
|
87 | ||
82 | 88 | |
83 | 89 |
delete = AuthenticatorDeleteView.as_view() |
84 | 90 |
src/authentic2/authenticators.py | ||
---|---|---|
16 | 16 | |
17 | 17 |
import logging |
18 | 18 | |
19 |
from django.db.models import Count |
|
20 |
from django.shortcuts import render |
|
21 |
from django.utils.translation import ugettext as _ |
|
22 |
from django.utils.translation import ugettext_lazy |
|
23 | ||
24 |
from authentic2.a2_rbac.models import OrganizationalUnit as OU |
|
25 |
from authentic2.a2_rbac.models import Role |
|
26 |
from authentic2.custom_user.models import User |
|
27 | ||
28 |
from . import app_settings, views |
|
29 |
from .forms import authentication as authentication_forms |
|
30 |
from .utils import misc as utils_misc |
|
31 | 19 |
from .utils.evaluate import evaluate_condition |
32 |
from .utils.service import get_service |
|
33 |
from .utils.views import csrf_token_check |
|
34 | 20 | |
35 | 21 |
logger = logging.getLogger(__name__) |
36 | 22 | |
... | ... | |
61 | 47 | |
62 | 48 |
def get_identifier(self): |
63 | 49 |
return self.id |
64 | ||
65 | ||
66 |
class LoginPasswordAuthenticator(BaseAuthenticator): |
|
67 |
id = 'password' |
|
68 |
how = ['password', 'password-on-https'] |
|
69 |
submit_name = 'login-password-submit' |
|
70 |
priority = 0 |
|
71 | ||
72 |
def enabled(self): |
|
73 |
return app_settings.A2_AUTH_PASSWORD_ENABLE |
|
74 | ||
75 |
def name(self): |
|
76 |
return ugettext_lazy('Password') |
|
77 | ||
78 |
def get_service_ous(self, service): |
|
79 |
roles = Role.objects.filter(allowed_services=service).children() |
|
80 |
if not roles: |
|
81 |
return [] |
|
82 |
service_ou_ids = [] |
|
83 |
qs = ( |
|
84 |
User.objects.filter(roles__in=roles) |
|
85 |
.values_list('ou') |
|
86 |
.annotate(count=Count('ou')) |
|
87 |
.order_by('-count') |
|
88 |
) |
|
89 |
for ou_id, dummy_count in qs: |
|
90 |
if not ou_id: |
|
91 |
continue |
|
92 |
service_ou_ids.append(ou_id) |
|
93 |
if not service_ou_ids: |
|
94 |
return [] |
|
95 |
return OU.objects.filter(pk__in=service_ou_ids) |
|
96 | ||
97 |
def get_preferred_ous(self, request): |
|
98 |
service = get_service(request) |
|
99 |
preferred_ous_cookie = utils_misc.get_remember_cookie(request, 'preferred-ous') |
|
100 |
preferred_ous = [] |
|
101 |
if preferred_ous_cookie: |
|
102 |
preferred_ous.extend(OU.objects.filter(pk__in=preferred_ous_cookie)) |
|
103 |
# for the special case of services open to only one OU, pre-select it |
|
104 |
if service: |
|
105 |
for ou in self.get_service_ous(service): |
|
106 |
if ou in preferred_ous: |
|
107 |
continue |
|
108 |
preferred_ous.append(ou) |
|
109 |
return preferred_ous |
|
110 | ||
111 |
def login(self, request, *args, **kwargs): |
|
112 |
context = kwargs.get('context', {}) |
|
113 |
is_post = request.method == 'POST' and self.submit_name in request.POST |
|
114 |
data = request.POST if is_post else None |
|
115 |
initial = {} |
|
116 |
preferred_ous = [] |
|
117 |
request.failed_logins = {} |
|
118 | ||
119 |
# Special handling when the form contains an OU selector |
|
120 |
if app_settings.A2_LOGIN_FORM_OU_SELECTOR: |
|
121 |
preferred_ous = self.get_preferred_ous(request) |
|
122 |
if preferred_ous: |
|
123 |
initial['ou'] = preferred_ous[0] |
|
124 | ||
125 |
form = authentication_forms.AuthenticationForm( |
|
126 |
request=request, data=data, initial=initial, preferred_ous=preferred_ous |
|
127 |
) |
|
128 |
if request.user.is_authenticated and request.login_token.get('action'): |
|
129 |
form.initial['username'] = request.user.username or request.user.email |
|
130 |
form.fields['username'].widget.attrs['readonly'] = True |
|
131 |
form.fields['password'].widget.attrs['autofocus'] = True |
|
132 |
else: |
|
133 |
form.fields['username'].widget.attrs['autofocus'] = not (bool(context.get('block_index'))) |
|
134 |
if app_settings.A2_ACCEPT_EMAIL_AUTHENTICATION: |
|
135 |
form.fields['username'].label = _('Username or email') |
|
136 |
if app_settings.A2_USERNAME_LABEL: |
|
137 |
form.fields['username'].label = app_settings.A2_USERNAME_LABEL |
|
138 |
is_secure = request.is_secure |
|
139 |
context['submit_name'] = self.submit_name |
|
140 |
if is_post: |
|
141 |
csrf_token_check(request, form) |
|
142 |
if form.is_valid(): |
|
143 |
if is_secure: |
|
144 |
how = 'password-on-https' |
|
145 |
else: |
|
146 |
how = 'password' |
|
147 |
if form.cleaned_data.get('remember_me'): |
|
148 |
request.session['remember_me'] = True |
|
149 |
request.session.set_expiry(app_settings.A2_USER_REMEMBER_ME) |
|
150 |
response = utils_misc.login(request, form.get_user(), how) |
|
151 |
if 'ou' in form.fields: |
|
152 |
utils_misc.prepend_remember_cookie( |
|
153 |
request, response, 'preferred-ous', form.cleaned_data['ou'].pk |
|
154 |
) |
|
155 | ||
156 |
if hasattr(request, 'needs_password_change'): |
|
157 |
del request.needs_password_change |
|
158 |
return utils_misc.redirect( |
|
159 |
request, 'password_change', params={'next': response.url}, resolve=True |
|
160 |
) |
|
161 | ||
162 |
return response |
|
163 |
else: |
|
164 |
username = form.cleaned_data.get('username', '').strip() |
|
165 |
if request.failed_logins: |
|
166 |
for user, failure_data in request.failed_logins.items(): |
|
167 |
request.journal.record( |
|
168 |
'user.login.failure', |
|
169 |
user=user, |
|
170 |
reason=failure_data.get('reason', None), |
|
171 |
username=username, |
|
172 |
) |
|
173 |
elif username: |
|
174 |
request.journal.record('user.login.failure', username=username) |
|
175 |
context['form'] = form |
|
176 |
return render(request, 'authentic2/login_password_form.html', context) |
|
177 | ||
178 |
def profile(self, request, *args, **kwargs): |
|
179 |
return views.login_password_profile(request, *args, **kwargs) |
|
180 | ||
181 |
def registration(self, request, *args, **kwargs): |
|
182 |
context = kwargs.get('context', {}) |
|
183 |
return render(request, 'authentic2/login_password_registration_form.html', context) |
src/authentic2/forms/authentication.py | ||
---|---|---|
51 | 51 | |
52 | 52 |
def __init__(self, *args, **kwargs): |
53 | 53 |
preferred_ous = kwargs.pop('preferred_ous', []) |
54 |
self.authenticator = kwargs.pop('authenticator') |
|
54 | 55 | |
55 | 56 |
super().__init__(*args, **kwargs) |
56 | 57 | |
... | ... | |
60 | 61 |
factor=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR, |
61 | 62 |
) |
62 | 63 | |
63 |
if not app_settings.A2_USER_REMEMBER_ME:
|
|
64 |
if not self.authenticator.remember_me:
|
|
64 | 65 |
del self.fields['remember_me'] |
65 | 66 | |
66 |
if not app_settings.A2_LOGIN_FORM_OU_SELECTOR:
|
|
67 |
if not self.authenticator.include_ou_selector:
|
|
67 | 68 |
del self.fields['ou'] |
68 | 69 |
else: |
69 | 70 |
if preferred_ous: |
... | ... | |
135 | 136 |
def media(self): |
136 | 137 |
media = super().media |
137 | 138 |
media = media + Media(js=['authentic2/js/js_seconds_until.js']) |
138 |
if app_settings.A2_LOGIN_FORM_OU_SELECTOR:
|
|
139 |
if self.authenticator.include_ou_selector:
|
|
139 | 140 |
media = media + Media(js=['authentic2/js/ou_selector.js']) |
140 | 141 |
return media |
141 | 142 |
src/authentic2/settings.py | ||
---|---|---|
191 | 191 |
'authentic2_auth_saml.authenticators.SAMLAuthenticator', |
192 | 192 |
'authentic2_auth_oidc.authenticators.OIDCAuthenticator', |
193 | 193 |
'authentic2_auth_fc.authenticators.FcAuthenticator', |
194 |
) + plugins.register_plugins_authenticators(('authentic2.authenticators.LoginPasswordAuthenticator',))
|
|
194 |
) |
|
195 | 195 | |
196 | 196 |
########################### |
197 | 197 |
# RBAC settings |
src/authentic2/utils/misc.py | ||
---|---|---|
164 | 164 |
'''Return the list of enabled cleaned backends.''' |
165 | 165 |
backends = [] |
166 | 166 |
if setting_name == 'AUTH_FRONTENDS': |
167 |
from authentic2.apps.authenticators.models import BaseAuthenticator |
|
167 |
from authentic2.apps.authenticators.models import BaseAuthenticator, LoginPasswordAuthenticator
|
|
168 | 168 | |
169 |
backends = list(BaseAuthenticator.authenticators.filter(enabled=True)) |
|
169 |
backends = list( |
|
170 |
BaseAuthenticator.authenticators.filter(enabled=True).exclude(slug='password-authenticator') |
|
171 |
) |
|
172 |
password_backend, dummy = LoginPasswordAuthenticator.objects.get_or_create( |
|
173 |
slug='password-authenticator', |
|
174 |
defaults={'enabled': True}, |
|
175 |
) |
|
176 |
if password_backend.enabled: |
|
177 |
backends.append(password_backend) |
|
170 | 178 | |
171 | 179 |
for backend_path in getattr(app_settings, setting_name): |
172 | 180 |
kwargs = {} |
src/authentic2/views.py | ||
---|---|---|
28 | 28 |
from django.contrib.auth.decorators import login_required |
29 | 29 |
from django.contrib.auth.views import PasswordChangeView as DjPasswordChangeView |
30 | 30 |
from django.core.exceptions import FieldDoesNotExist, ValidationError |
31 |
from django.db.models import Count |
|
31 | 32 |
from django.db.models.query import Q |
32 | 33 |
from django.db.transaction import atomic |
33 | 34 |
from django.forms import CharField |
... | ... | |
51 | 52 |
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView |
52 | 53 |
from ratelimit.utils import is_ratelimited |
53 | 54 | |
55 |
from authentic2.a2_rbac.models import Role |
|
54 | 56 |
from authentic2.custom_user.models import iter_attributes |
57 |
from authentic2.forms import authentication as authentication_forms |
|
55 | 58 |
from authentic2_idp_oidc.models import OIDCAuthorization |
56 | 59 | |
57 | 60 |
from . import app_settings, attribute_kinds, cbv, constants, decorators, hooks, models, validators |
... | ... | |
67 | 70 |
from .utils.evaluate import make_condition_context |
68 | 71 |
from .utils.service import get_service, set_home_url |
69 | 72 |
from .utils.view_decorators import enable_view_restriction |
73 |
from .utils.views import csrf_token_check |
|
70 | 74 | |
71 | 75 |
User = get_user_model() |
72 | 76 | |
... | ... | |
697 | 701 |
return response |
698 | 702 | |
699 | 703 | |
704 |
def login_password_login(request, authenticator, *args, **kwargs): |
|
705 |
def get_service_ous(service): |
|
706 |
roles = Role.objects.filter(allowed_services=service).children() |
|
707 |
if not roles: |
|
708 |
return [] |
|
709 |
service_ou_ids = [] |
|
710 |
qs = ( |
|
711 |
User.objects.filter(roles__in=roles) |
|
712 |
.values_list('ou') |
|
713 |
.annotate(count=Count('ou')) |
|
714 |
.order_by('-count') |
|
715 |
) |
|
716 |
for ou_id, dummy_count in qs: |
|
717 |
if not ou_id: |
|
718 |
continue |
|
719 |
service_ou_ids.append(ou_id) |
|
720 |
if not service_ou_ids: |
|
721 |
return [] |
|
722 |
return OU.objects.filter(pk__in=service_ou_ids) |
|
723 | ||
724 |
def get_preferred_ous(request): |
|
725 |
service = get_service(request) |
|
726 |
preferred_ous_cookie = utils_misc.get_remember_cookie(request, 'preferred-ous') |
|
727 |
preferred_ous = [] |
|
728 |
if preferred_ous_cookie: |
|
729 |
preferred_ous.extend(OU.objects.filter(pk__in=preferred_ous_cookie)) |
|
730 |
# for the special case of services open to only one OU, pre-select it |
|
731 |
if service: |
|
732 |
for ou in get_service_ous(service): |
|
733 |
if ou in preferred_ous: |
|
734 |
continue |
|
735 |
preferred_ous.append(ou) |
|
736 |
return preferred_ous |
|
737 | ||
738 |
context = kwargs.get('context', {}) |
|
739 |
is_post = request.method == 'POST' and 'login-password-submit' in request.POST |
|
740 |
data = request.POST if is_post else None |
|
741 |
initial = {} |
|
742 |
preferred_ous = [] |
|
743 |
request.failed_logins = {} |
|
744 | ||
745 |
# Special handling when the form contains an OU selector |
|
746 |
if authenticator.include_ou_selector: |
|
747 |
preferred_ous = get_preferred_ous(request) |
|
748 |
if preferred_ous: |
|
749 |
initial['ou'] = preferred_ous[0] |
|
750 | ||
751 |
form = authentication_forms.AuthenticationForm( |
|
752 |
request=request, data=data, initial=initial, preferred_ous=preferred_ous, authenticator=authenticator |
|
753 |
) |
|
754 |
if request.user.is_authenticated and request.login_token.get('action'): |
|
755 |
form.initial['username'] = request.user.username or request.user.email |
|
756 |
form.fields['username'].widget.attrs['readonly'] = True |
|
757 |
form.fields['password'].widget.attrs['autofocus'] = True |
|
758 |
else: |
|
759 |
form.fields['username'].widget.attrs['autofocus'] = not (bool(context.get('block_index'))) |
|
760 |
if app_settings.A2_ACCEPT_EMAIL_AUTHENTICATION: |
|
761 |
form.fields['username'].label = _('Username or email') |
|
762 |
if app_settings.A2_USERNAME_LABEL: |
|
763 |
form.fields['username'].label = app_settings.A2_USERNAME_LABEL |
|
764 |
is_secure = request.is_secure |
|
765 |
context['submit_name'] = 'login-password-submit' |
|
766 |
if is_post: |
|
767 |
csrf_token_check(request, form) |
|
768 |
if form.is_valid(): |
|
769 |
if is_secure: |
|
770 |
how = 'password-on-https' |
|
771 |
else: |
|
772 |
how = 'password' |
|
773 |
if form.cleaned_data.get('remember_me'): |
|
774 |
request.session['remember_me'] = True |
|
775 |
request.session.set_expiry(authenticator.remember_me) |
|
776 |
response = utils_misc.login(request, form.get_user(), how) |
|
777 |
if 'ou' in form.fields: |
|
778 |
utils_misc.prepend_remember_cookie( |
|
779 |
request, response, 'preferred-ous', form.cleaned_data['ou'].pk |
|
780 |
) |
|
781 | ||
782 |
if hasattr(request, 'needs_password_change'): |
|
783 |
del request.needs_password_change |
|
784 |
return utils_misc.redirect( |
|
785 |
request, 'password_change', params={'next': response.url}, resolve=True |
|
786 |
) |
|
787 | ||
788 |
return response |
|
789 |
else: |
|
790 |
username = form.cleaned_data.get('username', '').strip() |
|
791 |
if request.failed_logins: |
|
792 |
for user, failure_data in request.failed_logins.items(): |
|
793 |
request.journal.record( |
|
794 |
'user.login.failure', |
|
795 |
user=user, |
|
796 |
reason=failure_data.get('reason', None), |
|
797 |
username=username, |
|
798 |
) |
|
799 |
elif username: |
|
800 |
request.journal.record('user.login.failure', username=username) |
|
801 |
context['form'] = form |
|
802 |
return render(request, 'authentic2/login_password_form.html', context) |
|
803 | ||
804 | ||
700 | 805 |
def login_password_profile(request, *args, **kwargs): |
701 | 806 |
context = kwargs.pop('context', {}) |
702 | 807 |
can_change_password = utils_misc.user_can_change_password(request=request) |
tests/auth_fc/test_auth_fc.py | ||
---|---|---|
30 | 30 | |
31 | 31 |
from authentic2.a2_rbac.models import OrganizationalUnit as OU |
32 | 32 |
from authentic2.a2_rbac.utils import get_default_ou |
33 |
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator |
|
33 | 34 |
from authentic2.apps.journal.models import Event |
34 | 35 |
from authentic2.custom_user.models import DeletedUser |
35 | 36 |
from authentic2.models import Attribute |
... | ... | |
76 | 77 | |
77 | 78 |
def test_login_autorun(settings, app, franceconnect): |
78 | 79 |
# hide password block |
79 |
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'remote_addr==\'0.0.0.0\''}} |
|
80 |
LoginPasswordAuthenticator.objects.update_or_create( |
|
81 |
slug='password-authenticator', defaults={'enabled': False} |
|
82 |
) |
|
80 | 83 |
response = app.get('/login/') |
81 | 84 |
assert response.location.startswith('https://fcp') |
82 | 85 |
tests/test_auth_oidc.py | ||
---|---|---|
38 | 38 | |
39 | 39 |
from authentic2.a2_rbac.models import OrganizationalUnit |
40 | 40 |
from authentic2.a2_rbac.utils import get_default_ou |
41 |
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator |
|
41 | 42 |
from authentic2.custom_user.models import DeletedUser |
42 | 43 |
from authentic2.models import Attribute, AttributeValue |
43 | 44 |
from authentic2.utils.misc import last_authentication_event |
... | ... | |
494 | 495 |
assert 'Server' in response |
495 | 496 | |
496 | 497 |
# hide password block |
497 |
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'remote_addr==\'0.0.0.0\''}} |
|
498 |
LoginPasswordAuthenticator.objects.update_or_create( |
|
499 |
slug='password-authenticator', defaults={'enabled': False} |
|
500 |
) |
|
498 | 501 |
response = app.get('/login/', status=302) |
499 | 502 |
assert response['Location'] == '/accounts/oidc/login/%s/' % oidc_provider.pk |
500 | 503 |
tests/test_auth_saml.py | ||
---|---|---|
24 | 24 |
from mellon.adapters import UserCreationError |
25 | 25 |
from mellon.models import Issuer, UserSAMLIdentifier |
26 | 26 | |
27 |
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator |
|
27 | 28 |
from authentic2.custom_user.models import DeletedUser |
28 | 29 |
from authentic2.models import Attribute |
29 | 30 |
from authentic2_auth_saml.adapters import AuthenticAdapter, MappingError |
... | ... | |
278 | 279 |
{"METADATA": os.path.join(os.path.dirname(__file__), 'metadata.xml')} |
279 | 280 |
] |
280 | 281 |
# hide password block |
281 |
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'remote_addr==\'0.0.0.0\''}} |
|
282 |
LoginPasswordAuthenticator.objects.update_or_create( |
|
283 |
slug='password-authenticator', defaults={'enabled': False} |
|
284 |
) |
|
282 | 285 |
response = app.get('/login/', status=302) |
283 | 286 |
assert '/accounts/saml/login/?entityID=' in response['Location'] |
284 | 287 |
tests/test_ldap.py | ||
---|---|---|
36 | 36 |
from authentic2 import models |
37 | 37 |
from authentic2.a2_rbac.models import OrganizationalUnit, Role |
38 | 38 |
from authentic2.a2_rbac.utils import get_default_ou |
39 |
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator |
|
39 | 40 |
from authentic2.backends import ldap_backend |
40 | 41 |
from authentic2.models import Service |
41 | 42 |
from authentic2.utils import crypto, switch_user |
... | ... | |
1776 | 1777 |
'use_tls': False, |
1777 | 1778 |
} |
1778 | 1779 |
] |
1779 |
settings.A2_LOGIN_FORM_OU_SELECTOR = True
|
|
1780 |
LoginPasswordAuthenticator.objects.update(include_ou_selector=True)
|
|
1780 | 1781 | |
1781 | 1782 |
# Check login to the wrong ou does not work |
1782 | 1783 |
response = app.get('/login/') |
... | ... | |
1806 | 1807 |
'use_tls': False, |
1807 | 1808 |
} |
1808 | 1809 |
] |
1809 |
settings.A2_LOGIN_FORM_OU_SELECTOR = True
|
|
1810 |
LoginPasswordAuthenticator.objects.update(include_ou_selector=True)
|
|
1810 | 1811 | |
1811 | 1812 |
# Check login to the wrong ou does not work |
1812 | 1813 |
response = app.get('/login/') |
tests/test_login.py | ||
---|---|---|
20 | 20 |
from django.contrib.auth import get_user_model |
21 | 21 | |
22 | 22 |
from authentic2 import models |
23 |
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator |
|
23 | 24 |
from authentic2.utils.misc import get_token_login_url |
24 | 25 | |
25 | 26 |
from .utils import assert_event, login, set_service |
... | ... | |
72 | 73 |
response = app.get('/login/') |
73 | 74 |
assert 'name="login-password-submit"' in response |
74 | 75 | |
75 |
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'False'}}
|
|
76 |
LoginPasswordAuthenticator.objects.update(show_condition='False')
|
|
76 | 77 |
response = app.get('/login/') |
77 | 78 |
# login form must not be displayed |
78 | 79 |
assert 'name="login-password-submit"' not in response |
79 | 80 |
assert len(caplog.records) == 0 |
80 | 81 |
# set a condition with error |
81 | 82 | |
82 |
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': '\'admin\' in unknown'}}
|
|
83 |
LoginPasswordAuthenticator.objects.update(show_condition='\'admin\' in unknown')
|
|
83 | 84 |
response = app.get('/login/') |
84 | 85 |
assert 'name="login-password-submit"' in response |
85 | 86 |
assert len(caplog.records) == 1 |
... | ... | |
88 | 89 |
def test_show_condition_service(db, rf, app, settings): |
89 | 90 |
portal = models.Service.objects.create(pk=1, name='Service', slug='portal') |
90 | 91 |
service = models.Service.objects.create(pk=2, name='Service', slug='service') |
91 |
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'service_slug == \'portal\''}}
|
|
92 |
LoginPasswordAuthenticator.objects.update(show_condition='service_slug == \'portal\'')
|
|
92 | 93 | |
93 | 94 |
response = app.get('/login/') |
94 | 95 |
assert 'name="login-password-submit"' not in response |
... | ... | |
104 | 105 |
assert 'name="login-password-submit"' not in response |
105 | 106 | |
106 | 107 | |
107 |
def test_show_condition_with_headers(app, settings): |
|
108 |
def test_show_condition_with_headers(db, app, settings):
|
|
108 | 109 |
settings.A2_AUTH_OIDC_ENABLE = False # prevent db access by OIDC frontend |
109 |
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': '\'X-Entrouvert\' in headers'}}
|
|
110 |
LoginPasswordAuthenticator.objects.update(show_condition='\'X-Entrouvert\' in headers')
|
|
110 | 111 |
response = app.get('/login/') |
111 | 112 |
assert 'name="login-password-submit"' not in response |
112 | 113 |
response = app.get('/login/', headers={'x-entrouvert': '1'}) |
... | ... | |
172 | 173 | |
173 | 174 | |
174 | 175 |
def test_session_remember_me_ok(app, settings, simple_user, freezer): |
175 |
settings.A2_USER_REMEMBER_ME = 3600 * 24 * 30
|
|
176 |
LoginPasswordAuthenticator.objects.update(remember_me=3600 * 24 * 30)
|
|
176 | 177 |
freezer.move_to('2018-01-01') |
177 | 178 |
# Verify session are longer |
178 | 179 |
login(app, simple_user, remember_me=True) |
... | ... | |
187 | 188 | |
188 | 189 | |
189 | 190 |
def test_session_remember_me_nok(app, settings, simple_user, freezer): |
190 |
settings.A2_USER_REMEMBER_ME = 3600 * 24 * 30
|
|
191 |
LoginPasswordAuthenticator.objects.update(remember_me=3600 * 24 * 30)
|
|
191 | 192 |
freezer.move_to('2018-01-01') |
192 | 193 |
# Verify session are longer |
193 | 194 |
login(app, simple_user, remember_me=True) |
... | ... | |
202 | 203 | |
203 | 204 | |
204 | 205 |
def test_ou_selector(app, settings, simple_user, ou1, ou2, user_ou1, role_ou1): |
205 |
settings.A2_LOGIN_FORM_OU_SELECTOR = True
|
|
206 |
LoginPasswordAuthenticator.objects.update(include_ou_selector=True)
|
|
206 | 207 |
response = app.get('/login/') |
207 | 208 |
# Check selector is here and there are no errors |
208 | 209 |
assert not response.pyquery('.errorlist') |
... | ... | |
359 | 360 |
assert simple_user.first_name in resp.text |
360 | 361 |
assert app.session['_auth_user_id'] == str(simple_user.pk) |
361 | 362 |
assert_event('user.login', user=simple_user, session=app.session, how='token') |
363 | ||
364 | ||
365 |
def test_password_authenticator_data_migration(migration, settings): |
|
366 |
app = 'authenticators' |
|
367 |
migrate_from = [(app, '0002_loginpasswordauthenticator')] |
|
368 |
migrate_to = [(app, '0003_auto_20220413_1504')] |
|
369 | ||
370 |
old_apps = migration.before(migrate_from) |
|
371 |
LoginPasswordAuthenticator = old_apps.get_model(app, 'LoginPasswordAuthenticator') |
|
372 |
assert not LoginPasswordAuthenticator.objects.exists() |
|
373 | ||
374 |
settings.AUTH_FRONTENDS_KWARGS = { |
|
375 |
"password": {"priority": -1, "show_condition": "'backoffice' not in login_hint"} |
|
376 |
} |
|
377 |
settings.A2_LOGIN_FORM_OU_SELECTOR = True |
|
378 |
settings.A2_AUTH_PASSWORD_ENABLE = False |
|
379 |
settings.A2_USER_REMEMBER_ME = 42 |
|
380 | ||
381 |
new_apps = migration.apply(migrate_to) |
|
382 |
LoginPasswordAuthenticator = new_apps.get_model(app, 'LoginPasswordAuthenticator') |
|
383 |
authenticator = LoginPasswordAuthenticator.objects.get() |
|
384 |
assert authenticator.slug == 'password-authenticator' |
|
385 |
assert authenticator.order == -1 |
|
386 |
assert authenticator.show_condition == "'backoffice' not in login_hint" |
|
387 |
assert authenticator.enabled is False |
|
388 |
assert authenticator.remember_me == 42 |
|
389 |
assert authenticator.include_ou_selector is True |
tests/test_manager_authenticators.py | ||
---|---|---|
27 | 27 | |
28 | 28 |
resp = resp.click('Authenticators') |
29 | 29 |
assert 'Authenticators' in resp.text |
30 | ||
31 | ||
32 |
def test_authenticators_password(app, superuser): |
|
33 |
resp = login(app, superuser, path='/manage/authenticators/') |
|
34 |
# Password authenticator already exists |
|
35 |
assert 'Password' in resp.text |
|
36 | ||
37 |
resp = resp.click('Configure') |
|
38 |
assert 'Click "Edit" to change configuration.' in resp.text |
|
39 |
# cannot delete password authenticator |
|
40 |
assert 'Delete' not in resp.text |
|
41 |
app.get('/manage/authenticators/1/delete/', status=403) |
|
42 | ||
43 |
resp = resp.click('Edit') |
|
44 |
assert list(resp.form.fields) == [ |
|
45 |
'csrfmiddlewaretoken', |
|
46 |
'order', |
|
47 |
'show_condition', |
|
48 |
'remember_me', |
|
49 |
'include_ou_selector', |
|
50 |
None, |
|
51 |
] |
|
52 | ||
53 |
resp.form['show_condition'] = '}' |
|
54 |
resp = resp.form.submit() |
|
55 |
assert 'template syntax error: Could not parse' in resp.text |
|
56 | ||
57 |
resp.form['show_condition'] = "'backoffice' in login_hint or remotre_addr == '1.2.3.4'" |
|
58 |
resp = resp.form.submit().follow() |
|
59 |
assert 'Click "Edit" to change configuration.' not in resp.text |
|
60 |
assert ( |
|
61 |
"Show condition: 'backoffice' in login_hint or remotre_addr == '1.2.3.4'" in resp.text |
|
62 |
) |
|
63 | ||
64 |
resp = resp.click('Disable').follow() |
|
65 |
assert 'Authenticator has been disabled.' in resp.text |
|
66 | ||
67 |
resp = app.get('/manage/authenticators/') |
|
68 |
assert 'class="section disabled"' in resp.text |
|
69 | ||
70 |
resp = resp.click('Configure') |
|
71 |
resp = resp.click('Enable').follow() |
|
72 |
assert 'Authenticator has been enabled.' in resp.text |
|
73 | ||
74 |
# cannot add another password authenticator |
|
75 |
resp = app.get('/manage/authenticators/add/') |
|
76 |
assert 'Password' not in resp.text |
|
30 |
- |