Repository: privacyidea/privacyidea

Issue:
Disabled token can be enabled despite Active Tokens Count exceeding the value set in Policy
<!--
Thank you for filing a bug within privacyIDEA. The more information you provide, the easier it is to identify the problem!

Issues are not meant for asking questions! You're welcome to ask questions in our community forum: https://community.privacyidea.org

privacyIDEA is licensed under the AGPL and comes without warranty. If you require reliable, professional help, please consider getting either
- consultancy 
  (https://netknights.it/en/leistungen/one-time-services/)
- or the privacyIDEA Enterprise Edition 
  (https://netknights.it/en/leistungen/service-level-agreements/)
-->

### Top-level intent

What did you try to achieve?
Token policy is set to have only 01 Active TOTP Token for the user. Checking validation if system prevents users to have multiple Active TOTP tokens despite policy set to allow only 01 active token.

### Steps to reproduce

1. Login as admin, Create policy to allow max 01 Active TOTP token and Max 10 tokens.
2. Enroll TOTP token for a user, disable it.
3. Enroll another TOTP token for same user and its a active/enabled state.
4. Try to enroll another TOTP token for the same user and system will prevent the enrollment.
5. Now try to Enable the disabled token for the same user and system doesn't prevent enabling.



### Expected outcome

What do you think, how the system should have worked?
System should have checked the policy and prevented user/admin from enabling the token, if the user already has max active tokens for token type.

### Actual outcome
**Bug** - Now user has 2 active TOTP tokens despite policy only allowing max 1 Active TOTP token.

What did actually happen?

### Configuration

* **privacyIDEA version**:
* **Installation method**: (from Ubuntu packages, github, PyPI, ...)
* **Python version**:
* **Operating system**:
* **Webserver**:
* **Token database**: (MySQL, PostgreSQL, ...)

### Log file

**Set PI_LOGLEVEL = logging.DEBUG in pi.cfg and take a look at the privacyidea.log!**
**If appropriate, attach the log file or paste relevant portions.**



Discussion:
The decorator ``check_max_token_user`` checks all max_token-policies, also the ``ttype_max_active_token_per_user`` policies.

The decorator actually decorates the API endpoints ``/token/init``, ``/token/assign`` and ``/token/enable``. 

If the tokentype would not be specified, it determines the tokentype by the serial.
https://github.com/privacyidea/privacyidea/blob/97a4d95ff3a41156ac6fb17a5e57720948d05b76/privacyidea/api/lib/prepolicy.py#L623
So basically it should work.

The problem seems to be, that when *enabling* a token, the request has no user_obj, and thus the policy is not verified:
https://github.com/privacyidea/privacyidea/blob/97a4d95ff3a41156ac6fb17a5e57720948d05b76/privacyidea/api/lib/prepolicy.py#L620

Resolving patch:
diff --git a/privacyidea/api/before_after.py b/privacyidea/api/before_after.py
--- a/privacyidea/api/before_after.py
+++ b/privacyidea/api/before_after.py
@@ -133,6 +133,9 @@ def before_request():
     # from the Form data or from JSON in the request body.
     ensure_no_config_object()
     request.all_data = get_all_params(request.values, request.data)
+    # get additional request information such as parameters in the
+    # call path from the view_args
+    request.all_data.update(request.view_args)
     if g.logged_in_user.get("role") == "user":
         # A user is calling this API. First thing we do is restricting the user parameter.
         # ...to restrict token view, audit view or token actions.
diff --git a/privacyidea/api/lib/prepolicy.py b/privacyidea/api/lib/prepolicy.py
--- a/privacyidea/api/lib/prepolicy.py
+++ b/privacyidea/api/lib/prepolicy.py
@@ -70,13 +70,13 @@
 
 from OpenSSL import crypto
 
-from privacyidea.lib.error import PolicyError, RegistrationError, TokenAdminError
+from privacyidea.lib.error import PolicyError, RegistrationError, TokenAdminError, ResourceNotFoundError
 from flask import g, current_app
 from privacyidea.lib.policy import SCOPE, ACTION, PolicyClass
 from privacyidea.lib.policy import Match
 from privacyidea.lib.user import (get_user_from_param, get_default_realm,
                                   split_user, User)
-from privacyidea.lib.token import (get_tokens, get_realms_of_token, get_token_type)
+from privacyidea.lib.token import (get_tokens, get_realms_of_token, get_token_type, get_token_owner)
 from privacyidea.lib.utils import (get_client_ip,
                                    parse_timedelta, is_true, generate_charlists_from_pin_policy,
                                    check_pin_policy, get_module_class,
@@ -692,9 +692,16 @@ def check_max_token_user(request=None, action=None):
     ERROR_ACTIVE = "The number of active tokens for this user is limited!"
     ERROR_ACTIVE_TYPE = "The number of active tokens of type {0!s} for this user is limited!"
     params = request.all_data
+    serial = getParam(params, "serial")
+    tokentype = getParam(params, "type")
     user_object = get_user_from_param(params)
+    if user_object.is_empty() and serial:
+        try:
+            user_object = get_token_owner(serial) or User()
+        except ResourceNotFoundError:
+            # in case of token init the token does not yet exist in the db
+            pass
     if user_object.login:
-        serial = getParam(params, "serial")
         tokentype = getParam(params, "type")
         if not tokentype:
             if serial:


Test patch:
diff --git a/tests/test_api_caconnector.py b/tests/test_api_caconnector.py
--- a/tests/test_api_caconnector.py
+++ b/tests/test_api_caconnector.py
@@ -22,7 +22,9 @@ def test_01_fail_without_auth(self):
     def test_02_create_ca_connector(self):
         # create a CA connector
         with self.app.test_request_context('/caconnector/con1',
-                                           data={'type': 'local'},
+                                           data={'type': 'local',
+                                                 'cacert': '/etc/key.pem',
+                                                 'cakey': '/etc/cert.pem'},
                                            method='POST',
                                            headers={'Authorization': self.at}):
             res = self.app.full_dispatch_request()
@@ -51,7 +53,8 @@ def test_03_update_ca_connector(self):
         ca_list = get_caconnector_list()
         self.assertEqual(len(ca_list), 1)
         self.assertEqual(ca_list[0].get("data"), {u'cacert': u'/etc/cert.pem',
-                                                  u'cakey': u'/etc/key.pem'})
+                                                  u'cakey': u'/etc/key.pem',
+                                                  u'name': u'con1'})
 
     def test_04_read_ca_connector(self):
         with self.app.test_request_context('/caconnector/',
diff --git a/tests/test_api_lib_policy.py b/tests/test_api_lib_policy.py
--- a/tests/test_api_lib_policy.py
+++ b/tests/test_api_lib_policy.py
@@ -370,9 +370,31 @@ def test_04a_check_max_active_token_user(self):
                         "type": "totp"}
         self.assertTrue(check_max_token_user(req))
 
+        # Now, we disable the token NEW001, so the user again has NO active token
+        enable_token("NEW001", enable=False)
+        # we enroll a new HOTP token, this would now succeed
+        init_token({"serial": "NEW002", "type": "hotp",
+                    "otpkey": "1234567890123456"},
+                   user=User(login="cornelius",
+                             realm=self.realm1))
+        tokenobject_list = get_tokens(user=User(login="cornelius",
+                                                realm=self.realm1))
+        self.assertTrue(len(tokenobject_list) == 2)
+        # now we enable the first hotp token again, which fails due to the policy
+        req.all_data = {"serial": "NEW001"}
+        self.assertRaises(PolicyError,
+                          check_max_token_user, req)
+
+        # not we unassign the token and try to enable it which succeeds, since
+        # there is no tokenowner anymore
+        unassign_token("NEW001")
+        req.all_data = {"serial": "NEW001"}
+        self.assertTrue(check_max_token_user(req))
+
         # clean up
         delete_policy("pol1")
         remove_token("NEW001")
+        remove_token("NEW002")
 
     def test_04_check_max_token_user(self):
         g.logged_in_user = {"username": "admin1",