def validate_registration(self, req): if req.authname and req.authname != 'anonymous': return username = AccountManager(self.env).handle_username_casing( req.args.get('username', '').strip()) # NOTE: We can't use 'get_user_permissions(username)' here # as this always returns a list - even if the user doesn't exist. # In this case the permissions of "anonymous" are returned. # # Also note that we can't simply compare the result of # 'get_user_permissions(username)' to some known set of permission, # i.e. "get_user_permissions('authenticated') as this is always # false when 'username' is the name of an existing permission group. # # And again obfuscate whether an existing user or group name # was responsible for rejection of this username. for (perm_user, perm_action) in \ perm.PermissionSystem(self.env).get_all_permissions(): if perm_user.lower() == username.lower(): raise RegistrationError(N_( "Another account or group already exists, who's name " "differs from %s only by case or is identical."), tag.b(username) )
def post_process_request(self, req, template, data, content_type): if not req.session.authenticated: # Don't start the email verification precedure on anonymous users. return template, data, content_type email = req.session.get('email') # Only send verification if the user entered an email address. acctmgr = AccountManager(self.env) if acctmgr.verify_email and self.email_enabled is True and email and \ email != req.session.get('email_verification_sent_to') and \ not req.perm.has_permission('ACCTMGR_ADMIN'): req.session['email_verification_token'] = self._gen_token() req.session['email_verification_sent_to'] = email acctmgr._notify('email_verification_requested', req.authname, req.session['email_verification_token']) # TRANSLATOR: An email has been sent to %(email)s # with a token to ... (the link label for following message) link = tag.a(_("verify your new email address"), href=req.href.verify_email()) # TRANSLATOR: ... verify your new email address chrome.add_notice( req, Markup( tag.span( Markup( _("""An email has been sent to %(email)s with a token to %(link)s.""", email=email, link=link))))) return template, data, content_type
def use_account_manager_integration(self, member_name): if self.account_manager_is_enabled(): account_already_created = AccountManager( self.env).has_user(member_name) if not account_already_created: return AccountManager(self.env).supports('set_password') return False
def post_process_request(self, req, template, data, content_type): if not req.session.authenticated: # Don't start the email verification procedure on anonymous users. return template, data, content_type email = req.session.get('email') # Only send verification if the user entered an email address. acctmgr = AccountManager(self.env) if acctmgr.verify_email and self.email_enabled is True and email and \ email != req.session.get('email_verification_sent_to') and \ not req.perm.has_permission('ACCTMGR_ADMIN'): req.session['email_verification_token'] = self._gen_token() req.session['email_verification_sent_to'] = email acctmgr._notify( 'email_verification_requested', req.authname, req.session['email_verification_token'] ) # TRANSLATOR: An email has been sent to <%(email)s> # with a token to ... (the link label for following message) link = tag.a(_("verify your new email address"), href=req.href.verify_email() ) # TRANSLATOR: ... verify your new email address chrome.add_notice(req, Markup(tag.span(Markup(_( """An email has been sent to <%(email)s> with a token to %(link)s.""", email=tag(email), link=link)))) ) return template, data, content_type
def delete_user(self, username): try: from acct_mgr.api import AccountManager if AccountManager(self.env).has_user(username): AccountManager(self.env).delete_user(username) except Exception, e: self.log.error("Unable to delete user's authentication details")
def validate_registration(self, req): if req.path_info == '/prefs': return acctmgr = AccountManager(self.env) username = acctmgr.handle_username_casing( req.args.get('username', '').strip()) if not username: raise RegistrationError(N_("Username cannot be empty.")) # Always exclude some special characters, i.e. # ':' can't be used in HtPasswdStore # '[' and ']' can't be used in SvnServePasswordStore blacklist = acctmgr.username_char_blacklist if contains_any(username, blacklist): pretty_blacklist = '' for c in blacklist: if pretty_blacklist == '': pretty_blacklist = tag(' \'', tag.b(c), '\'') else: pretty_blacklist = tag(pretty_blacklist, ', \'', tag.b(c), '\'') raise RegistrationError(N_( "The username must not contain any of these characters: %s"), tag.b(pretty_blacklist) ) # All upper-cased names are reserved for permission action names. if username.isupper(): raise RegistrationError(N_("A username with only upper-cased " "characters is not allowed.")) # Prohibit some user names, that are important for Trac and therefor # reserved, even if not in the permission store for some reason. if username.lower() in ['anonymous', 'authenticated']: raise RegistrationError(N_("Username %s is not allowed."), tag.b(username)) # NOTE: A user may exist in a password store but not in the permission # store. I.e. this happens, when the user (from the password store) # never logged in into Trac. So we have to perform this test here # and cannot just check for the user being in the permission store. # And better obfuscate whether an existing user or group name # was responsible for rejection of this user name. for store_user in acctmgr.get_users(): # Do it carefully by disregarding case. if store_user.lower() == username.lower(): raise RegistrationError(tag_( "Another account or group already exists, who's name " "differs from %(username)s only by case or is identical.", username=tag.b(username))) # Password consistency checks follow. password = req.args.get('password') if not password: raise RegistrationError(N_("Password cannot be empty.")) elif password != req.args.get('password_confirm'): raise RegistrationError(N_("The passwords must match."))
def process_request(self, req): req.perm.assert_permission('TRAC_ADMIN') if not re.match(r'/autocompleteperms/autocompleteperms\.js$', req.path_info): return subjects = set([]) db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT username,action FROM permission") rows = cursor.fetchall() while True: num_users = len(subjects) for user, action in rows: if user not in subjects: subjects.add(user) for provider in self.group_providers: subjects.update(provider.get_permission_groups(action)) if not action.isupper() and action not in subjects: subjects.add(action) for provider in self.group_providers: subjects.update(provider.get_permission_groups(action)) if num_users == len(subjects): break try: from acct_mgr.api import AccountManager acc_mgr = AccountManager(self.env) users = acc_mgr.get_users() except: users = [x[0] for x in self.env.get_known_users()] group_list = list(subjects - set(users) - set('#%s' % user for user in users)) subjects.update(users) user_list = list(subjects) user_list.sort() group_list.sort() out = """ var data = "%s".split(" "); var data_groups = "%s".split(" "); $(document).ready(function() { $("#gp_subject").autocomplete(data, {minChars: 0, max:9999}); $("#sg_subject").autocomplete(data, {minChars: 0, max:9999}); $("#sg_group").autocomplete(data_groups, {minChars: 0, max:9999}); }); """ % (" ".join(user_list), " ".join(group_list)) req.send(out.encode("utf-8"), "text/javascript")
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='******', name='', email='') incookie = Cookie() incookie['trac_session'] = '123456' self.req = Mock(authname='', args=args, authenticated=True, base_path='/', callbacks=dict(), href=Mock(prefs=lambda x: None), incookie=incookie, outcookie=Cookie(), redirect=lambda x: None) self.req.path_info = '/'
def authenticate(self, req): """Return the name of the remote user, or `None` if the identity of the user is unknown.""" # check for an authenticated user login_module = LoginModule(self.env) remote_user = login_module.authenticate(req) if remote_user: return remote_user # authenticate via a CAPTCHA if 'captchaauth' in req.args and 'captchaid' in req.args: # ensure CAPTCHA identification captcha = self.captcha(req) if captcha != req.args['captchaauth']: return # ensure sane identity name, email = self.identify(req) if name is None: return if AccountManager and name in AccountManager(self.env).get_users(): return # delete used CAPTCHA on success try: execute_non_query(self.env, "DELETE FROM captcha WHERE id=%s", req.args['captchaid']) except: pass # log the user in req.environ['REMOTE_USER'] = name login_module._do_login(req)
def create_user_and_grant_permissions(self, req, team_member): if self.use_account_manager_integration(team_member.name): password = team_member.name AccountManager(self.env).set_password(team_member.name, password) permission_system = PermissionSystem(self.env) if not permission_system.check_permission(Role.TEAM_MEMBER, team_member.name): permission_system.grant_permission(team_member.name, Role.TEAM_MEMBER)
def process_request(self, req): if not req.session.authenticated: chrome.add_warning( req, Markup( tag.span( tag_( "Please log in to finish email verification procedure." )))) req.redirect(req.href.login()) if 'email_verification_token' not in req.session: chrome.add_notice(req, _("Your email is already verified.")) elif req.method == 'POST' and 'resend' in req.args: AccountManager(self.env)._notify( 'email_verification_requested', req.authname, req.session['email_verification_token']) chrome.add_notice( req, _("A notification email has been resent to <%s>."), req.session.get('email')) elif 'verify' in req.args: # allow via POST or GET (the latter for email links) if req.args['token'] == req.session['email_verification_token']: del req.session['email_verification_token'] chrome.add_notice( req, _("Thank you for verifying your email address.")) req.redirect(req.href.prefs()) else: chrome.add_warning(req, _("Invalid verification token")) data = {'_dgettext': dgettext} if 'token' in req.args: data['token'] = req.args['token'] if 'email_verification_token' not in req.session: data['button_state'] = {'disabled': 'disabled'} return 'verify_email.html', data, None
def setUp(self): self.env = EnvironmentStub(enable=[ 'trac.*', 'acct_mgr.api.*', 'acct_mgr.admin.*', 'acct_mgr.db.*', 'acct_mgr.register.*', 'acct_mgr.pwhash.HtDigestHashMethod', 'acct_mgr.tests.admin.BadCheck', 'acct_mgr.tests.admin.DummyCheck' ]) self.env.path = tempfile.mkdtemp() self.perm = PermissionSystem(self.env) # Create a user reference in the permission system. self.perm.grant_permission('admin', 'ACCTMGR_ADMIN') # Prepare a generic request object for admin actions. self.req = Mock(authname='admin', method='GET', args=dict(), abs_href=self.env.abs_href, chrome=dict(notices=[], warnings=[]), href=self.env.abs_href, locale='', redirect=lambda x: None, session=dict(), tz='') self.req.perm = PermissionCache(self.env, 'admin') self.acctmgr = AccountManager(self.env)
def __init__(self): self.account_manager = None try: from acct_mgr.api import AccountManager self.account_manager = AccountManager(self.env) except ImportError: pass
def _check_password(self, req): header = req.get_header('Authorization') if header: token = header.split()[1] user, passwd = b64decode(token).split(':', 1) if AccountManager(self.env).check_password(user, passwd): return user
def _do_import_current_users(self, req, dry_run=False): """ """ active_users = [ user.username for user in UserManager(self.env).get_active_users() ] try: from acct_mgr.api import AccountManager known_users = list(AccountManager(self.env).get_users()) except: return [] imported_users = [] for username in known_users: if not username in active_users: imported_users.append(username) if not dry_run: UserManager(self.env).create_user(User(username)) if dry_run: return imported_users if len(imported_users) > 0: return _("Successfully imported the following users [%s].") % ( ','.join(imported_users)) else: return _("No users imported.")
def authenticate(self, req): if req.remote_user: return req.remote_user for path in self.paths: if req.path_info.startswith(path): header = req.get_header('Authorization') if header is None: self.log.info( 'HTTPAuthFilter: No authentication data given, returing 403' ) return None # Run HTTP auth else: token = header.split()[1] user, passwd = b64decode(token).split(':', 1) if AccountManager(self.env).check_password(user, passwd): self.log.debug('HTTPAuthFilter: Authentication okay') # req.environ['REMOTE_USER'] = user # self.log.debug(req.remote_user) return user else: self.log.info( 'HTTPAuthFilter: Bad authentication data given, returing 403' ) return None # Failed auth return None
def pre_process_request(self, req, handler): if not req.authname or req.authname == 'anonymous': # Permissions for anonymous users remain unchanged. return handler elif req.path_info == '/prefs' and \ req.method == 'POST' and \ 'restore' not in req.args and \ req.get_header( 'X-Requested-With') != 'XMLHttpRequest': try: AccountManager(self.env).validate_account(req) # Check passed without error: New email address seems good. except RegistrationError, e: # Always warn about issues. chrome.add_warning(req, e) # Look, if the issue existed before. attributes = get_user_attribute(self.env, req.authname, attribute='email') email = req.authname in attributes and \ attributes[req.authname][1].get('email') or None new_email = req.args.get('email', '').strip() if (email or new_email) and email != new_email: # Attempt to change email to an empty or invalid # address detected, resetting to previously stored value. req.redirect(req.href.prefs(None))
def _do_create_user(self, req): """ """ if not req.args.get('um_newuser_username') or not req.args.get( 'um_newuser_username').strip(): raise TracError(_("Username field is mandatory")) is_trac_managed = req.args.get('um_newuser_type') == 'trac-managed' if is_trac_managed and not req.args.get('um_newuser_password'): raise TracError(_('Password field it\'s mandatory')) user = User(req.args.get('um_newuser_username').strip()) for field in ['name', 'email', 'role' ] + (is_trac_managed and ['password'] or []): if field == 'password': if req.args.get('um_newuser_password') == req.args.get( 'um_newuser_confirm_password'): try: from acct_mgr.api import AccountManager AccountManager(self.env).set_password( user.username, req.args.get('um_newuser_password')) except Exception, e: self.log.error(e) raise TracError( _('Unable to set %s\'s password. Please check out log messages.' % (user.username))) else: raise TracError(_('Passwords don\'t match')) continue if req.args.get('um_newuser_%s' % (field)): user[field] = req.args.get('um_newuser_%s' % (field))
def __init__(self): self.authz_file = self.env.config.get("trac", "authz_file") self.authz_module = self.env.config.get("trac", "authz_module_name") self.show_all_repos = self.env.config.getbool("svnauthzadmin", "show_all_repos") if self.authz_module != None and self.authz_module.strip() == "": self.authz_module = None self.account_manager = AccountManager(self.env)
def post_process_request(self, req, template, data, content_type): if template is None or not req.session.authenticated: # Don't start the email verification procedure on anonymous users. return template, data, content_type email = req.session.get('email') # Only send verification if the user entered an email address. if self.verify_email and self.email_enabled is True and email and \ email != req.session.get('email_verification_sent_to') and \ 'ACCTMGR_ADMIN' not in req.perm: req.session['email_verification_token'] = self._gen_token() req.session['email_verification_sent_to'] = email try: AccountManager(self.env)._notify( 'email_verification_requested', req.authname, req.session['email_verification_token'] ) 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)) else: # TRANSLATOR: An email has been sent to <%(email)s> # with a token to ... (the link label for following message) link = tag.a(_("verify your new email address"), href=req.href.verify_email()) # TRANSLATOR: ... verify your new email address chrome.add_notice(req, tag_( "An email has been sent to <%(email)s> with a token to " "%(link)s.", email=tag(email), link=link))
def process_request(self, req): if not req.session.authenticated: chrome.add_warning(req, tag_( "Please log in to finish email verification procedure.")) req.redirect(req.href.login()) if 'email_verification_token' not in req.session: chrome.add_notice(req, _("Your email is already verified.")) elif req.method == 'POST' and 'resend' in req.args: try: AccountManager(self.env)._notify( 'email_verification_requested', req.authname, req.session['email_verification_token'] ) 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 verification notification: %s', exception_to_unicode(e, traceback=True)) else: chrome.add_notice(req, _("A notification email has been " "resent to <%s>."), req.session.get('email'))
def process_request(self, req): if req.path_info.startswith('/login') and req.authname == 'anonymous': try: referer = self._referer(req) except AttributeError: # Fallback for Trac 0.11 compatibility. referer = req.get_header('Referer') # Steer clear of requests going nowhere or loop to self. if referer is None or \ referer.startswith(str(req.abs_href()) + '/login'): referer = req.abs_href() data = { '_dgettext': dgettext, 'login_opt_list': self.login_opt_list, 'persistent_sessions': AccountManager(self.env).persistent_sessions, 'referer': referer, 'registration_enabled': RegistrationModule(self.env).enabled, 'reset_password_enabled': AccountModule(self.env).reset_password_enabled } if req.method == 'POST': self.log.debug( "LoginModule.process_request: 'user_locked' = %s" % req.args.get('user_locked')) if not req.args.get('user_locked'): # TRANSLATOR: Intentionally obfuscated login error data['login_error'] = _("Invalid username or password") else: f_user = req.args.get('username') release_time = AccountGuard(self.env).pretty_release_time( req, f_user) if not release_time is None: data['login_error'] = _( """Account locked, please try again after %(release_time)s """, release_time=release_time) else: data['login_error'] = _("Account locked") return 'login.html', data, None else: n_plural = req.args.get('failed_logins') if n_plural > 0: add_warning( req, Markup( tag.span( tag( ngettext( "Login after %(attempts)s failed attempt", "Login after %(attempts)s failed attempts", n_plural, attempts=n_plural))))) return auth.LoginModule.process_request(self, req)
def _remote_user(self, req): """The real authentication using configured providers and stores.""" user = req.args.get('user') self.env.log.debug( "LoginModule._remote_user: Authentication attempted for '%s'" % user) password = req.args.get('password') if not user: return None acctmgr = AccountManager(self.env) acctmod = AccountModule(self.env) if acctmod.reset_password_enabled == True: reset_store = acctmod.store else: reset_store = None if acctmgr.check_password(user, password) == True: if reset_store: # Purge any temporary password set for this user before, # to avoid DOS by continuously triggered resets from # a malicious third party. if reset_store.delete_user(user) == True and \ 'PASSWORD_RESET' not in req.environ: db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute( """ DELETE FROM session_attribute WHERE sid=%s AND name='force_change_passwd' AND authenticated=1 """, (user, )) db.commit() return user # Alternative authentication provided by password reset procedure elif reset_store: if reset_store.check_password(user, password) == True: # Lock, required to prevent another authentication # (spawned by `set_password()`) from possibly deleting # a 'force_change_passwd' db entry for this user. req.environ['PASSWORD_RESET'] = user # Change password to temporary password from reset procedure acctmgr.set_password(user, password) return user return None
class EmailVerificationModule(CommonTemplateProvider): """Performs email verification on every new or changed address. A working email sender for Trac (!TracNotification or !TracAnnouncer) is strictly required to enable this module's functionality. Anonymous users should register and perms should be tweaked, so that anonymous users can't edit wiki pages and change or create tickets. So this email verification code won't be used on them. """ implements(IRequestFilter, IRequestHandler) def __init__(self, *args, **kwargs): self.email_enabled = True if self.config.getbool('announcer', 'email_enabled') != True and \ self.config.getbool('notification', 'smtp_enabled') != True: self.email_enabled = False if is_enabled(self.env, self.__class__) == True: self.env.log.warn(' '.join([ self.__class__.__name__, "can't work because of missing email setup." ])) # IRequestFilter methods def pre_process_request(self, req, handler): if not req.session.authenticated: # Permissions for anonymous users remain unchanged. return handler elif req.path_info == '/prefs' and req.method == 'POST' and \ not 'restore' in req.args: try: EmailCheck(self.env).validate_registration(req) # Check passed without error: New email address seems good. except RegistrationError, e: # Attempt to change email to an empty or invalid # address detected, resetting to previously stored value. chrome.add_warning(req, Markup(gettext(e.message))) req.redirect(req.href.prefs(None)) if AccountManager(self.env).verify_email and handler is not self and \ 'email_verification_token' in req.session and \ not req.perm.has_permission('ACCTMGR_ADMIN'): # TRANSLATOR: Your permissions have been limited until you ... link = tag.a(_("verify your email address"), href=req.href.verify_email()) # TRANSLATOR: ... verify your email address chrome.add_warning( req, Markup( tag.span( Markup( _("Your permissions have been limited until you %(link)s.", link=link))))) req.perm = perm.PermissionCache(self.env, 'anonymous') return handler
def __init__(self): self.mgr = AccountManager(self.env) # Adjust related value to promote sane configurations, because the # combination of some default values is not meaningful. if not self.login_attempt_max_count > 0 and \ self.user_lock_max_time != 0: self.config.set('account-manager', 'user_lock_max_time', '0') # Write change back to file to make it permanent. self.config.save()
def _set_password(env, req, username, password, old_password=None): try: AccountManager(env).set_password(username, password, old_password=old_password) except NotificationError, e: add_warning(req, _("Error raised while sending a change " "notification.") + _("You should report that issue to a Trac admin.")) env.log.error('Unable to send password change notification: %s', exception_to_unicode(e, traceback=True))
def validate_registration(self, req): acctmgr = AccountManager(self.env) username = acctmgr.handle_username_casing( req.args.get('username', '').strip()) if self.username_regexp != "" and \ not re.match(self.username_regexp.strip(), username): raise RegistrationError( N_("Username %s doesn't match local naming policy."), tag.b(username)) email = req.args.get('email', '').strip() if acctmgr.verify_email and is_enabled(self.env, EmailCheck) and \ is_enabled(self.env, EmailVerificationModule): if self.email_regexp.strip() != "" and \ not re.match(self.email_regexp.strip(), email): raise RegistrationError( N_("The email address specified appears to be invalid. " "Please specify a valid email address."))
def _remote_user(self, req): """The real authentication using configured providers and stores.""" user = req.args.get('user') self.env.log.debug("LoginModule._remote_user: Authentication attempted for '%s'" % user) password = req.args.get('password') if not user or not password: return None acctmgr = AccountManager(self.env) acctmod = AccountModule(self.env) if acctmod.reset_password_enabled == True: reset_store = acctmod.store else: reset_store = None if acctmgr.check_password(user, password) == True: if reset_store: # Purge any temporary password set for this user before, # to avoid DOS by continuously triggered resets from # a malicious third party. if reset_store.delete_user(user) == True and \ 'PASSWORD_RESET' not in req.environ: db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute(""" DELETE FROM session_attribute WHERE sid=%s AND name='force_change_passwd' AND authenticated=1 """, (user,)) db.commit() return user # Alternative authentication provided by password reset procedure elif reset_store: if reset_store.check_password(user, password) == True: # Lock, required to prevent another authentication # (spawned by `set_password()`) from possibly deleting # a 'force_change_passwd' db entry for this user. req.environ['PASSWORD_RESET'] = user # Change password to temporary password from reset procedure acctmgr.set_password(user, password) return user return None
def setUp(self): _BaseTestCase.setUp(self) self.env = EnvironmentStub( enable=['trac.*', 'acct_mgr.api.*', 'acct_mgr.register.*']) self.env.path = tempfile.mkdtemp() self.reg_template = 'register.html' self.req.method = 'POST' self.acctmgr = AccountManager(self.env) self.check = BasicCheck(self.env) self.rmod = RegistrationModule(self.env)
def validate_registration(self, req): acctmgr = AccountManager(self.env) username = acctmgr.handle_username_casing( req.args.get('username', '').strip()) if self.username_regexp != "" and \ not re.match(self.username_regexp.strip(), username): raise RegistrationError(N_( "Username %s doesn't match local naming policy."), tag.b(username) ) email = req.args.get('email', '').strip() if acctmgr.verify_email and is_enabled(self.env, EmailCheck) and \ is_enabled(self.env, EmailVerificationModule): if self.email_regexp.strip() != "" and \ not re.match(self.email_regexp.strip(), email): raise RegistrationError(N_( "The email address specified appears to be invalid. " "Please specify a valid email address.") )
def process_admin_request(self, req, category, page, path_info): mgr = AccountManager(self.env) if path_info: return self._process_user_request(req, category, page, path_info, mgr) else: if req.method == 'POST': if req.args.get('add'): self._do_add(req, mgr) elif req.args.get('remove'): self._do_remove(req, mgr) # run user list through a set to work around a bug in # account manager # see http://trac-hacks.org/ticket/180 users = list(sets.Set(mgr.get_users())) users.sort() req.hdf['admin.users'] = \ [{'name': u, 'key': u, 'href': self.env.href.admin(category, page, u) } for u in users] return 'admin_users.cs', None
def validate_registration(self, req): acctmgr = AccountManager(self.env) email = req.args.get('email', '').strip() if is_enabled(self.env, EmailVerificationModule) and \ acctmgr.verify_email: if not email: raise RegistrationError( N_("You must specify a valid email address.")) elif email_associated(self.env, email): raise RegistrationError( N_("The email address specified is already in use. " "Please specify a different one."))
def render_usermanager_admin_panel(self, req, panel, user, path_info): data = { 'TYPES': ['trac-managed', 'server-managed'], 'set_password_enabled': AccountManager(self.env).supports('set_password'), 'delete_enabled': AccountManager(self.env).supports('delete_user') } messages = [] errors = [] if req.method == 'POST': if req.args.has_key('um_account_update_type'): if req.args.get('um_account_type' ) == 'trac-managed' and not AccountManager( self.env).has_user(user.username): AccountManager(self.env).set_password( user.username, ''.join([ Random().choice('pleaseChangeThisPassword') for x in range(10) ])) messages.append( _("Successfully changed %s's authentication method") % (user.username)) elif req.args.get('um_account_type') == 'server-managed': AccountManager(self.env).delete_user(user.username) messages.append( _("Successfully changed %s's authentication method") % (user.username)) else: raise TracError("Unknow account type") elif req.args.has_key('um_account_change_password'): if req.args['um_account_confirm_password'] == req.args[ 'um_account_new_password']: AccountManager(self.env).set_password( user.username, req.args['um_account_new_password']) messages.append( _("Successfully changed %s's password") % (user.username)) else: errors.append(_('Passwords don\'t match')) else: raise TracError("Unknow action") # Adding type data.update(type=AccountManager(self.env).has_user(user.username) and 'trac-managed' or 'server-managed') return 'admin_um_account.html', { 'um_account': data, 'messages': messages, 'errors': errors }
def get_users(self): users = {} try: from acct_mgr.api import AccountManager, get_user_attribute acct_mgr = AccountManager(self.env) for username in acct_mgr.get_users(): users[username] = { 'username': username } for username, status in get_user_attribute(self.env, username=None, authenticated=None).iteritems(): user = users.get(username) if user is not None and 1 in status: user['name'] = status[1].get('name') user['email'] = status[1].get('email') user.update(self.get_last_login(username)) except: for username, name, email in self.env.get_known_users(): user = { 'username': username, 'name': name, 'email': email } user.update(self.get_last_login(username)) users[username] = user return users
def authenticate(self, req): if req.method == 'POST' and req.path_info.startswith('/login'): user = self._remote_user(req) acctmgr = AccountManager(self.env) guard = AccountGuard(self.env) if guard.login_attempt_max_count > 0: if user is None: if req.args.get('user_locked') is None: # get user for failed authentication attempt f_user = req.args.get('user') req.args['user_locked'] = False if acctmgr.user_known(f_user) is True: if guard.user_locked(f_user) is False: # log current failed login attempt guard.failed_count(f_user, req.remote_addr) if guard.user_locked(f_user) is True: # step up lock time prolongation # only when just triggering the lock guard.lock_count(f_user, 'up') req.args['user_locked'] = True else: # enforce lock req.args['user_locked'] = True else: if guard.user_locked(user) is not False: req.args['user_locked'] = True # void successful login as long as user is locked user = None else: req.args['user_locked'] = False if req.args.get('failed_logins') is None: # Reset failed login attempts counter req.args['failed_logins'] = guard.failed_count( user, reset=True) if 'REMOTE_USER' not in req.environ: req.environ['REMOTE_USER'] = user return auth.LoginModule.authenticate(self, req)
def authenticate(self, req): if req.method == 'POST' and req.path_info.startswith('/login'): user = self._remote_user(req) acctmgr = AccountManager(self.env) guard = AccountGuard(self.env) if guard.login_attempt_max_count > 0: if user is None: if req.args.get('user_locked') is None: # get user for failed authentication attempt f_user = req.args.get('user') req.args['user_locked'] = False if acctmgr.user_known(f_user) is True: if guard.user_locked(f_user) is False: # log current failed login attempt guard.failed_count(f_user, req.remote_addr) if guard.user_locked(f_user) is True: # step up lock time prolongation # only when just triggering the lock guard.lock_count(f_user, 'up') req.args['user_locked'] = True else: # enforce lock req.args['user_locked'] = True else: if guard.user_locked(user) is not False: req.args['user_locked'] = True # void successful login as long as user is locked user = None else: req.args['user_locked'] = False if req.args.get('failed_logins') is None: # Reset failed login attempts counter req.args['failed_logins'] = guard.failed_count( user, reset = True) if 'REMOTE_USER' not in req.environ: req.environ['REMOTE_USER'] = user return auth.LoginModule.authenticate(self, req)
def fetch_user_data(env, req): acctmgr = AccountManager(env) guard = AccountGuard(env) accounts = {} for username in acctmgr.get_users(): if req.perm.has_permission('ACCTMGR_USER_ADMIN'): url = req.href.admin('accounts', 'users', user=username) else: url = None accounts[username] = {'username': username, 'review_url': url} if guard.user_locked(username): accounts[username]['locked'] = True t_lock = guard.lock_time(username) if t_lock > 0: t_release = guard.pretty_release_time(req, username) accounts[username]['release_hint'] = _( "Locked until %(t_release)s", t_release=t_release) for acct, status in get_user_attribute(env, username=None, authenticated=None).iteritems(): account = accounts.get(acct) if account is not None and 1 in status: # Only use attributes related to authenticated # accounts. account['name'] = status[1].get('name') account['email'] = status[1].get('email') if account['email']: account['email'] = Chrome(env).format_author(req, account['email']) ts_seen = last_seen(env) if ts_seen is not None: for username, last_visit in ts_seen: account = accounts.get(username) if account and last_visit: account['last_visit'] = to_datetime(last_visit) return sorted(accounts.itervalues(), key=lambda acct: acct['username'])
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='******', name='', email='') incookie = Cookie() incookie['trac_session'] = '123456' self.req = Mock(authname='', args=args, authenticated=True, base_path='/', callbacks=dict(), chrome={'warnings': [], 'notices': []}, href=Mock(prefs=lambda x: None), incookie=incookie, outcookie=Cookie(), redirect=lambda x: None) self.req.path_info = '/'
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='******', name='', email='') incookie = Cookie() incookie['trac_session'] = '123456' self.req = Mock(authname='', args=args, authenticated=True, base_path='/', callbacks=dict(), chrome={'warnings': [], 'notices': []}, href=Mock(prefs=lambda x: None), incookie=incookie, outcookie=Cookie(), redirect=lambda x: None) self.req.path_info = '/' # Tests 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 PageAuthzPolicyEditor(Component): implements(IAdminPanelProvider, ITemplateProvider) def __init__(self): self.account_manager = AccountManager(self.env) # ITemplateProvider methods def get_templates_dirs(self): return [resource_filename(__name__, 'templates')] def get_htdocs_dirs(self): return [] # IAdminPanelProvider methods def get_admin_panels(self, req): if 'TRAC_ADMIN' in req.perm: yield ('accounts', translation._('Accounts'), 'pages', translation._('Page Permissions')) def _get_filename(self, section, name): file_name = self.config.get(section, name) if len(file_name): if (not file_name.startswith(os.path.sep)) and (not file_name[1] == (':')): file_name = os.path.join(self.env.path, file_name) return(file_name) else: return(None) def _get_users(self): user_list = ', '.join(self.account_manager.get_users()) return(user_list) def _group_filename(self): group_file_name = self._get_filename('account-manager', 'group_file') if not group_file_name: group_file_name = self._get_filename('htgroups', 'group_file') if not group_file_name: raise TracError('Group filename not found in the config file. In neither sections\ "account-manager" nor "htgroups" under the name "group_file".') if not os.path.exists(group_file_name): raise TracError('Group filename not found: %s.' % group_file_name) return(group_file_name) # Get the groups and their members so they can easily be included in the # groups section of the authz file. Need it as a dictionary of arrays so it be easily # iterated. def _get_groups_and_members(self): """ Get the groups and their members as a dictionary of lists. """ # could be in one of two places, depending if the # account-manager is installed or not group_file_name = self._group_filename() groups_dict = dict() group_file = file(group_file_name) try: for group_line in group_file: # Ignore blank lines and lines starting with # group_line = group_line.strip() if group_line and not group_line.startswith('#'): group_name = group_line.split(':', 1)[0] group_members = group_line.split(':', 2)[1].split(' ') groups_dict[group_name] = [ x for x in [member.strip() for member in group_members] if x ] finally: group_file.close() if len(groups_dict): return groups_dict else: return None def render_admin_panel(self, req, cat, page, path_info): req.perm.require('TRAC_ADMIN') authz_policy_file_name = self._get_filename('authz_policy', 'authz_file') group_details = self._get_groups_and_members() # Handle the return data if req.method == 'POST': if req.args.get('authz_file_contents'): # The data needs to be validated, otherwise duplicate # entries can break things. edited_contents = str(req.args.get('authz_file_contents')) edited_contents_stringio = StringIO(edited_contents) try: test_authz_policy_dict = ConfigObj(edited_contents_stringio) except: raise TracError(_('Error in edited file. Re-edit and check for duplicate entries.')) authz_policy_file = open(authz_policy_file_name, 'w') test_authz_policy_dict.write(authz_policy_file) authz_policy_file.close() authz_policy_dict = ConfigObj(authz_policy_file_name) # If there isn't a group file, don't destroy the existing entries if (group_details): authz_policy_dict['groups'] = group_details # This is purely to fill in the text area with the contents. contents = StringIO() authz_policy_dict.write(contents) #contents = open(authz_policy_file_name).readlines() data = { 'file_name' : authz_policy_file_name, 'contents': contents.getvalue(), 'users' : self._get_users() } return 'page_authz_policy_editor.html', {'pages_authz': data}
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 GroupsEditorPlugin(Component): """ Trac Groups Editor plugin Edit the groups in the group file. Select a group Display it's contents Delete or add listed members If the fine grained page permissions plugine is enabled, then as an option also update it. """ implements(IAdminPanelProvider, ITemplateProvider) def __init__(self): self.account_manager = AccountManager(self.env) ### ITemplateProvider methods def get_templates_dirs(self): from pkg_resources import resource_filename return [resource_filename(__name__, 'templates')] def get_htdocs_dirs(self): from pkg_resources import resource_filename return [('ge', resource_filename(__name__, 'htdocs'))] ### IAdminPanelProvider methods def get_admin_panels(self, req): if 'TRAC_ADMIN' in req.perm: yield ('accounts', _("Accounts"), 'groups', _("Groups")) def _get_filename(self, section, name): file_name = self.config.get(section, name) if len(file_name): if not file_name.startswith(os.path.sep) and \ not file_name[1] == ':': file_name = os.path.join(self.env.path, file_name) return file_name else: return None def _group_filename(self): group_file_name = self._get_filename('account-manager', 'group_file') if not group_file_name: group_file_name = self._get_filename('htgroups', 'group_file') if not group_file_name: raise TracError("""Group filename not found in the config file. In neither sections "account-manager" nor "htgroups" under the name "group_file".""") if not os.path.exists(group_file_name): raise TracError('Group filename not found: %s.' % group_file_name) return group_file_name def _get_groups_and_members(self): """Get the groups and their members as a dictionary of lists. """ # could be in one of two places, depending if the account-manager # is installed or not group_file_name = self._group_filename() groups_dict = dict() group_file = file(group_file_name) try: for group_line in group_file: # Ignore blank lines and lines starting with # group_line = group_line.strip() if group_line and not group_line.startswith('#'): group_name = group_line.split(':', 1)[0] group_members = group_line.split(':', 2)[1].split(' ') groups_dict[group_name] = [member.strip() for member in group_members if member] finally: group_file.close() if len(groups_dict): return groups_dict else: return None def _write_groups_file(self, entries): """ Write the groups and members to the groups file """ group_file = open(self._group_filename(), 'w') for group_name in entries.keys(): group_file.write(group_name + ': ' + ' '.join(entries[group_name]) + '\n') group_file.close() def _check_for_finegrained(self): """Check if the fine grained permission system is enabled.""" component = self.config.get('components', 'authzpolicy.authz_policy.authzpolicy') return component == 'enabled' def _update_fine_grained(self, group_details): authz_policy_file_name = self._get_filename('authz_policy', 'authz_file') authz_policy_dict = ConfigObj(authz_policy_file_name) # If there isn't a group file, don't destroy the existing entries if group_details: authz_policy_dict['groups'] = group_details authz_policy_dict.write() def render_admin_panel(self, req, cat, page, path_info): """Render the panel. When applying deletions and additions, additions happen post deletions, so additions in effect have a higher precedence. The way it is done, it shouldn't be possible to have both """ req.perm.require('TRAC_ADMIN') add_stylesheet(req, 'ge/css/htgroupeditor.css') page_args = {} group_details = self._get_groups_and_members() # For ease of understanding and future reading # being done in the ORDER displayed. if not req.method == 'POST': groups_list = [''] if group_details is not None: groups_list += group_details.keys() page_args['groups_list'] = groups_list else: group_name = str(req.args.get('group_name')) # put the selected entry at the top of the list groups_list = group_details.keys() groups_list.remove(group_name) groups_list.insert(0, group_name) # Get rid of duplicates users_list = list() for name in group_details[group_name]: if name not in users_list: users_list.append(name) group_details[group_name] = users_list if req.args.get('deletions'): deletions = req.args.get('deletions') if not isinstance(deletions, list): deletions = [deletions] for deletion in deletions: # In case there arer multiple entries while deletion in group_details[group_name]: group_details[group_name].remove(deletion) if req.args.get('additional_names'): additional_names = req.args.get('additional_names') if not isinstance(additional_names, list): additional_names = [additional_names] # If a reload is done after an add, a duplicate can be created for name in additional_names: if name not in group_details[group_name]: group_details[group_name].append(name) # get the list of users not in the group addable_usernames = [] for username in self.account_manager.get_users(): username = username.strip() if len(username) and not username in group_details[group_name]: addable_usernames.append(username) group_details[group_name].sort() page_args['groups_list'] = groups_list page_args['users_list'] = group_details[group_name] page_args['addable_usernames'] = addable_usernames page_args['group_name'] = group_name page_args['finegrained'] = self._check_for_finegrained() if req.args.get('apply_changes'): self._write_groups_file(group_details) # update the fine grained permissions, if it is installed if req.args.get('finegrained_check'): self._update_fine_grained(group_details) return 'htgroupeditor.html', page_args
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"
def __init__(self): self.acctmgr = AccountManager(self.env) self.store = ResetPwStore(self.env) self._write_check(log=True)
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 GroupsEditorPlugin(Component): implements(IAdminPanelProvider, ITemplateProvider, IPermissionGroupProvider) def __init__(self): self.account_manager = AccountManager(self.env) # ITemplateProvider methods # Used to add the plugin's templates and htdocs def get_templates_dirs(self): return [resource_filename(__name__, 'templates')] def get_htdocs_dirs(self): return [] # IPermissionGroupProvider methdos def get_permission_groups(self, username): """Return a list of names of the groups that the user with the specified name is a member of.""" groups_list = [] groups_dict = self._get_groups_and_members() or {} for group,usernames in groups_dict.items(): if username in usernames: groups_list.append(group) return groups_list # IAdminPanelProvider methods def get_admin_panels(self, req): """Return a list of available admin panels. The items returned by this function must be tuples of the form `(category, category_label, page, page_label)`. """ if 'TRAC_ADMIN' in req.perm: # Simply put, it's what goes in the menu on the left! # the page is the name of the page that will be called for the menu entry # it will go ...../category/page yield ('accounts', translation._('Accounts'), 'groups', translation._('Groups')) def _get_filename(self, section, name): file_name = self.config.get(section, name) if len(file_name): if (not file_name.startswith(os.path.sep)) and (not file_name[1] == (':')): file_name = os.path.join(self.env.path, file_name) return(file_name) else: return(None) def _group_filename(self): group_file_name = self._get_filename('account-manager', 'group_file') if not group_file_name: group_file_name = self._get_filename('htgroups', 'group_file') if not group_file_name: raise TracError('Group filename not found in the config file. In neither sections\ "account-manager" nor "htgroups" under the name "group_file".') if not os.path.exists(group_file_name): raise TracError('Group filename not found: %s.' % group_file_name) return(group_file_name) def _get_groups_and_members(self): """ Get the groups and their members as a dictionary of lists. """ # could be in one of two places, depending if the # account-manager is installed or not group_file_name = self._group_filename() groups_dict = dict() group_file = file(group_file_name) try: for group_line in group_file: # Ignore blank lines and lines starting with # group_line = group_line.strip() if group_line and not group_line.startswith('#'): group_name = group_line.split(':', 1)[0] group_members = group_line.split(':', 2)[1].split(' ') groups_dict[group_name] = [ x for x in [member.strip() for member in group_members] if x ] finally: group_file.close() if len(groups_dict): return groups_dict else: return None def _write_groups_file(self, entries): """ Write the groups and members to the groups file """ group_file = open(self._group_filename(), 'w') for group_name in entries.keys(): group_file.write(group_name + ': ' + ' '.join(entries[group_name]) + '\n') group_file.close() def _check_for_finegrained(self): """ Check if the fine grained permission system is installed """ return (self.config.getbool('components', 'authzpolicy.authz_policy.authzpolicy') or self.config.getbool('components', 'authz_policy.authzpolicy') ) def _check_for_svnauthz(self): """ Check if the SVN Authz Plugin is installed """ return self.config.getbool('components','svnauthz.svnauthz.svnauthzplugin') def _update_fine_grained(self, group_details): #import ConfigObj authz_policy_file_name = self._get_filename('authz_policy', 'authz_file') authz_policy_dict = ConfigObj(authz_policy_file_name) # If there isn't a group file, don't destroy the existing entries if (group_details): authz_policy_dict['groups'] = group_details authz_policy_dict.write() def _update_svnauthz(self, group_details): svnauthz_policy_file_name = self._get_filename('trac','authz_file') svnauthz_policy_dict = ConfigObj(svnauthz_policy_file_name) # If there isn't a group file, don't destroy the existing entries if (group_details): svnauthz_policy_dict['groups'] = group_details svnauthz_policy_dict.write() def render_admin_panel(self, req, cat, page, path_info): """ Render up the panel. When applying deletions and additions, additions happen post deletions, so additions in effect have a higher precedence. The way it is done, it shouldn't be possible to have both """ req.perm.require('TRAC_ADMIN') add_stylesheet(req, 'ge/css/htgroupeditor.css') page_args = {} group_details = self._get_groups_and_members() # This option needs to be set if the admin can add and delete groups # usage in trac.ini: # [htgroupedit] # allowgroupedit = enabled allowgroupedit = self.config.getbool("htgroupeditor","allowgroupedit") # For ease of understanding and future reading # being done in the ORDER displayed. if not req.method == 'POST': groups_list = ['']; if group_details is not None: groups_list = groups_list + group_details.keys() page_args['groups_list'] = groups_list page_args['allowgroupedit'] = allowgroupedit return 'htgroupeditor.html', page_args else: if req.args.get('new_group') and group_details is None: # It can only happen for new_group, that group_details is None groups_list = [] group_details = {} else: groups_list = group_details.keys() if req.args.get('new_group') and allowgroupedit: # Create new group and select it if not req.args.get('new_group_name') or len(str(req.args.get('new_group_name')).strip()) == 0: raise TracError("Group name was empty") group_name = str(req.args.get('new_group_name')).strip() if group_name in group_details.keys(): raise TracError("Group already exists") group_details[group_name] = [] groups_list.append(group_name) elif req.args.get('delete_group') and allowgroupedit: # Deleting a group involves removing it from list and details delete_group_name = str(req.args.get('group_name')).strip() if not delete_group_name in groups_list: raise TracError("Invalid group for deletion") del group_details[delete_group_name] groups_list.remove(delete_group_name) if len(groups_list) == 0: # In case that was the last group, the subsequent won't work # Thus we have to do the writing of files here and return the empty page self._write_groups_file(group_details) if req.args.get('finegrained_check'): self._update_fine_grained(group_details) if req.args.get('svnauthz_check'): self._update_svnauthz(group_details) # Finally send the empty page page_args['groups_list'] = [''] page_args['allowgroupedit'] = allowgroupedit return 'htgroupeditor.html', page_args # Select first group in list group_name = groups_list[0] else: # Select group based on the request group_name = str(req.args.get('group_name')) # put the selected entry at the top of the list groups_list.remove(group_name) groups_list.insert(0, group_name) # Get rid of duplicates users_list = list() for name in group_details[group_name]: if name not in users_list: users_list.append(name) group_details[group_name] = sorted(users_list) if req.args.get('deletions'): deletions = req.args.get('deletions') # if only on entry it will be a string, so need to make it a list if not isinstance(deletions, list): deletions = [deletions] for deletion in deletions: # In case there arer multiple entries while deletion in group_details[group_name]: group_details[group_name].remove(deletion) if req.args.get('additional_names'): additional_names = req.args.get('additional_names') if not isinstance(additional_names, list): additional_names = [additional_names] # If a reload is done after an add, a duplicate can be created. for name in additional_names: if name not in group_details[group_name]: group_details[group_name].append(name) # get the list of users not in the group addable_usernames = [] for username in self.account_manager.get_users(): username = username.strip() if len(username) and not username in group_details[group_name]: addable_usernames.append(username) group_details[group_name].sort() addable_usernames.sort() page_args['groups_list'] = groups_list page_args['users_list'] = group_details[group_name] page_args['addable_usernames'] = addable_usernames page_args['group_name'] = group_name page_args['finegrained'] = self._check_for_finegrained() page_args['svnauthz'] = self._check_for_svnauthz() page_args['allowgroupedit'] = allowgroupedit if req.args.get('apply_changes') or req.args.get('new_group') or req.args.get('delete_group'): self._write_groups_file(group_details) # update the fine grained permissions, if it is installed if req.args.get('finegrained_check'): self._update_fine_grained(group_details) if req.args.get('svnauthz_check'): self._update_svnauthz(group_details) return 'htgroupeditor.html', page_args
#!/usr/bin/env python import os import sys from trac.env import Environment from acct_mgr.api import AccountManager env = Environment(sys.argv[1]) mgr = AccountManager(env) mgr.set_password(sys.argv[2], sys.argv[3])
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)
def expand_macro(self, formatter, name, content): env = formatter.env req = formatter.req if not content: args = [] kw = {} else: args, kw = parse_args(content) if name == 'ProjectStats': if 'wiki' in kw.keys(): prefix = 'prefix' in kw.keys() and kw['prefix'] or None wiki = WikiSystem(env) if kw['wiki'] == 'count' or 'count' in args: return tag(len(list(wiki.get_pages(prefix)))) elif name == 'UserQuery': msg_no_perm = tag.p(tag_("(required %(perm)s missing)", perm=tag.strong('USER_VIEW')), class_='hint') if 'perm' in kw.keys(): perm_sys = PermissionSystem(self.env) users = perm_sys.get_users_with_permission(kw['perm'].upper()) else: acct_mgr = AccountManager(env) users = list(set(acct_mgr.get_users())) if 'locked' in kw.keys() or 'locked' in args: guard = AccountGuard(env) locked = [] for user in users: if guard.user_locked(user): locked.append(user) if kw.get('locked', 'True').lower() in ('true', 'yes', '1'): users = locked else: users = list(set(users) - set(locked)) elif 'visit' in kw.keys() or 'visit' in args: if 'USER_VIEW' not in req.perm: return msg_no_perm cols = [] data = {'accounts': fetch_user_data(env, req), 'cls': 'wiki'} for col in ('email', 'name'): if col in args: cols.append(col) data['cols'] = cols return Chrome(env).render_template( req, 'user_table.html', data, 'text/html', True) if kw.get('format') == 'count' or 'count' in args: return tag(len(users)) if 'USER_VIEW' not in req.perm: return msg_no_perm if 'email' in args or 'name' in args: # Replace username with full name, add email if available. for username, name, email in self.env.get_known_users(): if username in users: if 'name' not in args or name is None: name = username if 'email' in args and email is not None: email = ''.join(['<', email, '>']) name = ' '.join([name, email]) if not username == name: users.pop(users.index(username)) users.append(name) if not users and 'nomatch' in kw.keys(): return format_to_oneliner(env, formatter.context, kw['nomatch']) users = sorted(users) if kw.get('format') == 'list': return tag.ul([tag.li(Chrome(env).format_author(req, user)) for user in users]) else: # Default output format: comma-separated list. return tag(', '.join([Chrome(env).format_author(req, user) for user in users]))
def __init__(self): self.acctmgr = AccountManager(self.env) self._enable_check(log=True)
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, 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(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)
def _create_user(req, env, check_permissions=True): acctmgr = AccountManager(env) username = acctmgr.handle_username_casing( req.args.get('username').strip()) name = req.args.get('name') email = req.args.get('email').strip() account = {'username' : username, 'name' : name, 'email' : email, } error = TracError('') error.account = account if not username: error.message = _("Username cannot be empty.") raise error # Prohibit some user names that are important for Trac and therefor # reserved, even if they're not in the permission store for some reason. if username in ['authenticated', 'anonymous']: error.message = _("Username %s is not allowed.") % username raise error # NOTE: A user may exist in the password store but not in the permission # store. I.e. this happens, when the user (from the password store) # never logged in into Trac. So we have to perform this test here # and cannot just check for the user being in the permission store. # And obfuscate whether an existing user or group name # was responsible for rejection of this user name. if acctmgr.has_user(username): error.message = _( "Another account or group named %s already exists.") % username raise error # Check whether there is also a user or a group with that name. if check_permissions: # NOTE: We can't use 'get_user_permissions(username)' here # as this always returns a list - even if the user doesn't exist. # In this case the permissions of "anonymous" are returned. # # Also note that we can't simply compare the result of # 'get_user_permissions(username)' to some known set of permission, # i.e. "get_user_permissions('authenticated') as this is always # false when 'username' is the name of an existing permission group. # # And again obfuscate whether an existing user or group name # was responsible for rejection of this username. for (perm_user, perm_action) in \ perm.PermissionSystem(env).get_all_permissions(): if perm_user == username: error.message = _( "Another account or group named %s already exists.") \ % username raise error # Always exclude some special characters, i.e. # ':' can't be used in HtPasswdStore # '[' and ']' can't be used in SvnServePasswordStore blacklist = acctmgr.username_char_blacklist if containsAny(username, blacklist): pretty_blacklist = '' for c in blacklist: if pretty_blacklist == '': pretty_blacklist = tag(' \'', tag.b(c), '\'') else: pretty_blacklist = tag(pretty_blacklist, ', \'', tag.b(c), '\'') error.message = tag(_( "The username must not contain any of these characters:"), pretty_blacklist) raise error # Validation of username passed. password = req.args.get('password') if not password: error.message = _("Password cannot be empty.") raise error if password != req.args.get('password_confirm'): error.message = _("The passwords must match.") raise error # Validation of password passed. if if_enabled(EmailVerificationModule) and acctmgr.verify_email: if not email: error.message = _("You must specify a valid email address.") raise error elif not re.match('^[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', email, re.IGNORECASE): error.message = _("""The email address specified appears to be invalid. Please specify a valid email address. """) raise error elif acctmgr.has_email(email): error.message = _("""The email address specified is already in use. Please specify a different one. """) raise error # Validation of email address passed. acctmgr.set_password(username, password) # INSERT new sid, needed as foreign key in some db schemata later on, # at least for PostgreSQL. db = env.get_db_cnx() cursor = db.cursor() cursor.execute(""" SELECT COUNT(*) FROM session WHERE sid=%s """, (username,)) exists = cursor.fetchone() if not exists: cursor.execute(""" INSERT INTO session (sid,authenticated,last_visit) VALUES (%s,0,0) """, (username,)) for attribute in ('name', 'email'): value = req.args.get(attribute) if not value: continue set_user_attribute(env, username, attribute, value)
class ComponentPermissionsPolicy(Component): """ This component provides permissions based on ticket components for Trac. """ implements(IPermissionRequestor, IPermissionPolicy, IRequestFilter) ticket_field_name = Option('component-permissions', 'ticket_field_name', '', """The name of the field which should be checked to see if the component permission is required. If not defined or empty, component permission is always required.""") allow_reporter = BoolOption('component-permissions', 'allow_reporter', 'false', """"Whether the reporter of a ticket should have access to that ticket even if they do not have COMPONENT_VIEW or COMPONENT_*_VIEW privileges.""") allow_cc = BoolOption('component-permissions', 'allow_cc', 'false', """Whether users listed in the cc field of a ticket should have access to that ticket even if they do not have COMPONENT_VIEW or COMPONENT_*_VIEW privileges.""") allow_owner = BoolOption('component-permissions', 'allow_owner', 'false', """Whether the owner of a ticket should have access to that ticket even if they do not have COMPONENT_VIEW or COMPONENT_*_VIEW privileges.""") allow_cc_email = BoolOption('component-permissions', 'allow_cc_email', 'false', """Whether users with their e-mail listed in the cc field of a ticket should have access to that ticket even if they do not have COMPONENT_VIEW or COMPONENT_*_VIEW privileges. Make sure e-mail is verified and cannot be freely changed.""") hide_components = BoolOption('component-permissions', 'hide_components', 'false', """Whether components the user does not have permissions for should be hidden.""") def __init__(self): self.account_manager = None try: from acct_mgr.api import AccountManager self.account_manager = AccountManager(self.env) except ImportError: pass # IPermissionRequestor methods def _get_permission_name(self, component): name = re.sub('[^a-zA-Z0-9]+', '_', component).strip('_').upper() if name: return 'COMPONENT_%s_VIEW' % (name,) else: return None def _get_email(self, username): cnx = self.env.get_db_cnx() cursor = cnx.cursor() cursor.execute("""SELECT DISTINCT e.value FROM session AS s LEFT JOIN session_attribute AS e ON (e.sid=s.sid AND e.authenticated=1 AND e.name = 'email') WHERE s.authenticated=1 AND s.sid=%s""", (username,)) for email, in cursor: return email return None def _get_bypass(self, ticket, username): if not username or username == 'anonymous': return False if self.allow_owner and ticket['owner'] == username: return True if self.allow_reporter and ticket['reporter'] == username: return True if not self.allow_cc and not self.allow_cc_email: return False cc_list = [user for user in NotifyEmail.addrsep_re.split(ticket['cc']) if user] if self.allow_cc and username in cc_list: return True if self.allow_cc_email: email = self._get_email(username) if email and email in cc_list: if self.account_manager: if self.account_manager.email_verified(username, email): return True else: return True return False def get_permission_actions(self): """Return a list of actions defined by this component.""" permissions = ['COMPONENT_VIEW'] for component in model.Component.select(self.env): permission = self._get_permission_name(component.name) if permission: permissions.append(permission) return permissions # IPermissionPolicy methods def check_permission(self, action, username, resource, perm): """Check that the action can be performed by username on the resource.""" # To prevent recursion if action in self.get_permission_actions(): return # To prevent recursion when used together with sensitive tickets if action == 'SENSITIVE_VIEW': return # Check whether we're dealing with a ticket resource while resource: if resource.realm == 'ticket': break resource = resource.parent if resource and resource.realm == 'ticket' and resource.id is not None: component_permission = 'COMPONENT_VIEW' # Default just to make check logic simpler bypass = False try: ticket = model.Ticket(self.env, int(resource.id)) should_check_permissions = not self.ticket_field_name or ticket.values.get(self.ticket_field_name, 0) if as_bool(should_check_permissions): if 'component' in ticket.values and ticket['component'] and self._get_permission_name(ticket['component']) in self.get_permission_actions(): component_permission = self._get_permission_name(ticket['component']) bypass = self._get_bypass(ticket, username) except ResourceNotFound: should_check_permissions = 1 # Fail safe to prevent a race condition if as_bool(should_check_permissions): if component_permission not in perm and 'COMPONENT_VIEW' not in perm and not bypass: return False # IRequestFilter methods def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, content_type): if self.hide_components and not self.ticket_field_name and 'COMPONENT_VIEW' not in req.perm and template in ['ticket_box.html', 'ticket.html', 'ticket_preview.html', 'query.html']: objects = [] if data.get('fields', None): objects.append(data['fields'].values() if hasattr(data['fields'], 'values') else data['fields']) if req.chrome.get('script_data', None) and req.chrome['script_data'].get('properties', None): properties = [] for name, prop in req.chrome['script_data']['properties'].items(): prop['name'] = name properties.append(prop) objects.append(properties) for obj in objects: for field in obj: if field['name'] == 'component': field['options'] = [component for component in field['options'] if self._get_permission_name(component) in req.perm] break query = data.get('query', None) if query and query.group == 'component' and 'groups' in data: groups = [] for (component, tickets) in data['groups']: if self._get_permission_name(component) in req.perm: groups.append((component, tickets)) data['groups'] = groups return (template, data, content_type)
def __init__(self): self.account_manager = AccountManager(self.env)
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