Example #1
0
    def test_verify_conf_changes(self):
        """Registration challenges with EmailVerificationModule enabled."""
        self.env = EnvironmentStub(
                enable=['trac.*', 'acct_mgr.admin.*', 'acct_mgr.register.*'])
        self.env.path = tempfile.mkdtemp()
        set_user_attribute(self.env, 'admin', 'email', '*****@*****.**')

        check = EmailCheck(self.env)
        req = self.req

        # Inspector provides the email text input field.
        old_email_input = '*****@*****.**'
        acct = dict(username='******', email=old_email_input, name='User')
        req.args.update(acct)
        field_res = check.render_registration_fields(req, acct)
        self.assertEqual(len(field_res), 2)
        self.assertTrue(Markup(field_res[0]).startswith('<label>Email:'))
        # Ensure, that old input is restored on failure.
        self.assertTrue(old_email_input in Markup(field_res[0]))
        # Ensure, that template data dict is passed unchanged.
        self.assertEqual(field_res[1], acct)
        req.args.update(dict(email=''))

        # 1st: Initially try with account verification disabled by setting.
        self.env.config.set('account-manager', 'verify_email', False)
        self.assertEqual(check.validate_registration(req), None)
        # 2nd: Again no email, but now with account verification enabled. 
        self.env.config.set('account-manager', 'verify_email', True)
        self.assertRaises(RegistrationError, check.validate_registration, req)
        # 3th attempt: Valid email, but already registered with a username.
        req.args['email'] = '*****@*****.**'
        self.assertRaises(RegistrationError, check.validate_registration, req)
        # 4th attempt: Finally some valid input.
        req.args['email'] = '*****@*****.**'
        self.assertEqual(check.validate_registration(req), None)
 def _create_user(self, req):
     """Set password and prime a new authenticated Trac session."""
     username = req.args.get('username', '').strip()
     username = self.handle_username_casing(username)
     # Result of a successful account creation request is a made-up
     # authenticated session, that a new user can refer to later on.
     # Strictly required to create a primary key for additional attributes,
     # perhaps even something as critical as the SessionStore password.
     from acct_mgr.model import prime_auth_session, set_user_attribute
     try:
         prime_auth_session(self.env, username)
         # Save attributes for the user with reference to that session ID.
         # Done before writing to a password store to preserve attributes
         # in case of non-fatal errors (especially notification errors).
         for attribute in ('name', 'email'):
             value = req.args.get(attribute)
             if not value:
                 continue
             set_user_attribute(self.env, username, attribute, value)
         # Create the user in the configured (primary) password store.
         self.set_password(username, req.args.get('password'),
                           overwrite=False)
     finally:
         if not self.has_user(username):
             # Rollback.
             from acct_mgr.model import delete_user
             delete_user(self.env, username)
Example #3
0
    def failed_count(self, user, ipnr=None, reset=False):
        """Report number of previously logged failed login attempts.

        Enforce login policy with regards to tracking of login attempts
        and user account lock behavior.
        Default `False` for reset value causes logging of another attempt.
        `None` value for reset just reads failed login attempts count.
        `True` value for reset triggers final log deletion.
        """
        value = get_user_attribute(self.env, user, 1, 'failed_logins_count')
        count = value and int(value[user][1].get('failed_logins_count')) or 0
        if reset is None:
            # Report failed attempts count only.
            return count
        if not reset:
            # Trigger the failed attempt logger.
            attempts = self.get_failed_log(user)
            log_length = len(attempts)
            if log_length > self.login_attempt_max_count:
                # Truncate attempts list preserving most recent events.
                del attempts[:(log_length - self.login_attempt_max_count)]
            attempts.append({'ipnr': ipnr,
                             'time': to_utimestamp(to_datetime(None))})
            count += 1
            # Update or create attempts counter and list.
            set_user_attribute(self.env, user, 'failed_logins', str(attempts))
            set_user_attribute(self.env, user, 'failed_logins_count', count)
            self.log.debug("AcctMgr:failed_count(%s): %s" % (user, count))
        else:
            # Delete existing attempts counter and list.
            del_user_attribute(self.env, user, 1, 'failed_logins')
            del_user_attribute(self.env, user, 1, 'failed_logins_count')
            # Delete the lock count too.
            self.lock_count(user, 'reset')
        return count
Example #4
0
 def test_check_email_used(self):
     set_user_attribute(self.env, 'admin', 'email', '*****@*****.**')
     # Try email, that is already associated to another user.
     self.req.args['email'] = '*****@*****.**'
     self.vmod.pre_process_request(self.req, None)
     warnings = self.req.chrome.get('warnings')
     self.assertTrue(string.find(str(warnings and warnings[0] or ''),
                                 'already in use') > 0)
Example #5
0
 def process_request(self, req):
     acctmgr = self.acctmgr
     if req.authname != 'anonymous':
         req.redirect(req.href.prefs('account'))
     action = req.args.get('action')
     name = req.args.get('name', '').strip()
     username = acctmgr.handle_username_casing(req.args.get('username',
                                                            '').strip())
     data = {
             '_dgettext': dgettext,
               'acctmgr': dict(name=name, username=username),
      'ignore_auth_case': self.config.getbool('trac', 'ignore_auth_case')
     }
     verify_enabled = is_enabled(self.env, EmailVerificationModule) and \
                      EmailVerificationModule(self.env).verify_email
     data['verify_account_enabled'] = verify_enabled
     if req.method == 'POST' and action == 'create':
         try:
             # Check request and prime account on success.
             acctmgr.validate_account(req, True)
         except RegistrationError, e:
             # Attempt deferred translation.
             message = gettext(e.message)
             # Check for (matching number of) message arguments before
             #   attempting string substitution.
             if e.msg_args and \
                     len(e.msg_args) == len(re.findall('%s', message)):
                 message = message % e.msg_args
             chrome.add_warning(req, Markup(message))
         else:
             if self.require_approval:
                 set_user_attribute(self.env, username, 'approval',
                                    N_('pending'))
                 # Notify admin user about registration pending for review.
                 acctmgr._notify('registration_approval_required',
                                 username)
                 chrome.add_notice(req, Markup(tag.span(Markup(_(
                     "Your username has been registered successfully, but "
                     "your account requires administrative approval. "
                     "Please proceed according to local policy."))))
                 )
             if verify_enabled:
                 chrome.add_notice(req, Markup(tag.span(Markup(_(
                     """Your username has been successfully registered but
                     your account still requires activation. Please login
                     as user %(user)s, and follow the instructions.""",
                     user=tag.b(username)))))
                 )
                 req.redirect(req.href.login())
             chrome.add_notice(req, Markup(tag.span(Markup(_(
                  """Registration has been finished successfully.
                  You may log in as user %(user)s now.""",
                  user=tag.b(username)))))
             )
             req.redirect(req.href.login())
