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."))
class HtDigestStore(AbstractPasswordFileStore): """Manages user accounts stored in Apache's htdigest format. To use this implementation add the following configuration section to trac.ini: {{{ [account-manager] password_store = HtDigestStore htdigest_file = /path/to/trac.htdigest htdigest_realm = TracDigestRealm }}} """ implements(IPasswordStore) filename = EnvRelativePathOption( 'account-manager', 'htdigest_file', '', doc=N_("""Path relative to Trac environment or full host machine path to password file""")) realm = Option('account-manager', 'htdigest_realm', '', doc=N_("Realm to select relevant htdigest file entries")) def config_key(self): return 'htdigest' def prefix(self, user): return '%s:%s:' % (user, self.realm.encode('utf-8')) def userline(self, user, password): return self.prefix(user) + htdigest(user, self.realm.encode('utf-8'), password) def _check_userline(self, user, password, suffix): return suffix == htdigest(user, self.realm.encode('utf-8'), password) def _get_users(self, filename): _realm = self.realm.encode('utf-8') f = open(filename) for line in f: args = line.split(':')[:2] if len(args) == 2: user, realm = args if realm == _realm and user: yield user.decode('utf-8')
class HttpAuthStore(Component): implements(IPasswordStore) auth_url = Option('account-manager', 'authentication_url', '', doc=N_("URL of the HTTP authentication service")) def check_password(self, user, password): acctmgr = HTTPPasswordMgrWithDefaultRealm() acctmgr.add_password(None, self.auth_url, user, password) try: build_opener(HTTPBasicAuthHandler(acctmgr), HTTPDigestAuthHandler(acctmgr)).open(self.auth_url) except IOError: return None except ValueError: return None else: return True def get_users(self): return [] def has_user(self, user): return False
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) )
class HtPasswdStore(AbstractPasswordFileStore): """Manages user accounts stored in Apache's htpasswd format. To use this implementation add the following configuration section to trac.ini: {{{ [account-manager] password_store = HtPasswdStore htpasswd_file = /path/to/trac.htpasswd htpasswd_hash_type = crypt|md5|sha|sha256|sha512 <- None or one of these }}} Default behaviour is to detect presence of 'crypt' and use it or fallback to generation of passwords with md5 hash otherwise. """ implements(IPasswordStore) filename = EnvRelativePathOption( 'account-manager', 'htpasswd_file', '', doc=N_("""Path relative to Trac environment or full host machine path to password file""")) hash_type = Option('account-manager', 'htpasswd_hash_type', 'crypt', doc=N_("Default hash type of new/updated passwords")) def config_key(self): return 'htpasswd' def prefix(self, user): return user + ':' def userline(self, user, password): return self.prefix(user) + mkhtpasswd(password, self.hash_type) def _check_userline(self, user, password, suffix): return suffix == htpasswd(password, suffix) def _get_users(self, filename): f = open(filename, 'rU') for line in f: user = line.split(':', 1)[0] if user: yield user.decode('utf-8')
def validate_registration(self, req): acctmgr = AccountManager(self.env) email = req.args.get('email', '').strip() if is_enabled(self.env, EmailVerificationModule) and \ EmailVerificationModule(self.env).verify_email: # Initial configuration case. if not email and not req.args.get('active'): raise RegistrationError( N_("You must specify a valid email address.")) # User preferences case. elif req.path_info == '/prefs' and email == req.session.get( 'email'): return elif email_associated(self.env, email): raise RegistrationError( N_("The email address specified is already in use. " "Please specify a different one."))
def validate_registration(self, req): # Input must be an exact replication of the required token. basic_token = req.args.get('basic_token', '') # Unlike the former, the hidden bot-trap input field must stay empty. keep_empty = req.args.get('sentinel', '') if keep_empty or self.reg_basic_token and \ self.reg_basic_token != basic_token: raise RegistrationError(N_("Are you human? If so, try harder!"))
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 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), '\'') 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( N_("Another account or group already exists, who's name " "differs from %s only by case or is identical."), 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."))
class SvnServePasswordStore(Component): """PasswordStore implementation for reading svnserve's password file format """ implements(IPasswordStore) filename = EnvRelativePathOption( 'account-manager', 'password_file', doc=N_("""Path to the users file; leave blank to locate the users file by reading svnserve.conf""")) def __init__(self): repo_dir = RepositoryManager(self.env).repository_dir self._svnserve_conf = Configuration( os.path.join(os.path.join(repo_dir, 'conf'), 'svnserve.conf')) self._userconf = None def _config(self): filename = self.filename if not filename: self._svnserve_conf.parse_if_needed() filename = self._svnserve_conf['general'].getpath('password-db') if self._userconf is None or filename != self._userconf.filename: self._userconf = Configuration(filename) # Overwrite default with str class to preserve case. self._userconf.parser.optionxform = str self._userconf.parse_if_needed(force=True) else: self._userconf.parse_if_needed() return self._userconf _config = property(_config) # IPasswordStore methods def get_users(self): return [user for (user, password) in self._config.options('users')] def has_user(self, user): return user in self._config['users'] def set_password(self, user, password, old_password=None): cfg = self._config cfg.set('users', user, password) cfg.save() def check_password(self, user, password): if self.has_user(user): return password == self._config.get('users', user) return None def delete_user(self, user): cfg = self._config cfg.remove('users', user) cfg.save()
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 __init__(self, message, *args, **kwargs): """TracError sub-class with extended i18n support. It eases error initialization with messages optionally including arguments meant for string substitution after deferred translation. """ title = N_("Registration Error") tb = 'show_traceback' # Care for the 2nd TracError standard keyword argument only. show_traceback = tb in kwargs and kwargs.pop(tb, False) TracError.__init__(self, message, title, show_traceback) self.msg_args = args
class HtDigestHashMethod(Component): implements(IPasswordHashMethod) realm = Option('account-manager', 'db_htdigest_realm', '', doc=N_("Realm to select relevant htdigest db entries")) def generate_hash(self, user, password): user, password, realm = _encode(user, password, self.realm) return ':'.join([realm, htdigest(user, realm, password)]) def check_hash(self, user, password, hash): return hash == self.generate_hash(user, password)
class HtPasswdHashMethod(Component): implements(IPasswordHashMethod) hash_type = Option('account-manager', 'db_htpasswd_hash_type', 'crypt', doc=N_("Default hash type of new/updated passwords")) def generate_hash(self, user, password): password = password.encode('utf-8') return mkhtpasswd(password, self.hash_type) def check_hash(self, user, password, hash): password = password.encode('utf-8') hash2 = htpasswd(password, hash) return hash == hash2
class HttpAuthStore(Component): implements(IPasswordStore) auth_url = Option('account-manager', 'authentication_url', '', doc=N_("URL of the HTTP authentication service")) def check_password(self, username, password): self.log.debug("Trac.ini authentication_url = '%s'" % self.auth_url) # Nothing to do, if URL is absolute. if self.auth_url.startswith('http://') or \ self.auth_url.startswith('https://'): authUrl = self.auth_url # Handle server-relative URLs. elif self.auth_url.startswith('/'): # Prepend the Trac server component. pr = urlparse(self.env.abs_href()) href = Href(pr[0] + '://' + pr[1]) authUrl = href(self.auth_url) elif '/' in self.auth_url: # URLs with path like 'common/authFile' or 'site/authFile'. authUrl = self.env.abs_href.chrome(self.auth_url) else: # Bare file name option value like 'authFile'. authUrl = self.env.abs_href.chrome('common', self.auth_url) self.log.debug("Final auth_url = '%s'" % authUrl) acctmgr = HTTPPasswordMgrWithDefaultRealm() acctmgr.add_password(None, authUrl, username, password) try: build_opener(HTTPBasicAuthHandler(acctmgr), HTTPDigestAuthHandler(acctmgr)).open(authUrl) except IOError, e: if hasattr(e, 'code') and e.code == 404: self.log.debug("""HttpAuthStore page not found; we are authenticated nonetheless""") return True if hasattr(e, 'code') and e.code == 401: self.log.debug("HttpAuthStore authentication failed") return None except ValueError, e: self.log.debug("""HttpAuthStore: 'authentication_url' specifies an invalid URL""") return None
class SessionStore(Component): implements(IPasswordStore) hash_method = ExtensionOption('account-manager', 'hash_method', IPasswordHashMethod, 'HtDigestHashMethod', doc = N_("IPasswordHashMethod used to create new/updated passwords")) def __init__(self): self.key = 'password' # Check for valid hash method configuration. self.hash_method_enabled def get_users(self): """Returns an iterable of the known usernames.""" db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute(""" SELECT DISTINCT sid FROM session_attribute WHERE authenticated=1 AND name=%s """, (self.key,)) for sid, in cursor: yield sid def has_user(self, user): db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute(""" SELECT * FROM session_attribute WHERE authenticated=1 AND name=%s AND sid=%s """, (self.key, user)) for row in cursor: return True return False def set_password(self, user, password, old_password=None): """Sets the password for the user. This should create the user account, if it doesn't already exist. Returns True, if a new account was created, and False, if an existing account was updated. """ if not self.hash_method_enabled: return hash = self.hash_method.generate_hash(user, password) db = self.env.get_db_cnx() cursor = db.cursor() sql = """ WHERE authenticated=1 AND name=%s AND sid=%s """ cursor.execute(""" UPDATE session_attribute SET value=%s """ + sql, (hash, self.key, user)) cursor.execute(""" SELECT value FROM session_attribute """ + sql, (self.key, user)) not_exists = cursor.fetchone() is None if not_exists: cursor.execute(""" INSERT INTO session_attribute (sid,authenticated,name,value) VALUES (%s,1,%s,%s) """, (user, self.key, hash)) db.commit() return not_exists def check_password(self, user, password): """Checks if the password is valid for the user.""" if not self.hash_method_enabled: return db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute(""" SELECT value FROM session_attribute WHERE authenticated=1 AND name=%s AND sid=%s """, (self.key, user)) for hash, in cursor: return self.hash_method.check_hash(user, password, hash) # Return value 'None' allows to proceed with another, chained store. return def delete_user(self, user): """Deletes the user account. Returns True, if the account existed and was deleted, False otherwise. """ db = self.env.get_db_cnx() cursor = db.cursor() sql = """ WHERE authenticated=1 AND name=%s AND sid=%s """ # Avoid has_user() to make this transaction atomic. cursor.execute(""" SELECT * FROM session_attribute """ + sql, (self.key, user)) exists = cursor.fetchone() is not None if exists: cursor.execute(""" DELETE FROM session_attribute """ + sql, (self.key, user)) db.commit() return exists @property def hash_method_enabled(self): try: hash_method = self.hash_method except AttributeError: self.env.log.error("%s: no IPasswordHashMethod enabled " "- fatal, can't work" % self.__class__) return return 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())
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())
class AbstractPasswordFileStore(Component): """Base class for managing password files. Derived classes support different formats such as Apache's htpasswd and htdigest format. See these concrete sub-classes for usage information. """ abstract = True # DEVEL: This option is subject to removal after next major release. filename = EnvRelativePathOption( 'account-manager', 'password_file', '', doc=N_("""Path relative to Trac environment or full host machine path to password file""")) def has_user(self, user): return user in self.get_users() def get_users(self): filename = str(self.filename) if not os.path.exists(filename): self.log.error('acct_mgr: get_users() -- ' 'Can\'t locate password file "%s"' % filename) return [] return self._get_users(filename) def set_password(self, user, password, old_password=None): user = user.encode('utf-8') password = password.encode('utf-8') return not self._update_file(self.prefix(user), self.userline(user, password)) def delete_user(self, user): user = user.encode('utf-8') return self._update_file(self.prefix(user), None) def check_password(self, user, password): filename = str(self.filename) if not os.path.exists(filename): self.log.error('acct_mgr: check_password() -- ' 'Can\'t locate password file "%s"' % filename) return False user = user.encode('utf-8') password = password.encode('utf-8') prefix = self.prefix(user) try: f = open(filename, 'rU') for line in f: if line.startswith(prefix): return self._check_userline( user, password, line[len(prefix):].rstrip('\n')) # DEVEL: Better use new 'finally' statement here, but # still need to care for Python 2.4 (RHEL5.x) for now except: self.log.error('acct_mgr: check_password() -- ' 'Can\'t read password file "%s"' % filename) pass if isinstance(f, file): f.close() return None def _update_file(self, prefix, userline): """Add or remove user and change password. If `userline` is empty, the line starting with `prefix` is removed from the user file. Otherwise the line starting with `prefix` is updated to `userline`. If no line starts with `prefix`, the `userline` is appended to the file. Returns `True` if a line matching `prefix` was updated, `False` otherwise. """ filename = str(self.filename) matched = False new_lines = [] try: # Open existing file read-only to read old content. # DEVEL: Use `with` statement available in Python >= 2.5 # as soon as we don't need to support 2.4 anymore. eol = '\n' f = open(filename, 'r') lines = f.readlines() # DEVEL: Beware, in shared use there is a race-condition, # since file changes by other programs that occure from now on # are currently not detected and will get overwritten. # This could be fixed by file locking, but a cross-platform # implementation is certainly non-trivial. # DEVEL: I've seen the AtomicFile object in trac.util lately, # that may be worth a try. if len(lines) > 0: # predict eol style for lines without eol characters if not os.linesep == '\n': if lines[-1].endswith('\r') and os.linesep == '\r': # antique MacOS newline style safeguard # DEVEL: is this really still needed? eol = '\r' elif lines[-1].endswith('\r\n') and os.linesep == '\r\n': # Windows newline style safeguard eol = '\r\n' for line in lines: if line.startswith(prefix): if not matched and userline: new_lines.append(userline + eol) matched = True # preserve existing lines with proper eol elif line.endswith(eol) and not \ (eol == '\n' and line.endswith('\r\n')): new_lines.append(line) # unify eol style using confirmed default and # make sure the (last) line has a newline anyway else: new_lines.append(line.rstrip('\r\n') + eol) except EnvironmentError, e: if e.errno == errno.ENOENT: # Ignore, when file doesn't exist and create it below. pass elif e.errno == errno.EACCES: raise TracError( _("""The password file could not be read. Trac requires read and write access to both the password file and its parent directory.""")) else: raise # Finally add the new line here, if it wasn't used before # to update or delete a line, creating content for a new file as well. if not matched and userline: new_lines.append(userline + eol) # Try to (re-)open file write-only now and save new content. try: f = open(filename, 'w') f.writelines(new_lines) except EnvironmentError, e: if e.errno == errno.EACCES or e.errno == errno.EROFS: raise TracError( _("""The password file could not be updated. Trac requires read and write access to both the password file and its parent directory.""")) else: raise