class AccountModule(Component): """Exposes methods for users to do account management on their own. Allows users to change their password, reset their password, if they've forgotten it, even delete their account. The settings for the AccountManager module must be set in trac.ini in order to use this. """ implements(IPreferencePanelProvider, IRequestHandler, ITemplateProvider, INavigationContributor, IRequestFilter) _password_chars = string.ascii_letters + string.digits password_length = IntOption( 'account-manager', 'generated_password_length', 8, """Length of the randomly-generated passwords created when resetting the password for an account.""") reset_password = BoolOption( 'account-manager', 'reset_password', True, 'Set to False, if there is no email system setup.') def __init__(self): self.acctmgr = AccountManager(self.env) self.store = ResetPwStore(self.env) self._write_check(log=True) def _write_check(self, log=False): writable = self.acctmgr.get_all_supporting_stores('set_password') if not writable and log: self.log.warn('AccountModule is disabled because the password ' 'store does not support writing.') return writable # IPreferencePanelProvider methods def get_preference_panels(self, req): writable = self._write_check() if not writable: return if req.authname and req.authname != 'anonymous': user_store = self.acctmgr.find_user_store(req.authname) if user_store in writable: yield 'account', _("Account") def render_preference_panel(self, req, panel): data = { 'account': self._do_account(req), '_dgettext': dgettext, } return 'prefs_account.html', data # IRequestHandler methods def match_request(self, req): return req.path_info == '/reset_password' and \ self._reset_password_enabled(log=True) def process_request(self, req): data = {'_dgettext': dgettext, 'reset': self._do_reset_password(req)} return 'reset_password.html', data, None # IRequestFilter methods def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, content_type): if req.authname and req.authname != 'anonymous': if req.session.get('force_change_passwd', False): redirect_url = req.href.prefs('account') if req.href(req.path_info) != redirect_url: req.redirect(redirect_url) return (template, data, content_type) # INavigationContributor methods def get_active_navigation_item(self, req): return 'reset_password' def get_navigation_items(self, req): if not self.reset_password_enabled or LoginModule(self.env).enabled: return if req.authname == 'anonymous': yield 'metanav', 'reset_password', tag.a( _("Forgot your password?"), href=req.href.reset_password()) def _reset_password_enabled(self, log=False): return is_enabled(self.env, self.__class__) and \ self.reset_password and (self._write_check(log) != []) reset_password_enabled = property(_reset_password_enabled) def _do_account(self, req): if not req.authname or req.authname == 'anonymous': # DEVEL: Shouldn't this be a more generic URL? req.redirect(req.href.wiki()) action = req.args.get('action') delete_enabled = self.acctmgr.supports('delete_user') and \ self.acctmgr.allow_delete_account data = { 'delete_enabled': delete_enabled, 'delete_msg_confirm': _("Are you sure you want to delete your account?"), } force_change_password = req.session.get('force_change_passwd', False) if req.method == 'POST': if action == 'save': data.update(self._do_change_password(req)) if force_change_password: del (req.session['force_change_passwd']) req.session.save() chrome.add_notice( req, Markup( tag.span( tag_( "Thank you for taking the time to update your password." )))) force_change_password = False elif action == 'delete' and delete_enabled: data.update(self._do_delete(req)) else: data.update({'error': 'Invalid action'}) if force_change_password: chrome.add_warning( req, Markup( tag.span( _("You are required to change password because of a recent " "password change request. "), tag.b(_("Please change your password now."))))) return data def _do_reset_password(self, req): if req.authname and req.authname != 'anonymous': return {'logged_in': True} if req.method != 'POST': return {} username = req.args.get('username') email = req.args.get('email') if not username: return {'error': _("Username is required")} if not email: return {'error': _("Email is required")} for username_, name, email_ in self.env.get_known_users(): if username_ == username and email_ == email: self._reset_password(username, email) break else: return { 'error': _("The email and username must match a known account.") } return {'sent_to_email': email} def _reset_password(self, username, email): acctmgr = self.acctmgr new_password = self._random_password() try: acctmgr._notify('password_reset', username, email, new_password) except Exception, e: return {'error': ','.join(map(to_unicode, e.args))} self.store.set_password(username, new_password) if acctmgr.force_passwd_change: set_user_attribute(self.env, username, 'force_change_passwd', 1)
class RegistrationModule(Component): """Provides users the ability to register a new account. Requires configuration of the AccountManager module in trac.ini. """ implements(INavigationContributor, IRequestHandler, ITemplateProvider) def __init__(self): self.acctmgr = AccountManager(self.env) self._enable_check(log=True) def _enable_check(self, log=False): env = self.env writable = self.acctmgr.supports('set_password') ignore_case = auth.LoginModule(env).ignore_case if log: if not writable: self.log.warn('RegistrationModule is disabled because the ' 'password store does not support writing.') if ignore_case: self.log.debug('RegistrationModule will allow lowercase ' 'usernames only and convert them forcefully ' 'as required, while \'ignore_auth_case\' is ' 'enabled in [trac] section of your trac.ini.') return is_enabled(env, self.__class__) and writable enabled = property(_enable_check) # INavigationContributor methods def get_active_navigation_item(self, req): return 'register' def get_navigation_items(self, req): loginmod = LoginModule(self.env) if not self.enabled: return if req.authname == 'anonymous': yield 'metanav', 'register', tag.a(_("Register"), href=req.href.register()) # IRequestHandler methods def match_request(self, req): return req.path_info == '/register' and self._enable_check(log=True) def process_request(self, req): if req.authname != 'anonymous': req.redirect(req.href.prefs('account')) action = req.args.get('action') data = { 'acctmgr': { 'username': None, 'name': None, 'email': None, }, '_dgettext': dgettext, } data['verify_account_enabled'] = is_enabled( self.env, EmailVerificationModule) and self.acctmgr.verify_email if req.method == 'POST' and action == 'create': try: _create_user(req, self.env) except TracError, e: data['registration_error'] = e.message data['acctmgr'] = getattr(e, 'acctmgr', '') else: chrome.add_notice( req, Markup( tag.span( Markup( _("""Registration has been finished successfully. You may login as user %(user)s now.""", user=tag.b(req.args.get('username'))))))) req.redirect(req.href.login()) data['reset_password_enabled'] = AccountModule( self.env).reset_password_enabled return 'register.html', data, None
class AccountManagerAdminPage(Component): implements(IAdminPanelProvider, ITemplateProvider) def __init__(self): self.account_manager = AccountManager(self.env) # IAdminPageProvider def get_admin_panels(self, req): if req.perm.has_permission("TRAC_ADMIN"): yield ("accounts", "Accounts", "config", "Configuration") yield ("accounts", "Accounts", "users", "Users") def render_admin_panel(self, req, cat, page, path_info): if page == "config": return self._do_config(req) elif page == "users": return self._do_users(req) def _do_config(self, req): stores = StoreOrder(stores=self.account_manager.stores, list=self.account_manager.password_store) if req.method == "POST": _setorder(req, stores) self.config.set("account-manager", "password_store", ",".join(stores.get_enabled_store_names())) for store in stores.get_all_stores(): for attr, option in _getoptions(store): newvalue = req.args.get("%s.%s" % (store.__class__.__name__, attr)) self.log.debug("%s.%s: %s" % (store.__class__.__name__, attr, newvalue)) if newvalue is not None: self.config.set(option.section, option.name, newvalue) self.config.save() self.config.set("account-manager", "force_passwd_change", req.args.get("force_passwd_change")) self.config.set("account-manager", "persistent_sessions", req.args.get("persistent_sessions")) self.config.save() sections = [] for store in self.account_manager.stores: options = [] for attr, option in _getoptions(store): opt_val = option.__get__(store, store) opt_val = isinstance(opt_val, Component) and opt_val.__class__.__name__ or opt_val options.append({"label": attr, "name": "%s.%s" % (store.__class__.__name__, attr), "value": opt_val}) continue sections.append( { "name": store.__class__.__name__, "classname": store.__class__.__name__, "order": stores[store], "options": options, } ) continue sections = sorted(sections, key=lambda i: i["name"]) numstores = range(0, stores.numstores() + 1) data = { "sections": sections, "numstores": numstores, "force_passwd_change": self.account_manager.force_passwd_change, "persistent_sessions": self.account_manager.persistent_sessions, } return "admin_accountsconfig.html", data def _do_users(self, req): perm = PermissionSystem(self.env) listing_enabled = self.account_manager.supports("get_users") create_enabled = self.account_manager.supports("set_password") password_change_enabled = self.account_manager.supports("set_password") delete_enabled = self.account_manager.supports("delete_user") data = { "listing_enabled": listing_enabled, "create_enabled": create_enabled, "delete_enabled": delete_enabled, "password_change_enabled": password_change_enabled, "acctmgr": {"username": None, "name": None, "email": None}, } if req.method == "POST": if req.args.get("add"): if create_enabled: try: _create_user(req, self.env, check_permissions=False) except TracError, e: data["registration_error"] = e.message data["acctmgr"] = e.acctmgr else: data["registration_error"] = "The password store does " "not support creating users" elif req.args.get("remove"): if delete_enabled: sel = req.args.get("sel") sel = isinstance(sel, list) and sel or [sel] for account in sel: self.account_manager.delete_user(account) else: data["deletion_error"] = "The password store does not " "support deleting users" elif req.args.get("change"): if password_change_enabled: try: user = req.args.get("change_user") acctmgr = {"change_username": user} error = TracError("") error.acctmgr = acctmgr if not user: error.message = "Username cannot be empty." raise error password = req.args.get("change_password") if not password: error.message = "Password cannot be empty." raise error if password != req.args.get("change_password_confirm"): error.message = "The passwords must match." raise error self.account_manager.set_password(user, password) except TracError, e: data["password_change_error"] = e.message data["acctmgr"] = getattr(e, "acctmgr", "") else: data["password_change_error"] = "The password store does not " "support changing passwords"
class AccountManagerAdminPage(Component): implements(IAdminPanelProvider, ITemplateProvider) def __init__(self): self.account_manager = AccountManager(self.env) # IAdminPageProvider def get_admin_panels(self, req): if req.perm.has_permission('TRAC_ADMIN'): yield ('accounts', 'Accounts', 'config', 'Configuration') yield ('accounts', 'Accounts', 'users', 'Users') def render_admin_panel(self, req, cat, page, path_info): if page == 'config': return self._do_config(req) elif page == 'users': return self._do_users(req) def _do_config(self, req): stores = StoreOrder(stores=self.account_manager.stores, list=self.account_manager.password_store) if req.method == 'POST': _setorder(req, stores) self.config.set('account-manager', 'password_store', ','.join(stores.get_enabled_store_names())) for store in stores.get_all_stores(): for attr, option in _getoptions(store): newvalue = req.args.get('%s.%s' % (store.__class__.__name__, attr)) self.log.debug("%s.%s: %s" % (store.__class__.__name__, attr, newvalue)) if newvalue is not None: self.config.set(option.section, option.name, newvalue) self.config.save() self.config.set('account-manager', 'force_passwd_change', req.args.get('force_passwd_change')) self.config.save() sections = [] for store in self.account_manager.stores: options = [] for attr, option in _getoptions(store): opt_val = option.__get__(store, store) opt_val = isinstance(opt_val, Component) and \ opt_val.__class__.__name__ or opt_val options.append( {'label': attr, 'name': '%s.%s' % (store.__class__.__name__, attr), 'value': opt_val, }) continue sections.append( {'name': store.__class__.__name__, 'classname': store.__class__.__name__, 'order': stores[store], 'options' : options, }) continue sections = sorted(sections, key=lambda i: i['name']) numstores = range(0, stores.numstores() + 1) data = {'sections': sections, 'numstores': numstores, 'force_passwd_change': self.account_manager.force_passwd_change} return 'admin_accountsconfig.html', data def _do_users(self, req): perm = PermissionSystem(self.env) listing_enabled = self.account_manager.supports('get_users') create_enabled = self.account_manager.supports('set_password') password_change_enabled = self.account_manager.supports('set_password') delete_enabled = self.account_manager.supports('delete_user') data = { 'listing_enabled': listing_enabled, 'create_enabled': create_enabled, 'delete_enabled': delete_enabled, 'password_change_enabled': password_change_enabled, 'acctmgr' : { 'username' : None, 'name' : None, 'email' : None, } } if req.method == 'POST': if req.args.get('add'): if create_enabled: try: _create_user(req, self.env, check_permissions=False) except TracError, e: data['registration_error'] = e.message data['acctmgr'] = e.acctmgr else: data['registration_error'] = 'The password store does ' \ 'not support creating users' elif req.args.get('remove'): if delete_enabled: sel = req.args.get('sel') sel = isinstance(sel, list) and sel or [sel] for account in sel: self.account_manager.delete_user(account) else: data['deletion_error'] = 'The password store does not ' \ 'support deleting users' elif req.args.get('change'): if password_change_enabled: try: user = req.args.get('change_user') acctmgr = { 'change_username' : user, } error = TracError('') error.acctmgr = acctmgr if not user: error.message = 'Username cannot be empty.' raise error password = req.args.get('change_password') if not password: error.message = 'Password cannot be empty.' raise error if password != req.args.get('change_password_confirm'): error.message = 'The passwords must match.' raise error self.account_manager.set_password(user, password) except TracError, e: data['password_change_error'] = e.message data['acctmgr'] = getattr(e, 'acctmgr', '') else: data['password_change_error'] = 'The password store does not ' \ 'support changing passwords'
class RegistrationModule(CommonTemplateProvider): """Provides users the ability to register a new account. Requires configuration of the AccountManager module in trac.ini. """ implements(chrome.INavigationContributor, IRequestHandler) require_approval = BoolOption( 'account-manager', 'require_approval', False, doc=""" Whether account registration requires administrative approval to enable the account or not. """) def __init__(self): self.acctmgr = AccountManager(self.env) self._enable_check(log=True) def _enable_check(self, log=False): env = self.env writable = self.acctmgr.supports('set_password') ignore_case = auth.LoginModule(env).ignore_case if log: if not writable: self.log.warning("RegistrationModule is disabled because the " "password store does not support writing.") if ignore_case: self.log.debug("RegistrationModule will allow lowercase " "usernames only and convert them forcefully " "as required, while 'ignore_auth_case' is " "enabled in [trac] section of your trac.ini.") return env.is_enabled(self.__class__) and writable enabled = property(_enable_check) # INavigationContributor methods def get_active_navigation_item(self, req): return 'register' def get_navigation_items(self, req): if not self.enabled: return if req.authname == 'anonymous': yield 'metanav', 'register', tag.a(_("Register"), href=req.href.register()) # IRequestHandler methods def match_request(self, req): return req.path_info == '/register' and self._enable_check(log=True) def process_request(self, req): acctmgr = self.acctmgr if req.authname != 'anonymous': req.redirect(req.href.prefs('account')) action = req.args.get('action') name = req.args.get('name', '') if isinstance(name, list): raise HTTPBadRequest(_("Invalid request arguments.")) name = name.strip() username = req.args.get('username', '') if isinstance(username, list): raise HTTPBadRequest(_("Invalid request arguments.")) username = acctmgr.handle_username_casing(username.strip()) data = { '_dgettext': dgettext, 'acctmgr': {'name': name, 'username': username}, 'ignore_auth_case': self.config.getbool('trac', 'ignore_auth_case') } verify_enabled = self.env.is_enabled(EmailVerificationModule) and \ EmailVerificationModule(self.env).verify_email data['verify_account_enabled'] = verify_enabled if req.method == 'POST' and action == 'create': try: try: # Check request and prime account on success. acctmgr.validate_account(req, True) except NotificationError, e: chrome.add_warning(req, _( "Error raised while sending a change notification." ) + _("You should report that issue to a Trac admin.")) self.log.error( 'Unable to send registration notification: %s', exception_to_unicode(e, traceback=True)) except RegistrationError, e: chrome.add_warning(req, e) else: if self.require_approval: set_user_attribute(self.env, username, 'approval', N_('pending')) # Notify admin user about registration pending for review. try: acctmgr._notify('registration_approval_required', username) except NotificationError, e: chrome.add_warning(req, _( "Error raised while sending a change " "notification.") + _( "You should report that issue to a Trac admin.")) self.log.error( 'Unable to send admin notification: %s', exception_to_unicode(e, traceback=True)) else: chrome.add_notice(req, tag_( "Your username has been registered successfully, " "but your account requires administrative " "approval. Please proceed according to local " "policy.")) if verify_enabled: chrome.add_notice(req, tag_( "Your username has been successfully registered but " "your account still requires activation. Please " "login as user %(user)s, and follow the " "instructions.", user=tag.b(username))) req.redirect(req.href.login()) chrome.add_notice(req, tag_( "Registration has been finished successfully. " "You may log in as user %(user)s now.", user=tag.b(username))) req.redirect(req.href.login())
class AccountModule(CommonTemplateProvider): """Exposes methods for users to do account management on their own. Allows users to change their password, reset their password, if they've forgotten it, even delete their account. The settings for the AccountManager module must be set in trac.ini in order to use this. Password reset procedure depends on both, ResetPwStore and an IPasswordHashMethod implementation being enabled as well. """ implements(IPreferencePanelProvider, IRequestHandler, INavigationContributor, IRequestFilter) _password_chars = string.ascii_letters + string.digits password_length = IntOption( 'account-manager', 'generated_password_length', 8, """Length of the randomly-generated passwords created when resetting the password for an account.""") reset_password = BoolOption( 'account-manager', 'reset_password', True, 'Set to False, if there is no email system setup.') def __init__(self): self.acctmgr = AccountManager(self.env) self.store = ResetPwStore(self.env) self._write_check(log=True) def _write_check(self, log=False): """Returns all configured write-enabled password stores.""" writable = self.acctmgr.get_all_supporting_stores('set_password') if writable: try: writable = writable.remove(self.store) except ValueError: # ResetPwStore is not enabled. if log: self.log.warn("ResetPwStore is disabled, therefor " "password reset won't work.") # Require at least one more write-enabled password store. if not writable and log: self.log.warn("AccountModule is disabled because no configured " "password store supports writing.") return writable # INavigationContributor methods def get_active_navigation_item(self, req): return 'reset_password' def get_navigation_items(self, req): if not self.reset_password_enabled or LoginModule(self.env).enabled: return if req.authname == 'anonymous': yield 'metanav', 'reset_password', tag.a( _("Forgot your password?"), href=req.href.reset_password()) def _reset_password_enabled(self, log=False): try: self.store.hash_method except AttributeError: return False return is_enabled(self.env, self.__class__) and \ self.reset_password and (self._write_check(log) != []) and \ is_enabled(self.env, self.store.__class__) and \ self.store.hash_method and True or False reset_password_enabled = property(_reset_password_enabled) # IPreferencePanelProvider methods def get_preference_panels(self, req): writable = self._write_check() if not writable: return if req.authname and req.authname != 'anonymous': user_store = self.acctmgr.find_user_store(req.authname) if user_store in writable: yield 'account', _("Account") def render_preference_panel(self, req, panel): data = dict(_dgettext=dgettext) data.update(self._do_account(req)) return 'prefs_account.html', data # IRequestFilter methods def pre_process_request(self, req, handler): if req.path_info == '/prefs/account' and \ not (req.authname and req.authname != 'anonymous'): # An anonymous session has no account associated with it, and # no account properies too, but general session preferences should # always be available. req.redirect(req.href.prefs()) return handler def post_process_request(self, req, template, data, content_type): if req.authname and req.authname != 'anonymous': if req.session.get('force_change_passwd', False): # Prevent authenticated usage before another password change. redirect_url = req.href.prefs('account') if req.href(req.path_info) != redirect_url: req.redirect(redirect_url) return (template, data, content_type) # IRequestHandler methods def match_request(self, req): return req.path_info == '/reset_password' and \ self._reset_password_enabled(log=True) def process_request(self, req): data = dict(_dgettext=dgettext) if req.authname and req.authname != 'anonymous': add_notice( req, Markup( tag_( "You're already logged in. If you need to change your " "password please use the %(prefs_href)s page.", prefs_href=tag.a(_("Account Preferences"), href=req.href.prefs('account'))))) data['authenticated'] = True if req.method == 'POST': self._do_reset_password(req) return 'reset_password.html', data, None def _do_account(self, req): assert (req.authname and req.authname != 'anonymous') action = req.args.get('action') delete_enabled = self.acctmgr.supports('delete_user') and \ self.acctmgr.allow_delete_account data = { 'delete_enabled': delete_enabled, 'delete_msg_confirm': _("Are you sure you want to delete your account?"), } force_change_password = req.session.get('force_change_passwd', False) if req.method == 'POST': if action == 'save': if self._do_change_password(req) and force_change_password: del req.session['force_change_passwd'] req.session.save() add_notice( req, _("Thank you for taking the time to " "update your password.")) force_change_password = False elif action == 'delete' and delete_enabled: self._do_delete(req) if force_change_password: add_warning( req, Markup( _( "You are required to change password because of a recent " "password change request. %(invitation)s", invitation=tag.b( _("Please change your password now."))))) return data def _do_change_password(self, req): username = req.authname old_password = req.args.get('old_password') if not self.acctmgr.check_password(username, old_password): if old_password: add_warning(req, _("Old password is incorrect.")) else: add_warning(req, _("Old password cannot be empty.")) return password = req.args.get('password') if not password: add_warning(req, _("Password cannot be empty.")) elif password != req.args.get('password_confirm'): add_warning(req, _("The passwords must match.")) elif password == old_password: add_warning(req, _("Password must not match old password.")) else: self.acctmgr.set_password(username, password, old_password) if req.session.get('password') is not None: # Fetch all session_attributes in case new user password is in # SessionStore, preventing overwrite by session.save(). req.session.get_session(req.authname, authenticated=True) add_notice(req, _("Password updated successfully.")) return True def _do_delete(self, req): username = req.authname password = req.args.get('password') if not password: add_warning(req, _("Password cannot be empty.")) elif not self.acctmgr.check_password(username, password): add_warning(req, _("Password is incorrect.")) else: self.acctmgr.delete_user(username) # Delete the whole session, since records in session_attribute # would get restored on logout otherwise. req.session.clear() req.session.save() req.redirect(req.href.logout()) def _do_reset_password(self, req): email = req.args.get('email') username = req.args.get('username') if not username: add_warning(req, _("Username is required.")) elif not email: add_warning(req, _("Email is required.")) else: for username_, name, email_ in self.env.get_known_users(): if username_ == username and email_ == email: self._reset_password(req, username, email) return add_warning(req, _("Email and username must match a known account.")) @property def _random_password(self): return ''.join([ random.choice(self._password_chars) for _ in xrange(self.password_length) ]) def _reset_password(self, req, username, email): acctmgr = self.acctmgr new_password = self._random_password try: self.store.set_password(username, new_password) acctmgr._notify('password_reset', username, email, new_password) # No message, if method has been called from user admin panel. if not req.path_info.startswith('/admin'): add_notice( req, _("A new password has been sent to you at " "<%(email)s>.", email=email)) except Exception, e: add_warning( req, _("Cannot reset password: %(error)s", error=', '.join(map(to_unicode, e.args)))) return if acctmgr.force_passwd_change: set_user_attribute(self.env, username, 'force_change_passwd', 1)
class AccountModule(CommonTemplateProvider): """Exposes methods for users to do account management on their own. Allows users to change their password, reset their password, if they've forgotten it, even delete their account. The settings for the AccountManager module must be set in trac.ini in order to use this. Password reset procedure depends on both, ResetPwStore and an IPasswordHashMethod implementation being enabled as well. """ implements(IPreferencePanelProvider, IRequestHandler, INavigationContributor, IRequestFilter) _password_chars = string.ascii_letters + string.digits password_length = IntOption( 'account-manager', 'generated_password_length', 8, """Length of the randomly-generated passwords created when resetting the password for an account.""") reset_password = BoolOption( 'account-manager', 'reset_password', True, 'Set to False, if there is no email system setup.') def __init__(self): self.acctmgr = AccountManager(self.env) self.store = ResetPwStore(self.env) self._write_check(log=True) def _write_check(self, log=False): writable = self.acctmgr.get_all_supporting_stores('set_password') if not writable and log: self.log.warn("AccountModule is disabled because the password " "store does not support writing.") return writable # IPreferencePanelProvider methods def get_preference_panels(self, req): writable = self._write_check() if not writable: return if req.authname and req.authname != 'anonymous': user_store = self.acctmgr.find_user_store(req.authname) if user_store in writable: yield 'account', _("Account") def render_preference_panel(self, req, panel): data = {'account': self._do_account(req), '_dgettext': dgettext, } return 'prefs_account.html', data # IRequestHandler methods def match_request(self, req): return req.path_info == '/reset_password' and \ self._reset_password_enabled(log=True) def process_request(self, req): data = {'_dgettext': dgettext, 'reset': self._do_reset_password(req) } return 'reset_password.html', data, None # IRequestFilter methods def pre_process_request(self, req, handler): if req.path_info == '/prefs/account' and \ not (req.authname and req.authname != 'anonymous'): # An anonymous session has no account associated with it, and # no account properies too, but general session preferences should # always be available. req.redirect(req.href.prefs()) return handler def post_process_request(self, req, template, data, content_type): if req.authname and req.authname != 'anonymous': if req.session.get('force_change_passwd', False): redirect_url = req.href.prefs('account') if req.href(req.path_info) != redirect_url: req.redirect(redirect_url) return (template, data, content_type) # INavigationContributor methods def get_active_navigation_item(self, req): return 'reset_password' def get_navigation_items(self, req): if not self.reset_password_enabled or LoginModule(self.env).enabled: return if req.authname == 'anonymous': yield 'metanav', 'reset_password', tag.a( _("Forgot your password?"), href=req.href.reset_password()) def _reset_password_enabled(self, log=False): return is_enabled(self.env, self.__class__) and \ self.reset_password and (self._write_check(log) != []) and \ is_enabled(self.env, self.store.__class__) and \ self.store.hash_method reset_password_enabled = property(_reset_password_enabled) def _do_account(self, req): assert(req.authname and req.authname != 'anonymous') action = req.args.get('action') delete_enabled = self.acctmgr.supports('delete_user') and \ self.acctmgr.allow_delete_account data = {'delete_enabled': delete_enabled, 'delete_msg_confirm': _( "Are you sure you want to delete your account?"), } force_change_password = req.session.get('force_change_passwd', False) if req.method == 'POST': if action == 'save': data.update(self._do_change_password(req)) if force_change_password: del(req.session['force_change_passwd']) req.session.save() chrome.add_notice(req, Markup(tag.span(tag_( "Thank you for taking the time to update your password." )))) force_change_password = False elif action == 'delete' and delete_enabled: data.update(self._do_delete(req)) else: data.update({'error': 'Invalid action'}) if force_change_password: chrome.add_warning(req, Markup(tag.span(_( "You are required to change password because of a recent " "password change request. "), tag.b(_("Please change your password now."))))) return data def _do_reset_password(self, req): if req.authname and req.authname != 'anonymous': return {'logged_in': True} if req.method != 'POST': return {} username = req.args.get('username') email = req.args.get('email') if not username: return {'error': _("Username is required")} if not email: return {'error': _("Email is required")} for username_, name, email_ in self.env.get_known_users(): if username_ == username and email_ == email: error = self._reset_password(username, email) return error and error or {'sent_to_email': email} return {'error': _( "The email and username must match a known account.")} def _reset_password(self, username, email): acctmgr = self.acctmgr new_password = self._random_password() try: self.store.set_password(username, new_password) acctmgr._notify('password_reset', username, email, new_password) except Exception, e: return {'error': ','.join(map(to_unicode, e.args))} if acctmgr.force_passwd_change: set_user_attribute(self.env, username, 'force_change_passwd', 1)
class RegistrationModule(CommonTemplateProvider): """Provides users the ability to register a new account. Requires configuration of the AccountManager module in trac.ini. """ implements(chrome.INavigationContributor, IRequestHandler) require_approval = BoolOption( 'account-manager', 'require_approval', False, doc="Whether account registration requires administrative approval " "to enable the account or not.") def __init__(self): self.acctmgr = AccountManager(self.env) self._enable_check(log=True) def _enable_check(self, log=False): env = self.env writable = self.acctmgr.supports('set_password') ignore_case = auth.LoginModule(env).ignore_case if log: if not writable: self.log.warn('RegistrationModule is disabled because the ' 'password store does not support writing.') if ignore_case: self.log.debug('RegistrationModule will allow lowercase ' 'usernames only and convert them forcefully ' 'as required, while \'ignore_auth_case\' is ' 'enabled in [trac] section of your trac.ini.') return is_enabled(env, self.__class__) and writable enabled = property(_enable_check) # INavigationContributor methods def get_active_navigation_item(self, req): return 'register' def get_navigation_items(self, req): if not self.enabled: return if req.authname == 'anonymous': yield 'metanav', 'register', tag.a(_("Register"), href=req.href.register()) # IRequestHandler methods def match_request(self, req): return req.path_info == '/register' and self._enable_check(log=True) def process_request(self, req): acctmgr = self.acctmgr if req.authname != 'anonymous': req.redirect(req.href.prefs('account')) action = req.args.get('action') name = req.args.get('name', '').strip() username = acctmgr.handle_username_casing(req.args.get('username', '').strip()) data = { '_dgettext': dgettext, 'acctmgr': dict(name=name, username=username), 'ignore_auth_case': self.config.getbool('trac', 'ignore_auth_case') } verify_enabled = is_enabled(self.env, EmailVerificationModule) and \ EmailVerificationModule(self.env).verify_email data['verify_account_enabled'] = verify_enabled if req.method == 'POST' and action == 'create': try: # Check request and prime account on success. acctmgr.validate_account(req, True) except RegistrationError, e: # Attempt deferred translation. message = gettext(e.message) # Check for (matching number of) message arguments before # attempting string substitution. if e.msg_args and \ len(e.msg_args) == len(re.findall('%s', message)): message = message % e.msg_args chrome.add_warning(req, Markup(message)) else: if self.require_approval: set_user_attribute(self.env, username, 'approval', N_('pending')) # Notify admin user about registration pending for review. acctmgr._notify('registration_approval_required', username) chrome.add_notice(req, Markup(tag.span(Markup(_( "Your username has been registered successfully, but " "your account requires administrative approval. " "Please proceed according to local policy.")))) ) if verify_enabled: chrome.add_notice(req, Markup(tag.span(Markup(_( """Your username has been successfully registered but your account still requires activation. Please login as user %(user)s, and follow the instructions.""", user=tag.b(username))))) ) req.redirect(req.href.login()) chrome.add_notice(req, Markup(tag.span(Markup(_( """Registration has been finished successfully. You may log in as user %(user)s now.""", user=tag.b(username))))) ) req.redirect(req.href.login()) # Collect additional fields from IAccountRegistrationInspector's. fragments = dict(required=[], optional=[]) for inspector in acctmgr.register_checks: try: fragment, f_data = inspector.render_registration_fields(req, data) except TypeError, e: # Add some robustness by logging the most likely errors. self.env.log.warn("%s.render_registration_fields failed: %s" % (inspector.__class__.__name__, e)) fragment = None if fragment: try: # Python<2.5: Can't have 'except' and 'finally' in same # 'try' statement together. try: if 'optional' in fragment.keys(): fragments['optional'].append(fragment['optional']) except AttributeError: # No dict, just append Genshi Fragment or str/unicode. fragments['required'].append(fragment) else: fragments['required'].append(fragment.get('required', '')) finally: data.update(f_data)
class RegistrationModule(CommonTemplateProvider): """Provides users the ability to register a new account. Requires configuration of the AccountManager module in trac.ini. """ implements(chrome.INavigationContributor, IRequestHandler) def __init__(self): self.acctmgr = AccountManager(self.env) self._enable_check(log=True) def _enable_check(self, log=False): env = self.env writable = self.acctmgr.supports('set_password') ignore_case = auth.LoginModule(env).ignore_case if log: if not writable: self.log.warn('RegistrationModule is disabled because the ' 'password store does not support writing.') if ignore_case: self.log.debug('RegistrationModule will allow lowercase ' 'usernames only and convert them forcefully ' 'as required, while \'ignore_auth_case\' is ' 'enabled in [trac] section of your trac.ini.') return is_enabled(env, self.__class__) and writable enabled = property(_enable_check) # INavigationContributor methods def get_active_navigation_item(self, req): return 'register' def get_navigation_items(self, req): if not self.enabled: return if req.authname == 'anonymous': yield 'metanav', 'register', tag.a(_("Register"), href=req.href.register()) # IRequestHandler methods def match_request(self, req): return req.path_info == '/register' and self._enable_check(log=True) def process_request(self, req): acctmgr = self.acctmgr if req.authname != 'anonymous': req.redirect(req.href.prefs('account')) action = req.args.get('action') name = req.args.get('name', '').strip() username = acctmgr.handle_username_casing( req.args.get('username', '').strip()) data = { '_dgettext': dgettext, 'acctmgr': dict(name=name, username=username), 'ignore_auth_case': self.config.getbool('trac', 'ignore_auth_case') } verify_enabled = is_enabled(self.env, EmailVerificationModule) and \ acctmgr.verify_email data['verify_account_enabled'] = verify_enabled if req.method == 'POST' and action == 'create': try: # Check request and prime account on success. acctmgr.validate_registration(req) except RegistrationError, e: # Attempt deferred translation. message = gettext(e.message) # Check for (matching number of) message arguments before # attempting string substitution. if e.msg_args and \ len(e.msg_args) == len(re.findall('%s', message)): message = message % e.msg_args chrome.add_warning(req, Markup(message)) else: if verify_enabled: chrome.add_notice( req, Markup( tag.span( Markup( _("""Your username has been successfully registered but your account still requires activation. Please login as user %(user)s, and follow the instructions.""", user=tag.b(username)))))) req.redirect(req.href.login()) chrome.add_notice( req, Markup( tag.span( Markup( _("""Registration has been finished successfully. You may log in as user %(user)s now.""", user=tag.b(username)))))) req.redirect(req.href.login()) # Collect additional fields from IAccountRegistrationInspector's. fragments = dict(required=[], optional=[]) for inspector in acctmgr._register_check: try: fragment, f_data = inspector.render_registration_fields( req, data) except TypeError, e: # Add some robustness by logging the most likely errors. self.env.log.warn("%s.render_registration_fields failed: %s" % (inspector.__class__.__name__, e)) fragment = None if fragment: try: # Python<2.5: Can't have 'except' and 'finally' in same # 'try' statement together. try: if 'optional' in fragment.keys(): fragments['optional'].append(fragment['optional']) except AttributeError: # No dict, just append Genshi Fragment or str/unicode. fragments['required'].append(fragment) else: fragments['required'].append( fragment.get('required', '')) finally: data.update(f_data)
class AccountManagerAdminPage(Component): implements(IAdminPageProvider) def __init__(self): self.account_manager = AccountManager(self.env) # IAdminPageProvider def get_admin_pages(self, req): if req.perm.has_permission('TRAC_ADMIN'): yield ('accounts', 'Accounts', 'config', 'Configuration') yield ('accounts', 'Accounts', 'users', 'Users') def process_admin_request(self, req, cat, page, path_info): if page == 'config': return self._do_config(req) elif page == 'users': return self._do_users(req) def _do_config(self, req): if req.method == 'POST': selected_class = req.args.get('selected') self.config.set('account-manager', 'password_store', selected_class) selected = self.account_manager.password_store for attr, option in _getoptions(selected): newvalue = req.args.get('%s.%s' % (selected_class, attr)) if newvalue is not None: self.config.set(option.section, option.name, newvalue) self.config.save() try: selected = self.account_manager.password_store except AttributeError: selected = None sections = [{ 'name': store.__class__.__name__, 'classname': store.__class__.__name__, 'selected': store is selected, 'options': [{ 'label': attr, 'name': '%s.%s' % (store.__class__.__name__, attr), 'value': option.__get__(store, store), } for attr, option in _getoptions(store)], } for store in self.account_manager.stores] sections = sorted(sections, key=lambda i: i['name']) req.hdf['sections'] = sections return 'admin_accountsconfig.cs', None def _do_users(self, req): perm = PermissionSystem(self.env) listing_enabled = self.account_manager.supports('get_users') create_enabled = self.account_manager.supports('set_password') delete_enabled = self.account_manager.supports('delete_user') req.hdf['listing_enabled'] = listing_enabled req.hdf['create_enabled'] = create_enabled req.hdf['delete_enabled'] = delete_enabled if req.method == 'POST': if req.args.get('add'): if create_enabled: try: _create_user(req, self.env, check_permissions=False) except TracError, e: req.hdf['registration.error'] = e.message else: req.hdf['registration_error'] = 'The password store does ' \ 'not support creating users' elif req.args.get('remove'): if delete_enabled: sel = req.args.get('sel') sel = isinstance(sel, list) and sel or [sel] for account in sel: self.account_manager.delete_user(account) else: req.hdf['deletion_error'] = 'The password store does not ' \ 'support deleting users' if listing_enabled: accounts = {} for username in self.account_manager.get_users(): accounts[username] = {'username': username} for username, name, email in self.env.get_known_users(): account = accounts.get(username) if account: account['name'] = name account['email'] = email db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute( "SELECT sid,last_visit FROM session WHERE authenticated=1") for username, last_visit in cursor: account = accounts.get(username) if account and last_visit: account['last_visit'] = format_datetime(last_visit) req.hdf['accounts'] = sorted(accounts.itervalues(), key=lambda acct: acct['username']) return 'admin_users.cs', None
class AccountModule(CommonTemplateProvider): """Exposes methods for users to do account management on their own. Allows users to change their password, reset their password, if they've forgotten it, even delete their account. The settings for the AccountManager module must be set in trac.ini in order to use this. Password reset procedure depends on both, ResetPwStore and an IPasswordHashMethod implementation being enabled as well. """ implements(IPreferencePanelProvider, IRequestHandler, INavigationContributor, IRequestFilter) _password_chars = string.ascii_letters + string.digits password_length = IntOption( 'account-manager', 'generated_password_length', 8, """Length of the randomly-generated passwords created when resetting the password for an account.""") reset_password = BoolOption( 'account-manager', 'reset_password', True, 'Set to False, if there is no email system setup.') def __init__(self): self.acctmgr = AccountManager(self.env) self.store = ResetPwStore(self.env) self._write_check(log=True) def _write_check(self, log=False): """Returns all configured write-enabled password stores.""" writable = self.acctmgr.get_all_supporting_stores('set_password') if writable: try: writable = writable.remove(self.store) except ValueError: # ResetPwStore is not enabled. if log: self.log.warn("ResetPwStore is disabled, therefor " "password reset won't work.") # Require at least one more write-enabled password store. if not writable and log: self.log.warn("AccountModule is disabled because no configured " "password store supports writing.") return writable # INavigationContributor methods def get_active_navigation_item(self, req): return 'reset_password' def get_navigation_items(self, req): if not self.reset_password_enabled or LoginModule(self.env).enabled: return if req.authname == 'anonymous': yield 'metanav', 'reset_password', tag.a( _("Forgot your password?"), href=req.href.reset_password()) def _reset_password_enabled(self, log=False): try: self.store.hash_method except (AttributeError, ConfigurationError): return False return is_enabled(self.env, self.__class__) and \ self.reset_password and (self._write_check(log) != []) and \ is_enabled(self.env, self.store.__class__) and \ self.store.hash_method and True or False reset_password_enabled = property(_reset_password_enabled) # IPreferencePanelProvider methods def get_preference_panels(self, req): writable = self._write_check() if not writable: return if req.authname and req.authname != 'anonymous': user_store = self.acctmgr.find_user_store(req.authname) if user_store in writable: yield 'account', _("Account") def render_preference_panel(self, req, panel): data = dict(_dgettext=dgettext) data.update(self._do_account(req)) return 'prefs_account.html', data # IRequestFilter methods def pre_process_request(self, req, handler): if req.path_info == '/prefs/account' and \ not (req.authname and req.authname != 'anonymous'): # An anonymous session has no account associated with it, and # no account properies too, but general session preferences should # always be available. req.redirect(req.href.prefs()) return handler def post_process_request(self, req, template, data, content_type): if req.authname and req.authname != 'anonymous': if req.session.get('force_change_passwd', False): # Prevent authenticated usage before another password change. redirect_url = req.href.prefs('account') if req.href(req.path_info) != redirect_url: req.redirect(redirect_url) return (template, data, content_type) # IRequestHandler methods def match_request(self, req): return req.path_info == '/reset_password' and \ self._reset_password_enabled(log=True) def process_request(self, req): data = dict(_dgettext=dgettext) if req.authname and req.authname != 'anonymous': add_notice(req, Markup(tag_( "You're already logged in. If you need to change your " "password please use the %(prefs_href)s page.", prefs_href=tag.a(_("Account Preferences"), href=req.href.prefs('account'))))) data['authenticated'] = True if req.method == 'POST': self._do_reset_password(req) return 'reset_password.html', data, None def _do_account(self, req): assert(req.authname and req.authname != 'anonymous') action = req.args.get('action') delete_enabled = self.acctmgr.supports('delete_user') and \ self.acctmgr.allow_delete_account data = {'delete_enabled': delete_enabled, 'delete_msg_confirm': _( "Are you sure you want to delete your account?"), } force_change_password = req.session.get('force_change_passwd', False) if req.method == 'POST': if action == 'save': if self._do_change_password(req) and force_change_password: del req.session['force_change_passwd'] req.session.save() add_notice(req, _("Thank you for taking the time to " "update your password.")) force_change_password = False elif action == 'delete' and delete_enabled: self._do_delete(req) if force_change_password: add_warning(req, Markup(_( "You are required to change password because of a recent " "password change request. %(invitation)s", invitation=tag.b(_("Please change your password now."))))) return data def _do_change_password(self, req): username = req.authname old_password = req.args.get('old_password') if not self.acctmgr.check_password(username, old_password): if old_password: add_warning(req, _("Old password is incorrect.")) else: add_warning(req, _("Old password cannot be empty.")) return password = req.args.get('password') if not password: add_warning(req, _("Password cannot be empty.")) elif password != req.args.get('password_confirm'): add_warning(req, _("The passwords must match.")) elif password == old_password: add_warning(req, _("Password must not match old password.")) else: _set_password(self.env, req, username, password, old_password) if req.session.get('password') is not None: # Fetch all session_attributes in case new user password is in # SessionStore, preventing overwrite by session.save(). req.session.get_session(req.authname, authenticated=True) add_notice(req, _("Password updated successfully.")) return True def _do_delete(self, req): username = req.authname password = req.args.get('password') if not password: add_warning(req, _("Password cannot be empty.")) elif not self.acctmgr.check_password(username, password): add_warning(req, _("Password is incorrect.")) else: try: self.acctmgr.delete_user(username) except NotificationError, e: # User wont care for notification, only care for logging here. self.log.error( 'Unable to send account deletion notification: %s', exception_to_unicode(e, traceback=True)) # Delete the whole session, since records in session_attribute # would get restored on logout otherwise. req.session.clear() req.session.save() req.redirect(req.href.logout())
class RegistrationModule(Component): """Provides users the ability to register a new account. Requires configuration of the AccountManager module in trac.ini. """ implements(INavigationContributor, IRequestHandler, ITemplateProvider) def __init__(self): self.acctmgr = AccountManager(self.env) self._enable_check(log=True) def _enable_check(self, log=False): env = self.env writable = self.acctmgr.supports('set_password') ignore_case = auth.LoginModule(env).ignore_case if log: if not writable: self.log.warn('RegistrationModule is disabled because the ' 'password store does not support writing.') if ignore_case: self.log.debug('RegistrationModule will allow lowercase ' 'usernames only and convert them forcefully ' 'as required, while \'ignore_auth_case\' is ' 'enabled in [trac] section of your trac.ini.') return is_enabled(env, self.__class__) and writable enabled = property(_enable_check) # INavigationContributor methods def get_active_navigation_item(self, req): return 'register' def get_navigation_items(self, req): loginmod = LoginModule(self.env) if not self.enabled: return if req.authname == 'anonymous': yield 'metanav', 'register', tag.a(_("Register"), href=req.href.register()) # IRequestHandler methods def match_request(self, req): return req.path_info == '/register' and self._enable_check(log=True) def process_request(self, req): if req.authname != 'anonymous': req.redirect(req.href.prefs('account')) action = req.args.get('action') data = {'acctmgr' : { 'username' : None, 'name' : None, 'email' : None, }, '_dgettext': dgettext, } data['verify_account_enabled'] = is_enabled( self.env, EmailVerificationModule) and self.acctmgr.verify_email if req.method == 'POST' and action == 'create': try: _create_user(req, self.env) except TracError, e: data['registration_error'] = e.message data['acctmgr'] = getattr(e, 'acctmgr', '') else: chrome.add_notice(req, Markup(tag.span(Markup(_( """Registration has been finished successfully. You may login as user %(user)s now.""", user=tag.b(req.args.get('username'))))))) req.redirect(req.href.login()) data['reset_password_enabled'] = AccountModule(self.env ).reset_password_enabled return 'register.html', data, None
class AccountManagerAdminPage(Component): implements(IAdminPageProvider) def __init__(self): self.account_manager = AccountManager(self.env) # IAdminPageProvider def get_admin_pages(self, req): if req.perm.has_permission('TRAC_ADMIN'): yield ('accounts', 'Accounts', 'config', 'Configuration') yield ('accounts', 'Accounts', 'users', 'Users') def process_admin_request(self, req, cat, page, path_info): if page == 'config': return self._do_config(req) elif page == 'users': return self._do_users(req) def _do_config(self, req): if req.method == 'POST': selected_class = req.args.get('selected') self.config.set('account-manager', 'password_store', selected_class) selected = self.account_manager.password_store for attr, option in _getoptions(selected): newvalue = req.args.get('%s.%s' % (selected_class, attr)) if newvalue is not None: self.config.set(option.section, option.name, newvalue) self.config.save() try: selected = self.account_manager.password_store except AttributeError: selected = None sections = [ {'name': store.__class__.__name__, 'classname': store.__class__.__name__, 'selected': store is selected, 'options': [ {'label': attr, 'name': '%s.%s' % (store.__class__.__name__, attr), 'value': option.__get__(store, store), } for attr, option in _getoptions(store) ], } for store in self.account_manager.stores ] sections = sorted(sections, key=lambda i: i['name']) req.hdf['sections'] = sections return 'admin_accountsconfig.cs', None def _do_users(self, req): perm = PermissionSystem(self.env) listing_enabled = self.account_manager.supports('get_users') create_enabled = self.account_manager.supports('set_password') delete_enabled = self.account_manager.supports('delete_user') req.hdf['listing_enabled'] = listing_enabled req.hdf['create_enabled'] = create_enabled req.hdf['delete_enabled'] = delete_enabled if req.method == 'POST': if req.args.get('add'): if create_enabled: try: _create_user(req, self.env, check_permissions=False) except TracError, e: req.hdf['registration.error'] = e.message else: req.hdf['registration_error'] = 'The password store does ' \ 'not support creating users' elif req.args.get('remove'): if delete_enabled: sel = req.args.get('sel') sel = isinstance(sel, list) and sel or [sel] for account in sel: self.account_manager.delete_user(account) else: req.hdf['deletion_error'] = 'The password store does not ' \ 'support deleting users' if listing_enabled: accounts = {} for username in self.account_manager.get_users(): accounts[username] = {'username': username} for username, name, email in self.env.get_known_users(): account = accounts.get(username) if account: account['name'] = name account['email'] = email db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT sid,last_visit FROM session WHERE authenticated=1") for username, last_visit in cursor: account = accounts.get(username) if account and last_visit: account['last_visit'] = format_datetime(last_visit) req.hdf['accounts'] = sorted(accounts.itervalues(), key=lambda acct: acct['username']) return 'admin_users.cs', None