try:
    from hashlib import sha1 as hash_sha1
except ImportError:
    from sha import sha as hash_sha1
import base64
import cPickle
import os
import imp
import lasso
import crypt
import random
import string
import time

from quixote import get_publisher

from qommon import get_cfg, get_logger
from qommon.storage import StorableObject
from qommon.publisher import sitecharset2utf8, utf82sitecharset
import qommon.form as qform

import misc
from form import *
import admin.configuration as configuration

ROLE_NONE = 0
ROLE_ADMIN = 1

_singleton = object()

class AlreadyExists(Exception):
    pass

class WidgetList(quixote.form.widget.WidgetList):
    def render(self):
        get_response().add_javascript(['jquery.js', 'widget_list.js'])
        r = TemplateIO(html=True)
        classnames = '%s widget' % self.__class__.__name__
        r += htmltext('<div class="%s">') % classnames
        r += self.render_title(self.get_title())
        r += htmltext('<div class="content">')
        r += self.render_hint(self.get_hint())
        r += self.render_error(self.get_error())
        add_element_widget = self.get_widget('add_element')
        for widget in self.get_widgets():
            if widget is add_element_widget:
                continue
            r += widget.render()
        r += add_element_widget.render()
        if self.render_br:
            r += htmltext('<br class="%s" />') % classnames
        r += htmltext('\n')
        return r.getvalue()


dumb_questions = {
    'rps': N_('Rock, paper, scissors?'),
    'mother-in-law': N_('Your mother-in-law first name'),
    'best-friend': N_('Your best friend first name'),
    'teacher': N_('Your first teacher name'),
    'dog': N_('Your dog name'),
    'cat': N_('Your cat name'),
    'car': N_('Your car brand'),
}
available_dumb_questions = dumb_questions.keys()
    # so a authentic derivative can use less (or others) questions

__pwd_letters = list(string.letters) + list(string.digits)
def create_password(length):
    return ''.join([random.choice(__pwd_letters) for x in range(length)])

def ssha_hash_password(password, salt = None):
    '''Salted SHA1 hash of password'''
    if not salt:
        salt = create_password(8)
    else:
        # Extract salt from previous hashing
        salt = base64.b64decode(salt[6:])[20:]
    h = hash_sha1()
    h.update(password)
    h.update(salt)
    return '{SSHA}' + base64.b64encode(h.digest()+salt)

def sha_hash_password(password, salt = None):
    h = hash_sha1()
    h.update(password)
    return '{SHA}' + base64.b64encode(h.digest())

def crypt_hash_password(password, salt = None):
    if not salt:
        a = random.choice(string.letters)
        b = random.choice(string.letters)
        salt = '%c%c' % (a, b)
    else:
        salt = salt[7:9]
    return '{crypt}%s' % crypt.crypt(password, salt)

__hash_schemes = {
        'ssha': ssha_hash_password,
        'sha': sha_hash_password,
        'crypt': crypt_hash_password,
}

def hash_password(password, format, salt = None):
    scheme = __hash_schemes.get(format)
    if scheme is None:
        return password
    return scheme(password, salt = salt)

def get_hash_format(pwd):
    if pwd and pwd[0] == '{':
        i = pwd.find('}', 1)
        if i != -1:
            return pwd[1:i].lower()
    return None

def hashed_password_equals(pwd1, pwd2):
    format = get_hash_format(pwd1)
    if format:
        prefix_len = len(format) + 2
        return pwd1[prefix_len:] == hash_password(pwd2, format, salt = pwd1)[prefix_len:]
    else:
        return pwd1 == pwd2

def try_int(s):
    try:
        return int(s)
    except ValueError:
        return -1


class Field:
    def __init__(self, key, title, admin_required = False,
            on_register = True, widget_class = 'StringWidget',
            read_only = False, unique = False, multivalued = False,
            invisible = False, **keywords):
        '''
           Field defines parts of the user identity

           admin_required   whether the field is required even in the
                            administrative part
           on_register      whether the registry page should show a field for
                            this information
           widget_class     the widget to use for showing value of this field
           read_only        whether the field can be modified by the user
           unique           whether the field must be unique for recorde in the
                            identity storage (adds a check when saving the user
                            record)
           multivalued      whether the field can have multiple values for the
                            same user
        '''
        self.key = key
        self.title = title
        self.keywords = keywords
        self.admin_required = admin_required
        if isinstance(widget_class, str):
            self.widget_class = getattr(qform, widget_class)
        else:
            self.widget_class = widget_class
        self.on_register = on_register
        self.keywords = keywords
        self.read_only = read_only
        self.unique = unique
        self.multivalued = multivalued
        self.invisible = invisible

    def get_value(self, identity = None):
        '''Extra value from the identity object for this field, or return the
           default value from the keyword dictionnary'''
        value = self.keywords.pop('value', None)
        if identity is not None:
            value = getattr(identity, self.key, value)
        if self.multivalued:
            if value is None:
                value = []
            if not isinstance(value, list):
                value = [ value ]
        return value

    def get_one_value_human_representation(self, value):
        '''Return a human representation for a scalar value'''
        if value is True:
            return _('true')
        if value is False:
            return _('false')
        if value is None:
            return ''
        if isinstance(value, str) or isinstance(value, unicode):
            return value
        return str(value)

    def get_human_representation(self, identity = None):
        value = self.get_value(identity)
        if self.multivalued:
            return ', '.join(map(self.get_one_value_human_representation, value))
        else:
            return self.get_one_value_human_representation(value)

    def get_tabular_representation(self, identity = None):
        value = self.get_value(identity)
        if self.multivalued:
            elements = [ htmltext('<div class="value">%s</div>') % \
                    self.get_one_value_human_representation(v) for v in value]
            return htmltext(''.join(map(str, elements)))
        else:
            return self.get_one_value_human_representation(value)

    def add_to_form(self, form, identity = None, admin = False):
        widget_class = self.widget_class
        if self.key == 'email':
            widget_class = EmailWidget
        if self.keywords.get('regex'):
            widget_class = ValidatedStringWidget
        keywords = dict(self.keywords)
        if admin:
            keywords['required'] = self.admin_required
        value = keywords.pop('value', None)
        if identity:
            value = getattr(identity, self.key, value)
        if not self.read_only or admin:
            if self.multivalued:
                keywords['render_br'] = False
                form.add(WidgetList, self.key, title = _(self.title),
                    value = value,
                    hint = keywords.pop('hint', None),
                    required = keywords.pop('required', False),
                    element_type = widget_class, element_kwargs = keywords,
                    add_element_label = _('Add value'))
            else:
                if value is not None:
                    keywords['value'] = value
                form.add(widget_class, self.key, title = _(self.title),
                        **keywords)
        elif not self.invisible:
            form.add(HtmlWidget, htmltext('''<div class="HtmlWidget widget">
  <div class="title">%s:</div>
  <div class="content">%s&nbsp;</div></div>''') % (_(self.title),
                self.get_human_representation(identity)))
        return form.get_widget(self.key)

