Projet

Général

Profil

0001-general-allow-marking-form-as-required-a-strong-auth.patch

Frédéric Péters, 10 janvier 2017 11:04

Télécharger (15,1 ko)

Voir les différences:

Subject: [PATCH] general: allow marking form as required a strong
 authentication level (#13177)

 tests/test_admin_pages.py    | 33 +++++++++++++++++++++++++++++++++
 tests/test_api.py            |  5 +++++
 tests/test_form_pages.py     | 37 +++++++++++++++++++++++++++++++++++--
 tests/test_formdef_import.py |  8 ++++++++
 wcs/admin/forms.py           |  9 +++++++++
 wcs/api.py                   |  2 ++
 wcs/backoffice/submission.py |  3 +++
 wcs/formdef.py               | 19 +++++++++++++++++++
 wcs/forms/root.py            | 25 ++++++++++++++++++++++++-
 wcs/qommon/publisher.py      | 28 ++++++++++++++++++++++++++++
 wcs/qommon/sessions.py       |  7 +++++++
 11 files changed, 173 insertions(+), 3 deletions(-)
tests/test_admin_pages.py
477 477
    assert data_class.get(formdata1.id).status == 'wf-finished'
478 478
    assert data_class.get(formdata2.id).status == 'draft'
479 479

  
480
def test_form_submitter_roles(pub):
481
    create_superuser(pub)
482
    role = create_role()
483

  
484
    FormDef.wipe()
485
    formdef = FormDef()
486
    formdef.name = 'form title'
487
    formdef.fields = []
488
    formdef.store()
489

  
490
    app = login(get_app(pub))
491
    resp = app.get('/backoffice/forms/1/')
492
    resp = resp.click(href=re.compile('^roles$'))
493
    resp.form['roles$element0'] = 'logged-users'
494
    assert not 'required_authentication_levels' in resp.body
495
    resp = resp.form.submit()
496
    assert FormDef.get(formdef.id).roles == ['logged-users']
497

  
498
    # add auth levels support
499
    if not pub.site_options.has_section('options'):
500
        pub.site_options.add_section('options')
501
    pub.site_options.set('options', 'auth-levels', 'fedict')
502
    with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
503
        pub.site_options.write(fd)
504

  
505
    app = login(get_app(pub))
506
    resp = app.get('/backoffice/forms/1/')
507
    resp = resp.click(href=re.compile('^roles$'))
508
    assert 'required_authentication_levels' in resp.body
509
    resp.form['required_authentication_levels$element0'].checked = True
510
    resp = resp.form.submit()
511
    assert FormDef.get(formdef.id).required_authentication_levels == ['fedict']
512

  
480 513
def test_form_workflow_role(pub):
481 514
    create_superuser(pub)
482 515
    role = create_role()
tests/test_api.py
335 335
    assert resp.json[0]['authentication_required']
336 336
    assert resp.json == resp2.json == resp3.json == resp4.json
337 337

  
338
    formdef.required_authentication_levels = ['fedict']
339
    formdef.store()
340
    resp = get_app(pub).get('/api/formdefs/')
341
    assert resp.json[0]['required_authentication_levels'] == ['fedict']
342

  
338 343
def test_formdef_list_redirection(pub):
339 344
    FormDef.wipe()
340 345
    formdef = FormDef()
tests/test_form_pages.py
325 325
    login(get_app(pub), username='foo', password='foo').get('/test/', status=200)
326 326

  
327 327
    # check special "logged users" role
328
    formdef.roles = [logged_users_role()]
328
    formdef.roles = [logged_users_role().id]
329 329
    formdef.store()
330 330
    user = create_user(pub)
331
    login(get_app(pub), username='foo', password='foo').get('/test/', status=403)
331
    login(get_app(pub), username='foo', password='foo').get('/test/', status=200)
332 332
    resp = get_app(pub).get('/test/', status=302) # redirect to login
333 333

  
334 334
    # check "receiver" can also access the formdef
......
341 341
    user.store()
342 342
    login(get_app(pub), username='foo', password='foo').get('/test/', status=200)
343 343

  
344
def test_form_access_auth_level(pub):
345
    user = create_user(pub)
346

  
347
    if not pub.site_options.has_section('options'):
348
        pub.site_options.add_section('options')
349
    pub.site_options.set('options', 'auth-levels', 'fedict')
350
    with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
351
        pub.site_options.write(fd)
352

  
353
    formdef = create_formdef()
354
    get_app(pub).get('/test/', status=200)
355

  
356
    formdef.required_authentication_levels = ['fedict']
357
    formdef.roles = [logged_users_role().id]
358
    formdef.store()
359

  
360
    # an unlogged user will get a redirect to login
361
    resp = get_app(pub).get('/test/', status=302)
362
    assert '/login' in resp.location
363

  
364
    # a user logged in with a simple username/password tuple will get a page
365
    # to relogin with a stronger auth
366
    app = login(get_app(pub), username='foo', password='foo')
367
    resp = app.get('/test/')
368
    assert 'You need a stronger authentication level to fill this form.' in resp.body
369

  
370
    for session in pub.session_manager.values():
371
        session.saml_authn_context = 'urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI'
372
        session.store()
373
    resp = app.get('/test/')
374
    assert 'You need a stronger authentication level to fill this form.' not in resp.body
375
    assert resp.form
376

  
344 377
def test_form_submit(pub):
345 378
    formdef = create_formdef()
346 379
    formdef.data_class().wipe()
tests/test_formdef_import.py
408 408
    formdef.backoffice_submission_roles = [role.id]
409 409
    fd2 = assert_xml_import_export_works(formdef, include_id=True)
410 410
    assert fd2.backoffice_submission_roles == formdef.backoffice_submission_roles
411

  
412
def test_required_authentication_levels():
413
    formdef = FormDef()
414
    formdef.name = 'foo'
415
    formdef.fields = []
416
    formdef.required_authentication_levels = ['fedict']
417
    fd2 = assert_xml_import_export_works(formdef, include_id=True)
418
    assert fd2.required_authentication_levels == formdef.required_authentication_levels
wcs/admin/forms.py
613 613
                    'render_br': False,
614 614
                    'options': options
615 615
                })
