Introduction

When a task has been performed by the Launchpad web application, a notifiction message needs to appear on the subsequent page that is loaded to inform the user that the task has been performed, or if an error has occurred.

The naive solution to this problem is to simply stuff the message into the query component of the URL. However, this is trivially spoofable. We need an approch that is secure and a simple API for developers to make use of.

Rationale

Allowing people to create links to Launchpad pages containing arbitrary alert text is both embarassing and a vector for a number of attacks.

Further Details

Assumptions

Adding stuff to the query part of the URL is OK, provided we ensure there are no namespace conflicts.

We cannot reliably pass information to a particular browser window using the session.

Use Cases

Bob submits a form, with valid data, to add an external link to a Bug. The Bug overview page appears with a notice displayed at the top explaining that the link has been added.

Bob then emails the URL of the page he is on to Alice. Alice clicks on the url and goes to the bug overview page. The notice is not displayed.

Bob then clicks his browser's Reload button. The bug overview page reloads. The notice is not displayed.

Fred is impatient and often has multiple browser windows open to launchpad. The notices always appear in the correct browser window.

Alice submits a form, and Launchpad shunts her through several redirects before a document is returned to her. There is a notice at the top of the document saying that her form was submitted successfully.

Implementation Plan

Schema Changes

N/A

Data Migration

N/A

Code Changes

Extend the existing REQUEST and RESPONSE objects with Launchpad specific ones and have them implement the following interfaces:

   1 class PageNotificationLevel:
   2     DEBUG = 0  # a debugging message
   3     INFO = 10  # simple confirmation of a change
   4     NOTICE = 20 # the previous action had effects you might not have intended
   5     WARNING = 30 # the previous action will not be successful unless you ...
   6     ERROR = 40 # the previous action did not succeed, and why
   7 
   8     ALL_LEVELS = (DEBUG, INFO, NOTICE, WARNING, ERROR)
   9 
  10 class INotificationResponse(Interface):
  11     """This class is responsible for removing notifications that it assumes have been
  12     displayed to the user (ie. there was a lpnotifications in the request, but redirect
  13     has not been called). It is also responsible for checking all the notifications in the
  14     session and removing ones that are more than 30 minutes old.
  15     """
  16     def addNotification(msg, level=PageNotificationLevel.NOTICE, **kw):
  17         """Append the given message to the list of notifications
  18 
  19         msg may be an XHTML fragment suitable for inclusion in a block
  20         tag such as <div>. It may also contain standard Python string replacement
  21         markers to be filled out by the keyword arguments (ie. %(foo)s). The keyword
  22         arguments inserted this way are automatically HTML quoted.
  23 
  24         level is one of the PageNotificationLevels: DEBUG, INFO, NOTICE, WARNING, ERROR.
  25         """
  26     def addDebugNotification(msg, **kw):
  27         """Shortcut to addNotification(msg, DEBUG, **kw)"""
  28     [...]
  29     def addErrorNotification(msg, **kw):
  30         """Shortcut to addNotification(msg, ERROR, **kw)"""
  31     def redirect(...):
  32         """As per IHTTPResponse.redirect, except that notifications are preserved"""
  33 
  34 class INotificationRequest(Interface):
  35     def getNotifications(levels=PageNotificationLevel.ALL_LEVELS):
  36         """Retrieve a list of notifications, which are XHTML fragments.
  37         
  38         levels is a sequence of levels. Only the notifications with matching
  39         levels are returned. By default, all notifications are returned.
  40         Notifications are always returned in the order they were added.
  41 
  42         An empty list is returned if there are no matching notifications.
  43 
  44         An empty list is returned if there is no notification information
  45         in the query string, or if the notification parameter is invalid
  46         for the current user.
  47         """

Add a parameter to launchpad.conf detailing the minimum message level that is rendered to clients. Developers will typically have this set to DEBUG. Production and Staging servers will have this set to INFO. This allows developers to easily display debugging information (such as the ids of database rows inserted) and not have them rendered on the live systems.

We will render the messages from the main template by using a view on the request. This could, technically speaking, be a "resource" rather than a view. But, we aren't making much use of Zope 3 "resources" in Launchpad, so we'll just use a view. There will be one view for the standard request, and another for the debug layer request.

addNotification

If the level of the message is one we care about (to avoid writing to the session storage unnecessarily), the message is stuffed into the user's session using a class implementing the following interface:

   1 class ISessionNotification(Interface):
   2     created = Attribute('datetime.utcnow(tz=UTC) of when instantiated')
   3     notifications = Attribute('List of (level, msg) tuples')

The lpnotification query string paramter is used to identify the correct ISessionNotification instance for the current request. This query string paramter is documented in ReservedQueryParameters so should not cause conflicts. This parameter is used as a key into the launchpad.notifications session storage, where the ISessionNotification is stored so it persists across requests.

If the lpnotification parameter in the query string is set, then the UID is retrieved from it and used to extract the current ISessionNotification from the session. The message and level is appended to the notifications parameter of this instance.

If the lpnotification paramter in the query string is not set, then a UID is generated and a new ISessionNotification instance created (with the notifications attribute initialized with the level and message) and stuffed into the session.

   1 uid = '%s.%s.%s' % (time.time(), random.random(), thread.get_ident())
   2 notification = SessionNotification()
   3 notification.notifications.append((WARNING,'Oops'))
   4 session = ISession(request)['launchpad.notifications']
   5 session[uid] = notification

getNotifications

This method interogates the query string for the lpnotification parameter and returns the notifications from the session as per the getNotifications docstring given above.

Web app design changes

The main Launchpad template needs to be updated to render the list of notifications. INFO and NOTICE messages will be rendered identically, using the info icon (the extra granulatity gives us rendering flexibility for the future). WARNING messages will be presented with an exclamation mark icon, and ERROR messages with a "no entry" sign icon.

Existing code setting alerts or notifications will be tracked down and updated to the new API. Because the existing mechanisms are insecure, this should not be left until later.

Discussion

Unresolved Issues

Matthew is feeling paranoid and has suggested a whitelist for allowed HTML fragments in the notifications. This may be overkill, since the messages do not come from untrusted sources.

Steve thinks we should not expose the base addNotification method, and developers instead only use the shortcuts.

Questions and Answers

Q. How are "the existing mechanisms ... insecure"? They don't use query URLs.

  1. (SteveA) They aren't exactly insecure, provided the messages are rendered as quoted. However, they do allow for easy defacement, which can support social engineering attacks, and public ridicule and embarassment.

Reviewer comments

BrowserNotificationMessages (last edited 2009-07-29 16:04:59 by gary)