class IdentityStoreException(Exception):
    pass

class TooMuchAccounts(IdentityStoreException):
    pass

class Identity(StorableObject):
    _names = 'identities'

    id = None
    name = None
    email = None
    accounts = None
    roles = None
    lasso_dump = None  # identity dump
    lasso_proxy_dump = None  # identity dump, when acting as service provider
    lasso_proxy_dump_migrated = False
    resource_id = None
    entry_id = None
    disabled = False
    proxied_identity_origin = None
    shared_attributes = None

    ecp_id = None

    locked_down = False

    def __init__(self):
        self.accounts = []

    def __str__(self):
        if self.name:
            return self.name
        if self.email:
            return self.email
        return str(self.id)

    def migrate(self):
        if hasattr(self, 'uid'):
            self.id = self.uid
            del self.uid
            self.store()
        if str(self.id).startswith('https-') and not self.proxied_identity_origin:
            proxied_idp = '-'.join(self.id.split('-')[:-1])
            p = None
            for klp, lp in get_cfg('providers', {}).items():
                if lp['role'] != lasso.PROVIDER_ROLE_IDP:
                    continue
                p = lasso.Provider(lp['role'],
                        misc.get_abs_path(lp['metadata']),
                        misc.get_abs_path(lp['publickey']), None)
                base_key = misc.get_provider_key(p.providerId)
                if base_key == proxied_idp:
                    self.proxied_identity_origin = p.providerId
                    self.store()
                    break
        if not self.lasso_proxy_dump_migrated:
            self.lasso_proxy_dump_migrated = True
            if not self.lasso_proxy_dump_migrated and get_cfg('idp', {}).get('idff_proxy'):
                # bootstrap with a copy
                self.lasso_proxy_dump = self.lasso_dump
            self.store()

        if not self.ecp_id:
            self.ecp_id = ''.join([random.choice(string.lowercase) for x in range(4)])
            self.store()

    def is_admin(self):
        return ROLE_ADMIN in (self.roles or [])

    def get_dst_view(self, href):
        if href == lasso.PP_HREF:
            return self.get_pp_view()
        raise 'Unknown dst'

    def get_pp_view(self):
        l = ['<PP xmlns="urn:liberty:id-sis-pp:2003-08">']
        if self.name:
            l.append('  <InformalName>%s</InformalName>' % sitecharset2utf8(str(self)))
        if self.email:
            l.append('  <MsgContact><MsgAccount>%s</MsgAccount><MsgProvider>%s</MsgProvider></MsgContact>' % tuple(self.email.split('@')))
        l.append('</PP>')
        return '\n'.join(l)

    def get_display_name(self):
        if self.name:
            return self.name
        if self.email:
            return self.email
        return '#%s' % self.id
    display_name = property(get_display_name)

class CertificateAccount:
    certificate_sha1 = None
    dn = None

    def __repr__(self):
        return '<CertificateAccount: certificate_sha1: %s' % self.certificate_sha1 + '>'

    def __init__(self, sha1 = None, certificate = None, dn = None):
        self.dn = dn
        if certificate:
            start = '-----BEGIN CERTIFICATE-----\n'
            end = '\n-----END CERTIFICATE-----'
            i = certificate.find(start)
            j = certificate.find(end)
            if i != -1:
                i = i + len(start)
                certificate = certificate[i:j]
            der = base64.b64decode(certificate)
            h = hash_sha1()
            h.update(der)
            sha1 = h.hexdigest()
        self.certificate_sha1 = sha1

    def equals(self, account):
        return hasattr(account, 'certificate_sha1') and self.certificate_sha1 == account.certificate_sha1

class PasswordAccount:
    username = None
    password = None
    dumb_question = None
    smart_answer = None

    def equals(self, account):
        if not isinstance(account, PasswordAccount):
            return False
        if self.username != account.username:
            return False
        return hashed_password_equals(self.password, account.password)

    def type(self):
        return _("Password Account")

def get_pwd_length():
    '''Compute a password length from the min and max password settings'''
    pwd_cfg = configuration.get_configuration('passwords')
    min_pw_length = pwd_cfg.get('min_length')
    max_pw_length = pwd_cfg.get('max_length')
    pw_length = max([min_pw_length, 6])
    if max_pw_length:
        pw_length = min([pw_length, max_pw_length])
    return pw_length

def get_pwd_hashing_scheme():
    pwd_cfg = configuration.get_configuration('passwords')
    return pwd_cfg.get('hashed_scheme')

class BaseIdentitiesStore:
    fields = [ Field('name', N_('Name'), required = True, size = 30),
               Field('email', N_('Email'), required = True, size = 30) ]

    username_regex = r'[a-zA-Z0-9.-]+$'

    def create_password(self,for_account='unknown'):
        '''Create a new password for an user contained in this store'''
        password = create_password(get_pwd_length())
        get_logger().debug('Generated new password %r for account %r' % (password, for_account))
        return password

    def hash_password(self, password):
        return hash_password(password, get_pwd_hashing_scheme())

    def get_identity_for_account(self, account):
        for i in self.values():
            for a in i.accounts or []:
                if a.equals(account):
                    return i
        return None

    def load_identities(self):
        pass

    def reload_identities(self):
        pass

    def has_identity_with_username(self, username):
        return self.get_identity_for_username(username) is not None

    def get_identity_for_username(self, username, throw=False):
        for i in self.values():
            for a in i.accounts or []:
                if hasattr(a, 'username') and a.username == username:
                    return i
        return None

    def get_identity_for_name_identifier(self, name_identifier):
        for i in self.values():
            if not (i.lasso_dump or i.lasso_proxy_dump):
                continue
            if not ((name_identifier in (i.lasso_dump or '')) or (
                        name_identifier in (i.lasso_proxy_dump or ''))):
                continue # shortcut so it doesn't do the whole thing if not necessary
            if i.lasso_dump:
                identity = lasso.Identity.newFromDump(i.lasso_dump)
                for p in identity.providerIds:
                    federation = identity.getFederation(p)
                    if federation.localNameIdentifier and \
                            federation.localNameIdentifier.content == name_identifier:
                        return i
                    if federation.remoteNameIdentifier and \
                            federation.remoteNameIdentifier.content == name_identifier:
                        return i

            if i.lasso_proxy_dump:
                identity = lasso.Identity.newFromDump(i.lasso_proxy_dump)
                for p in identity.providerIds:
                    federation = identity.getFederation(p)
                    if federation.localNameIdentifier and \
                            federation.localNameIdentifier.content == name_identifier:
                        return i
                    if federation.remoteNameIdentifier and \
                            federation.remoteNameIdentifier.content == name_identifier:
                        return i

        return None

    def count(self, letter = None):
        return -1

    def is_big(self):
        return False

    def values_by_letter(self, letter, limit = 0, offset = 0):
        letter = letter.lower()
        return [x for x in self.values() if x.name and x.name.lower().startswith(letter)]

    def get_identities_by_attributes(self, map):
        '''Select identity objects by attributes'''
        result = []
        for x in self.values():
            for key, value in map.iteritems():
                if getattr(x, key, _singleton) != value:
                    break
            else:
                result.append(x)
        return result

    def last_modified(self):
        return time.time()

    def last_modified_uid(self, uid):
        return time.time()

    def get_identity_class(cls):
        return Identity
    get_identity_class = classmethod(get_identity_class)

    def administrators(self):
        return [ identity for identity in self.values() \
                if identity.is_admin() ]


