Repository: privacyidea/privacyidea

Issue:
max_active_token_per_user
<!--
Thank you for filing a bug within privacyIDEA.
Please read the following blog post.
https://www.privacyidea.org/getting-help/

The more information you provide, the easier it is to identify 
the problem!

Issues are not meant for asking questions!
If you simply wonder how things work and need some explanation 
please visit the community forum:

https://community.privacyidea.org

If you do not like the NO-WARRANTY of this software and need
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/)

If you still think, you should open an issue, please do so:
-->

# What did you try to do?
Assign only one (01) active token, while the user has multiple disabled tokens.

Please provide your top level intent!
privacyIDEA provides configuration option to set max_token_per_user, but doesn't allow to set only one (01) active token per user, while old tokens are disabled.

Please provide steps to reproduce!

# What outcome did you expect?

What do you think, how the system should have worked?
Admin can  set max_token_per_user, and while to avoid misuse if admin sets the max_token_per_user = 01, user can generate a token and uses it for a while. But in an event user loses the phone and wants helpdesk/admin to disable old token and wants to enroll a new token, privacyIDEA policy will restrict the user from enrolling the token due to token limit set in policy. Admin doesn't want to delete the disable token to keep the token history and audit log.

Hence there should be an option to set max_active_token_per_user, which means a user can have several disabled tokens and only 01 token can be active at a time. max_active_token_value should be less than max_token_per_user.

Another safety guard should be that once a new token is generated for the user, privacyIDEA must not allow activation of disabled tokens for the same user.

# What outcome did you experience?

What did acutally happen?
Once user's old token is disabled, and while token limit is set to 01 in config, user's can't register a new token.
# Configuration

* privacyIDEA Version:

* Installation method:
  (from source, github, virtualenv, ubuntu packages...)


## more details: 

* OS:

* Webserver:

* Tokendatabase: (like MySQL, PostgreSQL, ...)


# Log file

**Turn on debug level and take a look at the privacyidea.log!**
**If appropriate, attach the log file.**




Discussion:
Sounds sensible to me. There are probably some interesting use cases. Like prepare some disabled tokens and only activate them, e.g. if the active token is lost.

Resolving patch:
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
@@ -679,6 +679,7 @@ def check_max_token_user(request=None, action=None):
     Pre Policy
     This checks the maximum token per user policy.
     Check ACTION.MAXTOKENUSER
+    Check ACTION.MAXACTIVETOKENUSER
 
     This decorator can wrap:
         /token/init  (with a realm and user)
@@ -694,9 +695,11 @@ def check_max_token_user(request=None, action=None):
     serial = getParam(params, "serial")
     if user_object.login:
         policy_object = g.policy_object
+        # check maximum tokens of user
         limit_list = policy_object.get_action_values(ACTION.MAXTOKENUSER,
                                                      scope=SCOPE.ENROLL,
                                                      realm=user_object.realm,
+                                                     resolver=user_object.resolver,
                                                      user=user_object.login,
                                                      client=g.client_ip)
         if limit_list:
@@ -711,6 +714,30 @@ def check_max_token_user(request=None, action=None):
             if already_assigned_tokens >= max_value:
                 g.audit_object.add_policy(limit_list.get(str(max_value)))
                 raise PolicyError(ERROR)
+
+        # check maximum active tokens of user
+        limit_list = policy_object.get_action_values(ACTION.MAXACTIVETOKENUSER,
+                                                     scope=SCOPE.ENROLL,
+                                                     realm=user_object.realm,
+                                                     resolver=user_object.resolver,
+                                                     user=user_object.login,
+                                                     client=g.client_ip)
+        if limit_list:
+            # we need to check how many active tokens the user already has assigned!
+            tokenobject_list = get_tokens(user=user_object, active=True)
+            if serial:
+                for tok in tokenobject_list:
+                    if tok.token.serial == serial and tok.is_active():
+                        # If a serial is provided and this token already exists (and is active), the
+                        # token can be regenerated. If the token would be inactive, regenerating this
+                        # token would reactivate it and thus the user would have more tokens!
+                        return True
+            already_assigned_tokens = len(tokenobject_list)
+            max_value = max([int(x) for x in limit_list])
+            if already_assigned_tokens >= max_value:
+                g.audit_object.add_policy(limit_list.get(str(max_value)))
+                raise PolicyError(ERROR)
+
     return True
 
 
