Repository: privacyidea/privacyidea

Issue:
Support button: Create support request
The green support button in the footer of the UI could do the following:

Condition:

* An administrator is logged in
* The administrator has sensible rights (to ensure, that it is not only an operator) like writing config. Alternatively, we could add  a new admin policy like "open support requests".
* A subscription is installed and valid.

Action:

Open an email program (mailto:) to the issued email address specified in the subscription.


Discussion:

Resolving patch:
diff --git a/privacyidea/api/lib/postpolicy.py b/privacyidea/api/lib/postpolicy.py
--- a/privacyidea/api/lib/postpolicy.py
+++ b/privacyidea/api/lib/postpolicy.py
@@ -42,6 +42,7 @@
 import datetime
 import logging
 import traceback
+from urllib.parse import quote
 from privacyidea.lib.error import PolicyError, ValidateError
 from flask import g, current_app, make_response
 from privacyidea.lib.policy import SCOPE, ACTION, AUTOASSIGNVALUE, AUTHORIZED
@@ -61,10 +62,15 @@
 from privacyidea.lib.auth import ROLE
 from privacyidea.lib.user import User
 from privacyidea.lib.realm import get_default_realm
-from privacyidea.lib.subscriptions import subscription_status
-from privacyidea.lib.utils import create_img
+from privacyidea.lib.subscriptions import (subscription_status,
+                                           get_subscription,
+                                           check_subscription,
+                                           SubscriptionError,
+                                           EXPIRE_MESSAGE)
+from privacyidea.lib.utils import create_img, get_version
 from privacyidea.lib.config import get_privacyidea_node
 from privacyidea.lib.tokenclass import ROLLOUTSTATE
+from privacyidea.lib import _
 
 log = logging.getLogger(__name__)
 
@@ -77,6 +83,15 @@
 DEFAULT_TIMEOUT_ACTION = "lockscreeen"
 DEFAULT_POLICY_TEMPLATE_URL = "https://raw.githubusercontent.com/privacyidea/" \
                               "policy-templates/master/templates/"
+BODY_TEMPLATE = _("""
+<--- Please describe your Problem in detail --->
+
+<--- Please provide as many additional information as possible --->
+
+privacyIDEA Version: {version}
+Subscriber: {subscriber_name}
+Subscriptions: {subscriptions}
+""")
 
 
 class postpolicy(object):
@@ -669,6 +684,31 @@ def get_webui_settings(request, response):
         content["result"]["value"]["qr_image_custom"] = qr_image_custom
         content["result"]["value"]["logout_redirect_url"] = logout_redirect_url
         content["result"]["value"]["require_description"] = require_description
+        if role == ROLE.ADMIN:
+            # Add a support mailto, for administrators with systemwrite rights.
+            subscriptions = get_subscription("privacyidea")
+            if len(subscriptions) == 1:
+                subscription = subscriptions[0]
+                try:
+                    version = get_version()
+                    subject = "Problem with {0!s}".format(version)
+                    check_subscription("privacyidea")
+                except SubscriptionError:
+                    subject = EXPIRE_MESSAGE
+                # Check policy, if the admin is allowed to save config
+                action_allowed = Match.generic(g, scope=role,
+                                               action=ACTION.SYSTEMWRITE,
+                                               adminuser=loginname,
+                                               adminrealm=realm).allowed()
+                if action_allowed:
+                    body = BODY_TEMPLATE.format(subscriptions=subscriptions,
+                                                version=version,
+                                                subscriber_name=subscription.get("for_name"))
+
+                    body = quote(body)
+                    content["result"]["value"]["supportmail"] = \
+                        ("mailto:{0!s}?subject={1!s}&body={2!s}").format(
+                            subscription.get("by_email"), subject, body)
         response.set_data(json.dumps(content))
     return response
 
diff --git a/privacyidea/lib/subscriptions.py b/privacyidea/lib/subscriptions.py
--- a/privacyidea/lib/subscriptions.py
+++ b/privacyidea/lib/subscriptions.py
@@ -32,6 +32,7 @@
 from privacyidea.lib.error import SubscriptionError
 from privacyidea.lib.token import get_tokens
 from privacyidea.lib.crypto import Sign
+from privacyidea.lib import _
 import functools
 from privacyidea.lib.framework import get_app_config_value
 import os
@@ -39,6 +40,7 @@
 from sqlalchemy import func
 
 
