## $Id: authentication.py 7658 2012-02-16 15:29:13Z henrik $
##
## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
## 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, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
##
"""Authentication for SIRP.
"""
import grok
from zope.event import notify
from zope.component import getUtility, getUtilitiesFor
from zope.interface import Interface
from zope.securitypolicy.interfaces import (
    IPrincipalRoleMap, IPrincipalRoleManager)
from zope.pluggableauth.factories import Principal
from zope.pluggableauth.plugins.session import SessionCredentialsPlugin
from zope.pluggableauth.interfaces import (
        ICredentialsPlugin, IAuthenticatorPlugin,
        IAuthenticatedPrincipalFactory, AuthenticatedPrincipalCreated)
from zope.publisher.interfaces import IRequest
from zope.password.interfaces import IPasswordManager
from zope.securitypolicy.principalrole import principalRoleManager
from waeup.sirp.interfaces import (ILocalRoleSetEvent,
    IUserAccount, IAuthPluginUtility, IPasswordValidator,
    ISIRPPrincipal, ISIRPPrincipalInfo)

def setup_authentication(pau):
    """Set up plugguble authentication utility.

    Sets up an IAuthenticatorPlugin and
    ICredentialsPlugin (for the authentication mechanism)

    Then looks for any external utilities that want to modify the PAU.
    """
    pau.credentialsPlugins = ('No Challenge if Authenticated', 'credentials')
    pau.authenticatorPlugins = ('users',)

    # Give any third-party code and subpackages a chance to modify the PAU
    auth_plugin_utilities = getUtilitiesFor(IAuthPluginUtility)
    for name, util in auth_plugin_utilities:
        util.register(pau)

def get_principal_role_manager():
    """Get a role manager for principals.

    If we are currently 'in a site', return the role manager for the
    portal or the global rolemanager else.
    """
    portal = grok.getSite()
    if portal is not None:
        return IPrincipalRoleManager(portal)
    return principalRoleManager

class SIRPSessionCredentialsPlugin(grok.GlobalUtility,
                                    SessionCredentialsPlugin):
    grok.provides(ICredentialsPlugin)
    grok.name('credentials')

    loginpagename = 'login'
    loginfield = 'form.login'
    passwordfield = 'form.password'

class SIRPPrincipalInfo(object):
    """An implementation of ISIRPPrincipalInfo.

    A SIRP principal info is created with id, login, title, description,
    phone, email and user_type.
    """
    grok.implements(ISIRPPrincipalInfo)

    def __init__(self, id, title, description, email, phone, user_type):
        self.id = id
        self.title = title
        self.description = description
        self.email = email
        self.phone = phone
        self.user_type = user_type
        self.credentialsPlugin = None
        self.authenticatorPlugin = None

class SIRPPrincipal(Principal):
    """A portal principal.

    SIRP principals provide an extra `email`, `phone` and `user_type`
    attribute extending ordinary principals.
    """

    grok.implements(ISIRPPrincipal)

    def __init__(self, id, title=u'', description=u'', email=u'',
                 phone=None, user_type=u'', prefix=None):
        self.id = id
        if prefix is not None:
            self.id = '%s.%s' % (prefix, self.id)
        self.title = title
        self.description = description
        self.groups = []
        self.email = email
        self.phone = phone
        self.user_type = user_type

    def __repr__(self):
        return 'SIRPPrincipal(%r)' % self.id

class AuthenticatedSIRPPrincipalFactory(grok.MultiAdapter):
    """Creates 'authenticated' SIRP principals.

    Adapts (principal info, request) to a SIRPPrincipal instance.

    This adapter is used by the standard PAU to transform
    SIRPPrincipalInfos into SIRPPrincipal instances.
    """
    grok.adapts(ISIRPPrincipalInfo, IRequest)
    grok.implements(IAuthenticatedPrincipalFactory)

    def __init__(self, info, request):
        self.info = info
        self.request = request

    def __call__(self, authentication):
        principal = SIRPPrincipal(
            self.info.id,
            self.info.title,
            self.info.description,
            self.info.email,
            self.info.phone,
            self.info.user_type,
            authentication.prefix,
            )
        notify(
            AuthenticatedPrincipalCreated(
                authentication, principal, self.info, self.request))
        return principal

class Account(grok.Model):
    grok.implements(IUserAccount)

    _local_roles = dict()

    def __init__(self, name, password, title=None, description=None,
                 email=None, phone=None, roles = []):
        self.name = name
        if title is None:
            title = name
        self.title = title
        self.description = description
        self.email = email
        self.phone = phone
        self.setPassword(password)
        self.setSiteRolesForPrincipal(roles)
        # We don't want to share this dict with other accounts
        self._local_roles = dict()

    def setPassword(self, password):
        passwordmanager = getUtility(IPasswordManager, 'SHA1')
        self.password = passwordmanager.encodePassword(password)

    def checkPassword(self, password):
        passwordmanager = getUtility(IPasswordManager, 'SHA1')
        return passwordmanager.checkPassword(self.password, password)

    def getSiteRolesForPrincipal(self):
        prm = get_principal_role_manager()
        roles = [x[0] for x in prm.getRolesForPrincipal(self.name)
                 if x[0].startswith('waeup.')]
        return roles

    def setSiteRolesForPrincipal(self, roles):
        prm = get_principal_role_manager()
        old_roles = self.getSiteRolesForPrincipal()
        if sorted(old_roles) == sorted(roles):
            return
        for role in old_roles:
            # Remove old roles, not to be set now...
            if role.startswith('waeup.') and role not in roles:
                prm.unsetRoleForPrincipal(role, self.name)
        for role in roles:
            # Convert role to ASCII string to be in line with the
            # event handler
            prm.assignRoleToPrincipal(str(role), self.name)
        return

    roles = property(getSiteRolesForPrincipal, setSiteRolesForPrincipal)

    def getLocalRoles(self):
        return self._local_roles

    def notifyLocalRoleChanged(self, obj, role_id, granted=True):
        objects = self._local_roles.get(role_id, [])
        if granted and obj not in objects:
            objects.append(obj)
        if not granted and obj in objects:
            objects.remove(obj)
        self._local_roles[role_id] = objects
        if len(objects) == 0:
            del self._local_roles[role_id]
        self._p_changed = True
        return

