~ubuntu-branches/ubuntu/raring/trac-accountmanager/raring

« back to all changes in this revision

Viewing changes to acct_mgr/guard.py

  • Committer: Package Import Robot
  • Author(s): Leo Costela
  • Date: 2012-06-30 20:40:10 UTC
  • mfrom: (1.1.4)
  • mto: This revision was merged to the branch mainline in revision 7.
  • Revision ID: package-import@ubuntu.com-20120630204010-xyoy9dnabof4jsbo
* new upstream checkout (closes: #654292)
* convert to dh short style and "--with python2"
* bump dh compat to 9
* update watch file for new version (0.3.2 based on setup.py; no 
  official release)
* move packaging to git (dump old out-of-sync history)
* debian/control: bump policy to 3.9.3 (no changes)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
 
 
3
# Author: Steffen Hoffmann <hoff.st@web.de>
 
4
 
 
5
from datetime           import timedelta
 
6
 
 
7
from trac.config        import Configuration, IntOption, Option
 
8
from trac.core          import Component
 
9
from trac.util.datefmt  import format_datetime, pretty_timedelta, \
 
10
                               to_datetime
 
11
try:
 
12
    from trac.util.datefmt  import to_utimestamp
 
13
except ImportError:
 
14
    # Fallback for Trac 0.11 compatibility
 
15
    from trac.util.datefmt  import to_timestamp as to_utimestamp
 
16
 
 
17
from acct_mgr.api       import AccountManager
 
18
 
 
19
 
 
20
class AccountGuard(Component):
 
21
    """The AccountGuard component protects against brute-force attacks
 
22
    on user passwords.
 
23
 
 
24
    It does so by adding logging of failed login attempts,
 
25
    account status tracking and administative user account locking.
 
26
    Configurable time-locks with exponential lock time prolongation
 
27
    allow to balance graceful handling of failed login attempts and
 
28
    reasonable protection against attempted brute-force attacks. 
 
29
    """
 
30
 
 
31
    login_attempt_max_count = IntOption(
 
32
        'account-manager', 'login_attempt_max_count', 0,
 
33
        doc="""Lock user account after specified number of login attempts.
 
34
            Value zero means no limit.""")
 
35
    user_lock_time = IntOption(
 
36
        'account-manager', 'user_lock_time', '0',
 
37
        doc="""Drop user account lock after specified time (seconds).
 
38
            Value zero means unlimited lock time.""")
 
39
    user_lock_max_time = IntOption(
 
40
        'account-manager', 'user_lock_max_time', '86400',
 
41
        doc="""Limit user account lock time to specified time (seconds).
 
42
            This is relevant only with user_lock_time_progression > 1.""")
 
43
    user_lock_time_progression = Option(
 
44
        'account-manager', 'user_lock_time_progression', '1',
 
45
        doc="""Extend user account lock time incrementally. This is
 
46
            based on logarithmic calculation and decimal numbers accepted:
 
47
            Value '1' means constant lock time per failed login attempt.
 
48
            Value '2' means double locktime after 2nd lock activation,
 
49
            four times the initial locktime after 3rd, and so on.""")
 
50
 
 
51
    def __init__(self):
 
52
        self.mgr = AccountManager(self.env)
 
53
 
 
54
        # adjust related value to promote sane configurations
 
55
        if not self.login_attempt_max_count > 0:
 
56
            self.config.set('account-manager', 'user_lock_max_time', '0')
 
57
 
 
58
    def failed_count(self, user, ipnr = None, reset = False):
 
59
        """Report number of previously logged failed login attempts.
 
60
 
 
61
        Enforce login policy with regards to tracking of login attempts
 
62
        and user account lock behavior.
 
63
        Default `False` for reset value causes logging of another attempt.
 
64
        `None` value for reset just reads failed login attempts count.
 
65
        `True` value for reset triggers final log deletion.
 
66
        """
 
67
        db = self.env.get_db_cnx()
 
68
        cursor = db.cursor()
 
69
        cursor.execute("""
 
70
            SELECT value
 
71
              FROM session_attribute
 
72
             WHERE authenticated=1 AND
 
73
                   name='failed_logins_count' AND sid=%s
 
74
            """, (user,))
 
75
        count = None
 
76
        for row in cursor:
 
77
            count = eval(row[0])
 
78
            break
 
79
        if count is None:
 
80
            count = 0
 
81
        if reset is None:
 
82
            # report failed attempts count
 
83
            return count
 
84
 
 
85
        if reset is not True:
 
86
            # failed attempt logger
 
87
            attempts = self.get_failed_log(user)
 
88
            log_length = len(attempts)
 
89
            if log_length > self.login_attempt_max_count:
 
90
                # truncate attempts list
 
91
                del attempts[:(log_length - self.login_attempt_max_count)]
 
92
            attempts.append({'ipnr': ipnr,
 
93
                             'time': to_utimestamp(to_datetime(None))})
 
94
            count += 1
 
95
            # update or create existing attempts list
 
96
            for key, value in [('failed_logins', str(attempts)),
 
97
                               ('failed_logins_count', count)]:
 
98
                sql = """
 
99
                    WHERE   authenticated=1
 
100
                        AND name=%s
 
101
                        AND sid=%s
 
102
                    """
 
103
                cursor.execute("""
 
104
                    UPDATE  session_attribute
 
105
                        SET value=%s
 
106
                """ + sql, (value, key, user))
 
107
                cursor.execute("""
 
108
                    SELECT  value
 
109
                    FROM    session_attribute
 
110
                """ + sql, (key, user))
 
111
                if cursor.fetchone() is None:
 
112
                    cursor.execute("""
 
113
                        INSERT
 
114
                        INTO session_attribute
 
115
                                 (sid,authenticated,name,value)
 
116
                        VALUES   (%s,1,%s,%s)
 
117
                    """, (user, key, value))
 
118
            db.commit()
 
119
            self.log.debug(
 
120
                'AcctMgr:failed_count(%s): ' % user + str(count))
 
121
            return count
 
122
        else:
 
123
            # delete existing attempts list
 
124
            cursor.execute("""
 
125
                DELETE
 
126
                FROM   session_attribute
 
127
                WHERE  authenticated=1
 
128
                    AND (name='failed_logins'
 
129
                        OR name='failed_logins_count')
 
130
                    AND sid=%s
 
131
                """, (user,))
 
132
            db.commit()
 
133
            # delete lock count too
 
134
            self.lock_count(user, 'reset')
 
135
            return count
 
136
 
 
137
    def get_failed_log(self, user):
 
138
        """Returns an iterable of previously logged failed login attempts.
 
139
 
 
140
        Iterable content: {'ipnr': ipnr, 'time': posix_microsec_time_stamp}
 
141
        """
 
142
        db = self.env.get_db_cnx()
 
143
        cursor = db.cursor()
 
144
        cursor.execute("""
 
145
            SELECT  value
 
146
            FROM    session_attribute
 
147
            WHERE   authenticated=1
 
148
                AND name='failed_logins'
 
149
                AND sid=%s
 
150
            """, (user,))
 
151
        # read list and add new attempt
 
152
        attempts = []
 
153
        for row in cursor:
 
154
            attempts = eval(row[0])
 
155
            break
 
156
        return attempts
 
157
 
 
158
    def lock_count(self, user, action = 'get'):
 
159
        """Count, log and report, how often in succession user account
 
160
        lock conditions have been met.
 
161
 
 
162
        This is the exponent for lock time prolongation calculation too.
 
163
        """
 
164
        db = self.env.get_db_cnx()
 
165
        cursor = db.cursor()
 
166
        if not action == 'reset':
 
167
            sql = """
 
168
                WHERE   authenticated=1
 
169
                    AND name='lock_count'
 
170
                    AND sid=%s
 
171
                """ 
 
172
            cursor.execute("""
 
173
                SELECT  value
 
174
                FROM    session_attribute
 
175
            """ + sql, (user,))
 
176
            lock_count = None
 
177
            for row in cursor:
 
178
                lock_count = eval(row[0])
 
179
                break
 
180
            if action == 'get':
 
181
                return (lock_count or 0)
 
182
            else:
 
183
                # push and update cached lock_count
 
184
                if lock_count is None:
 
185
                    # create lock_count cache
 
186
                    cursor.execute("""
 
187
                        INSERT INTO session_attribute
 
188
                                (sid,authenticated,name,value)
 
189
                        VALUES  (%s,1,'lock_count',%s)
 
190
                        """, (user, 1))
 
191
                    lock_count = 1
 
192
                else:
 
193
                    lock_count += 1
 
194
                    cursor.execute("""
 
195
                        UPDATE  session_attribute
 
196
                            SET value=%s
 
197
                        """ + sql, (lock_count, user))
 
198
                db.commit()
 
199
                return lock_count
 
200
        else:
 
201
            # reset/delete lock_count cache
 
202
            cursor.execute("""
 
203
                DELETE
 
204
                FROM    session_attribute
 
205
                WHERE   authenticated=1
 
206
                    AND name='lock_count'
 
207
                    AND sid=%s
 
208
                """, (user,))
 
209
            db.commit()
 
210
            return 0
 
211
 
 
212
    def lock_time(self, user, next = False):
 
213
        """Calculate current time-lock length a user.
 
214
        """
 
215
        base = float(self.user_lock_time_progression)
 
216
        lock_count = self.lock_count(user)
 
217
        if not lock_count > 0:
 
218
            return 0
 
219
        else:
 
220
            if next is False:
 
221
                exponent = lock_count - 1
 
222
            else:
 
223
                exponent = lock_count
 
224
        t_lock = self.user_lock_time * 1000000 * base ** exponent
 
225
        # limit maximum lock time
 
226
        t_lock_max = self.user_lock_max_time * 1000000
 
227
        if t_lock > t_lock_max:
 
228
            t_lock = t_lock_max
 
229
        self.log.debug('AcctMgr:lock_time(%s): ' % user + str(t_lock))
 
230
        return t_lock
 
231
 
 
232
    def pretty_lock_time(self, user, next = False):
 
233
        """Convenience method for formatting lock time to string.
 
234
        """
 
235
        t_lock = self.lock_time(user, next)
 
236
        return (t_lock > 0) and pretty_timedelta(to_datetime(None) - \
 
237
            timedelta(microseconds = t_lock)) or None
 
238
 
 
239
    def pretty_release_time(self, req, user):
 
240
        """Convenience method for formatting lock time to string.
 
241
        """
 
242
        ts_release = self.release_time(user)
 
243
        if ts_release is None:
 
244
            return None
 
245
        return format_datetime(to_datetime(
 
246
            self.release_time(user)), tzinfo=req.tz)
 
247
 
 
248
    def release_time(self, user):
 
249
        # logged attempts required for further checking
 
250
        attempts = self.get_failed_log(user)
 
251
        if len(attempts) > 0:
 
252
            t_lock = self.lock_time(user)
 
253
            if t_lock == 0:
 
254
                return None
 
255
            return (attempts[-1]['time'] + t_lock)
 
256
 
 
257
    def user_locked(self, user):
 
258
        """Returns whether the user account is currently locked.
 
259
 
 
260
        Expect False, if not, and True, if locked.
 
261
        """
 
262
        if not self.login_attempt_max_count > 0:
 
263
            # account locking turned off by configuration
 
264
            return None
 
265
        count = self.failed_count(user, reset=None)
 
266
        ts_release = self.release_time(user)
 
267
        if count < self.login_attempt_max_count:
 
268
            self.log.debug(
 
269
                'AcctMgr:user_locked(%s): False (try left)' % user)
 
270
            return False
 
271
        else:
 
272
            if ts_release is None:
 
273
                # permanently locked
 
274
                self.log.debug(
 
275
                    'AcctMgr:user_locked(%s): True (permanently)' % user)
 
276
                return True
 
277
        # time-locked or time-lock expired
 
278
        self.log.debug(
 
279
            'AcctMgr:user_locked(%s): ' % user + \
 
280
            str((ts_release - to_utimestamp(to_datetime(None))) > 0))
 
281
        return ((ts_release - to_utimestamp(to_datetime(None))) > 0)