Repository: privacyidea/privacyidea

Issue:
Subscription checking to hard
The subscription check for certain plugins will fail as soon as it is exceeded by only one token.

If a plugin with 5000 subscriptions is hit by a system with 20.000 users, the subscription check should be fine for 5000 cases out of 20.000.


Discussion:

Resolving patch:
diff --git a/privacyidea/lib/subscriptions.py b/privacyidea/lib/subscriptions.py
--- a/privacyidea/lib/subscriptions.py
+++ b/privacyidea/lib/subscriptions.py
@@ -205,8 +205,8 @@ def delete_subscription(application):
 
 def raise_exception_probability(subscription=None):
     """
-    Depending on the subscription this will return True, so that an exception
-    can be raised
+    Depending on the subscription expiration data this will return True,
+    so that an exception can be raised
 
     :param subscription: Subscription dictionary
     :return: Bool
@@ -229,6 +229,26 @@ def raise_exception_probability(subscription=None):
     return False
 
 
+def subscription_exceeded_probability(active_tokens, allowed_tokens):
+    """
+    Depending on the subscription token numbers, this will return True,
+    so that an exception can be raised.
+
+    Returns true if a Subscription Exception is to be raised.
+
+    :param active_tokens: The number of the active tokens
+    :param allowed_tokens: The number of the allowed tokens
+    :return:
+    """
+    # old, hard behaviour
+    # return active_tokens > allowed_tokens
+    if active_tokens > allowed_tokens:
+        prob_check = random.randrange(active_tokens +1)
+        return prob_check > allowed_tokens
+    else:
+        return False
+
+
 def check_subscription(application, max_free_subscriptions=None):
     """
     This checks if the subscription for the given application is valid.
@@ -246,7 +266,7 @@ def check_subscription(application, max_free_subscriptions=None):
         token_users = get_users_with_active_tokens()
         free_subscriptions = max_free_subscriptions or APPLICATIONS.get(application.lower())
         if len(subscriptions) == 0:
-            if token_users > free_subscriptions:
+            if subscription_exceeded_probability(token_users, free_subscriptions):
                 raise SubscriptionError(description="No subscription for your client.",
                                         application=application)
         else:
@@ -261,7 +281,8 @@ def check_subscription(application, max_free_subscriptions=None):
             else:
                 # subscription is still valid, so check the signature.
                 check_signature(subscription)
-                if token_users > subscription.get("num_tokens"):
+                allowed_tokennums = subscription.get("num_tokens")
+                if subscription_exceeded_probability(token_users, allowed_tokennums):
                     # subscription is exceeded
                     raise SubscriptionError(description="Too many users "
                                                         "with assigned tokens. "


Test patch:
diff --git a/tests/test_lib_subscriptions.py b/tests/test_lib_subscriptions.py
--- a/tests/test_lib_subscriptions.py
+++ b/tests/test_lib_subscriptions.py
@@ -12,6 +12,7 @@
                                            SUBSCRIPTION_DATE_FORMAT)
 from privacyidea.lib.token import init_token
 from privacyidea.lib.user import User
+import mock
 
 # 100 users
 SUBSCRIPTION1 = {'by_address': 'provider-address', 'for_email': 'customer@example.com', 'num_tokens': 100, 'num_users': 100, 'level': 'Gold', 'for_comment': 'comment', 'date_from': '2016-10-24', 'for_address': 'customer-address', 'signature': '24287419543134291932335914280232067571967865893672677932354574121521748844689122490399903572722627692437421759860332653860825381771420923865100775095168810778157750122430333094307912014590689769228979527735405954705615614505247995506136338010930079794077541100403759754392432809967862978004604278914337052409517895998984832947211907032852653171723886377329563223486623362230032551555536271158219094006763746441282022250783412321241299993657761512776112262708235357995055119379697774465205945934356687189514600830870353192115780195534680601265109038104466390286558785622582056183085321696667197925775161589029048460315', 'for_phone': '12345', 'by_email': 'provider@example.com', 'date_till': '2026-10-22', 'by_name': 'NetKnights GmbH', 'application': 'demo_application', 'by_url': 'http://provider', 'for_name': 'customer', 'by_phone': '12345', 'for_url': 'http://customer', 'num_clients': 100}
@@ -103,9 +104,18 @@ def test_03_check_subscription(self):
 
         init_token({"type": "spass"}, user=User("shadow", self.realm1))
         init_token({"type": "spass"}, user=User("nopw", self.realm1))
-        # Now we have three users with tokens, subscription will fail
-        self.assertRaises(SubscriptionError, check_subscription,
-                          "demo_application")
+        # Now we have three users with tokens, but only two are allowed. We fail with a probabiliy of 1/3
+        # Fail subscription check
+        with mock.patch("random.randrange") as mock_random:
+            mock_random.return_value = 3
+            self.assertRaises(SubscriptionError, check_subscription, "demo_application")
+        # succeed subscription check
+        with mock.patch("random.randrange") as mock_random:
+            mock_random.return_value = 2
+            self.assertTrue(check_subscription("demo_application"))
+        with mock.patch("random.randrange") as mock_random:
+            mock_random.return_value = 1
+            self.assertTrue(check_subscription("demo_application"))
 
         # try to save some broken subscriptions
         sub1 = SUBSCRIPTION1.copy()