Commits

Leho Kraav committed c242ea3

trac:#2456 patches from wkornewald 4 years ago

  • Participants
  • Parent commits 460f516

Comments (0)

Files changed (7)

t2456-wkornewald-4y-back/user.1.py

+# -*- coding: utf-8 -*-
+#
+# Copyright 2006 Waldemar Kornewald, wkornewald@haiku-os.org
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
+#
+# Author: Waldemar Kornewald, wkornewald@haiku-os.org
+
+from trac.core import *
+from trac.config import *
+
+
+class IUserStore(Interface):
+    """
+    Extension point interface for backends that store known users.
+    """
+
+    def supports_user_operation(self, operation):
+        """
+        Returns whether the operation (a method name) is supported.
+
+        @return supported
+        """
+        return hasattr(self, operation)
+
+    def create_user(self, username, password):
+        """
+        Creates a new user with the given username and password.
+
+        @return success
+        """
+
+    def get_users(self):
+        """
+        Generator that yields an ordered list of known users.
+
+        @return username
+        """
+
+    def has_user(self, username):
+        """
+        Returns whether the user exists.
+        """
+        return username in self.get_users()
+
+    def check_password(self, username, password):
+        """
+        Checks if the password is correct for the given user.
+        """
+
+    def change_password(self, username, password):
+        """
+        Changes a user's password.
+
+        @return success
+        """
+
+    def delete_user(self, username):
+        """
+        Deletes a user.
+
+        @return success
+            Returns False if the user didn't exist.
+        """
+
+
+class IUserAttributeProvider(Interface):
+    """
+    Extension point interface for backends that store user attributes.
+    """
+
+    def supports_attribute_operation(self, operation):
+        """
+        Returns whether the operation (a method name) is supported.
+
+        @return supported
+        """
+        return hasattr(self, operation)
+
+    def get_user_attribute(self, username, attribute):
+        """
+        Returns a user attribute.
+
+        If the attribute is not set it returs an empty string.
+        If the attribute is not supported None is returned.
+        """
+
+    def set_user_attribute(self, username, attribute, value):
+        """
+        Sets a user attribute. If value is None the attribute gets deleted.
+
+        @return success
+            Returns False if setting the attribute is not supported.
+        """
+
+    def delete_all_user_attributes(self, username):
+        """
+        Deletes all of the given user's attributes.
+
+        @return success
+        """
+
+
+class UserManager(Component):
+    """
+    Component responsible for managing users and user attributes.
+    """
+
+    store = ExtensionOption('users',
+        'store', IUserStore, 'SessionUserStore',
+        doc="""The user store that should be used for authentication
+            (''since 0.11'').""")
+    attribute_providers = OrderedExtensionsOption('users',
+        'attribute_providers', IUserAttributeProvider,
+        doc="""Ordered list of user attribute providers (''since 0.11'').""")
+
+    # general methods
+
+    def supports_operation(self, operation):
+        if store.supports_user_operation(operation):
+            return True
+        for provider in providers:
+            if provider.supports_attribute_operation(operation):
+                return True
+        return False
+
+    # IUserStore methods
+
+    def create_user(self, username, password):
+        if not store.supports_user_operation('create_user'):
+            return False
+        return self.store.create_user(username, password)
+
+    def get_users(self):
+        if not store.supports_user_operation('get_users'):
+            return []
+        return self.store.get_users()
+
+    def has_user(self, username):
+        if not store.supports_user_operation('has_user'):
+            return False
+        return self.store.has_user(username)
+
+    def check_password(self, username, password):
+        if not store.supports_user_operation('check_password'):
+            return False
+        return self.store.check_password(username, password)
+
+    def change_password(self, username, password):
+        if not store.supports_user_operation('change_password'):
+            return False
+        return self.store.change_password(username, password)
+
+    def delete_user(self, username):
+        if not store.supports_user_operation('delete_user'):
+            return False
+        return self.store.delete_user(username)
+
+    # IUserAttributeProvider methods
+
+    def get_user_attribute(self, username, attribute):
+        for provider in self.attribute_providers:
+            if not provider.supports_attribute_operation('get_user_attribute'):
+                continue
+            value = provider.get_user_attribute(username, attribute)
+            if value is not None:
+                return value
+        return None
+
+    def set_user_attribute(self, username, attribute, value):
+        for provider in self.attribute_providers:
+            if not provider.supports_attribute_operation('set_user_attribute'):
+                continue
+            result = provider.set_user_attribute(username, attribute, value)
+            if result:
+                return True
+        return False
+
+    def delete_all_user_attributes(self, username):
+        for provider in self.attribute_providers:
+            if not provider.supports_attribute_operation('delete_all_user_attributes'):
+                continue
+            result = provider.delete_all_user_attributes(username)
+            if result:
+                return True
+        return False
+
+
+class SessionUserStore(Component):
+    """
+    Component for managing authenticated users stored in sessions.
+    """
+
+    implements(IUserStore)
+
+    def get_users(self):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT sid FROM session "
+                       "WHERE authenticated=1 "
+                       "ORDER BY sid")
+        for row in cursor:
+            yield row[0]
+
+
+class SessionUserAttributeProvider(Component):
+    """
+    Component for providing user attributes via Trac sessions.
+    """
+
+    implements(IUserAttributeProvider)

t2456-wkornewald-4y-back/user.2.py

