Repository: TACC/agavepy

Issue:
Improve the way the users interact with multiple tenants/users/clients
Issue #53 addresses a new format to store credentials. The introduced format makes it possible to switch between multiple tenants.
However, users who have admin capabilities in agave may have multiple accounts and/or may have different oauth clients for different purposes.  

## Proposed changes
Currently the configurations file has entries organized by tenant id. To allow for indexing by tenant, username, and client name. The new format could be something like this:
```
{
    "current": {
        "client-1" : {
            // the configurations include tenant and user info already.
        }
    },
    "sessions": {                                                                               
        "sd2e": {                                                                   
            "admin": {                                                              
                "client-1": {                                                       
                    ...                                                        
                },                                                                  
                "client-2": {                                                       
                    ...                                                        
                }                                                                   
            },                                                                      
            "user1":{                                                               
                "client-1": {                                                       
                    ...                                                        
                },                                                                  
                "client-2": {                                                       
                    ...                                                        
                }                                                                   
            }                                                                       
        },                                                                          
        "tacc": {                                                                   
            "admin": {                                                              
                "client-1": {                                                       
                    ...                                                        
                }                                                                   
            }                                                                       
        }                                                                           
    }
}
```

Discussion:

Patch:
diff --git a/Makefile b/Makefile
index 174afb5..43a069c 100644
--- a/Makefile
+++ b/Makefile
@@ -55,7 +55,7 @@ shell: build # Start a shell inside the build environment.
 	$(DOCKER_RUN_AGAVECLI) bash
 
 tests:
-	pytest -v --cache-clear tests/
+	pytest -vv --cache-clear tests/
 
 tests-py2:
-	python2 -m pytest -v tests
+	python2 -m pytest -vv tests
diff --git a/agavepy/agave.py b/agavepy/agave.py
index ca599ce..a0d4b3a 100644
--- a/agavepy/agave.py
+++ b/agavepy/agave.py
@@ -568,7 +568,7 @@ class Agave(object):
 
         if cache_dir is None:
             cache_dir = os.path.expanduser("~/.agave")
-        save_config(cache_dir, current_context)
+        save_config(cache_dir, current_context, self.client_name)
 
 
     def list_tenants(self, tenantsurl="https://api.tacc.utexas.edu/tenants"):
@@ -609,6 +609,8 @@ class Agave(object):
 
         self.api_key, self.api_secret = clients_create(
             self.username, client_name, description, tenant_url)
+        # Save client name upon successful return of function.
+        self.client_name = client_name
 
 
     def clients_list(self):
diff --git a/agavepy/utils/cachedir_helpers.py b/agavepy/utils/cachedir_helpers.py
index 2f0af07..0345f30 100644
--- a/agavepy/utils/cachedir_helpers.py
+++ b/agavepy/utils/cachedir_helpers.py
@@ -6,6 +6,8 @@ import json
 import requests
 import sys
 import os
+from collections import defaultdict
+
 
 
 def make_cache_dir(cache_dir):
@@ -23,7 +25,7 @@ def make_cache_dir(cache_dir):
         os.makedirs(cache_dir)
 
 
