# HG changeset patch # User Paul Boddie # Date 1387586934 -3600 # Node ID 799c7b86719f1643a7e54e65d6f53b9df63996d7 # Parent f4c4380dc038878adc6aab6a9849f4b6cc766e44 Added a user actions mapping that defines the actions allowed for a user and the pages on which such actions can be performed. diff -r f4c4380dc038 -r 799c7b86719f MoinMessage.py --- a/MoinMessage.py Tue Dec 17 14:11:38 2013 +0100 +++ b/MoinMessage.py Sat Dec 21 01:48:54 2013 +0100 @@ -27,6 +27,9 @@ def to_store(message): return message.get("Update-Action") == "store" +def get_update_action(message): + return message.get("Update-Action", "update") + class Message: "An update message." diff -r f4c4380dc038 -r 799c7b86719f MoinMessageSupport.py --- a/MoinMessageSupport.py Tue Dec 17 14:11:38 2013 +0100 +++ b/MoinMessageSupport.py Sat Dec 21 01:48:54 2013 +0100 @@ -13,6 +13,7 @@ from MoinSupport import getHeader, getMetadata, getWikiDict, writeHeaders, \ parseDictEntry from ItemSupport import ItemStore +from TokenSupport import getIdentifiers from MoinMessage import GPG, Message, MoinMessageError, \ MoinMessageMissingPart, MoinMessageBadContent, \ is_signed, is_encrypted, getContentAndSignature @@ -228,6 +229,32 @@ return homedir + def can_perform_action(self, action): + + """ + Determine whether the user in the request has the necessary privileges + to change the current page using a message requesting the given + 'action'. + """ + + for identifier in get_update_actions_for_user(self.request): + + # Expect "action:pagename", rejecting ill-formed identifiers. + + details = identifier.split(":", 1) + if len(details) != 2: + continue + + # If the action and page name match, return success. + + permitted, pagename = details + if permitted.lower() == action.lower() and pagename == self.page.page_name: + return True + + return False + +# More specific errors. + class MoinMessageRecipientError(MoinMessageError): pass @@ -240,6 +267,8 @@ class MoinMessageBadRecipient(MoinMessageRecipientError): pass +# Utility functions. + def get_homedir(request): "Locate the GPG home directory." @@ -302,20 +331,42 @@ 'fingerprint' or None if no correspondence is present in the mapping page. """ + # Since this function must be able to work before any user has been + # identified, the wikidict operation uses superuser privileges. + gpg_users = getWikiDict( getattr(request.cfg, "moinmessage_gpg_users_page", "MoinMessageUserDict"), request, - superuser=True # disable user test because we have no user yet + superuser=True ) - # With a user mapping and a fingerprint corresponding to a known - # user, temporarily switch user in order to make the edit. - if gpg_users and gpg_users.has_key(fingerprint): return gpg_users[fingerprint] else: return None +def get_update_actions_for_user(request): + + """ + For the user associated with the 'request', return the permitted actions for + the user in the form of + """ + + if not request.user or not request.user.valid: + return [] + + actions = getWikiDict( + getattr(request.cfg, "moinmessage_user_actions_page", "MoinMessageUserActionsDict"), + request + ) + + username = request.user.name + + if actions and actions.has_key(username): + return getIdentifiers(actions[username]) + else: + return [] + def get_recipient_details(request, recipient, main=False, fetching=False): """ diff -r f4c4380dc038 -r 799c7b86719f README.txt --- a/README.txt Tue Dec 17 14:11:38 2013 +0100 +++ b/README.txt Sat Dec 21 01:48:54 2013 +0100 @@ -175,6 +175,38 @@ added to this mapping and specify the same relaying user; there is no restriction on each fingerprint needing to map to a different user. +The Username-to-Actions Mapping +------------------------------- + +Each user may have a set of permitted actions defined for them so that they +may perform these actions by sending an incoming message to the wiki. This +mapping is typically defined by the MoinMessageUserActionsDict page as a +WikiDict having the following general format: + + username:: permitted-action ... + +To add content to a page, an entry of the following form would be used: + + username:: Update:SomePage + +Similarly, to allow an incoming message to replace a page's content, the +following would be used: + + username:: Replace:SomePage + +And to be able to add messages to a page's message store, the following would +be used: + + username:: Store:SomePage + +Multiple actions can be given in a space-separated list, with shell-like +quoting used for names containing spaces (and quote characters). For example: + + username:: Store:"Some user's special page" + +Without an entry in this mapping, messages may not perform content +modification or storage actions in the wiki. + The Username-to-Signing-Key Mapping ----------------------------------- diff -r f4c4380dc038 -r 799c7b86719f actions/PostMessage.py --- a/actions/PostMessage.py Tue Dec 17 14:11:38 2013 +0100 +++ b/actions/PostMessage.py Sat Dec 21 01:48:54 2013 +0100 @@ -9,7 +9,7 @@ from MoinMoin.Page import Page from MoinMoin.PageEditor import PageEditor from MoinSupport import getMetadata, writeHeaders -from MoinMessage import is_collection, to_replace, to_store +from MoinMessage import is_collection, to_replace, to_store, get_update_action from MoinMessageSupport import MoinMessageAction Dependencies = ['pages'] @@ -26,22 +26,28 @@ # Handle each update. + all_successful = True + for update in message.updates: # Handle a single part. if not is_collection(update): - self.handle_message_parts([update], update) + all_successful = all_successful and self.handle_message_parts([update], update) # Or a collection of alternative representations for a single # update. else: - self.handle_message_parts(update.get_payload(), update) + all_successful = all_successful and self.handle_message_parts(update.get_payload(), update) # Default output. - writeHeaders(request, "text/plain", getMetadata(self.page), "204 No Content") + writeHeaders(request, "text/plain", getMetadata(self.page), "200 OK") + if all_successful: + request.write("All updates were successful.") + else: + request.write("Some updates were unsuccessful.") def handle_message_parts(self, parts, update): @@ -53,6 +59,13 @@ request = self.request + # Test for privileges to change the page or message store. + + update_action = get_update_action(update) + + if not self.can_perform_action(update_action): + return False + # Handle the different update actions. # Update a message store for the page. @@ -95,6 +108,8 @@ self.page = Page(request, self.pagename) + return True + # Action function. def execute(pagename, request):