wcs/wcs/qommon/ident/idp.py

1076 lines
45 KiB
Python

# w.c.s. - web application for online forms
# Copyright (C) 2005-2010 Entr'ouvert
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# 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 re
try:
import lasso
except ImportError:
lasso = None
from django.utils.encoding import force_bytes, force_text
from django.utils.six.moves.urllib import parse as urllib
from django.utils.six.moves.urllib import parse as urlparse
from quixote.directory import Directory
from quixote import redirect, get_session, get_response, get_publisher
from quixote.html import htmltext, TemplateIO
from .. import _, N_
from .. import misc, get_cfg, get_logger
from ..form import *
from ..tokens import Token
from .. import template
from .. import errors
from ..backoffice.menu import html_top
from ..admin.menu import command_icon
from .base import AuthMethod
from ..storage import atomic_write
from .. import x509utils
from .. import saml2utils
ADMIN_TITLE = N_('SAML2')
def is_idp_managing_user_attributes():
return get_cfg('sp', {}).get('idp-manage-user-attributes', False)
def is_idp_managing_user_roles():
return get_cfg('sp', {}).get('idp-manage-roles', False)
def get_file_content(filename):
try:
return open(filename,'r').read()
except:
return None
def get_text_file_preview(filename):
'''Return a preformatted HTML blocks displaying content
of filename, or None if filename is not accessible
'''
content = get_file_content(str(filename))
if content:
return htmltext("<pre>%s</pre>") % content
else:
return None
class MethodDirectory(Directory):
_q_exports = ['login', 'register', 'token']
def login(self):
idps = get_cfg('idp', {})
if not lasso:
get_logger().error('/login unavailable - lasso is not installed')
raise Exception("lasso is missing, idp method cannot be used")
if len(idps) == 0:
return template.error_page(_('SSO support is not yet configured'))
t = IdPAuthMethod().login()
if t:
return t
if get_cfg('sp', {}).get('common_domain_getter_url'):
# use common domain to get IdP id
if get_session().saml_idp_cookie is None:
token = Token(expiration_delay = 600) # ten minutes
token.session_id = get_session().id
token.protocol = 'saml2'
token.next_url = get_request().get_frontoffice_url()
token.store()
common_domain_getter_url = get_cfg('sp', {}).get('common_domain_getter_url')
return redirect(common_domain_getter_url + '?tok=%s' % token.id)
form = Form(enctype='multipart/form-data')
form.add_hidden('method', 'idp')
options = []
value = None
providers = {}
for kidp, idp in sorted(get_cfg('idp', {}).items(), key=lambda k: k[0]):
if idp.get('hide'):
continue
p = lasso.Provider(lasso.PROVIDER_ROLE_IDP,
misc.get_abs_path(idp['metadata']),
misc.get_abs_path(idp.get('publickey')), None)
providers[p.providerId] = p
for p in providers.values():
label = misc.get_provider_label(p)
options.append((p.providerId, label, p.providerId))
if not value:
value = p.providerId
form.add(RadiobuttonsWidget, 'idp', value = value, options = options,
delim = htmltext('<br/>'))
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_submit() == 'cancel':
return redirect('..')
if form.is_submitted() and not form.has_errors():
idp = form.get_widget('idp').parse()
saml = get_publisher().root_directory_class.saml
return saml.perform_login(idp)
template.html_top(_('Login'))
r = TemplateIO(html=True)
r += htmltext('<p>%s</p>') % _('Select the identity provider you want to use.')
r += form.render()
return r.getvalue()
def register(self):
if get_cfg('saml_identities', {}).get('identity-creation', 'admin') == 'admin':
raise errors.TraversalError()
if not get_request().user or not get_session().name_identifier:
if get_cfg('saml_identities', {}).get('registration-url'):
vars = get_publisher().substitutions.get_context_variables(mode='lazy')
vars['next_url'] = get_request().get_frontoffice_url()
registration_url = misc.get_variadic_url(
get_cfg('saml_identities', {}).get('registration-url'),
vars)
return redirect(registration_url)
ident_methods = get_cfg('identification', {}).get('methods', [])
if len(ident_methods) > 1:
login_url = get_publisher().get_root_url() + 'login/idp/'
else:
login_url = get_publisher().get_root_url() + 'login/'
login_url += '?' + urllib.urlencode({'next': get_request().get_frontoffice_url()})
return redirect(login_url)
if get_request().user:
raise errors.AccessForbiddenError()
form = Form(enctype = 'multipart/form-data', use_tokens = False)
formdef = get_publisher().user_class.get_formdef()
if formdef:
formdef.add_fields_to_form(form)
else:
form.add(StringWidget, 'name', title = _('Name'), size=20, required=True)
form.add(EmailWidget, 'email', title = _('Email'), size=20, required=False)
form.add_submit('submit', _('Create Account'))
if form.is_submitted() and not form.has_errors():
tmp = self.register_submit(form, formdef)
if not form.has_errors():
return tmp
get_response().breadcrumb.append(('register', _('New Account')))
template.html_top(_('New Account'))
r = TemplateIO(html=True)
r += form.render()
return r.getvalue()
def register_submit(self, form, formdef):
user = get_publisher().user_class()
if form.get_widget('name'):
user.name = form.get_widget('name').parse()
if form.get_widget('email'):
user.email = form.get_widget('email').parse()
if formdef:
data = formdef.get_data(form)
user.set_attributes_from_formdata(data)
user.form_data = data
if get_publisher().user_class.count() == 0:
user.is_admin = True
session = get_session()
user.name_identifiers.append(session.name_identifier)
user.lasso_dump = get_request().user.lasso_dump
user.store()
session.set_user(user.id)
return redirect(get_publisher().get_root_url())
class AdminIDPDir(Directory):
title = N_('Identity Providers')
_q_exports = ['', 'new', 'new_remote']
def _q_traverse(self, path):
get_response().breadcrumb.append( ('idp/', _(self.title)))
return Directory._q_traverse(self, path)
def _q_index(self):
html_top('settings', title = _(self.title))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Identity Providers')
r += htmltext('<ul id="nav-idp-admin">\n')
r += htmltext(' <li><a href="new">%s</a></li>\n') % _('New')
r += htmltext(' <li><a href="new_remote">%s</a></li>\n') % _('Create new from remote URL')
r += htmltext('</ul>')
r += htmltext('<ul class="biglist">')
for kidp, idp in sorted(get_cfg('idp', {}).items(), key=lambda k: k[0]):
p = None
if idp and type(idp) is dict:
p = lasso.Provider(lasso.PROVIDER_ROLE_IDP,
misc.get_abs_path(idp.get('metadata')),
misc.get_abs_path(idp.get('publickey')),
None)
try: # this handling since "if p is None: continue" doesn't work
if p.providerId == '':
pass
except TypeError:
p = None
r += htmltext('<li>')
if p:
r += htmltext('<strong class="label">%s</strong>') % misc.get_provider_label(p)
else:
r += htmltext('<strong class="label">%s %s</strong>') % (kidp, _('Broken'))
if p:
r += htmltext('<p class="details">')
r += htmltext('<span class="data">%s</span>') % p.providerId
r += htmltext('</p>')
r += htmltext('<p class="commands">')
r += command_icon('%s/' % kidp, 'view')
r += command_icon('%s/edit' % kidp, 'edit')
r += command_icon('%s/delete' % kidp, 'remove')
r += htmltext('</p></li>')
r += htmltext('</ul>')
return r.getvalue()
def _q_lookup(self, component):
return AdminIDPUI(component)
@classmethod
def user_fields_options(cls):
'''List user formdef fields for the SelectWidget of the attribute
mapping setting'''
user_class = get_publisher().user_class
options = []
for field in user_class.get_formdef().fields:
options.append((str(field.id), field.label, str(field.id)))
return options
@classmethod
def get_form(cls, instance={}):
form = Form(enctype='multipart/form-data')
form.add(FileWidget, 'metadata', title = _('Metadata'), required=not instance)
form.add(FileWidget, 'publickey', title = _('Public Key'), required=False)
form.add(FileWidget, 'cacertchain', title = _('CA Certificate Chain'), required=False)
form.add(FileWidget, 'clientcertificate', title = _('Client Key and Certificate'))
form.add(CheckboxWidget, 'hide', title = _('Hide this provider from user lists'),
required = False, value = instance.get('hide'))
form.add(SingleSelectWidget, 'nameidformat',
title=_('Requested NameID format'),
value=instance.get('nameidformat', 'persistent'),
options=[('persistent', _('Persistent')),
('unspecified', _('Username (like Google Apps)')),
('email', _('Email'))])
form.add(WidgetDict, 'admin-attributes',
value = instance.get('admin-attributes', {
'local-admin': 'true',
}),
title=_('Administrator attribute matching rules'),
element_value_type=StringWidget,
hint=_('First column match attribute names, second is for matching '
'attribute value. If no rule is given, admin flag is never '
'set. Flag is set if any rule match.'))
options = cls.user_fields_options()
if options:
form.add(WidgetDict, 'attribute-mapping',
value=instance.get('attribute-mapping', {}),
title=_('Attribute mapping'),
element_value_type=SingleSelectWidget,
element_value_kwargs={'options': options},
hint=_('First column match attribute names, second row is the user field to fill'))
form.add_submit('submit', _('Submit'))
return form
def new(self):
get_response().breadcrumb.append(('new', _('New')))
form = self.get_form()
if not ('submit' in get_request().form and form.is_submitted()) or form.has_errors():
html_top('settings', title = _('New Identity Provider'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('New Identity Provider')
r += form.render()
return r.getvalue()
else:
return self.submit_new(form)
def submit_new(self, form, key_provider_id = None):
get_publisher().reload_cfg()
cfg_idp = get_cfg('idp', {})
get_publisher().cfg['idp'] = cfg_idp
metadata, publickey, cacertchain, clientcertificate = None, None, None, None
if form.get_widget('metadata').parse():
metadata = force_text(form.get_widget('metadata').parse().fp.read())
if form.get_widget('publickey').parse():
publickey = form.get_widget('publickey').parse().fp.read()
if form.get_widget('cacertchain').parse():
cacertchain = form.get_widget('cacertchain').parse().fp.read()
if form.get_widget('clientcertificate').parse():
clientcertificate = form.get_widget('clientcertificate').parse().fp.read()
if not key_provider_id:
try:
provider_id = re.findall(r'(provider|entity)ID="(.*?)"', metadata)[0][1]
except IndexError:
return template.error_page(_('Bad metadata'))
key_provider_id = misc.get_provider_key(provider_id)
dir = get_publisher().app_dir
metadata_fn = 'idp-%s-metadata.xml' % key_provider_id
if metadata:
atomic_write(os.path.join(dir, metadata_fn), force_bytes(metadata))
if publickey:
publickey_fn = 'idp-%s-publickey.pem' % key_provider_id
atomic_write(os.path.join(dir, publickey_fn), force_bytes(publickey))
else:
publickey_fn = None
if cacertchain:
cacertchain_fn = 'idp-%s-cacertchain.pem' % key_provider_id
atomic_write(os.path.join(dir, cacertchain_fn), force_bytes(cacertchain))
else:
cacertchain_fn = None
if clientcertificate:
clientcertificate_fn = 'idp-%s-clientcertificate.pem' % key_provider_id
atomic_write(os.path.join(dir, clientcertificate_fn), force_bytes(clientcertificate))
else:
clientcertificate_fn = None
cfg_idp[key_provider_id] = {
'metadata': metadata_fn,
'publickey': publickey_fn,
'cacertchain': cacertchain_fn,
'clientcertificate': clientcertificate_fn,
}
for key in ('hide', 'nameidformat', 'admin-attributes', 'attribute-mapping'):
if form.get_widget(key):
cfg_idp[key_provider_id][key] = form.get_widget(key).parse()
idp = cfg_idp[key_provider_id]
p = lasso.Provider(lasso.PROVIDER_ROLE_IDP,
misc.get_abs_path(idp['metadata']),
misc.get_abs_path(idp.get('publickey')), None)
try:
misc.get_provider_label(p)
except TypeError:
del cfg_idp[key_provider_id]
if metadata:
os.unlink(os.path.join(dir, metadata_fn))
if publickey:
os.unlink(os.path.join(dir, publickey_fn))
if cacertchain:
os.unlink(os.path.join(dir, cacertchain_fn))
if clientcertificate:
os.unlink(os.path.join(dir, clientcertificate_fn))
return template.error_page(_('Bad metadata'))
get_publisher().write_cfg()
return redirect('.')
def new_remote(self):
form = Form(enctype='multipart/form-data')
form.add(StringWidget, 'metadata_url', title = _('URL to metadata'), required=True,
size = 60)
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_submit() == 'cancel':
return redirect('.')
if form.is_submitted() and not form.has_errors():
metadata_pathname = None
publickey_pathname = None
metadata_url = form.get_widget('metadata_url').parse()
publickey_url = None
try:
rfd = misc.urlopen(metadata_url)
except misc.ConnectionError as e:
form.set_error('metadata_url', _('Failed to retrieve file (%s)') % e)
except:
form.set_error('metadata_url', _('Failed to retrieve file'))
else:
s = rfd.read()
(bfd, metadata_pathname) = tempfile.mkstemp(str('.metadata'))
atomic_write(metadata_pathname, force_bytes(s))
try:
p = lasso.Provider(lasso.PROVIDER_ROLE_IDP, metadata_pathname, None, None)
except lasso.Error:
pass
else:
t = self.submit_new_remote(metadata_pathname, publickey_pathname,
metadata_url, publickey_url)
if t:
return t
form.get_widget('metadata_url').set_error(_('Bad metadata'))
publickey_url = get_request().form.get('publickey_url')
if publickey_url:
form.add(StringWidget, 'publickey_url', title = _('URL to public key'),
required=True, size = 60)
try:
rfd = misc.urlopen(publickey_url)
except misc.ConnectionError as e:
form.set_error('metadata_url', _('Failed to retrieve file (%s)') % e)
except:
form.set_error('publickey_url', _('Failed to retrieve file'))
else:
s = rfd.read()
(bfd, publickey_pathname) = tempfile.mkstemp(str('.publickey'))
atomic_write(publickey_pathname, force_bytes(s))
try:
p = lasso.Provider(lasso.PROVIDER_ROLE_IDP, metadata_pathname,
publickey_pathname, None)
except lasso.Error:
form.get_widget('metadata_url').set_error(
_('Error in this metadata file'))
else:
t = self.submit_new_remote(metadata_pathname, publickey_pathname,
metadata_url, publickey_url)
if t:
return t
form.get_widget('metadata_url').set_error(_('Bad metadata'))
else:
# this will be the first time with a public key field; but
# perhaps it is not necessary, this is just the metadata
# being broken; test them with our our public key file
pubkey = misc.get_abs_path(get_cfg('sp')['publickey'])
try:
p = lasso.Provider(lasso.PROVIDER_ROLE_IDP, metadata_pathname,
pubkey, None)
except lasso.Error:
# this was an error in the metadata file itself
form.get_widget('metadata_url').set_error(
_('File looks like a bad metadata file'))
else:
# ok when provided with a public key -> adding it for real
form.add(StringWidget, 'publickey_url', title = _('URL to public key'),
required=True, size = 60,
hint = _('The metadata file does not embed a public key, please provide it here.'))
if publickey_pathname and os.path.exists(publickey_pathname):
os.unlink(publickey_pathname)
if metadata_pathname and os.path.exists(metadata_pathname):
os.unlink(metadata_pathname)
get_response().breadcrumb.append(('new_remote', _('New')))
html_top('settings', title = _('New Identity Provider'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('New Identity Provider')
r += form.render()
return r.getvalue()
def submit_new_remote(self, metadata_pathname, publickey_pathname,
metadata_url, publickey_url, key_provider_id = None):
role = lasso.PROVIDER_ROLE_IDP
get_publisher().reload_cfg()
cfg_idp = get_cfg('idp', {})
get_publisher().cfg['idp'] = cfg_idp
metadata = open(metadata_pathname).read()
if publickey_pathname:
publickey = open(publickey_pathname).read()
else:
publickey = None
if publickey_pathname and os.path.exists(publickey_pathname):
os.unlink(publickey_pathname)
if metadata_pathname and os.path.exists(metadata_pathname):
os.unlink(metadata_pathname)
try:
provider_id = re.findall(r'(provider|entity)ID="(.*?)"', metadata)[0][1]
except IndexError:
return None
new_key_provider_id = misc.get_provider_key(provider_id)
old_metadata_fn = None
old_publickey_fn = None
if key_provider_id and new_key_provider_id != key_provider_id:
# provider id changed, remove old files
cfg_idp[new_key_provider_id] = cfg_idp[key_provider_id]
old_metadata_fn = 'idp-%s-metadata.xml' % key_provider_id
old_publickey_fn = 'idp-%s-publickey.pem' % key_provider_id
del cfg_idp[key_provider_id]
key_provider_id = new_key_provider_id
dir = get_publisher().app_dir
metadata_fn = 'idp-%s-metadata.xml' % key_provider_id
publickey_fn = 'idp-%s-publickey.pem' % key_provider_id
if old_metadata_fn and os.path.exists(misc.get_abs_path(old_metadata_fn)):
os.rename(misc.get_abs_path(old_metadata_fn), misc.get_abs_path(metadata_fn))
if old_publickey_fn and os.path.exists(old_publickey_fn):
os.rename(misc.get_abs_path(old_publickey_fn), misc.get_abs_path(publickey_fn))
if key_provider_id not in cfg_idp:
cfg_idp[key_provider_id] = {}
cfg_idp[key_provider_id]['role'] = role
cfg_idp[key_provider_id]['metadata'] = metadata_fn
# save URL so they can be automatically updated later
cfg_idp[key_provider_id]['metadata_url'] = metadata_url
cfg_idp[key_provider_id]['publickey_url'] = publickey_url
atomic_write(misc.get_abs_path(metadata_fn), force_bytes(metadata))
if publickey:
atomic_write(misc.get_abs_path(publickey_fn), force_bytes(publickey))
get_publisher().write_cfg()
if not get_request():
# this allows this method to be called outsite of a
# request/response cycle.
return key_provider_id
return redirect('.')
class AdminIDPUI(Directory):
_q_exports = ['', 'delete', 'edit', 'update_remote']
def __init__(self, component):
self.idp = get_cfg('idp')[component]
self.idpk = component
get_response().breadcrumb.append(('%s/' % component, _('Provider')))
def _q_index(self):
p = lasso.Provider(lasso.PROVIDER_ROLE_IDP,
misc.get_abs_path(self.idp['metadata']),
misc.get_abs_path(self.idp.get('publickey')),
misc.get_abs_path(self.idp.get('cacertchain', None)))
html_top('settings', title = _('Identity Provider'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s - %s</h2>') % (_('Identity Provider'), p.providerId)
r += htmltext('<div class="form">')
r += htmltext('<h3>%s</h3>') % _('Metadata')
r += htmltext('<pre>')
metadata = open(misc.get_abs_path(self.idp['metadata'])).read()
try:
t = metadata.decode(str('utf8')).encode(get_publisher().site_charset)
except:
t = metadata
r += htmltext(t)
r += htmltext('</pre>')
r += htmltext('</div>')
r += htmltext('<p>')
r += htmltext('<a href="edit">%s</a> ') % _('Edit')
if self.idp.get('metadata_url'):
r += htmltext('<a href="update_remote">%s</a>') % _('Update from remote URL')
r += htmltext('</p>')
return r.getvalue()
def edit(self):
form = AdminIDPDir.get_form(self.idp)
if not ('submit' in get_request().form and form.is_submitted()) or form.has_errors():
html_top('settings', title = _('Edit Identity Provider'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Edit Identity Provider')
r += form.render()
return r.getvalue()
else:
return AdminIDPDir().submit_new(form, self.idpk) # XXX: not ok for metadata file path
def delete(self):
try:
p = lasso.Provider(lasso.PROVIDER_ROLE_SP,
misc.get_abs_path(self.idp.get('metadata')),
misc.get_abs_path(self.idp.get('publickey')), None)
if p.providerId is None:
# this is an empty test to refer to p.providerId and raise an
# exception if it is not available
pass
except:
p = None
form = Form(enctype='multipart/form-data')
form.widgets.append(HtmlWidget('<p>%s</p>' % _(
'You are about to irrevocably remove this identity provider.')))
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('..')
if not form.is_submitted() or form.has_errors():
html_top('settings', title = _('Identity Provider'))
r = TemplateIO(html=True)
if p:
r += htmltext('<h2>%s %s</h2>') % (_('Deleting'), p.providerId)
else:
r += htmltext('<h2>%s</h2>') % _('Deleting Identity Provider')
r += form.render()
return r.getvalue()
else:
return self.delete_submitted()
def delete_submitted(self):
del get_publisher().cfg['idp'][self.idpk]
dir = get_publisher().app_dir
metadata_fn = os.path.join(dir, 'idp-%s-metadata.xml' % self.idpk)
publickey_fn = os.path.join(dir, 'idp-%s-publickey.pem' % self.idpk)
cacertchain_fn = os.path.join(dir, 'idp-%s-cacertchain.pem' % self.idpk)
for f in (metadata_fn, publickey_fn, cacertchain_fn):
if os.path.exists(f):
os.unlink(f)
get_publisher().write_cfg()
return redirect('..')
def update_remote(self):
get_publisher().reload_cfg()
cfg_idp = get_cfg('idp', {})
get_publisher().cfg['idp'] = cfg_idp
metadata_url = self.idp.get('metadata_url')
try:
metadata_fd = misc.urlopen(metadata_url)
except misc.ConnectionError:
return template.error_page('failed to download')
metadata = force_text(metadata_fd.read())
publickey_url = self.idp.get('publickey_url')
if publickey_url:
try:
publickey_fd = misc.urlopen(publickey_url)
except misc.ConnectionError:
return template.error_page('failed to download')
publickey = publickey_fd.read()
else:
publickey = None
cacertchain = None
provider_id = re.findall(r'(provider|entity)ID="(.*?)"', metadata)[0][1]
try:
provider_id = re.findall(r'(provider|entity)ID="(.*?)"', metadata)[0][1]
except IndexError:
return template.error_page(_('Bad metadata'))
new_key_provider_id = misc.get_provider_key(provider_id)
key_provider_id = self.idpk
old_metadata_fn = None
old_publickey_fn = None
old_cacertchain_fn = None
old_dict = {}
if key_provider_id and new_key_provider_id != key_provider_id:
# provider id changed, remove old files
cfg_idp[new_key_provider_id] = cfg_idp[key_provider_id]
old_metadata_fn = 'idp-%s-metadata.xml' % key_provider_id
old_publickey_fn = 'idp-%s-publickey.pem' % key_provider_id
old_cacertchain_fn = 'idp-%s-cacertchain.pem' % key_provider_id
old_dict = cfg_idp[key_provider_id]
del cfg_idp[key_provider_id]
key_provider_id = new_key_provider_id
dir = get_publisher().app_dir
metadata_fn = 'idp-%s-metadata.xml' % key_provider_id
publickey_fn = 'idp-%s-publickey.pem' % key_provider_id
cacertchain_fn = 'idp-%s-cacertchain.pem' % key_provider_id
if old_publickey_fn and os.path.exists(old_publickey_fn):
os.rename(misc.get_abs_path(old_publickey_fn), misc.get_abs_path(publickey_fn))
if old_cacertchain_fn and os.path.exists(old_cacertchain_fn):
os.rename(misc.get_abs_path(old_cacertchain_fn), misc.get_abs_path(cacertchain_fn))
if key_provider_id not in cfg_idp:
cfg_idp[key_provider_id] = {}
cfg_idp[key_provider_id]['metadata'] = metadata_fn
if metadata:
atomic_write(misc.get_abs_path(metadata_fn), force_bytes(metadata))
if publickey:
atomic_write(misc.get_abs_path(publickey_fn), force_bytes(publickey))
cfg_idp[key_provider_id]['publickey'] = publickey_fn
if cacertchain:
atomic_write(misc.get_abs_path(cacertchain_fn), force_bytes(cacertchain))
cfg_idp[key_provider_id]['cacertchain'] = cacertchain_fn
lp = cfg_idp[key_provider_id]
publickey_fn = None
cacertchain_fn = None
if 'publickey' in lp and os.path.exists(misc.get_abs_path(lp['publickey'])):
publickey_fn = misc.get_abs_path(lp['publickey'])
if 'cacertchain' in lp and os.path.exists(misc.get_abs_path(lp['cacertchain'])):
cacertchain_fn = misc.get_abs_path(lp['cacertchain'])
try:
p = lasso.Provider(lasso.PROVIDER_ROLE_IDP, misc.get_abs_path(lp['metadata']),
publickey_fn, cacertchain_fn)
except lasso.Error:
# this happens when the public key is missing from both params
# and metadata file
if publickey_fn:
return (None, template.error_page(_('Bad metadata')))
else:
return (None, template.error_page(_('Bad metadata or missing public key')))
try:
p = misc.get_provider(key_provider_id)
except (TypeError, KeyError):
del cfg_idp[key_provider_id]
if metadata:
os.unlink(misc.get_abs_path(metadata_fn))
if publickey:
os.unlink(misc.get_abs_path(publickey_fn))
if cacertchain:
os.unlink(misc.get_abs_path(cacertchain_fn))
return template.error_page(_('Bad metadata'))
get_publisher().write_cfg()
return redirect('../%s/' % key_provider_id)
class MethodAdminDirectory(Directory):
title = ADMIN_TITLE
label = N_('Configure SAML identification method')
_q_exports = ['', 'sp', 'idp', 'identities']
idp = AdminIDPDir()
def _q_traverse(self, path):
get_response().breadcrumb.append( ('idp/', _(self.title)))
return Directory._q_traverse(self, path)
def _q_index(self):
html_top('settings', title = _(self.title))
r = TemplateIO(html=True)
r += htmltext('<h2>SAML 2.0</h2>')
r += htmltext('<dl> <dt><a href="sp">%s</a></dt> <dd>%s</dd>') % (
_('Service Provider'), _('Configure SAML 2.0 parameters'))
if get_cfg('sp', {}).get('saml2_providerid') and (
hasattr(get_publisher().root_directory_class, 'saml')):
metadata_url = '%s/metadata.xml' % get_cfg('sp')['saml2_base_url']
r += htmltext('<dt><a href="%s">%s</a></dt> <dd>%s</dd>') % (
metadata_url,
_('SAML 2.0 Service Provider Metadata'),
_('Download Service Provider SAML 2.0 Metadata file'))
r += htmltext('<dt><a href="idp/">%s</a></dt> <dd>%s</dd>') % (
_('Identity Providers'), _('Add and remove identity providers'))
r += htmltext('<dt><a href="identities">%s</a></dt> <dd>%s</dd>') % (
_('Identities'), _('Configure identities creation'))
r += htmltext('</dl>')
return r.getvalue()
def generate_rsa_keypair(self, branch = 'sp'):
publickey, privatekey = x509utils.generate_rsa_keypair()
encryptionpublickey, encryptionprivatekey = x509utils.generate_rsa_keypair()
cfg_sp = get_cfg(branch, {})
self.configure_sp_metadatas(cfg_sp, publickey, privatekey, encryptionpublickey, encryptionprivatekey)
def sp(self):
get_response().breadcrumb.append( ('sp', _('Service Provider')))
saml2_base_url = get_cfg('sp', {}).get('saml2_base_url', None)
req = get_request()
if not saml2_base_url:
saml2_base_url = '%s://%s%ssaml' % (req.get_scheme(), req.get_server(),
get_publisher().get_root_url())
form = Form(enctype='multipart/form-data')
form.add(StringWidget, 'saml2_providerid', title=_('SAML 2.0 Provider ID'),
size=50, required=False,
value = get_cfg('sp', {}).get(
'saml2_providerid', saml2_base_url + '/metadata'))
form.add(StringWidget, 'saml2_base_url', title=_('SAML 2.0 Base URL'),
size=50, required=False, value=saml2_base_url)
form.add(StringWidget, 'organization_name', title=_('Organisation Name'), size=50,
value = get_cfg('sp', {}).get('organization_name', None))
dir = get_publisher().app_dir
publickey_fn = os.path.join(dir, 'public-key.pem')
encryption_publickey_fn = os.path.join(dir, 'encryption-public-key.pem')
form.add(FileWidget, 'privatekey', title = _('Signing Private Key'))
form.add(FileWidget, 'publickey', title = _('Signing Public Key'), hint = get_text_file_preview(publickey_fn) or _('There is no signing key pair configured.'))
form.add(FileWidget, 'encryption_privatekey', title = _('Encryption Private Key'))
form.add(FileWidget, 'encryption_publickey', title = _('Encryption Public Key'), hint = get_text_file_preview(encryption_publickey_fn) or _('There is no encryption key pair configured.'))
form.add(StringWidget, 'common_domain',
title = _('Identity Provider Introduction, Common Domain'),
hint = _('Disabled if empty'),
value = get_cfg('sp', {}).get('common_domain'))
form.add(StringWidget, 'common_domain_getter_url',
title = _('Identity Provider Introduction, URL of Cookie Getter'),
hint = _('Disabled if empty'),
value = get_cfg('sp', {}).get('common_domain_getter_url'))
form.add(CheckboxWidget, 'authn-request-signed',
title = _('Sign authentication request'),
hint = _('Better to let it checked'),
value = get_cfg('sp',{}).get('authn-request-signed', True))
form.add(CheckboxWidget, 'want-assertion-signed',
title = _('IdP must crypt assertions'),
hint = _('Better to let it checked'),
value = get_cfg('sp',{}).get('want-assertion-signed', True))
form.add(CheckboxWidget, 'idp-manage-user-attributes',
title = _('IdP manage user attributes'),
value = get_cfg('sp',{}).get('idp-manage-user-attributes', False))
form.add(CheckboxWidget, 'idp-manage-roles',
title = _('IdP manage roles'),
value = get_cfg('sp',{}).get('idp-manage-roles', False))
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if x509utils.can_generate_rsa_key_pair():
form.add_submit('generate_rsa', _('Generate signing and encryption key pairs'))
if form.get_widget('cancel').parse():
return redirect('.')
if form.get_widget('generate_rsa') and form.get_widget('generate_rsa').parse():
result = self.sp_save(form)
if result:
form.set_error(*result)
else:
self.generate_rsa_keypair()
return redirect('')
if form.is_submitted() and not form.has_errors():
result = self.sp_save(form)
if result:
form.set_error(*result)
else:
return redirect('.')
html_top('settings', title = _('Service Provider Configuration'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Service Provider Configuration')
r += form.render()
return r.getvalue()
def write_sp_metadatas(self, signing_pem_key, private_signing_pem_key,
encryption_pem_key, private_encryption_pem_key, saml2_metadata):
'''Write SP metadatas, that key files and metadata files'''
dir = get_publisher().app_dir
if signing_pem_key:
privatekey_fn = os.path.join(dir, 'private-key.pem')
publickey_fn = os.path.join(dir, 'public-key.pem')
atomic_write(publickey_fn, force_bytes(signing_pem_key))
atomic_write(privatekey_fn, force_bytes(private_signing_pem_key))
if encryption_pem_key:
encryption_privatekey_fn = os.path.join(dir, 'encryption-private-key.pem')
encryption_publickey_fn = os.path.join(dir, 'encryption-public-key.pem')
atomic_write(encryption_publickey_fn, force_bytes(encryption_pem_key))
atomic_write(encryption_privatekey_fn, force_bytes(private_encryption_pem_key))
saml2_metadata_fn = os.path.join(dir, 'saml2-metadata.xml')
atomic_write(saml2_metadata_fn, force_bytes(saml2_metadata))
def configure_sp_metadatas(self, cfg_sp, signing_pem_key, private_signing_pem_key,
encryption_pem_key, private_encryption_pem_key):
if x509utils.can_generate_rsa_key_pair():
if signing_pem_key and not x509utils.check_key_pair_consistency(signing_pem_key, private_signing_pem_key):
return ('publickey', _('Signing key pair is invalid'))
if encryption_pem_key and not x509utils.check_key_pair_consistency(encryption_pem_key, private_encryption_pem_key):
return ('encryption_publickey', _('Encryption key pair is invalid'))
if signing_pem_key:
cfg_sp['publickey'] = 'public-key.pem'
cfg_sp['privatekey'] = 'private-key.pem'
if encryption_pem_key:
cfg_sp['encryption_privatekey'] = 'encryption-private-key.pem'
cfg_sp['encryption_publickey'] = 'encryption-public-key.pem'
cfg_sp['saml2_metadata'] = 'saml2-metadata.xml'
saml2_metadata = self.get_saml2_metadata(cfg_sp, signing_pem_key, encryption_pem_key)
self.write_sp_metadatas(signing_pem_key, private_signing_pem_key,
encryption_pem_key, private_encryption_pem_key,
saml2_metadata)
get_publisher().write_cfg()
return None
def sp_save(self, form):
get_publisher().reload_cfg()
cfg_sp = get_cfg('sp', {})
get_publisher().cfg['sp'] = cfg_sp
old_common_domain_getter_url = cfg_sp.get('common_domain_getter_url')
for k in ('organization_name', 'common_domain',
'saml2_providerid', 'saml2_base_url', 'common_domain_getter_url',
'grab_user_with_id_wsf', 'identity-creation',
'authn-request-signed', 'want-assertion-signed',
'idp-manage-user-attributes',
'idp-manage-roles'):
if form.get_widget(k):
cfg_sp[k] = form.get_widget(k).parse()
def get_key(name):
try:
return form.get_widget(name).parse().fp.read()
except:
return None
signing_pem_key = get_key('publickey')
private_signing_pem_key = get_key('privatekey')
encryption_pem_key = get_key('encryption_publickey')
private_encryption_pem_key = get_key('encryption_privatekey')
saml2 = bool('saml2_providerid' in cfg_sp)
new_common_domain_getter_url = cfg_sp.get('common_domain_getter_url')
if new_common_domain_getter_url != old_common_domain_getter_url:
old_domain = None
new_domain = None
if old_common_domain_getter_url:
old_domain = urlparse.urlparse(old_common_domain_getter_url)[1]
if ':' in old_domain:
old_domain = old_domain.split(':')[0]
old_domain_dir = os.path.normpath(os.path.join(dir, '..', old_domain))
try:
os.unlink(os.path.join(old_domain_dir, 'common_cookie'))
os.rmdir(old_domain_dir)
except OSError:
# bad luck, but ignore this
pass
if new_common_domain_getter_url:
new_domain = urlparse.urlparse(new_common_domain_getter_url)[1]
if ':' in new_domain:
new_domain = new_domain.split(':')[0]
new_domain_dir = os.path.normpath(os.path.join(dir, '..', new_domain))
try:
os.mkdir(new_domain_dir)
except OSError:
pass
fn = os.path.join(new_domain_dir, 'common_cookie')
atomic_write(fn, force_bytes(get_publisher().app_dir))
return self.configure_sp_metadatas(cfg_sp, signing_pem_key, private_signing_pem_key, encryption_pem_key, private_encryption_pem_key)
def get_saml2_metadata(self, sp_config, signing_pem_key, encryption_pem_key):
meta = saml2utils.Metadata(publisher = get_publisher(), config =
sp_config, provider_id = sp_config['saml2_providerid'])
return meta.get_saml2_metadata(signing_pem_key, encryption_pem_key, do_sp = True)
def identities(self):
form = Form(enctype='multipart/form-data')
identities_cfg = get_cfg('saml_identities', {})
compatibility_id_wsf_user = get_cfg('misc', {}).get('grab-user-with-wsf')
form.add(CheckboxWidget, 'grab-user-with-wsf',
title = _('Grab user details with ID-WSF on first logon'),
value = identities_cfg.get('grab-user-with-wsf', compatibility_id_wsf_user))
if not lasso.WSF_SUPPORT:
widget = form.get_widget('grab-user-with-wsf')
widget.hint = _('Lasso version is too old for this support.')
widget.attrs[str('disabled')] = str('disabled')
form.add(SingleSelectWidget, 'identity-creation',
title = _('Identity Creation'),
value = identities_cfg.get('identity-creation', 'admin'),
options = [(str('admin'), _('Site Administrator')),
(str('self'), _('Self-registration'))])
form.add(CheckboxWidget, 'email-confirmation',
title = _('Require email confirmation for new accounts'),
value = identities_cfg.get('email-confirmation', False),
disabled = True) # TODO
form.add(CheckboxWidget, 'notify-on-register',
title = _('Notify Administrators on Registration'),
value = identities_cfg.get('notify-on-register', False),
disabled = True) # TODO
form.add(StringWidget, 'registration-url',
title=_('Registration URL'),
hint=_('URL on Identity Provider where users can register '\
'an account. Available variable: next_url.'),
value=identities_cfg.get('registration-url', ''))
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_submit() == 'cancel':
return redirect('.')
if form.is_submitted() and not form.has_errors():
self.identities_submit(form)
return redirect('.')
get_response().breadcrumb.append(('identities', _('Identities Interface')))
html_top('settings', title = _('Identities Interface'))
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % _('Identities Interface')
r += form.render()
return r.getvalue()
def identities_submit(self, form):
from wcs.qommon.admin.settings import cfg_submit
cfg_submit(form, 'saml_identities',
('grab-user-with-wsf', 'identity-creation', 'notify-on-register',
'email-confirmation', 'registration-url'))
class MethodUserDirectory(Directory):
_q_exports = []
def __init__(self, user):
self.user = user
def get_actions(self):
return []
class IdPAuthMethod(AuthMethod):
key = 'idp'
description = N_('SAML identity provider')
method_directory = MethodDirectory
method_admin_directory = MethodAdminDirectory
method_user_directory = MethodUserDirectory
def is_interactive(self):
idps = get_cfg('idp', {})
if len(idps) == 1 or len([x for x in idps.values() if not x.get('hide', False)]) == 1:
return False
return True
def login(self):
idps = get_cfg('idp', {})
# there is only one visible IdP, perform login automatically on
# this one.
server = misc.get_lasso_server()
for x in sorted(server.providerIds):
key_provider_id = misc.get_provider_key(x)
if not idps.get(key_provider_id, {}).get('hide', False):
saml = get_publisher().root_directory_class.saml
return saml.perform_login(x)