Example #6
0
 def _maybe_update_hash(self, user, password):
     if not get_user_attribute(self.env, 1, user, 'password_refreshed', 1):
         self.log.debug("Refresh password for user: %s" % user)
         store = self.find_user_store(user)
         pwstore = self.get_supporting_store('set_password')
         if pwstore.set_password(user, password) == True:
             # Account re-created according to current settings.
             if store and not (store.delete_user(user) == True):
                 self.log.warn(
                     "Failed to remove old entry for user: %s" % user)
         set_user_attribute(self.env, user, 'password_refreshed', 1)
 def _maybe_update_hash(self, user, password):
     from acct_mgr.model import get_user_attribute, set_user_attribute
     if get_user_attribute(self.env, user, 1,
                           'password_refreshed', 1) == [0]:
         self.log.debug("Refresh password for user: %s", user)
         store = self.find_user_store(user)
         pwstore = self.get_supporting_store('set_password')
         if pwstore.set_password(user, password) is True:
             # Account re-created according to current settings.
             if store and not (store.delete_user(user) is True):
                 self.log.warning("Failed to remove old entry for user: "******"%s", user)
         set_user_attribute(self.env, user, 'password_refreshed', 1)
Example #8
0
 def _create_user(self, req):
     """Set password and prime a new authenticated Trac session."""
     email = req.args.get('email', '').strip()
     name = req.args.get('name', '').strip()
     username = self.handle_username_casing(
                    req.args.get('username', '').strip())
     # Create the user in the configured (primary) password store.
     if self.set_password(username, req.args.get('password'), None, False):
         # Result of a successful account creation request is a made-up
         # authenticated session, that a new user can refer to later on.
         from acct_mgr.model import prime_auth_session, set_user_attribute
         prime_auth_session(self.env, username)
         # Save attributes for the user with reference to that session ID.
         for attribute in ('name', 'email'):
             value = req.args.get(attribute)
             if not value:
                 continue
             set_user_attribute(self.env, username, attribute, value)
Example #9
0
    def lock_count(self, user, action='get'):
        """Count, log and report, how often in succession user account
        lock conditions have been met.

        This is the exponent for lock time prolongation calculation too.
        """
        key = 'lock_count'
        if not action == 'reset':
            value = get_user_attribute(self.env, user, 1, key)
            count = value and int(value[user][1].get(key)) or 0
            if not action == 'get':
                # Push and create or update cached count.
                count += 1
                set_user_attribute(self.env, user, key, count)
        else:
            # Reset/delete lock count cache.
            del_user_attribute(self.env, user, 1, key)
            count = 0
        return count
Example #10
0
    def lock_count(self, user, action='get'):
        """Count, log and report, how often in succession user account
        lock conditions have been met.

        This is the exponent for lock time prolongation calculation too.
        """
        key = 'lock_count'
        if action != 'reset':
            value = get_user_attribute(self.env, user, 1, key)
            count = value and user in value and \
                    int(value[user][1].get(key)) or 0
            if action != 'get':
                # Push and create or update cached count.
                count += 1
                set_user_attribute(self.env, user, key, count)
        else:
            # Reset/delete lock count cache.
            del_user_attribute(self.env, user, 1, key)
            count = 0
        return count
Example #11
0
    def validate_registration(self, req):
        """Run configured registration checks and prime account on success."""
        for inspector in self._register_check:
            inspector.validate_registration(req)

        username = self.handle_username_casing(
            req.args.get('username').strip())
        name = req.args.get('name').strip()
        email = req.args.get('email').strip()
        # Create the user in the configured (primary) password store.
        self.set_password(username, req.args.get('password'))
        # Output of a successful account creation request is a made-up
        # authenticated session, that a new user can refer to later on.
        prime_auth_session(self.env, username)
        # Save attributes for the user with reference to that session ID.
        for attribute in ('name', 'email'):
            value = req.args.get(attribute)
            if not value:
                continue
            set_user_attribute(self.env, username, attribute, value)
Example #12
0
    def failed_count(self, user, ipnr=None, reset=False):
        """Report number of previously logged failed login attempts.

        Enforce login policy with regards to tracking of login attempts
        and user account lock behavior.
        Default `False` for reset value causes logging of another attempt.
        `None` value for reset just reads failed login attempts count.
        `True` value for reset triggers final log deletion.
        """
        if not user or not user_known(self.env, user):
            return 0
        key = 'failed_logins_count'
        value = get_user_attribute(self.env, user, 1, key)
        count = value and user in value and int(value[user][1].get(key)) or 0
        if reset is None:
            # Report failed attempts count only.
            return count
        if not reset:
            # Trigger the failed attempt logger.
            attempts = self.get_failed_log(user)
            log_length = len(attempts)
            if log_length > self.login_attempt_max_count:
                # Truncate attempts list preserving most recent events.
                del attempts[:(log_length - self.login_attempt_max_count)]
            attempts.append({
                'ipnr': ipnr,
                'time': to_timestamp(to_datetime(None))
            })
            count += 1
            # Update or create attempts counter and list.
            set_user_attribute(self.env, user, 'failed_logins', str(attempts))
            set_user_attribute(self.env, user, key, count)
            self.log.debug("AccountGuard.failed_count(%s) = %s" %
                           (user, count))
        else:
            # Delete existing attempts counter and list.
            del_user_attribute(self.env, user, 1, 'failed_logins')
            del_user_attribute(self.env, user, 1, key)
            # Delete the lock count too.
            self.lock_count(user, 'reset')
        return count
