Repository: privacyidea/privacyidea

Issue:
Allow GET /token list to filter by active tokens
It would be helpful to be able to filter the token list by active/inactive tokens.

Example: `GET https://piserver/token/?active=True`

This would reduce the list size for users that have several disabled or otherwise inactive tokens. Also it would allow better token management using the api (similar to what we can do with token janitor on the cli).

The implementation is fairly easy as the current [get_tokens_paginate](https://github.com/privacyidea/privacyidea/blob/3d82e9c8060fe45a25103d0b0240f5888d39afa2/privacyidea/lib/token.py#L426) implementation in the token lib already has an "active" parameter. It is just a matter of exposing it as a query param on GET /token api.

I'm willing to send a PR for your evaluation.

Thank you.


Discussion:

Resolving patch:
diff --git a/privacyidea/api/token.py b/privacyidea/api/token.py
--- a/privacyidea/api/token.py
+++ b/privacyidea/api/token.py
@@ -358,6 +358,7 @@ def list_api():
     :query sortdir: asc/desc
     :query page: request a certain page
     :query assigned: Only return assigned (True) or not assigned (False) tokens
+    :query active: Only return active (True) or inactive (False) tokens
     :query pagesize: limit the number of returned tokens
     :query user_fields: additional user fields from the userid resolver of
         the owner (user)
@@ -384,6 +385,7 @@ def list_api():
     ufields = getParam(param, "user_fields", optional)
     output_format = getParam(param, "outform", optional)
     assigned = getParam(param, "assigned", optional)
+    active = getParam(param, "active", optional)
     tokeninfokey = getParam(param, "infokey", optional)
     tokeninfovalue = getParam(param, "infovalue", optional)
     tokeninfo = None
@@ -391,6 +393,8 @@ def list_api():
         tokeninfo = {tokeninfokey: tokeninfovalue}
     if assigned:
         assigned = assigned.lower() == "true"
+    if active:
+        active = active.lower() == "true"
     
     user_fields = []
     if ufields:
@@ -404,7 +408,7 @@ def list_api():
     # get list of tokens as a dictionary
     tokens = get_tokens_paginate(serial=serial, realm=realm, page=page,
                                  user=user, assigned=assigned, psize=psize,
-                                 sortby=sort, sortdir=sdir,
+                                 active=active, sortby=sort, sortdir=sdir,
                                  tokentype=tokentype,
                                  resolver=resolver,
                                  description=description,
diff --git a/privacyidea/lib/token.py b/privacyidea/lib/token.py
--- a/privacyidea/lib/token.py
+++ b/privacyidea/lib/token.py
@@ -440,7 +440,7 @@ def get_tokens_paginate(tokentype=None, realm=None, assigned=None, user=None,
     :param user: The user, whose token should be displayed
     :type user: User object
     :param serial: a pattern for matching the serial
-    :param active:
+    :param active: Returns active (True) or inactive (False) tokens
     :param resolver: A resolver name, which may contain "*" for filtering.
     :type resolver: basestring
     :param userid: A userid, which may contain "*" for filtering.


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
@@ -261,6 +261,51 @@ def test_01_list_tokens(self):
             tokenlist = result.get("value").get("tokens")
             self.assertTrue(len(tokenlist) == 1, len(tokenlist))
 
+        # prepare active tests
+        init_token({"serial": "totp1", "genkey": 1}, tokenkind="totp")
+        enable_token("totp1", enable=False)
+        # get active tokens
+        with self.app.test_request_context('/token/',
+                                           method='GET',
+                                           query_string=urlencode({
+                                               "active": True}),
+                                           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")
+            # NO token assigned, yet
+            self.assertTrue(len(tokenlist) == 1, "{0!s}".format(tokenlist))
+
+        # get inactive tokens
+        with self.app.test_request_context('/token/',
+                                           method='GET',
+                                           query_string=urlencode({
+                                               "active": False}),
+                                           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")
+            self.assertTrue(len(tokenlist) == 1, len(tokenlist))
+            token0 = tokenlist[0]
+            self.assertTrue(token0.get("serial") == "totp1", token0)
+
+        # get all tokens
+        with self.app.test_request_context('/token/',
+                                           method='GET',
+                                           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")
+            self.assertTrue(len(tokenlist) == 2, len(tokenlist))
+
+        remove_token(serial="totp1")
+
         # get tokens with a specific tokeninfo
         with self.app.test_request_context('/token/',
                                            method='GET',