10
11
# Author: Matthew Good <trac@matt-good.net>
14
import os # to get not only os.path method but os.linesep too
15
# DEVEL: Use `with` statement for better file access code,
16
# taking care of Python 2.5, but not needed for Python >= 2.6
17
#from __future__ import with_statement
16
19
from trac.core import *
17
20
from trac.config import Option
19
from api import IPasswordStore
20
from pwhash import htpasswd, htdigest
21
from util import EnvRelativePathOption
22
from acct_mgr.api import IPasswordStore, _, N_
23
from acct_mgr.pwhash import htpasswd, mkhtpasswd, htdigest
24
from acct_mgr.util import EnvRelativePathOption
24
27
class AbstractPasswordFileStore(Component):
25
"""Base class for managing password files such as Apache's htpasswd and
28
"""Base class for managing password files.
28
See the concrete sub-classes for usage information.
30
Derived classes support different formats such as
31
Apache's htpasswd and htdigest format.
32
See these concrete sub-classes for usage information.
31
filename = EnvRelativePathOption('account-manager', 'password_file')
36
# DEVEL: This option is subject to removal after next major release.
37
filename = EnvRelativePathOption('account-manager', 'password_file', '',
38
doc = N_("""Path relative to Trac environment or full host machine
39
path to password file"""))
33
41
def has_user(self, user):
34
42
return user in self.get_users()
36
44
def get_users(self):
37
filename = self.filename
45
filename = str(self.filename)
38
46
if not os.path.exists(filename):
47
self.log.error('acct_mgr: get_users() -- '
48
'Can\'t locate password file "%s"' % filename)
40
50
return self._get_users(filename)
42
def set_password(self, user, password):
52
def set_password(self, user, password, old_password = None):
43
53
user = user.encode('utf-8')
44
54
password = password.encode('utf-8')
45
55
return not self._update_file(self.prefix(user),
50
60
return self._update_file(self.prefix(user), None)
52
62
def check_password(self, user, password):
53
filename = self.filename
63
filename = str(self.filename)
54
64
if not os.path.exists(filename):
65
self.log.error('acct_mgr: check_password() -- '
66
'Can\'t locate password file "%s"' % filename)
56
68
user = user.encode('utf-8')
57
69
password = password.encode('utf-8')
58
70
prefix = self.prefix(user)
72
f = open(filename, 'rU')
62
74
if line.startswith(prefix):
63
75
return self._check_userline(user, password,
64
line[len(prefix):].rstrip('\n'))
76
line[len(prefix):].rstrip('\n'))
77
# DEVEL: Better use new 'finally' statement here, but
78
# still need to care for Python 2.4 (RHEL5.x) for now
80
self.log.error('acct_mgr: check_password() -- '
81
'Can\'t read password file "%s"' % filename)
83
if isinstance(f, file):
69
87
def _update_file(self, prefix, userline):
70
"""If `userline` is empty the line starting with `prefix` is
71
removed from the user file. Otherwise the line starting with `prefix`
72
is updated to `userline`. If no line starts with `prefix` the
73
`userline` is appended to the file.
88
"""Add or remove user and change password.
90
If `userline` is empty, the line starting with `prefix` is removed
91
from the user file. Otherwise the line starting with `prefix`
92
is updated to `userline`. If no line starts with `prefix`,
93
the `userline` is appended to the file.
75
95
Returns `True` if a line matching `prefix` was updated,
78
filename = self.filename
98
filename = str(self.filename)
81
for line in fileinput.input(str(filename), inplace=True):
82
if line.startswith(prefix):
83
if not matched and userline:
86
elif line.endswith('\n'):
88
else: # make sure the last line has a newline
102
# Open existing file read-only to read old content.
103
# DEVEL: Use `with` statement available in Python >= 2.5
104
# as soon as we don't need to support 2.4 anymore.
106
f = open(filename, 'r')
107
lines = f.readlines()
109
# DEVEL: Beware, in shared use there is a race-condition,
110
# since file changes by other programs that occure from now on
111
# are currently not detected and will get overwritten.
112
# This could be fixed by file locking, but a cross-platform
113
# implementation is certainly non-trivial.
114
# DEVEL: I've seen the AtomicFile object in trac.util lately,
115
# that may be worth a try.
117
# predict eol style for lines without eol characters
118
if not os.linesep == '\n':
119
if lines[-1].endswith('\r') and os.linesep == '\r':
120
# antique MacOS newline style safeguard
121
# DEVEL: is this really still needed?
123
elif lines[-1].endswith('\r\n') and os.linesep == '\r\n':
124
# Windows newline style safeguard
128
if line.startswith(prefix):
129
if not matched and userline:
130
new_lines.append(userline + eol)
132
# preserve existing lines with proper eol
133
elif line.endswith(eol) and not \
134
(eol == '\n' and line.endswith('\r\n')):
135
new_lines.append(line)
136
# unify eol style using confirmed default and
137
# make sure the (last) line has a newline anyway
139
new_lines.append(line.rstrip('\r\n') + eol)
90
140
except EnvironmentError, e:
91
141
if e.errno == errno.ENOENT:
92
pass # ignore when file doesn't exist and create it below
142
# Ignore, when file doesn't exist and create it below.
93
144
elif e.errno == errno.EACCES:
94
raise TracError('The password file could not be updated. '
95
'Trac requires read and write access to both '
96
'the password file and its parent directory.')
146
"""The password file could not be read. Trac requires
147
read and write access to both the password file
148
and its parent directory."""))
152
# Finally add the new line here, if it wasn't used before
153
# to update or delete a line, creating content for a new file as well.
99
154
if not matched and userline:
100
f = open(filename, 'a')
155
new_lines.append(userline + eol)
157
# Try to (re-)open file write-only now and save new content.
159
f = open(filename, 'w')
160
f.writelines(new_lines)
161
except EnvironmentError, e:
162
if e.errno == errno.EACCES or e.errno == errno.EROFS:
164
"""The password file could not be updated. Trac requires
165
read and write access to both the password file
166
and its parent directory."""))
169
# DEVEL: Better use new 'finally' statement here, but
170
# still need to care for Python 2.4 (RHEL5.x) for now
171
if isinstance(f, file):
172
# Close open file now, even after exception raised.
175
self.log.debug('acct_mgr: _update_file() -- '
176
'Closing password file "%s" failed' % filename)