0001-misc-allow-authenticators-display-conditions-28215.patch
src/authentic2/authenticators.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import logging |
|
18 | ||
17 | 19 |
from django.db.models import Count |
18 | 20 |
from django.shortcuts import render |
19 | 21 |
from django.utils.translation import ugettext as _, ugettext_lazy |
... | ... | |
22 | 24 |
from authentic2.custom_user.models import User |
23 | 25 |
from . import views, app_settings, utils, constants, models |
24 | 26 |
from .forms import authentication as authentication_forms |
27 |
from .utils.evaluate import evaluate_condition |
|
28 | ||
29 |
logger = logging.getLogger(__name__) |
|
30 | ||
31 | ||
32 |
class BaseAuthenticator(object): |
|
25 | 33 | |
34 |
def __init__(self, show_condition=None, **kwargs): |
|
35 |
self.show_condition = show_condition |
|
26 | 36 | |
27 |
class LoginPasswordAuthenticator(object): |
|
37 |
def get_show_condition(self, instance_id=None): |
|
38 |
if isinstance(self.show_condition, dict): |
|
39 |
if instance_id and instance_id in self.show_condition: |
|
40 |
return self.show_condition[instance_id] |
|
41 |
else: |
|
42 |
return self.show_condition |
|
43 | ||
44 |
def shown(self, remote_addr=None, instance_id=None): |
|
45 |
show_condition = self.get_show_condition(instance_id) |
|
46 |
if not show_condition: |
|
47 |
return True |
|
48 |
try: |
|
49 |
return evaluate_condition(show_condition, {'id': instance_id, 'remote_addr': remote_addr}) |
|
50 |
except Exception as e: |
|
51 |
logger.error(e) |
|
52 |
return False |
|
53 | ||
54 | ||
55 |
class LoginPasswordAuthenticator(BaseAuthenticator): |
|
56 |
id = 'password' |
|
28 | 57 |
submit_name = 'login-password-submit' |
29 | 58 | |
30 | 59 |
def enabled(self): |
... | ... | |
33 | 62 |
def name(self): |
34 | 63 |
return ugettext_lazy('Password') |
35 | 64 | |
36 |
def id(self): |
|
37 |
return 'password' |
|
38 | ||
39 | 65 |
def get_service_ous(self, request): |
40 | 66 |
service_slug = request.GET.get(constants.SERVICE_FIELD_NAME) |
41 | 67 |
if not service_slug: |
src/authentic2/utils/__init__.py | ||
---|---|---|
149 | 149 |
return list |
150 | 150 | |
151 | 151 | |
152 |
def load_backend(path): |
|
152 |
def load_backend(path, kwargs):
|
|
153 | 153 |
'''Load an IdP backend by its module path''' |
154 | 154 |
i = path.rfind('.') |
155 | 155 |
module, attr = path[:i], path[i + 1:] |
... | ... | |
165 | 165 |
except AttributeError: |
166 | 166 |
raise ImproperlyConfigured('Module "%s" does not define a "%s" idp backend' |
167 | 167 |
% (module, attr)) |
168 |
return cls() |
|
168 |
backend_kwargs = {} |
|
169 |
if hasattr(cls, 'id'): |
|
170 |
backend_kwargs.update(kwargs.get(cls.id, {})) |
|
171 |
return cls(**backend_kwargs) |
|
169 | 172 | |
170 | 173 | |
171 | 174 |
def get_backends(setting_name='IDP_BACKENDS'): |
... | ... | |
175 | 178 |
kwargs = {} |
176 | 179 |
if not isinstance(backend_path, six.string_types): |
177 | 180 |
backend_path, kwargs = backend_path |
178 |
backend = load_backend(backend_path) |
|
181 |
kwargs_settings = getattr(app_settings, setting_name + '_KWARGS', {}) |
|
182 |
backend = load_backend(backend_path, kwargs_settings) |
|
179 | 183 |
# If no enabled method is defined on the backend, backend enabled by default. |
180 | 184 |
if hasattr(backend, 'enabled') and not backend.enabled(): |
181 | 185 |
continue |
182 |
kwargs_settings = getattr(app_settings, setting_name + '_KWARGS', {}) |
|
183 | 186 |
if backend_path in kwargs_settings: |
184 | 187 |
kwargs.update(kwargs_settings[backend_path]) |
185 | 188 |
# Clean id and name for legacy support |
src/authentic2/views.py | ||
---|---|---|
324 | 324 |
auth_blocks = [] |
325 | 325 |
parameters = {'request': request, |
326 | 326 |
'context': context} |
327 |
remote_addr = request.META.get('REMOTE_ADDR') |
|
327 | 328 |
# check if the authenticator has multiple instances |
328 | 329 |
if hasattr(authenticator, 'instances'): |
329 | 330 |
for instance_id, instance in authenticator.instances(**parameters): |
330 | 331 |
parameters['instance'] = instance |
331 | 332 |
parameters['instance_id'] = instance_id |
333 |
if not authenticator.shown(remote_addr, instance_id): |
|
334 |
continue |
|
332 | 335 |
block = utils.get_authenticator_method(authenticator, 'login', parameters) |
333 | 336 |
# update block id in order to separate instances |
334 | 337 |
block['id'] = '%s_%s' % (block['id'], instance_id) |
335 | 338 |
auth_blocks.append(block) |
336 | 339 |
else: |
337 |
auth_blocks.append(utils.get_authenticator_method(authenticator, 'login', parameters)) |
|
340 |
if authenticator.shown(remote_addr): |
|
341 |
auth_blocks.append(utils.get_authenticator_method(authenticator, 'login', parameters)) |
|
338 | 342 |
# If a login frontend method returns an HttpResponse with a status code != 200 |
339 | 343 |
# this response is returned. |
340 | 344 |
for block in auth_blocks: |
... | ... | |
344 | 348 |
block['is_hidden'] = False |
345 | 349 |
blocks.append(block) |
346 | 350 |
# TODO: remove attribute below after cleaning up the templates |
347 |
blocks[-1]['is_hidden'] = False |
|
351 |
if blocks: |
|
352 |
blocks[-1]['is_hidden'] = False |
|
348 | 353 | |
349 | 354 |
# Old frontends API |
350 | 355 |
for block in blocks: |
src/authentic2_auth_fc/authenticators.py | ||
---|---|---|
19 | 19 |
from django.template.response import TemplateResponse |
20 | 20 | |
21 | 21 |
from authentic2 import app_settings as a2_app_settings, utils as a2_utils |
22 |
from authentic2.authenticators import BaseAuthenticator |
|
22 | 23 | |
23 | 24 |
from . import app_settings |
24 | 25 | |
25 | 26 | |
26 |
class FcAuthenticator(object): |
|
27 |
class FcAuthenticator(BaseAuthenticator): |
|
28 |
id = 'fc' |
|
29 | ||
27 | 30 |
def enabled(self): |
28 | 31 |
return app_settings.enable |
29 | 32 | |
30 | 33 |
def name(self): |
31 | 34 |
return gettext_noop('FranceConnect') |
32 | 35 | |
33 |
def id(self): |
|
34 |
return 'fc' |
|
35 | ||
36 | 36 |
@property |
37 | 37 |
def popup(self): |
38 | 38 |
return app_settings.popup |
src/authentic2_auth_oidc/authenticators.py | ||
---|---|---|
19 | 19 | |
20 | 20 |
from . import app_settings, utils |
21 | 21 |
from authentic2.utils import make_url |
22 |
from authentic2.authenticators import BaseAuthenticator |
|
23 | ||
24 | ||
25 |
class OIDCAuthenticator(BaseAuthenticator): |
|
26 |
id = 'oidc' |
|
22 | 27 | |
23 | 28 | |
24 |
class OIDCAuthenticator(object): |
|
25 | 29 |
def enabled(self): |
26 | 30 |
return app_settings.ENABLE and utils.has_providers() |
27 | 31 | |
28 | 32 |
def name(self): |
29 | 33 |
return gettext_noop('OpenIDConnect') |
30 | 34 | |
31 |
def id(self): |
|
32 |
return 'oidc' |
|
33 | ||
34 | 35 |
def instances(self, request, *args, **kwargs): |
35 | 36 |
for p in utils.get_providers(shown=True): |
36 | 37 |
yield (p.slug, p) |
src/authentic2_auth_saml/authenticators.py | ||
---|---|---|
20 | 20 |
from mellon.utils import get_idp, get_idps |
21 | 21 | |
22 | 22 |
from authentic2.utils import redirect_to_login |
23 |
from authentic2.authenticators import BaseAuthenticator |
|
23 | 24 | |
24 | 25 |
from . import app_settings |
25 | 26 | |
26 | 27 | |
27 |
class SAMLAuthenticator(object):
|
|
28 |
class SAMLAuthenticator(BaseAuthenticator):
|
|
28 | 29 |
id = 'saml' |
29 | 30 | |
30 | 31 |
def enabled(self): |
... | ... | |
35 | 36 | |
36 | 37 |
def instances(self, request, *args, **kwargs): |
37 | 38 |
for idx, idp in enumerate(get_idps()): |
38 |
yield(idp.get('SLUG') or idx, idp)
|
|
39 |
yield(idp.get('SLUG') or str(idx), idp)
|
|
39 | 40 | |
40 | 41 |
def login(self, request, *args, **kwargs): |
41 | 42 |
context = kwargs.pop('context', {}) |
tests/auth_fc/test_auth_fc.py | ||
---|---|---|
85 | 85 |
return parsed['state'] |
86 | 86 | |
87 | 87 | |
88 |
def test_login_with_condition(app, fc_settings, settings): |
|
89 |
# open the page first time so session cookie can be set |
|
90 |
response = app.get('/login/') |
|
91 |
assert 'fc-button' in response |
|
92 | ||
93 |
settings.AUTH_FRONTENDS_KWARGS = {'fc': {'show_condition': 'remote_addr==\'0.0.0.0\''}} |
|
94 |
response = app.get('/login/') |
|
95 |
assert 'fc-button' not in response |
|
96 | ||
97 | ||
88 | 98 |
@pytest.mark.parametrize('exp', [timestamp_from_datetime(now() + datetime.timedelta(seconds=1000)), |
89 | 99 |
timestamp_from_datetime(now() - datetime.timedelta(seconds=1000))]) |
90 | 100 |
def test_login_simple(app, fc_settings, caplog, hooks, exp): |
tests/test_auth_oidc.py | ||
---|---|---|
154 | 154 |
provider = OIDCProvider.objects.create( |
155 | 155 |
ou=get_default_ou(), |
156 | 156 |
name='OIDIDP', |
157 |
slug='oididp', |
|
157 | 158 |
issuer='http://server.example.com', |
158 | 159 |
authorization_endpoint='https://server.example.com/authorize', |
159 | 160 |
token_endpoint='https://server.example.com/token', |
... | ... | |
348 | 349 |
assert response.pyquery('p#oidc-p-oidcidp-2') |
349 | 350 | |
350 | 351 | |
352 |
def test_login_with_conditionnal_authenticators(oidc_provider, app, settings, caplog): |
|
353 | ||
354 |
oidc2_provider = OIDCProvider.objects.create( |
|
355 |
id=2, |
|
356 |
ou=get_default_ou(), |
|
357 |
name='My IDP', |
|
358 |
slug='myidp', |
|
359 |
issuer='https://idp2.example.com/', |
|
360 |
authorization_endpoint='https://idp2.example.com/authorize', |
|
361 |
token_endpoint='https://idp2.example.com/token', |
|
362 |
end_session_endpoint='https://idp2.example.com/logout', |
|
363 |
userinfo_endpoint='https://idp*é.example.com/user_info', |
|
364 |
token_revocation_endpoint='https://idp2.example.com/revoke', |
|
365 |
max_auth_age=10, |
|
366 |
strategy=OIDCProvider.STRATEGY_CREATE, |
|
367 |
jwkset_json=None, |
|
368 |
idtoken_algo=OIDCProvider.ALGO_RSA, |
|
369 |
claims_parameter_supported=False |
|
370 |
) |
|
371 |
response = app.get('/login/') |
|
372 |
assert 'My IDP' in response |
|
373 |
assert 'OIDIDP' in response |
|
374 | ||
375 |
settings.AUTH_FRONTENDS_KWARGS = { |
|
376 |
'oidc': { |
|
377 |
'show_condition': { |
|
378 |
'myidp': 'remote_addr==\'0.0.0.0\'' |
|
379 |
} |
|
380 |
} |
|
381 |
} |
|
382 |
response = app.get('/login/') |
|
383 |
assert 'OIDIDP' in response |
|
384 |
assert 'My IDP' not in response |
|
385 | ||
386 |
settings.AUTH_FRONTENDS_KWARGS = { |
|
387 |
'oidc': { |
|
388 |
'show_condition': { |
|
389 |
'myid': 'remote_addr==\'0.0.0.0\'', |
|
390 |
'oididp': 'remote_addr==\'127.0.0.1\'' |
|
391 |
} |
|
392 |
} |
|
393 |
} |
|
394 |
response = app.get('/login/') |
|
395 |
assert 'OIDIDP' in response |
|
396 |
assert 'My IDP' in response |
|
397 | ||
398 |
settings.AUTH_FRONTENDS_KWARGS = { |
|
399 |
'oidc': { |
|
400 |
'show_condition': { |
|
401 |
'myidp': 'remote_addr==\'0.0.0.0\'', |
|
402 |
'oididp': 'remote_addr==\'127.0.0.1\'' |
|
403 |
} |
|
404 |
} |
|
405 |
} |
|
406 |
response = app.get('/login/') |
|
407 |
assert 'OIDIDP' in response |
|
408 |
assert 'My IDP' not in response |
|
409 | ||
410 |
settings.AUTH_FRONTENDS_KWARGS = { |
|
411 |
'oidc': { |
|
412 |
'show_condition': 'remote_addr==\'127.0.0.1\'' |
|
413 |
} |
|
414 |
} |
|
415 |
response = app.get('/login/') |
|
416 |
assert 'OIDIDP' in response |
|
417 |
assert 'My IDP' in response |
|
418 | ||
419 | ||
351 | 420 |
def test_sso(app, caplog, code, oidc_provider, oidc_provider_jwkset, hooks): |
352 | 421 |
OU = get_ou_model() |
353 | 422 |
cassis = OU.objects.create(name='Cassis', slug='cassis') |
tests/test_auth_saml.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import os |
|
17 | 18 |
import logging |
18 | 19 | |
19 | 20 |
import pytest |
... | ... | |
23 | 24 |
from django.contrib.auth import get_user_model |
24 | 25 |
from authentic2.models import Attribute |
25 | 26 | |
26 | ||
27 | 27 |
def test_providers_on_login_page(db, app, settings): |
28 | 28 |
settings.A2_AUTH_SAML_ENABLE = True |
29 | 29 |
PROVIDERS = [ |
... | ... | |
133 | 133 |
# on missing mandatory attribute, no user is created |
134 | 134 |
del saml_attributes['mail'] |
135 | 135 |
assert adapter.lookup_user(idp, saml_attributes) is None |
136 | ||
137 | ||
138 |
def test_login_with_conditionnal_authenticators(db, app, settings, caplog): |
|
139 | ||
140 |
settings.A2_AUTH_SAML_ENABLE = True |
|
141 |
settings.MELLON_IDENTITY_PROVIDERS = [ |
|
142 |
{"METADATA": os.path.join(os.path.dirname(__file__), 'metadata.xml')} |
|
143 |
] |
|
144 |
response = app.get('/login/') |
|
145 |
assert 'login-saml-0' in response |
|
146 | ||
147 |
settings.AUTH_FRONTENDS_KWARGS = { |
|
148 |
'saml': { |
|
149 |
'show_condition': 'remote_addr==\'0.0.0.0\'' |
|
150 |
} |
|
151 |
} |
|
152 |
response = app.get('/login/') |
|
153 |
assert 'login-saml-0' not in response |
|
154 | ||
155 |
settings.MELLON_IDENTITY_PROVIDERS.append( |
|
156 |
{"METADATA": os.path.join(os.path.dirname(__file__), 'metadata.xml')} |
|
157 |
) |
|
158 |
settings.AUTH_FRONTENDS_KWARGS = { |
|
159 |
'saml': { |
|
160 |
'show_condition': { |
|
161 |
'0': 'remote_addr==\'0.0.0.0\'' |
|
162 |
} |
|
163 |
} |
|
164 |
} |
|
165 |
response = app.get('/login/') |
|
166 |
assert 'login-saml-0' not in response |
|
167 |
assert 'login-saml-1' in response |
|
168 | ||
169 |
settings.AUTH_FRONTENDS_KWARGS = { |
|
170 |
'saml': { |
|
171 |
'show_condition': { |
|
172 |
'0': 'remote_addr==\'0.0.0.0\'', |
|
173 |
'1': 'remote_addr==\'0.0.0.0\'' |
|
174 |
} |
|
175 |
} |
|
176 |
} |
|
177 |
response = app.get('/login/') |
|
178 |
assert 'login-saml-0' not in response |
|
179 |
assert 'login-saml-1' not in response |
tests/test_login.py | ||
---|---|---|
21 | 21 | |
22 | 22 |
from authentic2 import models |
23 | 23 | |
24 |
from utils import login |
|
24 |
from utils import login, check_log
|
|
25 | 25 | |
26 | 26 | |
27 | 27 |
def test_login_inactive_user(db, app): |
... | ... | |
50 | 50 |
assert '_auth_user_id' not in app.session |
51 | 51 | |
52 | 52 | |
53 |
def test_login_with_conditionnal_enabled_authenticators(db, app, settings, caplog): |
|
54 |
response = app.get('/login/') |
|
55 |
assert 'name="login-password-submit"' in response |
|
56 | ||
57 |
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'False'}} |
|
58 |
response = app.get('/login/') |
|
59 |
# login form must not be displayed |
|
60 |
assert 'name="login-password-submit"' not in response |
|
61 |
assert len(caplog.records) == 0 |
|
62 |
# set a condition with error |
|
63 |
with check_log(caplog, 'name \'login_hint\' is not defined'): |
|
64 |
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': '\'admin\' in login_hint'}} |
|
65 |
response = app.get('/login/') |
|
66 |
assert 'name="login-password-submit"' not in response |
|
67 | ||
68 | ||
53 | 69 |
def test_registration_url_on_login_page(db, app): |
54 | 70 |
response = app.get('/login/?next=/whatever') |
55 | 71 |
assert 'register/?next=/whatever"' in response |
56 |
- |