Exemple #1
0
    def setUp(self):
        self.env = EnvironmentStub(default_data=True,
                                   enable=['trac.*', 'acct_mgr.guard.*'])
        self.env.path = tempfile.mkdtemp()
        self.env.config.set('account-manager', 'login_attempt_max_count', 1)

        self.user = '******'
        self.session = self._create_session(self.user)
        self.guard = AccountGuard(self.env)
Exemple #2
0
 def process_request(self, req):
     env = self.env
     if req.path_info.startswith('/login') and req.authname == 'anonymous':
         guard = AccountGuard(env)
         try:
             referer = self._referer(req)
         except AttributeError:
             # Fallback for Trac 0.11 compatibility.
             referer = req.get_header('Referer')
         # Steer clear of requests going nowhere or loop to self
         if referer is None or \
                 referer.startswith(str(req.abs_href()) + '/login'):
             referer = req.abs_href()
         data = {
             '_dgettext': dgettext,
             'login_opt_list': self.login_opt_list == True,
             'persistent_sessions': AccountManager(env).persistent_sessions,
             'referer': referer,
             'registration_enabled': RegistrationModule(env).enabled,
             'reset_password_enabled':
             AccountModule(env).reset_password_enabled
         }
         if req.method == 'POST':
             self.log.debug('user_locked: ' + \
                            str(req.args.get('user_locked', False)))
             if not req.args.get('user_locked') is True:
                 # TRANSLATOR: Intentionally obfuscated login error
                 data['login_error'] = _("Invalid username or password")
             else:
                 f_user = req.args.get('user')
                 release_time = guard.pretty_release_time(req, f_user)
                 if not release_time is None:
                     data['login_error'] = _(
                         """Account locked, please try again after
                         %(release_time)s
                         """,
                         release_time=release_time)
                 else:
                     data['login_error'] = _("Account locked")
         return 'login.html', data, None
     else:
         n_plural = req.args.get('failed_logins')
         if n_plural > 0:
             chrome.add_warning(
                 req,
                 Markup(
                     tag.span(
                         tag(
                             ngettext(
                                 "Login after %(attempts)s failed attempt",
                                 "Login after %(attempts)s failed attempts",
                                 n_plural,
                                 attempts=n_plural)))))
     return auth.LoginModule.process_request(self, req)
Exemple #3
0
 def process_request(self, req):
     env = self.env
     if req.path_info.startswith('/login') and req.authname == 'anonymous':
         guard = AccountGuard(env)
         try:
             referer = self._referer(req)
         except AttributeError:
             # Fallback for Trac 0.11 compatibility.
             referer = req.get_header('Referer')
         # Steer clear of requests going nowhere or loop to self
         if referer is None or \
                 referer.startswith(str(req.abs_href()) + '/login'):
             referer = req.abs_href()
         data = {
             '_dgettext': dgettext,
             'login_opt_list': self.login_opt_list == True,
             'persistent_sessions': AccountManager(env
                                    ).persistent_sessions,
             'referer': referer,
             'registration_enabled': RegistrationModule(env).enabled,
             'reset_password_enabled': AccountModule(env
                                       ).reset_password_enabled
         }
         if req.method == 'POST':
             self.log.debug('user_locked: ' + \
                            str(req.args.get('user_locked', False)))
             if not req.args.get('user_locked') is True:
                 # TRANSLATOR: Intentionally obfuscated login error
                 data['login_error'] = _("Invalid username or password")
             else:
                 f_user = req.args.get('user')
                 release_time = guard.pretty_release_time(req, f_user)
                 if not release_time is None:
                     data['login_error'] = _(
                         """Account locked, please try again after
                         %(release_time)s
                         """, release_time=release_time)
                 else:
                     data['login_error'] = _("Account locked")
         return 'login.html', data, None
     else:
         n_plural=req.args.get('failed_logins')
         if n_plural > 0:
             chrome.add_warning(req, Markup(tag.span(tag(ngettext(
                 "Login after %(attempts)s failed attempt",
                 "Login after %(attempts)s failed attempts",
                 n_plural, attempts=n_plural
             )))))
     return auth.LoginModule.process_request(self, req)
    def setUp(self):
        self.env = EnvironmentStub(default_data=True,
                                   enable=['trac.*', 'acct_mgr.guard.*'])
        self.env.path = tempfile.mkdtemp()
        self.env.config.set('account-manager', 'login_attempt_max_count', 1)

        self.user = '******'
        self.session = self._create_session(self.user)
        self.guard = AccountGuard(self.env)