class UserAuthenticatorPlugin(grok.GlobalUtility):
    grok.implements(IAuthenticatorPlugin)
    grok.provides(IAuthenticatorPlugin)
    grok.name('users')

    def authenticateCredentials(self, credentials):
        if not isinstance(credentials, dict):
            return None
        if not ('login' in credentials and 'password' in credentials):
            return None
        account = self.getAccount(credentials['login'])
        if account is None:
            return None
        if not account.checkPassword(credentials['password']):
            return None
        return SIRPPrincipalInfo(
            id=account.name,
            title=account.title,
            description=account.description,
            email=account.email,
            phone=account.phone,
            user_type=u'user')

    def principalInfo(self, id):
        account = self.getAccount(id)
        if account is None:
            return None
        return SIRPPrincipalInfo(
            id=account.name,
            title=account.title,
            description=account.description,
            email=account.email,
            phone=account.phone,
            user_type=u'user')

    def getAccount(self, login):
        # ... look up the account object and return it ...
        userscontainer = self.getUsersContainer()
        if userscontainer is None:
            return
        return userscontainer.get(login, None)

    def addAccount(self, account):
        userscontainer = self.getUsersContainer()
        if userscontainer is None:
            return
        # XXX: complain if name already exists...
        userscontainer.addAccount(account)

    def addUser(self, name, password, title=None, description=None):
        userscontainer = self.getUsersContainer()
        if userscontainer is None:
            return
        userscontainer.addUser(name, password, title, description)

    def getUsersContainer(self):
        site = grok.getSite()
        return site['users']

class PasswordValidator(grok.GlobalUtility):

  grok.implements(IPasswordValidator)

  def validate_password(self, pw, pw_repeat):
       errors = []
       if len(pw) < 3:
         errors.append('Password must have at least 3 chars.')
       if pw != pw_repeat:
         errors.append('Passwords do not match.')
       return errors

class LocalRoleSetEvent(object):

    grok.implements(ILocalRoleSetEvent)

    def __init__(self, object, role_id, principal_id, granted=True):
        self.object = object
        self.role_id = role_id
        self.principal_id = principal_id
        self.granted = granted

@grok.subscribe(IUserAccount, grok.IObjectRemovedEvent)
def handle_account_removed(account, event):
    """When an account is removed, local roles might have to be deleted.
    """
    local_roles = account.getLocalRoles()
    principal = account.name
    for role_id, object_list in local_roles.items():
        for object in object_list:
            try:
                role_manager = IPrincipalRoleManager(object)
            except TypeError:
                # No role manager, no roles to remove
                continue
            role_manager.unsetRoleForPrincipal(role_id, principal)
    return

@grok.subscribe(IUserAccount, grok.IObjectAddedEvent)
def handle_account_added(account, event):
    """When an account is added, the local owner role and the global
    AcademicsOfficer role must be set.
    """
    # We set the local Owner role
    role_manager_account = IPrincipalRoleManager(account)
    role_manager_account.assignRoleToPrincipal(
        'waeup.local.Owner', account.name)
    # We set the global AcademicsOfficer role
    site = grok.getSite()
    role_manager_site = IPrincipalRoleManager(site)
    role_manager_site.assignRoleToPrincipal(
        'waeup.AcademicsOfficer', account.name)
    # Finally we have to notify the user account that the local role
    # of the same object has changed
    notify(LocalRoleSetEvent(
        account, 'waeup.local.Owner', account.name, granted=True))
    return

@grok.subscribe(Interface, ILocalRoleSetEvent)
def handle_local_role_changed(obj, event):
    site = grok.getSite()
    if site is None:
        return
    users = site.get('users', None)
    if users is None:
        return
    role_id = event.role_id
    if event.principal_id not in users.keys():
        return
    user = users[event.principal_id]
    user.notifyLocalRoleChanged(event.object, event.role_id, event.granted)
    return

@grok.subscribe(Interface, grok.IObjectRemovedEvent)
def handle_local_roles_on_obj_removed(obj, event):
    try:
        role_map = IPrincipalRoleMap(obj)
    except TypeError:
        # no map, no roles to remove
        return
    for local_role, user_name, setting in role_map.getPrincipalsAndRoles():
        notify(LocalRoleSetEvent(
                obj, local_role, user_name, granted=False))
    return
