Repository: privacyidea/privacyidea

Issue:
After upgrade to 2.23.2 SMS sending doesn't work
# What did you try to do?
upgrade from 2.22 to 2.23.2

what we do:
request with user from ldap user store to /validate/check:

```
curl -s -G  --data-urlencode "user=someuser" --data-urlencode 'pass=somepass' "https://10.21.14.18/validate/check"

{"jsonrpc": "2.0", "signature": "201049308401531134466178065189764571120937256495599666440236272523262083204662134655387268637499363188487990132679731070653758739645758790604682520139088908523530166515196551385947250801353401744023649917
61335985015785180973348712642884481634456623589860941400203299037026267601077802940029462910865089468985755023177633327339270482067992148997967215436282706193767404151694564368848775892562148180428656143686034291765257208083046673602651163540338616213706951511582975868905374570955954941580904582151397903198452403577024861800750169502719282326113884454626836528094982171139409841463332640108623768324753050274757", "detail": {"message": "The user authenticated against his 
userstore according to policy 'some_authentication'.", "threadid": 140115258906368}, "versionnumber": "2.22", "version": "privacyIDEA 2.22", "result": {"status": true, "value": true}, "time": 1541424060.54929, "id": 1
}
```

, we have policy which creates new SMS token when user from ldap doesn't have token assigned yet, SMS should be (and in 2.22 it is) in form +031058788 when clicking on SMS token detail, in 2.23 it is in form [+031058788] and PI tries to send SMS to this invalid number so SMS sending fails

we don't use dynamic with SMS token


Discussion:
I guess you have an event handler (Not a policy), that creates an SMS token? 
It seems you are creating this token from the userstores phone number?

Could you please provide some information on the policies and the event handler?
It looks like you have a passthru policy and some token-event-handler?

Also take a look at your resolver. The LDAP resolver allows you to configure attributes, that return lists and not strings. I assume you have set the mobile number to return multiple values?
sorry yes we have eventhandler for this
yes we are creating token from userstore phone number
we don't have anything set as multivalue attribute but as i it is in docs mobile should be multivalue attribute by default

eventhandler:

```
    action: "enroll"
    active: "true"
    condition: ""
    conditions:
        realm: "somedomain.com"
        result_value: "True"
        user_token_number: "0"
    event: "validate_check"
    handlermodule: "Token"
    ordering: "0"
    option.realm: "somedomain.com"
    option.tokentype: "sms"
    option.user: "true"
```

policy:

```
     action:
      - otppin=userstore
      - reset_all_user_tokens=true
      - passthru=userstore
      - challenge_response=totp
    scope: authentication
    realm:
      - somedomain.com
    resolver:
      - ldap_resolver
    user: ''
    active: 'true'
    check_all_resolvers: 'false'
    client: ''
    time: ''
    adminrealm: []
    priority: 2
```
resolver:

```
    type: ldapresolver
    BINDDN: "some_dn"
    AUTHTYPE: "Simple"
    LDAPBASE: "somebase"
    LDAPURI: "ldaps://somedomain.com"
    EDITABLE: "false"
    LDAPSEARCHFILTER: "(uid=*)(objectClass=inetOrgPerson)"
    UIDTYPE: "DN"
    BINDPW: "somepass"
    USERINFO: "{ "mobile" : "mobile","email" : "mail","surname" : "sn","givenname" : "givenName" }"
    TIMEOUT: "5"
    SIZELIMIT: "500"
    SCOPE: "SUBTREE"
    NOREFERRALS: "false"
    LOGINNAMEATTRIBUTE: "uid"
```
Try to set multivalue attribute in the LDAP resolver to another value, which you do not need.
Like 

~~~~python
["phone"]
~~~~

Leaving this value empty or even adding an empty list like ``[]`` will default to

~~~~python
["mobile"]
~~~~
After all, the problem arises here:

https://github.com/privacyidea/privacyidea/blob/master/privacyidea/lib/eventhandler/tokenhandler.py#L379

In this case we need a single phone number and not all mobile numbers of the user. We probably should enhance the method ``get_user_phone``.
hmm, i am not sure because i already checked this in 2.22 and 2.23 and it returns same value..., but in 2.22 it works and in 2.23 not
MultivalueAttributes in the LDAP Resolver were added in 2.21.
I recommend you follow the recommendation above and set the multivalue in the LDAP resolver to ["phone"].

(If you have a valid SLA, you are welcome to write your mail to our support@ email address. Then it is sometimes easier to help)


Resolving patch:
diff --git a/privacyidea/lib/eventhandler/tokenhandler.py b/privacyidea/lib/eventhandler/tokenhandler.py
--- a/privacyidea/lib/eventhandler/tokenhandler.py
+++ b/privacyidea/lib/eventhandler/tokenhandler.py
@@ -377,7 +377,7 @@ def do(self, action, options=None):
                         init_param["dynamic_phone"] = 1
                     else:
                         init_param['phone'] = user.get_user_phone(
-                            phone_type='mobile')
+                            phone_type='mobile', index=0)
                         if not init_param['phone']:
                             log.warning("Enrolling SMS token. But the user "
                                         "{0!r} has no mobile number!".format(user))
diff --git a/privacyidea/lib/user.py b/privacyidea/lib/user.py
--- a/privacyidea/lib/user.py
+++ b/privacyidea/lib/user.py
@@ -278,20 +278,32 @@ def info(self):
         return userInfo
     
     @log_with(log)
