Repository: privacyidea/privacyidea

Issue:
No PIN field for Admin Rollover 
If an admin does a token-rollover for a user, no PIN field will be shown. The admin only ever sees a PIN field when enrolling tokens (admin PIN policies) or assigning users to tokens (which PIN policies apply then?).
A user PIN policy will be ignored for an admin rollover.
A rollover will fail if an admin PIN policy is set (even though the admin won't see a PIN field).



Discussion:
Is this the intended behaviour? Does it currently work differently? Please describe.

In which situation would an admin do a rollover?

@plettich I think if an admin does a token rollover, I would not expect them to *change* the PIN of the token. 

Ah, ok, but if a PIN policy is set, it fails? I would consider this a bug. 
My expectation would be:
* The admin can not set a PIN (since e.g. the user had set a PIN, which the admin does not know)
* No new PIN is set, the old PIN is unchanged. - No PIN change during rollover. 

We probably need to pass this information that it is a rollover and the PIN policy is not checked. Maybe we can identify this by the existing serial number...

## Implementation

Ignore the PIN policy and keep the old PIN.

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
@@ -320,10 +320,14 @@ def check_otp_pin(request=None, action=None):
     raised.
     """
     params = request.all_data
-    realm = params.get("realm")
     pin = params.get("otppin", "") or params.get("pin", "")
     serial = params.get("serial")
     tokentype = params.get("type")
+    rollover = params.get("rollover")
+    verify = params.get("verify")
+    if rollover or verify:
+        log.debug(f"Disable PIN checking due to rollover ({rollover}) or verify ({verify})")
+        return True
     if not serial and action == ACTION.SETPIN:
         path_elems = request.path.split("/")
         serial = path_elems[-1]
@@ -1525,12 +1529,12 @@ def u2ftoken_allowed(request, action):
      action=U2FACTION.REQ it checks, if the assertion certificate is an
      allowed U2F token type.
 
-     If the token, which is enrolled contains a non allowed attestation 
+     If the token, which is enrolled contains a non allowed attestation
      certificate, we bail out.
 
-    :param request: 
-    :param action: 
-    :return: 
+    :param request:
+    :param action:
+    :return:
     """
 
     ttype = request.all_data.get("type")
@@ -1568,7 +1572,7 @@ def u2ftoken_allowed(request, action):
 
 def allowed_audit_realm(request=None, action=None):
     """
-    This decorator function takes the request and adds additional parameters 
+    This decorator function takes the request and adds additional parameters
     to the request according to the policy
     for the SCOPE.ADMIN or ACTION.AUDIT
     :param request:


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
@@ -5,7 +5,8 @@
 The api.lib.policy.py depends on lib.policy and on flask!
 """
 import json
-
+import logging
+from testfixtures import log_capture
 from privacyidea.lib.tokens.webauthn import (webauthn_b64_decode, AUTHENTICATOR_ATTACHMENT_TYPE,
                                              ATTESTATION_LEVEL, ATTESTATION_FORM,
                                              USER_VERIFICATION_LEVEL)
@@ -946,7 +947,8 @@ def test_08b_enroll_pin_user(self):
         # finally delete policy
         delete_policy("pol1")
 
-    def test_09_pin_policies(self):
+    @log_capture(level=logging.DEBUG)
+    def test_09_pin_policies(self, capture):
         create_realm("home", [self.resolvername1])
         g.logged_in_user = {"username": "user1",
                             "realm": "",
@@ -1014,11 +1016,32 @@ def test_09_pin_policies(self):
                         "pin": ""}
         self.assertTrue(check_otp_pin(req))
 
+        # check that no pin is checked during rollover or verify
+        logging.getLogger('privacyidea').setLevel(logging.DEBUG)
+        req.all_data = {
+            "user": "cornelius",
+            "realm": "home",
+            "rollover": True
+        }
+        self.assertTrue(check_otp_pin(req))
+        capture.check_present(("privacyidea.api.lib.prepolicy", "DEBUG",
+                               "Disable PIN checking due to rollover (True) or verify (None)"))
+        req.all_data = {
+            "user": "cornelius",
+            "realm": "home",
+            "verify": 123456
+        }
+        self.assertTrue(check_otp_pin(req))
+        capture.check_present(("privacyidea.api.lib.prepolicy", "DEBUG",
+                               "Disable PIN checking due to rollover (None) or verify (123456)"))
+        logging.getLogger('privacyidea').setLevel(logging.INFO)
+
         # finally delete policy
         delete_policy("pol1")
         delete_realm("home")
 
-    def test_09_pin_policies_admin(self):
+    @log_capture(level=logging.DEBUG)
+    def test_09_pin_policies_admin(self, capture):
         create_realm("home", [self.resolvername1])
         g.logged_in_user = {"username": "super",
                             "realm": "",
@@ -1084,6 +1107,26 @@ def test_09_pin_policies_admin(self):
                         "pin": ""}
         self.assertTrue(check_otp_pin(req))
 
+        # check that no pin is checked during rollover or verify
+        logging.getLogger('privacyidea').setLevel(logging.DEBUG)
+        req.all_data = {
+            "user": "cornelius",
+            "realm": "home",
+            "rollover": True
+        }
+        self.assertTrue(check_otp_pin(req))
+        capture.check_present(("privacyidea.api.lib.prepolicy", "DEBUG",
+                               "Disable PIN checking due to rollover (True) or verify (None)"))
+        req.all_data = {
+            "user": "cornelius",
+            "realm": "home",
+            "verify": 123456
+        }
+        self.assertTrue(check_otp_pin(req))
+        capture.check_present(("privacyidea.api.lib.prepolicy", "DEBUG",
+                               "Disable PIN checking due to rollover (None) or verify (123456)"))
+        logging.getLogger('privacyidea').setLevel(logging.INFO)
+
         # finally delete policy
         delete_policy("pol1")
         delete_realm("home")