Repository: privacyidea/privacyidea

Issue:
Assign a token with an empty serial number
# What did you try to do?

Assign a token to a user in the webui.
Got to the user view and click assign token.

# What outcome did you expect?

Receive an error, when the serial number is empty.

# What outcome did you experience?

If the serial number is left blank, i can still press "assign". The token with the lowest database id gets assigned to the user.

# Configuration

* privacyIDEA Version: 2.22



Discussion:
The line https://github.com/privacyidea/privacyidea/blob/master/privacyidea/api/token.py#L418 uses ``getParam`` ``required`` which only checks, if the parameter exists, but not, if the parameter is empty!

In fact we would also need ot check, if the param is empty.
I think we can put this into ``getParam`` using an additional parameter.

We could also disable the "assign" button in the UI, if the serial number is empty.

Resolving patch:
diff --git a/privacyidea/api/lib/utils.py b/privacyidea/api/lib/utils.py
--- a/privacyidea/api/lib/utils.py
+++ b/privacyidea/api/lib/utils.py
@@ -66,7 +66,7 @@ def get_version():
     return "privacyIDEA {0!s}".format(version)
 
 
-def getParam(param, key, optional=True, default=None):
+def getParam(param, key, optional=True, default=None, allow_empty=True):
     """
     returns a parameter from the request parameters.
     
@@ -79,6 +79,9 @@ def getParam(param, key, optional=True, default=None):
     :type optional: bool
     :param default: The value to assign to the parameter, if it is not
                     contained in the param.
+    :param allow_empty: Set to False is the parameter is a string and is
+        not allowed to be empty
+    :type allow_empty: bool
     
     :return: the value (literal) of the parameter if exists or nothing
              in case the parameter is optional, otherwise throw an exception
@@ -92,6 +95,9 @@ def getParam(param, key, optional=True, default=None):
     elif not optional:
         raise ParameterError("Missing parameter: {0!r}".format(key), id=905)
 
+    if not allow_empty and ret == "":
+        raise ParameterError("Parameter {0!r} must not be empty".format(key), id=905)
+
     return ret
 
 
diff --git a/privacyidea/api/token.py b/privacyidea/api/token.py
--- a/privacyidea/api/token.py
+++ b/privacyidea/api/token.py
@@ -415,7 +415,7 @@ def assign_api():
     :rtype: json object
     """
     user = get_user_from_param(request.all_data, required)
-    serial = getParam(request.all_data, "serial", required)
+    serial = getParam(request.all_data, "serial", required, allow_empty=False)
     pin = getParam(request.all_data, "pin")
     encrypt_pin = getParam(request.all_data, "encryptpin")
     res = assign_token(serial, user, pin=pin, encrypt_pin=encrypt_pin)


Test patch:
diff --git a/tests/test_api_lib_utils.py b/tests/test_api_lib_utils.py
new file mode 100644
--- /dev/null
+++ b/tests/test_api_lib_utils.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+"""
+This tests the file api.lib.utils
+"""
+from .base import MyTestCase
+
+from privacyidea.api.lib.utils import (getParam)
+from privacyidea.lib.error import ParameterError
+
+
+class UtilsTestCase(MyTestCase):
+
+    def test_01_getParam(self):
+        s = getParam({"serial": ""}, "serial", optional=False, allow_empty=True)
+        self.assertEqual(s, "")
+
+        self.assertRaises(ParameterError, getParam, {"serial": ""}, "serial", optional=False, allow_empty=False)
diff --git a/tests/test_api_token.py b/tests/test_api_token.py
--- a/tests/test_api_token.py
+++ b/tests/test_api_token.py
@@ -1437,4 +1437,18 @@ def test_26_supply_key_size(self):
             self.assertEqual(seed_url[:len('seed://')], 'seed://')
             seed = seed_url[len('seed://'):]
             self.assertEqual(len(seed.decode('hex')), 42)
-        remove_token(serial)
\ No newline at end of file
+        remove_token(serial)
+
+    def test_27_fail_to_assign_empty_serial(self):
+        with self.app.test_request_context('/token/assign',
+                                           method='POST',
+                                           data={"user": "cornelius",
+                                                 "realm": self.realm1,
+                                                 "serial": "",
+                                                 "pin": "test"},
+                                           headers={'Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 400, res)
+            result = json.loads(res.data).get("result")
+            self.assertEqual(result.get("status"), False)
+            self.assertEqual(result.get("error").get("code"), 905)
\ No newline at end of file