class IdentitiesStorePickle(BaseIdentitiesStore):
    label = N_('Old default storage (pickled file)')

    def __init__(self):
        self.identities = None

    def is_bootstrapping(self):
        return len(self.identities.keys()) == 0

    def load_identities(self):
        if not self.identities:
            self.reload_identities()

    def reload_identities(self):
        filename = os.path.join(get_publisher().app_dir, 'identities.pck')
        try:
            self.identities = cPickle.load(file(filename))
        except:
            self.identities = {}
        for i in self.identities.values():
            if not i.id:
                i.id = i.uid

    def count(self, letter = None):
        return len(self.identities)

    def keys(self):
        return self.identities.keys()

    def values(self, limit = 0, offset = 0):
        return self.identities.values()

    def get_identity(self, uid):
        return self.identities[uid]

    def has_identity(self, uid):
        return self.identities.has_key(uid)

    def add(self, identity):
        if not identity.id:
            identity.id = self.get_new_uid()
        self.identities[identity.id] = identity

    def remove(self, identity):
        del self.identities[identity.id]
        self.save(None)

    def save(self, identity):
        # there is no way to save a single identity; dumps them all
        filename = os.path.join(get_publisher().app_dir, 'identities.pck')
        s = cPickle.dumps(self.identities)
        file(filename, 'w').write(s)

    def get_new_uid(self):
        uids = [try_int(x.id) for x in self.values()]
        if len(uids) == 0:
            return 1
        else:
            return max(uids)+1

    def connect(self, session):
        pass

class MiniIdentityLdap(StorableObject):
    _names = 'ldap_identities'
    roles = None
    lasso_dump = None
    lasso_proxy_dump = None
    uid = None
    resource_id = None
    disabled = False



try:
    import ldap
    import ldap.async
    import ldap.modlist
    import ldap.filter
except ImportError:
    ldap = None