Exemple #5
0
 def authenticate(self, req):
     if req.method == 'POST' and req.path_info.startswith('/login'):
         user = self._remote_user(req)
         acctmgr = AccountManager(self.env)
         guard = AccountGuard(self.env)
         if guard.login_attempt_max_count > 0:
             if user is None:
                 if req.args.get('user_locked') is None:
                     # get user for failed authentication attempt
                     f_user = req.args.get('user')
                     req.args['user_locked'] = False
                     if acctmgr.user_known(f_user) is True:
                         if guard.user_locked(f_user) is False:
                             # log current failed login attempt
                             guard.failed_count(f_user, req.remote_addr)
                             if guard.user_locked(f_user) is True:
                                 # step up lock time prolongation
                                 # only when just triggering the lock
                                 guard.lock_count(f_user, 'up')
                                 req.args['user_locked'] = True
                         else:
                             # enforce lock
                             req.args['user_locked'] = True
             else:
                 if guard.user_locked(user) is not False:
                     req.args['user_locked'] = True
                     # void successful login as long as user is locked
                     user = None
                 else:
                     req.args['user_locked'] = False
                     if req.args.get('failed_logins') is None:
                         # Reset failed login attempts counter
                         req.args['failed_logins'] = guard.failed_count(
                             user, reset=True)
         if 'REMOTE_USER' not in req.environ:
             req.environ['REMOTE_USER'] = user
     return auth.LoginModule.authenticate(self, req)
Exemple #6
0
def fetch_user_data(env, req):
    acctmgr = AccountManager(env)
    guard = AccountGuard(env)
    accounts = {}
    for username in acctmgr.get_users():
        if req.perm.has_permission('ACCTMGR_USER_ADMIN'):
            url = req.href.admin('accounts', 'users', user=username)
        else:
            url = None
        accounts[username] = {'username': username, 'review_url': url}
        if guard.user_locked(username):
            accounts[username]['locked'] = True
            t_lock = guard.lock_time(username)
            if t_lock > 0:
                t_release = guard.pretty_release_time(req, username)
                accounts[username]['release_hint'] = _(
                        "Locked until %(t_release)s",
                        t_release=t_release)
    for acct, status in get_user_attribute(env, username=None,
                                           authenticated=None).iteritems():
        account = accounts.get(acct)
        if account is not None and 1 in status:
            # Only use attributes related to authenticated
            # accounts.
            account['name'] = status[1].get('name')
            account['email'] = status[1].get('email')
            if account['email']:
                account['email'] = Chrome(env).format_author(req,
                                                             account['email'])
    ts_seen = last_seen(env)
    if ts_seen is not None:
        for username, last_visit in ts_seen:
            account = accounts.get(username)
            if account and last_visit:
                account['last_visit'] = to_datetime(last_visit)
    return sorted(accounts.itervalues(), key=lambda acct: acct['username'])
Exemple #7
0
def fetch_user_data(env, req):
    acctmgr = AccountManager(env)
    guard = AccountGuard(env)
    accounts = {}
    for username in acctmgr.get_users():
        if req.perm.has_permission('ACCTMGR_USER_ADMIN'):
            url = req.href.admin('accounts', 'users', user=username)
        else:
            url = None
        accounts[username] = {'username': username, 'review_url': url}
        if guard.user_locked(username):
            accounts[username]['locked'] = True
            t_lock = guard.lock_time(username)
            if t_lock > 0:
                t_release = guard.pretty_release_time(req, username)
                accounts[username]['release_hint'] = _(
                    "Locked until %(t_release)s", t_release=t_release)
    for acct, status in get_user_attribute(env,
                                           username=None,
                                           authenticated=None).iteritems():
        account = accounts.get(acct)
        if account is not None and 1 in status:
            # Only use attributes related to authenticated
            # accounts.
            account['name'] = status[1].get('name')
            account['email'] = status[1].get('email')
            if account['email']:
                account['email'] = Chrome(env).format_author(
                    req, account['email'])
    ts_seen = last_seen(env)
    if ts_seen is not None:
        for username, last_visit in ts_seen:
            account = accounts.get(username)
            if account and last_visit:
                account['last_visit'] = to_datetime(last_visit)
    return sorted(accounts.itervalues(), key=lambda acct: acct['username'])
