Thomas Waldmann avatar Thomas Waldmann committed a997882

user profile storage: use revision metadata (not item metadata)

user profiles were the only piece of moin that needed item metadata.
item metadata is not indexed and needs quite some extra storage code.

by switching to revision metadata, we get:
* indexing (not implemented yet, but easy todo), faster lookup
* less and simpler storage code

long term we likely can also share quite some code with revisioned
wiki items, because user items then will work quite similar.

Changes:
userobj.id -> userobj.uuid (to match the UUID metadata key)

before, the "name" of the user object was its (numeric) id.
the user's name was just some data in the profile.

now, we have metadata key NAME storing the user's name and metadata key UUID
storing the (uu)id. we store profile items under the user's name, not under
his id.

removed userobj.last_saved, this is replaced by MTIME of revision.

improved the __repr__ to also show the uuid.

removed the rename user test, renaming needs to be done using the storage
api, not just by overwriting the name attr.

using CONTENTTYPE = u'application/x.moin.userprofile' for now

Comments (0)

Files changed (6)

MoinMoin/_tests/test_user.py

         theUser.subscribe(pagename)
         assert not theUser.isSubscribedTo([testPagename]) # list(!) of pages to check
 
-    def testRenameUser(self):
-        """ create user and then rename user and check if it still
-        exists under old name
-        """
-        # Create test user
-        name = u'__Some Name__'
-        password = name
-        self.createUser(name, password)
-        # Login - this should replace the old password in the user file
-        theUser = user.User(name=name)
-        # Rename user
-        theUser.name = u'__SomeName__'
-        theUser.save()
-        theUser = user.User(name=name, password=password)
-
-        assert not theUser.exists()
-
     def test_upgrade_password_from_ssha_to_ssha256(self):
         """
         Create user with {SSHA} password and check that logging in
         # could have a param where the admin could tell whether he wants to
         # trust it)
         userobj.auth_trusted = userobj.auth_method in app.cfg.auth_methods_trusted
-        session['user.id'] = userobj.id
+        session['user.id'] = userobj.uuid
         session['user.auth_method'] = userobj.auth_method
         session['user.auth_attribs'] = userobj.auth_attribs
     return userobj

MoinMoin/security/ticket.py

         # for age-check of ticket
         tm = "%010x" % time.time()
 
-    kw['uid'] = flaskg.user.valid and flaskg.user.id or ''
+    kw['uid'] = flaskg.user.valid and flaskg.user.uuid or ''
 
     hmac_data = []
     for value in sorted(kw.items()):

MoinMoin/storage/backends/indexing.py

                     self[HOSTNAME] = hostname
         try:
             if flaskg.user.valid:
-                self[USERID] = unicode(flaskg.user.id)
+                self[USERID] = unicode(flaskg.user.uuid)
         except:
             # when loading xml via script, we have no flaskg.user
             pass
 from flask import session, request, url_for
 
 from MoinMoin import config, wikiutil
+from MoinMoin.config import NAME, UUID, ACTION, CONTENTTYPE
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.util.interwiki import getInterwikiHome, getInterwikiName, is_local_wiki
 from MoinMoin.util.crypto import crypt_password, upgrade_password, valid_password, \
                                  generate_token, valid_token
-
+from MoinMoin.storage.error import NoSuchItemError, ItemAlreadyExistsError, NoSuchRevisionError
 
 
 def create_user(username, password, email, openid=None):
     :rtype: list
     :returns: all user IDs
     """
-    all_users = get_user_backend().iteritems()
-    return [item.name for item in all_users]
+    userlist = []
+    for item in get_user_backend().iteritems():
+        rev = item.get_revision(-1)
+        userlist.append(rev[UUID])
+    return userlist
 
 
-def get_items_by_filter(key, value):
+def get_revs_by_filter(key, value):
     """ Searches for a user with a given filter """
     backend = get_user_backend()
-    items_found = []
+    revs_found = []
     for item in backend.iteritems():
-        if item.get(key) == value:
-            items_found.append(item)
-    return items_found
+        rev = item.get_revision(-1)
+        if rev.get(key) == value:
+            revs_found.append(rev)
+    return revs_found
 
 
 def get_by_email_address(email_address):
     """ Searches for an user with a particular e-mail address and returns it. """
-    items = get_items_by_filter('email', email_address)
-    if items:
-        return User(items[0].name)
+    revs = get_revs_by_filter('email', email_address)
+    if revs:
+        return User(revs[0][UUID])
 
 def get_by_openid(openid):
     """
     :returns: the user whose openid is this one
     :rtype: user object or None
     """
-    items = get_items_by_filter('openid', openid)
-    if items:
-        return User(items[0].name)
+    revs = get_revs_by_filter('openid', openid)
+    if revs:
+        return User(revs[0][UUID])
+
+def getName(uuid):
+    """ Get the name for a specific uuid.
+
+    :param uuid: the user uuid to look up
+    :rtype: string
+    :returns: the corresponding user name or None
+    """
+    revs = get_revs_by_filter(UUID, uuid)
+    if revs:
+        return revs[0][NAME]
 
 def getUserId(searchName):
     """ Get the user ID for a specific user NAME.
     :rtype: string
     :returns: the corresponding user ID or None
     """
