Projet

Général

Profil

0004-auth_oidc-migrate-authenticator-to-database-53902.patch

Valentin Deniaud, 19 avril 2022 18:14

Télécharger (25,2 ko)

Voir les différences:

Subject: [PATCH 4/4] auth_oidc: migrate authenticator to database (#53902)

 src/authentic2/settings.py                    |  1 -
 src/authentic2/views.py                       |  2 +-
 src/authentic2_auth_oidc/authenticators.py    | 64 ---------------
 src/authentic2_auth_oidc/forms.py             | 32 ++++++++
 .../migrations/0001_initial.py                |  3 +
 ...0009_oidcprovider_baseauthenticator_ptr.py | 19 +++++
 .../migrations/0010_auto_20220413_1622.py     | 43 ++++++++++
 .../migrations/0011_auto_20220413_1632.py     | 46 +++++++++++
 src/authentic2_auth_oidc/models.py            | 52 +++++++++----
 src/authentic2_auth_oidc/utils.py             | 10 ---
 tests/conftest.py                             |  3 +-
 tests/test_auth_oidc.py                       | 52 ++++---------
 tests/test_commands.py                        |  2 +
 tests/test_manager_authenticators.py          | 78 +++++++++++++++++++
 14 files changed, 279 insertions(+), 128 deletions(-)
 delete mode 100644 src/authentic2_auth_oidc/authenticators.py
 create mode 100644 src/authentic2_auth_oidc/forms.py
 create mode 100644 src/authentic2_auth_oidc/migrations/0009_oidcprovider_baseauthenticator_ptr.py
 create mode 100644 src/authentic2_auth_oidc/migrations/0010_auto_20220413_1622.py
 create mode 100644 src/authentic2_auth_oidc/migrations/0011_auto_20220413_1632.py
src/authentic2/settings.py
190 190
AUTH_USER_MODEL = 'custom_user.User'
191 191
AUTH_FRONTENDS = (
192 192
    'authentic2_auth_saml.authenticators.SAMLAuthenticator',
193
    'authentic2_auth_oidc.authenticators.OIDCAuthenticator',
194 193
    'authentic2_auth_fc.authenticators.FcAuthenticator',
195 194
)
196 195

  
src/authentic2/views.py
443 443
        if hasattr(authenticator, 'autorun'):
444 444
            if 'message' in token:
445 445
                messages.info(request, token['message'])
446
            return authenticator.autorun(request, block['id'])
446
            return authenticator.autorun(request, block.get('id'))
447 447

  
448 448
    # Old frontends API
449 449
    for block in blocks:
src/authentic2_auth_oidc/authenticators.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.shortcuts import render
18
from django.utils.translation import gettext_noop
19

  
20
from authentic2.authenticators import BaseAuthenticator
21
from authentic2.utils.misc import make_url, redirect_to_login
22

  
23
from . import app_settings, utils
24
from .models import OIDCProvider
25

  
26

  
27
class OIDCAuthenticator(BaseAuthenticator):
28
    id = 'oidc'
29
    how = ['oidc']
30
    priority = 2
31

  
32
    def enabled(self):
33
        return app_settings.ENABLE and utils.has_providers()
34

  
35
    def name(self):
36
        return gettext_noop('OpenIDConnect')
37

  
38
    def instances(self, request, *args, **kwargs):
39
        for p in utils.get_providers(shown=True):
40
            yield (p.slug, p)
41

  
42
    def autorun(self, request, block_id):
43
        auth_id, instance_slug = block_id.split('_')
44
        assert auth_id == self.id
45

  
46
        try:
47
            provider = OIDCProvider.objects.get(slug=instance_slug)
48
        except OIDCProvider.DoesNotExist():
49
            return redirect_to_login(request)
50
        return redirect_to_login(request, login_url='oidc-login', kwargs={'pk': provider.pk})
51

  
52
    def login(self, request, *args, **kwargs):
53
        context = kwargs.get('context', {})
54
        if kwargs.get('instance'):
55
            instance = kwargs['instance']
56
            context['provider'] = instance
57
            context['login_url'] = make_url(
58
                'oidc-login', kwargs={'pk': instance.id}, request=request, keep_params=True
59
            )
60
            template_names = [
61
                'authentic2_auth_oidc/login_%s.html' % instance.slug,
62
                'authentic2_auth_oidc/login.html',
63
            ]
64
            return render(request, template_names, context)
src/authentic2_auth_oidc/forms.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2022 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django import forms
18

  
19
from authentic2.apps.authenticators.forms import AuthenticatorFormMixin
20

  
21
from .models import OIDCProvider
22

  
23

  
24
class OIDCProviderEditForm(AuthenticatorFormMixin, forms.ModelForm):
25
    class Meta:
26
        model = OIDCProvider
27
        fields = '__all__'
28

  
29
    def __init__(self, *args, **kwargs):
30
        super().__init__(*args, **kwargs)
31
        self.fields['ou'].required = True
32
        self.fields['ou'].empty_label = None
src/authentic2_auth_oidc/migrations/0001_initial.py
123 123
                    ),
