Edgewall Software

Ticket #2456: user_API.2.diff

File user_API.2.diff, 27.0 kB (added by Waldemar Kornewald <wkornewald>, 20 months ago)

don't expose email address (users want privacy!), optimizations for huge numbers of users (no more email_map)

  • trac/env.py

     
    2323from trac.core import Component, ComponentManager, implements, Interface, \ 
    2424                      ExtensionPoint, TracError 
    2525from trac.db import DatabaseManager 
     26from trac.user import UserManager, User 
    2627from trac.util import get_pkginfo 
    2728from trac.versioncontrol import RepositoryManager 
    2829from trac.web.href import Href 
     
    283284            logfile = os.path.join(self.get_log_dir(), logfile) 
    284285        self.log = logger_factory(logtype, logfile, self.log_level, self.path) 
    285286 
    286     def get_known_users(self, cnx=None): 
     287    def get_known_users(self): 
    287288        """Generator that yields information about all known users, i.e. users 
    288289        that have logged in to this Trac environment and possibly set their name 
    289290        and email. 
    290291 
    291292        This function generates one tuple for every user, of the form 
    292293        (username, name, email) ordered alpha-numerically by username. 
    293  
    294         @param cnx: the database connection; if ommitted, a new connection is 
    295                     retrieved 
    296294        """ 
    297         if not cnx: 
    298             cnx = self.get_db_cnx() 
    299         cursor = cnx.cursor() 
    300         cursor.execute("SELECT DISTINCT s.sid, n.value, e.value " 
    301                        "FROM session AS s " 
    302                        " LEFT JOIN session_attribute AS n ON (n.sid=s.sid " 
    303                        "  and n.authenticated=1 AND n.name = 'name') " 
    304                        " LEFT JOIN session_attribute AS e ON (e.sid=s.sid " 
    305                        "  AND e.authenticated=1 AND e.name = 'email') " 
    306                        "WHERE s.authenticated=1 ORDER BY s.sid") 
    307         for username,name,email in cursor: 
    308             yield username, name, email 
     295        for user in UserManager(self).get_all_users(): 
     296            yield user.username, user['name'], user['email'] 
    309297 
    310298    def backup(self, dest=None): 
    311299        """Simple SQLite-specific backup of the database. 
  • trac/prefs/web_ui.py

     
    2121 
    2222from trac.core import * 
    2323from trac.prefs.api import IPreferencePanelProvider 
     24from trac.user import UserManager, User 
    2425from trac.util.datefmt import all_timezones, utc 
    2526from trac.web import HTTPNotFound, IRequestHandler 
    2627from trac.web.chrome import add_stylesheet, INavigationContributor 
     
    9091                self._do_save(req) 
    9192            req.redirect(req.href.prefs(panel or None)) 
    9293 
     94        name = email = '' 
     95        if req.authname != 'anonymous': 
     96            user = UserManager(self.env).get_user(req.authname) 
     97            name, email = user['name'], user['email'] 
     98        else: 
     99            name = req.session.get('name') 
     100            email = req.session.get('email') 
    93101        return 'prefs_%s.html' % (panel or 'general'), { 
    94             'settings': {'session': req.session, 'session_id': req.session.sid}, 
     102            'settings': {'session': req.session, 'session_id': req.session.sid, 
     103                         'name': name, 'email': email}, 
    95104            'timezones': all_timezones 
    96105        } 
    97106 
     
    107116                elif field == 'newsid' and val: 
    108117                    req.session.change_sid(val) 
    109118                else: 
    110                     req.session[field] = val 
    111             elif field in req.args and field in req.session: 
    112                 del req.session[field] 
     119                    if req.authname != 'anonymous' and \ 
     120                            field in ('name', 'email'): 
     121                        user = UserManager(self.env).get_user(req.authname) 
     122                        user[field] = val 
     123                    else: 
     124                        req.session[field] = val 
     125            elif field in req.args: 
     126                if req.authname != 'anonymous' and \ 
     127                        field in ('name', 'email'): 
     128                    user = UserManager(self.env).get_user(req.authname) 
     129                    user[field] = '' 
     130                elif field in req.session: 
     131                    del req.session[field] 
    113132 
    114133    def _do_load(self, req): 
    115134        if req.authname == 'anonymous': 
  • trac/ticket/admin.py

     
    1717from trac.core import * 
    1818from trac.perm import PermissionSystem 
    1919from trac.ticket import model 
     20from trac.user import UserManager 
    2021from trac.util import datefmt 
    2122from trac.web.chrome import add_link, add_script 
    2223 
     
    9192            perm = PermissionSystem(self.env) 
    9293            def valid_owner(username): 
    9394                return perm.get_user_permissions(username).get('TICKET_MODIFY') 
    94             data['owners'] = [username for username, name, email 
    95                               in self.env.get_known_users() 
     95            data['owners'] = [username for username 
     96                              in UserManager(self.env).get_usernames() 
    9697                              if valid_owner(username)] 
    9798 
    9899        return 'admin_components.html', data 
  • trac/ticket/api.py

     
    2020from trac.config import * 
    2121from trac.core import * 
    2222from trac.perm import IPermissionRequestor, PermissionSystem 
     23from trac.user import UserManager 
    2324from trac.util import Ranges 
    2425from trac.util.html import html 
    2526from trac.util.text import shorten_line 
     
    102103        field = {'name': 'owner', 'label': 'Owner'} 
    103104        if self.restrict_owner: 
    104105            field['type'] = 'select' 
    105             users = [''] # for clearing assignment 
    106106            perm = PermissionSystem(self.env) 
    107             for username, name, email in self.env.get_known_users(db): 
    108                 if perm.get_user_permissions(username).get('TICKET_MODIFY'): 
    109                     users.append(username) 
    110             field['options'] = users 
     107            def valid_owner(username): 
     108                return perm.get_user_permissions(username).get('TICKET_MODIFY') 
     109            field['options'] = [username for username 
     110                                in UserManager(self.env).get_usernames() 
     111                                if valid_owner(username)] 
    111112            field['optional'] = True 
    112113        else: 
    113114            field['type'] = 'text' 
  • trac/ticket/report.py

     
    2222from trac.core import * 
    2323from trac.db import get_column_names 
    2424from trac.perm import IPermissionRequestor 
     25from trac.user import UserManager 
    2526from trac.util import sorted 
    2627from trac.util.text import to_unicode, unicode_urlencode 
    2728from trac.util.html import html 
     
    276277                header_groups.append([]) 
    277278            header_group.append(header) 
    278279 
    279         # Get the email addresses of all known users 
    280         email_map = {} 
    281         for username, name, email in self.env.get_known_users(): 
    282             if email: 
    283                 email_map[username] = email 
    284  
    285280        # Structure the rows and cells: 
    286281        #  - group rows according to __group__ value, if defined 
    287282        #  - group cells the same way headers are grouped 
     
    313308                    # Special casing based on column name 
    314309                    col = col.strip('_') 
    315310                    if col == 'reporter': 
    316                         if '@' in value: 
    317                             cell['author'] = value 
    318                         elif value in email_map: 
    319                             cell['author'] = email_map[value] 
     311                        cell['author'] = value 
    320312                    elif col == 'resource': 
    321313                        resource = value 
    322314                    cell_group.append(cell) 
  • trac/ticket/query.py

     
    690690        query.verbose = True 
    691691        db = self.env.get_db_cnx() 
    692692        results = query.execute(req, db) 
    693         for result in results: 
    694             if result['reporter'].find('@') == -1: 
    695                 result['reporter'] = '' 
    696693        query_href = req.abs_href.query(group=query.group, 
    697694                                        groupdesc=query.groupdesc and 1 or None, 
    698695                                        verbose=query.verbose and 1 or None, 
  • trac/versioncontrol/web_ui/log.py

     
    2121 
    2222from trac.core import * 
    2323from trac.perm import IPermissionRequestor 
     24from trac.user import UserManager 
    2425from trac.util import Ranges 
    2526from trac.util.datefmt import http_date 
    2627from trac.util.html import html 
     
    178179        revs = [i['rev'] for i in info] 
    179180        changes = get_changes(repos, revs) 
    180181        extra_changes = {} 
    181         email_map = {} 
    182         if format == 'rss': 
    183             # Get the email addresses of all known users 
    184             email_map = {} 
    185             for username,name,email in self.env.get_known_users(): 
    186                 if email: 
    187                     email_map[username] = email 
    188         elif format == 'changelog': 
     182        if format == 'changelog': 
    189183            for rev in revs: 
    190184                changeset = changes[rev] 
    191185                cs = {} 
     
    206200            'mode': mode, 'verbose': verbose, 
    207201            'path_links': path_links, 
    208202            'items': info, 'changes': changes, 
    209             'email_map': email_map, 'extra_changes': extra_changes, 
     203            'extra_changes': extra_changes, 
    210204            'wiki_format_messages': 
    211205            self.config['changeset'].getbool('wiki_format_messages') 
    212206            } 
  • trac/user.py

     
     1# -*- coding: utf-8 -*- 
     2# 
     3# Copyright 2006 Waldemar Kornewald, wkornewald@haiku-os.org 
     4# All rights reserved. 
     5# 
     6# This software is licensed as described in the file COPYING, which 
     7# you should have received as part of this distribution. The terms 
     8# are also available at http://trac.edgewall.org/wiki/TracLicense. 
     9# 
     10# This software consists of voluntary contributions made by many 
     11# individuals. For the exact contribution history, see the revision 
     12# history and logs, available at http://trac.edgewall.org/log/. 
     13# 
     14# Author: Waldemar Kornewald, wkornewald@haiku-os.org 
     15 
     16from trac.core import * 
     17from trac.config import * 
     18 
     19 
     20class IUserStore(Interface): 
     21    """ 
     22    Extension point interface for backends that store known users. 
     23    """ 
     24 
     25    def supports_user_operation(self, operation): 
     26        """ 
     27        Returns whether the operation (a method name) is supported. 
     28 
     29        @return supported 
     30        """ 
     31 
     32    def create_user(self, username, password): 
     33        """ 
     34        Creates a new user with the given username and password. 
     35 
     36        @return success 
     37        """ 
     38 
     39    def get_usernames(self): 
     40        """ 
     41        Generator that yields an ordered list of known usernames. 
     42 
     43        @return username 
     44        """ 
     45 
     46    def check_password(self, username, password): 
     47        """ 
     48        Checks if the password is correct for the given user. 
     49        """ 
     50 
     51    def change_password(self, username, password): 
     52        """ 
     53        Changes a user's password. 
     54 
     55        @return success 
     56        """ 
     57 
     58    def delete_user(self, username): 
     59        """ 
     60        Deletes a user. 
     61 
     62        @return success 
     63            Returns False if the user didn't exist. 
     64        """ 
     65 
     66 
     67class IUserAttributeProvider(Interface): 
     68    """ 
     69    Extension point interface for backends that store user attributes. 
     70    """ 
     71 
     72    def supports_attribute_operation(self, operation): 
     73        """ 
     74        Returns whether the operation (a method name) is supported. 
     75 
     76        @return supported 
     77        """ 
     78 
     79    def get_user_attribute(self, username, attribute): 
     80        """ 
     81        Returns a user attribute. 
     82 
     83        If the attribute is not set it returs an empty string. 
     84        If the attribute is not supported None is returned. 
     85        """ 
     86 
     87    def set_user_attribute(self, username, attribute, value): 
     88        """ 
     89        Sets a user attribute. 
     90 
     91        @return success 
     92            Returns False if setting the attribute is not supported. 
     93        """ 
     94 
     95    def delete_all_user_attributes(self, username): 
     96        """ 
     97        Deletes all of the given user's attributes. 
     98        """ 
     99 
     100 
     101class UserManager(Component): 
     102    """ 
     103    Component responsible for managing users and user attributes. 
     104    """ 
     105 
     106    store = ExtensionOption('users', 
     107        'store', IUserStore, 'SessionUserStore', 
     108        doc="""The user store that should be used for authentication 
     109            (''since 0.11'').""") 
     110    attribute_providers = OrderedExtensionsOption('users', 
     111        'attribute_providers', IUserAttributeProvider, 
     112        doc="""Ordered list of user attribute providers (''since 0.11'').""") 
     113 
     114    # public API 
     115 
     116    def supports_operation(self, operation): 
     117        if self.store.supports_user_operation(operation): 
     118            return True 
     119        for provider in self.attribute_providers: 
     120            if self.provider.supports_attribute_operation(operation): 
     121                return True 
     122        return False 
     123 
     124    # IUserStore methods 
     125 
     126    def create_user(self, username, password): 
     127        """ 
     128        Creates a new user with the given username and password. 
     129 
     130        @return User object or None if the user couldn't be created 
     131        """ 
     132        if not self.store.supports_user_operation('create_user'): 
     133            return None 
     134        if self.store.create_user(username, password): 
     135            return User(username, self.store, self.attribute_providers) 
     136 
     137    def get_user(self, username): 
     138        """ 
     139        Returns a User object for the username. 
     140 
     141        @return User object or None if the user doesn't exist 
     142        """ 
     143        if username in self.get_usernames(): 
     144            return User(username, self.store, self.attribute_providers) 
     145        return None 
     146 
     147    def get_all_users(self): 
     148        """ 
     149        Generator for User objects. 
     150        """ 
     151        if not self.store.supports_user_operation('get_usernames'): 
     152            return 
     153        for username in self.store.get_usernames(): 
     154            yield User(username, self.store, self.attribute_providers) 
     155 
     156    def get_usernames(self): 
     157        """ 
     158        Generator for usernames. 
     159        """ 
     160        if not self.store.supports_user_operation('get_usernames'): 
     161            return 
     162        return self.store.get_usernames() 
     163 
     164 
     165class User(object): 
     166    """ 
     167    Object representing a user. 
     168    """ 
     169 
     170    def __init__(self, username, store, attribute_providers): 
     171        self._username = username 
     172        self.store = store 
     173        self.attribute_providers = attribute_providers 
     174 
     175    # public API 
     176 
     177    @property 
     178    def username(self): 
     179        return self._username 
     180 
     181    # IUserStore methods 
     182 
     183    def check_password(self, password): 
     184        if not self.store.supports_user_operation('check_password'): 
     185            return False 
     186        return self.store.check_password(self.username, password) 
     187 
     188    def change_password(self, password): 
     189        if not self.store.supports_user_operation('change_password'): 
     190            return False 
     191        return self.store.change_password(self.username, password) 
     192 
     193    def delete(self): 
     194        if not self.store.supports_user_operation('delete_user'): 
     195            return False 
     196        self.delete_all_attributes() 
     197        return self.store.delete_user(self.username) 
     198 
     199    # IUserAttributeProvider methods 
     200 
     201    def __getitem__(self, attribute): 
     202        for provider in self.attribute_providers: 
     203            if not provider.supports_attribute_operation('get_user_attribute'): 
     204                continue 
     205            value = provider.get_user_attribute(self.username, attribute) 
     206            if value is not None: 
     207                return value 
     208        return None 
     209 
     210    def __setitem__(self, attribute, value): 
     211        for provider in self.attribute_providers: 
     212            if provider.supports_attribute_operation('set_user_attribute') \ 
     213                    and provider.set_user_attribute(self.username, attribute, 
     214                                                    value): 
     215                return True 
     216        return False 
     217 
     218    def delete_all_attributes(self): 
     219        for provider in self.attribute_providers: 
     220            if provider.supports_attribute_operation('delete_all_user_attributes'): 
     221                provider.delete_all_user_attributes(username) 
     222 
     223 
     224class SessionUserStore(Component): 
     225    """ 
     226    Component for managing authenticated users stored in sessions. 
     227    """ 
     228 
     229    implements(IUserStore) 
     230 
     231    def supports_user_operation(self, operation): 
     232        return hasattr(self, operation) 
     233 
     234    def get_usernames(self): 
     235        db = self.env.get_db_cnx() 
     236        cursor = db.cursor() 
     237        cursor.execute("SELECT sid FROM session " 
     238                       "WHERE authenticated=1 " 
     239                       "ORDER BY sid") 
     240        for row in cursor: 
     241            yield row[0] 
     242 
     243 
     244class SessionUserAttributeProvider(Component): 
     245    """ 
     246    Component for providing user attributes via Trac sessions. 
     247    """ 
     248 
     249    implements(IUserAttributeProvider) 
     250 
     251    def supports_attribute_operation(self, operation): 
     252        return hasattr(self, operation) 
     253 
     254    def get_user_attribute(self, username, attribute): 
     255        db = self.env.get_db_cnx() 
     256        cursor = db.cursor() 
     257        cursor.execute("SELECT value FROM session_attribute " 
     258                       "WHERE sid=%s AND name=%s AND authenticated=1", 
     259                       (username, attribute)) 
     260        row = cursor.fetchone() 
     261        if row: 
     262            return row[0] 
     263 
     264        # if the attribute doesn't exist we return an empty string to 
     265        # indicate that the attribute is supported, but not set 
     266        return '' 
     267 
     268    def set_user_attribute(self, username, attribute, value): 
     269        db = self.env.get_db_cnx() 
     270        cursor = db.cursor() 
     271 
     272        if not value or value == '': 
     273            cursor.execute("DELETE FROM session_attribute " 
     274                           "WHERE sid=%s AND name=%s AND authenticated=1", 
     275                          (username, attribute)) 
     276            db.commit() 
     277            return True 
     278 
     279        # check if attribute exists 
     280        cursor.execute("SELECT value FROM session_attribute " 
     281                       "WHERE sid=%s AND name=%s AND authenticated=1", 
     282                       (username, attribute)) 
     283        if cursor.fetchone(): 
     284            # update the attribute 
     285            cursor.execute("UPDATE session_attribute SET value=%s " 
     286                           "WHERE sid=%s AND name=%s AND authenticated=1", 
     287                           (value, username, attribute)) 
     288        else: 
     289            # create new attribute 
     290            cursor.execute("INSERT INTO session_attribute " 
     291                           "(sid,authenticated,name,value) " 
     292                           "VALUES(%s,1,%s,%s)", 
     293                           (username, attribute, value)) 
     294        db.commit() 
     295        return True 
     296 
     297    def delete_all_user_attributes(self, username): 
     298        db = self.env.get_db_cnx() 
     299        cursor = db.cursor() 
     300        cursor.execute("DELETE FROM session_attribute " 
     301                       "WHERE sid=%s AND authenticated=1", 
     302                      (username,)) 
     303        db.commit() 
  • trac/timeline/web_ui.py

     
    2626from trac.core import * 
    2727from trac.perm import IPermissionRequestor 
    2828from trac.timeline.api import ITimelineEventProvider, TimelineEvent 
     29from trac.user import UserManager 
    2930from trac.util.datefmt import format_date, parse_date, to_timestamp, utc 
    3031from trac.util.html import html, Markup 
    3132from trac.util.text import to_unicode 
     
    137138                break 
    138139 
    139140        if format == 'rss': 
    140             # Get the email addresses of all known users 
    141             email_map = {} 
    142             for username, name, email in self.env.get_known_users(): 
    143                 if email: 
    144                     email_map[username] = email 
    145             data['email_map'] = email_map 
    146141            return 'timeline.rss', data, 'application/rss+xml' 
    147142 
    148143        add_stylesheet(req, 'common/css/timeline.css') 
  • trac/test.py

     
    174174    def get_db_cnx(self): 
    175175        return self.db 
    176176 
    177     def get_known_users(self, db): 
     177    def get_known_users(self): 
    178178        return self.known_users 
    179179 
    180180 
  • trac/notification.py

     
    2222from trac import __version__ 
    2323from trac.config import BoolOption, IntOption, Option 
    2424from trac.core import * 
     25from trac.user import UserManager 
    2526from trac.util.text import CRLF 
    2627from trac.web.chrome import Chrome 
    2728 
     
    148149    from_email = 'trac+tickets@localhost' 
    149150    subject = '' 
    150151    server = None 
    151     email_map = None 
    152152    template_name = None 
    153153    addrfmt = r"[\w\d_\.\-\+=]+\@(([\w\d\-])+\.)+([\w\d]{2,4})+" 
    154154    shortaddr_re = re.compile(addrfmt) 
     
    161161 
    162162        self._use_tls = self.env.config.getbool('notification', 'use_tls') 
    163163        self._init_pref_encoding() 
    164         # Get the email addresses of all known users 
    165         self.email_map = {} 
    166         for username, name, email in self.env.get_known_users(self.db): 
    167             if email: 
    168                 self.email_map[username] = email 
    169                  
     164 
    170165    def _init_pref_encoding(self): 
    171166        from email.Charset import Charset, QP, BASE64 
    172167        self._charset = Charset() 
     
    241236        if address.find('@') == -1: 
    242237            if address == 'anonymous': 
    243238                return None 
    244             if self.email_map.has_key(address): 
    245                 address = self.email_map[address] 
     239            user = UserManager(self.env).get_user(address) 
     240            email = user and user['email'] 
     241            if user and email: 
     242                address = email 
    246243            elif NotifyEmail.nodomaddr_re.match(address): 
    247244                if self.config.getbool('notification', 'use_short_addr'): 
    248245                    return address 
  • templates/prefs_general.html

     
    1414      <tr class="field"> 
    1515        <th><label for="name">Full name:</label></th> 
    1616        <td><input type="text" id="name" name="name" size="30" 
    17             value="${settings.session.name}" /></td> 
     17            value="${settings.name}" /></td> 
    1818      </tr> 
    1919      <tr class="field"> 
    2020        <th><label for="email">Email address:</label></th> 
    2121        <td><input type="text" id="email" name="email" size="30" 
    22             value="${settings.session.email}" /></td> 
     22            value="${settings.email}" /></td> 
    2323      </tr> 
    2424    </table> 
    2525    <p py:choose="" class="hint"> 
  • templates/report.rss

     
    1717        <py:with vars="col = cell.header.col.strip('_')"> 
    1818          <py:choose> 
    1919            <py:when test="col == 'reporter'"> 
    20               <author py:if="cell.author">$cell.author</author> 
     20              <dc:creator py:if="cell.author">$cell.author</cd:creator> 
    2121            </py:when> 
    2222            <py:when test="col in ('time', 'changetime', 'created', 'modified')"> 
    2323              <!-- FIXME: we end up with multiple pubDate --> 
  • templates/revisionlog.rss

     
    1313    </image> 
    1414 
    1515    <item py:for="item in items" py:with="change = changes[item.rev]; item_context = context('changeset', change.rev, abs_urls=True)"> 
    16       <author py:if="change.author" py:with="a = change.author">${a and '@' in a and a or email_map.get(a)}</author> 
     16      <dc:creator py:if="change.author">${change.author}</dc:creator> 
    1717      <pubDate>${http_date(change.date)}</pubDate> 
    1818      <title>Revision $item.rev: ${shorten_line(change.message)}</title> 
    1919      <link>${abs_href.changeset(rev, path)}</link> 
  • templates/timeline.rss

     
    1414 
    1515    <item py:for="event in events"> 
    1616      <title>${plaintext(event.title, keeplinebreaks=False)}</title> 
    17       <py:with vars="author=event.author; author = author and '@' in author and author or email_map.get(author)"> 
    18         <author py:if="author">$author</author> 
    19       </py:with> 
     17      <dc:creator py:if="event.author">${event.author}</dc:creator> 
    2018      <pubDate>${http_date(event.date)}</pubDate> 
    2119      <link>${event.abs_href}</link> 
    2220      <guid isPermaLink="false">${event.abs_href}/${event.dateuid()}</guid> 
  • templates/ticket.rss

     
    1313    <generator>Trac $trac.version</generator> 
    1414 
    1515    <item py:for="change in changes"> 
    16       <author py:if="change.author">$change.author</author> 
     16      <dc:creator py:if="change.author">$change.author</dc:creator> 
    1717      <pubDate>${http_date(change.date)}</pubDate> 
    1818      <title>$change.title</title> 
    1919      <link>${abs_href.ticket(ticket.id)}<py:if test="change.cnum">#comment:$change.cnum</py:if></link> 
  • templates/query.rss

     
    1616      <guid isPermaLink="false">$href</guid> 
    1717      <title>#$result.id: $result.summary</title> 
    1818      <pubDate py:if="result.time">${http_date(result.time)}</pubDate> 
    19       <author py:if="result.reporter">$result.reporter</author> 
     19      <dc:creator py:if="result.reporter">$result.reporter</dc:creator> 
    2020      <description>${unicode(context('ticket', result.id, abs_urls=True).wiki_to_html(result.description))}</description> 
    2121      <category>Results</category>