Exemple #8
0
 def authenticate(self, req):
     if req.method == 'POST' and req.path_info.startswith('/login') and \
             req.args.get('user_locked') is None:
         username = self._remote_user(req)
         acctmgr = AccountManager(self.env)
         guard = AccountGuard(self.env)
         if guard.login_attempt_max_count > 0:
             if username is None:
                 # Get user for failed authentication attempt.
                 f_user = req.args.get('username')
                 req.args['user_locked'] = False
                 # Log current failed login attempt.
                 guard.failed_count(f_user, req.remote_addr)
                 if guard.user_locked(f_user):
                     # Step up lock time prolongation only while locked.
                     guard.lock_count(f_user, 'up')
                     req.args['user_locked'] = True
             elif guard.user_locked(username):
                 req.args['user_locked'] = True
                 # Void successful login as long as user is locked.
                 username = None
             else:
                 req.args['user_locked'] = False
                 if req.args.get('failed_logins') is None:
                     # Reset failed login attempts counter.
                     req.args['failed_logins'] = guard.failed_count(
                         username, reset=True)
         else:
             req.args['user_locked'] = False
         if not 'REMOTE_USER' in req.environ or self.environ_auth_overwrite:
             if 'REMOTE_USER' in req.environ:
                 # Complain about another component setting environment
                 # variable for authenticated user.
                 self.env.log.warn("LoginModule.authenticate: "
                                   "'REMOTE_USER' was set to '%s'" %
                                   req.environ['REMOTE_USER'])
             self.env.log.debug("LoginModule.authenticate: Set "
                                "'REMOTE_USER' = '%s'" % username)
             req.environ['REMOTE_USER'] = username
     return auth.LoginModule.authenticate(self, req)
Exemple #9
0
 def authenticate(self, req):
     if req.method == 'POST' and req.path_info.startswith('/login'):
         user = self._remote_user(req)
         acctmgr = AccountManager(self.env)
         guard = AccountGuard(self.env)
         if guard.login_attempt_max_count > 0:
             if user is None:
                 if req.args.get('user_locked') is None:
                     # get user for failed authentication attempt
                     f_user = req.args.get('user')
                     req.args['user_locked'] = False
                     if user_known(self.env, f_user):
                         if guard.user_locked(f_user) is False:
                             # log current failed login attempt
                             guard.failed_count(f_user, req.remote_addr)
                             if guard.user_locked(f_user) is True:
                                 # step up lock time prolongation
                                 # only when just triggering the lock
                                 guard.lock_count(f_user, 'up')
                                 req.args['user_locked'] = True
                         else:
                             # enforce lock
                             req.args['user_locked'] = True
             else:
                 if guard.user_locked(user) is not False:
                     req.args['user_locked'] = True
                     # void successful login as long as user is locked
                     user = None
                 else:
                     req.args['user_locked'] = False
                     if req.args.get('failed_logins') is None:
                         # Reset failed login attempts counter
                         req.args['failed_logins'] = guard.failed_count(
                                                      user, reset = True)
         if 'REMOTE_USER' not in req.environ:
             req.environ['REMOTE_USER'] = user
     return auth.LoginModule.authenticate(self, req)