Example #13
0
 def test_set_user_attribute(self):
     set_user_attribute(self.env, 'user', 'attribute1', 'value1')
     cursor = self.db.cursor()
     cursor.execute("""
         SELECT name,value
         FROM   session_attribute
         WHERE  sid='user'
         AND    authenticated=1
     """)
     self.assertEqual(cursor.fetchall(), [('attribute1', 'value1')])
     # Setting an attribute twice will eventually just update the value.
     set_user_attribute(self.env, 'user', 'attribute1', 'value2')
     cursor.execute("""
         SELECT name,value
         FROM   session_attribute
         WHERE  sid='user'
         AND    authenticated=1
     """)
     self.assertEqual(cursor.fetchall(), [('attribute1', 'value2')])
     # All values are stored as strings internally, but the function
     # should take care to handle forseeable abuse gracefully.
     # This is a test for possible regressions of #10772.
     set_user_attribute(self.env, 'user', 'attribute1', 0)
     cursor.execute("""
         SELECT name,value
         FROM   session_attribute
         WHERE  sid='user'
         AND    authenticated=1
     """)
     self.assertEqual(cursor.fetchall(), [('attribute1', '0')])
Example #14
0
 def test_set_user_attribute(self):
     set_user_attribute(self.env, 'user', 'attribute1', 'value1')
     cursor = self.db.cursor()
     cursor.execute("""
         SELECT name,value
         FROM   session_attribute
         WHERE  sid='user'
         AND    authenticated=1
     """)
     self.assertEqual(cursor.fetchall(), [('attribute1', 'value1')])
     # Setting an attribute twice will eventually just update the value.
     set_user_attribute(self.env, 'user', 'attribute1', 'value2')
     cursor.execute("""
         SELECT name,value
         FROM   session_attribute
         WHERE  sid='user'
         AND    authenticated=1
     """)
     self.assertEqual(cursor.fetchall(), [('attribute1', 'value2')])
     # All values are stored as strings internally, but the function
     # should take care to handle forseeable abuse gracefully.
     # This is a test for possible regressions of #10772.
     set_user_attribute(self.env, 'user', 'attribute1', 0)
     cursor.execute("""
         SELECT name,value
         FROM   session_attribute
         WHERE  sid='user'
         AND    authenticated=1
     """)
     self.assertEqual(cursor.fetchall(), [('attribute1', '0')])
    def test_set_user_attribute(self):
        set_user_attribute(self.env, 'user', 'attribute1', 'value1')

        with self.env.db_query as db:
            for name, value in db("""
                    SELECT name,value FROM session_attribute
                    WHERE sid='user' AND authenticated=1
                    """):
                self.assertEqual(('attribute1', 'value1'), (name, value))
            # Setting an attribute twice will just update the value.
            set_user_attribute(self.env, 'user', 'attribute1', 'value2')
            for name, value in db("""
                    SELECT name,value FROM session_attribute
                    WHERE sid='user' AND authenticated=1
                    """):
                self.assertEqual(('attribute1', 'value2'), (name, value))
            # All values are stored as strings internally, but the function
            # should take care to handle foreseeable abuse gracefully.
            # This is a test for possible regressions of #10772.
            set_user_attribute(self.env, 'user', 'attribute1', 0)
            for name, value in db("""
                    SELECT name,value FROM session_attribute
                    WHERE  sid='user' AND authenticated=1
                    """):
                self.assertEqual(('attribute1', '0'), (name, value))
    def test_set_user_attribute(self):
        set_user_attribute(self.env, 'user', 'attribute1', 'value1')

        with self.env.db_query as db:
            for name, value in db("""
                    SELECT name,value FROM session_attribute
                    WHERE sid='user' AND authenticated=1
                    """):
                self.assertEqual(('attribute1', 'value1'), (name, value))
            # Setting an attribute twice will just update the value.
            set_user_attribute(self.env, 'user', 'attribute1', 'value2')
            for name, value in db("""
                    SELECT name,value FROM session_attribute
                    WHERE sid='user' AND authenticated=1
                    """):
                self.assertEqual(('attribute1', 'value2'), (name, value))
            # All values are stored as strings internally, but the function
            # should take care to handle foreseeable abuse gracefully.
            # This is a test for possible regressions of #10772.
            set_user_attribute(self.env, 'user', 'attribute1', 0)
            for name, value in db("""
                    SELECT name,value FROM session_attribute
                    WHERE  sid='user' AND authenticated=1
                    """):
                self.assertEqual(('attribute1', '0'), (name, value))
Example #17
0
    def test_verify_conf_changes(self):
        """Registration challenges with EmailVerificationModule enabled."""
        self.env = EnvironmentStub(enable=[
            'trac.*', 'acct_mgr.admin.*', 'acct_mgr.register.*',
            'acct_mgr.pwhash.HtDigestHashMethod'
        ])
        self.env.path = tempfile.mkdtemp()
        set_user_attribute(self.env, 'admin', 'email', '*****@*****.**')

        check = EmailCheck(self.env)
        req = self.req

        # Inspector provides the email text input field.
        old_email_input = '*****@*****.**'
        acct = dict(username='******', email=old_email_input, name='User')
        req.args.update(acct)
        field_res = check.render_registration_fields(req, acct)
        self.assertEqual(len(field_res), 2)
        self.assertTrue(Markup(field_res[0]).startswith('<label>Email:'))
        # Ensure, that old input is restored on failure.
        self.assertTrue(old_email_input in Markup(field_res[0]))
        # Ensure, that template data dict is passed unchanged.
        self.assertEqual(field_res[1], acct)
        req.args.update(dict(email=''))

        # 1st: Initially try with account verification disabled by setting.
        self.env.config.set('account-manager', 'verify_email', False)
        self.assertEqual(check.validate_registration(req), None)
        # 2nd: Again no email, but now with account verification enabled.
        self.env.config.set('account-manager', 'verify_email', True)
        self.assertRaises(RegistrationError, check.validate_registration, req)
        # 3th attempt: Valid email, but already registered with a username.
        req.args['email'] = '*****@*****.**'
        self.assertRaises(RegistrationError, check.validate_registration, req)
        # 4th attempt: Finally some valid input.
        req.args['email'] = '*****@*****.**'
        self.assertEqual(check.validate_registration(req), None)