diff --git a/privacyidea/lib/policy.py b/privacyidea/lib/policy.py
--- a/privacyidea/lib/policy.py
+++ b/privacyidea/lib/policy.py
@@ -1,5 +1,7 @@
 # -*- coding: utf-8 -*-
 #
+#  2019-05-25 Cornelius Kölbel <cornelius.koelbel@netknights.it>
+#             Add max_active_token_per_user
 #  2018-09-07 Cornelius Kölbel <cornelius.koelbel@netknights.it>
 #             Add App Image URL
 #  2018-01-15 Cornelius Kölbel <cornelius.koelbel@netknights.it>
@@ -242,6 +244,7 @@ class ACTION(object):
     MANGLE = "mangle"
     MAXTOKENREALM = "max_token_per_realm"
     MAXTOKENUSER = "max_token_per_user"
+    MAXACTIVETOKENUSER = "max_active_token_per_user"
     NODETAILSUCCESS = "no_detail_on_success"
     ADDUSERINRESPONSE = "add_user_in_response"
     ADDRESOLVERINRESPONSE = "add_resolver_in_response"
@@ -1498,6 +1501,10 @@ def get_static_policy_definitions(scope=None):
                 'desc': _('Limit the number of tokens a user may have '
                           'assigned.'),
                 'group': GROUP.TOKEN},
+            ACTION.MAXACTIVETOKENUSER: {
+                'type': 'int',
+                'desc': _('Limit the number of active tokens a user may have assigned.'),
+                'group': GROUP.TOKEN},
             ACTION.OTPPINRANDOM: {
                 'type': 'int',
                 'value': list(range(0, 32)),


Test patch:
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
@@ -37,7 +37,8 @@
                                             add_user_detail_to_response,
                                             mangle_challenge_response)
 from privacyidea.lib.token import (init_token, get_tokens, remove_token,
-                                   set_realms, check_user_pass, unassign_token)
+                                   set_realms, check_user_pass, unassign_token,
+                                   enable_token)
 from privacyidea.lib.user import User
 from privacyidea.lib.tokens.papertoken import PAPERACTION
 from privacyidea.lib.tokens.tantoken import TANACTION
@@ -255,6 +256,54 @@ def test_03_check_token_upload(self):
         # finally delete policy
         delete_policy("pol1")
 
+    def test_04a_check_max_active_token_user(self):
+        g.logged_in_user = {"username": "admin1",
+                            "role": "admin"}
+        builder = EnvironBuilder(method='POST',
+                                 data={'serial': "OATH123456"},
+                                 headers={})
+        env = builder.get_environ()
+        # Set the remote address so that we can filter for it
+        env["REMOTE_ADDR"] = "10.0.0.1"
+        g.client_ip = env["REMOTE_ADDR"]
+        req = Request(env)
+
+        # Set a policy, that allows one active token per user
+        set_policy(name="pol1",
+                   scope=SCOPE.ENROLL,
+                   action="{0!s}={1!s}".format(ACTION.MAXACTIVETOKENUSER, 1))
+        g.policy_object = PolicyClass()
+        # The user has one token, everything is fine.
+        self.setUp_user_realms()
+        tokenobject = init_token({"serial": "NEW001", "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) == 1)
+        # First we can create the same active token again
+        req.all_data = {"user": "cornelius",
+                        "realm": self.realm1,
+                        "serial": "NEW001"}
+        self.assertTrue(check_max_token_user(req))
+
+        # The user has one token. The check that will run in this case,
+        # before the user would be assigned the NEW 2nd token, will raise a
+        # PolicyError
+        req.all_data = {"user": "cornelius",
+                        "realm": self.realm1,
+                        "serial": "NEW0002"}
+        self.assertRaises(PolicyError,
+                          check_max_token_user, req)
+
+        # Now, we disable the token NEW001, so the user has NO active token
+        enable_token("NEW001", False)
+        self.assertTrue(check_max_token_user(req))
+        # finally delete policy
+        delete_policy("pol1")
+        remove_token("NEW001")
+
     def test_04_check_max_token_user(self):
         g.logged_in_user = {"username": "admin1",
                             "role": "admin"}