paul@35 | 1 | # -*- coding: iso-8859-1 -*- |
paul@35 | 2 | """ |
paul@35 | 3 | MoinMoin - PGP authentication using incoming MIME request bodies |
paul@35 | 4 | |
paul@36 | 5 | @copyright: 2001-2003 Juergen Hermann <jh@web.de> |
paul@35 | 6 | 2003-2006 MoinMoin:ThomasWaldmann |
paul@35 | 7 | 2007 MoinMoin:JohannesBerg |
paul@35 | 8 | 2013 Paul Boddie <paul@boddie.org.uk> |
paul@35 | 9 | @license: GNU GPL, see COPYING for details. |
paul@35 | 10 | """ |
paul@35 | 11 | |
paul@35 | 12 | from MoinMoin import log |
paul@35 | 13 | logging = log.getLogger(__name__) |
paul@35 | 14 | |
paul@35 | 15 | from MoinMoin.user import User |
paul@35 | 16 | from MoinMoin.auth import BaseAuth |
paul@35 | 17 | from MoinSupport import getHeader, getWikiDict |
paul@35 | 18 | from MoinMessage import GPG, is_signed, is_encrypted, \ |
paul@35 | 19 | MoinMessageDecodingError, MoinMessageError |
paul@35 | 20 | from MoinMessageSupport import get_homedir |
paul@35 | 21 | from email.parser import Parser |
paul@35 | 22 | |
paul@35 | 23 | try: |
paul@35 | 24 | from cStringIO import StringIO |
paul@35 | 25 | except ImportError: |
paul@35 | 26 | from StringIO import StringIO |
paul@35 | 27 | |
paul@35 | 28 | class PGPAuth(BaseAuth): |
paul@35 | 29 | |
paul@35 | 30 | """ |
paul@35 | 31 | Authenticate a user by inspecting the signature on a signed MIME message |
paul@35 | 32 | sent in the body of a request. |
paul@35 | 33 | """ |
paul@35 | 34 | |
paul@35 | 35 | name = "pgp" |
paul@35 | 36 | |
paul@35 | 37 | def __init__(self, autocreate=False): |
paul@35 | 38 | |
paul@35 | 39 | "Initialise the authenticator using the given 'autocreate' setting." |
paul@35 | 40 | |
paul@35 | 41 | self.autocreate = autocreate |
paul@35 | 42 | BaseAuth.__init__(self) |
paul@35 | 43 | |
paul@35 | 44 | def request(self, request, user_obj, **kw): |
paul@35 | 45 | |
paul@35 | 46 | """ |
paul@35 | 47 | Evaluate the 'request' given an existing 'user_obj', returning a tuple |
paul@35 | 48 | of the form (user, continue) where the user is either the supplied |
paul@35 | 49 | 'user_obj' or a new user object, and where an indication of whether to |
paul@35 | 50 | continue the authentication process is also given. |
paul@35 | 51 | """ |
paul@35 | 52 | |
paul@35 | 53 | _ = request.getText |
paul@35 | 54 | user = None |
paul@35 | 55 | |
paul@35 | 56 | # Always revalidate the identity if the provided user claims to be |
paul@35 | 57 | # identified using this method. |
paul@35 | 58 | |
paul@35 | 59 | if user_obj and user_obj.auth_method == self.name: |
paul@35 | 60 | user_obj = None |
paul@35 | 61 | |
paul@35 | 62 | # If something else authenticated before us, accept this. |
paul@35 | 63 | |
paul@35 | 64 | if user_obj: |
paul@35 | 65 | return user_obj, True |
paul@35 | 66 | |
paul@35 | 67 | logging.debug("request: %r" % request) |
paul@35 | 68 | |
paul@35 | 69 | # Read the request body and perform signature validation. |
paul@35 | 70 | |
paul@35 | 71 | # Need to obtain the message text and yet still make it available |
paul@35 | 72 | # normally in the request. |
paul@35 | 73 | |
paul@35 | 74 | content_length = getHeader(request, "Content-Length", "HTTP") |
paul@35 | 75 | if content_length: |
paul@35 | 76 | content_length = int(content_length) |
paul@35 | 77 | message_body = message_text = request.read(content_length) |
paul@35 | 78 | |
paul@35 | 79 | # Obtain a message from the text. |
paul@35 | 80 | |
paul@35 | 81 | message = Parser().parse(StringIO(message_text)) |
paul@35 | 82 | |
paul@35 | 83 | try: |
paul@35 | 84 | homedir = get_homedir(request) |
paul@35 | 85 | gpg = GPG(homedir) |
paul@35 | 86 | |
paul@35 | 87 | try: |
paul@35 | 88 | # Encrypted messages must be decrypted first. |
paul@35 | 89 | |
paul@35 | 90 | if is_encrypted(message): |
paul@35 | 91 | message_text = gpg.decryptMessage(message) |
paul@35 | 92 | message = Parser().parse(StringIO(message_text)) |
paul@35 | 93 | |
paul@35 | 94 | # Signed messages can be handled directly. |
paul@35 | 95 | |
paul@35 | 96 | if not is_signed(message): |
paul@35 | 97 | return user_obj, True |
paul@35 | 98 | |
paul@35 | 99 | fingerprint, identity, content = gpg.verifyMessage(message) |
paul@35 | 100 | |
paul@35 | 101 | except MoinMessageDecodingError: |
paul@35 | 102 | logging.info("Incoming message was improperly encoded.") |
paul@35 | 103 | return user_obj, True |
paul@35 | 104 | |
paul@35 | 105 | except MoinMessageError: |
paul@35 | 106 | logging.warning("Incoming message was not verifiable.") |
paul@35 | 107 | return user_obj, True |
paul@35 | 108 | |
paul@35 | 109 | # Restore the request body using an extension to the request and |
paul@35 | 110 | # replaced methods. |
paul@35 | 111 | |
paul@35 | 112 | finally: |
paul@35 | 113 | extension = RequestExtension(request, StringIO(message_body)) |
paul@35 | 114 | request.read = extension.read |
paul@35 | 115 | request._setup_args_from_cgi_form = extension._setup_args_from_cgi_form |
paul@35 | 116 | |
paul@35 | 117 | # Evaluate the result of the verification process. |
paul@35 | 118 | |
paul@35 | 119 | if fingerprint: |
paul@35 | 120 | gpg_users = getWikiDict( |
paul@35 | 121 | getattr(request.cfg, "moinmessage_gpg_users_page", "MoinMessageUserDict"), |
paul@35 | 122 | request, |
paul@35 | 123 | superuser=True # disable user test because we have no user yet |
paul@35 | 124 | ) |
paul@35 | 125 | |
paul@35 | 126 | # With a user mapping and a fingerprint corresponding to a known |
paul@35 | 127 | # user, temporarily switch user in order to make the edit. |
paul@35 | 128 | |
paul@35 | 129 | if gpg_users and gpg_users.has_key(fingerprint): |
paul@35 | 130 | username = gpg_users[fingerprint] |
paul@35 | 131 | user = User(request, auth_method="pgp", auth_username=username) |
paul@35 | 132 | logging.debug("username: %r" % username) |
paul@35 | 133 | |
paul@35 | 134 | logging.debug("user: %r" % user) |
paul@35 | 135 | |
paul@35 | 136 | # Handle user autocreation. |
paul@35 | 137 | |
paul@35 | 138 | if user and self.autocreate: |
paul@35 | 139 | logging.debug("autocreating user") |
paul@35 | 140 | user.create_or_update() |
paul@35 | 141 | |
paul@35 | 142 | # Either return the identified user, if valid. |
paul@35 | 143 | |
paul@35 | 144 | if user and user.valid: |
paul@35 | 145 | logging.debug("returning valid user %r" % user) |
paul@35 | 146 | return user, True # True to get other methods called, too |
paul@35 | 147 | |
paul@35 | 148 | # Or return the supplied user object. |
paul@35 | 149 | |
paul@35 | 150 | else: |
paul@35 | 151 | logging.debug("returning %r" % user_obj) |
paul@35 | 152 | return user_obj, True |
paul@35 | 153 | |
paul@35 | 154 | # Replacement request methods (based on the request_cgi.Request class). |
paul@35 | 155 | |
paul@35 | 156 | class RequestExtension: |
paul@35 | 157 | |
paul@35 | 158 | "An extension to the request providing replacement methods." |
paul@35 | 159 | |
paul@35 | 160 | def __init__(self, request, body): |
paul@35 | 161 | self.request = request |
paul@35 | 162 | self.body = body |
paul@35 | 163 | |
paul@35 | 164 | def _setup_args_from_cgi_form(self): |
paul@35 | 165 | """ Override to create cgi form """ |
paul@35 | 166 | form = cgi.FieldStorage(fp=self.body, environ=self.request.env, keep_blank_values=1) |
paul@35 | 167 | return RequestBase._setup_args_from_cgi_form(self.request, form) |
paul@35 | 168 | |
paul@35 | 169 | def read(self, n): |
paul@35 | 170 | """ Read from input stream. """ |
paul@35 | 171 | if n is None: |
paul@35 | 172 | logging.warning("calling request.read(None) might block") |
paul@35 | 173 | return self.body.read() |
paul@35 | 174 | else: |
paul@35 | 175 | return self.body.read(n) |
paul@35 | 176 | |
paul@35 | 177 | # vim: tabstop=4 expandtab shiftwidth=4 |