Projet

Général

Profil

0001-misc-add-support-for-enable-conditions-on-authentica.patch

Serghei Mihai (congés, retour 15/05), 03 décembre 2019 18:16

Télécharger (12,4 ko)

Voir les différences:

Subject: [PATCH] misc: add support for enable conditions on authentication
 frontends (#28215)

 src/authentic2/app_settings.py             |  2 ++
 src/authentic2/authenticators.py           | 25 +++++++++++++++---
 src/authentic2/utils/__init__.py           |  4 +--
 src/authentic2/views.py                    |  2 +-
 src/authentic2_auth_fc/app_settings.py     |  4 +++
 src/authentic2_auth_fc/authenticators.py   |  7 +++--
 src/authentic2_auth_oidc/app_settings.py   |  4 +++
 src/authentic2_auth_oidc/authenticators.py | 11 +++++---
 src/authentic2_auth_saml/app_settings.py   |  4 +++
 src/authentic2_auth_saml/authenticators.py | 11 +++++---
 tests/test_auth_oidc.py                    | 30 ++++++++++++++++++++++
 tests/test_login.py                        | 19 +++++++++++++-
 12 files changed, 108 insertions(+), 15 deletions(-)
src/authentic2/app_settings.py
239 239
    A2_AUTH_PASSWORD_ENABLE=Setting(
240 240
        default=True,
241 241
        definition='Activate login/password authentication', names=('AUTH_PASSWORD',)),
242
    A2_AUTH_PASSWORD_ENABLE_CONDITION=Setting(default=None, definition='Filters',
243
                                              names=('FRONTEND ENABLE CONDITION',)),
242 244
    A2_LOGIN_FAILURE_COUNT_BEFORE_WARNING=Setting(
243 245
        default=0,
244 246
        definition='Failure count before logging a warning to '
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.shortcuts import render
18 20
from django.utils.translation import ugettext as _, ugettext_lazy
19 21

  
20 22
from authentic2.a2_rbac.models import OrganizationalUnit as OU, Role
21 23
from authentic2.custom_user.models import User
24

  
22 25
from . import views, app_settings, utils, constants, models
23 26
from .forms import authentication as authentication_forms
24 27

  
25 28

  
26
class LoginPasswordAuthenticator(object):
29
class BaseAuthenticator(object):
30
    def eval_enable_condition(self, condition, request, **kwargs):
31
        if condition:
32
            try:
33
                context = {'request': request}
34
                if kwargs:
35
                    context.update(kwargs)
36
                return eval(condition, context)
37
            except Exception, e:
38
                logging.getLogger(__name__).warning('filter expression error: %r' % e)
39
                return False
40
        return True
41

  
42

  
43
class LoginPasswordAuthenticator(BaseAuthenticator):
27 44
    submit_name = 'login-password-submit'
28 45

  
29
    def enabled(self):
30
        return app_settings.A2_AUTH_PASSWORD_ENABLE
46
    def enabled(self, request=None):
47
        if app_settings.A2_AUTH_PASSWORD_ENABLE:
48
            return self.eval_enable_condition(app_settings.A2_AUTH_PASSWORD_ENABLE_CONDITION, request)
49
        return False
31 50

  
32 51
    def name(self):
33 52
        return ugettext_lazy('Password')
src/authentic2/utils/__init__.py
169 169
    return cls()
170 170

  
171 171

  
172
def get_backends(setting_name='IDP_BACKENDS'):
172
def get_backends(setting_name='IDP_BACKENDS', request=None):
173 173
    '''Return the list of enabled cleaned backends.'''
174 174
    backends = []
175 175
    for backend_path in getattr(app_settings, setting_name):
......
178 178
            backend_path, kwargs = backend_path
179 179
        backend = load_backend(backend_path)
180 180
        # If no enabled method is defined on the backend, backend enabled by default.
181
        if hasattr(backend, 'enabled') and not backend.enabled():
181
        if hasattr(backend, 'enabled') and not backend.enabled(request):
182 182
            continue
183 183
        kwargs_settings = getattr(app_settings, setting_name + '_KWARGS', {})
184 184
        if backend_path in kwargs_settings:
src/authentic2/views.py
277 277
            redirect_to = settings.LOGIN_REDIRECT_URL
278 278
    nonce = request.GET.get(constants.NONCE_FIELD_NAME)
279 279

  
280
    authenticators = utils.get_backends('AUTH_FRONTENDS')
280
    authenticators = utils.get_backends('AUTH_FRONTENDS', request)
281 281

  
282 282
    blocks = []
283 283

  
src/authentic2_auth_fc/app_settings.py
134 134
    def popup(self):
135 135
        return self._setting('POPUP', False)
136 136

  
137
    @property
138
    def enable_condition(self):
139
        return self._setting('ENABLE_CONDITION', None)
140

  
137 141
app_settings = AppSettings('A2_FC_')
138 142
app_settings.__name__ = __name__
139 143
sys.modules[__name__] = app_settings
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
    def enabled(self):
27
class FcAuthenticator(BaseAuthenticator):
28
    def enabled(self, request):
29
        if app_settings.enable:
30
            return self.eval_enable_condition(app_settings.enable_condition, request)
28 31
        return app_settings.enable
29 32

  
30 33
    def name(self):
src/authentic2_auth_oidc/app_settings.py
37 37
    def ENABLE(self):
38 38
        return self._setting('ENABLE', True)
39 39

  
40
    @property
41
    def IDP_ENABLE_CONDITION(self):
42
        return self._setting('IDP_ENABLE_CONDITION', None)
43

  
40 44
app_settings = AppSettings('A2_AUTH_OIDC_')
41 45
app_settings.__name__ = __name__
42 46
sys.modules[__name__] = app_settings
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
22 23

  
23 24

  
24
class OIDCAuthenticator(object):
25
    def enabled(self):
25
class OIDCAuthenticator(BaseAuthenticator):
26
    def enabled(self, request=None):
26 27
        return app_settings.ENABLE and utils.has_providers()
27 28

  
28 29
    def name(self):
......
32 33
        return 'oidc'
33 34

  
34 35
    def instances(self, request, *args, **kwargs):
35
        for p in utils.get_providers(shown=True):
36
        providers = utils.get_providers(shown=True)
37
        if app_settings.IDP_ENABLE_CONDITION:
38
            providers = self.eval_enable_condition(app_settings.IDP_ENABLE_CONDITION, request,
39
                                                   providers=providers)
40
        for p in providers:
36 41
            yield (p.slug, p)
37 42

  
38 43
    def login(self, request, *args, **kwargs):
src/authentic2_auth_saml/app_settings.py
37 37
    def enable(self):
38 38
        return self._setting('ENABLE', False)
39 39

  
40
    @property
41
    def enable_condition(self):
42
        return self._setting('ENABLE_CONDITION', False)
43

  
40 44
app_settings = AppSettings('A2_AUTH_SAML_')
41 45
app_settings.__name__ = __name__
42 46
sys.modules[__name__] = app_settings
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
24

  
23 25

  
24 26
from . import app_settings
25 27

  
26 28

  
27
class SAMLAuthenticator(object):
29
class SAMLAuthenticator(BaseAuthenticator):
28 30
    id = 'saml'
29 31

  
30
    def enabled(self):
31
        return app_settings.enable and list(get_idps())
32
    def enabled(self, request=None):
33
        idps = list(get_idps())
34
        if app_settings.enable and idps:
35
            return self.eval_enable_condition(app_settings.enable_condition, request, idps=idps)
36
        return False
32 37

  
33 38
    def name(self):
34 39
        return gettext_noop('SAML')
tests/test_auth_oidc.py
331 331
    assert response.pyquery('p#oidc-p-oidcidp-2')
332 332

  
333 333

  
334
def test_login_with_conditionnal_authenticators(oidc_provider, app, settings, caplog):
335

  
336
    OIDCProvider.objects.create(
337
        id=2,
338
        ou=get_default_ou(),
339
        name='My IDP',
340
        issuer='https://idp2.example.com/',
341
        authorization_endpoint='https://idp2.example.com/authorize',
342
        token_endpoint='https://idp2.example.com/token',
343
        end_session_endpoint='https://idp2.example.com/logout',
344
        userinfo_endpoint='https://idp*é.example.com/user_info',
345
        token_revocation_endpoint='https://idp2.example.com/revoke',
346
        max_auth_age=10,
347
        strategy=OIDCProvider.STRATEGY_CREATE,
348
        jwkset_json=None,
349
        idtoken_algo=OIDCProvider.ALGO_RSA,
350
        claims_parameter_supported=False
351
    )
352
    settings.A2_AUTH_OIDC_IDP_ENABLE_CONDITION = "providers.filter(name='My IDP')"
353
    response = app.get('/login/')
354
    assert 'My IDP' in response
355
    assert 'OIDIDP' not in response
356
    assert len(caplog.records) == 0
357

  
358
    settings.A2_AUTH_OIDC_IDP_ENABLE_CONDITION = "providers.filter(name='OIDIDP')"
359
    response = app.get('/login/')
360
    assert 'OIDIDP' in response
361
    assert 'My IDP' not in response
362

  
363

  
334 364

  
335 365

  
336 366
def test_sso(app, caplog, code, oidc_provider, oidc_provider_jwkset, login_url, login_callback_url, hooks):
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
    settings.A2_AUTH_PASSWORD_ENABLE_CONDITION = "request.META['REMOTE_ADDR'] in ('127.0.0.1',)"
55
    response = app.get('/login/')
56
    assert 'name="login-password-submit"' in response
57
    # set invalid display condition
58
    settings.A2_AUTH_PASSWORD_ENABLE_CONDITION = "request.META['REMOTE_ADDR'] in ('0.0.0.0',)"
59
    response = app.get('/login/')
60
    # login form must not be displayed
61
    assert 'name="login-password-submit"' not in response
62
    assert len(caplog.records) == 0
63
    # set a condition with error
64
    with check_log(caplog, 'filter expression error: SyntaxError(\'unexpected EOF while parsing\''):
65
        settings.A2_AUTH_PASSWORD_ENABLE_CONDITION = "request.META['REMOTE_ADDR'] in "
66
        response = app.get('/login/')
67
        assert 'name="login-password-submit"' not in response
68

  
69

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