+# -*- coding: utf-8 -*-
+#
+# Copyright 2006 Waldemar Kornewald, wkornewald@haiku-os.org
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
+#
+# Author: Waldemar Kornewald, wkornewald@haiku-os.org
+
+from trac.core import *
+from trac.config import *
+
+
+class IUserStore(Interface):
+    """
+    Extension point interface for backends that store known users.
+    """
+
+    def supports_user_operation(self, operation):
+        """
+        Returns whether the operation (a method name) is supported.
+
+        @return supported
+        """
+        return hasattr(self, operation)
+
+    def create_user(self, username, password):
+        """
+        Creates a new user with the given username and password.
+
+        @return success
+        """
+
+    def get_usernames(self):
+        """
+        Generator that yields an ordered list of known usernames.
+
+        @return username
+        """
+
+    def check_password(self, username, password):
+        """
+        Checks if the password is correct for the given user.
+        """
+
+    def change_password(self, username, password):
+        """
+        Changes a user's password.
+
+        @return success
+        """
+
+    def delete_user(self, username):
+        """
+        Deletes a user.
+
+        @return success
+            Returns False if the user didn't exist.
+        """
+
+
+class IUserAttributeProvider(Interface):
+    """
+    Extension point interface for backends that store user attributes.
+    """
+
+    def supports_attribute_operation(self, operation):
+        """
+        Returns whether the operation (a method name) is supported.
+
+        @return supported
+        """
+        return hasattr(self, operation)
+
+    def get_user_attribute(self, username, attribute):
+        """
+        Returns a user attribute.
+
+        If the attribute is not set it returs an empty string.
+        If the attribute is not supported None is returned.
+        """
+
+    def set_user_attribute(self, username, attribute, value):
+        """
+        Sets a user attribute.
+
+        @return success
+            Returns False if setting the attribute is not supported.
+        """
+
+    def delete_all_user_attributes(self, username):
+        """
+        Deletes all of the given user's attributes.
+
+        @return success
+        """
+
+
+class UserManager(Component):
+    """
+    Component responsible for managing users and user attributes.
+    """
+
+    store = ExtensionOption('users',
+        'store', IUserStore, 'SessionUserStore',
+        doc="""The user store that should be used for authentication
+            (''since 0.11'').""")
+    attribute_providers = OrderedExtensionsOption('users',
+        'attribute_providers', IUserAttributeProvider,
+        doc="""Ordered list of user attribute providers (''since 0.11'').""")
+
+    # public API
+
+    def supports_operation(self, operation):
+        if self.store.supports_user_operation(operation):
+            return True
+        for provider in self.attribute_providers:
+            if self.provider.supports_attribute_operation(operation):
+                return True
+        return False
+
+    # IUserStore methods
+
+    def create_user(self, username, password):
+        if not self.store.supports_user_operation('create_user'):
+            return False
+        return self.store.create_user(username, password)
+
+    def get_user(self, username):
+        """
+        Returns a User object for the username.
+
+        @return User object or None if the user doesn't exist
+        """
+        if username in self.get_usernames():
+            return User(username, self.store, self.attribute_providers)
+        return None
+
+    def get_usernames(self):
+        if not self.store.supports_user_operation('get_usernames'):
+            return []
+        return self.store.get_usernames()
+
+
+
+class User(object):
+    """
+    Object representing a user.
+    """
+
+    def __init__(self, username, store, attribute_providers):
+        self._username = username
+        self.store = store
+        self.attribute_providers = attribute_providers
+
+    # public API
+
+    @property
+    def username():
+        return self._username
+
+    # IUserStore methods
+
+    def check_password(self, password):
+        if not self.store.supports_user_operation('check_password'):
+            return False
+        return self.store.check_password(self.username, password)
+
+    def change_password(self, password):
+        if not self.store.supports_user_operation('change_password'):
+            return False
+        return self.store.change_password(self.username, password)
+
+    def delete(self):
+        if not self.store.supports_user_operation('delete_user') or \
+                not self.delete_all_attributes(self.username):
+            return False
+        return self.store.delete_user(self.username)
+
+    # IUserAttributeProvider methods
+
+    def __getattr__(self, attribute):
+        for provider in self.attribute_providers:
+            if not provider.supports_attribute_operation('get_user_attribute'):
+                continue
+            value = provider.get_user_attribute(self.username, attribute)
+            if value is not None:
+                return value
+        return None
+
+    def __setattr__(self, attribute, value):
+        for provider in self.attribute_providers:
+            if not provider.supports_attribute_operation('set_user_attribute'):
+                continue
+            result = provider.set_user_attribute(self.username, attribute,
+                                                 value)
+            if result:
+                return True
+        return False
+
+    def delete_all_attributes(self):
+        for provider in self.attribute_providers:
+            if not provider.supports_attribute_operation('delete_all_user_attributes'):
+                continue
+            result = provider.delete_all_user_attributes(username)
+            if result:
+                return True
+        return False
+
+
+class SessionUserStore(Component):
+    """
+    Component for managing authenticated users stored in sessions.
+    """
+
+    implements(IUserStore)
+
+    def get_usernames(self):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT sid FROM session "
+                       "WHERE authenticated=1 "
+                       "ORDER BY sid")
+        for row in cursor:
+            yield row[0]
+
+
+class SessionUserAttributeProvider(Component):
+    """
+    Component for providing user attributes via Trac sessions.
+    """
+
+    implements(IUserAttributeProvider)
+
+    def get_user_attribute(self, username, attribute):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT value FROM session_attribute "
+                       "WHERE sid=%s AND name=%s AND authenticated=1",
+                       (username, attribute))
+        row = cursor.fetchone()
+        if row:
+            return row[0]
+
+        # if the attribute isn't set we return empty string to indicate
+        # that the attribute is supported, but empty
+        return ''
+
+    def set_user_attribute(self, username, attribute, value):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+
+        # check if attribute exists
+        cursor.execute("SELECT value FROM session_attribute "
+                       "WHERE sid=%s AND name=%s AND authenticated=1",
+                       (username, attribute))
+        if cursor.fetchone():
+            # update the attribute
+            cursor.execute("UPDATE session_attribute SET value=%s "
+                           "WHERE sid=%s AND name=%s AND authenticated=1",
+                           (username, attribute, value))
+        else:
+            # create new attribute
+            cursor.execute("INSERT INTO session_attribute "
+                           "(sid,authenticated,name,value) "
+                           "VALUES(%s,1,%s,%s)",
+                           (username, attribute, value))
+        db.commit()
+        return True
+
+    def delete_all_user_attributes(self, username):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("DELETE FROM session_attribute WHERE sid=%s",
+                      (username,))
+        db.commit()
+        return True

t2456-wkornewald-4y-back/user.3.py