-    items = get_items_by_filter('name', searchName)
-    if items:
-        return items[0].name
+    revs = get_revs_by_filter(NAME, searchName)
+    if revs:
+        return revs[0][UUID]
 
 def get_editor(userid, addr, hostname):
     """ Return a tuple of type id and string or Page object
 
         self._cfg = app.cfg
         self.valid = 0
-        self.id = uid
+        self.uuid = uid
         self.auth_username = auth_username
         self.auth_method = kw.get('auth_method', 'internal')
         self.auth_attribs = kw.get('auth_attribs', ())
             self.enc_password = crypt_password(password)
 
         self._stored = False
-        self.last_saved = 0
 
         # attrs not saved to profile
 
         # we got an already authenticated username:
         check_password = None
-        if not self.id and self.auth_username:
-            self.id = getUserId(self.auth_username)
+        if not self.uuid and self.auth_username:
+            self.uuid = getUserId(self.auth_username)
             if not password is None:
                 check_password = password
-        if self.id:
+        if self.uuid:
             self.load_from_id(check_password)
-        elif self.name:
-            self.id = getUserId(self.name)
-            if self.id:
+        elif self.name and self.name != 'anonymous':
+            self.uuid = getUserId(self.name)
+            if self.uuid:
                 # no password given should fail
                 self.load_from_id(password or u'')
         # Still no ID - make new user
-        if not self.id:
-            self.id = self.make_id()
+        if not self.uuid:
+            self.uuid = self.make_id()
             if password is not None:
                 self.enc_password = crypt_password(password)
 
             self.may = Default(self)
 
     def __repr__(self):
-        return "<%s.%s at 0x%x name:%r valid:%r>" % (
+        return "<%s.%s at 0x%x name:%r uuid:%r valid:%r>" % (
             self.__class__.__module__, self.__class__.__name__,
-            id(self), self.name, self.valid)
+            id(self), self.name, self.uuid, self.valid)
 
     @property
     def language(self):
         # and some other things identifying remote users, then we could also
         # use it reliably in edit locking
         from random import randint
-        return "%s.%d" % (str(time.time()), randint(0, 65535))
+        return u"%s.%d" % (str(time.time()), randint(0, 65535))
 
     def create_or_update(self, changed=False):
         """ Create or update a user profile
         :rtype: bool
         :returns: true, if we have a user account
         """
-        return self._user_backend.has_item(self.id)
+        return self._user_backend.has_item(self.name)
 
     def load_from_id(self, password=None):
         """ Load user account data from disk.
         :param password: If not None, then the given password must match the
                          password in the user account file.
         """
-        if not self.exists():
+        name = getName(self.uuid) # XXX we need the name because backend API is still based on names
+        try:
+            item = self._user_backend.get_item(name)
+            self._user = item.get_revision(-1)
+        except (NoSuchItemError, NoSuchRevisionError):
             return
 
-        self._user = self._user_backend.get_item(self.id)
-
         user_data = dict()
         for metadata_key in self._user:
             user_data[metadata_key] = self._user[metadata_key]
 
     def persistent_items(self):
         """ items we want to store into the user profile """
-        nonpersistent_keys = ['id', 'valid', 'may', 'auth_username',
+        nonpersistent_keys = ['valid', 'may', 'auth_username',
                               'password', 'password2',
                               'auth_method', 'auth_attribs', 'auth_trusted',
                              ]
     def save(self):
         """
         Save user account data to user account file on disk.
-
-        This saves all member variables, except "id" and "valid" and
-        those starting with an underscore.
         """
-        if not self.exists():
-            self._user = self._user_backend.create_item(self.id)
-        else:
-            self._user = self._user_backend.get_item(self.id)
-
-        self._user.change_metadata()
-        for key in self._user.keys():
-            del self._user[key]
-
-        self.last_saved = int(time.time())
-
-        attrs = sorted(self.persistent_items())
-        for key, value in attrs:
+        try:
+            item = self._user_backend.get_item(self.name)
+        except NoSuchItemError:
+            item = self._user_backend.create_item(self.name)
+        try:
+            currentrev = item.get_revision(-1)
+            rev_no = currentrev.revno
+        except NoSuchRevisionError:
+            currentrev = None
+            rev_no = -1
+        new_rev_no = rev_no + 1
+        newrev = item.create_revision(new_rev_no)
+        for key, value in self.persistent_items():
             if isinstance(value, list):
                 value = tuple(value)
-            self._user[key] = value
-
-        self._user.publish_metadata()
+            newrev[key] = value
+        newrev[CONTENTTYPE] = u'application/x.moin.userprofile'
+        newrev[ACTION] = u'SAVE'
+        item.commit()
 
         if not self.disabled:
             self.valid = 1

MoinMoin/util/edit_lock.py

     timestamp = time.time()
     addr = request.remote_addr
     hostname = wikiutil.get_hostname(addr) or u''
-    userid = flaskg.user.valid and flaskg.user.id or ''
+    userid = flaskg.user.valid and flaskg.user.uuid or ''
 
     item.change_metadata()
     item[EDIT_LOCK_TIMESTAMP] = str(timestamp)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.