Example #18
0
class AccountModule(CommonTemplateProvider):
    """Exposes methods for users to do account management on their own.

    Allows users to change their password, reset their password, if they've
    forgotten it, even delete their account.  The settings for the
    AccountManager module must be set in trac.ini in order to use this.
    Password reset procedure depends on both, ResetPwStore and an
    IPasswordHashMethod implementation being enabled as well.
    """

    implements(IPreferencePanelProvider, IRequestHandler,
               INavigationContributor, IRequestFilter)

    _password_chars = string.ascii_letters + string.digits
    password_length = IntOption(
        'account-manager', 'generated_password_length', 8,
        """Length of the randomly-generated passwords created when resetting
        the password for an account.""")
    reset_password = BoolOption(
        'account-manager', 'reset_password', True,
        'Set to False, if there is no email system setup.')

    def __init__(self):
        self.acctmgr = AccountManager(self.env)
        self.store = ResetPwStore(self.env)
        self._write_check(log=True)

    def _write_check(self, log=False):
        """Returns all configured write-enabled password stores."""
        writable = self.acctmgr.get_all_supporting_stores('set_password')
        if writable:
            try:
                writable = writable.remove(self.store)
            except ValueError:
                # ResetPwStore is not enabled.
                if log:
                    self.log.warn("ResetPwStore is disabled, therefor "
                                  "password reset won't work.")
        # Require at least one more write-enabled password store.
        if not writable and log:
            self.log.warn("AccountModule is disabled because no configured "
                          "password store supports writing.")
        return writable

    # INavigationContributor methods

    def get_active_navigation_item(self, req):
        return 'reset_password'

    def get_navigation_items(self, req):
        if not self.reset_password_enabled or LoginModule(self.env).enabled:
            return
        if req.authname == 'anonymous':
            yield 'metanav', 'reset_password', tag.a(
                _("Forgot your password?"), href=req.href.reset_password())

    def _reset_password_enabled(self, log=False):
        try:
            self.store.hash_method
        except AttributeError:
            return False
        return is_enabled(self.env, self.__class__) and \
               self.reset_password and (self._write_check(log) != []) and \
               is_enabled(self.env, self.store.__class__) and \
               self.store.hash_method and True or False

    reset_password_enabled = property(_reset_password_enabled)

    # IPreferencePanelProvider methods

    def get_preference_panels(self, req):
        writable = self._write_check()
        if not writable:
            return
        if req.authname and req.authname != 'anonymous':
            user_store = self.acctmgr.find_user_store(req.authname)
            if user_store in writable:
                yield 'account', _("Account")

    def render_preference_panel(self, req, panel):
        data = dict(_dgettext=dgettext)
        data.update(self._do_account(req))
        return 'prefs_account.html', data

    # IRequestFilter methods

    def pre_process_request(self, req, handler):
        if req.path_info == '/prefs/account' and \
                not (req.authname and req.authname != 'anonymous'):
            # An anonymous session has no account associated with it, and
            # no account properies too, but general session preferences should
            # always be available.
            req.redirect(req.href.prefs())
        return handler

    def post_process_request(self, req, template, data, content_type):
        if req.authname and req.authname != 'anonymous':
            if req.session.get('force_change_passwd', False):
                # Prevent authenticated usage before another password change.
                redirect_url = req.href.prefs('account')
                if req.href(req.path_info) != redirect_url:
                    req.redirect(redirect_url)
        return (template, data, content_type)

    # IRequestHandler methods

    def match_request(self, req):
        return req.path_info == '/reset_password' and \
               self._reset_password_enabled(log=True)

    def process_request(self, req):
        data = dict(_dgettext=dgettext)
        if req.authname and req.authname != 'anonymous':
            add_notice(
                req,
                Markup(
                    tag_(
                        "You're already logged in. If you need to change your "
                        "password please use the %(prefs_href)s page.",
                        prefs_href=tag.a(_("Account Preferences"),
                                         href=req.href.prefs('account')))))
            data['authenticated'] = True
        if req.method == 'POST':
            self._do_reset_password(req)
        return 'reset_password.html', data, None

    def _do_account(self, req):
        assert (req.authname and req.authname != 'anonymous')
        action = req.args.get('action')
        delete_enabled = self.acctmgr.supports('delete_user') and \
                             self.acctmgr.allow_delete_account
        data = {
            'delete_enabled':
            delete_enabled,
            'delete_msg_confirm':
            _("Are you sure you want to delete your account?"),
        }
        force_change_password = req.session.get('force_change_passwd', False)
        if req.method == 'POST':
            if action == 'save':
                if self._do_change_password(req) and force_change_password:
                    del req.session['force_change_passwd']
                    req.session.save()
                    add_notice(
                        req,
                        _("Thank you for taking the time to "
                          "update your password."))
                    force_change_password = False
            elif action == 'delete' and delete_enabled:
                self._do_delete(req)
        if force_change_password:
            add_warning(
                req,
                Markup(
                    _(
                        "You are required to change password because of a recent "
                        "password change request. %(invitation)s",
                        invitation=tag.b(
                            _("Please change your password now.")))))
        return data

    def _do_change_password(self, req):
        username = req.authname

        old_password = req.args.get('old_password')
        if not self.acctmgr.check_password(username, old_password):
            if old_password:
                add_warning(req, _("Old password is incorrect."))
            else:
                add_warning(req, _("Old password cannot be empty."))
            return
        password = req.args.get('password')
        if not password:
            add_warning(req, _("Password cannot be empty."))
        elif password != req.args.get('password_confirm'):
            add_warning(req, _("The passwords must match."))
        elif password == old_password:
            add_warning(req, _("Password must not match old password."))
        else:
            self.acctmgr.set_password(username, password, old_password)
            if req.session.get('password') is not None:
                # Fetch all session_attributes in case new user password is in
                # SessionStore, preventing overwrite by session.save().
                req.session.get_session(req.authname, authenticated=True)
            add_notice(req, _("Password updated successfully."))
            return True

    def _do_delete(self, req):
        username = req.authname

        password = req.args.get('password')
        if not password:
            add_warning(req, _("Password cannot be empty."))
        elif not self.acctmgr.check_password(username, password):
            add_warning(req, _("Password is incorrect."))
        else:
            self.acctmgr.delete_user(username)
            # Delete the whole session, since records in session_attribute
            # would get restored on logout otherwise.
            req.session.clear()
            req.session.save()
            req.redirect(req.href.logout())

    def _do_reset_password(self, req):
        email = req.args.get('email')
        username = req.args.get('username')
        if not username:
            add_warning(req, _("Username is required."))
        elif not email:
            add_warning(req, _("Email is required."))
        else:
            for username_, name, email_ in self.env.get_known_users():
                if username_ == username and email_ == email:
                    self._reset_password(req, username, email)
                    return
            add_warning(req,
                        _("Email and username must match a known account."))

    @property
    def _random_password(self):
        return ''.join([
            random.choice(self._password_chars)
            for _ in xrange(self.password_length)
        ])

    def _reset_password(self, req, username, email):
        acctmgr = self.acctmgr
        new_password = self._random_password
        try:
            self.store.set_password(username, new_password)
            acctmgr._notify('password_reset', username, email, new_password)
            # No message, if method has been called from user admin panel.
            if not req.path_info.startswith('/admin'):
                add_notice(
                    req,
                    _("A new password has been sent to you at "
                      "<%(email)s>.",
                      email=email))
        except Exception, e:
            add_warning(
                req,
                _("Cannot reset password: %(error)s",
                  error=', '.join(map(to_unicode, e.args))))
            return
        if acctmgr.force_passwd_change:
            set_user_attribute(self.env, username, 'force_change_passwd', 1)
 def process_request(self, req):
     acctmgr = self.acctmgr
     if req.authname != 'anonymous':
         req.redirect(req.href.prefs('account'))
     action = req.args.get('action')
     name = req.args.get('name', '')
     if isinstance(name, list):
         raise HTTPBadRequest(_("Invalid request arguments."))
     name = name.strip()
     username = req.args.get('username', '')
     if isinstance(username, list):
         raise HTTPBadRequest(_("Invalid request arguments."))
     username = acctmgr.handle_username_casing(username.strip())
     data = {
         '_dgettext': dgettext,
         'acctmgr': {'name': name, 'username': username},
         'ignore_auth_case': self.config.getbool('trac',
                                                 'ignore_auth_case')
     }
     verify_enabled = self.env.is_enabled(EmailVerificationModule) and \
                      EmailVerificationModule(self.env).verify_email
     data['verify_account_enabled'] = verify_enabled
     if req.method == 'POST' and action == 'create':
         try:
             try:
                 # Check request and prime account on success.
                 acctmgr.validate_account(req, True)
             except NotificationError, e:
                 chrome.add_warning(req, _(
                     "Error raised while sending a change notification."
                 ) + _("You should report that issue to a Trac admin."))
                 self.log.error(
                     'Unable to send registration notification: %s',
                     exception_to_unicode(e, traceback=True))
         except RegistrationError, e:
             chrome.add_warning(req, e)
         else:
             if self.require_approval:
                 set_user_attribute(self.env, username, 'approval',
                                    N_('pending'))
                 # Notify admin user about registration pending for review.
                 try:
                     acctmgr._notify('registration_approval_required',
                                     username)
                 except NotificationError, e:
                     chrome.add_warning(req, _(
                         "Error raised while sending a change "
                         "notification.") + _(
                         "You should report that issue to a Trac admin."))
                     self.log.error(
                         'Unable to send admin notification: %s',
                         exception_to_unicode(e, traceback=True))
                 else:
                     chrome.add_notice(req, tag_(
                         "Your username has been registered successfully, "
                         "but your account requires administrative "
                         "approval. Please proceed according to local "
                         "policy."))
             if verify_enabled:
                 chrome.add_notice(req, tag_(
                     "Your username has been successfully registered but "
                     "your account still requires activation. Please "
                     "login as user %(user)s, and follow the "
                     "instructions.", user=tag.b(username)))
                 req.redirect(req.href.login())
             chrome.add_notice(req, tag_(
                 "Registration has been finished successfully. "
                 "You may log in as user %(user)s now.",
                 user=tag.b(username)))
             req.redirect(req.href.login())
