This is probably out of date. This does not mention the session database (which is now in postgres, not zodb AFAIK), and the pagetests are obsolete too. -- DavidAllouche 2007-11-14 16:57:10
Authentication in Launchpad
For most uses, Launchpad uses cookie authentication.
The cookie authentication details are stored client-side in a cookie. The cookie contains a cryptographically something token.
The token is matched up to data inside an in-memory ZODB on the server.
Launchpad also supports http basic authentication. This is what we use in page tests, as the authentication can be given one request at a time.
The +login page is in the file templates/launchpad-login.pt. It is registered as a view on the RootObject, and uses the helper class canonical.launchpad.webapp.login.LoginOrRegister.
The page offers the user the ability to log in, to go to the pages to reset a forgotten password, or to register with the system as a new user.
The /+login page at the root tells you that you have successfully logged in, when you have done so. All other +login pages immediately redirectly you to the URL with "/+login" removed when you successfully log in.
You can add /+login to the end of any URL in the system. Doing so makes the object publisher skip all of the normal traversal mechanisms, including setting new layers, to show the login page. However, virtual hosting works as usual.
The /+logout page is available only at the root. Its template is templates/launchpad-logout.pt. It uses the class canonical.launchpad.webapp.login.CookieLogoutPage.
The "403 Forbidden" page is in templates/launchpad-forbidden.pt.
It is shown when you are logged in, but try to do something that you do not have permission to do.
In the future, this page will be able to tell you what you need to do to get permission to see the page. For example, "join the administration team for this product, or the team of owners of this product".
View on the Unauthorized exception
The logic to choose whether to offer login or show the forbidden page is in the view on the Unauthorized exception. This view needs to be hooked up by two different ZCML directives: one for the default layer and one for the debug layer. In either case, the same class handles this: canonical.launchpad.webapp.login.UnauthorizedView.
Magic in the object publisher
When the publisher gets a request where the URL ends in /+login, it uses a different root object than the usual one. The usual root object is in canonical.publication.RootObject. The special one is in canonical.publication.LoginRoot.
The special LoginRoot instance provides IPublishTraverse, and returns itself for each traversal, until the requests traversal stack is empty. Then, it looks up the view with whatever name is being requested on the regular RootObject, and returns that.
The LoginRoot will itself support looking up any name that is available on the RootObject. However, in practise it will only look up "+login" because this is the only special case where the LoginRoot is used instead of the regular RootObject.
The choice of which root object to return is made in canonical.publication.BrowserPublisher.getApplication. It makes the decision by looking at the bottom of the request's traversal stack (that is, the end of the URL, or the start of the traversal stack's representation as a Python list), to see if it is ['+login']. It would be possible to add other names here, if there is a reason to do so in the future.
Writing page tests
Use the standard basic auth header to log in.
If you go to a page unauthenticated, and that page requires a login, then you'll get a redirect to the URL of that page with '/+login' added to the end. This redirect is a 303 See Other, with the new URL in a Location header.
If you go to a page logged in as a user, and that page requires permissions you don't have, you'll get a 403 Forbidden page in return. The page will have some text on it, but you don't need to worry about exactly what it says. The important thing is that it is a 403.
Here's an example from a malone test where an unauthenticated request gets redirected to the login page. Note the HTTP/1.1 303 See Other line and the Location: .../+login HTTP header.
>>> print http(r""" ... GET /malone/bugs/7 HTTP/1.1 ... Accept-Language: en-ca,en-us;q=0.8,en;q=0.5,fr-ca;q=0.3 ... """) HTTP/1.1 303 See Other ... Location: http://localhost:9000/malone/bugs/7/+index/+login <BLANKLINE>
Here's an example where a logged-in user "Foo Bar" is not allowed to access a particular page. Note the Authorization: Basic ... header in the request, and the HTTP/1.1 403 Forbidden in the response.
Foo Bar cannot access task #16, because it's filed on a private bug on which Foo Bar is not an explicit subscriber. >>> print http(r""" ... GET /malone/tasks/16/+edit HTTP/1.1 ... Accept-Language: en-ca,en-us;q=0.8,en;q=0.5,fr-ca;q=0.3 ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q= ... """) HTTP/1.1 403 Forbidden ...
- When we want to use more than one server, we'll need to use zeo, and sync() the connections, and probably use disk backed storage.
- We may want to make basic auth available only on the debug layer.