class IdentitiesStoreLdap(BaseIdentitiesStore):
    # use LDAP for standard attributes and authentic storage (cpickle) for
    # authentic specific attributes (roles, lasso_dump)

    label = N_('LDAP Directory')
    admin_keys = ('ldap_url', 'ldap_new_user_base', 'ldap_base',
            'ldap_bind_dn', 'ldap_bind_password', 'ldap_object_class',
            'ldap_object_uid', 'ldap_object_name', 'ldap_object_email',
            'ldap_big_directory', 'ldap_published_attributes',
            'ldap_field_mapping','ldap_read_only')
    default_field_mapping = { 'email': 'mail', 'name': 'cn' }

    ldap_object_name = None
    ldap_object_classes = None
    ldap_bind_dn = None
    ldap_bind_password = None
    ldap_published_attributes = None
    ldap_field_mapping = default_field_mapping

    def __init__(self):
        self.ldap_conn = None

    def connect_admin(self):
        if not self.ldap_conn:
            self.ldap_conn = ldap.initialize(self.ldap_url)
        self.ldap_conn.protocol_version = ldap.VERSION3
        try:
            if self.ldap_bind_dn is not None:
                if self.ldap_bind_password is None:
                    self.ldap_conn.simple_bind(self.ldap_bind_dn)
                else:
                    self.ldap_conn.simple_bind(self.ldap_bind_dn, self.ldap_bind_password)
        except ldap.LDAPError:
            pass

    def field_name2attribute(self, field_name):
        return self.ldap_field_mapping.get(field_name, field_name)

    def field2attribute(self, field):
        return self.field_name2attribute(field.key)

    def ldap2identity(self, v):
        i = Identity()
        i.id = v[0]
        if self.ldap_object_name:
            keys = self.ldap_object_name.split('+')
            values = [ v[1].get(key.strip())[0] for key in keys if key in v[1] ]
            if values:
                i.name = ' '.join([utf82sitecharset(x) for x in values if x])
            else:
                i.name = _('Unknown')
        else:
            i.name = _('Unknown')
        i.email = v[1].get(self.ldap_object_email, [None])[0]

        # Deprecated: use custom fields
        if self.ldap_published_attributes:
            i.attributes = {}
            i.attributes['dn'] = i.id
            for attr in self.ldap_published_attributes.split(' '):
                if v[1].has_key(attr):
                    i.attributes[attr] = [utf82sitecharset(x) for x in v[1][attr]]

        # Load custom fields
        for field in self.fields:
            attribute = self.field2attribute(field)
            if attribute in v[1]:
                if field.multivalued:
                    values = [ utf82sitecharset(value) for value in v[1][attribute] ]
                    setattr(i, field.key, values)
                else:
                    setattr(i, field.key, utf82sitecharset(v[1][attribute][0]))
        try:
            pa = PasswordAccount()
            pa.username = v[1][self.ldap_object_uid][0] 
            pa.password = v[1].get('userPassword', [None])[0]
            i.accounts = [pa]
        except KeyError:
            i.accounts = []

        try:
            mini = MiniIdentityLdap.get(i.id)
        except KeyError:
            pass
        else:
            i.roles = mini.roles[:]
            i.lasso_dump = mini.lasso_dump
            i.lasso_proxy_dump = mini.lasso_proxy_dump
            i.resource_id = mini.resource_id
            i.disabled = mini.disabled
        return i

    def identity2ldap(self, identity):
        # Corresponding object class instance
        identity_object_class = None
        # entry is the ldap Modlist dictionnary of `identity'
        entry = {}
        entry[self.ldap_object_uid] = [identity.accounts[0].username]
        entry[self.ldap_object_email] = [identity.email]
        for account in identity.accounts:
            if isinstance(account, PasswordAccount):
                entry['userPassword'] = [account.password]
                break

        if self.ldap_object_classes is None:
            self.ldap_object_classes = []
            # Construct object classes tree (WARNING: only one way tree climb).
            sub = ldap.schema.urlfetch(self.ldap_url)
            object_class = sub[1].get_obj(ldap.schema.ObjectClass,
                                                    self.ldap_object_class)
            identity_object_class = object_class
            self.ldap_object_classes.append(self.ldap_object_class)
            while object_class.sup:
                sup_class_name = object_class.sup[0]
                self.ldap_object_classes.append(sup_class_name)
                object_class = sub[1].get_obj(ldap.schema.ObjectClass, sup_class_name)
            self.ldap_object_classes.reverse()
        entry['objectclass'] = self.ldap_object_classes

        # Convert custom fields
        for field in self.fields:
            attribute = self.field2attribute(field)
            value = getattr(identity, field.key, None)
            if value is not None:
                if field.multivalued:
                    entry[attribute] = [sitecharset2utf8(str(v)) for v in value]
                else:
                    entry[attribute] = [sitecharset2utf8(str(value))]

        return identity.id, entry

    def init(self):
        if not self.ldap_object_uid:
            self.ldap_object_uid = 'uid'
        if not self.ldap_object_name:
            self.ldap_object_name = 'cn'
        if not self.ldap_object_email:
            self.ldap_object_email = 'mail'
        oc = self.ldap_object_class
        name_field = self.ldap_object_name.split('+')[0]
        uid = self.ldap_object_uid
        self.uid_request_str = '(&(%(uid)s=%%(username)s)(objectclass=%(oc)s))' % {
            'uid': self.ldap_object_uid, 'oc': oc }
        self.class_request_str = 'objectclass=%s' % oc
        self.alpha_request_str = '(&(%(name)s=%%s*)(objectclass=%(oc)s))' % {
            'name': name_field, 'oc': oc }

    def is_bootstrapping(self):
        return MiniIdentityLdap.count() == 0

    def connect(self, session):
        if session and hasattr(session, 'ldap_infos'):
            try:
                self.ldap_conn.simple_bind_s(session.ldap_infos[0], session.ldap_infos[1])
            except ldap.INVALID_CREDENTIALS:
                return

    def load_identities(self):
        if not ldap:
            raise IdentityStoreException('Missing ldap module')
        if not self.ldap_conn:
            self.ldap_conn = ldap.initialize(self.ldap_url)
    
    def keys(self):
        raise NotImplementedError

    def values(self, limit = 0, offset = 0):
        self.connect_admin()
        s = ldap.async.List(self.ldap_conn)
        s.startSearch(self.ldap_base, ldap.SCOPE_SUBTREE,
                self.class_request_str, sizelimit = limit)
        try:
            partial = s.processResults()
        except ldap.SIZELIMIT_EXCEEDED:
            pass
        except ldap.SERVER_DOWN:
            raise IdentityStoreException('LDAP server is down')
        else:
            if partial:
                # it is ignored; this info should be passed down
                pass
        return [self.ldap2identity(v[1]) for v in s.allResults]

    def count(self, letter = None):
        self.connect_admin()
        s = ldap.async.List(self.ldap_conn)
        if letter is None:
            request = self.class_request_str
        else:
            letter = sitecharset2utf8(letter)
            letter = ldap.filter.escape_filter_chars(letter)
            request = self.alpha_request_str % letter
        try:
            s.startSearch(self.ldap_base, ldap.SCOPE_SUBTREE,
                    request, attrsOnly = 1, sizelimit = 99999)
            partial = s.processResults()
        except ldap.SIZELIMIT_EXCEEDED:
            pass
        except ldap.SERVER_DOWN:
            raise IdentityStoreException('LDAP server is down')
        else:
            if partial:
                # it is ignored; this info should be passed down
                pass
        return len(s.allResults)

    def values_by_letter(self, letter, limit = None, offset = 0):
        self.connect_admin()
        s = ldap.async.List(self.ldap_conn)
        letter = sitecharset2utf8(letter)
        letter = ldap.filter.escape_filter_chars(letter)
        try:
            s.startSearch(self.ldap_base, ldap.SCOPE_SUBTREE,
                    self.alpha_request_str % letter, sizelimit = limit + offset)
            partial = s.processResults()
        except ldap.SIZELIMIT_EXCEEDED:
            pass
        except ldap.SERVER_DOWN:
            raise IdentityStoreException('LDAP server is down')
        else:
            if partial:
                # it is ignored; this info should be passed down
                pass
        return [self.ldap2identity(v[1]) for v in s.allResults[offset:offset+limit]]

    def get_identity(self, uid):
        self.connect_admin()
        try:
            if type(uid) == int: #uid is an int => new account.
                raise KeyError
            if uid is None:
                raise KeyError
            result = self.ldap_conn.search_s(uid, ldap.SCOPE_BASE)
            if len(result) > 1:
                raise TooMuchAccounts('Too much answer for dn:%s' % uid)
            if len(result) == 0:
                raise KeyError
            return self.ldap2identity(result[0])
        except IndexError:
            raise KeyError
        except ldap.SERVER_DOWN:
            raise IdentityStoreException('LDAP server is down')
        except ldap.LDAPError, e:
            raise KeyError

    def has_identity(self, uid):
        try:
            self.get_identity(uid)
        except KeyError:
            return False
        return True

    def add(self, identity):
        uid = ldap.dn.escape_dn_chars(identity.accounts[0].username)
        # Make the id right
        identity.id = '%s=%s,%s' % (self.ldap_object_uid, uid,
                self.ldap_new_user_base or self.ldap_base)
        try:
            existing_identity = self.get_identity(identity.id)
            return self.save(identity)
        except KeyError:
            pass
        # New identity, id == dn
        dn, entry = self.identity2ldap(identity)
        # Naming attribute must be present
        if self.ldap_object_uid in entry and \
                entry[self.ldap_object_uid] != [ identity.accounts[0].username ]:
                    raise IdentityStoreException('Mismatch of naming attribute: %s != %s'
                            % (entry[self.ldap_object_uid],
                                [ identity.accounts[0].username ]))
        entry[self.ldap_object_uid] = [ identity.accounts[0].username ]
        self.connect_admin()
        addList = ldap.modlist.addModlist(entry)

        # Verify uniqueness of attributes
        unique_fields = [ field for field in self.fields if field.unique ]
        if unique_fields:
            keys = [ (field.key, self.field2attribute(field)) for field in unique_fields ]
            predicates = []
            for attribute, ldap_key in keys:
                value = ldap.filter.escape_filter_chars(getattr(identity, attribute))
                predicates.append('(%s=%s)' % (ldap_key, value))
            predicate = '(|%s)' % ''.join(predicates)
            try:
                result = self.ldap_conn.search_s(self.ldap_base,
                        ldap.SCOPE_SUBTREE,
                        predicate)
                if result:
                    raise AlreadyExists([key[0] for key in keys])
            except ldap.LDAPError, e:
                raise IdentityStoreException(str(e))
        try:
            self.ldap_conn.add_s(dn, addList)
        except ldap.ALREADY_EXISTS:
            raise AlreadyExists()

    def remove(self, identity):
        self.connect_admin()
        self.ldap_conn.delete_s(identity.id)
        try:
            MiniIdentityLdap.remove_object(identity.id)
        except Exception, e:
            get_logger().exception('IdentitiesStoreLdap.remove: failure to remove MiniIdentityLdap for %s' % identity.id)

    def get_identity_for_account(self, account):
        if not isinstance(account, PasswordAccount):
            return
        self.connect_admin()
        try:
            escaped_username = ldap.filter.escape_filter_chars(account.username)
            result = self.ldap_conn.search_s(self.ldap_base,
                    ldap.SCOPE_SUBTREE, 
                    self.uid_request_str % { 'username': escaped_username } )
            if not result:
                return
            else:
                uid = result[0][0]
            self.ldap_conn.simple_bind_s(uid, account.password)
        except ldap.INVALID_CREDENTIALS:
            return
        except ldap.SERVER_DOWN:
            raise IdentityStoreException('LDAP server is down')
        except IndexError:
            return
        identity = self.get_identity(uid)
        # Workaroung in order to keep the password clear in memory.
        identity.accounts[0].password = account.password
        return identity

    def has_identity_with_username(self, username):
        self.connect_admin()
        try:
            escaped_username = ldap.filter.escape_filter_chars(username)
            result = self.ldap_conn.search_s(self.ldap_base,
                    ldap.SCOPE_SUBTREE,
                    self.uid_request_str % { 'username': escaped_username })
            if result:
                return True
        except ldap.SERVER_DOWN:
            raise IdentityStoreException('LDAP server is down')
        except ldap.LDAPError, e:
            get_logger().error('%s.has_identity_for_username: %s' % (type(self), e))
        return False

    def get_identity_for_username(self, username, throw=False):
        self.connect_admin()
        try:
            escaped_username = ldap.filter.escape_filter_chars(username)
            result = self.ldap_conn.search_s(self.ldap_base,
                    ldap.SCOPE_SUBTREE,
                    self.uid_request_str % { 'username': escaped_username })
            if len(result) == 0:
                return None
            if len(result) > 1:
                raise TooMuchAccounts('Too much accounts for username:%s' % username)
            return self.ldap2identity(result[0])
        except ldap.SERVER_DOWN:
            raise IdentityStoreException('LDAP server is down')
        except IdentityStoreException, e:
            get_logger().error('%s.get_identity_for_username: %s' % (type(self), e))
            if throw:
                raise
        except ldap.LDAPError, e:
            get_logger().error('%s.get_identity_for_username: %s' % (type(self), e))
        return None

    def init_session(self, session, account):
        self.connect_admin()
        # adds extraneous data to session
        uid = self.ldap_conn.search_s(self.ldap_base,
                ldap.SCOPE_SUBTREE,
                self.uid_request_str % { 'username': account.username})[0][0]
        session.ldap_infos = (uid, account.password)

    def is_big(self):
        return self.ldap_big_directory

    def save(self, identity):
        mini = MiniIdentityLdap()
        mini.id = identity.id
        mini.uid = identity.id
        mini.disabled = identity.disabled
        if identity.roles:
            mini.roles = identity.roles[:]
        else:
            mini.roles = []
        mini.lasso_dump = identity.lasso_dump
        mini.lasso_proxy_dump = identity.lasso_proxy_dump
        mini.resource_id = identity.resource_id
        mini.store()

        if self.ldap_read_only:
            return False

        # Save LDAP stored data, administrative bind, because we do not have the
        # session password. How to access session variables ? Is get_session()
        # acceptable ?
        self.connect_admin()
        entry = self.ldap_conn.search_s(identity.id, ldap.SCOPE_BASE)[0]
        newEntry = entry[1].copy()
        modified = False
        if entry[1].has_key(self.ldap_object_name) and \
                entry[1][self.ldap_object_name][0] != sitecharset2utf8(identity.name):
            newEntry[self.ldap_object_name] = [sitecharset2utf8(identity.name)]
            modified = True
        if entry[1].has_key(self.ldap_object_email) and \
                entry[1][self.ldap_object_email][0] != identity.email:
            newEntry[self.ldap_object_email] = [identity.email]
            modified = True
        # Save custom fields
        for field in self.fields:
            attribute = self.field2attribute(field)
            value = getattr(identity, field.key, None)
            if field.multivalued:
                if value is None or value == '':
                    value = []
                elif not isinstance(value, list):
                    value = [ str(value) ]
                old_values = set(entry[1].get(attribute, []))
                new_values = set(value)
                if old_values != new_values:
                    newEntry[attribute] = value
            else:
                if value is not None:
                    value = sitecharset2utf8(str(value))
                if entry[1].get(attribute, [None])[0] != value:
                    # None must mean, no more value
                    if value is None and attribute in entry[1]:
                        del newEntry[attribute]
                    else:
                        newEntry[attribute] = [value]
                    modified = True
        # If identity.accounts[0].password, we have the same crypted version, 
        # we do not modify it. FIXME ?
        for account in identity.accounts:
            if isinstance(account, PasswordAccount):
                break
        else:
            account = None
        if account and entry[1].has_key('userPassword') and \
                account.password not in entry[1]['userPassword']:
            newEntry['userPassword'] = account.password
            modified = True

        if modified:
            modList = ldap.modlist.modifyModlist(entry[1], newEntry)
            try:
                self.ldap_conn.modify_s(entry[0], modList)
                get_logger().debug('LDAPStore: updating account %s' % account.username)
            except ldap.STRONG_AUTH_REQUIRED:
                # failed to modify, probaly because not logged in, probably
                # because called from SOAP for a defederate or something,
                # ignore; the federation data is anyway stored elsewhere
                return False
            except ldap.INVALID_SYNTAX:
                return False
        return modified

    def get_identity_for_name_identifier(self, name_identifier):
        for i in MiniIdentityLdap.values():
            if not (i.lasso_dump or i.lasso_proxy_dump):
                continue
            if not ((name_identifier in (i.lasso_dump or '')) or (
                        name_identifier in (i.lasso_proxy_dump or ''))):
                continue # shortcut so it doesn't do the whole thing if not necessary
            identity = lasso.Identity.newFromDump(i.lasso_dump)
            for p in identity.providerIds:
                federation = identity.getFederation(p)
                if federation.localNameIdentifier and \
                        federation.localNameIdentifier.content == name_identifier:
                    return self.get_identity(i.id)
                if federation.remoteNameIdentifier and \
                        federation.remoteNameIdentifier.content == name_identifier:
                    return self.get_identity(i.id)

            if i.lasso_proxy_dump:
                identity = lasso.Identity.newFromDump(i.lasso_proxy_dump)
                for p in identity.providerIds:
                    federation = identity.getFederation(p)
                    if federation.localNameIdentifier and \
                            federation.localNameIdentifier.content == name_identifier:
                        return i
                    if federation.remoteNameIdentifier and \
                            federation.remoteNameIdentifier.content == name_identifier:
                        return i

        return None

    class LdapKeyWidget(qform.ValidatedStringWidget):
        regex = r'^[a-zA-Z][a-zA-Z0-9-;]*$'

    class LdapObjectNameWidget(qform.ValidatedStringWidget):
        regex = r'^([a-zA-Z][a-zA-Z0-9-;]*)(\+[a-zA-Z][a-zA-Z0-9-;]*)*$'

    class LdapDnWidget(qform.ValidatedStringWidget):
        regex = r'^([a-zA-Z][a-zA-Z0-9-;]*)=([^ #,+"\<>;=/](\\.|[^#,+"\<>;=/])*)(,([a-zA-Z][a-zA-Z0-9-;]*)=([^ #,+"\<>;=/](\\.|[^#,+"\<>;=/])*))*$'

    def fill_admin_form(self, form, data_source):
        form.add(StringWidget, 'ldap_url', title = _('LDAP URL'), required = True,
                hint = htmltext(_('Example: <tt>ldap://directory.example.com</tt>')),
                value = data_source.get('ldap_url', ''))
        form.add(CheckboxWidget, 'ldap_read_only', title=_('LDAP is Read only'), required=False,
                value = data_source.get('ldap_read_only',False))
        form.add(StringWidget, 'ldap_base', title = _('LDAP Base'), required = True,
                hint = htmltext(_('Example: <tt>dc=example, dc=com</tt>')),
                value = data_source.get('ldap_base', ''))
        form.add(StringWidget, 'ldap_new_user_base', title = _('LDAP New User Base'), required = False,
                hint = htmltext(_('Example: <tt>dc=example, dc=com</tt>, if not set LDAP Base is used')),
                value = data_source.get('ldap_new_user_base', ''))
        form.add(StringWidget, 'ldap_bind_dn', title = _('LDAP Administrative Bind DN'), 
                required = False,
                hint = htmltext(_('Example: <tt>cn=admin, dc=example, dc=com</tt>')),
                value = data_source.get('ldap_bind_dn', ''))
        form.add(StringWidget, 'ldap_bind_password', 
                title = _('LDAP Administrative Bind password'), required = False,
                hint = htmltext(_('Example: <tt>secret</tt>')),
                value = data_source.get('ldap_bind_password', ''))
        form.add(StringWidget, 'ldap_object_class', title = _('LDAP Object Class'),
                required = True,
                hint = htmltext(_('Example: <tt>posixAccount</tt>')),
                value = data_source.get('ldap_object_class', ''))
        form.add(self.LdapKeyWidget, 'ldap_object_uid', title = _('LDAP Object Username Attribute'),
                required = True,
                hint = htmltext(_('Example: <tt>uid</tt>')),
                value = data_source.get('ldap_object_uid', ''))
        form.add(self.LdapObjectNameWidget, 'ldap_object_name', title = _('LDAP Object Name Attribute'),
                hint = htmltext(_('Example: <tt>cn</tt>')),
                value = data_source.get('ldap_object_name', ''))
        form.add(self.LdapKeyWidget, 'ldap_object_email', title = _('LDAP Object Email Attribute'),
                hint = htmltext(_('Example: <tt>mail</tt>')),
                value = data_source.get('ldap_object_email', ''))
        form.add(CheckboxWidget, 'ldap_big_directory', title = _('Massive LDAP Directory'),
                value = data_source.get('ldap_big_directory', False))
        form.add(TextWidget, 'ldap_published_attributes',
                title = _('LDAP attributes published in SAML assertions'),
                hint = htmltext(_('Example: <tt>uid mail</tt> (space delimited list)')),
                value = data_source.get('ldap_published_attributes', ''),
                rows = 4,
                cols = 80)
        form.add(qform.WidgetDict, 'ldap_field_mapping',
                element_value_type = self.LdapKeyWidget,
                title = _('Mapping from custom field name to LDAP attributes'),
                hint = htmltext(_('Possible fields are: ') + \
                        ', '.join([field.key for field in self.fields])),
                value = data_source.get('ldap_field_mapping', self.default_field_mapping))

    def administrators(self):
        # XXX: add support for ldap groups
        def clause(x):
            return ROLE_ADMIN in (x.roles or [])
        mini_identities = MiniIdentityLdap.select(clause,
                ignore_errors = True)
        identities = []
        for x in mini_identities:
            try:
                identity = self.get_identity(x.id)
            except KeyError:
                get_logger().warning('IdentitiesStoreLdap.administrators(): could not get administrator %r' % x.id)
                continue
            identities.append(identity)
        return identities

    def simple_search(self, predicate, base = None, sizelimit = 0):
        if base is None:
            base = self.ldap_base
        self.connect_admin()
        s = ldap.async.List(self.ldap_conn)
        s.startSearch(base, ldap.SCOPE_SUBTREE,
                predicate, sizelimit = sizelimit)
        try:
            partial = s.processResults()
        except ldap.SIZELIMIT_EXCEEDED:
            pass
        except ldap.SERVER_DOWN:
            raise IdentityStoreException('LDAP server is down')
        else:
            if partial:
                # it is ignored; this info should be passed down
                pass
        return s.allResults

    def get_identities_by_attributes(self, map):
        predicate = []
        for key, value in map.iteritems():
            key = self.field_name2attribute(key)
            sub_predicate = '(%s=%s)' % (ldap.filter.escape_filter_chars(key),
                    ldap.filter.escape_filter_chars(value))
            predicate.append(sub_predicate)
        predicate = '(&%s)' % ''.join(predicate)
        ldap_results = self.simple_search(predicate)
        return ( self.ldap2identity(ldap_result[1]) for ldap_result in ldap_results )

