Repository: privacyidea/privacyidea

Issue:
delete a assigned tokengroup and the tokenlist can't be shown
version: master
Create a tokengroup.
Create token and assign the tokengroup
delete the tokengroup 
the tokenlist is empty and a none valid json-response error raise



Discussion:
I can not reproduce this, actually, i get an internal server error due to the fact that the database entry for the tokengroup has a foreign key constraint to the `tokentokengroup` table. So in fact it should only be able to be deleted, when no tokens are assigned to the tokengroup anymore. 

## TODO

We should make sure to check the constraint like you can not delete a resolver when it is still part of a realm.

Resolving patch:
diff --git a/privacyidea/lib/tokengroup.py b/privacyidea/lib/tokengroup.py
--- a/privacyidea/lib/tokengroup.py
+++ b/privacyidea/lib/tokengroup.py
@@ -23,14 +23,11 @@
 It depends on the models
 """
 
-import traceback
-import string
-import datetime
-import os
 import logging
 
-from privacyidea.lib.error import privacyIDEAError
-from privacyidea.models import Tokengroup, db
+from privacyidea.lib.utils import fetch_one_resource
+from privacyidea.lib.error import privacyIDEAError, ResourceNotFoundError
+from privacyidea.models import Tokengroup, TokenTokengroup, db
 
 log = logging.getLogger(__name__)
 
@@ -41,14 +38,36 @@ def set_tokengroup(name, description=None):
     return Tokengroup(name, description).save()
 
 
-def delete_tokengroup(name=None, id=None):
-    if not name and not id:
-        raise privacyIDEAError("You need to specify either a tokengroup ID or a name.")
-    if id:
-        Tokengroup.query.filter_by(id=id).delete()
-    elif name:
-        Tokengroup.query.filter_by(name=name).delete()
-    db.session.commit()
+def delete_tokengroup(name=None, tokengroup_id=None):
+    """
+    Delete the tokengroup given by either name or id.
+    If there are still tokens assigned to the tokengroup, the function fails
+    with an error.
+
+    :param name: Name of the tokengroup to be deleted
+    :type name: str
+    :param tokengroup_id: ID of the tokengroup to be deleted
+    :type tokengroup_id: int
+    """
+    tg = None
+    if name:
+        tg = fetch_one_resource(Tokengroup, name=name)
+    if tokengroup_id:
+        if tg:
+            if tg.id != tokengroup_id:
+                raise privacyIDEAError('ID of tokengroup with name {0!s} does not '
+                                       'match given ID ({1:d}).'.format(name, tokengroup_id))
+        else:
+            tg = fetch_one_resource(Tokengroup, id=tokengroup_id)
+    if tg:
+        tok_count = TokenTokengroup.query.filter_by(tokengroup_id=tg.id).count()
+        if tok_count > 0:
+            raise privacyIDEAError('The tokengroup with name {0!s} still has '
+                                   '{1:d} tokens assigned.'.format(tg.name, tok_count))
+        tg.delete()
+        db.session.commit()
+    else:
+        raise ResourceNotFoundError("You need to specify either a tokengroup ID or a name.")
 
 
 def get_tokengroups(name=None, id=None):
@@ -57,4 +76,4 @@ def get_tokengroups(name=None, id=None):
         query = query.filter(Tokengroup.name == name)
     if id:
         query = query.filter(Tokengroup.id == id)
-    return query.all()
\ No newline at end of file
+    return query.all()
diff --git a/privacyidea/models.py b/privacyidea/models.py
--- a/privacyidea/models.py
+++ b/privacyidea/models.py
@@ -254,9 +254,8 @@ def first_owner(self):
             
     @log_with(log)
     def delete(self):
-        # some DBs (eg. DB2) run in deadlock, if the TokenRealm entry
-        # is deleted via  key relation
-        # so we delete it explicit
+        # some DBs (e.g. DB2) run in deadlock, if the TokenRealm entry
+        # is deleted via  key relation, so we delete it explicit
         ret = self.id
         db.session.query(TokenRealm)\
                   .filter(TokenRealm.token_id == self.id)\
@@ -273,6 +272,9 @@ def delete(self):
         db.session.query(TokenInfo)\
                   .filter(TokenInfo.token_id == self.id)\
                   .delete()
+        db.session.query(TokenTokengroup)\
+            .filter(TokenTokengroup.token_id == self.id)\
+            .delete()
         db.session.delete(self)
         db.session.commit()
         return ret
@@ -653,7 +655,9 @@ def del_tokengroup(self, tokengroup=None, tokengroup_id=None):
         If tokengroup name and id are omitted, all tokengroups are deleted.
 
         :param tokengroup: The name of the tokengroup
+        :type tokengroup: str
         :param tokengroup_id: The id of the tokengroup
+        :type tokengroup_id: int
         :return:
         """
         if tokengroup:


