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()
+        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 4450)
+++ 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
@@ -138,10 +139,7 @@
 
         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()
             data['email_map'] = email_map
             return 'timeline.rss', data, 'application/rss+xml'
 
Index: trac/test.py
===================================================================
--- trac/test.py	(revision 4450)
+++ 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 4450)
+++ 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
 
@@ -162,11 +163,8 @@
         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
-                
+        self.email_map = UserManager(self.env).get_email_map()
+
     def _init_pref_encoding(self):
         from email.Charset import Charset, QP, BASE64
         self._charset = Charset()
Index: templates/prefs_general.html
===================================================================
--- templates/prefs_general.html	(revision 4450)
+++ 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">
