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)

#1964 introduced a bug: listing tokens from the API will always send a `tokenrealm` parameter thus forcing the token query to check for token realms as well.
Newly created and unassigned tokens do not have token realm set (except through policies and/or event handler) and so won't be found by the token query.
The problem obviously arises from the *join* between the token table and the tokenrealm table.
Tokens, that have not entry in the tokenrealm table will not be contained in the join.
See here: https://github.com/privacyidea/privacyidea/blob/master/privacyidea/lib/token.py#L189

The point is, we want to see tokens without a token realm if we do *not filter* for the realm, i.e. if the realm is empty. I am not sure is this can be done in a complex sql query or sqlalchemy statement.

The thing is that ``stripped_realm`` is not None, it is ``u'`` if there is no filter for the realm!
https://github.com/privacyidea/privacyidea/blob/master/privacyidea/lib/token.py#L186

So the problem looks quite simple.

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


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
@@ -1916,6 +1916,32 @@ def test_01_number_of_tokens(self):
             result = res.json.get("result")
             self.assertEqual(result.get("value").get("count"), self.token_count)
 
+        init_token({"genkey": 1, "serial": "realmtoken"}, tokenrealms=[self.realm1])
+        toks = get_tokens(realm="*realm1*")
+        self.assertEqual(len(toks), 1)
+
+        # Request tokens in tokenrealm
+        with self.app.test_request_context('/token/',
+                                           method='GET',
+                                           data={"tokenrealm": "**"},
+                                           headers={'Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = res.json.get("result")
+            # Even if we fetch tokenrealm=** we also get all the tokens without a tokenrealm
+            self.assertEqual(result.get("value").get("count"), self.token_count + 10 + 1)
+
+        with self.app.test_request_context('/token/',
+                                           method='GET',
+                                           data={"tokenrealm": "*alm1*"},
+                                           headers={'Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = res.json.get("result")
+            self.assertEqual(result.get("value").get("count"), 1)
+
+        remove_token(serial="realmtoken")
+
     def test_02_several_requests(self):
         # Run GET challenges
         with self.app.test_request_context('/token/challenges/*',