Test patch:
diff --git a/tests/test_lib_token.py b/tests/test_lib_token.py
--- a/tests/test_lib_token.py
+++ b/tests/test_lib_token.py
@@ -2105,6 +2105,11 @@ def test_01_add_tokengroups(self):
         tok2 = get_one_token(serial="s2")
         self.assertEqual(len(tok2.token.tokengroup_list), 0)
 
+        # check that deleting a tokengroup with tokens still assigned results in an error
+        self.assertRaises(privacyIDEAError, delete_tokengroup, name='g2')
+
         # cleanup
         for s in serials:
             remove_token(s)
+        delete_tokengroup('g1')
+        delete_tokengroup('g2')
diff --git a/tests/test_lib_tokengroup.py b/tests/test_lib_tokengroup.py
--- a/tests/test_lib_tokengroup.py
+++ b/tests/test_lib_tokengroup.py
@@ -13,55 +13,8 @@
 gettokensoftype
 getToken....
 """
-from .base import MyTestCase, FakeAudit, FakeFlaskG
-from privacyidea.lib.user import (User)
-from privacyidea.lib.tokenclass import TokenClass, TOKENKIND, FAILCOUNTER_EXCEEDED, FAILCOUNTER_CLEAR_TIMEOUT
-from privacyidea.lib.token import weigh_token_type
-from privacyidea.lib.tokens.totptoken import TotpTokenClass
-from privacyidea.models import (Token, Challenge, TokenRealm)
-from privacyidea.lib.config import (set_privacyidea_config, get_token_types, delete_privacyidea_config, SYSCONF)
-from privacyidea.lib.policy import set_policy, SCOPE, ACTION, delete_policy, PolicyClass, delete_policy
-from privacyidea.lib.utils import b32encode_and_unicode, hexlify_and_unicode
-from privacyidea.lib.error import PolicyError
-import datetime
-from dateutil import parser
-import hashlib
-import binascii
-import warnings
-from privacyidea.lib.token import (create_tokenclass_object,
-                                   get_tokens,
-                                   get_token_type, check_serial,
-                                   get_num_tokens_in_realm,
-                                   get_realms_of_token,
-                                   token_exist, get_token_owner, is_token_owner,
-                                   get_tokenclass_info,
-                                   get_tokens_in_resolver, get_otp,
-                                   get_token_by_otp, get_serial_by_otp,
-                                   gen_serial, init_token, remove_token,
-                                   set_realms, set_defaults, assign_token,
-                                   unassign_token, resync_token,
-                                   reset_token, set_pin, set_pin_user,
-                                   set_pin_so, enable_token,
-                                   is_token_active, set_hashlib, set_otplen,
-                                   set_count_auth, add_tokeninfo,
-                                   set_sync_window, set_count_window,
-                                   set_description, get_multi_otp,
-                                   set_max_failcount, copy_token_pin,
-                                   copy_token_user, lost_token,
-                                   check_token_list, check_serial_pass,
-                                   check_realm_pass,
-                                   check_user_pass,
-                                   get_dynamic_policy_definitions,
-                                   get_tokens_paginate,
-                                   set_validity_period_end,
-                                   set_validity_period_start, remove_token, delete_tokeninfo,
-                                   import_token, get_one_token, get_tokens_from_serial_or_user,
-                                   get_tokens_paginated_generator)
-
-from privacyidea.lib.error import (TokenAdminError, ParameterError,
-                                   privacyIDEAError, ResourceNotFoundError)
-from privacyidea.lib.tokenclass import DATE_FORMAT
-from dateutil.tz import tzlocal
+from .base import MyTestCase
+from privacyidea.lib.error import privacyIDEAError, ResourceNotFoundError
 
 from privacyidea.lib.tokengroup import set_tokengroup, delete_tokengroup, get_tokengroups
 from privacyidea.models import Tokengroup
@@ -92,11 +45,14 @@ def test_02_delete_tokengroup(self):
         r = set_tokengroup("gruppe1", "my other first group")
         self.assertGreaterEqual(r, 1)
 
-        delete_tokengroup(id=r)
+        self.assertRaises(privacyIDEAError,
+                          delete_tokengroup, tokengroup_id=(r + 1), name='gruppe1')
+
+        delete_tokengroup(tokengroup_id=r)
         tg = Tokengroup.query.filter_by(name="gruppe1").all()
         self.assertEqual(len(tg), 0)
 
-        self.assertRaises(privacyIDEAError, delete_tokengroup)
+        self.assertRaises(ResourceNotFoundError, delete_tokengroup)
 
     def test_03_get_tokengroups(self):
         r1 = set_tokengroup("gruppe1", "my first group")