-def save_config(cache_dir, current_context):
+def save_config(cache_dir, current_context, client_name):
     """ Initiate an Agave Tenant
 
     Create or switch the current context to a specified Agave tenant.
@@ -40,20 +42,35 @@ def save_config(cache_dir, current_context):
     For example:
     {
         "current": {
-            "access_token": "some-token",
-            ...
-            "username": "user"
-        },
-        "tenants": {
-            "3dem": {
-                "access_token": "other-token",
+            "client-name": {
+                "access_token": "some-token",
                 ...
                 "username": "user"
+            }
+        },
+        "sessions": {
+            "3dem": {
+                "username": {
+                    "client-name": {
+                        "access_token": "other-token",
+                        ...
+                        "username": "user"
+                    },
+                    "client-name-2": {
+                        "access_token": "some-other-token",
+                        ...
+                        "username"
+                    }
+                }
             },
             "sd2e": {
-                "acces_token": "some-token",
-                ...
-                "usernamer":user"
+                "username": {
+                    "client-name": {
+                        "acces_token": "some-token",
+                        ...
+                        "usernamer":user"
+                    }
+                }
             }
         }
     }
@@ -73,33 +90,52 @@ def save_config(cache_dir, current_context):
         with open(config_file, "r") as f:
             agave_context = json.load(f)
     else:
-        agave_context = dict()
+        agave_context = defaultdict(lambda: defaultdict(dict))
 
 
     # Set up ~/.agave/config.json
 
     # We are saving configurations for the first time so we have to set 
     # "current" and add it to "tenants".
-    if "tenants" not in agave_context:
-        # No existing tenants, so we just add the current context.
-        agave_context["current"] = current_context
+    if "sessions" not in agave_context:
+        # No current session, so we just add the current context.
+        agave_context["current"][client_name] = current_context
         
-        # Create an empty dictionary for "tenants" key.
-        agave_context["tenants"] = dict()
         # Save current tenant context.
-        agave_context["tenants"][current_context["tenantid"]] = agave_context["current"]
-    # "tenants" already exist so we just have to put the current context
-    # back in.
+        tenant_id = current_context["tenantid"]
+        username  = current_context["username"]
+
+        # Initialize fields as appropiate.
+        # Will save the saved current context, this already includes the client
+        # name.
+        agave_context["sessions"][tenant_id][username] = \
+            agave_context["current"]
+    # There are existing sessions already so we just have to properly save the
+    # current context.
     else:
-        # Save current tenant context.
-        agave_context["tenants"][agave_context["current"]["tenantid"]] = agave_context["current"]
+        # Save current tenant context to sessions.
+        # The saved client should be the only entry in the current session.
+        saved_client = list(agave_context["current"].keys())[0]
+        tenant_id = agave_context["current"][saved_client]["tenantid"]
+        username  = agave_context["current"][saved_client]["username"]
+        
+        # Initialized sessions fields if they don't already exist.
+        if tenant_id not in agave_context["sessions"].keys():
+            agave_context["sessions"][tenant_id] = dict()
+        if username not in agave_context["sessions"][tenant_id].keys():
+            agave_context["sessions"][tenant_id][username] = dict()
+
+        # Save current context on sessions.
+        agave_context["sessions"][tenant_id][username][saved_client] = \
+            agave_context["current"][saved_client]
 
-        # Save current_context as such.
-        agave_context["current"] =  current_context
+        # Save "current_context".
+        del agave_context["current"][saved_client]
+        agave_context["current"][client_name] =  current_context
 
 
     # Save data to cache dir files.
     with open(config_file, "w") as f:
-        json.dump(agave_context, f, sort_keys=True, indent=4)
+        json.dump(agave_context, f, indent=4)
     with open(current_file, "w") as f:
-        json.dump(agave_context["current"], f, sort_keys=True)
+        json.dump(agave_context["current"][client_name], f, sort_keys=True)


Test patch:
diff --git a/tests/sample_responses/sample-config.json b/tests/sample_responses/sample-config.json
index cfb1ae4..5bf44c6 100644
--- a/tests/sample_responses/sample-config.json
+++ b/tests/sample_responses/sample-config.json
@@ -1,43 +1,53 @@
 {
-  "current": {
-    "access_token": "",
-    "apikey": "",
-    "apisecret": "",
-    "baseurl": "https://api.sd2e.org",
-    "created_at": "",
-    "devurl": "",
-    "expires_at": "",
-    "expires_in": "",
-    "refresh_token": "",
-    "tenantid": "sd2e",
-    "username": ""
-  },
-  "tenants": {
-    "irec": {
-      "access_token": "",
-      "apikey": "",
-      "apisecret": "",
-      "baseurl": "https://irec.tenants.prod.tacc.cloud",
-      "created_at": "",
-      "devurl": "",
-      "expires_at": "",
-      "expires_in": "",
-      "refresh_token": "",
-      "tenantid": "irec",
-      "username": ""
+    "current": {
+        "client-3": {
+            "tenantid": "sd2e",
+            "baseurl": "https://api.sd2e.org",
+            "devurl": "",
+            "apisecret": "",
+            "apikey": "",
+            "username": "user-3",
+            "access_token": "",
+            "refresh_token": "",
+            "created_at": "",
+            "expires_in": "",
+            "expires_at": ""
+        }
     },
-    "sd2e": {
-      "access_token": "",
-      "apikey": "",
-      "apisecret": "",
-      "baseurl": "https://api.sd2e.org",
-      "created_at": "",
-      "devurl": "",
-      "expires_at": "",
-      "expires_in": "",
-      "refresh_token": "",
-      "tenantid": "sd2e",
-      "username": ""
+    "sessions": {
+        "sd2e": {
+            "user-1": {
+                "client-1": {
+                    "tenantid": "sd2e",
+                    "baseurl": "https://api.sd2e.org",
+                    "devurl": "",
+                    "apisecret": "",
+                    "apikey": "",
+                    "username": "user-1",
+                    "access_token": "",
+                    "refresh_token": "",
+                    "created_at": "",
+                    "expires_in": "",
+                    "expires_at": ""
+                }
+            }
+        },
+        "tacc.prod": {
+            "user-2": {
+                "client-2": {
+                    "tenantid": "tacc.prod",
+                    "baseurl": "https://api.tacc.utexas.edu",
+                    "devurl": "",
+                    "apisecret": "",
+                    "apikey": "",
+                    "username": "user-2",
+                    "access_token": "",
+                    "refresh_token": "",
+                    "created_at": "",
+                    "expires_in": "",
+                    "expires_at": ""
+                }
+            }
+        }
     }
-  }
 }
diff --git a/tests/save_configs_test.py b/tests/save_configs_test.py
index bb95c65..8b95504 100644
--- a/tests/save_configs_test.py
+++ b/tests/save_configs_test.py
@@ -49,10 +49,18 @@ class TestSaveConfigs:
             cache_dir = tempfile.mkdtemp()
 
             a = Agave(tenant_id="sd2e")
+            a.client_name = "client-1"
+            a.username = "user-1"
             a.save_configs(cache_dir)
-            b = Agave(tenant_id="irec")
+
+            b = Agave(tenant_id="tacc.prod")
+            b.client_name = "client-2"
+            b.username = "user-2"
             b.save_configs(cache_dir)
+
             c = Agave(tenant_id="sd2e")
+            c.client_name = "client-3"
+            c.username = "user-3"
             c.save_configs(cache_dir)
 
             with open("{}/config.json".format(cache_dir), "r") as f: