Edgewall Software

Ticket #287: register.diff

File register.diff, 16.3 KB (added by mgood, 4 years ago)

Pluggable module-based registration

  • templates/account.cs

    === templates/account.cs
    ==================================================================
     
     1<?cs include "header.cs"?> 
     2<?cs include "macros.cs"?> 
     3 
     4<div id="ctxtnav" class="nav"></div> 
     5 
     6<div id="content" class="register"> 
     7 
     8 <h1>My Account</h1> 
     9 
     10 <p> 
     11 Manage your user account. 
     12 </p> 
     13 
     14 <?cs if account.error ?> 
     15 <div class="system-message"> 
     16  <h2>Error</h2> 
     17  <p><?cs var:account.error ?></p> 
     18 </div> 
     19 <?cs /if ?> 
     20 
     21 <?cs if account.message ?> 
     22 <p><?cs var:account.message ?></p> 
     23 <?cs /if ?> 
     24 
     25 <form method="post" action=""> 
     26  <div> 
     27   <input type="hidden" name="action" value="change_password" /> 
     28   <label for="password">New Password:</label> 
     29   <input type="password" id="password" name="password" class="textwidget" 
     30          size="20" /> 
     31  </div> 
     32  <div> 
     33   <label for="password_confirm">Confirm Password:</label> 
     34   <input type="password" id="password_confirm" name="password_confirm" 
     35          class="textwidget" size="20" /> 
     36  </div> 
     37  <input type="submit" value="Change password" /> 
     38 </form> 
     39 
     40 <form method="post" action="" 
     41       onsubmit="return confirm('Are you sure you want to delete your account?');"> 
     42  <input type="hidden" name="action" value="delete" /> 
     43  <input type="submit" value="Delete account" /> 
     44 </form> 
     45 
     46</div> 
     47 
     48<?cs include:"footer.cs"?> 
  • templates/register.cs

    === templates/register.cs
    ==================================================================
     
     1<?cs include "header.cs"?> 
     2<?cs include "macros.cs"?> 
     3 
     4<div id="ctxtnav" class="nav"></div> 
     5 
     6<div id="content" class="register"> 
     7 
     8 <h1>Register an account</h1> 
     9 
     10 <p> 
     11 Description 
     12 </p> 
     13 
     14 <?cs if registration.error ?> 
     15 <div class="system-message"> 
     16  <h2>Error</h2> 
     17  <p><?cs var:registration.error ?></p> 
     18 </div> 
     19 <?cs /if ?> 
     20 
     21 <form method="post" action=""> 
     22  <div> 
     23   <input type="hidden" name="action" value="create" /> 
     24   <label for="user">Username:</label> 
     25   <input type="text" id="user" name="user" class="textwidget" size="20" /> 
     26  </div> 
     27  <div> 
     28   <label for="password">Password:</label> 
     29   <input type="password" id="password" name="password" class="textwidget" size="20" /> 
     30  </div> 
     31  <div> 
     32   <label for="password_confirm">Confirm Password:</label> 
     33   <input type="password" id="password_confirm" name="password_confirm" 
     34          class="textwidget" size="20" /> 
     35  </div> 
     36  <input type="submit" value="Create account" /> 
     37 </form> 
     38 
     39</div> 
     40 
     41<?cs include:"footer.cs"?> 
  • trac/util.py

    === trac/util.py
    ==================================================================
     
    375375                    return '</span>' 
    376376                return '<span class="code-%s">' % mtype 
    377377 
     378class switch(object): 
     379    def __init__(self, value): 
     380        self.value = value 
     381        self.fall = False 
     382 
     383    def __iter__(self): 
     384        """Return the match method once, then stop""" 
     385        yield self.match 
     386        raise StopIteration 
     387 
     388    def match(self, *args): 
     389        """Indicate whether or not to enter a case suite""" 
     390        if self.fall or not args: 
     391            return True 
     392        elif self.value in args: # changed for v1.5, see below 
     393            self.fall = True 
     394            return True 
     395        else: 
     396            return False 
     397 
  • trac/Account.py

    === trac/Account.py
    ==================================================================
     
     1# -*- coding: iso8859-1 -*- 
     2# 
     3# Copyright (C) 2005 Matthew Good <trac@matt-good.net> 
     4# 
     5# Trac is free software; you can redistribute it and/or 
     6# modify it under the terms of the GNU General Public License as 
     7# published by the Free Software Foundation; either version 2 of the 
     8# License, or (at your option) any later version. 
     9# 
     10# Trac is distributed in the hope that it will be useful, 
     11# but WITHOUT ANY WARRANTY; without even the implied warranty of 
     12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
     13# General Public License for more details. 
     14# 
     15# You should have received a copy of the GNU General Public License 
     16# along with this program; if not, write to the Free Software 
     17# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 
     18# 
     19# Author: Matthew Good <trac@matt-good.net> 
     20 
     21from __future__ import generators 
     22 
     23import os.path 
     24import fileinput 
     25import inspect 
     26import md5 
     27from binascii import hexlify 
     28 
     29from trac import perm, util 
     30from trac.util import switch 
     31from trac.core import * 
     32from trac.web.chrome import INavigationContributor 
     33from trac.web.main import IRequestHandler 
     34from trac.md5crypt import md5crypt 
     35 
     36class IPasswordStore(Interface): 
     37    def config_key(self): 
     38        ''' 
     39        Returns a string used to identify this implementation in the config. 
     40        This password storage implementation will be used if the value of 
     41        the config property "account-management.password_format" matches. 
     42        ''' 
     43 
     44    def get_users(self): 
     45        ''' 
     46        Returns an iterable of the known usernames 
     47        ''' 
     48 
     49    def has_user(self, user): 
     50        ''' 
     51        Returns whether the user account exists. 
     52        ''' 
     53 
     54    def set_password(self, user, password): 
     55        ''' 
     56        Sets the password for the user.  This should create the user account 
     57        if it doesn't already exist. 
     58        ''' 
     59 
     60    def delete_user(self, user): 
     61        ''' 
     62        Deletes the user account. 
     63        ''' 
     64 
     65# os.urandom was added in Python 2.4 
     66# try to fall back on reading from /dev/urandom on older Python versions 
     67try: 
     68    from os import urandom 
     69except ImportError: 
     70    def urandom(n): 
     71        return open('/dev/urandom').read(n) 
     72 
     73class AccountModule(Component): 
     74 
     75    implements(INavigationContributor, IRequestHandler) 
     76 
     77    #INavigationContributor methods 
     78    def get_active_navigation_item(self, req): 
     79        return 'account' 
     80 
     81    def get_navigation_items(self, req): 
     82        if not self.is_enabled(): 
     83            return 
     84        if req.authname != 'anonymous': 
     85            yield 'metanav', 'account', '<a href="%s">My Account</a>' \ 
     86                  % (self.env.href.account()) 
     87 
     88    # IRequestHandler methods 
     89    def match_request(self, req): 
     90        return self.is_enabled() and req.path_info == '/account' 
     91 
     92    def process_request(self, req): 
     93        if req.authname == 'anonymous': 
     94            req.redirect(self.env.href.wiki()) 
     95        action = req.args.get('action') 
     96        if req.method == 'POST': 
     97            for case in switch(action): 
     98                if case('change_password'): 
     99                    self._do_change_password(req) 
     100                    break 
     101                elif case('delete'): 
     102                    self._do_delete(req) 
     103                    break 
     104        return 'account.cs', None 
     105 
     106    def _do_change_password(self, req): 
     107        user = req.authname 
     108        password = req.args.get('password') 
     109        if not password: 
     110            req.hdf['account.error'] = 'Password cannot be empty.' 
     111            return 
     112 
     113        if password != req.args.get('password_confirm'): 
     114            req.hdf['account.error'] = 'The passwords must match.' 
     115            return 
     116 
     117        AccountManager(self.env).set_password(user, password) 
     118        req.hdf['account.message'] = 'Password successfully updated.' 
     119 
     120    def _do_delete(self, req): 
     121        user = req.authname 
     122        AccountManager(self.env).delete_user(user) 
     123        req.redirect(self.env.href.logout()) 
     124 
     125    def is_enabled(self): 
     126        return self.config.get('account-management', 
     127                               'enabled').lower() in util.TRUE 
     128 
     129 
     130class RegistrationModule(Component): 
     131 
     132    implements(INavigationContributor, IRequestHandler) 
     133 
     134    #INavigationContributor methods 
     135    def get_active_navigation_item(self, req): 
     136        return 'register' 
     137 
     138    def get_navigation_items(self, req): 
     139        if not self.is_enabled(): 
     140            return 
     141        if req.authname == 'anonymous': 
     142            yield 'metanav', 'register', '<a href="%s">Register</a>' \ 
     143                  % (self.env.href.register()) 
     144    # IRequestHandler methods 
     145 
     146    def match_request(self, req): 
     147        return self.is_enabled() and req.path_info == '/register' 
     148 
     149    def process_request(self, req): 
     150        if req.authname != 'anonymous': 
     151            req.redirect(self.env.href.account()) 
     152        action = req.args.get('action') 
     153        if req.method == 'POST' and action == 'create': 
     154            self._do_create(req) 
     155        return 'register.cs', None 
     156 
     157    def is_enabled(self): 
     158        return self.config.get('account-management', 
     159                               'enabled').lower() in util.TRUE and \ 
     160               self.config.get('account-management', 
     161                               'registration_enabled').lower() in util.TRUE 
     162 
     163    def _do_create(self, req): 
     164        mgr = AccountManager(self.env) 
     165 
     166        user = req.args.get('user') 
     167        if mgr.has_user(user): 
     168            req.hdf['registration.error'] = \ 
     169                'Another account with that name already exists.' 
     170            return 
     171 
     172        password = req.args.get('password') 
     173        if not password: 
     174            req.hdf['registration.error'] = 'Password cannot be empty.' 
     175            return 
     176 
     177        if password != req.args.get('password_confirm'): 
     178            req.hdf['registration.error'] = 'The passwords must match.' 
     179            return 
     180 
     181        mgr.set_password(user, password) 
     182        req.redirect(self.env.href.login()) 
     183 
     184class AccountManager(Component): 
     185 
     186    stores = ExtensionPoint(IPasswordStore) 
     187 
     188    def __init__(self): 
     189        self.store_map = {} 
     190        for s in self.stores: 
     191            self.store_map[s.config_key()] = s 
     192 
     193    def _dispatch(self, func): 
     194        return getattr(self._get_store(), func) 
     195 
     196    def _get_store(self): 
     197        fmt = self.config.get('account-management', 'password_format') 
     198        return self.store_map[fmt] 
     199 
     200class DispatchProperty(object): 
     201    def __init__(self, name, fget): 
     202        self.name = name 
     203        self.fget = fget 
     204 
     205    def __get__(self, obj, objtype=None): 
     206        if obj is None: 
     207            return self 
     208        return self.fget(obj, self.name) 
     209 
     210# Add the IPasswordStore methods to the AccountManager to dispatch to the 
     211# active implementation 
     212for func, v in inspect.getmembers(IPasswordStore, inspect.ismethod): 
     213    setattr(AccountManager, func, DispatchProperty(func, AccountManager._dispatch)) 
     214 
     215class AbstractPasswordFileStore(Component): 
     216    ''' 
     217    Abstract class to use as a base for Apache's htpasswd and htdigest style 
     218    password file formats 
     219    ''' 
     220 
     221    def has_user(self, user): 
     222        return user in self.get_users() 
     223 
     224    def get_users(self): 
     225        if not os.path.exists(self._get_filename()): 
     226            return [] 
     227        return self._get_users(self._get_filename()) 
     228 
     229    def set_password(self, user, password): 
     230        self._update_file(self.prefix(user), self.userline(user, password)) 
     231 
     232    def delete_user(self, user): 
     233        self._update_file(self.prefix(user), None) 
     234 
     235    def _get_filename(self): 
     236        return self.config.get('account-management', 'password_file') 
     237 
     238    def _update_file(self, prefix, userline): 
     239        filename = self._get_filename() 
     240        written = False 
     241        if os.path.exists(filename): 
     242            for line in fileinput.input(filename, inplace=True): 
     243                if not line.startswith(prefix): 
     244                    print line, 
     245                elif not written and userline: 
     246                    print userline 
     247                    written = True 
     248        if not written and userline: 
     249            f = open(filename, 'a') 
     250            print >>f, userline 
     251            f.close() 
     252 
     253 
     254def salt(): 
     255    s = '' 
     256    v = long(hexlify(urandom(4)), 16) 
     257    itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' 
     258    for i in range(8): 
     259        s += itoa64[v & 0x3f]; v >>= 6 
     260    return s 
     261 
     262 
     263class HtPasswdStore(AbstractPasswordFileStore): 
     264 
     265    implements(IPasswordStore) 
     266 
     267    def config_key(self): 
     268        return 'htpasswd' 
     269 
     270    def prefix(self, user): 
     271        return user + ':' 
     272 
     273    def userline(self, user, password): 
     274        return self.prefix(user) + md5crypt(password, salt(), 
     275                                            '$apr1$') 
     276 
     277    def _get_users(self, filename): 
     278        f = open(filename) 
     279        for line in f: 
     280            user = line.split(':', 1)[0] 
     281            if user: 
     282                yield user 
     283 
     284 
     285class HtDigestStore(AbstractPasswordFileStore): 
     286 
     287    implements(IPasswordStore) 
     288 
     289    def __init__(self): 
     290        self.realm = self.config.get('account-management', 'htdigest_realm') 
     291 
     292    def config_key(self): 
     293        return 'htdigest' 
     294 
     295    def prefix(self, user): 
     296        return '%s:%s:' % (user, self.realm) 
     297 
     298    def userline(self, user, password): 
     299        p = self.prefix(user) 
     300        return p + md5.new(p + password).hexdigest() 
     301 
     302    def _get_users(self, filename): 
     303        f = open(filename) 
     304        for line in f: 
     305            args = line.split(':')[:2] 
     306            if len(args) == 2: 
     307                user, realm = args 
     308                if realm == self.realm and user: 
     309                    yield user 
     310 
  • trac/md5crypt.py

    === trac/md5crypt.py
    ==================================================================
     
     1# Based on FreeBSD src/lib/libcrypt/crypt.c 1.2 
     2# http://www.freebsd.org/cgi/cvsweb.cgi/~checkout~/src/lib/libcrypt/crypt.c?rev=1.2&content-type=text/plain 
     3 
     4# Original license: 
     5# * "THE BEER-WARE LICENSE" (Revision 42): 
     6# * <phk@login.dknet.dk> wrote this file.  As long as you retain this notice you 
     7# * can do whatever you want with this stuff. If we meet some day, and you think 
     8# * this stuff is worth it, you can buy me a beer in return.   Poul-Henning Kamp 
     9 
     10# This port adds no further stipulations.  I forfeit any copyright interest. 
     11 
     12import md5 
     13 
     14def md5crypt(password, salt, magic='$1$'): 
     15    # /* The password first, since that is what is most unknown */ /* Then our magic string */ /* Then the raw salt */ 
     16    m = md5.new() 
     17    m.update(password + magic + salt) 
     18 
     19    # /* Then just as many characters of the MD5(pw,salt,pw) */ 
     20    mixin = md5.md5(password + salt + password).digest() 
     21    for i in range(0, len(password)): 
     22        m.update(mixin[i % 16]) 
     23 
     24    # /* Then something really weird... */ 
     25    # Also really broken, as far as I can tell.  -m 
     26    i = len(password) 
     27    while i: 
     28        if i & 1: 
     29            m.update('\x00') 
     30        else: 
     31            m.update(password[0]) 
     32        i >>= 1 
     33 
     34    final = m.digest() 
     35 
     36    # /* and now, just to make sure things don't run too fast */ 
     37    for i in range(1000): 
     38        m2 = md5.md5() 
     39        if i & 1: 
     40            m2.update(password) 
     41        else: 
     42            m2.update(final) 
     43 
     44        if i % 3: 
     45            m2.update(salt) 
     46 
     47        if i % 7: 
     48            m2.update(password) 
     49 
     50        if i & 1: 
     51            m2.update(final) 
     52        else: 
     53            m2.update(password) 
     54 
     55        final = m2.digest() 
     56 
     57    # This is the bit that uses to64() in the original code. 
     58 
     59    itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' 
     60 
     61    rearranged = '' 
     62    for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)): 
     63        v = ord(final[a]) << 16 | ord(final[b]) << 8 | ord(final[c]) 
     64        for i in range(4): 
     65            rearranged += itoa64[v & 0x3f]; v >>= 6 
     66 
     67    v = ord(final[11]) 
     68    for i in range(2): 
     69        rearranged += itoa64[v & 0x3f]; v >>= 6 
     70 
     71    return magic + salt + '$' + rearranged 
     72 
     73if __name__ == '__main__': 
     74 
     75    def test(clear_password, the_hash): 
     76        magic, salt = the_hash[1:].split('$')[:2] 
     77        magic = '$' + magic + '$' 
     78        return md5crypt(clear_password, salt, magic) == the_hash 
     79 
     80    test_cases = ( 
     81        (' ', '$1$yiiZbNIH$YiCsHZjcTkYd31wkgW8JF.'), 
     82        ('pass', '$1$YeNsbWdH$wvOF8JdqsoiLix754LTW90'), 
     83        ('____fifteen____', '$1$s9lUWACI$Kk1jtIVVdmT01p0z3b/hw1'), 
     84        ('____sixteen_____', '$1$dL3xbVZI$kkgqhCanLdxODGq14g/tW1'), 
     85        ('____seventeen____', '$1$NaH5na7J$j7y8Iss0hcRbu3kzoJs5V.'), 
     86        ('__________thirty-three___________', '$1$HO7Q6vzJ$yGwp2wbL5D7eOVzOmxpsy.'), 
     87        ('apache', '$apr1$J.w5a/..$IW9y6DR0oO/ADuhlMF5/X1') 
     88    ) 
     89 
     90    for clearpw, hashpw in test_cases: 
     91        if test(clearpw, hashpw): 
     92            print '%s: pass' % clearpw 
     93        else: 
     94            print '%s: FAIL' % clearpw