+EXPIRE_MESSAGE = _("My subscription has expired.")
 SUBSCRIPTION_DATE_FORMAT = "%Y-%m-%d"
 SIGN_FORMAT = """{application}
 {for_name}


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
@@ -22,6 +22,7 @@
 from privacyidea.lib.config import set_privacyidea_config, SYSCONF
 from .base import (MyApiTestCase)
 
+from privacyidea.lib.subscriptions import EXPIRE_MESSAGE
 from privacyidea.lib.policy import (set_policy, delete_policy, enable_policy,
                                     PolicyClass, SCOPE, ACTION, REMOTE_USER,
                                     AUTOASSIGNVALUE, AUTHORIZED)
@@ -3803,8 +3804,7 @@ def test_05_autoassign_any_pin(self):
         except Exception:
             print("no need to unassign token")
 
-        # The request with an OTP value and a PIN of a user, who has not
-        # token assigned
+        # The request with an OTP value and a PIN of a user, who has no token assigned
         builder = EnvironBuilder(method='POST',
                                  data={},
                                  headers={})
@@ -3863,8 +3863,7 @@ def test_05_autoassign_userstore(self):
         except Exception as e:
             print("no need to unassign token")
 
-        # The request with an OTP value and a PIN of a user, who has not
-        # token assigned
+        # The request with an OTP value and a PIN of a user, who has no token assigned
         builder = EnvironBuilder(method='POST',
                                  data={},
                                  headers={})
@@ -3928,8 +3927,7 @@ def test_06_offline_auth(self):
         self.assertEqual(mt.token.serial, serial)
         self.assertEqual(mt.token.machine_list[0].machine_id, "192.168.0.1")
 
-        # The request with an OTP value and a PIN of a user, who has not
-        # token assigned
+        # The request with an OTP value and a PIN of a user, who has no token assigned
         builder = EnvironBuilder(method='POST',
                                  data={},
                                  headers={})
@@ -3977,8 +3975,7 @@ def test_06a_offline_auth_postpend_pin(self):
         serial = "offline01"
         # Do prepend_pin == False
         set_privacyidea_config(SYSCONF.PREPENDPIN, False)
-        # The request with an OTP value and a PIN of a user, who has not
-        # token assigned
+        # The request with an OTP value and a PIN of a user, who has no token assigned
         builder = EnvironBuilder(method='POST',
                                  data={},
                                  headers={})
@@ -4070,8 +4067,7 @@ def test_07_sign_response(self):
 
     def test_08_get_webui_settings(self):
         self.setUp_user_realms()
-        # The request with an OTP value and a PIN of a user, who has not
-        # token assigned
+        # The request with an OTP value and a PIN of a user, who has no token assigned
         builder = EnvironBuilder(method='POST',
                                  data={},
                                  headers={})
@@ -4223,8 +4219,7 @@ def test_09_get_webui_settings_token_pagesize(self):
         # Test that policies like tokenpagesize are also user dependent
         self.setUp_user_realms()
 
-        # The request with an OTP value and a PIN of a user, who has not
-        # token assigned
+        # The request with an OTP value and a PIN of a user, who has no token assigned
         builder = EnvironBuilder(method='POST',
                                  data={},
                                  headers={})
@@ -4278,8 +4273,7 @@ def test_10_get_webui_settings_admin_dashboard(self):
         # Test admin_dashboard
         self.setUp_user_realms()
 
-        # The request with an OTP value and a PIN of a user, who has not
-        # token assigned
+        # The request with an OTP value and a PIN of a user, who has no token assigned
         builder = EnvironBuilder(method='POST',
                                  data={},
                                  headers={})
@@ -4313,6 +4307,51 @@ def test_10_get_webui_settings_admin_dashboard(self):
 
         delete_policy("pol_dashboard")
 
+    def test_11_get_webui_settings_support_link(self):
+        # Test the link to the support
+        self.setUp_user_realms()
+
+        # The request with an OTP value and a PIN of a user, who has no token assigned
+        builder = EnvironBuilder(method='POST',
+                                 data={},
+                                 headers={})
+        env = builder.get_environ()
+        env["REMOTE_ADDR"] = "192.168.0.1"
+        g.client_ip = env["REMOTE_ADDR"]
+        req = Request(env)
+        req.all_data = {}
+
+        res = {"jsonrpc": "2.0",
+               "result": {"status": True,
+                          "value": {"role": "admin",
+                                    "username": "cornelius"}},
+               "version": "privacyIDEA test",
+               "id": 1}
+        resp = jsonify(res)
+
+        new_response = get_webui_settings(req, resp)
+        jresult = new_response.json
+        self.assertNotIn("supportmail", jresult.get("result").get("value"))
+
+        # Add a subscription
+        from privacyidea.models import Subscription
+        s = Subscription(application="privacyidea",
+                         for_name="testuser",
+                         for_email="admin@example.com",
+                         for_phone="0",
+                         by_name="privacyIDEA project",
+                         by_email="privacyidea@example.com",
+                         date_from=datetime.utcnow(),
+                         date_till=datetime.utcnow() + timedelta(days=365),
+                         num_users=100,
+                         num_tokens=100,
+                         num_clients=100
+                         ).save()
+        new_response = get_webui_settings(req, resp)
+        jresult = new_response.json
+        self.assertIn("privacyidea@example.com", jresult.get("result").get("value").get("supportmail"))
+        self.assertIn(EXPIRE_MESSAGE, jresult.get("result").get("value").get("supportmail"))
+
     def test_16_init_token_defaults(self):
         g.logged_in_user = {"username": "cornelius",
                             "realm": "",