class IdentitiesStoreStorage(BaseIdentitiesStore):
    label = N_('Default storage (files)')
    identity_class = Identity

    def __init__(self, identity_class = Identity):
        self.identity_class = identity_class

    def is_bootstrapping(self):
        return self.identity_class.count() == 0

    def keys(self):
        return self.identity_class.keys()

    def values(self, limit = 0, offset = 0):
        return self.identity_class.values()

    def get_identity(self, uid):
        return self.identity_class.get(uid)

    def has_identity(self, uid):
        return self.identity_class.has_key(uid)

    def add(self, identity):
        identity.store()

    def remove(self, identity):
        identity.remove_self()

    def is_big(self):
        return self.count() > 1000

    def count(self, letter = None):
        if letter is None:
            return self.identity_class.count()
        else:
            return len(self.values_by_letter(letter = letter))

    def connect(self, session):
        pass

    def init_session(self, session, account):
        pass

    def save(self, identity):
        identity.store()

    def last_modified(self):
        return self.identity_class.last_modified()

    def last_modified_uid(self, uid):
        return self.identity_class.last_modified_id(uid)

    def get_identity_class(cls):
        return cls.identity_class
    get_identity_class = classmethod(get_identity_class)


sql_string_replace = [ # straight from sqlobject/converter.py
    ('\\', '\\\\'),
    ("'", "''"),
    ('\000', '\\0'),
    ('\b', '\\b'),
    ('\n', '\\n'),
    ('\r', '\\r'),
    ('\t', '\\t'),
    ]

