changeset 3922:586679a314f7

role checking for PGP mail and docs Erik's suggestion to allow the admin to specify a set of roles to perform PGP processing on seemed like a reasonable one I implemented it. There is a new config option to control it. I also realized that the signature verification had a slight problem: it was simply checking for a valid, known signature before continuing on. If another user in the keyring forged mail it was pass the PGP check and then modify the db as the forged user. I changed the logic to make sure that the author of the email matches the key of the verifying signature. As I was adding the documentation for the PGP processing I noticed that there were several other new-ish options that didn't appear in customizing.txt so I added them as well.
author Justus Pendleton <jpend@users.sourceforge.net>
date Wed, 26 Sep 2007 03:20:21 +0000
parents b49bbd4ff6ea
children d02aad94af5a
files doc/customizing.txt doc/installation.txt roundup/configuration.py roundup/mailgw.py
diffstat 4 files changed, 174 insertions(+), 31 deletions(-) [+]
line wrap: on
line diff
--- a/doc/customizing.txt	Wed Sep 26 03:07:55 2007 +0000
+++ b/doc/customizing.txt	Wed Sep 26 03:20:21 2007 +0000
@@ -2,7 +2,7 @@
 Customising Roundup
 ===================
 
-:Version: $Revision: 1.221 $
+:Version: $Revision: 1.222 $
 
 .. This document borrows from the ZopeBook section on ZPT. The original is at:
    http://www.zope.org/Documentation/Books/ZopeBook/current/ZPT.stx
@@ -95,6 +95,14 @@
   Path to the HTML templates directory. The path may be either absolute
   or relative to the directory containig this config file.
 
+ static_files -- default *blank*
+  Path to directory holding additional static files available via Web
+  UI.  This directory may contain sitewide images, CSS stylesheets etc.
+  and is searched for these files prior to the TEMPLATES directory
+  specified above.  If this option is not set, all static files are
+  taken from the TEMPLATES directory The path may be either absolute or
+  relative to the directory containig this config file.
+
  admin_email -- ``roundup-admin``
   Email address that roundup will complain to if it runs into trouble. If
   the email address doesn't contain an ``@`` part, the MAIL_DOMAIN defined
@@ -150,6 +158,9 @@
   your tracker. See the indexer source for the default list of
   stop-words (e.g. ``A,AND,ARE,AS,AT,BE,BUT,BY, ...``).
 
+ umask -- ``02``
+  Defines the file creation mode mask.
+
 Section **tracker**
  name -- ``Roundup issue tracker``
   A descriptive name for your roundup instance.
@@ -164,6 +175,11 @@
  email -- ``issue_tracker``
   Email address that mail to roundup should go to.
 
+ language -- default *blank*
+  Default locale name for this tracker. If this option is not set, the
+  language is determined by the environment variable LANGUAGE, LC_ALL,
+  LC_MESSAGES, or LANG, in that order of preference.
+
 Section **web**
  http_auth -- ``yes``
   Whether to use HTTP Basic Authentication, if present.
@@ -204,6 +220,13 @@
  password -- ``roundup``
   Database user password.
 
+ read_default_file -- ``~/.my.cnf``
+  Name of the MySQL defaults file. Only used in MySQL connections.
+
+ read_default_group -- ``roundup``
+  Name of the group to use in the MySQL defaults file. Only used in
+  MySQL connections.
+
 Section **logging**
  config -- default *blank*
   Path to configuration file for standard Python logging module. If this
@@ -277,6 +300,16 @@
   precedence. The path may be either absolute or relative to the directory
   containig this config file.
 
+ add_authorinfo -- ``yes``
+  Add a line with author information at top of all messages send by
+  roundup.
+
+ add_authoremail -- ``yes``
+  Add the mail address of the author to the author information at the
+  top of all messages.  If this is false but add_authorinfo is true,
+  only the name of the actor is added which protects the mail address
+  of the actor from being exposed at mail archives, etc.
+
 Section **mailgw**
  Roundup Mail Gateway options
 
@@ -294,6 +327,10 @@
   Default class to use in the mailgw if one isn't supplied in email subjects.
   To disable, leave the value blank.
 
+ language -- default *blank*
+  Default locale name for the tracker mail gateway.  If this option is
+  not set, mail gateway will use the language of the tracker instance.
+
  subject_prefix_parsing -- ``strict``
   Controls the parsing of the [prefix] on subject lines in incoming emails.
   ``strict`` will return an error to the sender if the [prefix] is not
@@ -319,6 +356,42 @@
   an issue for the interval after the issue's creation or last activity.
   The interval is a standard Roundup interval.
 
