Projet

Général

Profil

0001-misc-allow-authenticators-display-conditions-28215.patch

Serghei Mihai, 10 mars 2020 16:45

Télécharger (16,2 ko)

Voir les différences:

Subject: [PATCH] misc: allow authenticators display conditions (#28215)

 src/authentic2/authenticators.py           | 38 ++++++++++--
 src/authentic2/utils/__init__.py           | 11 ++--
 src/authentic2/views.py                    |  6 +-
 src/authentic2_auth_fc/authenticators.py   | 12 ++--
 src/authentic2_auth_oidc/authenticators.py |  9 +--
 src/authentic2_auth_saml/authenticators.py |  5 +-
 tests/auth_fc/test_auth_fc.py              | 10 ++++
 tests/test_auth_oidc.py                    | 69 ++++++++++++++++++++++
 tests/test_auth_saml.py                    | 46 +++++++++++++++
 tests/test_login.py                        | 20 ++++++-
 10 files changed, 204 insertions(+), 22 deletions(-)
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):
31
        return app_settings.A2_AUTH_PASSWORD_ENABLE
60
        if not app_settings.A2_AUTH_PASSWORD_ENABLE:
61
            return False
62
        return self.shown(self.show_condition)
32 63

  
33 64
    def name(self):
34 65
        return ugettext_lazy('Password')
35 66

  
36
    def id(self):
37
        return 'password'
38

  
39 67
    def get_service_ous(self, request):
40 68
        service_slug = request.GET.get(constants.SERVICE_FIELD_NAME)
41 69
        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:
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
        return app_settings.enable
31
        if not app_settings.enable:
32
            return False
33
        return self.shown(self.show_condition)
29 34

  
30 35
    def name(self):
31 36
        return gettext_noop('FranceConnect')
32 37

  
33
    def id(self):
34
        return 'fc'
35

  
36 38
    @property
37 39
    def popup(self):
38 40
        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

  
27
pytestmark = pytest.mark.django_db
26 28

  
27 29
def test_providers_on_login_page(db, app, settings):
28 30
    settings.A2_AUTH_SAML_ENABLE = True
......
133 135
    # on missing mandatory attribute, no user is created
134 136
    del saml_attributes['mail']
135 137
    assert adapter.lookup_user(idp, saml_attributes) is None
138

  
139

  
140
def test_login_with_conditionnal_authenticators(app, settings, caplog):
141

  
142
    settings.A2_AUTH_SAML_ENABLE = True
143
    settings.MELLON_IDENTITY_PROVIDERS = [
144
        {"METADATA": os.path.join(os.path.dirname(__file__), 'metadata.xml')}
145
    ]
146
    response = app.get('/login/')
147
    assert 'login-saml-0' in response
148

  
149
    settings.AUTH_FRONTENDS_KWARGS = {
150
        'saml': {
151
            'show_condition': 'remote_addr==\'0.0.0.0\''
152
        }
153
    }
154
    response = app.get('/login/')
155
    assert 'login-saml-0' not in response
156

  
157
    settings.MELLON_IDENTITY_PROVIDERS.append(
158
        {"METADATA": os.path.join(os.path.dirname(__file__), 'metadata.xml')}
159
    )
160
    settings.AUTH_FRONTENDS_KWARGS = {
161
        'saml': {
162
            'show_condition': {
163
                '0': 'remote_addr==\'0.0.0.0\''
164
            }
165
        }
166
    }
167
    response = app.get('/login/')
168
    assert 'login-saml-0' not in response
169
    assert 'login-saml-1' in response
170

  
171
    settings.AUTH_FRONTENDS_KWARGS = {
172
        'saml': {
173
            'show_condition': {
174
                '0': 'remote_addr==\'0.0.0.0\'',
175
                '1': 'remote_addr==\'0.0.0.0\''
176
            }
177
        }
178
    }
179
    response = app.get('/login/')
180
    assert 'login-saml-0' not in response
181
    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
    # open the page first time so session cookie can be set
55
    response = app.get('/login/')
56
    response = app.get('/login/')
57
    assert 'name="login-password-submit"' in response
58

  
59
    settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'False'}}
60
    response = app.get('/login/')
61
    # login form must not be displayed
62
    assert 'name="login-password-submit"' not in response
63
    assert len(caplog.records) == 0
64
    # set a condition with error
65
    with check_log(caplog, 'name \'login_hint\' is not defined'):
66
        settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': '\'admin\' in login_hint'}}
67
        response = app.get('/login/')
68
        assert 'name="login-password-submit"' not in response
69

  
70

  
53 71
def test_registration_url_on_login_page(db, app):
54 72
    response = app.get('/login/?next=/whatever')
55 73
    assert 'register/?next=/whatever"' in response
56
-