==================================
SchoolBell security infrastructure
==================================

General design
--------------

The security system of SchoolBell consists of:

 * SchoolBellAuthenticationUtility
 * views for login/logout
 * a view for editing the ACL

The login/logout views store/reset the authentication data in the
session.  The authentication utility authenticates the request
according to the data stored in the session.

The ACL view is a facade for the local grants mechanism, allowing the
admin to set permissions on individual objects for persons and groups.


SchoolBell authentication utility
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


(Handwaving...)

    >>> from zope.app.tests import setup
    >>> root = setup.placefulSetUp(True)
    >>> from schoolbell.relationship.tests import setUpRelationships
    >>> setUpRelationships()

    Global auth utility:

    >>> from zope.app.security.principalregistry import principalRegistry
    >>> from zope.app.security.interfaces import IAuthentication
    >>> from zope.app.tests import ztapi
    >>> ztapi.provideUtility(IAuthentication, principalRegistry)

    Session setup:

    >>> from zope.app.session.session import ClientId, Session
    >>> from zope.app.session.session import PersistentSessionDataContainer
    >>> from zope.publisher.interfaces import IRequest
    >>> from zope.app.session.http import CookieClientIdManager
    >>> from zope.app.session.interfaces import ISessionDataContainer
    >>> from zope.app.session.interfaces import IClientId
    >>> from zope.app.session.interfaces import IClientIdManager, ISession
    >>> ztapi.provideAdapter(IRequest, IClientId, ClientId)
    >>> ztapi.provideAdapter(IRequest, ISession, Session)
    >>> ztapi.provideUtility(IClientIdManager, CookieClientIdManager())
    >>> sdc = PersistentSessionDataContainer()
    >>> ztapi.provideUtility(ISessionDataContainer, sdc, 'schoolbell.auth')


SchoolBell is a possible site:

    >>> from zope.interface.verify import verifyObject
    >>> from zope.app.component.interfaces import IPossibleSite, ISite

    >>> from schoolbell.app.app import SchoolBellApplication
    >>> app = SchoolBellApplication()
    >>> verifyObject(IPossibleSite, app)
    True
    >>> verifyObject(ISite, app)
    Traceback (most recent call last):
    ...
    DoesNotImplement: ...
    ...


The SchoolBellAuthenticationUtility is an IAuthentication utility that
lives in that site:

    >>> from schoolbell.app.security import SchoolBellAuthenticationUtility
    >>> from schoolbell.app.interfaces import ISchoolBellAuthentication
    >>> auth = SchoolBellAuthenticationUtility()
    >>> verifyObject(IAuthentication, auth)
    True
    >>> verifyObject(ISchoolBellAuthentication, auth)
    True

Let's provide the location of auth:

    >>> root['frogpond'] = app
    >>> from schoolbell.app.security import setUpLocalAuth
    >>> setUpLocalAuth(app, auth)
    >>> from zope.app.component.hooks import setSite
    >>> setSite(app)

Now, we get our local authentication service:

    >>> from zope.app import zapi
    >>> zapi.getUtility(IAuthentication, context=app) is auth
    True
    >>> verifyObject(ISite, app)
    True

getPrincipal
------------

The utility knows about the users of the application:

    >>> from schoolbell.app.app import Person
    >>> from zope.app.security.interfaces import IPrincipal
    >>> person = Person(username=u"frog", title="Frog")
    >>> app['persons']['frog'] = person

    >>> principal = auth.getPrincipal('sb.person.frog')
    >>> verifyObject(IPrincipal, principal)
    True
    >>> principal.title
    'Frog'

The utility delegates to the next service if the principal is not
found:

    >>> p = principalRegistry.definePrincipal('zope.manager', 'Mgmt', '',
    ...                                       'gandalf', '123')

    >>> p1 = auth.getPrincipal('zope.manager')
    >>> p == p1
    True
    >>> p.title
    'Mgmt'


If the principal we are looking up does not exist, an exception is
raised:

    >>> auth.getPrincipal('sb.person.nonexistent')
    Traceback (most recent call last):
    ...
    PrincipalLookupError: 'sb.person.nonexistent'


The groups of the SchoolBell instance are also principals:

    >>> from schoolbell.app.app import Group
    >>> group = Group()
    >>> group.title = "The Management"
    >>> app['groups']['management'] = group

    >>> g1 = auth.getPrincipal('sb.group.management')
    >>> g1.title
    'The Management'
    >>> group.members.add(person)

And the user principal has a list ids of group principals he belongs
to.  This list is used by the Zope Security Policy:

    >>> from zope.security.interfaces import IGroupAwarePrincipal
    >>> p = auth.getPrincipal('sb.person.frog')
    >>> verifyObject(IGroupAwarePrincipal, p)
    True
    >>> p.groups
    [u'sb.group.management']

If the global principals like IEveryoneGroup, IAuthenticatedGroup are
defined, they are added to the list of groups of a principal, too:

    >>> from zope.app.security.interfaces import IAuthenticatedGroup
    >>> from zope.app.security.interfaces import IEveryoneGroup
    >>> from zope.app.security.principalregistry import AuthenticatedGroup
    >>> from zope.app.security.principalregistry import EverybodyGroup

    >>> authenticated = AuthenticatedGroup('zope.authenticated', '', '')
    >>> ztapi.provideUtility(IAuthenticatedGroup, authenticated)
    >>> everyone = EverybodyGroup('zope.everybody', 'All users', '')
    >>> ztapi.provideUtility(IEveryoneGroup, everyone)

    >>> p = auth.getPrincipal('sb.person.frog')
    >>> p.groups
    [u'sb.group.management', 'zope.authenticated', 'zope.everybody']


authenticate
------------

When no credentials are provided in the session, authenticate returns None:

    >>> from zope.publisher.browser import TestRequest
    >>> request = TestRequest()
    >>> auth.authenticate(request)

Suppose, the user 'frog' has a password:

    >>> app['persons']['frog'].setPassword('pond')

The frog has authenticated itself in the login form and the
credentials are stored in the session:

    >>> auth.setCredentials(request, 'frog', 'shmond')
    Traceback (most recent call last):
    ...
    ValueError: bad credentials
    >>> auth.setCredentials(request, 'snake', 'badgermushroom')
    Traceback (most recent call last):
    ...
    ValueError: bad credentials
    >>> auth.setCredentials(request, 'frog', 'pond')

Now, it is authenticated by our utility:

    >>> principal = auth.authenticate(request)
    >>> verifyObject(IGroupAwarePrincipal, principal)
    True
    >>> principal.id
    'sb.person.frog'

Our principal is adaptable to IPerson:

    >>> from schoolbell.app.interfaces import IPerson
    >>> from zope.security.proxy import removeSecurityProxy
    >>> removeSecurityProxy(IPerson(principal)) is  app['persons']['frog']
    True

The credentials can be cleared from the session:

    >>> auth.clearCredentials(request)
    >>> auth.authenticate(request)

If there are no credentials set, clearing does not fail:

    >>> auth.clearCredentials(request)

Also, if cookie based authentication fails, we support HTTP basic
authentication (by trying to adapt the request to ILoginPassword):

    >>> from zope.interface import Interface, directlyProvides, implements
    >>> from zope.app.security.interfaces import ILoginPassword

    >>> class Adapter:
    ...     implements(ILoginPassword)
    ...     def __init__(self, context):
    ...         self.context = context
    ...     def getLogin(self):
    ...         return "frog"
    ...     def getPassword(self):
    ...         return "pond"
    ...     def needLogin(self, realm):
    ...         self.context.unauthorized("basic realm=%s" % realm)
    ...
    >>> class IFrogMarker(Interface): pass
    >>> ztapi.provideAdapter(IFrogMarker, ILoginPassword, Adapter)
    >>> request = TestRequest()
    >>> directlyProvides(request, IFrogMarker)

    >>> auth.authenticate(request).id
    'sb.person.frog'


unauthorized
------------

Our view issues the authorization challenge by redirecting to the
login page:

    >>> request = TestRequest()
    >>> auth.unauthorized(None, request)
    >>> request.response.getStatus()
    302
    >>> request.response.getHeader('Location')
    'http://127.0.0.1/frogpond/@@login.html?forbidden=yes&nexturl=http://127.0.0.1'

However it does that only for regular browser requests.  Other kinds of
requests issue the default challenge by setting the response code to 401
(Unauthorized).

    >>> from zope.publisher.tests import httprequest
    >>> request = httprequest.TestRequest()
    >>> directlyProvides(request, IFrogMarker)
    >>> auth.unauthorized(None, request)
    >>> request.response.getStatus()
    401

HTTP PUT, does so likewise, even though it uses a real BrowserRequest.

    >>> request = TestRequest()
    >>> request.method = 'PUT'
    >>> directlyProvides(request, IFrogMarker)
    >>> auth.unauthorized(None, request)
    >>> request.response.getStatus()
    401

And, since this is not a perfect world, we need a workaround for Mozilla
Calendar -- HTTP GET requests for .ics files also issue the HTTP Basic
authentication challenge.

    >>> request = TestRequest()
    >>> request.getURL = lambda *a: '/dir/calendar.ics'
    >>> str(request.URL)
    '...calendar.ics'
    >>> directlyProvides(request, IFrogMarker)
    >>> auth.unauthorized(None, request)
    >>> request.response.getStatus()
    401


Clean up:

    >>> setup.placefulTearDown()
