Thomas Waldmann  committed 3488b0e

forward port moin 1.9 password reset notification code, templates, docs

* add sphinx docs about password resetting
* fix RECOVERPASS_KEY KeyError if users tries to use same token twice
* add moin account-password options for mail subject and text, skipping
of accounts with already invalidated passwords
* change order of input fields in password recovery form, so the browser
will be able to find the right username/password fields to remember

  • Participants
  • Parent commits 1501cd1

Comments (0)

Files changed (10)

File MoinMoin/config/

             SHOW_COMMENTS: False,
             WANT_TRIVIAL: False,
             ENC_PASSWORD: u'',  # empty value == invalid hash
+            RECOVERPASS_KEY: u'',  # empty value == invalid key
             SESSION_KEY: u'',  # empty value == invalid key
             DISABLED: False,
             BOOKMARKS: {},

File MoinMoin/script/account/

+import sys
+from flask import current_app as app
 from flask.ext.script import Command, Option
 from MoinMoin.constants.keys import ITEMID, NAME, NAME_EXACT, EMAIL
     """raised if no such user exists"""
+class UserHasNoEMail(Fault):
+    """raised if user has no e-mail address in his profile"""
 class MailFailed(Fault):
     """raised if e-mail sending failed"""
-def set_password(uid, password, notify=False):
+def set_password(uid, password, notify=False, skip_invalid=False, subject=None, text=None):
     u = user.User(uid)
     if u and u.exists():
+        if skip_invalid and u.has_invalidated_password():
+            return
-        if notify and not u.disabled and
-            mailok, msg = u.mail_password_recovery()
+        if not
+            raise UserHasNoEMail('User profile does not have an E-Mail address (name: %r id: %r)!' % (,
+        if notify and not u.disabled:
+            mailok, msg = u.mail_password_recovery(subject=subject, text=text)
             if not mailok:
                 raise MailFailed(msg)
                help='Notify user(s), send them an E-Mail with a password reset link.'),
         Option('--verbose', '-v', required=False, dest='verbose', action='store_true', default=False,
                help='Verbose operation'),
+        Option('--subject', required=False, dest='subject', type=unicode,
+               help='Subject text for the password reset notification E-Mail.'),
+        Option('--text', required=False, dest='text', type=unicode,
+               help='Template text for the password reset notification E-Mail. Default: use the builtin standard template'),
+        Option('--text-from-file', required=False, dest='text_file', type=unicode,
+               help='Read full template for the password reset notification E-Mail from the given file, overrides --text. Default: None'),
+        Option('--skip-invalid', required=False, dest='skip_invalid', action='store_true',
+               help='If a user\'s password hash is already invalid (pw is already reset), skip this user.'),
-    def run(self, name, uid, password, all_users, notify, verbose):
+    def run(self, name, uid, password, all_users, notify, verbose, subject, text, text_file, skip_invalid):
         flags_given = name or uid or all_users
         if not flags_given:
             print 'incorrect number of arguments'
-            import sys
-            sys.exit()
+            sys.exit(1)
+        if notify and not app.cfg.mail_enabled:
+            print "This wiki is not enabled for mail processing. The --notify option requires this. Aborting..."
+            sys.exit(1)
+        if text_file:
+            with open(text_file) as f:
+                text ='utf-8')
         if uid:
             name = meta[NAME]
             email = meta[EMAIL]
-                set_password(uid, password, notify=notify)
-            except Fault, err:
+                set_password(uid, password, notify=notify, skip_invalid=skip_invalid,
+                             subject=subject, text=text)
+            except Fault as err:
                 status = "FAILURE: [%s]" % str(err)
                 status = "SUCCESS"

File MoinMoin/templates/recoverpass.html

 {{, method="post", action=url_for('frontend.recoverpass')) }}
   {{ forms.render_errors(form) }}
+    {{ forms.render(form['token']) }}
     {{ forms.render(form['username']) }}
-    {{ forms.render(form['token']) }}
     {{ forms.render(form['password1']) }}
     {{ forms.render(form['password2']) }}

File MoinMoin/

         # Invalidate all other browser sessions except this one.
         session['user.session_token'] = self.generate_session_token(False)
+    def has_invalidated_password(self):
+        """
+        Check if the password hash of this user is invalid.
+        """
+        return self.profile[ENC_PASSWORD] == ''
     def save(self, force=False):
         Save user account data to user account file on disk.
         return True