Example #20
0
class AccountModule(CommonTemplateProvider):
    """Exposes methods for users to do account management on their own.

    Allows users to change their password, reset their password, if they've
    forgotten it, even delete their account.  The settings for the
    AccountManager module must be set in trac.ini in order to use this.
    Password reset procedure depends on both, ResetPwStore and an
    IPasswordHashMethod implementation being enabled as well.
    """

    implements(IPreferencePanelProvider, IRequestHandler,
               INavigationContributor, IRequestFilter)

    _password_chars = string.ascii_letters + string.digits
    password_length = IntOption(
        'account-manager', 'generated_password_length', 8,
        """Length of the randomly-generated passwords created when resetting
        the password for an account.""")

    reset_password = BoolOption(
        'account-manager', 'reset_password', True,
        'Set to False, if there is no email system setup.')

    def __init__(self):
        self.acctmgr = AccountManager(self.env)
        self.store = ResetPwStore(self.env)
        self._write_check(log=True)

    def _write_check(self, log=False):
        writable = self.acctmgr.get_all_supporting_stores('set_password')
        if not writable and log:
            self.log.warn("AccountModule is disabled because the password "
                          "store does not support writing.")
        return writable

    # IPreferencePanelProvider methods

    def get_preference_panels(self, req):
        writable = self._write_check()
        if not writable:
            return
        if req.authname and req.authname != 'anonymous':
            user_store = self.acctmgr.find_user_store(req.authname)
            if user_store in writable:
                yield 'account', _("Account")

    def render_preference_panel(self, req, panel):
        data = {'account': self._do_account(req),
                '_dgettext': dgettext,
               }
        return 'prefs_account.html', data

    # IRequestHandler methods

    def match_request(self, req):
        return req.path_info == '/reset_password' and \
               self._reset_password_enabled(log=True)

    def process_request(self, req):
        data = {'_dgettext': dgettext,
                'reset': self._do_reset_password(req)
               }
        return 'reset_password.html', data, None

    # IRequestFilter methods

    def pre_process_request(self, req, handler):
        if req.path_info == '/prefs/account' and \
                not (req.authname and req.authname != 'anonymous'):
            # An anonymous session has no account associated with it, and
            # no account properies too, but general session preferences should
            # always be available.
            req.redirect(req.href.prefs())
        return handler

    def post_process_request(self, req, template, data, content_type):
        if req.authname and req.authname != 'anonymous':
            if req.session.get('force_change_passwd', False):
                redirect_url = req.href.prefs('account')
                if req.href(req.path_info) != redirect_url:
                    req.redirect(redirect_url)
        return (template, data, content_type)

    # INavigationContributor methods

    def get_active_navigation_item(self, req):
        return 'reset_password'

    def get_navigation_items(self, req):
        if not self.reset_password_enabled or LoginModule(self.env).enabled:
            return
        if req.authname == 'anonymous':
            yield 'metanav', 'reset_password', tag.a(
                _("Forgot your password?"), href=req.href.reset_password())

    def _reset_password_enabled(self, log=False):
        return is_enabled(self.env, self.__class__) and \
               self.reset_password and (self._write_check(log) != []) and \
               is_enabled(self.env, self.store.__class__) and \
               self.store.hash_method

    reset_password_enabled = property(_reset_password_enabled)

    def _do_account(self, req):
        assert(req.authname and req.authname != 'anonymous')
        action = req.args.get('action')
        delete_enabled = self.acctmgr.supports('delete_user') and \
                             self.acctmgr.allow_delete_account
        data = {'delete_enabled': delete_enabled,
                'delete_msg_confirm': _(
                    "Are you sure you want to delete your account?"),
               }
        force_change_password = req.session.get('force_change_passwd', False)
        if req.method == 'POST':
            if action == 'save':
                data.update(self._do_change_password(req))
                if force_change_password:
                    del(req.session['force_change_passwd'])
                    req.session.save()
                    chrome.add_notice(req, Markup(tag.span(tag_(
                        "Thank you for taking the time to update your password."
                    ))))
                    force_change_password = False
            elif action == 'delete' and delete_enabled:
                data.update(self._do_delete(req))
            else:
                data.update({'error': 'Invalid action'})
        if force_change_password:
            chrome.add_warning(req, Markup(tag.span(_(
                "You are required to change password because of a recent "
                "password change request. "),
                tag.b(_("Please change your password now.")))))
        return data

    def _do_reset_password(self, req):
        if req.authname and req.authname != 'anonymous':
            return {'logged_in': True}
        if req.method != 'POST':
            return {}
        username = req.args.get('username')
        email = req.args.get('email')
        if not username:
            return {'error': _("Username is required")}
        if not email:
            return {'error': _("Email is required")}
        for username_, name, email_ in self.env.get_known_users():
            if username_ == username and email_ == email:
                error = self._reset_password(username, email)
                return error and error or {'sent_to_email': email}
        return {'error': _(
            "The email and username must match a known account.")}

    def _reset_password(self, username, email):
        acctmgr = self.acctmgr
        new_password = self._random_password()
        try:
            self.store.set_password(username, new_password)
            acctmgr._notify('password_reset', username, email, new_password)
        except Exception, e:
            return {'error': ','.join(map(to_unicode, e.args))}
        if acctmgr.force_passwd_change:
            set_user_attribute(self.env, username, 'force_change_passwd', 1)
            add_warning(req, msg)
            self.log.error('Unable to send password reset notification: %s',
                           exception_to_unicode(e, traceback=True))
        except Exception, e:
            add_warning(req, _("Cannot reset password: %(error)s",
                               error=exception_to_unicode(e)))
            self.log.error('Unable to reset password: %s',
                           exception_to_unicode(e, traceback=True))
            return
        else:
            # No message, if method has been called from user admin panel.
            if not req.path_info.startswith('/admin'):
                add_notice(req, _("A new password has been sent to you at "
                                  "<%(email)s>.", email=email))
        if acctmgr.force_passwd_change:
            set_user_attribute(self.env, username, 'force_change_passwd', 1)


