Repository: privacyidea/privacyidea

Issue:
Filter token realms in token view
The following additional filter attributes in token list would be great:

- user
- realm

Additionally, the REST API documentation for GET /TOKEN should be altered:

- `tokenrealm` parameter is actually used, not `viewrealm` like mentioned in code documentation


Discussion:
This can be very complex and time consuming, since the username is not stored in the token table. 
We would fetch all user objects from LDAP, which look like the entered values. Then we would have to fetch all tokens for this list of users.

This is intentionally not implemented.
I recommend to got to the user list, search for the user and the go to the user details to see his tokens.

The docstring is changed from viewreal -> tokenrealm. Thx! :+1: 

Filter only realms of assigned tokens,

Discuss...
* [x] We add a column "token realm" in the WebUI.
* [x] We add the possibility to search for tokens with a specified user realm or token realm.
The list of the tokens already contains the keys ``user_realm`` of the assigned used and the key ``realms`` which is a list of the token realms.

The API ``GET /token`` already accepts the parameter ``tokenrealm`` which return tokens with are located in the given realm.

As the user_realm is usually also a tokenrealm, we only add in a first step to search in the tokenrealm - otherwise we would have to extend the API.

Searching for the tokenrealm is already implemented in the API - but not to search with wildcards.
So we also add searching for wildcards.

Looks like this:
![image](https://user-images.githubusercontent.com/1908620/70713200-c5369a80-1ce5-11ea-8b11-5890d20d8c86.png)


Resolving patch:
diff --git a/privacyidea/lib/token.py b/privacyidea/lib/token.py
--- a/privacyidea/lib/token.py
+++ b/privacyidea/lib/token.py
@@ -182,13 +182,20 @@ def _create_token_query(tokentype=None, realm=None, assigned=None, user=None,
         else:
             log.warning("assigned value not in [True, False] {0!r}".format(assigned))
 
-    if realm is not None:
+    stripped_realm = None if realm is None else realm.strip("*")
+    if stripped_realm is not None:
         # filter for the realm
-        sql_query = sql_query.filter(and_(func.lower(Realm.name) ==
-                                          realm.lower(),
-                                          TokenRealm.realm_id == Realm.id,
-                                          TokenRealm.token_id ==
-                                          Token.id)).distinct()
+        if "*" in realm:
+            sql_query = sql_query.filter(and_(func.lower(Realm.name).like(realm.replace("*", "%").lower()),
+                                              TokenRealm.realm_id == Realm.id,
+                                              TokenRealm.token_id ==
+                                              Token.id)).distinct()
+        else:
+            # exact matching
+            sql_query = sql_query.filter(and_(func.lower(Realm.name) == realm.lower(),
+                                              TokenRealm.realm_id == Realm.id,
+                                              TokenRealm.token_id ==
+                                              Token.id)).distinct()
 
     if allowed_realms is not None:
         sql_query = sql_query.filter(and_(func.lower(Realm.name).in_([r.lower() for r in allowed_realms]),


Test patch:
diff --git a/tests/test_api_token.py b/tests/test_api_token.py
--- a/tests/test_api_token.py
+++ b/tests/test_api_token.py
@@ -352,6 +352,24 @@ def test_03_list_tokens_in_one_realm(self):
             self.assertTrue(len(tokenlist) == 2, res.data)
             self.assertTrue(count == 2, count)
 
+        # list tokens, that look a bit like realm1
+        search_realm = self.realm1[:-1] + "*"
+        with self.app.test_request_context('/token/',
+                                           method='GET',
+                                           query_string=urlencode({
+                                               "tokenrealm": search_realm}),
+                                           headers={'Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = res.json.get("result")
+            detail = res.json.get("detail")
+            tokenlist = result.get("value").get("tokens")
+            count = result.get("value").get("count")
+            next = result.get("value").get("next")
+            prev = result.get("value").get("prev")
+            self.assertTrue(len(tokenlist) == 2, res.data)
+            self.assertTrue(count == 2, count)
+
     def test_04_assign_unassign_token(self):
         with self.app.test_request_context('/token/assign',
                                            method='POST',