Exemple #10
0
 def authenticate(self, req):
     if req.method == 'POST' and req.path_info.startswith('/login') and \
             req.args.get('user_locked') is None:
         user = self._remote_user(req)
         acctmgr = AccountManager(self.env)
         guard = AccountGuard(self.env)
         if guard.login_attempt_max_count > 0:
             if user is None:
                 # Get user for failed authentication attempt.
                 f_user = req.args.get('user')
                 req.args['user_locked'] = False
                 # Log current failed login attempt.
                 guard.failed_count(f_user, req.remote_addr)
                 if guard.user_locked(f_user):
                     # Step up lock time prolongation only while locked.
                     guard.lock_count(f_user, 'up')
                     req.args['user_locked'] = True
             elif guard.user_locked(user):
                 req.args['user_locked'] = True
                 # Void successful login as long as user is locked.
                 user = None
             else:
                 req.args['user_locked'] = False
                 if req.args.get('failed_logins') is None:
                     # Reset failed login attempts counter.
                     req.args['failed_logins'] = guard.failed_count(user,
                                                              reset=True)
         else:
             req.args['user_locked'] = False
         if not 'REMOTE_USER' in req.environ or self.environ_auth_overwrite:
             if 'REMOTE_USER' in req.environ:
                 # Complain about another component setting environment
                 # variable for authenticated user.
                 self.env.log.warn("LoginModule.authenticate: "
                                   "'REMOTE_USER' was set to '%s'"
                                   % req.environ['REMOTE_USER'])
             self.env.log.debug("LoginModule.authenticate: Set "
                                "'REMOTE_USER' = '%s'" % user)
             req.environ['REMOTE_USER'] = user
     return auth.LoginModule.authenticate(self, req)
 def expand_macro(self, formatter, name, content):
     env = formatter.env
     req = formatter.req
     if not content:
         args = []
         kw = {}
     else:
         args, kw = parse_args(content)
     if name == 'ProjectStats':
         if 'wiki' in kw.keys():
             prefix = 'prefix' in kw.keys() and kw['prefix'] or None
             wiki = WikiSystem(env)
             if kw['wiki'] == 'count' or 'count' in args:
                 return tag(len(list(wiki.get_pages(prefix))))
     elif name == 'UserQuery':
         msg_no_perm = tag.p(tag_("(required %(perm)s missing)",
                                  perm=tag.strong('USER_VIEW')),
                             class_='hint')
         if 'perm' in kw.keys():
             perm_sys = PermissionSystem(self.env)
             users = perm_sys.get_users_with_permission(kw['perm'].upper())
         else:
             acct_mgr = AccountManager(env)
             users = list(set(acct_mgr.get_users()))
         if 'locked' in kw.keys() or 'locked' in args:
             guard = AccountGuard(env)
             locked = []
             for user in users:
                 if guard.user_locked(user):
                     locked.append(user)
             if kw.get('locked', 'True').lower() in ('true', 'yes', '1'):
                 users = locked
             else:
                 users = list(set(users) - set(locked))
         elif 'visit' in kw.keys() or 'visit' in args:
             if 'USER_VIEW' not in req.perm:
                 return msg_no_perm
             cols = []
             data = {'accounts': fetch_user_data(env, req), 'cls': 'wiki'}
             for col in ('email', 'name'):
                 if col in args:
                     cols.append(col)
             data['cols'] = cols
             return Chrome(env).render_template(req, 'user_table.html',
                                                data, 'text/html', True)
         if kw.get('format') == 'count' or 'count' in args:
             return tag(len(users))
         if 'USER_VIEW' not in req.perm:
             return msg_no_perm
         if 'email' in args or 'name' in args:
             # Replace username with full name, add email if available.
             for username, name, email in self.env.get_known_users():
                 if username in users:
                     if 'name' not in args or name is None:
                         name = username
                     if 'email' in args and email is not None:
                         email = ''.join(['<', email, '>'])
                         name = ' '.join([name, email])
                     if not username == name:
                         users.pop(users.index(username))
                         users.append(name)
         if not users and 'nomatch' in kw.keys():
             return format_to_oneliner(env, formatter.context,
                                       kw['nomatch'])
         users = sorted(users)
         if kw.get('format') == 'list':
             return tag.ul([
                 tag.li(Chrome(env).format_author(req, user))
                 for user in users
             ])
         else:
             # Default output format: comma-separated list.
             return tag(', '.join(
                 [Chrome(env).format_author(req, user) for user in users]))
