Repository: privacyidea/privacyidea

Issue:
ImportException is not a subclass of privacyIDEAError
The `ImportException` (thrown when something happens during token import) is not derived from the `privacyIDEAError` class which leads to a `500` Error in the WebUI if the import fails due to an error.
We could just derive it from `privacyIDEAError` and give it an `id`, this should give more meaningful error messages in the WebUI.


Discussion:

Resolving patch:
diff --git a/privacyidea/lib/error.py b/privacyidea/lib/error.py
--- a/privacyidea/lib/error.py
+++ b/privacyidea/lib/error.py
@@ -39,6 +39,7 @@ class ERROR:
     TOKENADMIN = 301
     CONFIGADMIN = 302
     POLICY = 303
+    IMPORTADMIN = 304
     VALIDATE = 401
     REGISTRATION = 402
     AUTHENTICATE = 403
@@ -107,6 +108,11 @@ def __repr__(self):
         return ret
 
 
+class TokenImportException(privacyIDEAError):
+    def __init__(self, description, id=ERROR.IMPORTADMIN):
+        privacyIDEAError.__init__(self, description=description, id=id)
+
+
 class AuthError(privacyIDEAError):
     def __init__(self, description, id=ERROR.AUTHENTICATE, details=None):
         self.details = details
diff --git a/privacyidea/lib/importotp.py b/privacyidea/lib/importotp.py
--- a/privacyidea/lib/importotp.py
+++ b/privacyidea/lib/importotp.py
@@ -58,6 +58,7 @@
 import html
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+from privacyidea.lib.error import TokenImportException
 from privacyidea.lib.utils import (modhex_decode, modhex_encode,
                                    hexlify_and_unicode, to_unicode, to_utf8,
                                    b64encode_and_unicode)
@@ -92,14 +93,6 @@ def _create_static_password(key_hex):
     return password
 
 
-class ImportException(Exception):
-    def __init__(self, description):
-        self.description = description
-
-    def __str__(self):
-        return ('{0!s}'.format(self.description))
-
-
 def getTagName(elem):
     match = re.match("^({.*?})(.*)$", elem.tag)
     if match:
@@ -357,10 +350,10 @@ def parseSafeNetXML(xml):
         elem_tokencontainer = etree.fromstring(xml)
     except etree.ParseError as e:
         log.debug(traceback.format_exc())
-        raise ImportException('Could not parse XML data: {0!s}'.format(e))
+        raise TokenImportException('Could not parse XML data: {0!s}'.format(e))
 
     if getTagName(elem_tokencontainer) != "Tokens":
-        raise ImportException("No toplevel element Tokens")
+        raise TokenImportException("No toplevel element Tokens")
 
     for elem_token in list(elem_tokencontainer):
         SERIAL = None
@@ -437,13 +430,13 @@ def derive_key(xml, password):
     :return: The derived key, hexlified
     """
     if not password:
-        raise ImportException("The XML KeyContainer specifies a derived "
+        raise TokenImportException("The XML KeyContainer specifies a derived "
                               "encryption key, but no password given!")
 
     keymeth = xml.keycontainer.encryptionkey.derivedkey.keyderivationmethod
     derivation_algo = keymeth["algorithm"].split("#")[-1]
     if derivation_algo.lower() != "pbkdf2":
-        raise ImportException("We only support PBKDF2 as Key derivation "
+        raise TokenImportException("We only support PBKDF2 as Key derivation "
                               "function!")
     salt = keymeth.find("salt").text.strip()
     keylength = keymeth.find("keylength").text.strip()
@@ -488,7 +481,7 @@ def parsePSKCdata(xml_data,
     xml = strip_prefix_from_soup(BeautifulSoup(xml_data, "lxml"))
 
     if not xml.keycontainer:
-        raise ImportException("No KeyContainer found in PSKC data. Could not "
+        raise TokenImportException("No KeyContainer found in PSKC data. Could not "
                               "import any tokens.")
     if xml.keycontainer.encryptionkey and \
             xml.keycontainer.encryptionkey.derivedkey:
@@ -549,7 +542,7 @@ def parsePSKCdata(xml_data,
                 encryptionmethod = key.data.secret.encryptedvalue.encryptionmethod
                 enc_algorithm = encryptionmethod["algorithm"].split("#")[-1]
                 if enc_algorithm.lower() != "aes128-cbc":
-                    raise ImportException("We only import PSKC files with "
+                    raise TokenImportException("We only import PSKC files with "
                                           "AES128-CBC.")
                 enc_data = key.data.secret.encryptedvalue.ciphervalue.text
                 enc_data = enc_data.strip()
@@ -587,7 +580,7 @@ def parsePSKCdata(xml_data,
         except Exception as exx:
             log.error("Failed to import tokendata: {0!s}".format(exx))
             log.debug(traceback.format_exc())
-            raise ImportException("Failed to import tokendata. Wrong "
+            raise TokenImportException("Failed to import tokendata. Wrong "
                                   "encryption key? %s" % exx)
 
         if token["type"] in ["hotp", "totp"] and key.data.counter:


Test patch:
diff --git a/tests/test_lib_importotp.py b/tests/test_lib_importotp.py
--- a/tests/test_lib_importotp.py
+++ b/tests/test_lib_importotp.py
@@ -7,8 +7,9 @@
 
 from .base import MyTestCase
 from privacyidea.lib.importotp import (parseOATHcsv, parseYubicoCSV,
-                                       parseSafeNetXML, ImportException,
+                                       parseSafeNetXML,
                                        parsePSKCdata, GPGImport)
+from privacyidea.lib.error import TokenImportException
 from privacyidea.lib.token import remove_token
 from privacyidea.lib.token import init_token
 from privacyidea.lib.importotp import export_pskc
@@ -600,13 +601,13 @@ def test_00_import_oath_user(self):
         self.assertEqual(tokens.get("tok3").get("user").get("resolver"), "resolver3")
 
     def test_01_import_aladdin_xml(self):
-        self.assertRaises(ImportException, parseSafeNetXML, 'no xml')
+        self.assertRaises(TokenImportException, parseSafeNetXML, 'no xml')
         tokens = parseSafeNetXML(ALADDINXML)
         self.assertTrue(len(tokens) == 2)
         self.assertTrue("00040008CFA52" in tokens, tokens)
 
         # fail to import without toplevel TOKENS tag
-        self.assertRaises(ImportException, parseSafeNetXML,
+        self.assertRaises(TokenImportException, parseSafeNetXML,
                           ALADDINXML_WITHOUT_TOKENS)
 
     def test_02_import_yubikey(self):
@@ -625,7 +626,7 @@ def test_02_import_yubikey(self):
         self.assertEqual(tokens["UBOM10944003_1"]["type"], "hotp")
 
     def test_03_import_pskc(self):
-        self.assertRaises(ImportException, parsePSKCdata, 'not xml')
+        self.assertRaises(TokenImportException, parsePSKCdata, 'not xml')
 
         tokens, _ = parsePSKCdata(XML_PSKC)
         self.assertEqual(len(tokens), 7)
@@ -688,7 +689,7 @@ def test_04_import_pskc_aes(self):
     def test_05_import_pskc_password(self):
         password = "qwerty"
 
-        self.assertRaises(ImportException, parsePSKCdata,
+        self.assertRaises(TokenImportException, parsePSKCdata,
                           XML_PSKC_PASSWORD_PREFIX)
 
         tokens, _ = parsePSKCdata(XML_PSKC_PASSWORD_PREFIX, password=password)