124 124
                ),
125 125
            ],
126
            options={
127
                'verbose_name': 'OpenIDConnect',
128
            },
126 129
        ),
127 130
        migrations.AddField(
128 131
            model_name='oidcclaimmapping',
src/authentic2_auth_oidc/migrations/0009_oidcprovider_baseauthenticator_ptr.py
1
# Generated by Django 2.2.28 on 2022-04-13 14:22
2

  
3
from django.db import migrations, models
4

  
5

  
6
class Migration(migrations.Migration):
7

  
8
    dependencies = [
9
        ('authentic2_auth_oidc', '0008_auto_20201102_1142'),
10
    ]
11

  
12
    operations = [
13
        migrations.AddField(
14
            model_name='oidcprovider',
15
            name='baseauthenticator_ptr',
16
            field=models.IntegerField(default=0),
17
            preserve_default=False,
18
        ),
19
    ]
src/authentic2_auth_oidc/migrations/0010_auto_20220413_1622.py
1
# Generated by Django 2.2.28 on 2022-04-13 14:22
2

  
3
from django.db import migrations
4
from django.utils.text import slugify
5

  
6
from authentic2 import app_settings as global_settings
7
from authentic2_auth_oidc import app_settings
8

  
9

  
10
def add_base_authenticators(apps, schema_editor):
11
    kwargs_settings = getattr(global_settings, 'AUTH_FRONTENDS_KWARGS', {})
12
    oidc_provider_settings = kwargs_settings.get('oidc', {})
13
    show_condition = oidc_provider_settings.get('show_condition')
14

  
15
    BaseAuthenticator = apps.get_model('authenticators', 'BaseAuthenticator')
16
    OIDCProvider = apps.get_model('authentic2_auth_oidc', 'OIDCProvider')
17

  
18
    for provider in OIDCProvider.objects.all():
19
        if isinstance(show_condition, dict):
20
            show_condition = show_condition.get(provider.slug, '')
21

  
22
        base_authenticator = BaseAuthenticator.objects.create(
23
            name=provider.name,
24
            slug=provider.slug or slugify(provider.name),
25
            ou=provider.ou,
26
            enabled=provider.show and app_settings.ENABLE,
27
            order=oidc_provider_settings.get('priority', 2),
28
            show_condition=oidc_provider_settings.get('show_condition', ''),
29
        )
30
        provider.baseauthenticator_ptr = base_authenticator.pk
31
        provider.save()
32

  
33

  
34
class Migration(migrations.Migration):
35

  
36
    dependencies = [
37
        ('authentic2_auth_oidc', '0009_oidcprovider_baseauthenticator_ptr'),
38
        ('authenticators', '0001_initial'),
39
    ]
40

  
41
    operations = [
42
        migrations.RunPython(add_base_authenticators, reverse_code=migrations.RunPython.noop),
43
    ]
src/authentic2_auth_oidc/migrations/0011_auto_20220413_1632.py
1
# Generated by Django 2.2.28 on 2022-04-13 14:32
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
        ('authentic2_auth_oidc', '0010_auto_20220413_1622'),
11
    ]