-    def get_user_phone(self, phone_type='phone'):
+    def get_user_phone(self, phone_type='phone', index=None):
         """
-        Returns the phone number of a user
+        Returns the phone number or a list of phone numbers of a user.
     
         :param phone_type: The type of the phone, i.e. either mobile or
                            phone (land line)
         :type phone_type: string
+        :param index: The index of the selected phone number of list of the phones of the user.
+            If the index is given, this phone number as string is returned.
+            If the index is omitted, all phone numbers are returned.
     
         :returns: list with phone numbers of this user object
         """
         userinfo = self.info
         if phone_type in userinfo:
-            log.debug("got user phone {0!r} of type {1!r}".format(userinfo[phone_type], phone_type))
-            return userinfo[phone_type]
+            phone = userinfo[phone_type]
+            log.debug("got user phone {0!r} of type {1!r}".format(phone, phone_type))
+            if type(phone) == list and index is not None:
+                if len(phone) > index:
+                    return phone[index]
+                else:
+                    log.warning("userobject ({0!r}) has not that much "
+                                "phone numbers ({1!r} of {2!r}).".format(self, index, phone))
+                    return ""
+            else:
+                return phone
         else:
             log.warning("userobject ({0!r}) has no phone of type {1!r}.".format(self, phone_type))
             return ""


Test patch:
diff --git a/tests/test_lib_resolver.py b/tests/test_lib_resolver.py
--- a/tests/test_lib_resolver.py
+++ b/tests/test_lib_resolver.py
@@ -29,8 +29,10 @@
                                       get_resolver_config,
                                       get_resolver_list,
                                       get_resolver_object, pretestresolver)
+from privacyidea.lib.realm import (set_realm, delete_realm)
 from privacyidea.models import ResolverConfig
 
+
 objectGUIDs = [
     '039b36ef-e7c0-42f3-9bf9-ca6a6c0d4d31',
     '039b36ef-e7c0-42f3-9bf9-ca6a6c0d4d77',
@@ -103,7 +105,13 @@
                                 'userPassword': 'ldaptest',
                                 "accountExpires": 9223372036854775807,
                                 "objectGUID": objectGUIDs[1],
-                                'oid': "1"}}
+                                'oid': "1"}},
+                       {"dn": 'cn=salesman,ou=example,o=test',
+                        "attributes": {'cn': 'salesman',
+                                       'givenName': 'hans',
+                                       'sn': 'Meyer',
+                                       'mobile': ['1234', '3456'],
+                                       'objectGUID': objectGUIDs[2]}}
                        ]
 # Same as above, but with curly-braced string representation of objectGUID
 # to imitate ldap3 > 2.4.1
diff --git a/tests/test_lib_user.py b/tests/test_lib_user.py
--- a/tests/test_lib_user.py
+++ b/tests/test_lib_user.py
@@ -8,7 +8,7 @@
 PWFILE2 = "tests/testdata/passwords"
 
 from .base import MyTestCase
-from privacyidea.lib.resolver import (save_resolver)
+from privacyidea.lib.resolver import (save_resolver, delete_resolver)
 from privacyidea.lib.realm import (set_realm, delete_realm)
 from privacyidea.lib.user import (User, create_user,
                                   get_username,
@@ -16,6 +16,8 @@
                                   get_user_list,
                                   split_user,
                                   get_user_from_param)
+import ldap3mock
+from test_lib_resolver import objectGUIDs, LDAPDirectory_small
 
 
 class UserTestCase(MyTestCase):
@@ -368,3 +370,43 @@ def test_17_check_nonascii_user(self):
         self.assertEqual(unicode(user_object), u'<nönäscii.SQL1@sqlrealm>')
         self.assertEqual(str(user_object), '<n\xc3\xb6n\xc3\xa4scii.SQL1@sqlrealm>')
 
+    @ldap3mock.activate
+    def test_18_user_with_several_phones(self):
+        ldap3mock.setLDAPDirectory(LDAPDirectory_small)
+        params = ({'LDAPURI': 'ldap://localhost',
+                   'LDAPBASE': 'o=test',
+                   'BINDDN': 'cn=manager,ou=example,o=test',
+                   'BINDPW': 'ldaptest',
+                   'LOGINNAMEATTRIBUTE': 'cn',
+                   'LDAPSEARCHFILTER': '(|(cn=*))',  # we use this weird search filter to get a unique resolver ID
+                   'USERINFO': '{ "username": "cn",'
+                               '"phone" : "telephoneNumber", '
+                               '"mobile" : "mobile"'
+                               ', "email" : "mail", '
+                               '"surname" : "sn", '
+                               '"givenname" : "givenName" }',
+                   'UIDTYPE': 'objectGUID',
+                   'NOREFERRALS': True,
+                   'CACHE_TIMEOUT': 0
+                   })
+        params["resolver"] = "ldapresolver"
+        params["type"] = "ldapresolver"
+        rid = save_resolver(params)
+        self.assertTrue(rid > 0)
+        (added, failed) = set_realm("ldap", ["ldapresolver"])
+        self.assertEqual(len(added), 1)
+        self.assertEqual(len(failed), 0)
+
+        u = User("salesman", "ldap")
+        # get the complete list
+        r = u.get_user_phone("mobile")
+        self.assertEqual(r, ["1234", "3456"])
+        # get the first entry
+        r = u.get_user_phone("mobile", index=0)
+        self.assertEqual(r, "1234")
+        # Index out of range
+        r = u.get_user_phone("mobile", index=2)
+        self.assertEqual(r, "")
+
+        delete_realm("ldap")
+        delete_resolver("ldapresolver")
\ No newline at end of file