class LoginModule(auth.LoginModule, CommonTemplateProvider):
    """Custom login form and processing.

    This is woven with the trac.auth.LoginModule it inherits and overwrites.
    But both can't co-exist, so Trac's built-in authentication module
    must be disabled to use this one.
    """

    # Trac core options, replicated here to not make them disappear by
    # disabling auth.LoginModule.
    check_ip = BoolOption('trac', 'check_auth_ip', 'false',
         """Whether the IP address of the user should be checked for
         authentication (''since 0.9'').""")
Example #22
0
    def _do_users(self, req):
        env = self.env
        perm = PermissionSystem(env)
        acctmgr = self.acctmgr
        acctmod = AccountModule(env)
        guard = self.guard
        listing_enabled = acctmgr.supports('get_users')
        create_enabled = acctmgr.supports('set_password')
        password_change_enabled = acctmgr.supports('set_password')
        password_reset_enabled = acctmod.reset_password_enabled
        delete_enabled = acctmgr.supports('delete_user')
        verify_enabled = acctmgr.verify_email and \
                         EmailVerificationModule(env).email_enabled

        account = dict(email=req.args.get('email', '').strip(),
                       name=req.args.get('name', '').strip(),
                       username=acctmgr.handle_username_casing(
                           req.args.get('username', '').strip()))
        data = {
            '_dgettext': dgettext,
            'acctmgr': account,
            'email_approved': True,
            'listing_enabled': listing_enabled,
            'create_enabled': create_enabled,
            'delete_enabled': delete_enabled,
            'verify_enabled': verify_enabled,
            'ignore_auth_case': self.config.getbool('trac',
                                                    'ignore_auth_case'),
            'password_change_enabled': password_change_enabled,
            'password_reset_enabled': password_reset_enabled
        }
        if req.method == 'GET':
            if 'user' in req.args.iterkeys():
                return self._do_acct_details(req)
            elif req.args.get('max_per_page'):
                return self._do_db_cleanup(req)

        if req.method == 'POST':
            email_approved = req.args.get('email_approved')
            # Preserve selection during a series of requests.
            data['email_approved'] = email_approved

            if req.args.get('add'):
                # Add new user account.
                if create_enabled:
                    # Check request and prime account on success.
                    try:
                        acctmgr.validate_registration(req)
                        # Account email approval for authoritative action.
                        if verify_enabled and email_approved and \
                                account['email']:
                            set_user_attribute(env, account['username'],
                                               'email_verification_sent_to',
                                               account['email'])
                        # User editor form clean-up.
                        data['acctmgr'] = {}
                    except RegistrationError, e:

                        # Attempt deferred translation.
                        message = gettext(e.message)
                        # Check for (matching number of) message arguments
                        #   before attempting string substitution.
                        if e.msg_args and \
                                len(e.msg_args) == len(re.findall('%s',
                                                                  message)):
                            message = message % e.msg_args
                        data['editor_error'] = Markup(message)
                else:
                    data['editor_error'] = _(
                        "The password store does not support creating users.")
            elif req.args.get('reset') and req.args.get('sel'):
                # Password reset for one or more accounts.
                if password_reset_enabled:
                    sel = req.args.get('sel')
                    sel = isinstance(sel, list) and sel or [sel]
                    for username, name, email in env.get_known_users():
                        if username in sel:
                            acctmod._reset_password(username, email)
                else:
                    data['deletion_error'] = _(
                        "The password reset procedure is not enabled.")
            elif req.args.get('remove') and req.args.get('sel'):
                # Delete one or more accounts.
                if delete_enabled:
                    sel = req.args.get('sel')
                    sel = isinstance(sel, list) and sel or [sel]
                    for account in sel:
                        acctmgr.delete_user(account)
                else:
                    data['deletion_error'] = _(
                        "The password store does not support deleting users.")
            elif req.args.get('change'):
                # Change attributes and or password of existing user account.
                attributes = {
                    'email': _("Email Address"),
                    'name': _("Pre-/Surname (Nickname)"),
                    'password': _("Password")
                }
                data['success'] = []
                error = TracError('')
                username = acctmgr.handle_username_casing(
                    req.args.get('username').strip())
                try:
                    if not username:
                        error.account = {'username': username}
                        error.message = _("Username cannot be empty.")
                        raise error

                    if not acctmgr.has_user(username):
                        error.account = {'username': username}
                        error.message = _("Unknown user %(user)s.",
                                          user=username)
                        raise error

                    password = req.args.get('password')
                    if password and (password.strip() != ''):
                        if password_change_enabled:
                            if password != req.args.get('password_confirm'):
                                error.message = _("The passwords must match.")
                                raise error
                            acctmgr.set_password(username, password)
                            data['success'].append(attributes.get('password'))
                        else:
                            data['editor_error'] = _(
                                """The password store does not support
                                changing passwords.
                                """)
                    for attribute in ('name', 'email'):
                        value = req.args.get(attribute, '').strip()
                        if value:
                            set_user_attribute(env, username, attribute, value)
                            data['success'].append(attributes.get(attribute))
                            # Account email approval for authoritative action.
                            if attribute == 'email' and verify_enabled and \
                                    email_approved:
                                set_user_attribute(
                                    env, username,
                                    'email_verification_sent_to', value)
                    # User editor form clean-up on success.
                    data['acctmgr'] = {}
                except TracError, e:
                    data['editor_error'] = e.message
                    data['acctmgr'] = getattr(e, 'account', '')
            add_warning(req, msg)
            self.log.error("Unable to send password reset notification: %s",
                           exception_to_unicode(e, traceback=True))
        except Exception, e:
            add_warning(req, _("Cannot reset password: %(error)s",
                               error=exception_to_unicode(e)))
            self.log.error("Unable to reset password: %s",
                           exception_to_unicode(e, traceback=True))
            return
        else:
            # No message, if method has been called from user admin panel.
            if not req.path_info.startswith('/admin'):
                add_notice(req, _("A new password has been sent to you at "
                                  "<%(email)s>.", email=email))
        if acctmgr.force_passwd_change:
            set_user_attribute(self.env, username, 'force_change_passwd', 1)