12

  
13
    operations = [
14
        migrations.RemoveField(
15
            model_name='oidcprovider',
16
            name='id',
17
        ),
18
        migrations.RemoveField(
19
            model_name='oidcprovider',
20
            name='name',
21
        ),
22
        migrations.RemoveField(
23
            model_name='oidcprovider',
24
            name='ou',
25
        ),
26
        migrations.RemoveField(
27
            model_name='oidcprovider',
28
            name='show',
29
        ),
30
        migrations.RemoveField(
31
            model_name='oidcprovider',
32
            name='slug',
33
        ),
34
        migrations.AlterField(
35
            model_name='oidcprovider',
36
            name='baseauthenticator_ptr',
37
            field=models.OneToOneField(
38
                auto_created=True,
39
                on_delete=django.db.models.deletion.CASCADE,
40
                parent_link=True,
41
                primary_key=True,
42
                serialize=False,
43
                to='authenticators.BaseAuthenticator',
44
            ),
45
        ),
46
    ]
src/authentic2_auth_oidc/models.py
21 21
from django.contrib.postgres.fields import JSONField
22 22
from django.core.exceptions import ValidationError
23 23
from django.db import models
24
from django.shortcuts import render
24 25
from django.utils.translation import ugettext_lazy as _
25 26
from jwcrypto.jwk import InvalidJWKValue, JWKSet
26 27

  
27
from authentic2.a2_rbac.models import OrganizationalUnit
28
from authentic2.apps.authenticators.models import BaseAuthenticator
29
from authentic2.utils.misc import make_url, redirect_to_login
28 30
from authentic2.utils.template import validate_template
29 31

  
30 32
from . import managers
......
38 40
        raise ValidationError(_('Invalid JWKSet: %s') % e)
39 41

  
40 42

  
41
class OIDCProvider(models.Model):
43
class OIDCProvider(BaseAuthenticator):
42 44
    STRATEGY_CREATE = 'create'
43 45
    STRATEGY_FIND_UUID = 'find-uuid'
44 46
    STRATEGY_FIND_USERNAME = 'find-username'
......
61 63
        (ALGO_EC, _('EC')),
62 64
    ]
63 65

  
64
    name = models.CharField(unique=True, max_length=128, verbose_name=_('name'))
65
    slug = models.SlugField(unique=True, max_length=256, verbose_name=_('slug'), blank=True, null=True)
66 66
    issuer = models.CharField(max_length=256, verbose_name=_('issuer'), unique=True, db_index=True)
67 67
    client_id = models.CharField(max_length=128, default=uuid.uuid4, verbose_name=_('client id'))
68 68
    client_secret = models.CharField(max_length=128, default=uuid.uuid4, verbose_name=_('client secret'))
......
89 89

  
90 90
    # ou where new users should be created
91 91
    strategy = models.CharField(max_length=32, choices=STRATEGIES, verbose_name=_('strategy'))
92
    ou = models.ForeignKey(
93
        to=OrganizationalUnit, verbose_name=_('organizational unit'), on_delete=models.CASCADE
94
    )
95 92

  
96 93
    # policy
97 94
    max_auth_age = models.PositiveIntegerField(
98 95
        verbose_name=_('max authentication age'), blank=True, null=True
99 96
    )
100 97

  
101
    # hide OP from login page
102
    show = models.BooleanField(verbose_name=_('show on login page'), blank=True, default=True)
103

  
104 98
    # metadata
105 99
    created = models.DateTimeField(verbose_name=_('created'), auto_now_add=True)
106 100
    modified = models.DateTimeField(verbose_name=_('modified'), auto_now=True)
107 101

  
108 102
    objects = managers.OIDCProviderManager()
109 103

  
104
    type = 'oidc'
105
    how = ['oidc']
106
    description_fields = ['show_condition', 'issuer', 'scopes', 'strategy', 'created', 'modified']
107

  
108
    class Meta:
109
        verbose_name = _('OpenIDConnect')
110

  
111
    @property
112
    def manager_form_class(self):
113
        from .forms import OIDCProviderEditForm
114

  
115
        return OIDCProviderEditForm
