# 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/>.

try:
    import lasso
except ImportError:
    lasso = None

from django.utils.encoding import force_bytes
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 _
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()
                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 not get_request().user.anonymous:
            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 = 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)