def sqlrepr(s):
    if s is None:
        return 'NULL'
    elif type(s) in (str, unicode):
        for orig, repl in sql_string_replace:
            s = s.replace(orig, repl)
        return "'%s'" % s
    elif type(s) in (int, long):
        return str(s)

    raise 'XXX (unknown type: %r)' % type(s)


class LazyPostgresqlIdentity(Identity):
    real_roles = None
    def get_roles(self):
        if self.real_roles is None:
            c = get_publisher().store.conn.cursor()
            c.execute('SELECT role_id FROM identity_roles WHERE identity_id = %s' % self.id)
            self.real_roles = []
            for r in c.fetchall():
                self.real_roles.append(int(r[0]))
            c.close()
        return self.real_roles

    def set_roles(self, roles):
        self.real_roles = roles
    roles = property(get_roles, set_roles)

    real_accounts = None
    def get_accounts(self):
        if self.real_accounts is None:
            c = get_publisher().store.conn.cursor()

            c.execute('SELECT * FROM password_accounts WHERE identity_id = %s' % self.id)
            self.real_accounts = []
            for r in c.fetchall():
                pa = PasswordAccount()
                for i, k in enumerate(('username', 'password', 'dumb_question', 'smart_answer')):
                    setattr(pa, k, r[i+1])
                self.real_accounts.append(pa)
            c.close()
        
        return self.real_accounts

    def set_accounts(self, accounts):
        self.real_accounts = accounts
    accounts = property(get_accounts, set_accounts)


