Repository: privacyidea/privacyidea

Issue:
User checking within policies can fail for LDAP resolvers with multiple login attributes
### Top-level intent

Log in as normal user to the Webui.

### Steps to reproduce

1. Define LDAP resolver with a list of allowed Loginattributes like ``sAMAccountName, objectGUID``.
2. Have a user with upper case letters in Active Directory like *HANSMEYER*
3. Have a user policy.
4. Login as user *hansmeyer*.

### Expected outcome

User should be able to login.

### Actual outcome

User gets the error message "Cannot pass user_object as well as user, resolver, realm"

### Configuration

* **privacyIDEA version**: 3.3

## Analysis

This only happens with a list of login attributes. Due to the list of login attributes the User object gets returned as it is represented in AD, i.e. with upper cases. This does not happen, if only the single LoginAttribute "sAMAccountName" is used.

So the user object containes ``HANSMEYER.resolver@realm``. While the passed attributes are ``hansmeyer`` and the resolver and realm.

The problem occurs here:
https://github.com/privacyidea/privacyidea/blob/master/privacyidea/lib/policy.py#L663

## Possible solution

We should compare the username case insensitive.
Or we should make case-sensitivity of the username comparision configurable.

**To be fixed in branch-3.3**


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
@@ -186,6 +186,7 @@
 import datetime
 import re
 import ast
+import traceback
 from six import with_metaclass, string_types
 
 log = logging.getLogger(__name__)
@@ -621,6 +622,7 @@ def list_policies(self, name=None, scope=None, realm=None, active=None,
 
         return reduced_policies
 
+    @log_with(log)
     def match_policies(self, name=None, scope=None, realm=None, active=None,
                        resolver=None, user=None, user_object=None,
                        client=None, action=None, adminrealm=None, adminuser=None, time=None,
@@ -660,10 +662,17 @@ def match_policies(self, name=None, scope=None, realm=None, active=None,
         """
         if user_object is not None:
             # if a user_object is passed, we check, if it differs from potentially passed user, resolver, realm:
-            if (user and user != user_object.login) \
-                    or (resolver and resolver != user_object.resolver) \
+            if (user and user.lower() != user_object.login.lower()) \
+                    or (resolver and resolver.lower() != user_object.resolver.lower()) \
                     or (realm and realm != user_object.realm):
-                raise ParameterError("Cannot pass user_object as well as user, resolver, realm")
+                tb_str = ''.join(traceback.format_stack())
+                log.warning("Cannot pass user_object as well as user, resolver, realm "
+                            "in policy {0!s}. "
+                            "{1!s} - {2!s}@{3!s} in resolver {4!s}".format((name, scope, action),
+                                                                           user_object, user, realm, resolver))
+                log.warning("Possible programming error: {0!s}".format(tb_str))
+                raise ParameterError("Cannot pass user_object as well as user, resolver, realm "
+                                     "in policy {0!s}".format((name, scope, action)))
             user = user_object.login
             realm = user_object.realm
             resolver = user_object.resolver
@@ -679,13 +688,11 @@ def match_policies(self, name=None, scope=None, realm=None, active=None,
                             (policy.get("time") and
                              check_time_in_range(policy.get("time"), time))
                             or not policy.get("time")]
-        log.debug("Policies after matching time: {0!s}".format(
-            reduced_policies))
+        log.debug("Policies after matching time: {0!s}".format([p.get("name") for p in reduced_policies]))
 
         # filter policies by the policy conditions
         reduced_policies = self.filter_policies_by_conditions(reduced_policies, user_object, request_headers)
-        log.debug("Policies after matching conditions: {0!s}".format(
-            reduced_policies))
+        log.debug("Policies after matching conditions".format([p.get("name") for p in reduced_policies]))
 
         if audit_data is not None:
             for p in reduced_policies:
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.3.2"
+VERSION = "3.3.3dev1"
 
 # Taken from kennethreitz/requests/setup.py
 package_directory = os.path.realpath(os.path.dirname(__file__))


Test patch: