general: allow marking form as required a given authentication context (#13177)
This commit is contained in:
parent
6ac8dc1396
commit
0dc2a83e47
|
@ -477,6 +477,40 @@ def test_form_workflow_remapping(pub):
|
|||
assert data_class.get(formdata1.id).status == 'wf-finished'
|
||||
assert data_class.get(formdata2.id).status == 'draft'
|
||||
|
||||
def test_form_submitter_roles(pub):
|
||||
create_superuser(pub)
|
||||
role = create_role()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = []
|
||||
formdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/1/')
|
||||
resp = resp.click(href=re.compile('^roles$'))
|
||||
resp.form['roles$element0'] = 'logged-users'
|
||||
assert not 'required_authentication_contexts' in resp.body
|
||||
resp = resp.form.submit()
|
||||
assert FormDef.get(formdef.id).roles == ['logged-users']
|
||||
|
||||
# add auth contexts support
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'auth-contexts', 'fedict')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/1/')
|
||||
resp = resp.click(href=re.compile('^roles$'))
|
||||
assert 'required_authentication_contexts' in resp.body
|
||||
resp.form['required_authentication_contexts$element0'].checked = True
|
||||
resp = resp.form.submit()
|
||||
resp = resp.follow()
|
||||
assert FormDef.get(formdef.id).required_authentication_contexts == ['fedict']
|
||||
|
||||
def test_form_workflow_role(pub):
|
||||
create_superuser(pub)
|
||||
role = create_role()
|
||||
|
|
|
@ -334,6 +334,11 @@ def test_limited_formdef_list(pub, local_user):
|
|||
assert resp.json[0]['authentication_required']
|
||||
assert resp.json == resp2.json == resp3.json == resp4.json
|
||||
|
||||
formdef.required_authentication_contexts = ['fedict']
|
||||
formdef.store()
|
||||
resp = get_app(pub).get('/api/formdefs/')
|
||||
assert resp.json[0]['required_authentication_contexts'] == ['fedict']
|
||||
|
||||
def test_formdef_list_redirection(pub):
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
|
|
|
@ -325,10 +325,10 @@ def test_form_access(pub):
|
|||
login(get_app(pub), username='foo', password='foo').get('/test/', status=200)
|
||||
|
||||
# check special "logged users" role
|
||||
formdef.roles = [logged_users_role()]
|
||||
formdef.roles = [logged_users_role().id]
|
||||
formdef.store()
|
||||
user = create_user(pub)
|
||||
login(get_app(pub), username='foo', password='foo').get('/test/', status=403)
|
||||
login(get_app(pub), username='foo', password='foo').get('/test/', status=200)
|
||||
resp = get_app(pub).get('/test/', status=302) # redirect to login
|
||||
|
||||
# check "receiver" can also access the formdef
|
||||
|
@ -341,6 +341,39 @@ def test_form_access(pub):
|
|||
user.store()
|
||||
login(get_app(pub), username='foo', password='foo').get('/test/', status=200)
|
||||
|
||||
def test_form_access_auth_context(pub):
|
||||
user = create_user(pub)
|
||||
|
||||
if not pub.site_options.has_section('options'):
|
||||
pub.site_options.add_section('options')
|
||||
pub.site_options.set('options', 'auth-contexts', 'fedict')
|
||||
with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
|
||||
pub.site_options.write(fd)
|
||||
|
||||
formdef = create_formdef()
|
||||
get_app(pub).get('/test/', status=200)
|
||||
|
||||
formdef.required_authentication_contexts = ['fedict']
|
||||
formdef.roles = [logged_users_role().id]
|
||||
formdef.store()
|
||||
|
||||
# an unlogged user will get a redirect to login
|
||||
resp = get_app(pub).get('/test/', status=302)
|
||||
assert '/login' in resp.location
|
||||
|
||||
# a user logged in with a simple username/password tuple will get a page
|
||||
# to relogin with a stronger auth
|
||||
app = login(get_app(pub), username='foo', password='foo')
|
||||
resp = app.get('/test/')
|
||||
assert 'You need a stronger authentication level to fill this form.' in resp.body
|
||||
|
||||
for session in pub.session_manager.values():
|
||||
session.saml_authn_context = 'urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI'
|
||||
session.store()
|
||||
resp = app.get('/test/')
|
||||
assert 'You need a stronger authentication level to fill this form.' not in resp.body
|
||||
assert resp.form
|
||||
|
||||
def test_form_submit(pub):
|
||||
formdef = create_formdef()
|
||||
formdef.data_class().wipe()
|
||||
|
|
|
@ -408,3 +408,11 @@ def test_backoffice_submission_roles():
|
|||
formdef.backoffice_submission_roles = [role.id]
|
||||
fd2 = assert_xml_import_export_works(formdef, include_id=True)
|
||||
assert fd2.backoffice_submission_roles == formdef.backoffice_submission_roles
|
||||
|
||||
def test_required_authentication_contexts():
|
||||
formdef = FormDef()
|
||||
formdef.name = 'foo'
|
||||
formdef.fields = []
|
||||
formdef.required_authentication_contexts = ['fedict']
|
||||
fd2 = assert_xml_import_export_works(formdef, include_id=True)
|
||||
assert fd2.required_authentication_contexts == formdef.required_authentication_contexts
|
||||
|
|
|
@ -430,7 +430,7 @@ class FormDefPage(Directory):
|
|||
wf_role_label, role_label)
|
||||
|
||||
r += add_option_line('roles', _('User Roles'),
|
||||
self._get_roles_label('roles'))
|
||||
self._get_roles_label_and_auth_context('roles'))
|
||||
r += add_option_line('backoffice-submission-roles',
|
||||
_('Backoffice Submission Role'),
|
||||
self._get_roles_label('backoffice_submission_roles'))
|
||||
|
@ -525,6 +525,15 @@ class FormDefPage(Directory):
|
|||
value = C_('roles|None')
|
||||
return value
|
||||
|
||||
def _get_roles_label_and_auth_context(self, attribute):
|
||||
value = self._get_roles_label(attribute)
|
||||
if self.formdef.required_authentication_contexts:
|
||||
auth_contexts = get_publisher().get_supported_authentication_contexts()
|
||||
value += ' (%s)' % ', '.join([auth_contexts.get(x)
|
||||
for x in self.formdef.required_authentication_contexts
|
||||
if auth_contexts.get(x)])
|
||||
return value
|
||||
|
||||
def get_sidebar(self):
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<ul id="sidebar-actions">')
|
||||
|
@ -615,6 +624,12 @@ class FormDefPage(Directory):
|
|||
'render_br': False,
|
||||
'options': options
|
||||
})
|
||||
auth_contexts = get_publisher().get_supported_authentication_contexts()
|
||||
if attribute == 'roles' and auth_contexts:
|
||||
form.add(CheckboxesWidget, 'required_authentication_contexts',
|
||||
title=_('Required authentication contexts'),
|
||||
value=self.formdef.required_authentication_contexts,
|
||||
options=auth_contexts.items())
|
||||
form.add_submit('submit', _('Submit'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
if form.get_widget('cancel').parse():
|
||||
|
@ -632,6 +647,9 @@ class FormDefPage(Directory):
|
|||
else:
|
||||
roles = form.get_widget('roles').parse() or []
|
||||
setattr(self.formdef, attribute, [x for x in roles if x])
|
||||
if form.get_widget('required_authentication_contexts'):
|
||||
self.formdef.required_authentication_contexts = form.get_widget(
|
||||
'required_authentication_contexts').parse()
|
||||
self.formdef.store()
|
||||
return redirect('.')
|
||||
|
||||
|
|
|
@ -289,6 +289,8 @@ class ApiFormdefsDirectory(Directory):
|
|||
'description': formdef.description or '',
|
||||
'keywords': formdef.keywords_list,
|
||||
'authentication_required': authentication_required}
|
||||
if formdef.required_authentication_contexts:
|
||||
formdict['required_authentication_contexts'] = formdef.required_authentication_contexts
|
||||
|
||||
formdict['redirection'] = bool(formdef.is_disabled() and
|
||||
formdef.disabled_redirection)
|
||||
|
|
|
@ -84,6 +84,9 @@ class FormFillPage(PublicFormFillPage):
|
|||
def html_top(self, *args, **kwargs):
|
||||
return html_top('submission', *args, **kwargs)
|
||||
|
||||
def check_authentication_context(self):
|
||||
pass
|
||||
|
||||
def check_role(self):
|
||||
if self.edit_mode:
|
||||
return True
|
||||
|
|
|
@ -73,6 +73,7 @@ class FormDef(StorableObject):
|
|||
workflow_options = None
|
||||
workflow_roles = None
|
||||
roles = None
|
||||
required_authentication_contexts = None
|
||||
backoffice_submission_roles = None
|
||||
discussion = False
|
||||
confirmation = True
|
||||
|
@ -566,6 +567,9 @@ class FormDef(StorableObject):
|
|||
if self.workflow_options:
|
||||
root['options'] = self.workflow_options
|
||||
|
||||
if self.required_authentication_contexts:
|
||||
root['required_authentication_contexts'] = self.required_authentication_contexts[:]
|
||||
|
||||
return json.dumps(root, indent=indent, cls=misc.JSONEncoder)
|
||||
|
||||
@classmethod
|
||||
|
@ -650,6 +654,10 @@ class FormDef(StorableObject):
|
|||
if value.get('geolocations'):
|
||||
formdef.geolocations = value.get('geolocations')
|
||||
|
||||
if value.get('required_authentication_contexts'):
|
||||
formdef.required_authentication_contexts = [str(x) for x in
|
||||
value.get('required_authentication_contexts')]
|
||||
|
||||
return formdef
|
||||
|
||||
def export_to_xml(self, include_id=False):
|
||||
|
@ -759,6 +767,11 @@ class FormDef(StorableObject):
|
|||
element.attrib['key'] = geoloc_key
|
||||
element.text = unicode(geoloc_label, charset)
|
||||
|
||||
if self.required_authentication_contexts:
|
||||
element = ET.SubElement(root, 'required_authentication_contexts')
|
||||
for auth_context in self.required_authentication_contexts:
|
||||
ET.SubElement(element, 'method').text = unicode(auth_context)
|
||||
|
||||
return root
|
||||
|
||||
@classmethod
|
||||
|
@ -946,6 +959,12 @@ class FormDef(StorableObject):
|
|||
geoloc_value = child.text.encode(charset)
|
||||
formdef.geolocations[geoloc_key] = geoloc_value
|
||||
|
||||
if tree.find('required_authentication_contexts') is not None:
|
||||
node = tree.find('required_authentication_contexts')
|
||||
formdef.required_authentication_contexts = []
|
||||
for child in node.getchildren():
|
||||
formdef.required_authentication_contexts.append(str(child.text))
|
||||
|
||||
return formdef
|
||||
|
||||
def get_detailed_email_form(self, formdata, url):
|
||||
|
|
|
@ -459,7 +459,10 @@ class FormPage(Directory):
|
|||
(tracking code and steps).'''
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<div id="side">')
|
||||
if self.formdef.enable_tracking_codes:
|
||||
if self.formdef.enable_tracking_codes and data:
|
||||
# display tracking code box if they are enabled and there's some
|
||||
# data (e.g. the user is not on a insufficient authenticiation
|
||||
# context page)
|
||||
r += self.tracking_code_box(data, magictoken)
|
||||
r += self.step(step_no, page_no, log_detail, data=data)
|
||||
r += htmltext('</div> <!-- #side -->')
|
||||
|
@ -524,8 +527,31 @@ class FormPage(Directory):
|
|||
def create_view_form(self, *args, **kwargs):
|
||||
return self.formdef.create_view_form(*args, **kwargs)
|
||||
|
||||
def check_authentication_context(self):
|
||||
if not self.formdef.required_authentication_contexts:
|
||||
return
|
||||
if get_session().get_authentication_context() in self.formdef.required_authentication_contexts:
|
||||
return
|
||||
|
||||
self.html_top(self.formdef.name)
|
||||
r = TemplateIO(html=True)
|
||||
r += self.form_side(step_no=0, page_no=0)
|
||||
auth_contexts = get_publisher().get_supported_authentication_contexts()
|
||||
r += htmltext('<div class="errornotice">')
|
||||
r += htmltext('<p>%s</p>') % _('You need a stronger authentication level to fill this form.')
|
||||
r += htmltext('</div>')
|
||||
root_url = get_publisher().get_root_url()
|
||||
for auth_context in self.formdef.required_authentication_contexts:
|
||||
r += htmltext('<p><a class="button" href="%slogin/?forceAuthn=true">%s</a></p>') % (
|
||||
root_url, _('Login with %s') % auth_contexts[auth_context])
|
||||
return r.getvalue()
|
||||
|
||||
def _q_index(self, log_detail=None):
|
||||
self.check_role()
|
||||
authentication_context_check_result = self.check_authentication_context()
|
||||
if authentication_context_check_result:
|
||||
return authentication_context_check_result
|
||||
|
||||
if self.check_disabled():
|
||||
return redirect(self.check_disabled())
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import collections
|
||||
import cPickle
|
||||
import ConfigParser
|
||||
import imp
|
||||
|
@ -972,6 +973,33 @@ class QommonPublisher(Publisher, object):
|
|||
default_position = '50.84;4.36'
|
||||
return default_position
|
||||
|
||||
def get_supported_authentication_contexts(self):
|
||||
contexts = collections.OrderedDict()
|
||||
labels = {
|
||||
'fedict': _('Belgian eID'),
|
||||
'franceconnect': _('FranceConnect'),
|
||||
}
|
||||
if self.get_site_option('auth-contexts'):
|
||||
for context in self.get_site_option('auth-contexts').split(','):
|
||||
context = context.strip()
|
||||
contexts[context] = labels[context]
|
||||
return contexts
|
||||
|
||||
def get_authentication_saml_contexts(self, context):
|
||||
return {
|
||||
'fedict': [
|
||||
# custom context, provided by authentic fedict plugin:
|
||||
'urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI',
|
||||
# native fedict contexts:
|
||||
'urn:be:fedict:iam:fas:citizen:eid',
|
||||
'urn:be:fedict:iam:fas:citizen:token',
|
||||
'urn:be:fedict:iam:fas:enterprise:eid',
|
||||
'urn:be:fedict:iam:fas:enterprise:token'],
|
||||
'franceconnect': [
|
||||
'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',
|
||||
]
|
||||
}[context]
|
||||
|
||||
def get_substitution_variables(self):
|
||||
import misc
|
||||
d = {
|
||||
|
|
|
@ -171,6 +171,13 @@ class Session(QommonSession, CaptchaSession, StorableObject):
|
|||
def get_user_object(self):
|
||||
return self.get_user()
|
||||
|
||||
def get_authentication_context(self):
|
||||
for context in get_publisher().get_supported_authentication_contexts():
|
||||
contexts = get_publisher().get_authentication_saml_contexts(context)
|
||||
if self.saml_authn_context in contexts:
|
||||
return context
|
||||
return None
|
||||
|
||||
def add_tempfile(self, upload):
|
||||
from wcs.qommon.form import PicklableUpload
|
||||
token = randbytes(8)
|
||||
|
|
Loading…
Reference in New Issue