class IdentitiesStorePostgresql:
    label = N_('PostgreSQL database')
    admin_keys = ('pg_dbname', 'pg_dbuser', 'pg_dbpassword', 'pg_dbhost')

    def init(self):
        self.conn = None

    def is_bootstrapping(self):
        c = self.conn.cursor()
        try:
            c.execute('SELECT id FROM identities LIMIT 1')
        except ProgrammingError:
            # will for instance happen if the user configured has no sufficient
            # priviledges to access the database
            raise IdentityStoreException('Error accessing PostgreSQL (insufficient privileges?)')
        return c.fetchone() is None

    def connect(self, session):
        pass

    def load_identities(self):
        try:
            import psycopg2
        except ImportError:
            raise IdentityStoreException('Missing python-psycopg2 module to connect to the database')

        # initializes DB connection
        try:
            self.conn = psycopg2.connect('dbname=%s user=%s password=%s host=%s' % (
                self.pg_dbname,
                self.pg_dbuser,
                self.pg_dbpassword,
                self.pg_dbhost))
        except psycopg2.OperationalError:
            raise IdentityStoreException('Failed to connect to the database')

    def add(self, identity):
        c = self.conn.cursor()

        # Get next available identity id
        c.execute("SELECT NEXTVAL('identities_id_sequence')")
        id = c.fetchone()[0]
        identity.id = id

        # Add a new identity
        statement = 'INSERT INTO identities VALUES (%s)' % ', '.join(
               [sqlrepr(id)] + [sqlrepr(getattr(identity, x.key)) for x in self.fields] + 
               [sqlrepr(getattr(identity, x)) for x in (
                   'lasso_dump', 'lasso_proxy_dump', 'resource_id', 'entry_id')])
        c.execute(statement)

        # Add a new password account for this identity
        for account in identity.accounts:
            # isinstance() didn't work well for that 
            if account.__class__.__name__ == 'PasswordAccount':
                statement = 'INSERT INTO password_accounts VALUES (%s)' % ', '.join(
                    ["NEXTVAL('password_accounts_id_sequence')"] + \
                    [sqlrepr(getattr(account, x)) for x in (
                        'username', 'password', 'dumb_question', 'smart_answer')] + \
                    [sqlrepr(id)])
                c.execute(statement)

        # Associate a role to this identity
        for r in identity.roles or []:
            statement = 'INSERT INTO identity_roles VALUES(%s, %s)' % (id, r)
            c.execute(statement)

        # Commit all database changes        
        self.conn.commit()
        self.save_name_identifiers(identity)

    def keys(self):
        raise NotImplementedError

    def db2identity(self, v):
        identity = LazyPostgresqlIdentity()
        identity.id = v[0]
        for i, f in enumerate(self.fields):
            setattr(identity, f.key, v[i+1])
        for k in ('lasso_dump', 'lasso_proxy_dump'):
            setattr(identity, k, v[i+2])
        return identity

    def values(self, limit = 0, offset = 0):
        c = self.conn.cursor()
        statement = 'SELECT * FROM identities ORDER BY name'
        if limit:
            statement += ' LIMIT %d OFFSET %d' % (limit, offset)
        c.execute(statement)
        return [self.db2identity(v) for v in c.fetchall()]

    def values_by_letter(self, letter, limit = 0, offset = 0):
        letter = letter.lower()
        c = self.conn.cursor()
        statement = "SELECT * FROM identities WHERE name ILIKE '%s%%' ORDER BY name" % letter
        if limit:
            statement += ' LIMIT %d OFFSET %d' % (limit, offset)
        c.execute(statement)
        return [self.db2identity(v) for v in c.fetchall()]

    def get_identity(self, uid):
        if self.conn is None:
            # XXX : could use a more appropriate exception class
            raise KeyError
        c = self.conn.cursor()
        c.execute('SELECT * FROM identities WHERE id = %s' % uid)
        one = c.fetchone()
        if one is None:
            raise KeyError
        return self.db2identity(one)

    def has_identity(self, uid):
        c = self.conn.cursor()
        c.execute('SELECT * FROM identities WHERE id = %s' % uid)
        return c.fetchone() is not None

    def get_identity_for_account(self, account):
        c = self.conn.cursor()
        if account.__class__.__name__ == 'PasswordAccount':
            c.execute('SELECT identity_id FROM password_accounts WHERE '
                      'username = %s AND password = %s' % (
                          sqlrepr(account.username), sqlrepr(account.password)))
            one = c.fetchone()
            if one is None:
                return None
            identity_id = one[0]
            return self.get_identity(identity_id)
        raise "XXX: unknown account type"

    def has_identity_with_username(self, username):
        c = self.conn.cursor()
        c.execute('SELECT id FROM password_accounts WHERE username = %s' % sqlrepr(username))
        return c.fetchone() is not None

    def init_session(self, session, account):
        pass

    def count(self, letter = None):
        # XXX: WARNING: this is expensive when there are lots (millions) of
        # identities.
        c = self.conn.cursor()
        statement = 'SELECT COUNT(id) FROM identities'
        if letter:
            statement = statement + " WHERE name ILIKE '%s%%'" % letter
        c.execute(statement)
        return int(c.fetchone()[0])

    def is_big(self):
        # see count() comment about expensiveness
        return True
        return self.count() > 1000

    def save(self, identity):
        c = self.conn.cursor()
        
        # Update identity attributes
        statement = 'UPDATE identities SET %s WHERE id = %s' % (', '.join(
               ['%s = %s' % (x.key, sqlrepr(getattr(identity, x.key))) for x in self.fields] + \
               ['%s = %s' % (x, sqlrepr(getattr(identity, x))) for x in [
                    'lasso_dump', 'lasso_proxy_dump']]),
               identity.id)
        c.execute(statement)

        # Delete old password accounts and add new ones
        c.execute('DELETE FROM password_accounts WHERE identity_id = %s' % identity.id)
        for account in identity.accounts:
            if account.__class__.__name__ == 'PasswordAccount':
                statement = 'INSERT INTO password_accounts VALUES (%s)' % ', '.join(
                    ["NEXTVAL('password_accounts_id_sequence')"] + \
                    [sqlrepr(getattr(account, x)) for x in (
                        'username', 'password', 'dumb_question', 'smart_answer')] + \
                    [sqlrepr(identity.id)])
                c.execute(statement)

        # Delete old roles and save new ones
        c.execute('DELETE FROM identity_roles WHERE identity_id = %s' % identity.id)
        for r in identity.roles or []:
            statement = 'INSERT INTO identity_roles VALUES(%s, %s)' % (identity.id, r)
            c.execute(statement)
        self.save_name_identifiers(identity)

    def save_name_identifiers(self, identity):
        name_identifiers = []
        if identity.lasso_dump:
            import xml.dom.minidom
            doc = xml.dom.minidom.parseString(identity.lasso_dump)
            name_identifiers = [x.childNodes[0].data for x in doc.getElementsByTagNameNS(
                    'urn:oasis:names:tc:SAML:1.0:assertion', 'NameIdentifier')]
        if identity.lasso_proxy_dump:
            import xml.dom.minidom
            doc = xml.dom.minidom.parseString(identity.lasso_proxy_dump)
            t = [x.childNodes[0].data for x in doc.getElementsByTagNameNS(
                    'urn:oasis:names:tc:SAML:1.0:assertion', 'NameIdentifier')]
            name_identifiers.extend(t)

        c = self.conn.cursor()
        c.execute('DELETE FROM name_identifiers WHERE identity_id = %s' % identity.id)
        for name_identifier in name_identifiers:
            statement = 'INSERT INTO name_identifiers VALUES (%s)' % ', '.join(
                    ["NEXTVAL('name_identifiers_id_sequence')",
                     sqlrepr(name_identifier),
                     sqlrepr(identity.id)])
            c.execute(str(statement))
        self.conn.commit()

    def get_identity_for_name_identifier(self, name_identifier):
        c = self.conn.cursor()
        c.execute('SELECT identity_id FROM name_identifiers WHERE '
                      'name_identifier = %s' % sqlrepr(name_identifier))
        one = c.fetchone()
        if one is None:
            raise KeyError
        identity_id = one[0]
        return self.get_identity(identity_id)

    def fill_admin_form(self, form, data_source):
        form.add(StringWidget, 'pg_dbname', title = _('Database Name'), required = True,
                value = data_source.get('pg_dbname', ''))
        form.add(StringWidget, 'pg_dbuser', title = _('Database User'), required = True,
                value = data_source.get('pg_dbuser', ''))
        form.add(StringWidget, 'pg_dbpassword', title = _('Database Password'), required = True,
                value = data_source.get('pg_dbpassword', ''))
        form.add(StringWidget, 'pg_dbhost', title = _('Database Hostname'), required = True,
                value = data_source.get('pg_dbhost', 'localhost'))
    def last_modified(self):
        return None

    def last_modified_uid(self, uid):
        return None

    def get_identities_by_attributes(self, map):
        predicates = []
        for key, value in map.iteritems():
            sub_predicate = '% = %' % (key, sqlrepr(value))
            predicates.append(sub_predicate)
        predicate = ' AND '.join(predicates)
        c = self.conn.cursor()
        c.execute('SELECT * FROM name_identifiers WHERE %s' % sqlrepr(name_identifier))
        return [self.db2identity(v) for v in c.fetchall()]

stores = {
    'file': IdentitiesStorePickle,
    'common': IdentitiesStoreStorage,
    'ldap': IdentitiesStoreLdap,
    'postgresql': IdentitiesStorePostgresql,
}

def load_store():
    identity_stores_dir = os.path.join(os.path.dirname(__file__), 'identity_stores')
    if os.path.exists(identity_stores_dir):
        for filename in os.listdir(identity_stores_dir):
            if filename.endswith('.py'):
                modulename = filename[:-3]
                fp, pathname, description = imp.find_module(modulename, [identity_stores_dir])
                try:
                    imp.load_module(modulename, fp, pathname, description)
                except:
                    if fp:
                        fp.close()
    _storage = get_cfg('identity_storage', {'source': 'common'})
    store = stores.get(_storage['source'], stores['common'])()
    if hasattr(store, 'admin_keys'):
        for k in store.admin_keys:
            if k in _storage or not hasattr(store, k):
                setattr(store, k, _storage.get(k, None))
    if hasattr(store, 'init'):
        store.init()
    return store

def get_store():
    return get_publisher().store

def get_store_class():
    _storage = get_cfg('identity_storage', {'source': 'common'})
    return stores.get(_storage['source'], stores['common'])