class LoginModule(auth.LoginModule, CommonTemplateProvider):
    """Custom login form and processing.

    This is woven with the trac.auth.LoginModule it inherits and overwrites.
    But both can't co-exist, so Trac's built-in authentication module
    must be disabled to use this one.
    """

    # Trac core options, replicated here to not make them disappear by
    # disabling auth.LoginModule.
    check_ip = BoolOption('trac', 'check_auth_ip', 'false',
                          """Whether the IP address of the user should be checked for
                          authentication (''since 0.9'').""")
Example #24
0
    def _do_users(self, req):
        env = self.env
        perm = PermissionSystem(env)
        acctmgr = self.acctmgr
        acctmod = AccountModule(env)
        guard = self.guard
        listing_enabled = acctmgr.supports('get_users')
        create_enabled = acctmgr.supports('set_password')
        password_change_enabled = acctmgr.supports('set_password')
        password_reset_enabled = acctmod.reset_password_enabled
        delete_enabled = acctmgr.supports('delete_user')
        verify_enabled = acctmgr.verify_email and \
                         EmailVerificationModule(env).email_enabled

        account = dict(email=req.args.get('email', '').strip(),
                       name=req.args.get('name', '').strip(),
                       username=acctmgr.handle_username_casing(
                                    req.args.get('username', '').strip()))
        data = {
            '_dgettext': dgettext,
            'acctmgr': account,
            'email_approved': True,
            'listing_enabled': listing_enabled,
            'create_enabled': create_enabled,
            'delete_enabled': delete_enabled,
            'verify_enabled': verify_enabled,
            'ignore_auth_case': self.config.getbool('trac',
                                                    'ignore_auth_case'),
            'password_change_enabled': password_change_enabled,
            'password_reset_enabled': password_reset_enabled
        }
        if req.method == 'GET':
            if 'user' in req.args.iterkeys():
                return self._do_acct_details(req)
            elif req.args.get('max_per_page'):
                return self._do_db_cleanup(req)

        if req.method == 'POST':
            email_approved = req.args.get('email_approved')
            # Preserve selection during a series of requests.
            data['email_approved'] = email_approved

            if req.args.get('add'):
                # Add new user account.
                if create_enabled:
                    # Check request and prime account on success.
                    try:
                        acctmgr.validate_registration(req)
                        # Account email approval for authoritative action.
                        if verify_enabled and email_approved and \
                                account['email']:
                            set_user_attribute(env, account['username'],
                                'email_verification_sent_to', account['email'])
                        # User editor form clean-up.
                        data['acctmgr'] = {}
                    except RegistrationError, e:

                        # Attempt deferred translation.
                        message = gettext(e.message)
                        # Check for (matching number of) message arguments
                        #   before attempting string substitution.
                        if e.msg_args and \
                                len(e.msg_args) == len(re.findall('%s',
                                                                  message)):
                            message = message % e.msg_args
                        data['editor_error'] = Markup(message)
                else:
                    data['editor_error'] = _(
                        "The password store does not support creating users.")
            elif req.args.get('reset') and req.args.get('sel'):
                # Password reset for one or more accounts.
                if password_reset_enabled:
                    sel = req.args.get('sel')
                    sel = isinstance(sel, list) and sel or [sel]
                    for username, name, email in env.get_known_users():
                        if username in sel:
                            acctmod._reset_password(username, email)
                else:
                    data['deletion_error'] = _(
                        "The password reset procedure is not enabled.")
            elif req.args.get('remove') and req.args.get('sel'):
                # Delete one or more accounts.
                if delete_enabled:
                    sel = req.args.get('sel')
                    sel = isinstance(sel, list) and sel or [sel]
                    for account in sel:
                        acctmgr.delete_user(account)
                else:
                    data['deletion_error'] = _(
                        "The password store does not support deleting users.")
            elif req.args.get('change'):
                # Change attributes and or password of existing user account.
                attributes = {
                    'email': _("Email Address"),
                    'name': _("Pre-/Surname (Nickname)"),
                    'password': _("Password")
                    }
                data['success'] = []
                error = TracError('')
                username = acctmgr.handle_username_casing(
                                   req.args.get('username').strip())
                try:
                    if not username:
                        error.account = {'username' : username}
                        error.message = _("Username cannot be empty.")
                        raise error

                    if not acctmgr.has_user(username):
                        error.account = {'username' : username}
                        error.message = _("Unknown user %(user)s.",
                                          user=username)
                        raise error

                    password = req.args.get('password')
                    if password and (password.strip() != ''):
                        if password_change_enabled:
                            if password != req.args.get('password_confirm'):
                                error.message = _("The passwords must match.")
                                raise error
                            acctmgr.set_password(username, password)
                            data['success'].append(attributes.get('password'))
                        else:
                            data['editor_error'] = _(
                                """The password store does not support
                                changing passwords.
                                """)
                    for attribute in ('name', 'email'):
                        value = req.args.get(attribute, '').strip()
                        if value:
                            set_user_attribute(env, username,
                                               attribute, value)
                            data['success'].append(attributes.get(attribute))
                            # Account email approval for authoritative action.
                            if attribute == 'email' and verify_enabled and \
                                    email_approved:
                                set_user_attribute(env, username,
                                    'email_verification_sent_to', value)
                    # User editor form clean-up on success.
                    data['acctmgr'] = {}
                except TracError, e:
                    data['editor_error'] = e.message
                    data['acctmgr'] = getattr(e, 'account', '')
