1
# -*- coding: utf-8 -*-
3
# Author: Steffen Hoffmann <hoff.st@web.de>
5
from datetime import timedelta
7
from trac.config import Configuration, IntOption, Option
8
from trac.core import Component
9
from trac.util.datefmt import format_datetime, pretty_timedelta, \
12
from trac.util.datefmt import to_utimestamp
14
# Fallback for Trac 0.11 compatibility
15
from trac.util.datefmt import to_timestamp as to_utimestamp
17
from acct_mgr.api import AccountManager
20
class AccountGuard(Component):
21
"""The AccountGuard component protects against brute-force attacks
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.
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.""")
52
self.mgr = AccountManager(self.env)
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')
58
def failed_count(self, user, ipnr = None, reset = False):
59
"""Report number of previously logged failed login attempts.
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.
67
db = self.env.get_db_cnx()
71
FROM session_attribute
72
WHERE authenticated=1 AND
73
name='failed_logins_count' AND sid=%s
82
# report failed attempts count
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))})
95
# update or create existing attempts list
96
for key, value in [('failed_logins', str(attempts)),
97
('failed_logins_count', count)]:
104
UPDATE session_attribute
106
""" + sql, (value, key, user))
109
FROM session_attribute
110
""" + sql, (key, user))
111
if cursor.fetchone() is None:
114
INTO session_attribute
115
(sid,authenticated,name,value)
117
""", (user, key, value))
120
'AcctMgr:failed_count(%s): ' % user + str(count))
123
# delete existing attempts list
126
FROM session_attribute
127
WHERE authenticated=1
128
AND (name='failed_logins'
129
OR name='failed_logins_count')
133
# delete lock count too
134
self.lock_count(user, 'reset')
137
def get_failed_log(self, user):
138
"""Returns an iterable of previously logged failed login attempts.
140
Iterable content: {'ipnr': ipnr, 'time': posix_microsec_time_stamp}
142
db = self.env.get_db_cnx()
146
FROM session_attribute
147
WHERE authenticated=1
148
AND name='failed_logins'
151
# read list and add new attempt
154
attempts = eval(row[0])
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.
162
This is the exponent for lock time prolongation calculation too.
164
db = self.env.get_db_cnx()
166
if not action == 'reset':
168
WHERE authenticated=1
169
AND name='lock_count'
174
FROM session_attribute
178
lock_count = eval(row[0])
181
return (lock_count or 0)
183
# push and update cached lock_count
184
if lock_count is None:
185
# create lock_count cache
187
INSERT INTO session_attribute
188
(sid,authenticated,name,value)
189
VALUES (%s,1,'lock_count',%s)
195
UPDATE session_attribute
197
""" + sql, (lock_count, user))
201
# reset/delete lock_count cache
204
FROM session_attribute
205
WHERE authenticated=1
206
AND name='lock_count'
212
def lock_time(self, user, next = False):
213
"""Calculate current time-lock length a user.
215
base = float(self.user_lock_time_progression)
216
lock_count = self.lock_count(user)
217
if not lock_count > 0:
221
exponent = lock_count - 1
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:
229
self.log.debug('AcctMgr:lock_time(%s): ' % user + str(t_lock))
232
def pretty_lock_time(self, user, next = False):
233
"""Convenience method for formatting lock time to string.
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
239
def pretty_release_time(self, req, user):
240
"""Convenience method for formatting lock time to string.
242
ts_release = self.release_time(user)
243
if ts_release is None:
245
return format_datetime(to_datetime(
246
self.release_time(user)), tzinfo=req.tz)
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)
255
return (attempts[-1]['time'] + t_lock)
257
def user_locked(self, user):
258
"""Returns whether the user account is currently locked.
260
Expect False, if not, and True, if locked.
262
if not self.login_attempt_max_count > 0:
263
# account locking turned off by configuration
265
count = self.failed_count(user, reset=None)
266
ts_release = self.release_time(user)
267
if count < self.login_attempt_max_count:
269
'AcctMgr:user_locked(%s): False (try left)' % user)
272
if ts_release is None:
275
'AcctMgr:user_locked(%s): True (permanently)' % user)
277
# time-locked or time-lock expired
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)