Repository: privacyidea/privacyidea

Issue:
Allow users to change token description
**Is your feature request related to a problem? Please describe.**

Users have several tokens and need to distinguish the tokens by changing the description.

**Describe the solution you'd like**

An additional user policy should allow users to change the description of their own tokens.

Description is currently set via an endpoint, which is "too mighty", ``/token/set`` and can also be used to set a lot of other token options. THis is why users were not allowed in the past to do so.

So we should add a new endpoint like ``/token/description`` to set the description of the token.


Discussion:

Resolving patch:
diff --git a/privacyidea/api/token.py b/privacyidea/api/token.py
--- a/privacyidea/api/token.py
+++ b/privacyidea/api/token.py
@@ -673,6 +673,31 @@ def setpin_api(serial=None):
     return send_result(res)
 
 
+@token_blueprint.route('/description', methods=['POST'])
+@token_blueprint.route('/description/<serial>', methods=['POST'])
+@prepolicy(check_base_action, request, action=ACTION.SETDESCRIPTION)
+@event("token_set", request, g)
+@log_with(log)
+def set_description_api(serial=None):
+    """
+    This endpoint can be used by the user or by the admin to set
+    the description of a token.
+
+    :jsonparam basestring description: The description for the token
+    :param serial:
+    :return:
+    """
+    user = request.User
+    if not serial:
+        serial = getParam(request.all_data, "serial", required)
+    g.audit_object.log({"serial": serial})
+    description = getParam(request.all_data, "description", optional=required)
+    g.audit_object.add_to_log({'action_detail': u"description={0!r}".format(description)})
+    res = set_description(serial, description, user=user)
+    g.audit_object.log({"success": True})
+    return send_result(res)
+
+
 @token_blueprint.route('/set', methods=['POST'])
 @token_blueprint.route('/set/<serial>', methods=['POST'])
 @prepolicy(check_base_action, request, action=ACTION.SET)
diff --git a/privacyidea/lib/policy.py b/privacyidea/lib/policy.py
--- a/privacyidea/lib/policy.py
+++ b/privacyidea/lib/policy.py
@@ -274,6 +274,7 @@ class ACTION(object):
     RESYNC = "resync"
     REVOKE = "revoke"
     SET = "set"
+    SETDESCRIPTION = "setdescription"
     SETPIN = "setpin"
     SETREALM = "setrealm"
     SERIAL = "serial"
@@ -1463,6 +1464,10 @@ def get_static_policy_definitions(scope=None):
                                       "PIN of his tokens."),
                             'mainmenu': [MAIN_MENU.TOKENS],
                             'group': GROUP.PIN},
+            ACTION.SETDESCRIPTION: {'type': 'bool',
+                                    'desc': _('The user is allowed to set the token description.'),
+                                    'mainmenu': [MAIN_MENU.TOKENS],
+                                    'group': GROUP.TOKEN},
             ACTION.ENROLLPIN: {'type': 'bool',
                                "desc": _("The user is allowed to set the OTP "
                                          "PIN during enrollment."),


Test patch:
diff --git a/tests/test_api_token.py b/tests/test_api_token.py
--- a/tests/test_api_token.py
+++ b/tests/test_api_token.py
@@ -1596,6 +1596,65 @@ def test_28_enroll_app_with_image_url(self):
         remove_token("goog1")
         delete_policy("imgurl")
 
+    def test_29_user_set_description(self):
+        self.authenticate_selfservice_user()
+        # create a token for the user
+        r = init_token({"serial": "SETDESC01",
+                        "otpkey": self.otpkey},
+                       user=User("selfservice", "realm1"))
+        self.assertTrue(r)
+
+        # create a token, that does not belong to the user
+        r = init_token({"serial": "SETDESC02",
+                        "otpkey": self.otpkey})
+        self.assertTrue(r)
+
+        # policy: allow user to set description
+        set_policy(name="SETDESCPOL", scope=SCOPE.USER,
+                   action=ACTION.SETDESCRIPTION)
+
+        # successful set description on own token
+        with self.app.test_request_context('/token/description/SETDESC01',
+                                           method='POST',
+                                           data={"description": "New Token"},
+                                           headers={'Authorization': self.at_user}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = json.loads(res.data.decode('utf8')).get("result")
+            self.assertTrue(result.get("status"))
+            self.assertEqual(result.get("value"), 1)
+
+        # check the description of the token
+        with self.app.test_request_context('/token/',
+                                           method='GET',
+                                           data={"serial": "SETDESC01"},
+                                           headers={'Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = json.loads(res.data.decode('utf8')).get("result")
+            self.assertTrue(result.get("status"))
+            self.assertEqual(result.get("value").get("tokens")[0].get("description"),
+                             "New Token")
+
+        # fail to set description on foreign token
+        with self.app.test_request_context('/token/description',
+                                           method='POST',
+                                           data={"serial": "SETDESC02",
+                                                 "description": "new token"},
+                                           headers={'Authorization': self.at_user}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 404, res)
+            result = json.loads(res.data.decode('utf8')).get("result")
+            self.assertFalse(result.get("status"))
+            self.assertEqual(result.get("error").get("message"),
+                             u"The requested token could not be found.")
+
+        # cleanup
+        delete_policy("SETDESCPOL")
+        remove_token("SETDESC01")
+        remove_token("SETDESC02")
+
+
 class API00TokenPerformance(MyApiTestCase):
 
     token_count = 21