Exemple #12
0
 def __init__(self):
     self.acctmgr = AccountManager(self.env)
     self.guard = AccountGuard(self.env)
 def expand_macro(self, formatter, name, content):
     env = formatter.env
     req = formatter.req
     if not content:
         args = []
         kw = {}
     else:
         args, kw = parse_args(content)
     if name == 'ProjectStats':
         if 'wiki' in kw.keys():
             prefix = 'prefix' in kw.keys() and kw['prefix'] or None
             wiki = WikiSystem(env)
             if kw['wiki'] == 'count' or 'count' in args:
                 return tag(len(list(wiki.get_pages(prefix))))
     elif name == 'UserQuery':
         msg_no_perm = tag.p(tag_("(required %(perm)s missing)",
                                  perm=tag.strong('USER_VIEW')),
                             class_='hint')
         if 'perm' in kw.keys():
             perm_sys = PermissionSystem(self.env)
             users = perm_sys.get_users_with_permission(kw['perm'].upper())
         else:
             acct_mgr = AccountManager(env)
             users = list(set(acct_mgr.get_users()))
         if 'locked' in kw.keys() or 'locked' in args:
             guard = AccountGuard(env)
             locked = []
             for user in users:
                 if guard.user_locked(user):
                     locked.append(user)
             if kw.get('locked', 'True').lower() in ('true', 'yes', '1'):
                 users = locked
             else:
                 users = list(set(users) - set(locked))
         elif 'visit' in kw.keys() or 'visit' in args:
             if 'USER_VIEW' not in req.perm:
                 return msg_no_perm
             cols = []
             data = {'accounts': fetch_user_data(env, req), 'cls': 'wiki'}
             for col in ('email', 'name'):
                 if col in args:
                     cols.append(col)
             data['cols'] = cols
             return Chrome(env).render_template(
                 req, 'user_table.html', data, 'text/html', True)
         if kw.get('format') == 'count' or 'count' in args:
             return tag(len(users))
         if 'USER_VIEW' not in req.perm:
             return msg_no_perm
         if 'email' in args or 'name' in args:
             # Replace username with full name, add email if available.
             for username, name, email in self.env.get_known_users():
                 if username in users:
                     if 'name' not in args or name is None:
                         name = username
                     if 'email' in args and email is not None:
                         email = ''.join(['<', email, '>'])
                         name = ' '.join([name, email])
                     if not username == name:
                         users.pop(users.index(username))
                         users.append(name)
         if not users and 'nomatch' in kw.keys():
             return format_to_oneliner(env, formatter.context,
                                       kw['nomatch'])
         users = sorted(users)
         if kw.get('format') == 'list':
             return tag.ul([tag.li(Chrome(env).format_author(req, user))
                            for user in users])
         else:
             # Default output format: comma-separated list.
             return tag(', '.join([Chrome(env).format_author(req, user)
                                   for user in users]))