-    def mail_password_recovery(self, cleartext_passwd=None):
+    def mail_password_recovery(self, cleartext_passwd=None, subject=None, text=None):
         """ Mail a user who forgot his password a message enabling
             him to login again.
+        if not
+            return False, "user has no E-Mail address in his profile."
         token = self.generate_recovery_token()
-        text = _("""\
+        if subject is None:
+            subject = _('[%(sitename)s] Your wiki password recovery link', sitename='%(sitename)s')
+        subject = subject % dict(sitename=self._cfg.sitename or "Wiki")
+        if text is None:
+            text = _("""\
 Somebody has requested to email you a password recovery link.
 Please use the link below to change your password to a known value:
 If you didn't forget your password, please ignore this email.
+""", link='%(link)s')
+        text = text % dict(link=url_for('frontend.recoverpass', username=self.name0, token=token, _external=True))
-""", link=url_for('frontend.recoverpass', username=self.name0, token=token, _external=True))
-        subject = _('[%(sitename)s] Your wiki password recovery link',
-                    sitename=self._cfg.sitename or "Wiki")
         mailok, msg = sendmail.sendmail(subject, text, to=[], mail_from=self._cfg.mail_from)
         return mailok, msg

File docs/admin/password-reset.rst

+Password Resetting/Invalidation
+There might be circumstances when the wiki admin wants or needs to reset one
+user's or all users' password (hash).
+For example:
+* you had a security breach on your wiki server (or somewhere else) and the
+  old password hashes (or passwords) were exposed
+* you want to make sure some user or all users set a new password, e.g. if:
+  - your password policy has changed (requiring longer passwords for example)
+  - you changed your passlib configuration and want to immediately have all
+    hashes upgraded
+Note: if we say "reset a password" (to use a commonly used term), we mean to
+"invalidate the password hash" (so that no password exists that validates
+against that hash). MoinMoin does not keep user passwords in cleartext.
+The files we refer to below are located in docs/examples/password-reset/...
+Resetting one or few password(s)
+If you somehow interact with the users corresponding to the user accounts in
+question (by phone or directly), you don't need the extensive procedure as
+described below, just use::
+    moin account-password --name JoeDoe
+That will reset JoeDoe's password. Tell him to visit the login URL and use
+the "forgot my password" functionality to define a new password.
+If that doesn't work (e.g. if e-mail is not enabled for your wiki or he has
+a non-working e-mail address in his profile), you can also set a password for
+    moin account-password --name JoeDoe --password uIkV9.-a3
+Choose a rather complicated password to make sure they change it a minute
+afterwards (to another, hopefully safe password).
+Resetting many or all password(s)
+If you have a lot of passwords to reset, you need a better procedure that
+avoids having to deal with too many users individually.
+Preparing your users
+Tell your users beforehands that you will be doing a password reset, otherwise
+they might find the automatically generated E-Mail they'll get suspicious and
+you'll have to explain it to them individually that the E-Mail is legitimate.
+Also, remind your users that having a valid E-Mail address in their user
+settings is essential for getting a password recovery E-Mail.
+If an active user does somehow not get such a mail, you likely will have to
+manually define a valid E-Mail address (or even password) for that user.
+Make sure E-Mail functionality works
+If you know you have working E-Mail functionality, skip this section.
+Password recovery and password reset notification work via E-Mail, so you
+should have it configured::
+    # the E-Mail address used for From: (consider using an address that
+    # can be directly replied to, at least while doing the pw reset):
+    mail_from = ''
+    # your smtp mail server hostname:port (default is 25)
+    mail_smarthost = ''
+    # the login there, if authentication is needed
+    mail_username = ''
+    mail_password = 'SuperSecretSMTPPassword'
+You can try whether it works by using the "forgot my password" functionality
+on the login page.
+Editing mailtemplate.txt
+If you edit mailtemplate.txt, please be very careful and follow these rules
+(otherwise you might just see the script command crashing):
+The contents must be utf-8 (or ascii, which is a subset of utf-8).
+In case of doubt, just use plain English.
+Some places you likely should edit are marked with XXX.
+Do not use any % character in your text (except for the placeholders).
+If you need a verbatim % character, you need to write %%.
+It is a very good idea to give some URL (e.g. of a web or wiki page) in
+the text where users can read more information.
+Of course the information at that URL should be readable without requiring
+a wiki login (you just have invalidated his/her password!), so the user can
+get informed before clicking links he got from someone via E-Mail.
+We have added a wikitemplate.txt you can use to create such a wiki page.
+Instead of creating a web or wiki page with the information, you could
+also write all the stuff into the mail template directly, but please consider
+that E-Mail delivery to some users might fail for misc. reasons, so having
+some information on the web/wiki is usually better.
+Editing wikitemplate.txt
+Just copy & paste it to some public page in your wiki, e.g. "PasswordReset".
+Some places you likely should edit are marked with XXX.
+Doing the password reset
+Maybe first try it with a single user account::
+    moin account-password --name JoeDoe --notify --subject 'Wiki password reset' --text-from-file mailtemplate.txt
+Use some valid name, maybe a testing account of yourself. You should now have
+mail. If that worked ok, you can now do a global password reset for your wiki::
+    moin account-password --verbose --all-users --notify --subject 'Wiki password reset' --text-from-file mailtemplate.txt
+The subject may contain a placeholder for the sitename, which is useful for
+wiki farms (showing the builtin default here)::
+    '[%(sitename)s] Your wiki account data'

File docs/examples/password-reset/mailtemplate-de.txt

+Der Wiki-Administrator hat Ihr Passwort invalidiert und angefordert,
+Ihnen diese E-Mail zu senden, damit Sie ein neues Passwort setzen koennen.
+Bitte lesen Sie wichtige Informationen hierzu (sowie Hinweise zur
+Problembeseitigung und Wiki-Administrator-Kontaktinformationen) dort:
+(XXX hier die URL angeben XXX)
+Bitte besuchen Sie nun die unten angezeigte Passwort-Ruecksetz-URL und
+setzen Sie ein neues Passwort.

File docs/examples/password-reset/mailtemplate.txt

+The wiki administrator has invalidated your wiki password and requested
+to send this E-Mail to you, so you can set a new one.
+Important information about this (including troubleshooting information and
+wiki administrator contact information) is available there, please read it:
+(XXX give URL here XXX)
+Now, please go to the password reset URL below and set a new password.

File docs/examples/password-reset/wikitemplate-de.txt

+Hinweis: einige Stellen, die man editieren muss, sind mit XXX markiert.
+= (XXX Datum angeben) YYYY-MM-DD Passworte zurückgesetzt =
+Der Wiki-Administrator hat für alle Benutzerkonten dieses Wikis die Passwörter invalidiert (auf ungültig gesetzt).
+Sie müssen eine Passwort-Rücksetzung ausführen, um ein ''neues und anderes'' Passwort für Ihr Konto zu setzen.
+== Gründe für die Passwort-Invalidierung ==
+ * (XXX Grund angeben)
+ * (XXX Grund angeben)
+== Was Sie tun müssen ==
+=== Lesen Sie wichtige Informationen ===
+Bitte besuchen Sie diese URLs für weitere Informationen:
+ * (allgemeine Hinweise, unbedingt lesen!)
+ * (über den spezifischen Vorfall, wenn Sie mehr Informationen möchten) (XXX URL korrigieren)
+=== Ein neues Passwort setzen ===
+Sie sollten eine E-Mail (inklusive eines Links, um Ihr Passwort zurückzusetzen) erhalten haben. Lesen und befolgen Sie sie.
+=== Problembehandlung ===
+==== Ungültiges Token? ====
+Das Token hat eine begrenzte Gültigkeitsdauer.
+Wenn es "Token ist ungültig" sagt, müssen Sie einfach ein neues Token anfordern, siehe unten.
+==== Sie haben eine unerwartete E-Mail über ein unbekanntes Benutzerkonto erhalten? ====
+Wenn Sie eine E-Mail erhalten haben, die Sie auffordert, das Passwort ''für ein Ihnen unbekanntes Benutzerkonto'' zurückzusetzen:
+ignorieren Sie sie einfach, setzen Sie das Passwort ''nicht'' zurück
+Wahrscheinlich hat jemand (z.B. ein Spammer) Ihre E-Mail-Adresse missbraucht, als er ein Benutzerkonto angelegt hat.
+Das passiert oft, Sie können das auch in Ihrem E-Mail-Spam-Ordner sehen.
+==== Sie haben die E-Mail nicht erhalten? ====
+Wenn Sie die E-Mail noch nicht gesehen haben, prüfen Sie bitte Ihren Spam-Ordner (oder auch andere Ordner, wo E-Mails vom Wiki sein könnten).
+Wenn Sie die von uns an Sie gesandte E-Mail nicht finden können, können Sie alternativ auch die normale Passwort-Rücksetz-Funktion des Wikis benutzen, siehe unten.
+==== Ein neues Passwort-Rücksetz-Token erhalten ====
+ * gehen Sie zur Anmelde-Seite des Wiki
+ * klicken Sie auf den "Password vergessen?"-Link
+ * Dann:
+  * ''entweder'' Ihren Wiki-Benutzernamen eingeben (empfehlenswert, wenn Sie ihn wissen oder einfach herausfinden können)
+  * ''oder'' Ihre E-Mail-Addresse (die gleiche wie in Ihrem Wiki-Benutzerprofil - möglicherweise brauchen Sie mehrere Versuche, wenn Sie sich hier nicht sicher sind)
+ * prüfen Sie Ihren E-Mail-Eingang, Sie sollten nun eine E-Mail mit einem Passwort-Rücksetz-Link haben
+ * klicken Sie auf den Link, definieren Sie ein neues Passwort
+==== Brauchen Sie Hilfe? ====
+Wenn das nicht funktioniert hat, brauchen Sie vermutlich Hilfe durch einen Wiki-Administrator, bitte kontaktieren Sie:
+(XXX hier Name und E-Mail des Wiki-Administrators angeben XXX)
+In Ihrer E-Mail, geben Sie bitte Folgendes an:
+ * die Wiki-Adresse (URL)
+ * Ihren Wiki-Benutzernamen
+ * die E-Mail-Adresse, die Sie in Ihrem Wiki-Benutzerprofil verwendet haben
+ * falls diese E-Mail-Adresse nicht mehr funktioniert, teilen Sie uns das bitte mit
+== Fragen? ==
+Wenn Sie Fragen haben, die nicht bereits auf dieser oder den verlinkten Seiten beantwortet werden, fragen Sie den Wiki-Administrator.
+E-Mails mit Fragen, die dort bereits beantwortet sind, werden ignoriert.
+Anstelle Fragen individuell über E-Mail zu beantworten, können oft gestellte Fragen auch auf dieser Seite oder den verlinkten Seiten beantwortet werden.

File docs/examples/password-reset/wikitemplate.txt

+Note: some stuff that needs editing is marked with XXX.
+= (XXX give date) YYYY-MM-DD Password Reset =
+The wiki administrator has invalidated the passwords for all user accounts on this wiki.
+You need to do a password recovery to set a ''new and changed'' password for your account.
+== Reason(s) for the password invalidation ==
+ * (XXX give reason)
+ * (XXX give reason)
+== What you need to do ==
+=== Read important information ===
+Please visit these URLs for more information:
+ * ('''generic advice, MUST READ!''')
+ * (about the specific incident, if you want more information) (XXX fix url)
+=== Enter a new password ===
+You should have received an E-Mail (including a link to reset your password). Just read and follow it.
+==== Invalid token? ====
+The token has a limited lifetime.
+If it says "Token is invalid", you just need to get a new password recovery token, see below.
+==== You didn't get the E-Mail? ====
+If you didn't see that E-Mail yet, please check your spam folder (or other folders where mails from the wiki could be).
+If you can't find the E-Mail we sent to you, you can alternatively just use the normal password recovery function of the wiki, see below.
+==== Getting a new password recovery token ====
+ * go to the login page
+ * click the "Forgot your password?" link
+ * now:
+  * ''either'' enter your wiki username (recommended, if you still remember it or you can find out easily)
+  * ''or'' your E-Mail address (same as in your wiki user profile - you might need multiple tries if you are not sure about this)
+ * check your E-Mail, you should have one now with a password recovery link
+ * click on the link, define a new password
+==== Need help? ====
+If that didn't work, you will need help by a wiki administrator, please contact:
+(XXX give name and E-Mail address of wiki administrator here XXX)
+In your E-Mail, please give:
+ * the wiki address (URL)
+ * your wiki user name
+ * the E-Mail address that you used in your wiki userprofile
+ * if that E-Mail address is not working any more, please tell so
+== Questions? ==
+If you have any questions that are not answered by reading this page or the pages linked from here, ask the wiki administrator.
+E-Mails asking questions that ''are'' answered on these pages will be ignored.
+Instead of answering individually via E-Mail, frequently asked questions might might also get answered on this page or the pages linked from here.

File docs/index.rst

+   admin/password-reset
 Getting Support for and Contributing to MoinMoin