+# -*- coding: utf-8 -*-
+#
+# Copyright 2006 Waldemar Kornewald, wkornewald@haiku-os.org
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.org/wiki/TracLicense.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://trac.edgewall.org/log/.
+#
+# Author: Waldemar Kornewald, wkornewald@haiku-os.org
+
+from trac.core import *
+from trac.config import *
+
+
+class IUserStore(Interface):
+    """
+    Extension point interface for backends that store known users.
+    """
+
+    def supports_user_operation(self, operation):
+        """
+        Returns whether the operation (a method name) is supported.
+
+        @return supported
+        """
+        return hasattr(self, operation)
+
+    def create_user(self, username, password):
+        """
+        Creates a new user with the given username and password.
+
+        @return success
+        """
+
+    def get_usernames(self):
+        """
+        Generator that yields an ordered list of known usernames.
+
+        @return username
+        """
+
+    def check_password(self, username, password):
+        """
+        Checks if the password is correct for the given user.
+        """
+
+    def change_password(self, username, password):
+        """
+        Changes a user's password.
+
+        @return success
+        """
+
+    def delete_user(self, username):
+        """
+        Deletes a user.
+
+        @return success
+            Returns False if the user didn't exist.
+        """
+
+
+class IUserAttributeProvider(Interface):
+    """
+    Extension point interface for backends that store user attributes.
+    """
+
+    def supports_attribute_operation(self, operation):
+        """
+        Returns whether the operation (a method name) is supported.
+
+        @return supported
+        """
+        return hasattr(self, operation)
+
+    def get_user_attribute(self, username, attribute):
+        """
+        Returns a user attribute.
+
+        If the attribute is not set it returs an empty string.
+        If the attribute is not supported None is returned.
+        """
+
+    def set_user_attribute(self, username, attribute, value):
+        """
+        Sets a user attribute.
+
+        @return success
+            Returns False if setting the attribute is not supported.
+        """
+
+    def delete_all_user_attributes(self, username):
+        """
+        Deletes all of the given user's attributes.
+
+        @return success
+        """
+
+
+class UserManager(Component):
+    """
+    Component responsible for managing users and user attributes.
+    """
+
+    store = ExtensionOption('users',
+        'store', IUserStore, 'SessionUserStore',
+        doc="""The user store that should be used for authentication
+            (''since 0.11'').""")
+    attribute_providers = OrderedExtensionsOption('users',
+        'attribute_providers', IUserAttributeProvider,
+        doc="""Ordered list of user attribute providers (''since 0.11'').""")
+
+    # public API
+
+    def supports_operation(self, operation):
+        if self.store.supports_user_operation(operation):
+            return True
+        for provider in self.attribute_providers:
+            if self.provider.supports_attribute_operation(operation):
+                return True
+        return False
+
+    # IUserStore methods
+
+    def create_user(self, username, password):
+        """
+        Creates a new user with the given username and password.
+
+        @return User object or None if the user couldn't be created
+        """
+        if not self.store.supports_user_operation('create_user'):
+            return None
+        if self.store.create_user(username, password):
+            return User(username, self.store, self.attribute_providers)
+
+    def get_user(self, username):
+        """
+        Returns a User object for the username.
+
+        @return User object or None if the user doesn't exist
+        """
+        if username in self.get_usernames():
+            return User(username, self.store, self.attribute_providers)
+        return None
+
+    def get_usernames(self):
+        if not self.store.supports_user_operation('get_usernames'):
+            return []
+        return self.store.get_usernames()
+
+
+
+class User(object):
+    """
+    Object representing a user.
+    """
+
+    def __init__(self, username, store, attribute_providers):
+        self._username = username
+        self.store = store
+        self.attribute_providers = attribute_providers
+
+    # public API
+
+    @property
+    def username():
+        return self._username
+
+    # IUserStore methods
+
+    def check_password(self, password):
+        if not self.store.supports_user_operation('check_password'):
+            return False
+        return self.store.check_password(self.username, password)
+
+    def change_password(self, password):
+        if not self.store.supports_user_operation('change_password'):
+            return False
+        return self.store.change_password(self.username, password)
+
+    def delete(self):
+        if not self.store.supports_user_operation('delete_user') or \
+                not self.delete_all_attributes(self.username):
+            return False
+        return self.store.delete_user(self.username)
+
+    # IUserAttributeProvider methods
+
+    def __getattr__(self, attribute):
+        for provider in self.attribute_providers:
+            if not provider.supports_attribute_operation('get_user_attribute'):
+                continue
+            value = provider.get_user_attribute(self.username, attribute)
+            if value is not None:
+                return value
+        return None
+
+    def __setattr__(self, attribute, value):
+        for provider in self.attribute_providers:
+            if not provider.supports_attribute_operation('set_user_attribute'):
+                continue
+            result = provider.set_user_attribute(self.username, attribute,
+                                                 value)
+            if result:
+                return True
+        return False
+
+    def delete_all_attributes(self):
+        for provider in self.attribute_providers:
+            if not provider.supports_attribute_operation('delete_all_user_attributes'):
+                continue
+            result = provider.delete_all_user_attributes(username)
+            if result:
+                return True
+        return False
+
+
+class SessionUserStore(Component):
+    """
+    Component for managing authenticated users stored in sessions.
+    """
+
+    implements(IUserStore)
+
+    def get_usernames(self):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT sid FROM session "
+                       "WHERE authenticated=1 "
+                       "ORDER BY sid")
+        for row in cursor:
+            yield row[0]
+
+
+class SessionUserAttributeProvider(Component):
+    """
+    Component for providing user attributes via Trac sessions.
+    """
+
+    implements(IUserAttributeProvider)
+
+    def get_user_attribute(self, username, attribute):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT value FROM session_attribute "
+                       "WHERE sid=%s AND name=%s AND authenticated=1",
+                       (username, attribute))
+        row = cursor.fetchone()
+        if row:
+            return row[0]
+
+        # if the attribute isn't set we return empty string to indicate
+        # that the attribute is supported, but empty
+        return ''
+
+    def set_user_attribute(self, username, attribute, value):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+
+        # check if attribute exists
+        cursor.execute("SELECT value FROM session_attribute "
+                       "WHERE sid=%s AND name=%s AND authenticated=1",
+                       (username, attribute))
+        if cursor.fetchone():
+            # update the attribute
+            cursor.execute("UPDATE session_attribute SET value=%s "
+                           "WHERE sid=%s AND name=%s AND authenticated=1",
+                           (username, attribute, value))
+        else:
+            # create new attribute
+            cursor.execute("INSERT INTO session_attribute "
+                           "(sid,authenticated,name,value) "
+                           "VALUES(%s,1,%s,%s)",
+                           (username, attribute, value))
+        db.commit()
+        return True
+
+    def delete_all_user_attributes(self, username):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("DELETE FROM session_attribute WHERE sid=%s",
+                      (username,))
+        db.commit()
+        return True

t2456-wkornewald-4y-back/user_API.2.diff