116

  
110 117
    @property
111 118
    def jwkset(self):
112 119
        if self.jwkset_json:
113 120
            return JWKSet.from_json(json.dumps(self.jwkset_json))
114 121
        return None
115 122

  
123
    def get_short_description(self):
124
        if self.issuer and self.scopes:
125
            return _('OIDC provider linked to issuer %(issuer)s with scopes %(scopes)s.') % {
126
                'issuer': self.issuer,
127
                'scopes': self.scopes.replace(' ', ', '),
128
            }
129

  
116 130
    def clean_fields(self, exclude=None):
117 131
        super().clean_fields(exclude=exclude)
118 132
        exclude = exclude or []
......
145 159
                    % key_sig_mapping[self.idtoken_algo]
146 160
                )
147 161

  
148
    def __str__(self):
149
        return str(self.name)
150

  
151 162
    def authorization_claims_parameter(self):
152 163
        idtoken_claims = {}
153 164
        userinfo_claims = {}
......
165 176
    def __repr__(self):
166 177
        return '<OIDCProvider %r>' % self.issuer
167 178

  
179
    def autorun(self, request, *args):
180
        return redirect_to_login(request, login_url='oidc-login', kwargs={'pk': self.pk})
181

  
182
    def login(self, request, *args, **kwargs):
183
        context = kwargs.get('context', {})
184
        context['provider'] = self
185
        context['login_url'] = make_url(
186
            'oidc-login', kwargs={'pk': self.id}, request=request, keep_params=True
187
        )
188
        template_names = [
189
            'authentic2_auth_oidc/login_%s.html' % self.slug,
190
            'authentic2_auth_oidc/login.html',
191
        ]
192
        return render(request, template_names, context)
193

  
168 194

  
169 195
class OIDCClaimMapping(models.Model):
170 196
    NOT_VERIFIED = 0
src/authentic2_auth_oidc/utils.py
29 29
from authentic2.models import Attribute
30 30
from authentic2.utils.cache import GlobalCache
31 31

  
32
from . import models
33

  
34 32
TIMEOUT = 1
35 33

  
36 34

  
37
@GlobalCache(timeout=5, kwargs=['shown'])
38
def get_providers(shown=None):
39
    qs = models.OIDCProvider.objects.all()
40
    if shown is not None:
41
        qs = qs.filter(show=shown)
42
    return qs
43

  
44

  
45 35
@GlobalCache(timeout=TIMEOUT)
46 36
def get_attributes():
47 37
    return Attribute.objects.all()