Example #25
0
 def process_request(self, req):
     acctmgr = self.acctmgr
     if req.authname != 'anonymous':
         req.redirect(req.href.prefs('account'))
     action = req.args.get('action')
     name = req.args.get('name', '').strip()
     username = acctmgr.handle_username_casing(
         req.args.get('username', '').strip())
     data = {
         '_dgettext': dgettext,
         'acctmgr': dict(name=name, username=username),
         'ignore_auth_case': self.config.getbool('trac', 'ignore_auth_case')
     }
     verify_enabled = is_enabled(self.env, EmailVerificationModule) and \
                      EmailVerificationModule(self.env).verify_email
     data['verify_account_enabled'] = verify_enabled
     if req.method == 'POST' and action == 'create':
         try:
             # Check request and prime account on success.
             acctmgr.validate_account(req, True)
         except RegistrationError, e:
             # Attempt deferred translation.
             message = gettext(e.message)
             # Check for (matching number of) message arguments before
             #   attempting string substitution.
             if e.msg_args and \
                     len(e.msg_args) == len(re.findall('%s', message)):
                 message = message % e.msg_args
             chrome.add_warning(req, Markup(message))
         else:
             if self.require_approval:
                 set_user_attribute(self.env, username, 'approval',
                                    N_('pending'))
                 # Notify admin user about registration pending for review.
                 acctmgr._notify('registration_approval_required', username)
                 chrome.add_notice(
                     req,
                     Markup(
                         tag.span(
                             Markup(
                                 _("Your username has been registered successfully, but "
                                   "your account requires administrative approval. "
                                   "Please proceed according to local policy."
                                   )))))
             if verify_enabled:
                 chrome.add_notice(
                     req,
                     Markup(
                         tag.span(
                             Markup(
                                 _("""Your username has been successfully registered but
                     your account still requires activation. Please login
                     as user %(user)s, and follow the instructions.""",
                                   user=tag.b(username))))))
                 req.redirect(req.href.login())
             chrome.add_notice(
                 req,
                 Markup(
                     tag.span(
                         Markup(
                             _("""Registration has been finished successfully.
                  You may log in as user %(user)s now.""",
                               user=tag.b(username))))))
             req.redirect(req.href.login())