trac-accountmanager-0.6~svn18547/0000755000175500017550000000000014513322506016366 5ustar debacledebacletrac-accountmanager-0.6~svn18547/.gitignore0000644000175500017550000000006314410443274020357 0ustar debacledebacle*.py[co] *.mo *.rej /build /dist /*.egg-info /.tox trac-accountmanager-0.6~svn18547/changelog0000644000175500017550000004217513313106623020246 0ustar debacledebacleAuthor: Matthew Good Maintainer: Steffen Hoffmann acct_mgr-0.5 (not yet released) - branch 0.11 resolved issues * #8217: Race condition when creating new accounts * #8796: notifications also get sent to the smtp_always_cc address * #10740: Checkbox columns are too wide on the Accounts: Cleanup page * #10754: Selected checkboxes should determine Accounts: Cleanup page items * #10772: ProgrammingError: operator does not exist: text = integer * #10829: Consider renaming 'Save' buttons to 'Apply changes' * #10910: Unable to change full name * #11038: Login fails due to issues with opening sibling Trac environments * #11090: Error on user creation "gaierror: [Errno -2] Name or service ..." * #11111: Please add a MANIFEST.in * #11213: The username input doesn't have focus when the page loads * #11312: Variable 'user' does not exist (in api.py) * #11469: Exceptions in AccountModule are not trapped in Trac 1.0.2dev * #11798: Display message when account is pending approval * #11867: Incorrect body for reset password email notification * #11991: RegistrationError cannot initialize on Python 2.4 * #12024: Username not inserted from admin panel * #12050: Ticket preferences are not saved * #12058: Bad email default regexp * #12067: Impossible to delete email * fix access to account properties clean-up from user admin panel * always show associated email in user details admin panel * fix and unify web-UI feedback on user and admin actions * prevent bypassing email address policy via user preferences * add modular user ID change support for Trac core as well as for some other Trac plugins, currently: * AnnouncerPlugin > 0.12.1 * ScreenshotsPlugin (all versions) * TracFormsPlugin > v0.2 * VotePlugin (all versions) new features * #843: Make admin approval required for account registration * #6788: Add a RadiusAuthStore to AccountManagerPlugin * #7426: Dynamicity of Trac (Show number of registered users) * #8930: Setup wizard for AcctMgr * #8595: Ability to ban accounts * #10680: Provide confirmation when password is changed * #10684: Provide feedback when performing actions on the Users page * #10739: Move 'Back to Accounts' button to the contextual navigation * #10741: Provide email verification status indicator on user admin panel * #10742: Rename "Update" button to "Refresh" on Review Account Details page * #10745: Add 'Select all' checkbox to header of list in user admin panel * #11214: Rename db_cleanup to admin_db_cleanup * #11215: Set focus when admin Users page loads * #11894: Make (clear that) username policy (is) configurable * #12054: add QuestionRegisterPlugin-like functionality * #12097: Provide confirmation when deleting accounts * #12534: Configuration admin can be disabled independent of User admin * add unit tests for db access functions * add account guard configuration to config admin panel * filter account list in user admin panel by account status * remake account editor following Trac admin panel style i.e. for enums acct_mgr-0.4.4 (03-Apr-2014) - branch 0.11 resolved issues * escape email for notification message against reported xss vulnerability acct_mgr-0.4.3 (13-Feb-2013) - branch 0.11 resolved issues * #8927: LoginModule with .htpasswd & passwd reset => not working * #10681: User with empty password can't reset their password * #10765: AttributeError: 'NoneType' object has no attribute 'strip' * #10871: AccountGuard destroys trac.ini * prevent two ways for a user to bypass a forced password change * skip BotTrapCheck on admin user requests, i.e. from inside user admin panel * keep trac.auth.LoginModule options defined after disabling that component by defining all options in acct_mgr.web_ui.LoginModule as well new features * allow logging-in into password-less accounts via acct_mgr.LoginModule acct_mgr-0.4.2 (27-Dec-2012) - branch 0.11 resolved issues * #10730: AccountGuard.lock_time effectively disables account locking acct_mgr-0.4.1 (26-Dec-2012) - branch 0.11 resolved issues * #5964: Prevent multiple calls to LoginModule._remote_user() by re-using a flag introduced for account locking * #8545: Authentication always fails by introducing authentication attempt debug logging and a new option 'environ_auth_overwrite' for additional control over REMOTE_USER's value * #10134: HttpAuth login throws traceback * #10625: AssertionError in trac.db.pool.PooledConnection.__del__ * #10700: AccountModule._do_reset_password discards error from _reset_password * #10701: Reset password reports `Cannot find ... "IPasswordHashMethod"` * several fixes for unreported account guard issues acct_mgr-0.4 (01-Dec-2012) - branch 0.11 resolved issues * #3459: Authentication information not available * #4677: Admin based chaining HtDigestStore & HtPasswdStore breaks config by adding dedicated options 'htdigest_file' and 'htpasswd_file' * #5691: No cookie warning shown when trying to log in with Konqueror * #6616: Invalid entries for usernames in table by adding a cleaner macro implementation outside of `UserStatsMacro` * #8685: User deletion ordering breaks 'deleted' notification for SessionStore * #8770: AttributeError: Cannot find implementation of "IPasswordHashMethod" * #8990: HtPasswdStore and SessionStore with HtPasswdHashMethod share option by adding dedicated options 'db_htpasswd_hash_type' and 'db_htdigest_realm' * #9052: acct_mgr.web_ui.emailverificationmodule - Doesn't send email * #9079: PostgreSQL: Database error when creating new user with attributes * #9090: AccountManager plugin does not email after user registration * #9139: SvnServePasswordStore and case sensitivity * #9246: InternalError when refresh_passwd = true * #9252: All session attributes are deleted when user logs in first time * #9547: Option `persistent_sessions` is not working in `0.4dev-r10747` * #9843: New user missing in 'session' table. * #9940: Admin unable to reset password * #10023: SQL Injection in acct_mgr.api.AccountManager.lastseen() * #10028: Account delete does not purge user's auth cookie * #10123: Registration with EmailVerification should instruct more clearly * #10204: Users can delete their email address even when verify_email=true * #10276: "Unknown preference panel" when logging out from account tab * #10397: Don't allow username with all capital letters * #10412: acct_mgr-0.4dev breaking 2.4 compatibility * #10594: Some options' docs are missing * #10644: Add a real license * do AccountManager class API cleanup by moving db access to model layer * prevent duplicate action entries in Trac core permission select box new features * #874: Add new fields to register form and a registration validation system * #5295: Add optional username regexp to registration checks * #7577: Prevent spammers from registering * #8076: Add optional account email regexp to registration checks * #8791: Obsolete patch needed for authentication against Jira by adding sha256/sha512 hash support (needs `passlib` or extended `crypt`) * #9618: HttpAuthStore authentication enhancement by allowing a relative URL for `authentication_url` configuration option * #9676: Incorporate optional Single-Sign-On functionality * #9852: Embed some user information in TracWiki by introducing WikiMacros `ProjectStats` and `UserQuery` * #10142: Allow admin to override verification status * add recursion to option parser for configuration admin page and provide available valid values for an `ExtensionOption` like `IPasswordHashMethod` by a select field (dropdown box) - or meaningful message on missing options * add cleanup page for purging `session_attribute` db table via admin web UI * add randomized authentication cookie ID refreshment, average refresh rate controlled by new option `cookie_refresh_pct` * switch to case-less username duplicate checking * add unit tests i.e. for hash creation and re-written registration checks to significantly extend code coverage * add Trac style shading of odd/even rows to user lists acct_mgr-0.3.2 (26-Aug-2011) - branch 0.11 resolved issues * #9051: Unable to add users due to existing email addresses by fixing SQL statements responsible for db cleanup on account deletion * #9082: Remove cookie's `expires` param (0.12) when rememberme is unchecked * #9088: Expire trac_auth_session cookie before LoginModule._do_logout * #9091: tags in user registration notification * #9092: TypeError: __call__() got an unexpected keyword argument 'link' * #9093: A href tags in verification notice * #9095: Delete session cookie if client sent it and rememberme is unchecked * #9099: Expire session cookie whenever trac_auth cookie gets expired * #9107: Error when building the egg file * #9108: TypeError: 'NoneType' object is not iterable * #9109: TypeError: 'NoneType' object is not iterable * fix TypeError in account details admin page for not yet authenticated users * make option `verify_email` effective for `RegistrationModule` * fix bug from initial password store chaining implementation leading to false-positives on user store discovery and later unexpected login failure * change account details admin page into users admin subpage acct_mgr-0.3.1 (13-Jul-2011) - branch 0.11 resolved issues * #8963: Restore compatibility with Trac 0.11 - holding 10 different issues * further improve redirect loop protection (infinite loop after /login) * add more verbose error log messages for missing/unreadable password file * remove duplicated message in Trac 0.11 at account details admin page * prevent argument duplication on POST requests of account details admin page * enable admin to restart password hash refresh from configuration admin page acct_mgr-0.3 (07-Jul-2011) - branch 0.11 resolved issues * #3233: Infinite redirect loop after resetting the password * #3783: Form based login fails to forward nicely on referrer outside of Trac * #3989: Email verification and password reset with notification lock users * #4040: TracError instance has no attribute 'acctmgr' on new user creation * #4160: Password reset oddness with multiple projects config * #5247: Stack trace escapes to user when htdigest file is not writeable * #6821: Register and 'Forgot your password?' links can no longer be enabled * #7850: Error after upgrade from 0.11 to trunk version * #7863: Syntax error found when building egg * #7880: 'ioerror: invalid mode: Ur' in htfile.py * #8061: An input element has no child nodes * #8063: Better i18n codes * #8381: Failure to verify valid passwords after migration Windows => FreeBSD * #8534: Can't resend password reset email * #8549: Changing password in SessionStore if forced has no effect * #8663: Disable register link on the login page * #8834: TypeError: sequence item 0: expected string, int found * #8813: German docs of options, even when browser's locale isn't 'de' * #8925: Register form user field should be username * #8936: Cannot delete user using AccountModule from web_ui * #8939: Fix for "mgr" not found error in http.py * fix AccountModule.reset_password_enabled() from type list to boolean * really disable reset password page, if feature is disabled * fix password reset procedure (preventing easy account takeover) new features * #442: Add email verification for new/changed email addresses by completing a matured procedure i.e. with account details display * #809: Fit long user list in users admin page to one screen height * #816: 'forgot password' should not reset password directly by introducing a separate ResetPwStore (a SessionStore derivate) * #2966: Add user account (name, email) edit support to user account page * #6803: Add i18n/l10n support adding i18n setup and message markup and several translations complete (>95%): English (default), German, Japanese, Russian, Swedish convenient (>75%): Czech, Italian partial (>33%): Dutch, French, Spanish Check https://www.transifex.net/projects/p/Trac_Plugin-L10N/ for more recently added and updated translations * #7111: Password reset from users admin page * #7437: Lock user after configurable number of failed login attempts by a new AccountGuard module for login attempt tracking and account locking * #8257: Display PasswordStore option docs on configuration admin page * #8487: AcctMgr creates blank lines in password_file under Windows * #8563: IndexError: list index out of range * #8774: KeyError: acct_mgr.web_ui after failed import of acct_mgr.web_ui * #8814: Generic word `for` is extracted, term is difficult to translate * #8843: XHTML invalid account_verify_email.html * extend AccountManager class API by 'email_verified' and 'user_known' * re-design 'ugly' HTML login form adding new 'login_opt_list' option and contribute recommended CSS styles * add account details admin page * add auth cookie options introduced in Trac 0.12 * add optional password hash refresh on successful login * code cleanup and more readable multiline SQL statement formatting * add changelog (this file) * add OpenPGP signed md5 and sha1 hash lists and verification script backported - branch 0.10 * #8381: Failure to verify valid passwords after migration Windows => FreeBSD * fix password reset procedure (preventing easy account takeover) acct_mgr-0.2.x (updates to 0.2.1, never officially released) - branch 0.11 resolved issues * #831: Case sensitive Authentication, but Case in-sensitive Authorization * #1382: Make 'Delete Account' function on 'My Account' page optional * #1602: Pass old_password when changing password * #1922: ValueError with HttpAuthStore when entering invalid credentials * #2044: AccountManagerPlugin README missing an example for HttpAuth backend * #2327: Fix unicode support in htdigest password file store * #2630: Registration of usernames which can corrupt a SvnServePasswordStore * #3086: Admin "Last Login" users info should use correct time zone * #3137: Fix tests and include functional tests * #3200: Add and register user corrupts password file with no carriage return * #3343: Error onClick 'Remove selected accounts' when no account is selected * #3401: Removing email from preferences makes account unusable * #4125: Fix message wrapper for AccountModule and EmailVerificationModule * #4276: HtPasswdStore changes ownership of htpasswd file (bad file IO) * #4525: SvnServePasswordStore looks at wrong place for svnserve.conf file * #4628: Fix SessionStore unicode errors htdigest hash method * #4682: Registration of user names with colon could corrupt htpasswd file * #4830: NameError: global name 'sorted' is not defined on Python 2.3 * #4895: AccountManagerPlugin + Trac 0.12 (no attribute 'smtp_server') * #4897: TracAccountManager htpasswd file handling clobbers symlinks * #4984: Prefer hashlib over deprecated md5 and sha * #5509: EmailVerificationModule undocumented, allows email-less registration * #5514: Typo 'acct_mge' in web_ui.py in 0.11 branch * #5789: Change description on notification admin page * #6453: AttributeError: 'NoneType' object has no attribute 'encode' * #6730: AnnouncerPlugin compatibility with AccountManager * #7087: Trailing spaces are not being removed from the username * #7396: Password salts and randomness length * #7576: Users redirected with no confirmation, fail to note register success * #7687: Always redirect to referer after login * #7807: Show error into 'after-registration form' * extend and fix IPasswordStore API implementation for HttpAuthStore * improve error reporting for failures in password stores * fix a bunch of small typos in Python doc-strings and elsewhere * redirect anonymous GET '/verify_email', no more 'email already verified' * several fixes against infinite redirect loop conditions new features * #131: Add 'Remember Me' functionality adding a new 'persistent_sessions' option * #442: Email verification for new/changed addresses * #1902: Allow more granular permissions * #2282: Make default htpasswd hash type configurable * #3153: Easy option to disable email verification * #3726: Split admin pages in seperate components * #5299: Improvements to the email verification page * #7700: Allow user management without having TRAC_ADMIN permission * added support for chained password stores * added password change in the users admin page * extend username checks before registration adding a new 'username_char_blacklist' option backported - branch 0.10 * #2327: Fix unicode support in htdigest password file store * #3200: Add and register user corrupts password file with no carriage return * #4125: Fix message wrapper for AccountModule and EmailVerificationModule * #4628: Fix SessionStore unicode errors htdigest hash method * #4830: NameError: global name 'sorted' is not defined on Python 2.3 acct_mgr-0.2.1 (28-May-2008) - branch 0.11 new features * #147: Email notification of account related events ToDo: historic entries below are still incomplete - more work needed acct_mgr-0.2 (10-Nov-2006) - new branch 0.11 * add SessionStore for storing user passwords as a Trac session attribute acct_mgr-0.1.3 (13-Nov-2006) branch 0.10 new features * #173: Integrate login-related plugins by adding HttpAuthStore acct_mgr-0.1.2 (10-Nov-2006) - new branch 0.10 acct_mgr-0.1.1 (10-Jan-2006) - new branch 0.9 acct_mgr-0.1 (20-Jul-2005) - initial release trac-accountmanager-0.6~svn18547/tox.ini0000644000175500017550000000074414413553530017710 0ustar debacledebacle[tox] envlist = py27-trac{12,14} py3{5,6,7,8,9,10,11}-trac15 [testenv] deps = py{27,35}: twill>=2,<3 py3{6,7,8,9,10,11,12}: twill>=2 pytidylib; os_name!="nt" py27,py3{6,7,8,9,10,11}: Babel passlib bcrypt Pyrad; os_name!="nt" trac12: Trac~=1.2.0 trac14: Trac~=1.4.0 trac15: https://download.edgewall.org/trac/Trac-1.5.4.tar.gz setenv = TMP = {envtmpdir} commands = {envpython} -Wdefault -m unittest -v acct_mgr.tests.test_suite trac-accountmanager-0.6~svn18547/contrib/0000755000175500017550000000000013065031720020022 5ustar debacledebacletrac-accountmanager-0.6~svn18547/contrib/signatures.py0000644000175500017550000000747313065031720022573 0ustar debacledebacle#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2011,2013 Steffen Hoffmann # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Steffen Hoffmann import os import sys from hashlib import md5, sha1 def walktree(top, filter): files = [] for items in os.walk('.'): if len(items[2]) < 1: # Skip empty directories. continue for filename in items[2]: path = ''.join([top, items[0].lstrip('.'), '/', filename]) if path not in filter: files.append(path) return files def _open(path, mode='rb'): f = None if 'w' not in mode and not os.path.exists(path): print('Can\'t locate "%s"' % path) else: try: f = open(path, mode) except: print('Can\'t read "%s"' % path) pass return f def sign(action='r'): filter_ = [] passed = True top = os.path.abspath('.') if action in ['r', 'w']: md5sums = _open(''.join([top, '/', 'acct_mgr-md5sums']), action) if md5sums: # Skip recursive operation on hash files. filter_.append(md5sums.name) sha1sums = _open(''.join([top, '/', 'acct_mgr-sha1sums']), action) if sha1sums: filter_.append(sha1sums.name) else: print('Error: Unsupported operation "%s".' % action) return hashes = {} f = None for path in walktree(top, filter_): f = _open(path, 'rb') lines = f.readlines() path = path[len(top) + 1:] # Skip SVN support files, if present. if '.svn/' in path: continue hashes[path] = {} m = md5() m.update(''.join(lines)) hashes[path]['md5'] = m.hexdigest() s = sha1() s.update(''.join(lines)) hashes[path]['sha1'] = s.hexdigest() if action == 'r': if md5sums: for line in md5sums.readlines(): sum_, path = line.strip(' \n').split(' ') if path not in hashes.keys(): print('md5: "%s" missing' % path) passed = False elif not hashes[path].pop('md5') == sum_: print('md5: "%s" changed' % path) passed = False if sha1sums: for line in sha1sums.readlines(): sum_, path = line.strip(' \n').split(' ') if path not in hashes.keys(): print('sha1: "%s" missing' % path) passed = False elif not hashes[path].pop('sha1') == sum_: print('sha1: "%s" changed' % path) passed = False for path in hashes.keys(): if len(hashes[path]) > 0: for hashtype in hashes[path].keys(): if (md5sums and hashtype == 'md5') or \ (sha1sums and hashtype == 'sha1'): # This is non-fatal, but warn about it anyway. print('%s: "%s" unknown (added)' % (hashtype, path)) elif action == 'w': for path in sorted(hashes.keys()): md5sums.write(''.join([hashes[path]['md5'], ' ', path, '\n'])) sha1sums.write(''.join([hashes[path]['sha1'], ' ', path, '\n'])) # DEVEL: Better use new 'finally' statement here, but # still need to care for Python 2.4 (RHEL5.x) for now if isinstance(f, file): f.close() for f in [md5sums, sha1sums]: if isinstance(f, file): f.close() if action == 'r' and md5sums and sha1sums and passed is True: print('Check passed.') if __name__ == '__main__': if len(sys.argv) > 1: sign(sys.argv[1]) else: sign() trac-accountmanager-0.6~svn18547/contrib/approval.xcf.bz20000644000175500017550000001354512076643325023067 0ustar debacledebacleBZh91AY&SY[6ar׳/CKs{}NkP۷UZA|E4jidOM ښ'ɣ4m#=1Oj)lS6d=M6)M4zMm&!AFd`D=M 4iEO$dѓ"fFcI& J~J4=4z m@C@h@=@E0dd&4 M0hꙔڛDjIzѠE=hjy42SPjDe=MFP=@4 P FP@h5ODM1#OIz 2d= PƠODɠ d 1h 4LBii=&)4S&ڙ5f#n\Iqoh;l};NbƫUUt׍Zͯ^5 ugm=BxXj:_-*t%]N廛HOLRY (Z^DYP]$e⥄RU]`@oT/=s#ͅQ#!BؕjyiD⧯ed| ,F*ӖA)ɬHr6'߳t h3ґu8fDm@)ԥ, beTJ\0.A,QH*]XUєĆX#}M!1m/, IB)ҙ|E)-(2ҙCf"*58O3"%@ց \‹lDKDk<4I8 k@$Ar`NGh(5z}ZE{qxo( {'Ph'd A﷊{2=D]r= o330 ߼+l@_kqs+qU z`;:F&U;Ͻb|8rˉbbVììjE8\告" )M9 /q `4iE(ءFH$ 4n&w6St!Gud 2h*C-J @|@H K 񩃺SkXbsIZBz@z7ʚ?&"܈5%dUwYǫ}9UIl/'V,s1D>],i4O9X0I9u}%> DN'j3&=|6[u?8{vR>v*UsYꞑ/T,"|}]X 9rwF| 4r˩a!qo?Buʲ^o0&|\LHu_Ȭ *xiR̝$jŐXnA׋H<6Dp:9.]:S; !8~,=ֺN {tX  >/#SZ S%&m]7gY:C 9(tzX cA2Ai{iX-y=#s]n&aK2ǒ^ϞT&).S,v}! 2xg\h=xۡ<;L2}I DNz4w~*P] ɕZQH&"EE#0QHlKE{Q=+>yڧY4O>̯ 'NZP .rӼ`G[4"i I4ʫD"n 4B5 t P!T |+ \*a1k*9dZ-JSWLo!aLصsSؐ٢ .G TȆR" 3u"3Aq2%ZˏI$JAhń2's 31.CS~<N3b :EZ最Hy AFI)l%)Wy31))|ΩV:;}i^QI*zij/;^] wɺE&K C/,EyLa8ϫVsGKy}m@8P!JIR5 ,,$:P-D* BNH =׷}Qʑh}4hR||Lz; RkH;|y굋 'GjwE?8.r )#u{"ьdo'S3U TU%^)Jm=2LVA$p> x7Zԝ E6ˋ X[M@)(J .H~ӺT)'bF^1T\nP#DS&6%^c]sr]D@>bb - biS%HbN .-\=t F:.:Ӯާ>?=kkqX'nvUZ#T=Y{Zj_pcXUprrއ/#Rev]!4MH= 7 ձLF KӞ?wZVH QΜ@  ̰֦Eqm틸cE~lMMύӛ0roR'`qxtEFT ,5 :"9 YdL"Zjvx%6YmՁtܒ quYZPv{;g/,_}ʦq[*E *ȢsT֝RY%%kC!p :E M{|6I02DR9Bzf! q#EWfȓ\z`)7NWd?}F Dɰ,:"gdWi]VROz +϶q'`1eĢ$ ސdJE${e,N4DX K-duqL0Mvn-3SNlq&OHC Q:6ƠvR#h8:r8)Ai!j"Me=G6u|hm4+A}& eJ{ Eji HhŶz`>`rGDpcJ"(T41SE5!V\ѐ}eBe߉`V6.N]al&RaV0$ Kk:'bA%*Ww Njޭek<\{K᳈ v.M!GWz $.66&څ}ieIsjsqpK5:ig?arD&A&5UJY*JJafKjT n3捵/5r$:QMH˞@^@;LkYfM_ o sOI.2 7+"ArĜrCk`U?> ';>ݜSZ!dENh2a qQٽm0-azclA X&~VG &7TLv ^!6"|ZbyZ.5AS瞐_'+. x`)̃aHfAH`RdtCk (`?&}IV ׀N9[/1p8*2! ܱ0M] JsT&QgIMx)Py)Y{nwcΖlB45{s2KNmIUU/iM*v y/rėʢU)rHOJh4W[K#U5kįd/-mhdVS׳|a`XbgVm˽UrRiSů$AW+4jp==Gz߭Y{կ 2ߘU$n%o wYN4UE*hb14~'Z% Te1moh- d)m6$4敵Pa3!+V UB̷*-Uf M2ԓ51-1QePPR3WtFl9wl 1)gf嶙[`R =5ze0@10 IMx"k\,$QLvKRm͎g~7u@U ~;w,,JA4LYoBĬfjܡEĄ6U_OHm;C8P録;>p6}L([|P8,سc!|ka5Y,5#7kfQn@[ӿyp mLbv!L70K/,S # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Steffen Hoffmann import sys from trac.env import Environment env = Environment(sys.argv[1]) if env: with env.db_transaction as db: fixed = 0 attempts = [] for sid, failed_logins in db(""" SELECT sid, value FROM session_attribute WHERE authenticated=1 AND name='failed_logins' """): l = [] replace = False for attempt in eval(failed_logins): time = attempt['time'] if isinstance(time, long): # Convert microseconds to seconds. time = int(time / 1000000) fixed += 1 replace = True l.append(dict(ipnr=attempt['ipnr'], time=time)) if replace: attempts.append((str(l), sid)) db.executemany(""" UPDATE session_attribute SET value=%s WHERE sid=%s AND authenticated=1 AND name='failed_logins' """, attempts) print >>, 'INFO: Fixed %s timestamp(s) in %s dataset(s).' \ % (fixed, len(attempts)) else: print >> sys.stderr, 'Error: Trac environment %s not found.' % sys.argv[1] sys.exit(1) trac-accountmanager-0.6~svn18547/contrib/style.css0000644000175500017550000000125013065031720021672 0ustar debacledebacle/* CSS definitions for AccountManagerPlugin */ /* styles for login page - new style */ /* Copy this file into your /htdocs/ folder, add the style definitions below to an existing one or include in another way, if you already have some Trac customization in place. */ body { background-color: #efefef; } div.login { background-color: #ffffff; margin: 2em auto 0; width: 420px; border: 1px solid #666666; } div.login h1 { margin-top: 1em; text-align: center; width: 100%; } div#login_options ul { margin: 1em 0pt; text-align: center; } .central { text-align: center; } .spacer { height: 7em; } .textbox { text-indent: 0.5em; margin: 0 120px 0.5em; } trac-accountmanager-0.6~svn18547/contrib/refresh.xcf.bz20000644000175500017550000003552212076643325022700 0ustar debacledebacleBZh91AY&SYe$Q 11@z+_uyȋ7:ej޾z0{|wyXkg7u׋^hvҽ^h :D-J]훻e%TPF#BzS"&=Qe3!4bMO&MShB4ɩL!@jSzjzJx)M@jyѦBmLb3Dɣ!hM &&&jm !lzTQ?"4hOT SOƩjz~O54 I&A44M1 fML&&hi3A3Fjz4RySUFe6dJ?=4ji=F'OaڞSxSړѵA MM55<ѓh7頦OC Fm&M2=M eʾ=q܅;c_ѯjU8h_X6JPwZ|!=fs?4!5{j`DDΥuƢ{q@Ʃ CÄ<;ܪ(y *څq];v}+x *9n]RеyLڅdy΅̸*׾{og[" {u K~WK86.iP8O{żmz8F/O!&]3pbUkWe'ծڷN>Vu^T {];#gݻX^ěOyXLJ6Rba5δ-^5EUL+'şuS|^< L) B<2(V,koUm̪EMy܌>me J;ISK沷DO^:^ZyK~GЈԪ*fU7R2 2F :#Y^<%pEn39\ާa ࿄FQaNcewf)\!8U󗳉hFq#~Z3G irLh$=V$l2B^+A 28&s=a{ ۢFi!P8E1rE<ᯧMB\o balkj8òS&}1'P('lgkچ:|U OMt[:p>d;;kH1uL `pً'vٝE#~o*"&ګb*֨;H,DFLEg !<-S:L( ]* \Ab:]<#~#*cbNxJ[Xb[:J%rdȂABi/9qخtq.YMNnj-IHR4z; \q ["AHCu[i+KH*CBbP&WC#*h%8z.K p( @'RRP#B)#?T4RVm2؉rΐZPdo`ކKHV~dS$ljITNUˍ\Ω=OFWo*4)tXIn֔G 70Q0饜H8xg3J6t7 B"ZfLɄ/@q!RX7pCI\Ew:mW(2@]J5,aLXW xk,91N>BL%5N{fsPS_N $ĐsLd_= $hD'Sm"5DpEC](5̧15hsP* Ήc5@bۜS‹)$b^,|0Pcl-4NE_a۪%} VH9Vu.8eAMS]N|CEK2L6\݌fBEqUDPD%!EdUM7`D}ҾSz>hݓ9;|a^?. !FTQkGvRee%7n[CYEp$̓d.jpo3Jf$;s=QYl {aE+ yIvT|If(`7AZ=`40Na6K kMU *4 lw44'ˆC=nt@TE0 7GQ2i()*Z}Kd4GR9_ݰn'牲0eX Ҧgxbz'5:$&2iec!l2#sϮbK0 Ժ"3EHJj$V 2l}Y:\(#$qN9 ]8 LlyMiRDP=686Gt40-k2χ%/.m lc*!ΰE?4aNP B`ℂOyBbNDB `Dr`|yYgMcRm9:EݧoVcRav`N D>HJ<,`L{gI)PdK'aXaPQAIB&|s,ZdeѐP`MJ+T3$29s)Ώ}~*gFgd/xI+)PޫF_1PE>0 aWb̃?;I+TDя  S~K#eT- -Dr_J6*-VGZg0pGNYrP&wn[Ʀ(30jwr'>٬ V&_ĥ5izK"O{*u!O&OU6,\VOUb? "(eB"ިd}:h_"ДWOB=BJB&+RbKnj c, cBNG&f .5z$sG8#.:6te >:ÔbjWӣ)*dm?ly^=d5X"Qxae:NjgtX/fT%a>-I8R> c̕yۭ>3E0"EOih[JӳkN=GD' =$"m;}Ni*5G=G#=ِ^^6C@NLoFƭ^o9!hZ`SyܖE wMIW92?|=x`՛˦#H"fN}nAx2Jap<㿾 eWX r%uLl'C 5X|a86:5 ; klŋm)=O~muXwuŏ^O /tؑ31f_U_sj5v B% 2f~*P:nUj&э5=46wLy1rj%vxúxH,dQSn.)OK^@lެpE.HH[#H SJ7v&8AX>q7"9@ͱ߅aq'*G9q,/*bz1|N!3x򡌠:c!8q qv°͉0a8_Z3%6$9 X-2,5xCtbA UPSEO}I{_e?QD@b]E@1/E9N+L9 4R@Jʢ/(I(%}$ءld tRI@" {2mkE2Dprz [rJg#K|V 1Iɘ&me6'{$vM 8o:v=اP]܆E=e7ȿprVTAV%SD( +TT2iLEꚧȢ5ΉХLv+Qry5)WRPh[dZOX{F"м+\C- ((%J{+` A%DfPl[N pRKЋI=ӧvOrc.((hI^.#lR 52ǧ"Mq6J║jܑJf ;W^8ɘ+XS~$j.CSJ jrWA οz) 2G~ݑ#eږDX^ZN[o13BT֙|uiJ4bmEm 8~>P+J?*1I3JN=(eNL̢\(@^ۤ43PD+Wp[U̓>%X^LEABUT,T,)P~LL\?BTH8jV']a kQ" JJ dWGҫ(AZ24Q&rK{D'+zE3vcD, ez PsZ)Vx" {aY ץP^."s&qTgV&%A&fR7'jspΦixJ%x&Jtͪ ,+"VT%pPq%R'+SUEXA=B΃sI̊.9ٲK󊙣l[TL|i0+Jۿq1l7Z{^bcQr\/l: Y2O sh2dQJ(3M$GpB;>%z5&H >'pc}A8YsԼۂ]-ˊWzg)L T08&*p~އCO"5'=C+xe5F̉ 7PfI c爴-dQE{6 5MF#L|UMhn~ȩI\U'L9r(+&άojHvcf/"#dϘsn~d- O^ 撍0}a8FyfqS<1|3\z`!E3\HB]3D.IdPESrs8N3O#EB+gƲd/J [d|S1 AbXwb;?V/Z >3K3園Rd˜PySTkꍦڜjZxb6/|<?a$ɓ8.QpfUgEA{:'S|m#Ȣ78/ze;OHN8Ttvb k<frO&KrOjY71&.mJ=}Zc;%Jmoф!Զ* NIf8=`fvr:E'FH)M pzUǭ'd\zAeIݨ~Tn3g8GOZ`pRVnαZ)\7Pۇy[c e8"&(Qye VœMݞʕ +SfNw:x=>J圶$0fL|sbM| CQ,v^R^kytP2bwFVAšT:IpRM&n lWA+փʃD̲ތzѐu=ąO3̤TOӪ"zO#d^JC1H:-Apj{J4YTz$]n^/4(Cw&`06\Rz C JK>5 [bE=cpl Qpt(:GՋUO5tte4eȨw wEV6:7U:k6 E+滽M߂׌jA0Ǚ?a$l($Җeq͍ͤV(KӶX~J >Q )hRzЏ޺C0na6&UP'7czt5.ӣ_~DTiR doҋVFį-ert_ɲ|C ֜drhtJ+S# F_}anQ@Pz.TPpЎӲ-9SāXl4q' 3)%FriRkWriE=b!8>xNf5?Ծry+m}L$<&2!cհC$>ӂ0޸>VxiY :m_o'@0mr^e:c^)ا[)Oдs<Yk] /Z-.YWE¦rVC^tx#YK*2T:ѫBckAB^UI~Kjl&&n늶f_lע5:ji~d QI>܄.Fcp~Th;TablXE&%IJKhzu}eb`rmض\2g+.;GL-H& q؟W)4# /J-KrOS4Vlh鲷5V!j;&rUO_ibNf[3iKw a6 %;c,~a~yg&d⽣9%M8TTϲ6.͈uUyz2ϔP%5CqJH?Hf׻͔za0r/ i͙=Z؇;(Z̽pkV-ʅP &L :a=99L߯@kN-D8ϰkʢaBʏm8b ~EL%Ѡ7:s-OxȒi0F} yy+ƘxsX=Z' }/m/A$5[m}]^0{"?*KeE%]'x`;F207՘d qn)E' :1cY gq wL1*L8ioj1?uc (_ rt(܉9A3LDᢉMACcf pv(ss1SgOVt zku_:UK ֯§5u `~83JqJj??SfRC'r c*c%67l4k^vtvCfoM v/Ћ8⋆DT'45B%jB;fX69‡ݖHI@Efq\ED. ]&z쯫 pHbl)J&cpĂV? *3Y2$oOo\gs7\vf$l50=#&vÈ-_jRۍdД]fiJo8>nG)HIIjW[ew7ξlOd8kr6 A-:NRB|Bߧ#%в%/qbw_:XMHZ;&mJnlrDn͎bDO.>u06U)*9Xʈ %V(ѿ<]>>ϸ|o>}4 ЅP|EꄑyԮ8BÓ4t|x 0ܱHew"$]JH<9^]u|[K3jx3HsI T3fϜ.yUoъ]Y6Ja{-a_}W%vY,E(o} Y_+}9J^$?$qӖɊ*"%^itX vkB?-+CF򅃪gxguNsyj2B:L)Ks^d!Z] sOOщ&QX Aɇv'xWg3̧s4e C\k72RqԣAv Jjg!lVׅtNk/L<~g'&OLIcJ*̢#ޣ/>=^-iKNn(ޒ$kY#PȻZ4F[hX_mGc%[$&M3R\a9=~=μ#j7Caٲ+}i2 ijJN゚֚.5̃Pv W18tÝK"O,񟠂R|+9r>׬~Gn](FɀǍkê8*A?e >YS2%iqpLhda=^=ﳩCyWtv>~-7e̼|Y+''Imͽm& zSFPs2!&=dSTE&*bU'8 o,<-d GЙATat\h~g=_3?_u#-40-r;uE:궊81-=xt -!\0 AFНM6IĽFNG^=d><& =2-W$~qZ<YCFWwT}WPwT`C38O4Y*To 5Ƿ6*oS4{UpSǕ#p;}zFGT^=H@散!s񊾪g:5+{A,bSe-jcl#b '6>:MkE?~Uciq8½aJ_1vs/"dᾇ("AoB?A!iDhi_l.~S lʢth9/fyS:SiQOG3t7:E!e%o߻]R:CADymXrX3{k6̑&S^$9lzg f2ckҧpbLzr`_JبqiQJF=ƈhF`4kڕ9s!GUM\VI=RE5s w h K&{\vsE9g<͔.`[Cj4v50R*7Gq|o3_ 2Lxg~_bk S]V|yz -_zmrE$#DBYQE1 ۺIy"/mΖFi)F9WxN$">PX;x}y L]GCpvI3|Kxb e:DnIzM``nbs"^eVR-~sX, 5Qšne᳻07/9Mmu[\'T_3uuAJjLW.y_rT~5eVnicp[aw\g)QggS)#fN-멌L27M14EKۧq/xQUQRfJ挞\ J3t-q)*]N13"n$c:h\:MZI~bOqt |X<ڸz閰tڑ,ʚzcReZ> P+<)I=a:r LڙV>v(YMr"7H-ehG1bg|e9CI8}e}~.{uqir `>ecʿ@_0涆fjO_i b{eK%F'rYM7r 4/y# ÿb\`/40aZwhAsYNF8BrE@"Ĉpm #Zqp;BulA>)K,YJ#-{fF^GL sduetqSOqq/D7,끅FH pRrqFCՓ<g.v6j`cG匿h5 ˶umr9`g,sA c^ Vt[xUҲIڼd\i#JMr/mhjŕIEj Mi9Ubqc(AuxBnbђS|YNys#4֨h6۱.sU I\|v` 2r0UdU{v"EB+YwF(EAt3?7Ot9! g|KNdvR|°L*%M6',~bwĂoG< kwPܜљ$ ? V- d}nVto[GqGtXjA[sYe1a,)$Q"tjѣ4h@`6S)J>-|.J Euէ4fF|P`ps9fLnhY!3Pg1 1Lƥ ,oaơ"cJ']⊛dX~ZE`EFk44U6ֺPhjT[~Ns3K=sEuicy7G)gU_Em]ՈG !jCQ>ىe'$Zqt.=#i=\$ϘzqDP; $.!hJF3Z ;c1*P&TE,\0O,`) ( ]Y 8F@KhCe(+R LApVmu @ϣ[SFW 0Jh5BBthi+9Ba4Sfy gB}vjoR6ymEТ:}ǨmI/%l ]f\|FzRMA R!K$qqQV/2عCEҙRZS^6,(׺4 #?%Ff#n_UYemYg #):rY#t c<Ǹ띆%hsLTGӷw$S QEtrac-accountmanager-0.6~svn18547/contrib/sessionstore_convert.py0000644000175500017550000000272213065031720024677 0ustar debacledebacle#!/usr/bin/env python # # Copyright (C) 2007 Matthew Good # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Matthew Good import os import sys from trac.env import Environment from acct_mgr.api import AccountManager from acct_mgr.htfile import HtPasswdStore, HtDigestStore env = Environment(sys.argv[1]) store = AccountManager(env).password_store if isinstance(store, HtPasswdStore): env.config.set('account-manager', 'hash_method', 'HtPasswdHashMethod') prefix = '' elif isinstance(store, HtDigestStore): env.config.set('account-manager', 'hash_method', 'HtDigestHashMethod') prefix = store.realm + ':' else: print >> sys.stderr, 'Unsupported password store:', \ store.__class__.__name__ sys.exit(1) password_file = os.path.join(env.path, env.config.get('account-manager', 'password_file')) with open(password_file) as f: hashes = [line.strip().split(':', 1) for line in f] hashes = [(u, p) for u, p in hashes if p.startswith(prefix)] if hashes: with env.db_transaction as db: db.executemany(""" INSERT INTO session_attribute (sid,authenticated,name,value) VALUES (%s,1,'password',%s) """, hashes) env.config.set('account-manager', 'password_store', 'SessionStore') env.config.save() trac-accountmanager-0.6~svn18547/.github/0000755000175500017550000000000014440331004017716 5ustar debacledebacletrac-accountmanager-0.6~svn18547/.github/workflows/0000755000175500017550000000000014440331004021753 5ustar debacledebacletrac-accountmanager-0.6~svn18547/.github/workflows/build.yml0000644000175500017550000000142414440331004023576 0ustar debacledebacle--- name: build on: [push] concurrency: group: ${{ github.workflow }}--${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-20.04, macos-12, windows-2022] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: | 2.7 3.5 3.6 3.7 3.8 3.9 3.10 3.11 - run: python3 -m pip install tox~=4.4.0 virtualenv~=20.21.0 - run: python3 -m tox run - uses: actions/upload-artifact@v3 if: always() with: name: log-${{ matrix.os }} path: | .tox/py*/log .tox/py*/tmp/testenv if-no-files-found: ignore trac-accountmanager-0.6~svn18547/COPYING0000644000175500017550000000653212432141335017424 0ustar debacledebacleCopyright (C) 2005 Matthew Good All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This software now contains voluntary contributions made by many individuals. For the exact contribution history, see the revision history and logs, available at https://trac-hacks.org/log/accountmanagerplugin/. ---- The previous, informal "license", that has been more of a statement rather than a real license in legal sense, is reproduced below as well: "THE BEER-WARE LICENSE" (Revision 42): wrote this file. As long as you retain this notice you can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return. Matthew Good Author: Matthew Good ---- exclusively for acct_mgr/md5crypt.py: Based on FreeBSD src/lib/libcrypt/crypt.c 1.2 http://www.freebsd.org/cgi/cvsweb.cgi/~checkout~/src/lib/libcrypt/crypt.c?rev=1.2&content-type=text/plain Original license: "THE BEER-WARE LICENSE" (Revision 42): wrote this file. As long as you retain this notice you can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp This port adds no further stipulations. I forfeit any copyright interest. Author: Matthew Good ---- Sources for art-work, that is included in this distribution: * Approval icon derived from man remix by Alexandre Garel, originally drawn by Francesco 'Architetto' and certificate symbol from SVG images from Tango Project icons * Refresh icon derived from grey-shaded-key SVG image drawn by silwol, "edit clear" and "view refresh" Tango Project icons by warszawianka * Sword and shield icon from SVG image uploaded by purzen, * System users icon and dialog error from SVG image from Tango Project icons by warszawianka * Info icon from SVG image uploaded by jean_victor_balin * Red square error or warning icon by molumen all set out to Public Domain and published at http://openclipart.org trac-accountmanager-0.6~svn18547/messages.cfg0000644000175500017550000000136414413553530020664 0ustar debacledebacle# mapping file for extracting messages from Jinja2 templates into # trac/locale/messages.pot (see setup.cfg) [ignore: **/tests/**] [python: **.py] [html: **/templates/**.html] extensions = jinja2.ext.do, jinja2.ext.with_ variable_start_string = ${ variable_end_string = } line_statement_prefix = # line_comment_prefix = ## trim_blocks = yes lstrip_blocks = yes newstyle_gettext = yes silent = no [text: **/templates/**.txt] extensions = jinja2.ext.do, jinja2.ext.with_ variable_start_string = ${ variable_end_string = } line_statement_prefix = # line_comment_prefix = ## trim_blocks = yes lstrip_blocks = yes newstyle_gettext = yes silent = no [extractors] python = trac.dist:extract_python html = trac.dist:extract_html text = trac.dist:extract_text trac-accountmanager-0.6~svn18547/MANIFEST.in0000644000175500017550000000054514413553530020132 0ustar debacledebacle# Define files to include in a distribution built by the 'sdist' command. include changelog include contrib/* exclude contrib/signatures.py include COPYING include README* include acct_mgr/htdocs/* include acct_mgr/locale/messages.pot include acct_mgr/locale/*/LC_MESSAGES/acct_mgr.po include acct_mgr/templates/*/*.html include acct_mgr/templates/*/*.txt trac-accountmanager-0.6~svn18547/acct_mgr/0000755000175500017550000000000014416346032020147 5ustar debacledebacletrac-accountmanager-0.6~svn18547/acct_mgr/db.py0000644000175500017550000001044714413553530021114 0ustar debacledebacle# -*- coding: utf-8 -*- # # Copyright (C) 2007 Matthew Good # Copyright (C) 2010-2012 Steffen Hoffmann # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Matthew Good from trac.core import Component, implements from trac.config import ExtensionOption from .api import IPasswordStore, N_ from .pwhash import IPasswordHashMethod class SessionStore(Component): implements(IPasswordStore) hash_method = ExtensionOption('account-manager', 'hash_method', IPasswordHashMethod, 'HtDigestHashMethod', doc=N_("IPasswordHashMethod used to create new/updated passwords")) def __init__(self): self.key = 'password' # Check for valid hash method configuration. self.hash_method_enabled def get_users(self): """Returns an iterable of the known usernames.""" for sid, in self.env.db_query(""" SELECT DISTINCT sid FROM session_attribute WHERE authenticated=1 AND name=%s """, (self.key,)): yield sid def has_user(self, user): for _ in self.env.db_query(""" SELECT * FROM session_attribute WHERE authenticated=1 AND name=%s AND sid=%s """, (self.key, user)): return True return False def set_password(self, user, password, old_password=None, overwrite=True): """Sets the password for the user. This should create the user account, if it doesn't already exist. Returns True, if a new account was created, and False, if an existing account was updated. """ if not self.hash_method_enabled: return hash_ = self.hash_method.generate_hash(user, password) with self.env.db_transaction as db: sql = "WHERE authenticated=1 AND name=%s AND sid=%s" if overwrite: db(""" UPDATE session_attribute SET value=%s """ + sql, (hash_, self.key, user)) exists = False for _ in db(""" SELECT value FROM session_attribute """ + sql, (self.key, user)): exists = True break if not exists: db(""" INSERT INTO session_attribute (sid,authenticated,name,value) VALUES (%s,1,%s,%s) """, (user, self.key, hash_)) return not exists def check_password(self, user, password): """Checks if the password is valid for the user.""" if not self.hash_method_enabled: return for hash_, in self.env.db_query(""" SELECT value FROM session_attribute WHERE authenticated=1 AND name=%s AND sid=%s """, (self.key, user)): return self.hash_method.check_hash(user, password, hash_) # Return value 'None' allows to proceed with another, chained store. return def delete_user(self, user): """Deletes the user account. Returns True, if the account existed and was deleted, False otherwise. """ with self.env.db_transaction as db: sql = "WHERE authenticated=1 AND name=%s AND sid=%s" # Avoid has_user() to make this transaction atomic. exists = False for _ in db(""" SELECT * FROM session_attribute %s """ % sql, (self.key, user)): exists = True break if exists: db(""" DELETE FROM session_attribute %s """ % sql, (self.key, user)) return exists @property def hash_method_enabled(self): """Prevent AttributeError on plugin load. This would happen, if the implementation of 'IPasswordHashMethod' interface configured in 'hash_method' has not been enabled. """ try: self.hash_method except AttributeError: self.log.error("%s: no IPasswordHashMethod enabled - fatal, " "can't work", self.__class__) return return True trac-accountmanager-0.6~svn18547/acct_mgr/svnserve.py0000644000175500017550000000541114413553530022375 0ustar debacledebacle# -*- coding: utf-8 -*- # # Copyright (C) 2005 Matthew Good # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Matthew Good import os from trac.config import Configuration from trac.core import Component, implements from trac.versioncontrol.api import RepositoryManager from trac.versioncontrol.cache import CachedRepository from .api import IPasswordStore, N_ from .util import EnvRelativePathOption class SvnServePasswordStore(Component): """PasswordStore implementation for reading svnserve's password file format """ implements(IPasswordStore) filename = EnvRelativePathOption('account-manager', 'password_file', doc=N_("Path to the users file; leave blank to locate the users file " "by reading svnserve.conf from the default repository.")) _userconf = None @property def _config(self): filename = self.filename or self._get_password_file() if self._userconf is None or filename != self._userconf.filename: self._userconf = Configuration(filename) # Overwrite default with str class to preserve case. self._userconf.parser.optionxform = str self._userconf.parse_if_needed(force=True) else: self._userconf.parse_if_needed() return self._userconf def _get_password_file(self): repos = RepositoryManager(self.env).get_repository('') if not repos: return None if isinstance(repos, CachedRepository): repos = repos.repos if repos.params['type'] in ('svn', 'svnfs', 'direct-svnfs'): conf = Configuration(os.path.join(repos.path, 'conf', 'svnserve.conf')) return conf['general'].getpath('password-db') # IPasswordStore methods def get_users(self): return [user for (user, password) in self._config.options('users')] def has_user(self, user): return user in self._config['users'] def set_password(self, user, password, old_password=None, overwrite=True): cfg = self._config new_acct = not self.has_user(user) if not new_acct and not overwrite: return new_acct cfg.set('users', user, password) cfg.save() return new_acct def check_password(self, user, password): if self.has_user(user): return password == self._config.get('users', user) return None def delete_user(self, user): if not self.has_user(user): return False cfg = self._config cfg.remove('users', user) cfg.save() return not self.has_user(user) trac-accountmanager-0.6~svn18547/acct_mgr/util.py0000644000175500017550000001123014414633245021476 0ustar debacledebacle# -*- coding: utf-8 -*- # # Copyright (C) 2005 Matthew Good # Copyright (C) 2010-2013 Steffen Hoffmann # Copyright (C) 2011 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Matthew Good from datetime import datetime import os import re try: from babel.support import LazyProxy except ImportError: LazyProxy = None from trac.config import Option from trac.util.datefmt import utc from trac.util.html import tag from trac.util.translation import dngettext from .api import _ from .compat import basestring class EnvRelativePathOption(Option): def __get__(self, instance, owner): if instance is None: return self path = super(EnvRelativePathOption, self).__get__(instance, owner) if not path: return path return os.path.normpath(os.path.join(instance.env.path, path)) # taken from a comment of Horst Hansen # at http://code.activestate.com/recipes/65441 def contains_any(str, set): for c in set: if c in str: return True return False def if_enabled(func): def wrap(self, *args, **kwds): if not self.enabled: return None return func(self, *args, **kwds) return wrap def format_timespan(seconds): if not seconds or seconds <= 0: return '' _dngettext = dngettext total = seconds days, seconds = divmod(total, 86400) if days != 0: datepart = _dngettext('messages', '%(num)d day', '%(num)d days', days) if seconds == 0: return datepart if seconds < 120: timepart = _dngettext('messages', '%(num)i second', '%(num)i seconds', seconds) else: timepart = datetime.fromtimestamp(seconds, utc).strftime('%H:%M:%S') if days == 0: return timepart return _("%(datepart)s %(timepart)s", datepart=datepart, timepart=timepart) def _create_zwsp_re(): ucs2_range = u'\\s\u200b-\u200f\u061c\u202a-\u202e\u2066-\u2069\u00ad' \ u'\u2060\ufeff\u2061-\u2064\u115f\u1160\u180b-\u180d' \ u'\ufe00-\ufe0f' try: pattern = re.compile(u'[%s\U000e0100-\U000e01ef]+' % ucs2_range, re.UNICODE) except re.error: # Narrow build, `re` cannot use characters >= 0x10000 pattern = re.compile(u'[%s]+|(?:\udb40[\udd00-\uddef])+' % ucs2_range, re.UNICODE) return pattern _zwsp_re = _create_zwsp_re() def remove_zwsp(text): """Strips unicode zero-width and whitespace characters. """ return _zwsp_re.sub('', text) _i18n_tag_re = re.compile(r'(?:\[([1-9][0-9]*)\:)|(? # Copyright (C) 2011 Steffen Hoffmann # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Matthew Good import base64 import hashlib try: import passlib except ImportError: passlib = None try: import crypt except ImportError: crypt = None from trac.config import Option from trac.core import Component, Interface, implements from .api import _, N_ from .compat import compare_digest, unicode class IPasswordHashMethod(Interface): def generate_hash(user, password): pass def check_hash(user, password, hash): pass class HtPasswdHashMethod(Component): implements(IPasswordHashMethod) hash_type = Option('account-manager', 'db_htpasswd_hash_type', 'crypt', doc="Default hash type of new/updated passwords") def generate_hash(self, user, password): return generate_hash(password, self.hash_type) def check_hash(self, user, password, hash): return check_hash(password, hash) class HtDigestHashMethod(Component): implements(IPasswordHashMethod) realm = Option('account-manager', 'db_htdigest_realm', '', doc=N_("Realm to select relevant htdigest db entries")) def generate_hash(self, user, password): hash_ = htdigest(user, self.realm, password) return '%s:%s' % (self.realm, hash_) def check_hash(self, user, password, hash): return compare_digest(hash, self.generate_hash(user, password)) def htdigest(user, realm, password): p = ':'.join([user, realm, password]).encode('utf-8') return hashlib.md5(p).hexdigest() if passlib: from passlib.context import CryptContext from passlib.hash import bcrypt from .compat import itervalues _passlib_schemes = { 'sha256': 'sha256_crypt', 'sha512': 'sha512_crypt', 'md5': 'apr_md5_crypt', 'crypt': 'des_crypt', } try: bcrypt.get_backend() except passlib.exc.MissingBackendError: pass else: _passlib_schemes['bcrypt'] = 'bcrypt' _passlib_context = CryptContext(schemes=list(itervalues(_passlib_schemes))) if not hasattr(_passlib_context, 'handler'): # passlib 1.5 and early def _passlib_hash(password, scheme): return _passlib_context.encrypt(password, scheme=scheme) elif hasattr(_passlib_context.handler('sha512_crypt'), 'hash'): # passlib 1.7+ def _passlib_hash(password, scheme): handler = _passlib_context.handler(scheme) return handler.hash(password) else: # passlib 1.6 def _passlib_hash(password, scheme): handler = _passlib_context.handler(scheme) return handler.encrypt(password) def _passlib_generate_hash(password, method): if method == 'sha': return _sha_digest(password) if method not in _passlib_schemes: method = 'crypt' scheme = _passlib_schemes[method] return _passlib_hash(password, scheme) def _passlib_check_hash(password, the_hash): if the_hash.startswith('{SHA}'): return compare_digest(the_hash, _sha_digest(password)) try: return _passlib_context.verify(password, the_hash) except ValueError: return False else: _passlib_generate_hash = _passlib_check_hash = None if not crypt: _crypt_generate_hash = _crypt_check_hash = None elif hasattr(crypt, 'methods'): # Python 3 from trac.util import salt, md5crypt def _crypt_methods(): pairs = [ ('crypt', 'METHOD_CRYPT'), ('sha256', 'METHOD_SHA256'), ('sha512', 'METHOD_SHA512'), ('bcrypt', 'METHOD_BLOWFISH'), ] pairs = [(name, getattr(crypt, method, None)) for name, method in pairs] return dict((name, method) for name, method in pairs if method) _crypt_methods = _crypt_methods() def _crypt_generate_hash(password, method): if method == 'md5': return md5crypt(password, salt(), '$apr1$') if method == 'sha': return _sha_digest(password) if method not in _crypt_methods: method = 'crypt' return crypt.crypt(password, crypt.mksalt(_crypt_methods[method])) def _crypt_check_hash(password, the_hash): if the_hash.startswith('$apr1$'): salt = the_hash[6:].split('$', 1)[0] hash_ = md5crypt(password, salt, '$apr1$') elif the_hash.startswith('{SHA}'): hash_ = _sha_digest(password) else: hash_ = crypt.crypt(password, the_hash) return compare_digest(hash_, the_hash) else: # Python 2 from trac.util import salt, md5crypt def _crypt_generate_hash(password, method): password = password.encode('utf-8') \ if isinstance(password, unicode) else password if method == 'md5': return md5crypt(password, salt(2), '$apr1$') if method == 'sha': return _sha_digest(password) return crypt.crypt(password, salt(2)) def _crypt_check_hash(password, the_hash): password = password.encode('utf-8') \ if isinstance(password, unicode) else password if the_hash.startswith('$apr1$'): salt = the_hash[6:].split('$', 1)[0] hash_ = md5crypt(password, salt, '$apr1$') elif the_hash.startswith('{SHA}'): hash_ = _sha_digest(password) else: hash_ = crypt.crypt(password, the_hash) return compare_digest(hash_, the_hash) def _sha_digest(password): digest = hashlib.sha1(password.encode('utf-8')).digest() return u'{SHA}' + unicode(base64.b64encode(digest), 'ascii') def _unavai_error(): return NotImplementedError(_("Neither passlib nor crypt module available")) def _unavai_generate_hash(password, method): raise _unavai_error() def _unavai_check_hash(password, the_hash): raise _unavai_error() if passlib: generate_hash = _passlib_generate_hash check_hash = _passlib_check_hash elif crypt: generate_hash = _crypt_generate_hash check_hash = _crypt_check_hash else: generate_hash = _unavai_generate_hash check_hash = _unavai_check_hash trac-accountmanager-0.6~svn18547/acct_mgr/notification.py0000644000175500017550000002010014416345315023203 0ustar debacledebacle# -*- coding: utf-8 -*- # # Copyright (C) 2008 Pedro Algarvio # Copyright (C) 2013-2015 Steffen Hoffmann # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Pedro Algarvio from trac.admin.api import IAdminPanelProvider from trac.config import ListOption from trac.core import Component, TracError, implements from trac.notification.api import ( IEmailDecorator, INotificationFormatter, NotificationEvent, NotificationSystem) from trac.notification.mail import RecipientMatcher, set_header from trac.util.text import exception_to_unicode from trac.util.translation import deactivate, dgettext, reactivate from trac.web.chrome import Chrome from .api import IAccountChangeListener, _ from .compat import iteritems, use_jinja2 from .util import i18n_tag class NotificationError(TracError): pass class AccountChangeEvent(NotificationEvent): realm = 'account' def __init__(self, category, username, data): super(AccountChangeEvent, self).__init__(self.realm, category, None, None, username) self.data = data class AccountChangeListener(Component): implements(IAccountChangeListener) _notify_actions = ListOption( 'account-manager', 'notify_actions', [], doc="""Comma separated list of notification actions. Available actions are 'new', 'change', 'delete'. """) _account_change_recipients = ListOption( 'account-manager', 'account_changes_notify_addresses', [], doc="""Email addresses to notify on account created, password changed and account deleted. """) action_category_map = { 'new': 'created', 'change': 'password changed', 'delete': 'deleted' } def __init__(self): self._notify_categories = [category for action, category in iteritems(self.action_category_map) if action in self._notify_actions] # IAccountChangeListener methods def user_created(self, username, password): data = {'password': password} self._send_notification('created', username, data) def user_password_changed(self, username, password): data = {'password': password} self._send_notification('password changed', username, data) def user_deleted(self, username): self._send_notification('deleted', username) def user_password_reset(self, username, email, password): data = {'password': password, 'email': email} self._send_notification('password reset', username, data) def user_email_verification_requested(self, username, token): data = {'token': token} self._send_notification('verify email', username, data) def user_registration_approval_required(self, username): self._send_notification('verify email', username) # Helper method def _send_notification(self, category, username, data=None): event = AccountChangeEvent(category, username, data) subscriptions = self._subscriptions(event) try: NotificationSystem(self.env).distribute_event(event, subscriptions) except Exception as e: self.log.error("Failure sending notification for '%s' for user " "%s: %s", category, username, exception_to_unicode(e)) raise NotificationError(e) def _subscriptions(self, event): matcher = RecipientMatcher(self.env) transport_and_format = ('email', 'text/plain') if event.category in ('verify email', 'password reset'): recipient = matcher.match_recipient(event.author) if recipient: yield recipient + transport_and_format elif event.category in self._notify_categories: for r in self._account_change_recipients: recipient = matcher.match_recipient(r) if recipient: yield recipient + transport_and_format class AccountNotificationFormatter(Component): implements(IEmailDecorator, INotificationFormatter) realm = 'account' # IEmailDecorator methods def decorate_message(self, event, message, charset): if event.realm != self.realm: return # Someday replace with method added in trac:#13208 prefix = self.config.get('notification', 'smtp_subject_prefix') subject = '[%s]' % self.env.project_name \ if prefix == '__default__' else prefix if event.category in ('created', 'password changed', 'deleted'): subject += " Account %s: %s" % (event.category, event.author) elif event.category == 'password reset': subject += " Account password reset: %s" % event.author elif event.category == 'verify email': subject += " Account email verification: %s" % event.author set_header(message, 'Subject', subject, charset) # INotificationFormatter methods def get_supported_styles(self, transport): yield 'text/plain', self.realm def format(self, transport, style, event): if event.realm != self.realm: return data = { 'account': {'username': event.author}, 'login': {'link': self.env.abs_href.login()}, } if event.category in ('created', 'password changed', 'deleted'): data['account']['action'] = event.category template_name = 'account_user_changes_email.txt' elif event.category == 'password reset': data['account']['password'] = event.data['password'] template_name = 'account_reset_password_email.txt' elif event.category == 'verify email': token = event.data['token'] data['account']['token'] = token data['verify'] = { 'link': self.env.abs_href.verify_email(token=token, verify=1) } template_name = 'account_verify_email.txt' t = deactivate() # don't translate the e-mail stream try: return self._format_body(data, template_name) finally: reactivate(t) # Internal methods def _format_body(self, data, template_name): chrome = Chrome(self.env) data = chrome.populate_data(None, data) if use_jinja2: template = chrome.load_template(template_name, text=True) body = chrome.render_template_string(template, data, text=True) return body.encode('utf-8') else: template = chrome.load_template(template_name, method='text') stream = template.generate(**data) return stream.render('text', encoding='utf-8') class AccountChangeNotificationAdminPanel(Component): implements(IAdminPanelProvider) # IAdminPageProvider methods def get_admin_panels(self, req): if 'ACCTMGR_CONFIG_ADMIN' in req.perm: yield ('accounts', _("Accounts"), 'notification', _("Notification")) def render_admin_panel(self, req, cat, page, path_info): if page == 'notification': return self._do_config(req) def _do_config(self, req): cfg = self.config['account-manager'] if req.method == 'POST': cfg.set('account_changes_notify_addresses', ' '.join(req.args.getlist('notify_addresses'))) cfg.set('notify_actions', ','.join(req.args.getlist('notify_actions'))) self.config.save() req.redirect(req.href.admin('accounts', 'notification')) notify_addresses = cfg.getlist('account_changes_notify_addresses', sep=' ') notify_actions = cfg.getlist('notify_actions') data = { 'i18n_tag': i18n_tag, 'notify_actions': notify_actions, 'notify_addresses': notify_addresses } return 'account_notification.html', data trac-accountmanager-0.6~svn18547/acct_mgr/tests/0000755000175500017550000000000014414633245021314 5ustar debacledebacletrac-accountmanager-0.6~svn18547/acct_mgr/tests/db.py0000644000175500017550000000736614413553530022264 0ustar debacledebacle# -*- coding: utf-8 -*- # # Copyright (C) 2007 Matthew Good # Copyright (C) 2011,2012 Steffen Hoffmann # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Matthew Good import unittest from trac.test import EnvironmentStub from ..db import SessionStore class _BaseTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(enable=['trac.*', 'acct_mgr.*']) self.env.config.set('account-manager', 'password_store', 'SessionStore') self.store = SessionStore(self.env) def test_get_users(self): with self.env.db_transaction as db: db.executemany(""" INSERT INTO session_attribute (sid,authenticated,name,value) VALUES (%s,1,'password',%s) """, [('a', 'a'), ('b', 'b'), ('c', 'c')]) self.assertEqual(set(['a', 'b', 'c']), set(self.store.get_users())) def test_has_user(self): self.env.db_transaction(""" INSERT INTO session_attribute (sid,authenticated,name,value) VALUES (%s,'1','password',%s) """, ('bar', 'bar')) self.assertFalse(self.store.has_user('foo')) self.assertTrue(self.store.has_user('bar')) def test_create_user(self): self.assertFalse(self.store.has_user('foo')) self.store.set_password('foo', 'password') self.assertTrue(self.store.has_user('foo')) def test_overwrite(self): self.assertTrue(self.store.set_password('foo', 'pass1')) self.assertFalse(self.store.set_password('foo', 'pass2', overwrite=False)) self.assertTrue(self.store.check_password('foo', 'pass1')) self.assertTrue(self.store.set_password('bar', 'pass', overwrite=False)) def test_update_password(self): self.store.set_password('foo', 'pass1') self.assertFalse(self.store.check_password('foo', 'pass2')) self.store.set_password('foo', 'pass2') self.assertTrue(self.store.check_password('foo', 'pass2')) self.store.set_password('foo', 'pass3', 'pass2') self.assertTrue(self.store.check_password('foo', 'pass3')) def test_delete_user(self): self.store.set_password('foo', 'password') self.assertTrue(self.store.has_user('foo')) self.assertTrue(self.store.delete_user('foo')) self.assertFalse(self.store.has_user('foo')) def test_delete_nonexistant_user(self): self.assertFalse(self.store.has_user('foo')) self.assertFalse(self.store.delete_user('foo')) def test_unicode_username_and_password(self): username = u'\u4e60' password = u'\u4e61' self.store.set_password(username, password) self.assertTrue(self.store.check_password(username, password)) class HtDigestTestCase(_BaseTestCase): def setUp(self): super(HtDigestTestCase, self).setUp() self.env.config.set('account-manager', 'hash_method', 'HtDigestHashMethod') self.env.config.set('account-manager', 'db_htdigest_realm', 'TestRealm') class HtPasswdTestCase(_BaseTestCase): def setUp(self): super(HtPasswdTestCase, self).setUp() self.env.config.set('account-manager', 'hash_method', 'HtPasswdHashMethod') def test_suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(HtDigestTestCase)) suite.addTest(unittest.makeSuite(HtPasswdTestCase)) return suite if __name__ == '__main__': unittest.main(defaultTest='test_suite') trac-accountmanager-0.6~svn18547/acct_mgr/tests/svnserve.py0000644000175500017550000001067714413553530023551 0ustar debacledebacle# -*- coding: utf-8 -*- """ Test suite for SvnServePasswordStore. """ import os.path import shutil import tempfile import unittest from trac.test import EnvironmentStub from ..svnserve import SvnServePasswordStore class SvnServePasswordTestCase(unittest.TestCase): """Test cases for SvnServePasswordStore. """ def setUp(self): env_path = os.path.realpath(tempfile.mkdtemp(prefix='trac-testdir-')) self.env = EnvironmentStub(path=env_path) self.store = SvnServePasswordStore(self.env) self.default_content = [ '[users]\n', 'spam = eggs\n', 'knights = ni\n', ] def tearDown(self): self.env.shutdown() shutil.rmtree(self.env.path) def _create_file(self, test_id, extra_content=None): file_name = os.path.join(self.env.path, 'test_' + test_id) with open(file_name, 'w') as f: f.writelines(self.default_content) if extra_content is not None: f.writelines(extra_content) self.env.config.set('account-manager', 'password_file', file_name) def test_check_password_ok(self): """Verify check_password success: correct user and password. """ self._create_file(test_id='check_password_ok') self.assertTrue(self.store.check_password('knights', 'ni')) def test_check_password_bad(self): """Verify check_password failure: correct user, incorrect password. """ self._create_file(test_id='check_password_bad') self.assertFalse(self.store.check_password('knights', 'noo')) def test_check_password_unknown(self): """Verify check_password failure: unknown user.""" self._create_file(test_id='check_password_unknown') self.assertIsNone(self.store.check_password('brian', 'naughty')) def test_delete_user_ok(self): """Verify delete_user success.""" self._create_file(test_id='delete_user_ok') user_to_delete = 'knights' self.assertTrue(self.store.delete_user(user_to_delete)) self.assertFalse(self.store.has_user(user_to_delete)) def test_delete_user_unknown(self): """Verify delete_user failure: unknown user.""" self._create_file(test_id='delete_user_unknown') user_to_delete = 'brian' self.assertFalse(self.store.delete_user(user_to_delete)) def test_get_users(self): self._create_file(test_id='get_users') self.assertEqual( self.store.get_users(), ['spam', 'knights'], ) def test_has_user_ok(self): """Verify has_user for an existing user.""" self._create_file(test_id='has_user_ok') self.assertTrue(self.store.has_user('knights')) def test_has_user_unknown(self): """Verify has_user for an unknown user.""" self._create_file(test_id='has_user_unknown') self.assertFalse(self.store.has_user('brian')) def test_set_password(self): """Verify set_password for a new user.""" self._create_file(test_id='set_password') new_user = 'brian' new_passwd = 'naughty' self.assertFalse(self.store.has_user(new_user)) self.assertTrue(self.store.set_password(new_user, new_passwd)) self.assertTrue(self.store.check_password(new_user, new_passwd)) def test_update_password(self): """Verify set_password for an existing user.""" self._create_file(test_id='update_password') existing_user = 'knights' new_passwd = 'noo' self.assertTrue(self.store.has_user(existing_user)) self.assertFalse(self.store.set_password(existing_user, new_passwd)) self.assertTrue(self.store.check_password(existing_user, new_passwd)) def test_overwrite_password(self): """Verify set_password rejects overwrite when instructed.""" self._create_file(test_id='update_password') existing_user = 'knights' new_passwd = 'noo' self.assertTrue(self.store.has_user(existing_user)) self.assertFalse(self.store.set_password(existing_user, new_passwd, overwrite=False)) self.assertFalse(self.store.check_password(existing_user, new_passwd)) def test_suite(): """ The assembled test suite. """ suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(SvnServePasswordTestCase)) return suite if __name__ == '__main__': unittest.main(defaultTest='test_suite') trac-accountmanager-0.6~svn18547/acct_mgr/tests/util.py0000644000175500017550000000664514414633245022656 0ustar debacledebacle# -*- coding: utf-8 -*- # # Copyright (C) 2012 Steffen Hoffmann # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Steffen Hoffmann import unittest from datetime import datetime from trac.util.html import Fragment, tag from ..compat import unicode from ..util import i18n_tag, format_timespan, remove_zwsp class UtilTestCase(unittest.TestCase): def test_format_timespan(self): self.assertEqual(format_timespan(0), '') self.assertEqual(format_timespan(1), '1 second') self.assertEqual(format_timespan(2), '2 seconds') self.assertEqual(format_timespan(119), '119 seconds') self.assertEqual(format_timespan(120), '00:02:00') self.assertEqual(format_timespan(86399), '23:59:59') self.assertEqual(format_timespan(86400), '1 day') self.assertEqual(format_timespan(86400 + 1), '1 day 1 second') d42 = 86400 * 42 self.assertEqual(format_timespan(d42 - 1), '41 days 23:59:59') self.assertEqual(format_timespan(d42), '42 days') self.assertEqual(format_timespan(d42 + 119), '42 days 119 seconds') self.assertEqual(format_timespan(d42 + 120), '42 days 00:02:00') self.assertEqual(format_timespan(d42 + 86399), '42 days 23:59:59') def test_remove_zwsp(self): self.assertEqual(u'user', remove_zwsp(u'user')) self.assertEqual(u'user', remove_zwsp(u'\u200buser\u200b')) self.assertEqual(u'user', remove_zwsp(u'\u200fu\ufe00ser\u061c')) self.assertEqual(u'u\U000e00ffser', remove_zwsp(u'u\U000e00ffser')) self.assertEqual(u'user', remove_zwsp(u'u\U000e0100ser')) self.assertEqual(u'user', remove_zwsp(u'u\U000e01efser')) self.assertEqual(u'u\U000e01f0ser', remove_zwsp(u'u\U000e01f0ser')) def test_i18n_tag(self): def do_i18n_tag(string, *args): result = i18n_tag(string, *args) self.assertIsInstance(result, Fragment) return unicode(result).replace(u'
', '
') self.assertEqual( do_i18n_tag('Try [1:downloading] the file instead', tag.a(href='http://localhost/')), 'Try downloading the file instead', ) self.assertEqual( do_i18n_tag('[1:Note:] See [2:TracBrowser] for help ...', tag.strong, tag.a(href='data:')), 'Note: See TracBrowser for ' 'help ...', ) self.assertEqual( do_i18n_tag('[1:Note:] See [2:TracBrowser] for help ...', 'strong', ('a', {'href': 'data:'})), 'Note: See TracBrowser for ' 'help ...', ) self.assertEqual( do_i18n_tag('Powered by [1:[2:Trac]][3:]By [4:Edgewall Software]', tag.a(href='/about'), 'strong', 'br', tag.a(href='https://www.edgewall.org/')), 'Powered by Trac
By ' 'Edgewall Software', ) def test_suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(UtilTestCase)) return suite if __name__ == '__main__': unittest.main(defaultTest='test_suite') trac-accountmanager-0.6~svn18547/acct_mgr/tests/pwhash.py0000644000175500017550000001106514413553530023160 0ustar debacledebacle# -*- coding: utf-8 -*- # # Copyright (C) 2023 Jun Omae # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. import unittest try: import bcrypt except ImportError: bcrypt = None from ..pwhash import ( passlib, crypt, generate_hash, check_hash, _passlib_generate_hash, _passlib_check_hash, _crypt_generate_hash, _crypt_check_hash, _unavai_generate_hash, _unavai_check_hash, ) _PASSWORD = 'wesViWip.Actyur3' _HASHES = { 'crypt': 'dZn8UE10rIAR6', 'md5': '$apr1$975LB2J8$pbifv0nfpzcxQ9gSe2tdM0', 'sha': '{SHA}91NeIA99KkOtqtRlpl4Z8jC4hlA=', 'sha256': '$5$rounds=2500$cZbJvPaLtPHPcPsa$5YC2vX.WXMmsXEinQuG50P48CLeZddf' 'YUPr7dczt2n5', 'sha512': '$6$rounds=2500$6yz3SzAbZhCb5Whm$aggkw/9XqtO4dhrlyXAhI7M/8o3vTBk' 'XrZ071QikKDnOtUv6DkafqbPtfAr8nuOfgoB1dXPwwxpcgAJc01x/P0', 'bcrypt': '$2b$11$oshDukXxHZQvPEOBonYqDehZ0QXnpqkc2jamNS/35vlfZ41psDL4O', } class BaseTestCase(unittest.TestCase): methods = () generate_hash = None check_hash = None def _test_prefix(self, method, prefix): if method not in self.methods: raise unittest.SkipTest('%s unsupported' % method) hash_ = self.generate_hash(_PASSWORD, method) self.assertTrue(hash_.startswith(prefix), "%r doesn't start with %r" % (hash_, prefix)) def test_md5_prefix(self): self._test_prefix('md5', '$apr1$') def test_sha_prefix(self): self._test_prefix('sha', '{SHA}') def test_sha256_prefix(self): self._test_prefix('sha256', '$5$') def test_sha512_prefix(self): self._test_prefix('sha512', '$6$') def test_bcrypt_prefix(self): self._test_prefix('bcrypt', ('$2b$', '$2a$')) def test_generate_hash(self): for method in self.methods: hash1 = self.generate_hash(_PASSWORD, method) hash2 = self.generate_hash(_PASSWORD, method) if method == 'sha': self.assertTrue(self.check_hash(_PASSWORD, hash1)) self.assertEqual(hash1, hash2) else: self.assertTrue(self.check_hash(_PASSWORD, hash1)) self.assertTrue(self.check_hash(_PASSWORD, hash2)) self.assertNotEqual(hash1, hash2) def test_check_hash(self): for method in self.methods: hash_ = _HASHES[method] self.assertTrue(self.check_hash(_PASSWORD, hash_)) def _passlib_methods(): if not passlib: return () methods = ['crypt', 'md5', 'sha', 'sha256', 'sha512'] if bcrypt: methods.append('bcrypt') return frozenset(methods) def _crypt_metdhos(): if not crypt: return () methods = ['md5', 'sha'] if hasattr(crypt, 'methods'): pairs = [ ('crypt', 'METHOD_CRYPT'), ('sha256', 'METHOD_SHA256'), ('sha512', 'METHOD_SHA512'), ('bcrypt', 'METHOD_BLOWFISH'), ] for name, method in pairs: if getattr(crypt, method, None) in crypt.methods: methods.append(name) return frozenset(methods) @unittest.skipUnless(passlib, 'passlib unavailable') class PasslibTestCase(BaseTestCase): methods = _passlib_methods() def generate_hash(self, password, method): return _passlib_generate_hash(password, method) def check_hash(self, password, hash): return _passlib_check_hash(password, hash) @unittest.skipUnless(crypt, 'crypt unavailable') class CryptTestCase(BaseTestCase): methods = _crypt_metdhos() def generate_hash(self, password, method): return _crypt_generate_hash(password, method) def check_hash(self, password, hash): return _crypt_check_hash(password, hash) class MethodTestCase(unittest.TestCase): def test_methods(self): if passlib: self.assertEqual(generate_hash, _passlib_generate_hash) self.assertEqual(check_hash, _passlib_check_hash) elif crypt: self.assertEqual(generate_hash, _crypt_generate_hash) self.assertEqual(check_hash, _crypt_check_hash) else: self.assertEqual(generate_hash, _unavai_generate_hash) self.assertEqual(check_hash, _unavai_check_hash) def test_suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(PasslibTestCase)) suite.addTest(unittest.makeSuite(CryptTestCase)) suite.addTest(unittest.makeSuite(MethodTestCase)) return suite if __name__ == '__main__': unittest.main(defaultTest='test_suite') trac-accountmanager-0.6~svn18547/acct_mgr/tests/api.py0000644000175500017550000001701314413553530022436 0ustar debacledebacle# -*- coding: utf-8 -*- # # Copyright (C) 2012,2013 Steffen Hoffmann # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Steffen Hoffmann import shutil import tempfile import unittest from trac.core import TracError from trac.perm import PermissionCache, PermissionSystem from trac.test import EnvironmentStub, MockRequest from trac.web.session import Session from ..api import AccountManager from ..db import SessionStore class _BaseTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True, enable=['trac.*', 'acct_mgr.api.*', 'acct_mgr.db.SessionStore', 'acct_mgr.pwhash.*', 'acct_mgr.htfile.HtDigestStore'] ) self.env.path = tempfile.mkdtemp() self.perm = PermissionSystem(self.env) def tearDown(self): # Really close db connections. self.env.shutdown() shutil.rmtree(self.env.path) class AccountManagerTestCase(_BaseTestCase): def setUp(self): _BaseTestCase.setUp(self) self.mgr = AccountManager(self.env) self.store = SessionStore(self.env) self.store.set_password('user', 'passwd') args = dict(username='user', name='', email='') self.req = MockRequest(self.env, authname='user1', args=args) def test_set_password(self): # Can't work without at least one password store. self.assertRaises(TracError, self.mgr.set_password, 'user', 'passwd') self.env.config.set( 'account-manager', 'password_store', 'SessionStore') self.mgr.set_password('user', 'passwd') # Refuse to overwrite existing credentials, if requested. self.assertRaises(TracError, self.mgr.set_password, 'user', 'passwd', overwrite=False) def test_approval_admin_keep_perm(self): self.perm.grant_permission('admin', 'ACCTMGR_ADMIN') # Some elevated permission action. action = 'USER_VIEW' self.assertFalse(action in PermissionCache(self.env)) req = self.req req.perm = PermissionCache(self.env, 'admin') req.session = Session(self.env, req) req.session.save() self.mgr.pre_process_request(req, None) self.assertTrue(action in req.perm) # Mock an authenticated request with account approval pending. req.session['approval'] = 'pending' req.session.save() # Don't touch admin user requests. self.mgr.pre_process_request(req, None) self.assertTrue(action in req.perm) def test_approval_user_strip_perm(self): # Some elevated permission action. action = 'USER_VIEW' self.assertFalse(action in PermissionCache(self.env)) self.perm.grant_permission('user', action) req = self.req req.perm = PermissionCache(self.env, 'user') req.session = Session(self.env, req) req.session.save() self.mgr.pre_process_request(req, None) self.assertTrue(action in req.perm) # Mock an authenticated request with account approval pending. req.session['approval'] = 'pending' req.session.save() # Remove elevated permission, if account approval is pending. self.mgr.pre_process_request(req, None) self.assertFalse(action in req.perm) def test_maybe_update_hash(self): # Configure another, primary password store. self.env.config.set('account-manager', 'password_store', 'HtDigestStore, SessionStore') self.env.config.set('account-manager', 'htdigest_file', '.htdigest') self.env.db_transaction(""" INSERT INTO session_attribute (sid,authenticated,name,value) VALUES (%s,%s,%s,%s) """, ('user', 1, 'password_refreshed', '1')) # Refresh not happening due to 'password_refreshed' attribute. self.mgr._maybe_update_hash('user', 'passwd') for _, in self.env.db_query(""" SELECT value FROM session_attribute WHERE sid='user' AND authenticated=1 AND name='password' """): break else: self.fail("Session attribute 'password' not found.") self.env.db_transaction(""" DELETE FROM session_attribute WHERE sid='user' AND authenticated=1 AND name='password_refreshed' """) # Refresh (and effectively migrate) user credentials. self.mgr._maybe_update_hash('user', 'passwd') for _, in self.env.db_query(""" SELECT value FROM session_attribute WHERE sid='user' AND authenticated=1 AND name='password' """): self.fail("Session attribute 'password' should not be found.") for value, in self.env.db_query(""" SELECT value FROM session_attribute WHERE sid='user' AND authenticated=1 AND name='password_refreshed' """): self.assertEqual('1', value) break else: self.fail("Session attribute 'password_refreshed' not found.") class PermissionTestCase(_BaseTestCase): def setUp(self): _BaseTestCase.setUp(self) self.req = MockRequest(self.env) self.actions = ['ACCTMGR_ADMIN', 'ACCTMGR_CONFIG_ADMIN', 'ACCTMGR_USER_ADMIN', 'EMAIL_VIEW', 'USER_VIEW'] # Tests def test_available_actions(self): for action in self.actions: self.assertFalse(action not in self.perm.get_actions()) def test_available_actions_no_perms(self): for action in self.actions: self.assertFalse(self.perm.check_permission(action, 'anonymous')) def test_available_actions_config_admin(self): user = 'config_admin' self.perm.grant_permission(user, 'ACCTMGR_CONFIG_ADMIN') actions = [self.actions[0]] + self.actions[2:] for action in actions: self.assertFalse(self.perm.check_permission(action, user)) def test_available_actions_user_admin(self): user = 'user_admin' self.perm.grant_permission(user, 'ACCTMGR_USER_ADMIN') for action in self.actions[2:]: self.assertTrue(self.perm.check_permission(action, user)) for action in self.actions[:2] + ['TRAC_ADMIN']: self.assertFalse(self.perm.check_permission(action, user)) def test_available_actions_full_perms(self): perm_map = dict(acctmgr_admin='ACCTMGR_ADMIN', trac_admin='TRAC_ADMIN') for user in perm_map: self.perm.grant_permission(user, perm_map[user]) for action in self.actions: self.assertTrue(self.perm.check_permission(action, user)) if user != 'trac_admin': self.assertFalse(self.perm.check_permission('TRAC_ADMIN', user)) def test_suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(AccountManagerTestCase)) suite.addTest(unittest.makeSuite(PermissionTestCase)) return suite if __name__ == '__main__': unittest.main(defaultTest='test_suite') trac-accountmanager-0.6~svn18547/acct_mgr/tests/htfile.py0000644000175500017550000002416514413553530023146 0ustar debacledebacle# -*- coding: utf-8 -*- # # Copyright (C) 2005 Matthew Good # Copyright (C) 2011-2013 Steffen Hoffmann # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Matthew Good import io import os.path import shutil import tempfile import unittest from trac.test import EnvironmentStub from ..htfile import HtDigestStore, HtPasswdStore class _BaseTestCase(unittest.TestCase): def setUp(self): self.basedir = os.path.realpath(tempfile.mkdtemp()) self.env = EnvironmentStub() self.env.path = os.path.join(self.basedir, 'trac-tempenv') os.mkdir(self.env.path) def tearDown(self): shutil.rmtree(self.basedir) # Helpers def _create_file(self, *path, **kw): filename = os.path.join(self.basedir, *path) dirname = os.path.dirname(filename) if not os.path.exists(dirname): os.makedirs(dirname) content = kw.get('content') with io.open(filename, 'w', encoding='utf-8') as fd: if content is not None: if isinstance(content, bytes): content = content.decode('utf-8') fd.write(content) return filename def _init_password_file(self, flavor, filename, content=''): filename = self._create_file(filename, content=content) self.env.config.set('account-manager', flavor + '_file', filename) def _do_password_test(self, flavor, filename, content): self._init_password_file(flavor, filename, content) self.assertTrue(self.store.check_password('user', 'password')) # Tests def test_overwrite(self): self._init_password_file(self.flavor, 'test_overwrite_%s' % self.flavor) self.assertTrue(self.store.set_password('user1', 'password1')) self.assertFalse(self.store.set_password('user1', 'password2', overwrite=False)) self.assertTrue(self.store.check_password('user1', 'password1')) self.assertTrue(self.store.set_password('user2', 'password', overwrite=False)) def test_unicode(self): self.env.config.set('account-manager', 'htdigest_realm', u'UnicodeRealm\u4e60') user = u'\u4e61' password = u'\u4e62' self._init_password_file(self.flavor, 'test_unicode_%s' % self.flavor) self.store.set_password(user, password) self.assertEqual(list(self.store.get_users()), [user]) self.assertTrue(self.store.check_password(user, password)) self.assertTrue(self.store.delete_user(user)) self.assertEqual(list(self.store.get_users()), []) class HtDigestTestCase(_BaseTestCase): flavor = 'htdigest' def setUp(self): _BaseTestCase.setUp(self) self.env.config.set('account-manager', 'htdigest_realm', 'TestRealm') self.store = HtDigestStore(self.env) def test_userline(self): self.assertEqual(self.store.userline('user', 'password'), 'user:TestRealm:752b304cc7cf011d69ee9b79e2cd0866') def test_file(self): self._do_password_test( self.flavor, 'test_file', 'user:TestRealm:752b304cc7cf011d69ee9b79e2cd0866') def test_update_password(self): self._init_password_file(self.flavor, 'test_passwdupd') self.store.set_password('foo', 'pass1') self.assertFalse(self.store.check_password('foo', 'pass2')) self.store.set_password('foo', 'pass2') self.assertTrue(self.store.check_password('foo', 'pass2')) self.store.set_password('foo', 'pass3', 'pass2') self.assertTrue(self.store.check_password('foo', 'pass3')) class HtPasswdTestCase(_BaseTestCase): flavor = 'htpasswd' def setUp(self): _BaseTestCase.setUp(self) self.store = HtPasswdStore(self.env) def test_md5(self): self._do_password_test(self.flavor, 'test_md5', 'user:$apr1$xW/09...$fb150dT95SoL1HwXtHS/I0\n') def test_crypt(self): self._do_password_test(self.flavor, 'test_crypt', 'user:QdQ/xnl2v877c\n') def test_sha(self): self._do_password_test(self.flavor, 'test_sha', 'user:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=\n') def test_sha256(self): try: self._do_password_test(self.flavor, 'test_sha256', 'user:$5$rounds=535000$saltsaltsaltsalt$' 'wfx3LZ09XA7qrZB.ttuCbBidMXt51Kgu5YQ.YFq' 'zxA7\n') except NotImplementedError: pass try: self._do_password_test(self.flavor, 'test_sha256', 'user:$5$.YGr6HhGl0TNN7dT$Jrq.j68U8/rrBo5Ks' 'YycA05JS3ZXzGn5C1u54Fh8l7.\n') except NotImplementedError: pass try: self._do_password_test(self.flavor, 'test_sha256', 'user:$5$/sSmB.1hM8M7YlWP$qQwILOVGJ7Z/JNIml' 'ZmweMBy7VKO9pwmmAwBf0YrOB6\n') except NotImplementedError: pass def test_sha512(self): try: self._do_password_test(self.flavor, 'test_sha512', 'user:$6$rounds=535000$saltsaltsaltsalt$' '9ExQK2S3YXW7/FlfUcw2vy7WF.NH5ZF6SIT14Dj' 'ngOGkcx.5mINko67cLRrqFFh1AltOT4uPnET7Bs' 'JXuI56H/\n') except NotImplementedError: pass try: self._do_password_test(self.flavor, 'test_sha512', 'user:$6$.Gz0dqxtsVYNeES3$H9cLym9oGJqILw1fj' 'XHm2Ha54ZJhQ/h8XMaxWpKnPziXqI0nu45a1Rsbrde' '42gNbh.78EiFB0A0Qlpdps7JTg1\n') except NotImplementedError: pass try: self._do_password_test(self.flavor, 'test_sha512', 'user:$6$/X87Imrz04AZGkLr$fxSvADugt7V3ufvvg' 'NGG0BStDjEAwLYRUGjWxlJX0zcD64RxBOAldIDdqNZ' 'bDHoDv.GjVOVfQxjMZwWVBFxgz0\n') except NotImplementedError: pass def test_no_trailing_newline(self): self._do_password_test(self.flavor, 'test_no_trailing_newline', 'user:$apr1$xW/09...$fb150dT95SoL1HwXtHS/I0') def test_add_with_no_trailing_newline(self): filename = self._create_file( 'test_add_with_no_trailing_newline', content='user:$apr1$xW/09...$fb150dT95SoL1HwXtHS/I0') self.env.config.set('account-manager', 'htpasswd_file', filename) self.assertTrue(self.store.check_password('user', 'password')) self.store.set_password('user2', 'password2') self.assertTrue(self.store.check_password('user', 'password')) self.assertTrue(self.store.check_password('user2', 'password2')) def test_update_password(self): self._init_password_file(self.flavor, 'test_passwdupd') self.store.set_password('foo', 'pass1') self.assertFalse(self.store.check_password('foo', 'pass2')) self.store.set_password('foo', 'pass2') self.assertTrue(self.store.check_password('foo', 'pass2')) self.store.set_password('foo', 'pass3', 'pass2') self.assertTrue(self.store.check_password('foo', 'pass3')) def test_create_hash(self): self._init_password_file(self.flavor, 'test_hash') self.env.config.set('account-manager', 'htpasswd_hash_type', 'bad') self.assertTrue(self.store.userline('user', 'password').startswith('user:')) self.assertFalse(self.store.userline('user', 'password' ).startswith('user:$apr1$')) self.assertFalse(self.store.userline('user', 'password' ).startswith('user:{SHA}')) self.store.set_password('user', 'password') self.assertTrue(self.store.check_password('user', 'password')) self.env.config.set('account-manager', 'htpasswd_hash_type', 'md5') self.assertTrue(self.store.userline('user', 'password' ).startswith('user:$apr1$')) self.store.set_password('user', 'password') self.assertTrue(self.store.check_password('user', 'password')) self.env.config.set('account-manager', 'htpasswd_hash_type', 'sha') self.assertTrue(self.store.userline('user', 'password' ).startswith('user:{SHA}')) self.store.set_password('user', 'password') self.assertTrue(self.store.check_password('user', 'password')) self.env.config.set('account-manager', 'htpasswd_hash_type', 'sha256') try: self.assertTrue(self.store.userline('user', 'password' ).startswith('user:$5$')) except NotImplementedError: pass else: self.store.set_password('user', 'password') self.assertTrue(self.store.check_password('user', 'password')) self.env.config.set('account-manager', 'htpasswd_hash_type', 'sha512') try: self.assertTrue(self.store.userline('user', 'password' ).startswith('user:$6$')) except NotImplementedError: pass else: self.store.set_password('user', 'password') self.assertTrue(self.store.check_password('user', 'password')) def test_suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(HtDigestTestCase)) suite.addTest(unittest.makeSuite(HtPasswdTestCase)) return suite if __name__ == '__main__': unittest.main(defaultTest='test_suite') trac-accountmanager-0.6~svn18547/acct_mgr/tests/__init__.py0000644000175500017550000000334314413553530023425 0ustar debacledebacle# -*- coding: utf-8 -*- # # Copyright (C) 2005 Matthew Good # Copyright (C) 2015 Steffen Hoffmann # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Matthew Good import sys import unittest _twill_required = 'Twill>=2' try: import twill except ImportError: twill = None INCLUDE_FUNCTIONAL_TESTS = False else: # XXX Avoid tracenv log writing to stdout via twill.log if hasattr(twill, 'log') and hasattr(twill, 'handler'): twill.log.removeHandler(twill.handler) import pkg_resources try: pkg_resources.require(_twill_required) except: INCLUDE_FUNCTIONAL_TESTS = False twill = None else: INCLUDE_FUNCTIONAL_TESTS = True def test_suite(): from . import (admin, api, db, guard, htfile, model, pwhash, register, svnserve, util) from ..opt import tests as opt_tests suite = unittest.TestSuite() for mod in (admin, api, db, guard, htfile, model, pwhash, register, svnserve, util, opt_tests): suite.addTest(mod.test_suite()) if INCLUDE_FUNCTIONAL_TESTS: from . import functional suite.addTest(functional.test_suite()) elif not twill: sys.stderr.write('SKIP: functional tests (%s unavailable)\n' % _twill_required) else: sys.stderr.write('SKIP: functional tests\n') return suite if __name__ == '__main__': if '--skip-functional-tests' in sys.argv: sys.argv.remove('--skip-functional-tests') INCLUDE_FUNCTIONAL_TESTS = False unittest.main(defaultTest='test_suite') trac-accountmanager-0.6~svn18547/acct_mgr/tests/functional/0000755000175500017550000000000014413553530023453 5ustar debacledebacletrac-accountmanager-0.6~svn18547/acct_mgr/tests/functional/__init__.py0000644000175500017550000000566514413553530025600 0ustar debacledebacle# -*- coding: utf-8 -*- # # Copyright (C) 2008 Matthew Good # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Pedro Algarvio import io import os import unittest import twill try: import tidylib except ImportError: tidylib = None class TwillCommands(object): def __init__(self, commands): self.commands = commands self.browser = commands.browser def __getattr__(self, name): value = getattr(self.commands, name) if not callable(value): return value def wrapper(*args, **kwargs): prev = self.browser.result try: try: return value(*args, **kwargs) finally: result = self.browser.result if result is not prev and 200 <= result.http_code < 300: self.commands.tidy_ok() except twill.errors.TwillException as e: testname = _state.testname if not testname: raise filename = os.path.join(_state.testenv.tracdir, 'log', testname + '.html') html = self.browser.html with io.open(filename, 'w', encoding='utf-8') as f: f.write(html) raise twill.errors.TwillAssertionError('%s at %s' % (e, filename)) return wrapper tc = TwillCommands(twill.commands) if tidylib: twill.commands.config('require_tidy', 1) twill.commands.config('tidy_show_warnings', 'no') class FunctionalTestState(object): testenv = None tester = None testname = None _state = FunctionalTestState() class FunctionalTestSuite(unittest.TestSuite): def run(self, result): if _state.testenv: testenv = None else: from .testenv import TestEnvironment from .tester import FunctionalTester testenv = TestEnvironment() testenv.init() _state.testenv = testenv _state.tester = FunctionalTester(testenv.url) try: return super(FunctionalTestSuite, self).run(result) finally: if testenv: testenv.cleanup() class FunctionalTestCaseSetup(unittest.TestCase): @property def _testenv(self): return _state.testenv @property def _tester(self): return _state.tester @property def _smtpd(self): return _state.testenv.smtpd def setUp(self): _state.testname = self.__class__.__name__ def tearDown(self): _state.testname = None def test_suite(): from . import testcases return testcases.test_suite() if __name__ == '__main__': unittest.main(defaultTest='test_suite') trac-accountmanager-0.6~svn18547/acct_mgr/tests/functional/testenv.py0000644000175500017550000001554714413553530025531 0ustar debacledebacle# -*- coding: utf-8 -*- # # Copyright (C) 2008 Matthew Good # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Pedro Algarvio import os import sys import time import socket import subprocess from trac.env import Environment from trac.util.compat import close_fds try: from trac.test import rmtree except ImportError: from shutil import rmtree from ...compat import unicode from .smtpd import SMTPThreadedServer TOX_ENV_DIR = os.environ.get('TOX_ENV_DIR') if hasattr(subprocess.Popen, '__enter__'): Popen = subprocess.Popen else: class Popen(subprocess.Popen): def __enter__(self): return self def __exit__(self, *args): try: if self.stdin: self.stdin.close() finally: self.wait() for f in (self.stdout, self.stderr): if f: f.close() def get_topdir(): path = os.path.dirname(os.path.abspath(__file__)) suffix = '/acct_mgr/tests/functional'.replace('/', os.sep) if not path.endswith(suffix): raise RuntimeError("%r doesn't end with %r" % (path, suffix)) return path[:-len(suffix)] def get_testdir(): dir_ = TOX_ENV_DIR if dir_ and os.path.isdir(dir_): dir_ = os.path.join(dir_, 'tmp') else: dir_ = get_topdir() if not os.path.isabs(dir_): raise RuntimeError('Non absolute directory: %s' % repr(dir_)) return os.path.join(dir_, 'testenv') def get_ephemeral_port(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(('127.0.0.1', 0)) s.listen(1) return s.getsockname()[1] finally: s.close() def to_b(value): if isinstance(value, unicode): return value.encode('utf-8') if isinstance(value, bytes): return value raise ValueError(type(value)) class TestEnvironment(object): _testdir = get_testdir() _plugins_dir = os.path.join(_testdir, 'plugins') if not TOX_ENV_DIR else '' _devnull = None _log = None port = None smtp_port = None tracdir = None url = None smtpd = None _env = None _tracd = None def __init__(self): if os.path.isdir(self._testdir): rmtree(self._testdir) os.mkdir(self._testdir) if self._plugins_dir: os.mkdir(self._plugins_dir) _inherit_template = """\ [inherit] plugins_dir = %(plugins_dir)s [logging] log_type = file log_level = INFO [trac] base_url = %(url)s use_chunked_encoding = disabled [project] url = %(url)s admin = testenv%(port)d@localhost [notification] smtp_enabled = enabled smtp_from = testenv%(port)d@localhost smtp_port = %(smtp_port)d smtp_server = localhost """ @property def inherit_file(self): return self._inherit_template % \ {'plugins_dir': self._plugins_dir, 'url': self.url, 'port': self.port, 'smtp_port': self.smtp_port} def init(self): self._devnull = os.open(os.devnull, os.O_RDWR) self._log = os.open(os.path.join(self._testdir, 'tracd.log'), os.O_WRONLY | os.O_CREAT | os.O_APPEND) self.port = get_ephemeral_port() self.smtp_port = get_ephemeral_port() if self._plugins_dir: self.check_call([sys.executable, 'setup.py', 'develop', '-mxd', self._plugins_dir]) self.tracdir = os.path.join(self._testdir, 'trac') self.url = 'http://127.0.0.1:%d/%s' % \ (self.port, os.path.basename(self.tracdir)) inherit = os.path.join(self._testdir, 'inherit.ini') with open(inherit, 'w') as f: f.write(self.inherit_file) args = [sys.executable, '-m', 'trac.admin.console', self.tracdir] with self.popen(args, stdin=subprocess.PIPE) as proc: proc.stdin.write( b'initenv --inherit=%s testenv%d sqlite:db/trac.db\n' b'config set components acct_mgr.* enabled\n' b'config set components trac.web.auth.loginmodule disabled\n' % (to_b(inherit), self.port)) self.smtpd = SMTPThreadedServer(self.smtp_port) self.smtpd.start() self.start() def cleanup(self): self.stop() if self.smtpd: self.smtpd.stop() self.smtpd = None if self._env: self._env.shutdown() self._env = None if self._devnull is not None: os.close(self._devnull) self._devnull = None if self._log is not None: os.close(self._log) self._log = None def start(self): if self._tracd and self._tracd.returncode is None: raise RuntimeError('tracd is running') args = [ sys.executable, '-m', 'trac.web.standalone', '--port=%d' % self.port, '--hostname=localhost', self.tracdir, ] self._tracd = self.popen(args, stdout=self._log, stderr=self._log) start = time.time() while time.time() - start < 10: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: s.connect(('127.0.0.1', self.port)) except socket.error: time.sleep(0.125) else: break finally: s.close() else: raise RuntimeError('Timed out waiting for tracd to start') def stop(self): if self._tracd: try: self._tracd.terminate() except EnvironmentError: pass self._tracd.wait() self._tracd = None def restart(self): self.stop() self.start() def popen(self, *args, **kwargs): kwargs.setdefault('stdin', self._devnull) kwargs.setdefault('stdout', self._devnull) kwargs.setdefault('stderr', self._devnull) kwargs.setdefault('close_fds', close_fds) return Popen(*args, **kwargs) def check_call(self, *args, **kwargs): kwargs.setdefault('stdin', self._devnull) kwargs.setdefault('stdout', subprocess.PIPE) kwargs.setdefault('stderr', subprocess.PIPE) with self.popen(*args, **kwargs) as proc: stdout, stderr = proc.communicate() if proc.returncode != 0: raise RuntimeError('Exited with %d (stdout %r, stderr %r)' % (proc.returncode, stdout, stderr)) def get_trac_environment(self): if not self._env: self._env = Environment(self.tracdir) return self._env def _tracadmin(self, *args): self.check_call((sys.executable, '-m', 'trac.admin.console', self.tracdir) + args) trac-accountmanager-0.6~svn18547/acct_mgr/tests/functional/smtpd.py0000644000175500017550000000741614413553530025164 0ustar debacledebacle# -*- coding: utf-8 -*- # # Copyright (C) 2008 Matthew Good # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Pedro Algarvio from __future__ import absolute_import import asyncore import smtpd import threading class NonForgetingSMTPServerStore(object): """ Non forgetting store for SMTP data. """ # We override trac's implementation of a mailstore because if forgets # the last message when a new one arrives. # Account Manager at times sends more than one email and we need to be # able to test both def __init__(self): self.messages = {} self.last_message = {} @property def recipients(self): return self.last_message.get('recipients') @property def sender(self): return self.last_message.get('sender') @property def message(self): return self.last_message.get('message') def process_message(self, peer, mailfrom, rcpttos, data, **kwargs): message = {'recipients': rcpttos, 'sender': mailfrom, 'message': data} self.messages.update((recipient, message) for recipient in rcpttos) self.last_message = message def full_reset(self): self.messages.clear() self.last_message.clear() class SMTPServer(smtpd.SMTPServer): store = None def __init__(self, localaddr, store): smtpd.SMTPServer.__init__(self, localaddr, None) self.store = store def process_message(self, peer, mailfrom, rcpttos, data, **kwargs): self.store.process_message(peer, mailfrom, rcpttos, data, **kwargs) class SMTPThreadedServer(threading.Thread): """ Run a SMTP server for a single connection, within a dedicated thread """ host = 'localhost' def __init__(self, port): self.port = port self.store = NonForgetingSMTPServerStore() super(SMTPThreadedServer, self).__init__(target=asyncore.loop, args=(0.1, True)) self.daemon = True def start(self): self.server = SMTPServer((self.host, self.port), self.store) super(SMTPThreadedServer, self).start() def stop(self): self.server.close() self.join() def get_sender(self, recipient=None): """Return the sender of a message. If recipient is passed, return the sender for the message sent to that recipient, else, send the sender for last message""" try: return self.store.messages[recipient]['sender'] except KeyError: return self.store.sender def get_recipients(self, recipient=None): """Return the recipients of a message. If recipient is passed, return the recipients for the message sent to that recipient, else, send recipients for last message""" try: return self.store.messages[recipient]['recipients'] except KeyError: return self.store.recipients def get_message(self, recipient=None): """Return the message of a message. If recipient is passed, return the actual message for the message sent to that recipient, else, send the last message""" try: return self.store.messages[recipient]['message'] except KeyError: return self.store.message def get_message_parts(self, recipient): """Return the message parts(dict). If recipient is passed, return the parts for the message sent to that recipient, else, send the parts for last message""" try: return self.store.messages[recipient] except KeyError: return None def full_reset(self): self.store.full_reset() trac-accountmanager-0.6~svn18547/acct_mgr/tests/functional/tester.py0000644000175500017550000000445514413553530025343 0ustar debacledebacle# -*- coding: utf-8 -*- # # Copyright (C) 2008 Matthew Good # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Pedro Algarvio import re from . import tc internal_error = 'Trac detected an internal error:' class FunctionalTester(object): url = None def __init__(self, url): self.url = url self.go_to_front() def go_to_url(self, url): tc.go(url) tc.url(re.escape(url)) tc.notfind(internal_error) def go_to_front(self): """Go to the Trac front page""" self.go_to_url(self.url) def login(self, username, passwd=None): """Override FunctionalTester.login, we're not using Basic Authentication.""" if not passwd: passwd = username login_form_name = 'acctmgr_loginform' self.go_to_front() tc.find('Login') tc.follow('Login') tc.formvalue(login_form_name, 'user', username) tc.formvalue(login_form_name, 'password', passwd) tc.submit() tc.find("logged in as ]+>%s" % username) tc.find("Logout") tc.url(self.url) tc.notfind(internal_error) def logout(self): tc.formvalue('logout', 'logout', 'Logout') tc.submit() tc.notfind(internal_error) tc.notfind('logged in as') def register(self, username, email='', passwd=None): """Allow user registration.""" if not passwd: passwd = username reg_form_name = 'acctmgr_registerform' tc.find("Register") tc.follow("Register") tc.formvalue(reg_form_name, 'user', username) tc.formvalue(reg_form_name, 'password', passwd) tc.formvalue(reg_form_name, 'password_confirm', passwd) tc.formvalue(reg_form_name, 'email', email) tc.submit() tc.notfind("The passwords must match.") tc.notfind(internal_error) tc.find(r'Your username has been successfully registered but your ' r'account still requires activation\. Please login as user ' r'{0}, and follow the instructions\.' .format(re.escape(username))) tc.url('/login$') trac-accountmanager-0.6~svn18547/acct_mgr/tests/functional/testcases.py0000644000175500017550000003700714413553530026032 0ustar debacledebacle# -*- coding: utf-8 -*- # # Copyright (C) 2008 Matthew Good # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Pedro Algarvio import email import re import unittest from . import FunctionalTestSuite, FunctionalTestCaseSetup, tc def parse_smtp_message(data): assert data is not None if isinstance(data, bytes): data = data.decode('utf-8') message = email.message_from_string(data) headers = dict(message.items()) if message.is_multipart(): for part in message.walk(): if part.get_content_type() == 'text/plain': break else: return headers, u'' else: part = message payload = part.get_payload(decode=True) encoding = part.get_content_charset() body = payload.decode(encoding) return headers, body class TestInitialSetup(FunctionalTestCaseSetup): def runTest(self): """initial Trac authentication setup""" tc.find('Login') tc.follow('Login') tc.find(r'logged in as ]+>setup') # step 1 tc.find(r'>\s*Step 1: Authentication Options\s*') tc.formvalue('cfg_wiz', 'acctmgr_login', '1') tc.formvalue('cfg_wiz', 'next', '1') tc.submit() # step 2 tc.find(r'>\s*Step 2: Password Store\s*') tc.formvalue('cfg_wiz', 'init_store', 'file') tc.formvalue('cfg_wiz', 'init_store_file', 'htpasswd') tc.formvalue('cfg_wiz', 'next', '1') tc.submit() # step 3 tc.find(r'>\s*Step 3: Password Policy\s*') tc.formvalue('cfg_wiz', 'next', '1') tc.submit() # step 4 tc.find(r'>\s*Step 4: Account Policy\s*') tc.formvalue('cfg_wiz', 'acctmgr_register', 'true') tc.formvalue('cfg_wiz', 'BasicCheck', '1') tc.formvalue('cfg_wiz', 'EmailCheck', '2') tc.formvalue('cfg_wiz', 'RegExpCheck', '3') tc.formvalue('cfg_wiz', 'RegExpCheck.username_regexp', '^[-A-Za-z0-9._]{3,}$') tc.formvalue('cfg_wiz', 'UsernamePermCheck', '4') tc.formvalue('cfg_wiz', 'next', '1') tc.submit() # step 5 tc.find(r'>\s*Step 5: Account Guard\s*') tc.formvalue('cfg_wiz', 'next', '1') tc.submit() # step 6 - admin tc.find(r'>\s*Step 6: Initialization\s*') tc.find(r'>\s*Add Admin Account:\s*') tc.formvalue('cfg_wiz', 'username', 'admin') tc.formvalue('cfg_wiz', 'password', 'admin') tc.formvalue('cfg_wiz', 'password_confirm', 'admin') tc.formvalue('cfg_wiz', 'add', '1') tc.submit() # step 6 - admin created tc.find(r'\bAccount <[a-z]+>admin created\.') tc.find(r'>\s*Step 6: Initialization\s*') #tc.notfind(r'>\s*Add Admin Account:\s*') tc.formvalue('cfg_wiz', 'save', '1') tc.submit() # wizard finished tc.url(re.escape(self._tester.url)) tc.find(r'>WikiStart') tc.find(r'logged in as ]+>setup') self._tester.logout() class TestFormLoginAdmin(FunctionalTestCaseSetup): def runTest(self): """Login with test user 'admin'""" self._tester.login('admin') self._tester.logout() class TestAdminFormAddUser(FunctionalTestCaseSetup): def runTest(self): self._tester.login('admin') tc.find('Admin') tc.follow('Admin') tc.find('Users') tc.follow('Users') tc.find(r'\s*Add New Account:\s*') form = 'account-editor' tc.formvalue(form, 'username', 'user') tc.formvalue(form, 'password', 'user') tc.formvalue(form, 'password_confirm', 'user') tc.formvalue(form, 'email', 'user@trac.example.org') tc.formvalue(form, 'email_approved', []) tc.submit() tc.find(r'\bAccount <[a-z]+>user created\.') tc.find('Users') tc.follow('Users') tc.find(r']*>user') self._tester.logout() class TestFormLoginUser(FunctionalTestCaseSetup): def runTest(self): """Login with test user 'user'""" self._tester.login('user') self._tester.logout() class TestRegisterNewUser(FunctionalTestCaseSetup): def runTest(self): """Register 'testuser'""" self._tester.register('testuser', 'testuser@trac.example.org') class TestLoginNewUser(FunctionalTestCaseSetup): def runTest(self): """Login just registered 'testuser'""" self._tester.login('testuser') self._tester.logout() class TestFailRegisterPasswdConfirmNotPassed(FunctionalTestCaseSetup): def runTest(self): """Fail if no password confirmation is passed""" reg_form_name = 'acctmgr_registerform' username = 'testuser1' tc.find("Register") tc.follow("Register") tc.formvalue(reg_form_name, 'user', username) tc.formvalue(reg_form_name, 'password', username) tc.submit() tc.find(r'The passwords must match\.') class TestFailRegisterDuplicateUsername(FunctionalTestCaseSetup): def runTest(self): """Fail if username exists""" reg_form_name = 'acctmgr_registerform' username = 'testuser' tc.find("Register") tc.follow("Register") tc.formvalue(reg_form_name, 'user', username) tc.formvalue(reg_form_name, 'password', username) tc.formvalue(reg_form_name, 'password_confirm', username) tc.submit() tc.find("Another account or group already exists") class TestNewAccountNotification(FunctionalTestCaseSetup): def runTest(self): """Send out notification on new account registrations""" tc.notfind('Logout') address_to_notify = 'admin@testenv%s.tld' % self._testenv.port new_username = 'foo' new_username_email = "foo@%s" % address_to_notify.split('@')[1] env = self._testenv.get_trac_environment() env.config.set('account-manager', 'account_changes_notify_addresses', address_to_notify) env.config.set('account-manager', 'notify_actions', 'new,change,delete') env.config.set('account-manager', 'force_passwd_change', 'true') env.config.save() self._tester.register(new_username, new_username_email) headers, body = parse_smtp_message(self._smtpd.get_message()) self.assertEqual(self._smtpd.get_recipients(), [address_to_notify]) self.assertEqual(headers['Subject'], '[testenv%d] Account created: %s' % (self._testenv.port, new_username)) self.assertEqual(headers['X-URL'], self._testenv.url) class TestNewAccountEmailVerification(FunctionalTestCaseSetup): def runTest(self): """User is shown info that he needs to verify his address""" user_email = "foo@testenv%s.tld" % self._testenv.port self._tester.login("foo") tc.find(r'An email has been sent to <%s> with a token to ' r'verify your new email address' r'' % re.escape(user_email)) self._tester.go_to_front() tc.find(r'Warning:\s*' r'Your permissions have been limited until you ' r'verify your email address') class VerifyNewAccountEmailAddress(FunctionalTestCaseSetup): def runTest(self): """User confirms his address with mailed token""" headers, body = parse_smtp_message(self._smtpd.get_message()) blines = body.splitlines() token = [l.split() for l in blines if 'Verification Token' in l][0][-1] warning = (r'Warning:\s*' r'Your permissions have been limited until you verify your email address') tc.find('Logout') # User is logged in from previous test self._tester.go_to_front() tc.find(warning) tc.go(self._testenv.url + '/verify_email') reg_form_name = 'acctmgr_verify_email' tc.formvalue(reg_form_name, 'token', token) tc.submit('verify') tc.notfind(warning) tc.find('Thank you for verifying your email address') self._tester.go_to_front() class PasswdResetsNotifiesAdmin(FunctionalTestCaseSetup): def runTest(self): """User password resets notifies admin by mail""" self._tester.logout() self._smtpd.full_reset() # Clean all previous sent emails tc.notfind('Logout') # Goto Login tc.find("Login") tc.follow("Login") # Do we have the Forgot passwd link tc.find('Forgot your password?') tc.follow('Forgot your password?') username = "foo" email_addr = "foo@testenv%s.tld" % self._testenv.port reset_form_name = 'acctmgr_passwd_reset' tc.formvalue(reset_form_name, 'username', username) tc.formvalue(reset_form_name, 'email', email_addr) tc.submit() headers, body = parse_smtp_message( self._smtpd.get_message('admin@testenv%s.tld' % self._testenv.port)) self.assertEqual(headers['Subject'], '[testenv%s] Account password reset: %s' % (self._testenv.port, username)) self.assertEqual(headers['X-URL'], self._testenv.url) class PasswdResetsNotifiesUser(FunctionalTestCaseSetup): def runTest(self): """Password reset sends new password to user by mail""" username = "foo" email_addr = "foo@testenv%s.tld" % self._testenv.port headers, self.body = parse_smtp_message(self._smtpd.get_message(email_addr)) self.assertEqual(headers['Subject'], '[testenv%d] Account password reset: %s' % (self._testenv.port, username)) class UserLoginWithMailedPassword(PasswdResetsNotifiesUser): def runTest(self): """User is able to login with the new password""" PasswdResetsNotifiesUser.runTest(self) # Does it include a new password body = self.body username = 'foo' self.assertTrue('Username: %s' % username in body) self.assertTrue('Password:' in body) passwd = [l.split(':')[1].strip() for l in body.splitlines() if 'Password:' in l][0] self._tester.login(username, passwd) class UserIsForcedToChangePassword(FunctionalTestCaseSetup): def runTest(self): """User is forced to change password after resets""" tc.find('Logout') tc.find(r'You are required to change password because of a recent ' r'password change request\.') class UserCantBrowseUntilPasswdChange(PasswdResetsNotifiesUser): def runTest(self): """User can't navigate out of '/prefs/account' before password change""" PasswdResetsNotifiesUser.runTest(self) tc.find('Logout') forced_passwd_change_url = '^%s/prefs/account$' % self._tester.url tc.follow('Roadmap') tc.url(forced_passwd_change_url) tc.follow('View Tickets') tc.url(forced_passwd_change_url) tc.follow('New Ticket') tc.url(forced_passwd_change_url) # Now, let's change his password body = self.body passwd = [l.split(':')[1].strip() for l in body.splitlines() if 'Password:' in l][0] username = 'foo' change_passwd_form = 'userprefs' tc.formvalue(change_passwd_form, 'old_password', passwd) tc.formvalue(change_passwd_form, 'password', username) tc.formvalue(change_passwd_form, 'password_confirm', username) tc.submit() tc.notfind("You are required to change password because of a recent " "password change request") tc.find(r'Thank you for taking the time to update your password\.') # We can now browse away from /prefs/accounts tc.follow('Roadmap') tc.url(self._tester.url + '/roadmap') # Clear the mailstore self._smtpd.full_reset() class DeleteAccountNotifiesAdmin(FunctionalTestCaseSetup): def runTest(self): """Delete account notifies admin""" tc.find("Logout") # We're logged-in from previous post tc.follow("Preferences") tc.follow("Account") tc.url(self._testenv.url + '/prefs/account') delete_account_form_name = 'acctmgr_delete_account' tc.formvalue(delete_account_form_name, 'password', 'foo') tc.submit() tc.find("Login") # We're logged out when we delete our account headers, _ = parse_smtp_message(self._smtpd.get_message()) self.assertEqual(headers['Subject'], '[testenv%d] Account deleted: %s' % (self._testenv.port, 'foo')) class UserNoLongerLogins(FunctionalTestCaseSetup): def runTest(self): """Deleted user can't login""" tc.follow('Login') login_form_name = 'acctmgr_loginform' tc.formvalue(login_form_name, 'user', 'foo') tc.formvalue(login_form_name, 'password', 'foo') tc.submit() tc.find("Invalid username or password") tc.notfind('Logout') class UserIsAbleToRegisterWithSameUserName(FunctionalTestCaseSetup): def runTest(self): """Register with deleted username (session and session_attributes clean)""" self._tester.register('foo', 'foo@trac.example.org') self._tester.login('foo') self._tester.logout() self._smtpd.full_reset() class NoEmailVerificationForAnonymousUsers(FunctionalTestCaseSetup): def runTest(self): """Anonymous users don't get their email address verified""" tc.find("Login") tc.follow("Preferences") form_name = 'userprefs' email_address = 'anonyous.user@fakedomain.tld' tc.formvalue(form_name, 'email', email_address) tc.submit() tc.notfind(r'Notice:\s*An email has been sent ' r'to {0} with a token to verify ' r'your new email address' .format(re.escape(email_address))) self._tester.go_to_front() tc.notfind(r'Warning:\s*Your permissions have ' r'been limited until you verify ' r'your email address') def test_suite(): suite = FunctionalTestSuite() suite.addTest(TestInitialSetup()) suite.addTest(TestFormLoginAdmin()) suite.addTest(TestAdminFormAddUser()) suite.addTest(TestFormLoginUser()) suite.addTest(TestRegisterNewUser()) suite.addTest(TestLoginNewUser()) suite.addTest(TestFailRegisterPasswdConfirmNotPassed()) suite.addTest(TestFailRegisterDuplicateUsername()) suite.addTest(TestNewAccountNotification()) suite.addTest(TestNewAccountEmailVerification()) suite.addTest(VerifyNewAccountEmailAddress()) suite.addTest(PasswdResetsNotifiesAdmin()) suite.addTest(PasswdResetsNotifiesUser()) suite.addTest(UserLoginWithMailedPassword()) suite.addTest(UserIsForcedToChangePassword()) suite.addTest(UserCantBrowseUntilPasswdChange()) suite.addTest(DeleteAccountNotifiesAdmin()) suite.addTest(UserNoLongerLogins()) suite.addTest(UserIsAbleToRegisterWithSameUserName()) suite.addTest(NoEmailVerificationForAnonymousUsers()) return suite if __name__ == '__main__': unittest.main(defaultTest='test_suite') trac-accountmanager-0.6~svn18547/acct_mgr/tests/register.py0000644000175500017550000004077114413553530023520 0ustar debacledebacle# -*- coding: utf-8 -*- # # Copyright (C) 2012 Steffen Hoffmann # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. # # Author: Steffen Hoffmann import shutil import string import tempfile import unittest from trac.perm import PermissionCache, PermissionSystem from trac.util.html import Markup from trac.test import EnvironmentStub, MockRequest from trac.web.api import RequestDone from trac.web.session import Session from ..api import AccountManager from ..compat import unicode from ..db import SessionStore from ..model import set_user_attribute from ..register import ( BasicCheck, BotTrapCheck, EmailCheck, EmailVerificationModule, RegExpCheck, RegistrationError, RegistrationModule, UsernamePermCheck) class _BaseTestCase(unittest.TestCase): def setUp(self, method='GET'): self.env = EnvironmentStub( enable=['trac.*', 'acct_mgr.api.*']) self.env.path = tempfile.mkdtemp() self.perm = PermissionSystem(self.env) # Create a user reference in the permission system. self.perm.grant_permission('admin', 'ACCTMGR_USER_ADMIN') # Prepare a generic registration request. args = dict(username='', name='', email='') self.req = MockRequest(self.env, method=method, path_info='/register', args=args) def tearDown(self): shutil.rmtree(self.env.path) class BasicCheckTestCase(_BaseTestCase): def setUp(self): _BaseTestCase.setUp(self) self.env = EnvironmentStub( enable=['trac.*', 'acct_mgr.admin.*', 'acct_mgr.db.sessionstore', 'acct_mgr.pwhash.HtDigestHashMethod']) self.env.path = tempfile.mkdtemp() self.env.config.set('account-manager', 'password_store', 'SessionStore') store = SessionStore(self.env) store.set_password('registered_user', 'password') def test_check(self): check = BasicCheck(self.env) req = self.req # Inspector doesn't provide additional fields. field_res = check.render_registration_fields(req, {}) self.assertEqual(len(field_res), 2) self.assertEqual((None, None), field_res) # 1st attempt: No input. self.assertRaises(RegistrationError, check.validate_registration, req) # 2nd attempt: Illegal character. req.args['username'] = 'user:name' self.assertRaises(RegistrationError, check.validate_registration, req) # 3rd attempt: All upper-case word. req.args['username'] = 'UPPER-CASE_USER' self.assertRaises(RegistrationError, check.validate_registration, req) # 4th attempt: Reserved word. req.args['username'] = 'Anonymous' self.assertRaises(RegistrationError, check.validate_registration, req) # 5th attempt: Existing user. req.args['username'] = 'registered_user' self.assertRaises(RegistrationError, check.validate_registration, req) # 6th attempt: Valid username, but no password. req.args['username'] = 'user' self.assertRaises(RegistrationError, check.validate_registration, req) # 7th attempt: Valid username, no matching passwords. req.args['password'] = 'password' self.assertRaises(RegistrationError, check.validate_registration, req) # 8th attempt: Finally some valid input. req.args['password_confirm'] = 'password' self.assertEqual(check.validate_registration(req), None) class BotTrapCheckTestCase(_BaseTestCase): def setUp(self): _BaseTestCase.setUp(self) self.basic_token = "Yes, I'm human" self.env.config.set('account-manager', 'register_basic_token', self.basic_token) def test_check(self): check = BotTrapCheck(self.env) req = self.req # Inspector provides the email text input field. wrong_input = "Hey, I'm a bot." data = dict(basic_token=wrong_input) req.args.update(data) field_res = check.render_registration_fields(req, data) self.assertEqual(len(field_res), 2) self.assertTrue(Markup(field_res[0]).startswith('