+Index: trac/env.py
+===================================================================
+--- trac/env.py	(revision 4456)
++++ trac/env.py	(working copy)
+@@ -23,6 +23,7 @@
+ from trac.core import Component, ComponentManager, implements, Interface, \
+                       ExtensionPoint, TracError
+ from trac.db import DatabaseManager
++from trac.user import UserManager, User
+ from trac.util import get_pkginfo
+ from trac.versioncontrol import RepositoryManager
+ from trac.web.href import Href
+@@ -283,29 +284,16 @@
+             logfile = os.path.join(self.get_log_dir(), logfile)
+         self.log = logger_factory(logtype, logfile, self.log_level, self.path)
+ 
+-    def get_known_users(self, cnx=None):
++    def get_known_users(self):
+         """Generator that yields information about all known users, i.e. users
+         that have logged in to this Trac environment and possibly set their name
+         and email.
+ 
+         This function generates one tuple for every user, of the form
+         (username, name, email) ordered alpha-numerically by username.
+-
+-        @param cnx: the database connection; if ommitted, a new connection is
+-                    retrieved
+         """
+-        if not cnx:
+-            cnx = self.get_db_cnx()
+-        cursor = cnx.cursor()
+-        cursor.execute("SELECT DISTINCT s.sid, n.value, e.value "
+-                       "FROM session AS s "
+-                       " LEFT JOIN session_attribute AS n ON (n.sid=s.sid "
+-                       "  and n.authenticated=1 AND n.name = 'name') "
+-                       " LEFT JOIN session_attribute AS e ON (e.sid=s.sid "
+-                       "  AND e.authenticated=1 AND e.name = 'email') "
+-                       "WHERE s.authenticated=1 ORDER BY s.sid")
+-        for username,name,email in cursor:
+-            yield username, name, email
++        for user in UserManager(self).get_all_users():
++            yield user.username, user['name'], user['email']
+ 
+     def backup(self, dest=None):
+         """Simple SQLite-specific backup of the database.
+Index: trac/prefs/web_ui.py
+===================================================================
+--- trac/prefs/web_ui.py	(revision 4456)
++++ trac/prefs/web_ui.py	(working copy)
+@@ -21,6 +21,7 @@
+ 
+ from trac.core import *
+ from trac.prefs.api import IPreferencePanelProvider
++from trac.user import UserManager, User
+ from trac.util.datefmt import all_timezones, utc
+ from trac.web import HTTPNotFound, IRequestHandler
+ from trac.web.chrome import add_stylesheet, INavigationContributor
+@@ -90,8 +91,16 @@
+                 self._do_save(req)
+             req.redirect(req.href.prefs(panel or None))
+ 
++        name = email = ''
++        if req.authname != 'anonymous':
++            user = UserManager(self.env).get_user(req.authname)
++            name, email = user['name'], user['email']
++        else:
++            name = req.session.get('name')
++            email = req.session.get('email')
+         return 'prefs_%s.html' % (panel or 'general'), {
+-            'settings': {'session': req.session, 'session_id': req.session.sid},
++            'settings': {'session': req.session, 'session_id': req.session.sid,
++                         'name': name, 'email': email},
+             'timezones': all_timezones
+         }
+ 
+@@ -107,9 +116,19 @@
+                 elif field == 'newsid' and val:
+                     req.session.change_sid(val)
+                 else:
+-                    req.session[field] = val
+-            elif field in req.args and field in req.session:
+-                del req.session[field]
++                    if req.authname != 'anonymous' and \
++                            field in ('name', 'email'):
++                        user = UserManager(self.env).get_user(req.authname)
++                        user[field] = val
++                    else:
++                        req.session[field] = val
++            elif field in req.args:
++                if req.authname != 'anonymous' and \
++                        field in ('name', 'email'):
++                    user = UserManager(self.env).get_user(req.authname)
++                    user[field] = ''
++                elif field in req.session:
++                    del req.session[field]
+ 
+     def _do_load(self, req):
+         if req.authname == 'anonymous':
+Index: trac/ticket/admin.py
+===================================================================
+--- trac/ticket/admin.py	(revision 4456)
++++ trac/ticket/admin.py	(working copy)
+@@ -17,6 +17,7 @@
+ from trac.core import *
+ from trac.perm import PermissionSystem
+ from trac.ticket import model
++from trac.user import UserManager
+ from trac.util import datefmt
+ from trac.web.chrome import add_link, add_script
+ 
+@@ -91,8 +92,8 @@
+             perm = PermissionSystem(self.env)
+             def valid_owner(username):
+                 return perm.get_user_permissions(username).get('TICKET_MODIFY')
+-            data['owners'] = [username for username, name, email
+-                              in self.env.get_known_users()
++            data['owners'] = [username for username
++                              in UserManager(self.env).get_usernames()
+                               if valid_owner(username)]
+ 
+         return 'admin_components.html', data
+Index: trac/ticket/api.py
+===================================================================
+--- trac/ticket/api.py	(revision 4456)
++++ trac/ticket/api.py	(working copy)
+@@ -20,6 +20,7 @@
+ from trac.config import *
+ from trac.core import *
+ from trac.perm import IPermissionRequestor, PermissionSystem
++from trac.user import UserManager
+ from trac.util import Ranges
+ from trac.util.html import html
+ from trac.util.text import shorten_line
+@@ -102,12 +103,12 @@
+         field = {'name': 'owner', 'label': 'Owner'}
+         if self.restrict_owner:
+             field['type'] = 'select'
+-            users = [''] # for clearing assignment
+             perm = PermissionSystem(self.env)
+-            for username, name, email in self.env.get_known_users(db):
+-                if perm.get_user_permissions(username).get('TICKET_MODIFY'):
+-                    users.append(username)
+-            field['options'] = users
++            def valid_owner(username):
++                return perm.get_user_permissions(username).get('TICKET_MODIFY')
++            field['options'] = [username for username
++                                in UserManager(self.env).get_usernames()
++                                if valid_owner(username)]
+             field['optional'] = True
+         else:
+             field['type'] = 'text'
+Index: trac/ticket/report.py
+===================================================================
+--- trac/ticket/report.py	(revision 4456)
++++ trac/ticket/report.py	(working copy)
+@@ -22,6 +22,7 @@
+ from trac.core import *
+ from trac.db import get_column_names
+ from trac.perm import IPermissionRequestor
++from trac.user import UserManager
+ from trac.util import sorted
+ from trac.util.text import to_unicode, unicode_urlencode
+ from trac.util.html import html
+@@ -276,12 +277,6 @@
+                 header_groups.append([])
+             header_group.append(header)
+ 
+-        # Get the email addresses of all known users
+-        email_map = {}
+-        for username, name, email in self.env.get_known_users():
+-            if email:
+-                email_map[username] = email
+-
+         # Structure the rows and cells:
+         #  - group rows according to __group__ value, if defined
+         #  - group cells the same way headers are grouped
+@@ -313,10 +308,7 @@
+                     # Special casing based on column name
+                     col = col.strip('_')
+                     if col == 'reporter':
+-                        if '@' in value:
+-                            cell['author'] = value
+-                        elif value in email_map:
+-                            cell['author'] = email_map[value]
++                        cell['author'] = value
+                     elif col == 'resource':
+                         resource = value
+                     cell_group.append(cell)
+Index: trac/ticket/query.py
+===================================================================
+--- trac/ticket/query.py	(revision 4456)
++++ trac/ticket/query.py	(working copy)
+@@ -690,9 +690,6 @@
+         query.verbose = True
+         db = self.env.get_db_cnx()
+         results = query.execute(req, db)
+-        for result in results:
+-            if result['reporter'].find('@') == -1:
+-                result['reporter'] = ''
+         query_href = req.abs_href.query(group=query.group,
+                                         groupdesc=query.groupdesc and 1 or None,
+                                         verbose=query.verbose and 1 or None,
+Index: trac/versioncontrol/web_ui/log.py
+===================================================================
+--- trac/versioncontrol/web_ui/log.py	(revision 4456)
++++ trac/versioncontrol/web_ui/log.py	(working copy)
+@@ -21,6 +21,7 @@
+ 
+ from trac.core import *
+ from trac.perm import IPermissionRequestor
++from trac.user import UserManager
+ from trac.util import Ranges
+ from trac.util.datefmt import http_date
+ from trac.util.html import html
+@@ -178,14 +179,7 @@
+         revs = [i['rev'] for i in info]
+         changes = get_changes(repos, revs)
+         extra_changes = {}
+-        email_map = {}
+-        if format == 'rss':
+-            # Get the email addresses of all known users
+-            email_map = {}
+-            for username,name,email in self.env.get_known_users():
+-                if email:
+-                    email_map[username] = email
+-        elif format == 'changelog':
++        if format == 'changelog':
+             for rev in revs:
+                 changeset = changes[rev]
+                 cs = {}
+@@ -206,7 +200,7 @@
+             'mode': mode, 'verbose': verbose,
+             'path_links': path_links,
+             'items': info, 'changes': changes,
+-            'email_map': email_map, 'extra_changes': extra_changes,
++            'extra_changes': extra_changes,
+             'wiki_format_messages':
+             self.config['changeset'].getbool('wiki_format_messages')
+             }
+Index: trac/user.py
+===================================================================
+--- trac/user.py	(revision 0)
++++ trac/user.py	(revision 0)
+@@ -0,0 +1,303 @@
++# -*- coding: utf-8 -*-
++#
++# Copyright 2006 Waldemar Kornewald, wkornewald@haiku-os.org
++# All rights reserved.
++#
++# This software is licensed as described in the file COPYING, which
++# you should have received as part of this distribution. The terms
++# are also available at http://trac.edgewall.org/wiki/TracLicense.
++#
++# This software consists of voluntary contributions made by many
++# individuals. For the exact contribution history, see the revision
++# history and logs, available at http://trac.edgewall.org/log/.
++#
++# Author: Waldemar Kornewald, wkornewald@haiku-os.org
++
++from trac.core import *
++from trac.config import *
++
++
++class IUserStore(Interface):
++    """
++    Extension point interface for backends that store known users.
++    """
++
++    def supports_user_operation(self, operation):
++        """
++        Returns whether the operation (a method name) is supported.
++
++        @return supported
++        """
++
++    def create_user(self, username, password):
++        """
++        Creates a new user with the given username and password.
++
++        @return success
++        """
++
++    def get_usernames(self):
++        """
++        Generator that yields an ordered list of known usernames.
++
++        @return username
++        """
++
++    def check_password(self, username, password):
++        """
++        Checks if the password is correct for the given user.
++        """
++
++    def change_password(self, username, password):
++        """
++        Changes a user's password.
++
++        @return success
++        """
++
++    def delete_user(self, username):
++        """
++        Deletes a user.
++
++        @return success
++            Returns False if the user didn't exist.
++        """
++
++
++class IUserAttributeProvider(Interface):
++    """
++    Extension point interface for backends that store user attributes.
++    """
++
++    def supports_attribute_operation(self, operation):
++        """
++        Returns whether the operation (a method name) is supported.
++
++        @return supported
++        """
++
++    def get_user_attribute(self, username, attribute):
++        """
++        Returns a user attribute.
++
++        If the attribute is not set it returs an empty string.
++        If the attribute is not supported None is returned.
++        """
++
++    def set_user_attribute(self, username, attribute, value):
++        """
++        Sets a user attribute.
++
++        @return success
++            Returns False if setting the attribute is not supported.
++        """
++
++    def delete_all_user_attributes(self, username):
++        """
++        Deletes all of the given user's attributes.
++        """
++
++
++class UserManager(Component):
++    """
++    Component responsible for managing users and user attributes.
++    """
++
++    store = ExtensionOption('users',
++        'store', IUserStore, 'SessionUserStore',
++        doc="""The user store that should be used for authentication
++            (''since 0.11'').""")
++    attribute_providers = OrderedExtensionsOption('users',
++        'attribute_providers', IUserAttributeProvider,
++        doc="""Ordered list of user attribute providers (''since 0.11'').""")
++
++    # public API
++
++    def supports_operation(self, operation):
++        if self.store.supports_user_operation(operation):
++            return True
++        for provider in self.attribute_providers:
++            if self.provider.supports_attribute_operation(operation):
++                return True
++        return False
++
++    # IUserStore methods
++
++    def create_user(self, username, password):
++        """
++        Creates a new user with the given username and password.
++
++        @return User object or None if the user couldn't be created
++        """
++        if not self.store.supports_user_operation('create_user'):
++            return None
++        if self.store.create_user(username, password):
++            return User(username, self.store, self.attribute_providers)
++
++    def get_user(self, username):
++        """
++        Returns a User object for the username.
++
++        @return User object or None if the user doesn't exist
++        """
++        if username in self.get_usernames():
++            return User(username, self.store, self.attribute_providers)
++        return None
++
++    def get_all_users(self):
++        """
++        Generator for User objects.
++        """
++        if not self.store.supports_user_operation('get_usernames'):
++            return
++        for username in self.store.get_usernames():
++            yield User(username, self.store, self.attribute_providers)
++
++    def get_usernames(self):
++        """
++        Generator for usernames.
++        """
++        if not self.store.supports_user_operation('get_usernames'):
++            return
++        return self.store.get_usernames()
++
++
++class User(object):
++    """
++    Object representing a user.
++    """
++
++    def __init__(self, username, store, attribute_providers):
++        self._username = username
++        self.store = store
++        self.attribute_providers = attribute_providers
++
++    # public API
++
++    @property
++    def username(self):
++        return self._username
++
++    # IUserStore methods
++
++    def check_password(self, password):
++        if not self.store.supports_user_operation('check_password'):
++            return False
++        return self.store.check_password(self.username, password)
++
++    def change_password(self, password):
++        if not self.store.supports_user_operation('change_password'):
++            return False
++        return self.store.change_password(self.username, password)
++
++    def delete(self):
++        if not self.store.supports_user_operation('delete_user'):
++            return False
++        self.delete_all_attributes()
++        return self.store.delete_user(self.username)
++
++    # IUserAttributeProvider methods
++
++    def __getitem__(self, attribute):
++        for provider in self.attribute_providers:
++            if not provider.supports_attribute_operation('get_user_attribute'):
++                continue
++            value = provider.get_user_attribute(self.username, attribute)
++            if value is not None:
++                return value
++        return None
++
++    def __setitem__(self, attribute, value):
++        for provider in self.attribute_providers:
++            if provider.supports_attribute_operation('set_user_attribute') \
++                    and provider.set_user_attribute(self.username, attribute,
++                                                    value):
++                return True
++        return False
++
++    def delete_all_attributes(self):
++        for provider in self.attribute_providers:
++            if provider.supports_attribute_operation('delete_all_user_attributes'):
++                provider.delete_all_user_attributes(username)
++
++
++class SessionUserStore(Component):
++    """
++    Component for managing authenticated users stored in sessions.
++    """
++
++    implements(IUserStore)
++
++    def supports_user_operation(self, operation):
++        return hasattr(self, operation)
++
++    def get_usernames(self):
++        db = self.env.get_db_cnx()
++        cursor = db.cursor()
++        cursor.execute("SELECT sid FROM session "
++                       "WHERE authenticated=1 "
++                       "ORDER BY sid")
++        for row in cursor:
++            yield row[0]
++
++
++class SessionUserAttributeProvider(Component):
++    """
++    Component for providing user attributes via Trac sessions.
++    """
++
++    implements(IUserAttributeProvider)
++
++    def supports_attribute_operation(self, operation):
++        return hasattr(self, operation)
++
++    def get_user_attribute(self, username, attribute):
++        db = self.env.get_db_cnx()
++        cursor = db.cursor()
++        cursor.execute("SELECT value FROM session_attribute "
++                       "WHERE sid=%s AND name=%s AND authenticated=1",
++                       (username, attribute))
++        row = cursor.fetchone()
++        if row:
++            return row[0]
++
++        # if the attribute doesn't exist we return an empty string to
++        # indicate that the attribute is supported, but not set
++        return ''
++
++    def set_user_attribute(self, username, attribute, value):
++        db = self.env.get_db_cnx()
++        cursor = db.cursor()
++
++        if not value or value == '':
++            cursor.execute("DELETE FROM session_attribute "
++                           "WHERE sid=%s AND name=%s AND authenticated=1",
++                          (username, attribute))
++            db.commit()
++            return True
++
++        # check if attribute exists
++        cursor.execute("SELECT value FROM session_attribute "
++                       "WHERE sid=%s AND name=%s AND authenticated=1",
++                       (username, attribute))
++        if cursor.fetchone():
++            # update the attribute
++            cursor.execute("UPDATE session_attribute SET value=%s "
++                           "WHERE sid=%s AND name=%s AND authenticated=1",
++                           (value, username, attribute))
++        else:
++            # create new attribute
++            cursor.execute("INSERT INTO session_attribute "
++                           "(sid,authenticated,name,value) "
++                           "VALUES(%s,1,%s,%s)",
++                           (username, attribute, value))
++        db.commit()
++        return True
++
++    def delete_all_user_attributes(self, username):
++        db = self.env.get_db_cnx()
++        cursor = db.cursor()
++        cursor.execute("DELETE FROM session_attribute "
++                       "WHERE sid=%s AND authenticated=1",
++                      (username,))
++        db.commit()
+Index: trac/timeline/web_ui.py
+===================================================================
+--- trac/timeline/web_ui.py	(revision 4456)
++++ trac/timeline/web_ui.py	(working copy)
+@@ -26,6 +26,7 @@
+ from trac.core import *
+ from trac.perm import IPermissionRequestor
+ from trac.timeline.api import ITimelineEventProvider, TimelineEvent
++from trac.user import UserManager
+ from trac.util.datefmt import format_date, parse_date, to_timestamp, utc
+ from trac.util.html import html, Markup
+ from trac.util.text import to_unicode
+@@ -137,12 +138,6 @@
+                 break
+ 
+         if format == 'rss':
+-            # Get the email addresses of all known users
+-            email_map = {}
+-            for username, name, email in self.env.get_known_users():
+-                if email:
+-                    email_map[username] = email
+-            data['email_map'] = email_map
+             return 'timeline.rss', data, 'application/rss+xml'
+ 
+         add_stylesheet(req, 'common/css/timeline.css')
+Index: trac/test.py
+===================================================================
+--- trac/test.py	(revision 4456)
++++ trac/test.py	(working copy)
+@@ -174,7 +174,7 @@
+     def get_db_cnx(self):
+         return self.db
+ 
+-    def get_known_users(self, db):
++    def get_known_users(self):
+         return self.known_users
+ 
+ 
+Index: trac/notification.py
+===================================================================
+--- trac/notification.py	(revision 4456)
++++ trac/notification.py	(working copy)
+@@ -22,6 +22,7 @@
+ from trac import __version__
+ from trac.config import BoolOption, IntOption, Option
+ from trac.core import *
++from trac.user import UserManager
+ from trac.util.text import CRLF
+ from trac.web.chrome import Chrome
+ 
+@@ -148,7 +149,6 @@
+     from_email = 'trac+tickets@localhost'
+     subject = ''
+     server = None
+-    email_map = None
+     template_name = None
+     addrfmt = r"[\w\d_\.\-\+=]+\@(([\w\d\-])+\.)+([\w\d]{2,4})+"
+     shortaddr_re = re.compile(addrfmt)
+@@ -161,12 +161,7 @@
+ 
+         self._use_tls = self.env.config.getbool('notification', 'use_tls')
+         self._init_pref_encoding()
+-        # Get the email addresses of all known users
+-        self.email_map = {}
+-        for username, name, email in self.env.get_known_users(self.db):
+-            if email:
+-                self.email_map[username] = email
+-                
++
+     def _init_pref_encoding(self):
+         from email.Charset import Charset, QP, BASE64
+         self._charset = Charset()
+@@ -241,8 +236,10 @@
+         if address.find('@') == -1:
+             if address == 'anonymous':
+                 return None
+-            if self.email_map.has_key(address):
+-                address = self.email_map[address]
++            user = UserManager(self.env).get_user(address)
++            email = user and user['email']
++            if user and email:
++                address = email
+             elif NotifyEmail.nodomaddr_re.match(address):
+                 if self.config.getbool('notification', 'use_short_addr'):
+                     return address
+Index: templates/prefs_general.html
+===================================================================
+--- templates/prefs_general.html	(revision 4456)
++++ templates/prefs_general.html	(working copy)
+@@ -14,12 +14,12 @@
+       <tr class="field">
+         <th><label for="name">Full name:</label></th>
+         <td><input type="text" id="name" name="name" size="30"
+-            value="${settings.session.name}" /></td>
++            value="${settings.name}" /></td>
+       </tr>
+       <tr class="field">
+         <th><label for="email">Email address:</label></th>
+         <td><input type="text" id="email" name="email" size="30"
+-            value="${settings.session.email}" /></td>
++            value="${settings.email}" /></td>
+       </tr>
+     </table>
+     <p py:choose="" class="hint">
+Index: templates/report.rss
+===================================================================
+--- templates/report.rss	(revision 4456)
++++ templates/report.rss	(working copy)
+@@ -17,7 +17,7 @@
+         <py:with vars="col = cell.header.col.strip('_')">
+           <py:choose>
+             <py:when test="col == 'reporter'">
+-              <author py:if="cell.author">$cell.author</author>
++              <dc:creator py:if="cell.author">$cell.author</cd:creator>
+             </py:when>
+             <py:when test="col in ('time', 'changetime', 'created', 'modified')">
+               <!-- FIXME: we end up with multiple pubDate -->
+Index: templates/revisionlog.rss
+===================================================================
+--- templates/revisionlog.rss	(revision 4456)
++++ templates/revisionlog.rss	(working copy)
+@@ -13,7 +13,7 @@
+     </image>
+ 
+     <item py:for="item in items" py:with="change = changes[item.rev]; item_context = context('changeset', change.rev, abs_urls=True)">
+-      <author py:if="change.author" py:with="a = change.author">${a and '@' in a and a or email_map.get(a)}</author>
++      <dc:creator py:if="change.author">${change.author}</dc:creator>
+       <pubDate>${http_date(change.date)}</pubDate>
+       <title>Revision $item.rev: ${shorten_line(change.message)}</title>
+       <link>${abs_href.changeset(rev, path)}</link>
+Index: templates/timeline.rss
+===================================================================
+--- templates/timeline.rss	(revision 4456)
++++ templates/timeline.rss	(working copy)
+@@ -14,9 +14,7 @@
+ 
+     <item py:for="event in events">
+       <title>${plaintext(event.title, keeplinebreaks=False)}</title>
+-      <py:with vars="author=event.author; author = author and '@' in author and author or email_map.get(author)">
+-        <author py:if="author">$author</author>
+-      </py:with>
++      <dc:creator py:if="event.author">${event.author}</dc:creator>
+       <pubDate>${http_date(event.date)}</pubDate>
+       <link>${event.abs_href}</link>
+       <guid isPermaLink="false">${event.abs_href}/${event.dateuid()}</guid>
+Index: templates/ticket.rss
+===================================================================
+--- templates/ticket.rss	(revision 4456)
++++ templates/ticket.rss	(working copy)
+@@ -13,7 +13,7 @@
+     <generator>Trac $trac.version</generator>
+ 
+     <item py:for="change in changes">
+-      <author py:if="change.author">$change.author</author>
++      <dc:creator py:if="change.author">$change.author</dc:creator>
+       <pubDate>${http_date(change.date)}</pubDate>
+       <title>$change.title</title>
+       <link>${abs_href.ticket(ticket.id)}<py:if test="change.cnum">#comment:$change.cnum</py:if></link>
+Index: templates/query.rss
+===================================================================
+--- templates/query.rss	(revision 4456)
++++ templates/query.rss	(working copy)
+@@ -16,7 +16,7 @@
+       <guid isPermaLink="false">$href</guid>
+       <title>#$result.id: $result.summary</title>
+       <pubDate py:if="result.time">${http_date(result.time)}</pubDate>
+-      <author py:if="result.reporter">$result.reporter</author>
++      <dc:creator py:if="result.reporter">$result.reporter</dc:creator>
+       <description>${unicode(context('ticket', result.id, abs_urls=True).wiki_to_html(result.description))}</description>
+       <category>Results</category>
+       <comments>$href#changelog</comments>

t2456-wkornewald-4y-back/user_API.diff

+Index: trac/prefs/web_ui.py
+===================================================================
+--- trac/prefs/web_ui.py	(revision 4450)
++++ trac/prefs/web_ui.py	(working copy)
+@@ -21,6 +21,7 @@
+ 
+ from trac.core import *
+ from trac.prefs.api import IPreferencePanelProvider
++from trac.user import UserManager, User
+ from trac.util.datefmt import all_timezones, utc
+ from trac.web import HTTPNotFound, IRequestHandler
+ from trac.web.chrome import add_stylesheet, INavigationContributor
+@@ -90,8 +91,16 @@
+                 self._do_save(req)
+             req.redirect(req.href.prefs(panel or None))
+ 
++        name = email = ''
++        if req.authname != 'anonymous':
++            user = UserManager(self.env).get_user(req.authname)
++            name, email = user['name'], user['email']
++        else:
++            name = req.session.get('name')
++            email = req.session.get('email')
+         return 'prefs_%s.html' % (panel or 'general'), {
+-            'settings': {'session': req.session, 'session_id': req.session.sid},
++            'settings': {'session': req.session, 'session_id': req.session.sid,
++                         'name': name, 'email': email},
+             'timezones': all_timezones
+         }
+ 
+@@ -107,9 +116,19 @@
+                 elif field == 'newsid' and val:
+                     req.session.change_sid(val)
+                 else:
+-                    req.session[field] = val
++                    if req.authname != 'anonymous' and \
++                            field in ('name', 'email'):
++                        user = UserManager(self.env).get_user(req.authname)
++                        user[field] = val
++                    else:
++                        req.session[field] = val
+             elif field in req.args and req.session:
+-                del req.session[field]
++                if req.authname != 'anonymous' and \
++                        field in ('name', 'email'):
++                    user = UserManager(self.env).get_user(req.authname)
++                    user[field] = ''
++                elif field in req.session:
++                    del req.session[field]
+ 
+     def _do_load(self, req):
+         if req.authname == 'anonymous':
+Index: trac/env.py
+===================================================================
+--- trac/env.py	(revision 4450)
++++ trac/env.py	(working copy)
+@@ -23,6 +23,7 @@
+ from trac.core import Component, ComponentManager, implements, Interface, \
+                       ExtensionPoint, TracError
+ from trac.db import DatabaseManager
++from trac.user import UserManager, User
+ from trac.util import get_pkginfo
+ from trac.versioncontrol import RepositoryManager
+ from trac.web.href import Href
+@@ -282,29 +283,16 @@
+             logfile = os.path.join(self.get_log_dir(), logfile)
+         self.log = logger_factory(logtype, logfile, self.log_level, self.path)
+ 
+-    def get_known_users(self, cnx=None):
++    def get_known_users(self):
+         """Generator that yields information about all known users, i.e. users
+         that have logged in to this Trac environment and possibly set their name
+         and email.
+ 
+         This function generates one tuple for every user, of the form
+         (username, name, email) ordered alpha-numerically by username.
+-
+-        @param cnx: the database connection; if ommitted, a new connection is
+-                    retrieved
+         """
+-        if not cnx:
+-            cnx = self.get_db_cnx()
+-        cursor = cnx.cursor()
+-        cursor.execute("SELECT DISTINCT s.sid, n.value, e.value "
+-                       "FROM session AS s "
+-                       " LEFT JOIN session_attribute AS n ON (n.sid=s.sid "
+-                       "  and n.authenticated=1 AND n.name = 'name') "
+-                       " LEFT JOIN session_attribute AS e ON (e.sid=s.sid "
+-                       "  AND e.authenticated=1 AND e.name = 'email') "
+-                       "WHERE s.authenticated=1 ORDER BY s.sid")
+-        for username,name,email in cursor:
+-            yield username, name, email
++        for user in UserManager(self).get_all_users():
++            yield user.username, user['name'], user['email']
+ 
+     def backup(self, dest=None):
+         """Simple SQLite-specific backup of the database.
+Index: trac/ticket/admin.py
+===================================================================
+--- trac/ticket/admin.py	(revision 4450)
++++ trac/ticket/admin.py	(working copy)
+@@ -17,6 +17,7 @@
+ from trac.core import *
+ from trac.perm import PermissionSystem
+ from trac.ticket import model
++from trac.user import UserManager
+ from trac.util import datefmt
+ from trac.web.chrome import add_link, add_script
+ 
+@@ -91,8 +92,8 @@
+             perm = PermissionSystem(self.env)
+             def valid_owner(username):
+                 return perm.get_user_permissions(username).get('TICKET_MODIFY')
+-            data['owners'] = [username for username, name, email
+-                              in self.env.get_known_users()
++            data['owners'] = [username for username
++                              in UserManager(self.env).get_usernames()
+                               if valid_owner(username)]
+ 
+         return 'admin_components.html', data
+Index: trac/ticket/api.py
+===================================================================
+--- trac/ticket/api.py	(revision 4450)
++++ trac/ticket/api.py	(working copy)
+@@ -20,6 +20,7 @@
+ from trac.config import *
+ from trac.core import *
+ from trac.perm import IPermissionRequestor, PermissionSystem
++from trac.user import UserManager
+ from trac.util import Ranges
+ from trac.util.html import html
+ from trac.util.text import shorten_line
+@@ -102,9 +103,9 @@
+         field = {'name': 'owner', 'label': 'Owner'}
+         if self.restrict_owner:
+             field['type'] = 'select'
+-            users = [''] # for clearing assignment
++            users = []
+             perm = PermissionSystem(self.env)
+-            for username, name, email in self.env.get_known_users(db):
++            for username in UserManager(self.env).get_usernames():
+                 if perm.get_user_permissions(username).get('TICKET_MODIFY'):
+                     users.append(username)
+             field['options'] = users
+Index: trac/ticket/report.py
+===================================================================
+--- trac/ticket/report.py	(revision 4450)
++++ trac/ticket/report.py	(working copy)
+@@ -22,6 +22,7 @@
+ from trac.core import *
+ from trac.db import get_column_names
+ from trac.perm import IPermissionRequestor
++from trac.user import UserManager
+ from trac.util import sorted
+ from trac.util.text import to_unicode, unicode_urlencode
+ from trac.util.html import html
+@@ -275,10 +276,7 @@
+             header_group.append(header)
+ 
+         # Get the email addresses of all known users
+-        email_map = {}
+-        for username, name, email in self.env.get_known_users():
+-            if email:
+-                email_map[username] = email
++        email_map = UserManager(self.env).get_email_map()
+ 
+         # Structure the rows and cells:
+         #  - group rows according to __group__ value, if defined
+Index: trac/versioncontrol/web_ui/log.py
+===================================================================
+--- trac/versioncontrol/web_ui/log.py	(revision 4450)
++++ trac/versioncontrol/web_ui/log.py	(working copy)
+@@ -21,6 +21,7 @@
+ 
+ from trac.core import *
+ from trac.perm import IPermissionRequestor
++from trac.user import UserManager
+ from trac.util import Ranges
+ from trac.util.datefmt import http_date
+ from trac.util.html import html
+@@ -181,10 +182,7 @@
+         email_map = {}
+         if format == 'rss':
+             # Get the email addresses of all known users
+-            email_map = {}
+-            for username,name,email in self.env.get_known_users():
+-                if email:
+-                    email_map[username] = email
++            email_map = UserManager(self.env).get_email_map()
+         elif format == 'changelog':
+             for rev in revs:
+                 changeset = changes[rev]
+Index: trac/user.py
+===================================================================
+--- trac/user.py	(revision 0)
++++ trac/user.py	(revision 0)
+@@ -0,0 +1,316 @@
++# -*- coding: utf-8 -*-
++#
++# Copyright 2006 Waldemar Kornewald, wkornewald@haiku-os.org
++# All rights reserved.
++#
++# This software is licensed as described in the file COPYING, which
++# you should have received as part of this distribution. The terms
++# are also available at http://trac.edgewall.org/wiki/TracLicense.
++#
++# This software consists of voluntary contributions made by many
++# individuals. For the exact contribution history, see the revision
++# history and logs, available at http://trac.edgewall.org/log/.
++#
++# Author: Waldemar Kornewald, wkornewald@haiku-os.org
++
++from trac.core import *
++from trac.config import *
++
++
++class IUserStore(Interface):
++    """
++    Extension point interface for backends that store known users.
++    """
++
++    def supports_user_operation(self, operation):
++        """
++        Returns whether the operation (a method name) is supported.
++
++        @return supported
++        """
++
++    def create_user(self, username, password):
++        """
++        Creates a new user with the given username and password.
++
++        @return success
++        """
++
++    def get_usernames(self):
++        """
++        Generator that yields an ordered list of known usernames.
++
++        @return username
++        """
++
++    def check_password(self, username, password):
++        """
++        Checks if the password is correct for the given user.
++        """
++
++    def change_password(self, username, password):
++        """
++        Changes a user's password.
++
++        @return success
++        """
++
++    def delete_user(self, username):
++        """
++        Deletes a user.
++
++        @return success
++            Returns False if the user didn't exist.
++        """
++
++
++class IUserAttributeProvider(Interface):
++    """
++    Extension point interface for backends that store user attributes.
++    """
++
++    def supports_attribute_operation(self, operation):
++        """
++        Returns whether the operation (a method name) is supported.
++
++        @return supported
++        """
++
++    def get_user_attribute(self, username, attribute):
++        """
++        Returns a user attribute.
++
++        If the attribute is not set it returs an empty string.
++        If the attribute is not supported None is returned.
++        """
++
++    def set_user_attribute(self, username, attribute, value):
++        """
++        Sets a user attribute.
++
++        @return success
++            Returns False if setting the attribute is not supported.
++        """
++
++    def delete_all_user_attributes(self, username):
++        """
++        Deletes all of the given user's attributes.
++        """
++
++
++class UserManager(Component):
++    """
++    Component responsible for managing users and user attributes.
++    """
++
++    store = ExtensionOption('users',
++        'store', IUserStore, 'SessionUserStore',
++        doc="""The user store that should be used for authentication
++            (''since 0.11'').""")
++    attribute_providers = OrderedExtensionsOption('users',
++        'attribute_providers', IUserAttributeProvider,
++        doc="""Ordered list of user attribute providers (''since 0.11'').""")
++
++    # public API
++
++    def supports_operation(self, operation):
++        if self.store.supports_user_operation(operation):
++            return True
++        for provider in self.attribute_providers:
++            if self.provider.supports_attribute_operation(operation):
++                return True
++        return False
++
++    # IUserStore methods
++
++    def create_user(self, username, password):
++        """
++        Creates a new user with the given username and password.
++
++        @return User object or None if the user couldn't be created
++        """
++        if not self.store.supports_user_operation('create_user'):
++            return None
++        if self.store.create_user(username, password):
++            return User(username, self.store, self.attribute_providers)
++
++    def get_user(self, username):
++        """
++        Returns a User object for the username.
++
++        @return User object or None if the user doesn't exist
++        """
++        if username in self.get_usernames():
++            return User(username, self.store, self.attribute_providers)
++        return None
++
++    def get_all_users(self):
++        """
++        Generator for User objects.
++        """
++        if not self.store.supports_user_operation('get_usernames'):
++            return
++        for username in self.store.get_usernames():
++            yield User(username, self.store, self.attribute_providers)
++
++    def get_usernames(self):
++        """
++        Generator for usernames.
++        """
++        if not self.store.supports_user_operation('get_usernames'):
++            return
++        return self.store.get_usernames()
++
++    # often-used specialized methods
++
++    def get_email_map(self):
++        """
++        Returns a hash mapping usernames to email addresses.
++        """
++        email_map = {}
++        for user in self.get_all_users():
++            email = user['email']
++            if email:
++                email_map[user.username] = email
++        return email_map
++
++
++class User(object):
++    """
++    Object representing a user.
++    """
++
++    def __init__(self, username, store, attribute_providers):
++        self._username = username
++        self.store = store
++        self.attribute_providers = attribute_providers
++
++    # public API
++
++    @property
++    def username(self):
++        return self._username
++
++    # IUserStore methods
++
++    def check_password(self, password):
++        if not self.store.supports_user_operation('check_password'):
++            return False
++        return self.store.check_password(self.username, password)
++
++    def change_password(self, password):
++        if not self.store.supports_user_operation('change_password'):
++            return False
++        return self.store.change_password(self.username, password)
++
++    def delete(self):
++        if not self.store.supports_user_operation('delete_user'):
++            return False
++        self.delete_all_attributes()
++        return self.store.delete_user(self.username)
++
++    # IUserAttributeProvider methods
++
++    def __getitem__(self, attribute):
++        for provider in self.attribute_providers:
++            if not provider.supports_attribute_operation('get_user_attribute'):
++                continue
++            value = provider.get_user_attribute(self.username, attribute)
++            if value is not None:
++                return value
++        return None
++
++    def __setitem__(self, attribute, value):
++        for provider in self.attribute_providers:
++            if provider.supports_attribute_operation('set_user_attribute') \
++                    and provider.set_user_attribute(self.username, attribute,
++                                                    value):
++                return True
++        return False
++
++    def delete_all_attributes(self):
++        for provider in self.attribute_providers:
++            if provider.supports_attribute_operation('delete_all_user_attributes'):
++                provider.delete_all_user_attributes(username)
++
++
++class SessionUserStore(Component):
++    """
++    Component for managing authenticated users stored in sessions.
++    """
++
++    implements(IUserStore)
++
++    def supports_user_operation(self, operation):
++        return hasattr(self, operation)
++
++    def get_usernames(self):
++        db = self.env.get_db_cnx()
++        cursor = db.cursor()
++        cursor.execute("SELECT sid FROM session "
++                       "WHERE authenticated=1 "
++                       "ORDER BY sid")
++        for row in cursor:
++            yield row[0]
++
++
++class SessionUserAttributeProvider(Component):
++    """
++    Component for providing user attributes via Trac sessions.
++    """
++
++    implements(IUserAttributeProvider)
++
++    def supports_attribute_operation(self, operation):
++        return hasattr(self, operation)
++
++    def get_user_attribute(self, username, attribute):
++        db = self.env.get_db_cnx()
++        cursor = db.cursor()
++        cursor.execute("SELECT value FROM session_attribute "
++                       "WHERE sid=%s AND name=%s AND authenticated=1",
++                       (username, attribute))
++        row = cursor.fetchone()
++        if row:
++            return row[0]
++
++        # if the attribute doesn't exist we return an empty string to
++        # indicate that the attribute is supported, but not set
++        return ''
++
++    def set_user_attribute(self, username, attribute, value):
++        db = self.env.get_db_cnx()