Repository: privacyidea/privacyidea

Issue:
Check the client definition during creation of a policy
**Is your feature request related to a problem? Please describe.**

The function [check_ip_in_policy](https://github.com/privacyidea/privacyidea/blob/982329aa684ad096e7836e048d808aaca6ece4b9/privacyidea/lib/utils/__init__.py#L712) verifies if the given client IP is contained in the given IP settings list.
However, if the list contains an invalid IP definition, it raises a 500 Error as expected.

We should use this very same function (check_ip_in_policy) during the creation of a policy, so that we already raise an error during the saving of the policy.

**Describe the solution you'd like**

We should check this on the library level [here](https://github.com/privacyidea/privacyidea/blob/2d240464961dfdc2f1a6560ed99fa051e543c6b7/privacyidea/lib/policy.py#L1338). We can ignore the outcome and simply use the fact, that an invalid definition raises an exception:

~~~~python
    if type(client) == list:
        client = ", ".join(client)
    # It does not matter, which IP we check. An invalid "client" definition will raise an exception
    check_ip_in_policy("127.0.0.1", client.split(","))
~~~~

**Describe alternatives you've considered**

None

**Additional context**

Do not forget the start with some tests in test_api_policy.


Discussion:

Resolving patch:
diff --git a/privacyidea/lib/policy.py b/privacyidea/lib/policy.py
--- a/privacyidea/lib/policy.py
+++ b/privacyidea/lib/policy.py
@@ -188,6 +188,8 @@
 from privacyidea.lib.utils.export import (register_import, register_export)
 from privacyidea.lib.user import User
 from privacyidea.lib import _
+from netaddr import AddrFormatError
+from privacyidea.lib.error import privacyIDEAError
 import datetime
 import re
 import ast
@@ -1337,6 +1339,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 type(pinode) == list:
         pinode = ", ".join(pinode)
     # validate conditions parameter


Test patch:
diff --git a/tests/test_api_policy.py b/tests/test_api_policy.py
--- a/tests/test_api_policy.py
+++ b/tests/test_api_policy.py
@@ -26,7 +26,7 @@ def test_00_get_policy(self):
             self.assertEqual(res.content_length, 0, res)
 
         # test export with a given policy
-        set_policy("hide_welcome", scope=SCOPE.WEBUI, action=ACTION.HIDE_WELCOME)
+        set_policy("hide_welcome", scope=SCOPE.WEBUI, action=ACTION.HIDE_WELCOME, client="10.1.2.3")
         with self.app.test_request_context('/policy/export/pols.txt',
                                            method='GET',
                                            headers={'Authorization': self.at}):
@@ -41,6 +41,7 @@ def test_01_set_policy(self):
         with self.app.test_request_context('/policy/pol1',
                                            method='POST',
                                            data={"action": ACTION.NODETAILFAIL,
+                                                 "client": "10.1.2.3",
                                                  "scope": SCOPE.AUTHZ,
                                                  "check_all_resolvers": "true",
                                                  "realm": "realm1",
@@ -83,6 +84,7 @@ def test_01_set_policy(self):
                                            method='POST',
                                            data={
                                                "action": ACTION.NODETAILFAIL,
+                                               "client": "10.1.2.3",
                                                "scope": SCOPE.AUTHZ,
                                                "check_all_resolvers": "false",
                                                "priority": 5,
@@ -115,6 +117,7 @@ def test_01_set_policy(self):
         with self.app.test_request_context('/policy/polpinode',
                                            method='POST',
                                            data={"action": ACTION.NODETAILFAIL,
+                                                 "client": "10.1.2.3",
                                                  "scope": SCOPE.AUTHZ,
                                                  "pinode": "Node1",
                                                  "priority": 1,
@@ -146,6 +149,7 @@ def test_02_set_policy_conditions(self):
         with self.app.test_request_context('/policy/cond1',
                                    method='POST',
                                    json={"action": ACTION.NODETAILFAIL,
+                                         "client": "10.1.2.3",
                                          "scope": SCOPE.AUTHZ,
                                          "realm": "realm1",
                                          "conditions": [
@@ -178,6 +182,7 @@ def test_02_set_policy_conditions(self):
         with self.app.test_request_context('/policy/cond1',
                                    method='POST',
                                    json={"action": ACTION.NODETAILFAIL,
+                                         "client": "10.1.2.3",
                                          "scope": SCOPE.AUTHZ,
                                          "realm": "realm2"},
                                    headers={'Authorization': self.at}):
@@ -205,6 +210,7 @@ def test_02_set_policy_conditions(self):
         with self.app.test_request_context('/policy/cond1',
                                    method='POST',
                                    json={"action": ACTION.NODETAILFAIL,
+                                         "client": "10.1.2.3",
                                          "scope": SCOPE.AUTHZ,
                                          "conditions": [["userinfo", "type", "equals", "secure", True]],
                                          "realm": "realm2"},
@@ -232,6 +238,7 @@ def test_02_set_policy_conditions(self):
         with self.app.test_request_context('/policy/cond1',
                                    method='POST',
                                    json={"action": ACTION.NODETAILFAIL,
+                                         "client": "10.1.2.3",
                                          "scope": SCOPE.AUTHZ,
                                          "conditions": [["userinfo", "type", "equals", "secure"]],
                                          "realm": "realm2"},
@@ -242,6 +249,7 @@ def test_02_set_policy_conditions(self):
         with self.app.test_request_context('/policy/cond1',
                                    method='POST',
                                    json={"action": ACTION.NODETAILFAIL,
+                                         "client": "10.1.2.3",
                                          "scope": SCOPE.AUTHZ,
                                          "conditions": [["userinfo", "type", "equals", "secure", True, "extra"]],
                                          "realm": "realm2"},
@@ -253,6 +261,7 @@ def test_02_set_policy_conditions(self):
         with self.app.test_request_context('/policy/cond1',
                                    method='POST',
                                    json={"action": ACTION.NODETAILFAIL,
+                                         "client": "10.1.2.3",
                                          "scope": SCOPE.AUTHZ,
                                          "conditions": [["userinfo", "type", "equals", 123, False]],
                                          "realm": "realm2"},
@@ -263,6 +272,7 @@ def test_02_set_policy_conditions(self):
         with self.app.test_request_context('/policy/cond1',
                                    method='POST',
                                    json={"action": ACTION.NODETAILFAIL,
+                                         "client": "10.1.2.3",
                                          "scope": SCOPE.AUTHZ,
                                          "conditions": [["userinfo", "type", "equals", "123", "true"]],
                                          "realm": "realm2"},
@@ -274,6 +284,7 @@ def test_02_set_policy_conditions(self):
         with self.app.test_request_context('/policy/cond1',
                                    method='POST',
                                    json={"action": ACTION.NODETAILFAIL,
+                                         "client": "10.1.2.3",
                                          "scope": SCOPE.AUTHZ,
                                          "conditions": [],
                                          "realm": "realm2"},
@@ -309,7 +320,8 @@ def test_03_set_adminuser_policy(self):
                                                ACTION.POLICYREAD),
                                                "scope": SCOPE.ADMIN,
                                                "realm": "",
-                                               "adminuser": "testadmin"},
+                                               "adminuser": "testadmin",
+                                               "client": "10.1.2.3"},
                                            headers={
                                                'Authorization': self.at}):
             res = self.app.full_dispatch_request()
@@ -389,6 +401,23 @@ def test_04_policy_defs(self):
             self.assertIn("Node1", value)
             self.assertIn("Node2", value)
 
+    def test_05_invalid_client(self):
+        with self.app.test_request_context('/policy/condFalse',
+                                           method='POST',
+                                           json={
+                                               "client": "10.1.2.3.4",
+                                               "action": ACTION.NODETAILSUCCESS,
+                                               "realm": "realm1",
+                                               "conditions": [[CONDITION_SECTION.HTTP_REQUEST_HEADER,
+                                                               "User-Agent", "broken",
+                                                               "SpecialApp", True]],
+                                               "scope": SCOPE.AUTHZ},
+                                           headers={'PI-Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertEqual(res.status_code, 400)
+            data = res.json
+            result = data.get("result")
+            self.assertEqual(result['error'], {'code': 302, 'message': 'ERR302: Invalid client definition!'})
 
 class APIPolicyConditionTestCase(MyApiTestCase):
 
@@ -398,7 +427,8 @@ def test_01_check_httpheader_condition(self):
         with self.app.test_request_context('/token/init',
                                            method='POST',
                                            json={"type": "spass", "pin": "1234",
-                                                 "serial": "sp1", "user": "cornelius", "realm": "realm1"},
+                                                 "serial": "sp1", "user": "cornelius", "realm": "realm1",
+                                                 "client": "10.1.2.3"},
                                            headers={'PI-Authorization': self.at}):
             res = self.app.full_dispatch_request()
             self.assertEqual(res.status_code, 200)
@@ -406,7 +436,8 @@ def test_01_check_httpheader_condition(self):
         # test an auth request
         with self.app.test_request_context('/validate/check',
                                            method='POST',
-                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1"}):
+                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1",
+                                                 "client": "10.1.2.3"}):
             res = self.app.full_dispatch_request()
             self.assertEqual(res.status_code, 200)
             result = res.json
@@ -418,6 +449,7 @@ def test_01_check_httpheader_condition(self):
         with self.app.test_request_context('/policy/cond1',
                                            method='POST',
                                            json={"action": ACTION.NODETAILSUCCESS,
+                                                 "client": "10.1.2.3",
                                                  "realm": "realm1",
                                                  "conditions": [[CONDITION_SECTION.HTTP_REQUEST_HEADER,
                                                                  "User-Agent", "equals", "SpecialApp", True]],
@@ -430,7 +462,8 @@ def test_01_check_httpheader_condition(self):
         with self.app.test_request_context('/validate/check',
                                            method='POST',
                                            headers={"User-Agent": "somethingelse"},
-                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1"}):
+                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1",
+                                                 "client": "10.1.2.3"}):
             res = self.app.full_dispatch_request()
             self.assertEqual(res.status_code, 200)
             result = res.json
@@ -441,7 +474,8 @@ def test_01_check_httpheader_condition(self):
         with self.app.test_request_context('/validate/check',
                                            method='POST',
                                            headers={'User-Agent': 'SpecialApp'},
-                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1"}):
+                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1",
+                                                 "client": "10.1.2.3"}):
             res = self.app.full_dispatch_request()
             self.assertEqual(res.status_code, 200)
             result = res.json
@@ -451,7 +485,8 @@ def test_01_check_httpheader_condition(self):
         with self.app.test_request_context('/validate/check',
                                            method='POST',
                                            headers={"Another": "header"},
-                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1"}):
+                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1",
+                                                 "client": "10.1.2.3"}):
             res = self.app.full_dispatch_request()
             self.assertEqual(res.status_code, 403)
             result = res.json
@@ -462,7 +497,8 @@ def test_01_check_httpheader_condition(self):
         # A request without such a specific header - always has a header
         with self.app.test_request_context('/validate/check',
                                            method='POST',
-                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1"}):
+                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1",
+                                                 "client": "10.1.2.3"}):
             res = self.app.full_dispatch_request()
             self.assertEqual(res.status_code, 403)
             result = res.json
@@ -475,6 +511,7 @@ def test_01_check_httpheader_condition(self):
         with self.app.test_request_context('/policy/cond1',
                                            method='POST',
                                            json={"action": ACTION.NODETAILSUCCESS,
+                                                 "client": "10.1.2.3",
                                                  "realm": "realm1",
                                                  "conditions": [[CONDITION_SECTION.HTTP_REQUEST_HEADER,
                                                                  "User-Agent", "broken",
@@ -488,7 +525,7 @@ def test_01_check_httpheader_condition(self):
                                            method='POST',
                                            headers={"User-Agent": "SpecialApp"},
                                            json={"pass": "1234", "user": "cornelius",
-                                                 "realm": "realm1"}):
+                                                 "realm": "realm1", "client": "10.1.2.3"}):
             res = self.app.full_dispatch_request()
             self.assertEqual(res.status_code, 403)
             result = res.json
@@ -500,6 +537,7 @@ def test_01_check_httpheader_condition(self):
         with self.app.test_request_context('/policy/cond1',
                                            method='POST',
                                            json={"action": ACTION.NODETAILSUCCESS,
+                                                 "client": "10.1.2.3",
                                                  "realm": "realm1",
                                                  "conditions": [['blabla',
                                                                  "User-Agent", "equals",
@@ -513,7 +551,7 @@ def test_01_check_httpheader_condition(self):
                                            method='POST',
                                            headers={"User-Agent": "SpecialApp"},
                                            json={"pass": "1234", "user": "cornelius",
-                                                 "realm": "realm1"}):
+                                                 "realm": "realm1", "client": "10.1.2.3"}):
             res = self.app.full_dispatch_request()
             self.assertEqual(res.status_code, 403)
             result = res.json
@@ -532,7 +570,8 @@ def test_02_check_httpenvironment_condition(self):
         with self.app.test_request_context('/token/init',
                                            method='POST',
                                            json={"type": "spass", "pin": "1234",
-                                                 "serial": "sp1", "user": "cornelius", "realm": "realm1"},
+                                                 "serial": "sp1", "user": "cornelius", "realm": "realm1",
+                                                 "client": "10.1.2.3"},
                                            headers={'PI-Authorization': self.at}):
             res = self.app.full_dispatch_request()
             self.assertEqual(res.status_code, 200)
@@ -540,7 +579,8 @@ def test_02_check_httpenvironment_condition(self):
         # test an auth request
         with self.app.test_request_context('/validate/check',
                                            method='POST',
-                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1"}):
+                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1",
+                                                 "client": "10.1.2.3"}):
             res = self.app.full_dispatch_request()
             self.assertEqual(res.status_code, 200)
             result = res.json
@@ -553,6 +593,7 @@ def test_02_check_httpenvironment_condition(self):
                                            method='POST',
                                            json={"action": ACTION.NODETAILSUCCESS,
                                                  "realm": "realm1",
+                                                 "client": "10.1.2.3",
                                                  "conditions": [[CONDITION_SECTION.HTTP_ENVIRONMENT,
                                                                  "REQUEST_METHOD", "equals", "POST", True]],
                                                  "scope": SCOPE.AUTHZ},
@@ -563,7 +604,8 @@ def test_02_check_httpenvironment_condition(self):
         # A GET request will contain the details!
         with self.app.test_request_context('/validate/check',
                                            method='GET',
-                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1"}):
+                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1",
+                                                 "client": "10.1.2.3"}):
             res = self.app.full_dispatch_request()
             self.assertEqual(res.status_code, 200)
             result = res.json
@@ -573,7 +615,8 @@ def test_02_check_httpenvironment_condition(self):
         # A POST request will NOT contain the details!
         with self.app.test_request_context('/validate/check',
                                            method='POST',
-                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1"}):
+                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1",
+                                                 "client": "10.1.2.3"}):
             res = self.app.full_dispatch_request()
             self.assertEqual(res.status_code, 200)
             result = res.json
@@ -585,6 +628,7 @@ def test_02_check_httpenvironment_condition(self):
                                            method='POST',
                                            json={"action": ACTION.NODETAILSUCCESS,
                                                  "realm": "realm1",
+                                                 "client": "10.1.2.3",
                                                  "conditions": [[CONDITION_SECTION.HTTP_ENVIRONMENT,
                                                                  "NON_EXISTING", "equals", "POST", True]],
                                                  "scope": SCOPE.AUTHZ},
@@ -594,7 +638,8 @@ def test_02_check_httpenvironment_condition(self):
 
         with self.app.test_request_context('/validate/check',
                                            method='POST',
-                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1"}):
+                                           json={"pass": "1234", "user": "cornelius", "realm": "realm1",
+                                                 "client": "10.1.2.3"}):
             res = self.app.full_dispatch_request()
             self.assertEqual(res.status_code, 403)
             result = res.json