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

« back to all changes in this revision

Viewing changes to acct_mgr/htfile.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
1
# -*- coding: utf-8 -*-
2
2
#
3
3
# Copyright (C) 2005,2006,2007 Matthew Good <trac@matt-good.net>
 
4
# Copyright (C) 2011 Steffen Hoffmann <hoff.st@web.de>
4
5
#
5
6
# "THE BEER-WARE LICENSE" (Revision 42):
6
7
# <trac@matt-good.net> wrote this file.  As long as you retain this notice you
10
11
# Author: Matthew Good <trac@matt-good.net>
11
12
 
12
13
import errno
13
 
import os.path
14
 
import fileinput
 
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
15
18
 
16
19
from trac.core import *
17
20
from trac.config import Option
18
21
 
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
22
25
 
23
26
 
24
27
class AbstractPasswordFileStore(Component):
25
 
    """Base class for managing password files such as Apache's htpasswd and
26
 
    htdigest formats.
 
28
    """Base class for managing password files.
27
29
 
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.
29
33
    """
 
34
    abstract = True
30
35
 
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"""))
32
40
 
33
41
    def has_user(self, user):
34
42
        return user in self.get_users()
35
43
 
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)
39
49
            return []
40
50
        return self._get_users(filename)
41
51
 
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)
51
61
 
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)
55
67
            return False
56
68
        user = user.encode('utf-8')
57
69
        password = password.encode('utf-8')
58
70
        prefix = self.prefix(user)
59
 
        fd = file(filename)
60
71
        try:
61
 
            for line in fd:
 
72
            f = open(filename, 'rU')
 
73
            for line in f:
62
74
                if line.startswith(prefix):
63
75
                    return self._check_userline(user, password,
64
 
                                                line[len(prefix):].rstrip('\n'))
65
 
        finally:
66
 
            fd.close()
 
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
 
79
        except:
 
80
            self.log.error('acct_mgr: check_password() -- '
 
81
                           'Can\'t read password file "%s"' % filename)
 
82
            pass
 
83
        if isinstance(f, file):
 
84
            f.close()
67
85
        return None
68
86
 
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.
 
89
 
 
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.
74
94
 
75
95
        Returns `True` if a line matching `prefix` was updated,
76
96
        `False` otherwise.
77
97
        """
78
 
        filename = self.filename
 
98
        filename = str(self.filename)
79
99
        matched = False
 
100
        new_lines = []
80
101
        try:
81
 
            for line in fileinput.input(str(filename), inplace=True):
82
 
                if line.startswith(prefix):
83
 
                    if not matched and userline:
84
 
                        print userline
85
 
                    matched = True
86
 
                elif line.endswith('\n'):
87
 
                    print line,
88
 
                else: # make sure the last line has a newline
89
 
                    print line
 
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.
 
105
            eol = '\n'
 
106
            f = open(filename, 'r')
 
107
            lines = f.readlines()
 
108
 
 
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.
 
116
            if len(lines) > 0:
 
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?
 
122
                        eol = '\r'
 
123
                    elif lines[-1].endswith('\r\n') and os.linesep == '\r\n':
 
124
                        # Windows newline style safeguard
 
125
                        eol = '\r\n'
 
126
 
 
127
                for line in lines:
 
128
                    if line.startswith(prefix):
 
129
                        if not matched and userline:
 
130
                            new_lines.append(userline + eol)
 
131
                        matched = True
 
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
 
138
                    else:
 
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.
 
143
                pass
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.')
 
145
                raise TracError(_(
 
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."""))
97
149
            else:
98
150
                raise
 
151
 
 
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')
101
 
            try:
102
 
                print >>f, userline
103
 
            finally:
104
 
                f.close()
 
155
            new_lines.append(userline + eol)
 
156
 
 
157
        # Try to (re-)open file write-only now and save new content.
 
158
        try:
 
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:
 
163
                raise TracError(_(
 
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."""))
 
167
            else:
 
168
                raise
 
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.
 
173
            f.close()
 
174
            if not f.closed:
 
175
                self.log.debug('acct_mgr: _update_file() -- '
 
176
                               'Closing password file "%s" failed' % filename)
105
177
        return matched
106
178
 
107
179
 
114
186
    [account-manager]
115
187
    password_store = HtPasswdStore
116
188
    password_file = /path/to/trac.htpasswd
 
189
    htpasswd_hash_type = crypt|md5|sha <- None or one of these options
117
190
    }}}
 
191
 
 
192
    Default behaviour is to detect presence of 'crypt' and use it or
 
193
    fallback to generation of passwords with md5 hash otherwise.
118
194
    """
119
195
 
120
196
    implements(IPasswordStore)
121
197
 
 
198
    hash_type = Option('account-manager', 'htpasswd_hash_type', 'crypt',
 
199
        doc = N_("Default hash type of new/updated passwords"))
 
200
 
122
201
    def config_key(self):
123
202
        return 'htpasswd'
124
203
 
126
205
        return user + ':'
127
206
 
128
207
    def userline(self, user, password):
129
 
        return self.prefix(user) + htpasswd(password)
 
208
        return self.prefix(user) + mkhtpasswd(password, self.hash_type)
130
209
 
131
210
    def _check_userline(self, user, password, suffix):
132
211
        return suffix == htpasswd(password, suffix)
133
212
 
134
213
    def _get_users(self, filename):
135
 
        f = open(filename)
 
214
        f = open(filename, 'rU')
136
215
        for line in f:
137
216
            user = line.split(':', 1)[0]
138
217
            if user:
152
231
    }}}
153
232
    """
154
233
 
155
 
 
156
234
    implements(IPasswordStore)
157
235
 
158
 
    realm = Option('account-manager', 'htdigest_realm', '')
 
236
    realm = Option('account-manager', 'htdigest_realm', '',
 
237
        doc = N_("Realm to select relevant htdigest file entries"))
159
238
 
160
239
    def config_key(self):
161
240
        return 'htdigest'