Repository: spacetelescope/jwql

Issue:
Build function to hide exclusive access content in web app
Before we build an external facing production web server (#294), we should build in a convenient and secure way to hide content within the web app if the user is not authenticated.


Discussion:
When you say "content", I take it you mean specific material on a page rather than a whole page? FYI, maybe someone could see the EDB page but then not be able to submit a query, or something similar?
@laurenmarietta Yeah, that is mainly what I meant, but you raise a good point.  There may be cases where we want to hide an entire page, so we will also have to address that functionality.  Perhaps we could do something like "if user isnt authenticated, redirect to a "not authorized" page". 

Resolving patch:
diff --git a/jwql/website/apps/jwql/api_views.py b/jwql/website/apps/jwql/api_views.py
--- a/jwql/website/apps/jwql/api_views.py
+++ b/jwql/website/apps/jwql/api_views.py
@@ -56,6 +56,7 @@
 from .data_containers import get_thumbnails_by_instrument
 from .data_containers import get_thumbnails_by_proposal
 from .data_containers import get_thumbnails_by_rootname
+from .oauth import auth_required
 
 
 def all_proposals(request):
diff --git a/jwql/website/apps/jwql/oauth.py b/jwql/website/apps/jwql/oauth.py
--- a/jwql/website/apps/jwql/oauth.py
+++ b/jwql/website/apps/jwql/oauth.py
@@ -42,8 +42,10 @@ def login(request):
 import requests
 
 from authlib.django.client import OAuth
-from django.shortcuts import redirect
+from django.shortcuts import redirect, render
 
+import jwql
+from jwql.utils.constants import MONITORS
 from jwql.utils.utils import get_base_url, get_config
 
 
@@ -122,9 +124,9 @@ def authorize(request):
     return response
 
 
-def auth_required(fn):
-    """A decorator function that requires the given function to have
-    authentication through ``auth.mast`` set up.
+def auth_info(fn):
+    """A decorator function that will return user credentials along
+    with what is returned by the original function.
 
     Parameters
     ----------
@@ -133,21 +135,19 @@ def auth_required(fn):
 
     Returns
     -------
-    check_auth : function
+    user_info : function
         The decorated function
     """
 
-    @auth_info
-    def check_auth(request, user):
-        """Check if the user is authenticated through ``auth.mast``.
-        If not, perform the authorization.
+    def user_info(request, **kwargs):
+        """Store authenticated user credentials in a cookie and return
+        it.  If the user is not authenticated, store no credentials in
+        the cookie.
 
         Parameters
         ----------
         request : HttpRequest object
             Incoming request from the webpage
-        user : dict
-            A dictionary of user credentials
 
         Returns
         -------
@@ -155,20 +155,28 @@ def check_auth(request, user):
             The decorated function
         """
 
-        # If user is currently anonymous, require a login
-        if user["anon"]:
-            # Redirect to oauth login
-            redirect_uri = os.path.join(get_base_url(), 'authorize')
-            return JWQL_OAUTH.mast_auth.authorize_redirect(request, redirect_uri)
+        cookie = request.COOKIES.get("ASB-AUTH")
 
-        return fn(request, user)
+        # If user is authenticated, return user credentials
+        if cookie is not None:
+            response = requests.get(
+                'https://{}/info'.format(get_config()['auth_mast']),
+                headers={'Accept': 'application/json',
+                         'Authorization': 'token {}'.format(cookie)})
+            response = response.json()
 
-    return check_auth
+        # If user is not authenticated, return no credentials
+        else:
+            response = {'ezid': None, "anon": True}
 
+        return fn(request, response, **kwargs)
 
-def auth_info(fn):
-    """A decorator function that will return user credentials along
-    with what is returned by the original function.
+    return user_info
+
+
+def auth_required(fn):
+    """A decorator function that requires the given function to have
+    authentication through ``auth.mast`` set up.
 
     Parameters
     ----------
@@ -177,19 +185,21 @@ def auth_info(fn):
 
     Returns
     -------
-    user_info : function
+    check_auth : function
         The decorated function
     """
 
-    def user_info(request, **kwargs):
-        """Store authenticated user credentials in a cookie and return
-        it.  If the user is not authenticated, store no credentials in
-        the cookie.
+    @auth_info
+    def check_auth(request, user, **kwargs):
+        """Check if the user is authenticated through ``auth.mast``.
+        If not, perform the authorization.
 
         Parameters
         ----------
         request : HttpRequest object
             Incoming request from the webpage
+        user : dict
+            A dictionary of user credentials
 
         Returns
         -------
@@ -197,26 +207,21 @@ def user_info(request, **kwargs):
             The decorated function
         """
 
-        cookie = request.COOKIES.get("ASB-AUTH")
+        # If user is currently anonymous, require a login
+        if user['ezid']:
 
-        # If user is authenticated, return user credentials
-        if cookie is not None:
-            response = requests.get(
-                'https://{}/info'.format(get_config()['auth_mast']),
-                headers={'Accept': 'application/json',
-                         'Authorization': 'token {}'.format(cookie)})
-            response = response.json()
+            return fn(request, user, **kwargs)
 
-        # If user is not authenticated, return no credentials
         else:
-            response = {'ezid' : None, "anon": True}
+            template = 'not_authenticated.html'
+            context = {'inst': ''}
 
-        return fn(request, response, **kwargs)
+            return render(request, template, context)
 
-    return user_info
+    return check_auth
 
 
-@auth_required
+@auth_info
 def login(request, user):
     """Spawn a login process for the user
 
@@ -237,7 +242,9 @@ def login(request, user):
         Outgoing response sent to the webpage
     """
 
-    return redirect("/")
+    # Redirect to oauth login
+    redirect_uri = os.path.join(get_base_url(), 'authorize')
+    return JWQL_OAUTH.mast_auth.authorize_redirect(request, redirect_uri)
 
 
 def logout(request):
diff --git a/jwql/website/apps/jwql/views.py b/jwql/website/apps/jwql/views.py
--- a/jwql/website/apps/jwql/views.py
+++ b/jwql/website/apps/jwql/views.py
@@ -51,7 +51,9 @@
 from .data_containers import data_trending
 from .data_containers import nirspec_trending
 from .forms import FileSearchForm
-from jwql.utils.constants import JWST_INSTRUMENT_NAMES_MIXEDCASE, MONITORS
+from .oauth import auth_info, auth_required
+import jwql
+from jwql.utils.constants import JWST_INSTRUMENT_NAMES, MONITORS, JWST_INSTRUMENT_NAMES_MIXEDCASE
 from jwql.utils.utils import get_base_url, get_config
 
 FILESYSTEM_DIR = os.path.join(get_config()['jwql_dir'], 'filesystem')
@@ -138,7 +140,8 @@ def about(request):
     return render(request, template, context)
 
 
-def archived_proposals(request, inst):
+@auth_required
+def archived_proposals(request, user, inst):
     """Generate the page listing all archived proposals in the database
 
     Parameters
@@ -163,7 +166,8 @@ def archived_proposals(request, inst):
     return render(request, template, context)
 
 
-def archived_proposals_ajax(request, inst):
+@auth_required
+def archived_proposals_ajax(request, user, inst):
     """Generate the page listing all archived proposals in the database
 
     Parameters
@@ -197,7 +201,8 @@ def archived_proposals_ajax(request, inst):
     return JsonResponse(context, json_dumps_params={'indent': 2})
 
 
-def archive_thumbnails(request, inst, proposal):
+@auth_required
+def archive_thumbnails(request, user, inst, proposal):
     """Generate the page listing all archived images in the database
     for a certain proposal
 
@@ -215,6 +220,7 @@ def archive_thumbnails(request, inst, proposal):
     HttpResponse object
         Outgoing response sent to the webpage
     """
+
     # Ensure the instrument is correctly capitalized
     inst = JWST_INSTRUMENT_NAMES_MIXEDCASE[inst.lower()]
 
@@ -226,7 +232,8 @@ def archive_thumbnails(request, inst, proposal):
     return render(request, template, context)
 
 
-def archive_thumbnails_ajax(request, inst, proposal):
+@auth_required
+def archive_thumbnails_ajax(request, user, inst, proposal):
     """Generate the page listing all archived images in the database
     for a certain proposal
 
@@ -420,7 +427,8 @@ def view_header(request, inst, file):
     return render(request, template, context)
 
 
-def view_image(request, inst, file_root, rewrite=False):
+@auth_required
+def view_image(request, user, inst, file_root, rewrite=False):
     """Generate the image view page
 
     Parameters


Test patch: