##
## authentication.py
## Login : <uli@pu.smp.net>
## Started on  Tue Jul 27 14:26:35 2010 Uli Fouquet
## $Id: authentication.py 7137 2011-11-19 08:37:08Z henrik $
## 
## Copyright (C) 2010 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
##
"""Special authentication for applicants.

   XXX: This is work in progress, experimental code! Don't do that at home!
"""
import grok
from zope.event import notify
from zope.pluggableauth.factories import Principal
from zope.pluggableauth.interfaces import (
    ICredentialsPlugin, IAuthenticatorPlugin,
    IAuthenticatedPrincipalFactory, AuthenticatedPrincipalCreated)
from zope.pluggableauth.plugins.session import SessionCredentialsPlugin
from zope.publisher.interfaces import IRequest
from zope.publisher.interfaces.http import IHTTPRequest
from zope.session.interfaces import ISession
from waeup.sirp.accesscodes import get_access_code
from waeup.sirp.applicants.interfaces import (
    IApplicantPrincipalInfo, IApplicantPrincipal, IApplicantSessionCredentials,
    )
from waeup.sirp.applicants import get_applicant_data
from waeup.sirp.interfaces import IAuthPluginUtility


class ApplicantPrincipalInfo(object):
    """Infos about an applicant principal.
    """
    grok.implements(IApplicantPrincipalInfo)

    def __init__(self, access_code):
        self.id = principal_id(access_code)
        self.title = u'Applicant'
        self.description = u'An Applicant'
        self.credentialsPlugin = None
        self.authenticatorPlugin = None
        self.access_code = access_code

class ApplicantPrincipal(Principal):
    """An applicant principal.

    Applicant principals provide an extra `access_code` and `reg_no`
    attribute extending ordinary principals.
    """

    grok.implements(IApplicantPrincipal)

    def __init__(self, access_code, prefix=None):
        self.id = principal_id(access_code)
        if prefix is not None:
            self.id = '%s.%s' % (prefix, self.id)
        self.title = u'Applicant'
        self.description = u'An applicant'
        self.groups = []
        self.access_code = access_code

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

class AuthenticatedApplicantPrincipalFactory(grok.MultiAdapter):
    """Creates 'authenticated' applicant principals.

    Adapts (principal info, request) to an ApplicantPrincipal instance.

    This adapter is used by the standard PAU to transform
    PrincipalInfos into Principal instances.
    """
    grok.adapts(IApplicantPrincipalInfo, IRequest)
    grok.implements(IAuthenticatedPrincipalFactory)

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

    def __call__(self, authentication):
        principal = ApplicantPrincipal(
            self.info.access_code,
            authentication.prefix,
            )
        notify(
            AuthenticatedPrincipalCreated(
                authentication, principal, self.info, self.request))
        return principal


#
# Credentials plugins and related....
#

class ApplicantCredentials(object):
    """Credentials class for ordinary applicants.
    """
    grok.implements(IApplicantSessionCredentials)

    def __init__(self, access_code):
        self.access_code = access_code

    def getAccessCode(self):
        """Get the access code.
        """
        return self.access_code

    def getLogin(self):
        """Stay compatible with non-applicant authenticators.
        """
        return None

    def getPassword(self):
        """Stay compatible with non-applicant authenticators.
        """
        return None

class WAeUPApplicantCredentialsPlugin(grok.GlobalUtility,
                                      SessionCredentialsPlugin):
    """A credentials plugin that scans requests for applicant credentials.
    """
    grok.provides(ICredentialsPlugin)
    grok.name('applicant_credentials')

    loginpagename = 'login'
    accesscode_prefix_field = 'form.ac_prefix'
    accesscode_series_field = 'form.ac_series'
    accesscode_number_field = 'form.ac_number'

    def extractCredentials(self, request):
        """Extracts credentials from a session if they exist.
        """
        if not IHTTPRequest.providedBy(request):
            return None
        session = ISession(request)
        sessionData = session.get(
            'zope.pluggableauth.browserplugins')
        access_code_prefix = request.get(self.accesscode_prefix_field, None)
        access_code_series = request.get(self.accesscode_series_field, None)
        access_code_no = request.get(self.accesscode_number_field, None)
        access_code = '%s-%s-%s' % (
            access_code_prefix, access_code_series, access_code_no)
        if None in [access_code_prefix, access_code_series, access_code_no]:
            access_code = None
        credentials = None

        if access_code:
            credentials = ApplicantCredentials(access_code)
        elif not sessionData:
            return None
        sessionData = session[
            'zope.pluggableauth.browserplugins']
        if credentials:
            sessionData['credentials'] = credentials
        else:
            credentials = sessionData.get('credentials', None)
        if not credentials:
            return None
        if not IApplicantSessionCredentials.providedBy(credentials):
            # If credentials were stored in session from another
            # credentials plugin then we cannot make assumptions about
            # its structure.
            return None
        return {'accesscode': credentials.getAccessCode()}