616
        auth_levels = get_publisher().get_supported_authentication_levels()
617
        if attribute == 'roles' and auth_levels:
618
            form.add(CheckboxesWidget, 'required_authentication_levels',
619
                    title=_('Required authentication levels'),
620
                    value=self.formdef.required_authentication_levels,
621
                    options=auth_levels.items())
616 622
        form.add_submit('submit', _('Submit'))
617 623
        form.add_submit('cancel', _('Cancel'))
618 624
        if form.get_widget('cancel').parse():
......
630 636
        else:
631 637
            roles = form.get_widget('roles').parse() or []
632 638
            setattr(self.formdef, attribute, [x for x in roles if x])
639
            if form.get_widget('required_authentication_levels'):
640
                self.formdef.required_authentication_levels = form.get_widget(
641
                        'required_authentication_levels').parse()
633 642
            self.formdef.store()
634 643
            return redirect('.')
635 644

  
wcs/api.py
291 291
                        'description': formdef.description or '',
292 292
                        'keywords': formdef.keywords_list,
293 293
                        'authentication_required': authentication_required}
294
            if formdef.required_authentication_levels:
295
                formdict['required_authentication_levels'] = formdef.required_authentication_levels
294 296

  
295 297
            formdict['redirection'] = bool(formdef.is_disabled() and
296 298
                    formdef.disabled_redirection)
wcs/backoffice/submission.py
85 85
    def html_top(self, *args, **kwargs):
86 86
        return html_top('submission', *args, **kwargs)
87 87

  
88
    def check_authentication_level(self):
89
        pass
90

  
88 91
    def check_role(self):
89 92
        if self.edit_mode:
90 93
            return True
wcs/formdef.py
73 73
    workflow_options = None
74 74
    workflow_roles = None
75 75
    roles = None
76
    required_authentication_levels = None
76 77
    backoffice_submission_roles = None
77 78
    discussion = False
78 79
    confirmation = True
......
566 567
        if self.workflow_options:
567 568
            root['options'] = self.workflow_options
568 569

  
570
        if self.required_authentication_levels:
571
            root['required_authentication_levels'] = self.required_authentication_levels[:]
572

  
569 573
        return json.dumps(root, indent=indent, cls=misc.JSONEncoder)
570 574

  
571 575
    @classmethod
......
650 654
        if value.get('geolocations'):
651 655
            formdef.geolocations = value.get('geolocations')
652 656

  
657
        if value.get('required_authentication_levels'):
658
            formdef.required_authentication_levels = [str(x) for x in
659
                    value.get('required_authentication_levels')]
660

  
653 661
        return formdef
654 662

  
655 663
    def export_to_xml(self, include_id=False):
......
759 767
            element.attrib['key'] = geoloc_key
760 768
            element.text = unicode(geoloc_label, charset)
761 769

  
770
        if self.required_authentication_levels:
771
            element = ET.SubElement(root, 'required_authentication_levels')
772
            for auth_level in self.required_authentication_levels:
773
                ET.SubElement(element, 'method').text = unicode(auth_level)
774

  
762 775
        return root
763 776

  
764 777
    @classmethod
......
946 959
                geoloc_value = child.text.encode(charset)
947 960
                formdef.geolocations[geoloc_key] = geoloc_value
948 961

  
962
        if tree.find('required_authentication_levels') is not None:
963
            node = tree.find('required_authentication_levels')
964
            formdef.required_authentication_levels = []
965
            for child in node.getchildren():
966
                formdef.required_authentication_levels.append(str(child.text))
967

  
949 968
        return formdef
950 969

  
951 970
    def get_detailed_email_form(self, formdata, url):
wcs/forms/root.py
459 459
           (tracking code and steps).'''
460 460
        r = TemplateIO(html=True)
461 461
        r += htmltext('<div id="side">')
462
        if self.formdef.enable_tracking_codes:
462
        if self.formdef.enable_tracking_codes and data:
463 463
            r += self.tracking_code_box(data, magictoken)
464 464
        r += self.step(step_no, page_no, log_detail, data=data)
465 465
        r += htmltext('</div> <!-- #side -->')
......
524 524
    def create_view_form(self, *args, **kwargs):
525 525
        return self.formdef.create_view_form(*args, **kwargs)
526 526

  
527
    def check_authentication_level(self):
528
        if not self.formdef.required_authentication_levels:
529
            return
530
        if get_session().get_authentication_level() in self.formdef.required_authentication_levels:
531
            return
532

  
533
        self.html_top(self.formdef.name)
534
        r = TemplateIO(html=True)
535
        r += self.form_side(step_no=0, page_no=0)
536
        auth_levels = get_publisher().get_supported_authentication_levels()
537
        r += htmltext('<div class="errornotice">')
538
        r += htmltext('<p>%s</p>') % _('You need a stronger authentication level to fill this form.')
539
        r += htmltext('</div>')
540
        root_url = get_publisher().get_root_url()
541
        for auth_level in self.formdef.required_authentication_levels:
542
            r += htmltext('<p><a class="button" href="%slogin/?forceAuthn=true">%s</a></p>') % (
543
                    root_url, _('Login with %s') % auth_levels[auth_level])
544
        return r.getvalue()
545

  
527 546
    def _q_index(self, log_detail=None):
528 547
        self.check_role()
548
        authentication_level_check_result = self.check_authentication_level()
549
        if authentication_level_check_result:
550
            return authentication_level_check_result
551

  
529 552
        if self.check_disabled():
530 553
            return redirect(self.check_disabled())
531 554

  
wcs/qommon/publisher.py
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import collections
17 18
import cPickle
18 19
import ConfigParser
19 20
import imp
......
972 973
            default_position = '50.84;4.36'
973 974
        return default_position
974 975

  
976
    def get_supported_authentication_levels(self):
977
        levels = collections.OrderedDict()
978
        labels = {
979
            'fedict': _('Belgian eID'),
980
            'franceconnect': _('FranceConnect'),
981
        }
982
        if self.get_site_option('auth-levels'):
983
            for level in self.get_site_option('auth-levels').split(','):
984
                level = level.strip()
985
                levels[level] = labels[level]
986
        return levels
987

  
988
    def get_authentication_level_saml_contexts(self, level):
989
        return {
990
            'fedict': [
991
                # custom context, provided by authentic fedict plugin:
992
                'urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI',
993
                # native fedict contexts:
994
                'urn:be:fedict:iam:fas:citizen:eid',
995
                'urn:be:fedict:iam:fas:citizen:token',
996
                'urn:be:fedict:iam:fas:enterprise:eid',
997
                'urn:be:fedict:iam:fas:enterprise:token'],
998
            'franceconnect': [
999
                'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',
1000
            ]
1001
        }[level]
1002

  
975 1003
    def get_substitution_variables(self):
976 1004
        import misc
977 1005
        d = {
wcs/qommon/sessions.py
171 171
    def get_user_object(self):
172 172
        return self.get_user()
173 173

  
174
    def get_authentication_level(self):
175
        for level in get_publisher().get_supported_authentication_levels():
176
            contexts = get_publisher().get_authentication_level_saml_contexts(level)
177
            if self.saml_authn_context in contexts:
178
                return level
179
        return None
180

  
174 181
    def add_tempfile(self, upload):
175 182
        from wcs.qommon.form import PicklableUpload
176 183
        token = randbytes(8)
177
-