Repository: privacyidea/privacyidea

Issue:
Allow to change the username via the API
In an editable resolver the username can not be modified.

Well, the library function allows to do so. Also the UI has a lineedit field to modify the username.
However, it looks like the API uses the username to identify the user, who should be edited.

When the username is changed in the UI, then the API request contains this new username and this user can not be found.

It would be great to allow changing the username.
So we would have to change the API this way, that it either uses the userid or the **old** username.


Discussion:

Resolving patch:
diff --git a/privacyidea/api/user.py b/privacyidea/api/user.py
--- a/privacyidea/api/user.py
+++ b/privacyidea/api/user.py
@@ -327,7 +327,12 @@ def update_user():
     attributes = _get_attributes_from_param(request.all_data)
     username = getParam(request.all_data, "user", optional=False)
     resolvername = getParam(request.all_data, "resolver", optional=False)
-    user_obj = User(login=username, resolver=resolvername)
+    userid = getParam(request.all_data, "userid")
+    if userid is not None:
+        # Create the user object by uid
+        user_obj = User(resolver=resolvername, uid=userid)
+    else:
+        user_obj = User(login=username, resolver=resolvername)
     # Remove the password from the attributes, so that we can hide it in the
     # logs
     password = attributes.get("password")
diff --git a/privacyidea/lib/user.py b/privacyidea/lib/user.py
--- a/privacyidea/lib/user.py
+++ b/privacyidea/lib/user.py
@@ -87,7 +87,7 @@ class User(object):
     # NOTE: Directly decorating the class ``User`` breaks ``isinstance`` checks,
     # which is why we have to decorate __init__
     @log_with(log)
-    def __init__(self, login="", realm="", resolver=""):
+    def __init__(self, login="", realm="", resolver="", uid=None):
         self.login = login or ""
         self.used_login = self.login
         self.realm = (realm or "").lower()
@@ -95,11 +95,13 @@ def __init__(self, login="", realm="", resolver=""):
         if resolver == "**":
             resolver = ""
         self.resolver = resolver or ""
-        self.uid = None
+        self.uid = uid
         self.rtype = None
+        if not self.login and not self.resolver and uid is not None:
+            raise UserError("Can not create a user object from a uid without a resolver!")
         # Enrich user object with information from the userstore or from the
         # usercache
-        if login:
+        if login or uid is not None:
             self._get_user_from_userstore()
             # Just store the resolver type
             self.rtype = get_resolver_type(self.resolver)
@@ -118,9 +120,14 @@ def _get_user_from_userstore(self):
             if y is None:
                 raise UserError("The resolver '{0!s}' does not exist!".format(
                     self.resolver))
-            self.uid = y.getUserId(self.login)
+            if self.uid is None:
+                # Determine the uid
+                self.uid = y.getUserId(self.login)
+            if not self.login:
+                # Determine the login if it does not exist or
+                self.used_login = self.login = y.getUsername(self.uid)
             if y.has_multiple_loginnames:
-                # In this case the primary login might be another value!
+                # if the resolver has multiple logins the primary login might be another value!
                 self.login = y.getUsername(self.uid)
 
     def is_empty(self):


Test patch:
diff --git a/tests/test_api_users.py b/tests/test_api_users.py
--- a/tests/test_api_users.py
+++ b/tests/test_api_users.py
@@ -234,8 +234,21 @@ def test_02_create_update_delete_user(self):
             role = result.get("value").get("role")
             self.assertTrue(role == "user", result)
 
+        # The administrator can update the username of the user by specifying the userid
+        with self.app.test_request_context('/user/',
+                                           method='PUT',
+                                           query_string=urlencode(
+                                               {"user": "wordy2",
+                                                "resolver": resolver,
+                                                "userid": "7"}),
+                                           headers={'Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = res.json.get("result")
+            self.assertTrue(result.get("value"))
+
         # Delete the users
-        with self.app.test_request_context('/user/{0!s}/{1!s}'.format(resolver, "wordy"),
+        with self.app.test_request_context('/user/{0!s}/{1!s}'.format(resolver, "wordy2"),
                                            method='DELETE',
                                            headers={'Authorization': self.at}):
             res = self.app.full_dispatch_request()
diff --git a/tests/test_lib_user.py b/tests/test_lib_user.py
--- a/tests/test_lib_user.py
+++ b/tests/test_lib_user.py
@@ -16,7 +16,8 @@
                                   get_username,
                                   get_user_list,
                                   split_user,
-                                  get_user_from_param)
+                                  get_user_from_param,
+                                  UserError)
 from . import ldap3mock
 from .test_lib_resolver import LDAPDirectory_small
 
@@ -93,14 +94,25 @@ def test_01_resolvers_of_user(self):
         self.assertFalse(self.resolvername2 in resolvers, resolvers)
         
     def test_02_get_user_identifiers(self):
+        # create user by login
         user = User(login="root",
                     realm=self.realm1)
         (uid, rtype, resolvername) = user.get_user_identifiers()
-        self.assertTrue(uid == "0", uid)
-        self.assertTrue(rtype == "passwdresolver", rtype)
-        self.assertTrue(resolvername == self.resolvername1, resolvername)
+        self.assertEqual("0", uid)
+        self.assertEqual("passwdresolver", rtype)
+        self.assertEqual(self.resolvername1, resolvername)
         self.assertEqual(user.realm_id, 1)
-        
+
+        # create user by uid. fail, since the resolver is missing
+        self.assertRaises(UserError, User, realm=self.realm1, uid="0")
+        # create user by uid.
+        user2 = User(realm=self.realm1, resolver=self.resolvername1, uid="0")
+        (uid, rtype, resolvername) = user2.get_user_identifiers()
+        self.assertEqual("root", user2.login)
+        self.assertEqual("passwdresolver", rtype)
+        self.assertEqual(self.resolvername1, resolvername)
+        self.assertEqual(user.realm_id, 1)
+
     def test_03_get_username(self):
         username = get_username("0", self.resolvername1)
         self.assertTrue(username == "root", username)