class ApplicantsAuthenticatorPlugin(grok.GlobalUtility):
    """Authenticate applicants.
    """
    grok.provides(IAuthenticatorPlugin)
    grok.name('applicants')

    def authenticateCredentials(self, credentials):
        """Validate the given `credentials`

        Credentials for applicants have to be passed as a regular
        dictionary with a key ``accesscode``. This access code is the
        password and username of an applicant.

        Returns a :class:`ApplicantPrincipalInfo` in case of
        successful validation, ``None`` else.

        Credentials are not valid if:

        - The passed accesscode does not exist (i.e. was not generated
          by the :mod:`waeup.sirp.accesscode` module).

        or

        - the accesscode was disabled

        or

        - the accesscode was already used and a dataset for this
          applicant was already generated with a different accesscode
          (currently impossible, as applicant datasets are indexed by
          accesscode)

        or

        - a dataset for the applicant already exists with an
          accesscode set and this accesscode does not match the given
          one.

        """
        if not isinstance(credentials, dict):
            return None
        accesscode = credentials.get('accesscode', None)
        if accesscode is None:
            return None
        applicant_data = get_applicant_data(accesscode)
        ac = get_access_code(accesscode) # Get the real access code object
        appl_ac = getattr(applicant_data, 'access_code', None)
        if ac is None:
            return None
        if ac.state == 'disabled':
            return None
        if ac.state == 'used' and appl_ac != ac.representation:
            return None
        # If the following fails we have a catalog error. Bad enough
        # to pull emergency break.
        assert appl_ac is None or appl_ac == ac.representation
        return ApplicantPrincipalInfo(accesscode)

    def principalInfo(self, id):
        """Returns an IPrincipalInfo object for the specified principal id.

        This method is used by the stadard PAU to lookup for instance
        groups. If a principal belongs to a group, the group is looked
        up by the id.  Currently we always return ``None``,
        indicating, that the principal could not be found. This also
        means, that is has no effect if applicant users belong to a
        certain group. They can not gain extra-permissions this way.
        """
        return None

class ApplicantsAuthUtility(grok.GlobalUtility):
    """A global utility that sets up any PAU passed.

    The methods of this utility are called during setup of a new site
    (`University`) instance and after the regular authentication
    systems (regular users, officers, etc.) were set up.
    """
    grok.implements(IAuthPluginUtility)
    grok.name('applicants_auth_setup')

    def register(self, pau):
        """Register our local applicants specific PAU components.

        Applicants provide their own authentication system resulting
        in a specialized credentials plugin and a specialized
        authenticator plugin.

        Here we tell a given PAU that these plugins exist and should
        be consulted when trying to authenticate a user.

        We stack our local plugins at end of the plugin list, so that
        other authentication mechanisms (the normal user
        authentication for instance) have precedence and to avoid
        "account-shadowing".
        """
        # The local credentials plugin is registered under the name
        # 'applicant_credentials' (see above).
        plugins = list(pau.credentialsPlugins) + ['applicant_credentials']
        pau.credentialsPlugins = tuple(plugins)
        # The local authenticator plugin is registered under the name
        # 'applicants' (subject to change?)
        plugins = list(pau.authenticatorPlugins) + ['applicants']
        pau.authenticatorPlugins = tuple(plugins)
        return pau

    def unregister(self, pau):
        """Unregister applicant specific authentication components from PAU.
        """
        pau.credentialsPlugins = tuple(
            [x for x in list(pau.credentialsPlugins)
             if x != 'applicant_credentials'])
        pau.authenticatorPlugins = tuple(
            [x for x in list(pau.authenticatorPlugins)
             if x != 'applicants'])
        return pau


def principal_id(access_code):
    """Get a principal ID for applicants.

    We need unique principal ids for appliants. As access codes must
    be unique we simply return them.
    """
    return access_code