Exemple #14
0
class AccountGuardTestCase(unittest.TestCase):
    def setUp(self):
        self.env = EnvironmentStub(default_data=True,
                                   enable=['trac.*', 'acct_mgr.guard.*'])
        self.env.path = tempfile.mkdtemp()
        self.env.config.set('account-manager', 'login_attempt_max_count', 1)
        self.db = self.env.get_db_cnx()

        self.user = '******'
        self.session = self._create_session(self.user)
        self.guard = AccountGuard(self.env)

    def tearDown(self):
        self.db.close()
        # Really close db connections.
        self.env.shutdown()
        shutil.rmtree(self.env.path)

    # Helpers

    def _create_session(self, user, authenticated=1, name='', email=''):
        args = dict(username=user, name=name, email=email)
        incookie = Cookie()
        incookie['trac_session'] = '123456'
        req = Mock(authname=bool(authenticated) and user or 'anonymous',
                   args=args,
                   base_path='/',
                   chrome=dict(warnings=list()),
                   href=Mock(prefs=lambda x: None),
                   incookie=incookie,
                   outcookie=Cookie(),
                   redirect=lambda x: None)
        req.session = Session(self.env, req)
        req.session.save()
        return req.session

    def _mock_failed_attempt(self, requests=1):
        ipnr = '127.0.0.1'
        ts = to_timestamp(to_datetime(None))
        attempts = eval(self.session.get('failed_logins', '[]'))
        count = int(self.session.get('failed_logins_count', 0))
        lock_count = int(self.session.get('lock_count', 0))
        max = self.env.config.getint('account-manager',
                                     'login_attempt_max_count')
        for r in range(requests):
            attempts.append(dict(ipnr=ipnr, time=ts))
            count += 1
            # Assume, that every lock is enforced.
            if not count < max:
                lock_count += 1
        self.session['failed_logins'] = str(attempts)
        self.session['failed_logins_count'] = count
        self.session['lock_count'] = lock_count
        self.session.save()
        return ts

    # Tests

    def test_failed_count(self):
        ipnr = '127.0.0.1'

        # Won't track anonymous sessions and unknown accounts/users.
        self.assertEqual(self.guard.failed_count(None, ipnr), 0)

        # Regular account without failed attempts logged.
        user = self.user
        # Start without failed attempts logged, accumulating failed attempts.
        self.assertEqual(self.guard.failed_count(user, ipnr), 1)
        self.assertEqual(self.guard.failed_count(user, ipnr), 2)
        # Read failed attempts.
        self.assertEqual(self.guard.failed_count(user, ipnr, None), 2)
        # Reset failed attempts, returning deleted attemps.
        self.assertEqual(self.guard.failed_count(user, reset=True), 2)
        self.assertEqual(self.guard.failed_count(user, reset=None), 0)

    def test_functional(self):
        ipnr = '127.0.0.1'
        user = self.user

        # Regular account without failed attempts logged.
        self.assertEqual(self.guard.lock_count(user), 0)
        self.assertEqual(self.guard.lock_time(user), 0)
        self.assertEqual(self.guard.release_time(user), 0)
        self.assertEqual(self.guard.user_locked(user), False)

        # Log failed attempt - this time with the real method.
        self.assertEqual(self.guard.failed_count(user, ipnr), 1)
        # Mock acct_mgr.LoginModule.authenticate behavior.
        if self.guard.user_locked(user):
            self.guard.lock_count(user, 'up')

        self.assertEqual(self.guard.lock_count(user), 1)
        self.assertEqual(self.guard.lock_time(user), 0)
        self.assertEqual(self.guard.release_time(user), 0)
        self.assertEqual(self.guard.user_locked(user), True)
        # Switch to time lock.
        self.env.config.set('account-manager', 'user_lock_time', 2)
        self.assertTrue(self.guard.release_time(user) > 0)
        self.assertEqual(self.guard.user_locked(user), True)
        sleep(2)
        self.assertEqual(self.guard.user_locked(user), False)

        self.assertEqual(self.guard.lock_time(user), 2)
        self.assertEqual(self.guard.lock_time(user, True), 2)
        self.env.config.set('account-manager', 'user_lock_time_progression', 3)
        self.assertEqual(self.guard.lock_time(user, True), 6)
        # Switch-back to permanent locking.
        self.env.config.set('account-manager', 'user_lock_time', 0)
        self.assertEqual(self.guard.user_locked(user), True)

    def test_lock_count(self):
        user = self.user
        self.assertEqual(self.guard.lock_count(user), 0)
        # Validate helper method too.
        self._mock_failed_attempt()
        # Increment per failed login.
        self.assertEqual(self.guard.lock_count(user, 'set'), 2)
        self.assertEqual(self.guard.lock_count(user), 2)
        # Return updated value on reset as well.
        self.assertEqual(self.guard.lock_count(user, 'reset'), 0)

    def test_lock_time(self):
        self.env.config.set('account-manager', 'user_lock_time', 30)
        self.env.config.set('account-manager', 'user_lock_time_progression', 1)

        # Won't track anonymous sessions and unknown accounts/users.
        self.assertEqual(self.guard.lock_time(None), 0)

        # Regular account without failed attempts logged.
        user = self.user
        self.assertEqual(self.guard.lock_time(user), 30)
        self._mock_failed_attempt(5)
        # Fixed lock time, no progression, with default configuration values.
        self.assertEqual(self.guard.lock_time(user), 30)

        # Preview calculation.
        self.assertEqual(self.guard.lock_time(user, True), 30)
        # Progression with base 3.
        self.env.config.set('account-manager', 'user_lock_time_progression', 3)
        self.assertEqual(self.guard.lock_time(user, True), 30 * 3**5)
        self.env.config.set('account-manager', 'user_lock_max_time', 1800)
        self.assertEqual(self.guard.lock_time(user, True), 1800)

    def test_release_time(self):
        lock_time = 30
        self.env.config.set('account-manager', 'user_lock_time', lock_time)
        self.env.config.set('account-manager', 'user_lock_time_progression', 1)

        # Won't track anonymous sessions and unknown accounts/users.
        self.assertEqual(self.guard.release_time(None), None)

        # Regular account without failed attempts logged.
        user = self.user
        self.assertEqual(self.guard.release_time(user), None)
        # Account with failed attempts logged.
        release_ts = self._mock_failed_attempt() + lock_time
        self.assertEqual(self.guard.release_time(user), release_ts)
        release_ts = self._mock_failed_attempt() + lock_time
        self.assertEqual(self.guard.release_time(user), release_ts)

        # Permanently locked account.
        self.env.config.set('account-manager', 'user_lock_time', 0)
        self.assertEqual(self.guard.release_time(user), 0)

        # Result with locking disabled.
        self.env.config.set('account-manager', 'login_attempt_max_count', 0)
        self.env.config.set('account-manager', 'user_lock_time', 30)
        self.assertEqual(self.guard.release_time(user), None)

    def test_user_locked(self):
        # Won't track anonymous sessions and unknown accounts/users.
        for user in [None, 'anonymous']:
            self.assertEqual(self.guard.user_locked(user), None)
        # Regular account without failed attempts logged.
        user = self.user
        self.assertEqual(self.guard.user_locked(user), False)

        # Permanently locked account.
        self._mock_failed_attempt()
        self.assertEqual(self.guard.user_locked(user), True)
        # Result with locking disabled.
        self.env.config.set('account-manager', 'login_attempt_max_count', 0)
        self.assertEqual(self.guard.user_locked(user), None)