+ refwd_re -- ``(\s*\W?\s*(fw|fwd|re|aw|sv|ang)\W)+``
+  Regular expression matching a single reply or forward prefix
+  prepended by the mailer. This is explicitly stripped from the
+  subject during parsing.  Value is Python Regular Expression
+  (UTF8-encoded).
+
+ origmsg_re -- `` ^[>|\s]*-----\s?Original Message\s?-----$``
+  Regular expression matching start of an original message if quoted
+  the in body.  Value is Python Regular Expression (UTF8-encoded).
+
+ sign_re -- ``^[>|\s]*-- ?$``
+  Regular expression matching the start of a signature in the message
+  body.  Value is Python Regular Expression (UTF8-encoded).
+
+ eol_re -- ``[\r\n]+``
+  Regular expression matching end of line.  Value is Python Regular
+  Expression (UTF8-encoded).
+
+ blankline_re -- ``[\r\n]+\s*[\r\n]+``
+  Regular expression matching a blank line.  Value is Python Regular
+  Expression (UTF8-encoded).
+
+Section **pgp**
+ OpenPGP mail processing options
+
+ enable -- ``no``
+  Enable PGP processing. Requires pyme.
+
+ roles -- default *blank*
+  If specified, a comma-separated list of roles to perform PGP
+  processing on. If not specified, it happens for all users.
+
+ homedir -- default *blank*
+  Location of PGP directory. Defaults to $HOME/.gnupg if not
+  specified.
+
 Section **nosy**
  Nosy messages sending
 
@@ -349,6 +422,12 @@
   a separate email is sent to each recipient. If ``single`` then a single
   email is sent with each recipient as a CC address.
 
+ max_attachment_size -- ``2147483647``
+  Attachments larger than the given number of bytes won't be attached
+  to nosy mails. They will be replaced by a link to the tracker's
+  download page for the file.
+
+
 You may generate a new default config file using the ``roundup-admin
 genconfig`` command.
 
--- a/doc/installation.txt	Wed Sep 26 03:07:55 2007 +0000
+++ b/doc/installation.txt	Wed Sep 26 03:20:21 2007 +0000
@@ -2,7 +2,7 @@
 Installing Roundup
 ==================
 
-:Version: $Revision: 1.126 $
+:Version: $Revision: 1.127 $
 
 .. contents::
    :depth: 2
@@ -81,10 +81,17 @@
   proxy through a server with SSL support (e.g. apache) then this is
   unnecessary.
 
+pyme
+  If pyme_ is installed you can configure the mail gateway to perform
+  verification or decryption of incoming OpenPGP MIME messages. When
+  configured, you can require email to be cryptographically signed
+  before roundup will allow it to make modifications to issues.
+
 .. _Xapian: http://www.xapian.org/
 .. _pytz: http://www.python.org/pypi/pytz
 .. _Olson tz database: http://www.twinsun.com/tz/tz-link.htm
 .. _pyopenssl: http://pyopenssl.sourceforge.net
+.. _pyme: http://pyme.sourceforge.net
 
 
 Getting Roundup
--- a/roundup/configuration.py	Wed Sep 26 03:07:55 2007 +0000
+++ b/roundup/configuration.py	Wed Sep 26 03:20:21 2007 +0000
@@ -1,6 +1,6 @@
 # Roundup Issue Tracker configuration support
 #
-# $Id: configuration.py,v 1.48 2007-09-22 07:25:34 jpend Exp $
+# $Id: configuration.py,v 1.49 2007-09-26 03:20:21 jpend Exp $
 #
 __docformat__ = "restructuredtext"
 