tests/conftest.py
34 34
from authentic2.manager.utils import get_ou_count
35 35
from authentic2.models import Attribute, Service
36 36
from authentic2.utils.evaluate import BaseExpressionValidator
37
from authentic2_auth_oidc.utils import get_provider_by_issuer, get_providers, has_providers
37
from authentic2_auth_oidc.utils import get_provider_by_issuer, has_providers
38 38
from authentic2_idp_oidc.models import OIDCClient
39 39

  
40 40
from . import utils
......
339 339
    for cached_el in (
340 340
        OrganizationalUnit.cached,
341 341
        a2_hooks.get_hooks,
342
        get_providers,
343 342
        get_provider_by_issuer,
344 343
        get_ou_count,
345 344
        has_providers,
tests/test_auth_oidc.py
44 44
from authentic2.utils.misc import last_authentication_event
45 45
from authentic2_auth_oidc.backends import OIDCBackend
46 46
from authentic2_auth_oidc.models import OIDCAccount, OIDCClaimMapping, OIDCProvider
47
from authentic2_auth_oidc.utils import (
48
    IDToken,
49
    IDTokenError,
50
    get_providers,
51
    has_providers,
52
    parse_id_token,
53
    register_issuer,
54
)
47
from authentic2_auth_oidc.utils import IDToken, IDTokenError, parse_id_token, register_issuer
55 48

  
56 49
from . import utils
57 50

  
......
176 169
        ou=get_default_ou(),
177 170
        name=name,
178 171
        slug=slug,
172
        enabled=True,
179 173
        issuer=issuer,
180 174
        authorization_endpoint='%s/authorize' % issuer,
181 175
        token_endpoint='%s/token' % issuer,
......
412 406
        ou=get_default_ou(),
413 407
        name='OIDCIDP 2',
414 408
        slug='oidcidp-2',
409
        enabled=True,
415 410
        issuer='https://idp2.example.com/',
416 411
        authorization_endpoint='https://idp2.example.com/authorize',
417 412
        token_endpoint='https://idp2.example.com/token',
......
431 426

  
432 427

  
433 428
def test_login_with_conditional_authenticators(oidc_provider, oidc_provider_jwkset, app, settings, caplog):
434
    make_oidc_provider(name='My IDP', slug='myidp', jwkset=oidc_provider_jwkset)
429
    myidp = make_oidc_provider(name='My IDP', slug='myidp', jwkset=oidc_provider_jwkset)
435 430
    response = app.get('/login/')
436 431
    assert 'My IDP' in response
437 432
    assert 'Server' in response
438 433

  
439
    settings.AUTH_FRONTENDS_KWARGS = {'oidc': {'show_condition': {'myidp': 'remote_addr==\'0.0.0.0\''}}}
434
    myidp.show_condition = 'remote_addr==\'0.0.0.0\''
435
    myidp.save()
440 436
    response = app.get('/login/')
441 437
    assert 'Server' in response
442 438
    assert 'My IDP' not in response
443 439

  
444
    settings.AUTH_FRONTENDS_KWARGS = {
445
        'oidc': {
446
            'show_condition': {'myid': 'remote_addr==\'0.0.0.0\'', 'server': 'remote_addr==\'127.0.0.1\''}
447
        }
448
    }
449
    response = app.get('/login/')
450
    assert 'Server' in response
451
    assert 'My IDP' in response
452

  
453
    settings.AUTH_FRONTENDS_KWARGS = {
454
        'oidc': {
455
            'show_condition': {'myidp': 'remote_addr==\'0.0.0.0\'', 'server': 'remote_addr==\'127.0.0.1\''}
456
        }
457
    }
440
    oidc_provider.show_condition = 'remote_addr==\'127.0.0.1\''
441
    oidc_provider.save()
458 442
    response = app.get('/login/')
459 443
    assert 'Server' in response
460 444
    assert 'My IDP' not in response
461 445

  
462
    settings.AUTH_FRONTENDS_KWARGS = {'oidc': {'show_condition': 'remote_addr==\'127.0.0.1\''}}
446
    myidp.show_condition = 'remote_addr==\'127.0.0.1\''
447
    myidp.save()
463 448
    response = app.get('/login/')
464 449
    assert 'Server' in response
465 450
    assert 'My IDP' in response
466 451

  
467
    settings.AUTH_FRONTENDS_KWARGS = {
468
        'oidc': {
469
            'show_condition': {
470
                'myidp': 'remote_addr==\'127.0.0.1\' and \'backoffice\' not in login_hint',
471
                'server': '\'backoffice\' in login_hint',
472
            }
473
        }
474
    }
452
    myidp.show_condition = 'remote_addr==\'127.0.0.1\' and \'backoffice\' not in login_hint'
453
    myidp.save()
454
    oidc_provider.show_condition = '\'backoffice\' in login_hint'
455
    oidc_provider.save()
475 456
    response = app.get('/login/')
476 457
    assert 'Server' not in response
477 458
    assert 'My IDP' in response
......
627 608
    assert 'oidc-a-server' in response.text
628 609

  
629 610
    # do not show this provider on login page anymore
630
    oidc_provider.show = False
611
    oidc_provider.enabled = False
631 612
    oidc_provider.save()
632 613

  
633
    # we have a 5 seconds cache on list of providers, we have to work around it
634
    get_providers.cache.clear()
635
    has_providers.cache.clear()
636 614
    response = app.get('/login/')
637 615
    assert 'oidc-a-server' not in response.text
638 616

  
tests/test_commands.py
250 250
        jwkset.add(JWK.generate(kty='EC', size=256, kid='tsrn'))
251 251
        return OIDCProvider.objects.create(
252 252
            name=name,
253
            slug='test',
254
            enabled=True,
253 255
            ou=ou,
254 256
            issuer=issuer,
255 257
            strategy='create',
tests/test_manager_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 pytest
18

  
19
from authentic2_auth_oidc.models import OIDCProvider
20

  
17 21
from .utils import login, logout
18 22

  
19 23

  
......
74 78
    # cannot add another password authenticator
75 79
    resp = app.get('/manage/authenticators/add/')
76 80
    assert 'Password' not in resp.text
81

  
82

  
83
@pytest.mark.freeze_time('2022-04-19 14:00')
84
def test_authenticators_oidc(app, superuser, ou1, ou2):
85
    resp = login(app, superuser, path='/manage/authenticators/')
86

  
87
    resp = resp.click('Add new authenticator')
88
    resp.form['name'] = 'Test'
89
    resp.form['authenticator'] = 'oidc'
90
    resp.form['ou'] = ou1.pk
91

  
92
    resp = resp.form.submit().follow()
93
    assert OIDCProvider.objects.filter(slug='test').count() == 1
94
    assert 'Created: April 19, 2022, 2 p.m.' in resp.text
95
    assert 'Modified: April 19, 2022, 2 p.m.' in resp.text
96
    assert 'Issuer' not in resp.text
97

  
98
    resp = resp.click('Edit')
99
    assert 'enabled' not in resp.form.fields
100
    resp.form['issuer'] = 'https://oidc.example.com'
101
    resp.form['scopes'] = 'profile email'
102
    resp.form['strategy'] = 'create'
103
    resp.form['authorization_endpoint'] = 'https://oidc.example.com/authorize'
104
    resp.form['token_endpoint'] = 'https://oidc.example.com/token'
105
    resp.form['userinfo_endpoint'] = 'https://oidc.example.com/user_info'
106
    resp.form['idtoken_algo'] = 2
107
    resp = resp.form.submit().follow()
108

  
109
    assert 'Issuer: https://oidc.example.com' in resp.text
110
    assert 'Scopes: profile email' in resp.text
111

  
112
    resp = app.get('/manage/authenticators/')
113
    assert 'OpenIDConnect - Test' in resp.text
114
    assert 'class="section disabled"' in resp.text
115
    assert 'OIDC provider linked to' not in resp.text
116

  
117
    resp = resp.click('Configure', index=1)
118
    resp = resp.click('Enable').follow()
119
    assert 'Authenticator has been enabled.' in resp.text
120

  
121
    resp = app.get('/manage/authenticators/')
122
    assert 'class="section disabled"' not in resp.text
123
    assert 'OIDC provider linked to https://oidc.example.com with scopes profile, email.' not in resp.text
124

  
125
    # same name, same OU
126
    resp = resp.click('Add new authenticator')
127
    resp.form['name'] = 'test'
128
    resp.form['authenticator'] = 'oidc'
129
    resp.form['ou'] = ou1.pk
130
    resp = resp.form.submit().follow()
131
    assert OIDCProvider.objects.filter(slug='test-1').count() == 1
132
    OIDCProvider.objects.filter(slug='test-1').delete()
133

  
134
    # same name, different OU
135
    resp = app.get('/manage/authenticators/add/')
136
    resp.form['name'] = 'test'
137
    resp.form['authenticator'] = 'oidc'
138
    resp.form['ou'] = ou2.pk
139
    resp = resp.form.submit().follow()
140
    assert OIDCProvider.objects.filter(slug='test').count() == 2
141

  
142
    # OU is required
143
    resp = app.get('/manage/authenticators/add/')
144
    resp.form['name'] = 'test'
145
    resp.form['authenticator'] = 'oidc'
146
    resp.form['ou'] = ''
147
    resp = resp.form.submit()
148
    assert 'This field is required' in resp.text
149

  
150
    resp = app.get('/manage/authenticators/')
151
    resp = resp.click('Configure', index=1)
152
    resp = resp.click('Delete')
153
    resp = resp.form.submit().follow()
154
    assert OIDCProvider.objects.filter(slug='test').count() == 1
77
-