Repository: privacyidea/privacyidea

Issue:
Unexpected results when disabling a policy from the webui
### Top-level intent

(from the webui)
A. Disabling a policy from the '/config/policies/list' page ('Active' column) wrongly deletes the contents of the 'Client' column.
B. Disabling a policy from the '/config/policies/details/<MyPolicy>' page is not taken into account after clicking on 'Disable' then 'Save Policy'.

### Steps to reproduce

A1. Create a new policy named MyPolicy
A2. Scope 'register', Priority '100'
A3. Enter an IP in the 'Client' field (for example 1.1.1.1)
A4. Save policy
A5. Disable MyPolicy from the list of policies

B1. Create a new policy named MyPolicy2
B2. Scope 'register', Priority '100'
B3. Enter an IP in the 'Client' field (for example 1.1.1.1)
B4. Save policy
B5. Edit MyPolicy2
B6. Click on 'Disable' then 'Save Policy'

### Expected outcome

A. Disabling a policy from the policy listing page should not delete the contents of the 'Client' column.
B. Disabling a policy from the edit policy page must be taken into account after clicking on 'Disable' then 'Save Policy'.

### Configuration

* **privacyIDEA version**: 3.7.2
* **Installation method**: from Ubuntu packages
* **Python version**: 3.8.10
* **Operating system**: Ubuntu 20.04
* **Webserver**: Apache/2.4.41
* **Token database**: MySQL


Discussion:
Thank you for filing an issue and sharing your observations or ideas. Please be sure to provide as many information as possible to help us working on this issue.
If you click on "Disable Policy" on the policy details the policy is directly save in disabled state.

If you *save* the policy, it is enabled. It works as designed, although it might seem unintuitive to you.
Hello,

_"B. Disabling a policy from the edit policy page must be taken into account after clicking on 'Disable' then 'Save Policy'."_

Ok, if it works as expected.

_"A. Disabling a policy from the policy listing page should not delete the contents of the 'Client' column."_

Unless I am mistaken, this point has not been addressed.

To make the point clearer, here are some screenshots:
![2022-07-27 10_49_16-Window](https://user-images.githubusercontent.com/110045809/181207490-392a44f3-50aa-495b-a304-81ad24de7c18.png)


I can confirm that the client information gets removed when disabling the policy. We will look into that.
This was added in 02/2022 v3.7 due to complains that clients could be entered in an invalid way which would only be realized, if the policy was used!
here: **https://github.com/privacyidea/privacyidea/blob/3dda06a6726efa0323201f7406f79f3aff418660/privacyidea/lib/policy.py#L1349**

When disabling the policy [set_policy](https://github.com/privacyidea/privacyidea/blob/3dda06a6726efa0323201f7406f79f3aff418660/privacyidea/lib/policy.py#L1283) is called with only the name of the policy and `active=False`.

Since all other parameters are None, they are kept unchanged. ...till the above mentioned client check was added --- by me.

I recommend the following change

old:

~~~~Python
    try:
        client = client or ""
        check_ip_in_policy("127.0.0.1", [c.strip() for c in client.split(",")])
    except AddrFormatError:
        raise privacyIDEAError(_("Invalid client definition!"), id=302)
~~~~

to:

~~~~Python
    if client is not None:
            try:
                check_ip_in_policy("127.0.0.1", [c.strip() for c in client.split(",")])
            except AddrFormatError:
                raise privacyIDEAError(_("Invalid client definition!"), id=302)
~~~~

which keeps the client as None.

I am just to lazy to write a test for it ;-)
    

Resolving patch:
diff --git a/doc/conf.py b/doc/conf.py
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -23,7 +23,7 @@
 # built documents.
 #
 # The short X.Y version.
-version = '3.7.2'
+version = '3.7.3'
 # The full version, including alpha/beta/rc tags.
 #release = '2.16dev5'
 release = version
diff --git a/privacyidea/lib/policy.py b/privacyidea/lib/policy.py
--- a/privacyidea/lib/policy.py
+++ b/privacyidea/lib/policy.py
@@ -1345,11 +1345,11 @@ def set_policy(name=None, scope=None, action=None, realm=None, resolver=None,
         resolver = ", ".join(resolver)
     if type(client) == list:
         client = ", ".join(client)
-    try:
-        client = client or ""
-        check_ip_in_policy("127.0.0.1", [c.strip() for c in client.split(",")])
-    except AddrFormatError:
-        raise privacyIDEAError(_("Invalid client definition!"), id=302)
+    if client is not None:
+        try:
+            check_ip_in_policy("127.0.0.1", [c.strip() for c in client.split(",")])
+        except AddrFormatError:
+            raise privacyIDEAError(_("Invalid client definition!"), id=302)
     if type(pinode) == list:
         pinode = ", ".join(pinode)
     # validate conditions parameter
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@
 import sys
 
 #VERSION = "2.1dev4"
-VERSION = "3.7.2"
+VERSION = "3.7.3"
 
 # Taken from kennethreitz/requests/setup.py
 package_directory = os.path.realpath(os.path.dirname(__file__))


Test patch:
diff --git a/tests/test_lib_policy.py b/tests/test_lib_policy.py
--- a/tests/test_lib_policy.py
+++ b/tests/test_lib_policy.py
@@ -1513,6 +1513,29 @@ class MockUser(object):
         self.assertEqual(sorted(d.get("set").get("hello")), ["one", "three", "two"])
         self.assertEqual(sorted(d.get("set").get("hello2")), ["*"])
 
+    def test_40_disable_policy_client_remains(self):
+        pname = "client_must_not_vanish"
+        test_ip = "1.2.3.4"
+        set_policy(pname, scope=SCOPE.AUTH,
+                   action="otppin=none", client=test_ip)
+        p = PolicyClass()
+        plist = p.list_policies(name=pname)
+        self.assertIn(test_ip, plist[0].get("client"))
+        # Now disable the policy
+        enable_policy(pname, enable=False)
+        # client is still there
+        p = PolicyClass()
+        plist = p.list_policies(name=pname)
+        self.assertIn(test_ip, plist[0].get("client"))
+        # enable policy again
+        enable_policy(pname, enable=True)
+        # client is still there
+        p = PolicyClass()
+        plist = p.list_policies(name=pname)
+        self.assertIn(test_ip, plist[0].get("client"))
+        # clean up
+        delete_policy(pname)
+
 
 class PolicyMatchTestCase(MyTestCase):
     @classmethod