@@ -720,6 +720,10 @@
     ("pgp", (
         (BooleanOption, "enable", "no",
             "Enable PGP processing. Requires pyme."),
+        (NullableOption, "roles", "",
+            "If specified, a comma-separated list of roles to perform\n"
+            "PGP processing on. If not specified, it happens for all\n"
+            "users."),
         (NullableOption, "homedir", "",
             "Location of PGP directory. Defaults to $HOME/.gnupg if\n"
             "not specified."),
--- a/roundup/mailgw.py	Wed Sep 26 03:07:55 2007 +0000
+++ b/roundup/mailgw.py	Wed Sep 26 03:20:21 2007 +0000
@@ -73,7 +73,7 @@
 an exception, the original message is bounced back to the sender with the
 explanatory message given in the exception.
 
-$Id: mailgw.py,v 1.191 2007-09-24 09:52:18 a1s Exp $
+$Id: mailgw.py,v 1.192 2007-09-26 03:20:21 jpend Exp $
 """
 __docformat__ = 'restructuredtext'
 
@@ -146,28 +146,69 @@
                 return rfc822.unquote(f[i+1:].strip())
     return None
 
-def check_pgp_sigs(sig):
-    ''' Theoretically a PGP message can have several signatures. GPGME returns
-        status on all signatures in a linked list. Walk that linked list making
-        sure all signatures are valid.
+def gpgh_key_getall(key, attr):
+    ''' return list of given attribute for all uids in
+        a key
+    '''
+    u = key.uids
+    while u:
+        yield getattr(u, attr)
+        u = u.next
+
+def gpgh_sigs(sig):
+    ''' more pythonic iteration over GPG signatures '''
+    while sig:
+        yield sig
+        sig = sig.next
+
+
+def iter_roles(roles):
+    ''' handle the text processing of turning the roles list
+        into something python can use more easily
+    '''
+    for role in [x.lower().strip() for x in roles.split(',')]:
+        yield role
+
+def user_has_role(db, userid, role_list):
+    ''' see if the given user has any roles that appear
+        in the role_list
     '''
-    while sig != None:
-        if not sig.summary & pyme.gpgme.GPGME_SIGSUM_VALID:
-            # try to narrow down the actual problem to give a more useful
-            # message in our bounce
-            if sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_MISSING:
-                raise MailUsageError, \
-                    _("Message signed with unknown key: %s") % sig.fpr
-            elif sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_EXPIRED:
-                raise MailUsageError, \
-                    _("Message signed with an expired key: %s") % sig.fpr
-            elif sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_REVOKED:
-                raise MailUsageError, \
-                    _("Message signed with a revoked key: %s") % sig.fpr
+    for role in iter_roles(db.user.get(userid, 'roles')):
+        if role in iter_roles(role_list):
+            return True
+    return False
+
+
+def check_pgp_sigs(sig, gpgctx, author):
+    ''' Theoretically a PGP message can have several signatures. GPGME
+        returns status on all signatures in a linked list. Walk that
+        linked list looking for the author's signature
+    '''
+    for sig in gpgh_sigs(sig):
+        key = gpgctx.get_key(sig.fpr, False)
+        # we really only care about the signature of the user who
+        # submitted the email
+        if key and (author in gpgh_key_getall(key, 'email')):
+            if sig.summary & pyme.gpgme.GPGME_SIGSUM_VALID:
+                return True
             else:
-                raise MailUsageError, \
-                    _("Invalid PGP signature detected.")
-        sig = sig.next
+                # try to narrow down the actual problem to give a more useful
+                # message in our bounce
+                if sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_MISSING:
+                    raise MailUsageError, \
+                        _("Message signed with unknown key: %s") % sig.fpr
+                elif sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_EXPIRED:
+                    raise MailUsageError, \
+                        _("Message signed with an expired key: %s") % sig.fpr
+                elif sig.summary & pyme.gpgme.GPGME_SIGSUM_KEY_REVOKED:
+                    raise MailUsageError, \
+                        _("Message signed with a revoked key: %s") % sig.fpr
+                else:
+                    raise MailUsageError, \
+                        _("Invalid PGP signature detected.")
+
+    # we couldn't find a key belonging to the author of the email
+    raise MailUsageError, _("Message signed with unknown key: %s") % sig.fpr
 
 class Message(mimetools.Message):
     ''' subclass mimetools.Message so we can retrieve the parts of the
@@ -349,7 +390,7 @@
         return self.gettype() == 'multipart/encrypted' \
             and self.typeheader.find('protocol="application/pgp-encrypted"') != -1
 
-    def decrypt(self):
+    def decrypt(self, author):
         ''' decrypt an OpenPGP MIME message
             This message must be signed as well as encrypted using the "combined"
             method. The decrypted contents are returned as a new message.
@@ -375,7 +416,7 @@
         # key to send it to us. now check the signatures to see if it
         # was signed by someone we trust
         result = context.op_verify_result()
-        check_pgp_sigs(result.signatures)
+        check_pgp_sigs(result.signatures, context, author)
 
         plaintext.seek(0,0)
         # pyme.core.Data implements a seek method with a different signature
@@ -386,7 +427,7 @@
         c.seek(0)
         return Message(c)
 
-    def verify_signature(self):
+    def verify_signature(self, author):
         ''' verify the signature of an OpenPGP MIME message
             This only handles detached signatures. Old style
             PGP mail (i.e. '-----BEGIN PGP SIGNED MESSAGE----')
@@ -419,7 +460,7 @@
 
         # check all signatures for validity
         result = context.op_verify_result()
-        check_pgp_sigs(result.signatures)
+        check_pgp_sigs(result.signatures, context, author)
 
 class MailGW:
 
@@ -1133,19 +1174,31 @@
 
         # if they've enabled PGP processing then verify the signature
         # or decrypt the message
-        if self.instance.config.PGP_ENABLE:
+
+        # if PGP_ROLES is specified the user must have a Role in the list
+        # or we will skip PGP processing
+        def pgp_role():
+            if self.instance.config.PGP_ROLES:
+                return user_has_role(self.db, author,
+                    self.instance.config.PGP_ROLES)
+            else:
+                return True
+
+        if self.instance.config.PGP_ENABLE and pgp_role():
             assert pyme, 'pyme is not installed'
+            # signed/encrypted mail must come from the primary address
+            author_address = self.db.user.get(author, 'address')
             if self.instance.config.PGP_HOMEDIR:
                 os.environ['GNUPGHOME'] = self.instance.config.PGP_HOMEDIR
             if message.pgp_signed():
-                message.verify_signature()
+                message.verify_signature(author_address)
             elif message.pgp_encrypted():
                 # replace message with the contents of the decrypted
                 # message for content extraction
                 # TODO: encrypted message handling is far from perfect
                 # bounces probably include the decrypted message, for
                 # instance :(
-                message = message.decrypt()
+                message = message.decrypt(author_address)
             else:
                 raise MailUsageError, _("""
 This tracker has been configured to require all email be PGP signed or

Roundup Issue Tracker: http://roundup-tracker.org/