class AccountGuardTestCase(unittest.TestCase):
    def setUp(self):
        self.env = EnvironmentStub(default_data=True,
                                   enable=['trac.*', 'acct_mgr.guard.*'])
        self.env.path = tempfile.mkdtemp()
        self.env.config.set('account-manager', 'login_attempt_max_count', 1)

        self.user = '******'
        self.session = self._create_session(self.user)
        self.guard = AccountGuard(self.env)

    def tearDown(self):
        self.env.shutdown()
        shutil.rmtree(self.env.path)

    # Helpers

    def _create_session(self, user, authenticated=1, name='', email=''):
        args = dict(username=user, name=name, email=email)
        incookie = Cookie()
        incookie['trac_session'] = '123456'
        req = Mock(authname=bool(authenticated) and user or 'anonymous',
                   args=args, base_path='/',
                   chrome=dict(warnings=list()),
                   href=Mock(prefs=lambda x: None),
                   incookie=incookie, outcookie=Cookie(),
                   redirect=lambda x: None)
        req.session = Session(self.env, req)
        req.session.save()
        return req.session

    def _mock_failed_attempt(self, requests=1):
        ipnr = '127.0.0.1'
        ts = to_timestamp(to_datetime(None))
        attempts = eval(self.session.get('failed_logins', '[]'))
        count = int(self.session.get('failed_logins_count', 0))
        lock_count = int(self.session.get('lock_count', 0))
        max_ = self.env.config.getint('account-manager',
                                     'login_attempt_max_count')
        for r in range(requests):
            attempts.append(dict(ipnr=ipnr, time=ts))
            count += 1
            # Assume, that every lock is enforced.
            if not count < max_:
                lock_count += 1
        self.session['failed_logins'] = str(attempts)
        self.session['failed_logins_count'] = count
        self.session['lock_count'] = lock_count
        self.session.save()
        return ts

    # Tests

    def test_failed_count(self):
        ipnr = '127.0.0.1'

        # Won't track anonymous sessions and unknown accounts/users.
        self.assertEqual(self.guard.failed_count(None, ipnr), 0)

        # Regular account without failed attempts logged.
        user = self.user
        # Start without failed attempts logged, accumulating failed attempts.
        self.assertEqual(self.guard.failed_count(user, ipnr), 1)
        self.assertEqual(self.guard.failed_count(user, ipnr), 2)
        # Read failed attempts.
        self.assertEqual(self.guard.failed_count(user, ipnr, None), 2)
        # Reset failed attempts, returning deleted attemps.
        self.assertEqual(self.guard.failed_count(user, reset=True), 2)
        self.assertEqual(self.guard.failed_count(user, reset=None), 0)

    def test_functional(self):
        ipnr = '127.0.0.1'
        user = self.user

        # Regular account without failed attempts logged.
        self.assertEqual(self.guard.lock_count(user), 0)
        self.assertEqual(self.guard.lock_time(user), 0)
        self.assertEqual(self.guard.release_time(user), 0)
        self.assertEqual(self.guard.user_locked(user), False)

        # Log failed attempt - this time with the real method.
        self.assertEqual(self.guard.failed_count(user, ipnr), 1)
        # Mock acct_mgr.LoginModule.authenticate behavior.
        if self.guard.user_locked(user):
            self.guard.lock_count(user, 'up')

        self.assertEqual(self.guard.lock_count(user), 1)
        self.assertEqual(self.guard.lock_time(user), 0)
        self.assertEqual(self.guard.release_time(user), 0)
        self.assertEqual(self.guard.user_locked(user), True)
        # Switch to time lock.
        self.env.config.set('account-manager', 'user_lock_time', 2)
        self.assertTrue(self.guard.release_time(user) > 0)
        self.assertEqual(self.guard.user_locked(user), True)
        sleep(2)
        self.assertEqual(self.guard.user_locked(user), False)

        self.assertEqual(self.guard.lock_time(user), 2)
        self.assertEqual(self.guard.lock_time(user, True), 2)
        self.env.config.set('account-manager', 'user_lock_time_progression',
                            3)
        self.assertEqual(self.guard.lock_time(user, True), 6)
        # Switch-back to permanent locking.
        self.env.config.set('account-manager', 'user_lock_time', 0)
        self.assertEqual(self.guard.user_locked(user), True)

    def test_lock_count(self):
        user = self.user
        self.assertEqual(self.guard.lock_count(user), 0)
        # Validate helper method too.
        self._mock_failed_attempt()
        # Increment per failed login.
        self.assertEqual(self.guard.lock_count(user, 'set'), 2)
        self.assertEqual(self.guard.lock_count(user), 2)
        # Return updated value on reset as well.
        self.assertEqual(self.guard.lock_count(user, 'reset'), 0)

    def test_lock_time(self):
        self.env.config.set('account-manager', 'user_lock_time', 30)
        self.env.config.set('account-manager', 'user_lock_time_progression',
                            1)

        # Won't track anonymous sessions and unknown accounts/users.
        self.assertEqual(self.guard.lock_time(None), 0)

        # Regular account without failed attempts logged.
        user = self.user
        self.assertEqual(self.guard.lock_time(user), 30)
        self._mock_failed_attempt(5)
        # Fixed lock time, no progression, with default configuration values.
        self.assertEqual(self.guard.lock_time(user), 30)

        # Preview calculation.
        self.assertEqual(self.guard.lock_time(user, True), 30)
        # Progression with base 3.
        self.env.config.set('account-manager', 'user_lock_time_progression',
                            3)
        self.assertEqual(self.guard.lock_time(user, True), 30 * 3 ** 5)
        self.env.config.set('account-manager', 'user_lock_max_time', 1800)
        self.assertEqual(self.guard.lock_time(user, True), 1800)

    def test_release_time(self):
        lock_time = 30
        self.env.config.set('account-manager', 'user_lock_time', lock_time)
        self.env.config.set('account-manager', 'user_lock_time_progression',
                            1)

        # Won't track anonymous sessions and unknown accounts/users.
        self.assertEqual(self.guard.release_time(None), None)

        # Regular account without failed attempts logged.
        user = self.user
        self.assertEqual(self.guard.release_time(user), None)
        # Account with failed attempts logged.
        release_ts = self._mock_failed_attempt() + lock_time
        self.assertEqual(self.guard.release_time(user), release_ts)
        release_ts = self._mock_failed_attempt() + lock_time
        self.assertEqual(self.guard.release_time(user), release_ts)

        # Permanently locked account.
        self.env.config.set('account-manager', 'user_lock_time', 0)
        self.assertEqual(self.guard.release_time(user), 0)

        # Result with locking disabled.
        self.env.config.set('account-manager', 'login_attempt_max_count', 0)
        self.env.config.set('account-manager', 'user_lock_time', 30)
        self.assertEqual(self.guard.release_time(user), None)

    def test_user_locked(self):
        # Won't track anonymous sessions and unknown accounts/users.
        for user in [None, 'anonymous']:
            self.assertEqual(self.guard.user_locked(user), None)
        # Regular account without failed attempts logged.
        user = self.user
        self.assertEqual(self.guard.user_locked(user), False)

        # Permanently locked account.
        self._mock_failed_attempt()
        self.assertEqual(self.guard.user_locked(user), True)
        # Result with locking disabled.
        self.env.config.set('account-manager', 'login_attempt_max_count', 0)
        self.assertEqual(self.guard.user_locked(user), None)