## $Id: authentication.py 7190 2011-11-25 07:10:06Z 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
##
"""
Authenticate students.
"""
import grok
from zope.component import getUtility
from zope.password.interfaces import IPasswordManager
from zope.pluggableauth.interfaces import (
    IAuthenticatorPlugin, ICredentialsPlugin)
from zope.pluggableauth.plugins.session import (
    SessionCredentialsPlugin, SessionCredentials)
from zope.publisher.interfaces.http import IHTTPRequest
from zope.session.interfaces import ISession
from waeup.sirp.authentication import PrincipalInfo, get_principal_role_manager
from waeup.sirp.interfaces import (
    IAuthPluginUtility, IUserAccount, IPasswordValidator)
from waeup.sirp.students.interfaces import IStudent

class StudentAccount(grok.Adapter):
    """An adapter to turn student objects into accounts on-the-fly.
    """
    grok.context(IStudent)
    grok.implements(IUserAccount)

    @property
    def name(self):
        return self.context.student_id

    @property
    def password(self):
        return getattr(self.context, 'password', None)

    @property
    def title(self):
        return self.context.fullname

    @property
    def description(self):
        return self.title

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

    def _set_roles(self, roles):
        """Set roles for principal denoted by this account.
        """
        prm = get_principal_role_manager()
        old_roles = self.roles
        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:
            prm.assignRoleToPrincipal(role, self.name)
        return

    roles = property(_get_roles, _set_roles)

    def setPassword(self, password):
        """Set a password (LDAP-compatible) SSHA encoded.

        We do not store passwords in plaintext. Encrypted password is
        stored as unicode string.
        """
        passwordmanager = getUtility(IPasswordManager, 'SSHA')
        self.context.password = u'%s' % (
            passwordmanager.encodePassword(password))

    def checkPassword(self, password):
        """Check whether the given `password` matches the one stored.
        """
        if not isinstance(password, basestring):
            return False
        if not getattr(self.context, 'password', None):
            # unset/empty passwords do never match
            return False
        passwordmanager = getUtility(IPasswordManager, 'SSHA')
        return passwordmanager.checkPassword(
            self.context.password.encode('utf-8'), # turn unicode into bytes
            password)

class StudentsAuthenticatorPlugin(grok.GlobalUtility):
    grok.implements(IAuthenticatorPlugin)
    grok.provides(IAuthenticatorPlugin)
    grok.name('students')

    def authenticateCredentials(self, credentials):
        """Authenticate `credentials`.

        `credentials` is a tuple (login, password).

        We look up students to find out whether a respective student
        exists, then check the password and return the resulting
        `PrincipalInfo` or ``None`` if no such student can be found.
        """
        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 PrincipalInfo(id=account.name,
                             title=account.title,
                             description=account.description)

    def principalInfo(self, id):
        """Get a principal identified by `id`.

        This one is required by IAuthenticatorPlugin.
        """
        account = self.getAccount(id)
        if account is None:
            return None
        return PrincipalInfo(id=account.name,
                             title=account.title,
                             description=account.description)

    def getAccount(self, login):
        """Look up a student identified by `login`. Returns an account.

        Currently, we simply look up the key under which the student
        is stored in the portal. That means we hit if login name and
        name under which the student is stored match.

        Returns not a student but an account object adapted from any
        student found.

        If no such student exists, ``None`` is returned.
        """
        site = grok.getSite()
        if site is None:
            return None
        studentscontainer = site.get('students', None)
        if studentscontainer is None:
            return None
        student = studentscontainer.get(login, None)
        if student is None:
            return None
        return IUserAccount(student)

class PasswordChangeCredentialsPlugin(grok.GlobalUtility,
                                      SessionCredentialsPlugin):
    """A session credentials plugin that handles the case of a user
       changing his/her own password.

    When users change their own password they might find themselves
    logged out on next request.

    To avoid this, we support to use a 'change password' page a bit
    like a regular login page. That means, on each request we lookup
    the sent data for a login field called 'student_id' and a
    password.

    If both exist, this means someone sent new credentials.

    We then look for the old credentials stored in the user session.
    If the new credentials' login (the student_id) matches the old
    one's, we set the new credentials in session _but_ we return the
    old credentials for the authentication plugins to check as for the
    current request (and for the last time) the old credentials apply.

    No valid credentials are returned by this plugin if one of the
    follwing circumstances is true

    - the sent request is not a regular IHTTPRequest

    - the credentials to set do not match the old ones

    - no credentials are sent with the request

    - no credentials were set before (i.e. the user has no session
      with credentials set before)

    - no session exists already

    - password and repeated password do not match

    Therefore it is mandatory to put this plugin in the line of all
    credentials plugins _before_ other plugins, so that the regular
    credentials plugins can drop in as a 'fallback'.

    This plugin was designed for students to change their passwords,
    but might be used to allow password resets for other types of
    accounts as well.
    """
    grok.provides(ICredentialsPlugin)
    grok.name('student_pw_change')

    loginpagename = 'login'
    loginfield = 'student_id'
    passwordfield = 'change_password'
    repeatfield = 'change_password_repeat'

    def extractCredentials(self, request):
        if not IHTTPRequest.providedBy(request):
            return None
        login = request.get(self.loginfield, None)
        password = request.get(self.passwordfield, None)
        password_repeat = request.get(self.repeatfield, None)

        if not login or not password:
            return None

        validator = getUtility(IPasswordValidator)
        errors = validator.validate_password(password, password_repeat)
        if errors:
            return None

        session = ISession(request)
        sessionData = session.get(
            'zope.pluggableauth.browserplugins')
        if not sessionData:
            return None

        old_credentials = sessionData.get('credentials', None)
        if old_credentials is None:
            # Password changes for already authenticated users only!
            return None
        if old_credentials.getLogin() != login:
            # Special treatment only for users that change their own pw.
            return None
        old_credentials = {
            'login': old_credentials.getLogin(),
            'password': old_credentials.getPassword()}

        # Set new credentials in session. These will be active on next request
        new_credentials = SessionCredentials(login, password)
        sessionData['credentials'] = new_credentials

        # Return old credentials for this one request only
        return old_credentials

class StudentsAuthenticatorSetup(grok.GlobalUtility):
    """Register or unregister student authentication for a PAU.

    This piece is called when a new site is created.
    """
    grok.implements(IAuthPluginUtility)

    def register(self, pau):
        plugins = list(pau.credentialsPlugins)
        # this plugin must come before the regular credentials plugins
        plugins.insert(0, 'student_pw_change')
        pau.credentialsPlugins = tuple(plugins)
        plugins = list(pau.authenticatorPlugins)
        plugins.append('students')
        pau.authenticatorPlugins = tuple(plugins)
        return pau

    def unregister(self, pau):
        plugins = [x for x in pau.authenticatorPlugins
                   if x != 'students']
        pau.authenticatorPlugins = tuple(plugins)
        return pau
