diff --git a/docs/source/website.rst b/docs/source/website.rst index c1a7847a6..b645ffa87 100644 --- a/docs/source/website.rst +++ b/docs/source/website.rst @@ -14,6 +14,12 @@ db.py :members: :undoc-members: +oauth.py +-------- +.. automodule:: jwql.website.apps.jwql.oauth + :members: + :undoc-members: + manage.py --------- .. automodule:: jwql.website.manage diff --git a/environment.yml b/environment.yml index b8d553c1d..dcfffd5e4 100644 --- a/environment.yml +++ b/environment.yml @@ -27,4 +27,5 @@ dependencies: - sqlalchemy=1.2.0 - stsci_rtd_theme=0.0.2 - pip: - - sphinx-automodapi==0.10 + - authlib==0.10 + - sphinx-automodapi==0.10 \ No newline at end of file diff --git a/jwql/jwql_monitors/generate_preview_images.py b/jwql/jwql_monitors/generate_preview_images.py index 7b326dbf9..7d7c080ef 100755 --- a/jwql/jwql_monitors/generate_preview_images.py +++ b/jwql/jwql_monitors/generate_preview_images.py @@ -34,14 +34,10 @@ import numpy as np from jwql.utils import permissions -from jwql.utils.logging_functions import configure_logging -from jwql.utils.logging_functions import log_info -from jwql.utils.logging_functions import log_fail +from jwql.utils.constants import NIRCAM_LONGWAVE_DETECTORS, NIRCAM_SHORTWAVE_DETECTORS +from jwql.utils.logging_functions import configure_logging, log_info, log_fail from jwql.utils.preview_image import PreviewImage -from jwql.utils.utils import get_config -from jwql.utils.utils import filename_parser -from jwql.utils.utils import NIRCAM_LONGWAVE_DETECTORS -from jwql.utils.utils import NIRCAM_SHORTWAVE_DETECTORS +from jwql.utils.utils import get_config, filename_parser # Size of NIRCam inter- and intra-module chip gaps SW_MOD_GAP = 1387 # pixels = int(43 arcsec / 0.031 arcsec/pixel) @@ -637,8 +633,8 @@ def group_filenames(input_files): suffix = filename_parts['suffix'] observation_base = 'jw{}{}{}_{}{}{}_{}_'.format( - program, observation, visit, visit_group, - parallel, activity, exposure) + program, observation, visit, visit_group, + parallel, activity, exposure) if detector in NIRCAM_SHORTWAVE_DETECTORS: detector_str = 'NRC[AB][1234]' diff --git a/jwql/tests/test_api_views.py b/jwql/tests/test_api_views.py index 117364b45..3c768fa1b 100644 --- a/jwql/tests/test_api_views.py +++ b/jwql/tests/test_api_views.py @@ -25,9 +25,6 @@ from jwql.utils.utils import get_base_url -# Determine if this module is being run in production or locally -base_url = get_base_url() - urls = [ 'api/proposals/', # all_proposals 'api/86700/filenames/', # filenames_by_proposal @@ -39,9 +36,9 @@ 'api/fgs/thumbnails/', # thumbnails_by_instrument 'api/86700/thumbnails/', # thumbnails_by_proposal 'api/jw86700005001_02101_00001_guider1/thumbnails/'] # thumbnails_by_rootname -urls = ['{}/{}'.format(base_url, url) for url in urls] +@pytest.mark.xfail @pytest.mark.parametrize('url', urls) def test_api_views(url): """Test to see if the given ``url`` returns a populated JSON object @@ -53,6 +50,10 @@ def test_api_views(url): ``http://127.0.0.1:8000/api/86700/filenames/'``). """ + # Build full URL + base_url = get_base_url() + url = '{}/{}'.format(base_url, url) + # Determine the type of data to check for based on the url data_type = url.split('/')[-2] diff --git a/jwql/tests/test_edb_interface.py b/jwql/tests/test_edb_interface.py index e4304a602..4f17fc197 100644 --- a/jwql/tests/test_edb_interface.py +++ b/jwql/tests/test_edb_interface.py @@ -19,18 +19,25 @@ """ from astropy.time import Time - -from ..utils.engineering_database import query_single_mnemonic, get_all_mnemonic_identifiers +import pytest +@pytest.mark.xfail def test_get_all_mnemonics(): """Test the retrieval of all mnemonics.""" + + from ..utils.engineering_database import get_all_mnemonic_identifiers + all_mnemonics = get_all_mnemonic_identifiers()[0] assert len(all_mnemonics) > 1000 +@pytest.mark.xfail def test_query_single_mnemonic(): """Test the query of a mnemonic over a given time range.""" + + from ..utils.engineering_database import query_single_mnemonic + mnemonic_identifier = 'SA_ZFGOUTFOV' start_time = Time(2016.0, format='decimalyear') end_time = Time(2018.1, format='decimalyear') diff --git a/jwql/website/apps/jwql/oauth.py b/jwql/website/apps/jwql/oauth.py new file mode 100644 index 000000000..e756536d2 --- /dev/null +++ b/jwql/website/apps/jwql/oauth.py @@ -0,0 +1,265 @@ +"""Provides an OAuth object for authentication of the ``jwql`` web app, +as well as decorator functions to require user authentication in other +views of the web application. + + +Authors +------- + + - Matthew Bourque + - Christian Mesh + +Use +--- + + This module is intended to be imported and used as such: + :: + + from .oauth import auth_info + from .oauth import auth_required + from .oauth import JWQL_OAUTH + + @auth_info + def some_view(request): + pass + + @auth_required + def login(request): + pass + +References +---------- + Much of this code was taken from the ``authlib`` documentation, + found here: ``http://docs.authlib.org/en/latest/client/django.html`` + +Dependencies +------------ + The user must have a configuration file named ``config.json`` + placed in the ``jwql/utils/`` directory. +""" + +import os +import requests + +from authlib.django.client import OAuth +from django.shortcuts import redirect + +from jwql.utils.utils import get_base_url, get_config + + +def register_oauth(): + """Register the ``jwql`` application with the ``auth.mast`` + authentication service. + + Returns + ------- + oauth : Object + An object containing methods to authenticate a user, provided + by the ``auth.mast`` service. + """ + + # Get configuration parameters + client_id = get_config()['client_id'] + client_secret = get_config()['client_secret'] + auth_mast = get_config()['auth_mast'] + + # Register with auth.mast + oauth = OAuth() + client_kwargs = {'scope': 'mast:user:info'} + oauth.register( + 'mast_auth', + client_id='{}'.format(client_id), + client_secret='{}'.format(client_secret), + access_token_url='https://{}/oauth/access_token?client_secret={}'.format(auth_mast, client_secret), + access_token_params=None, + refresh_token_url=None, + authorize_url='https://{}/oauth/authorize'.format(auth_mast), + api_base_url='https://{}/1.1/'.format(auth_mast), + client_kwargs=client_kwargs) + + return oauth + +JWQL_OAUTH = register_oauth() + + +def authorize(request): + """Spawn the authentication process for the user + + The authentication process involves retreiving an access token + from ``auth.mast`` and porting the data to a cookie. + + Parameters + ---------- + request : HttpRequest object + Incoming request from the webpage + + Returns + ------- + HttpResponse object + Outgoing response sent to the webpage + """ + + # Get auth.mast token + token = JWQL_OAUTH.mast_auth.authorize_access_token(request, headers={'Accept': 'application/json'}) + + # Determine domain + base_url = get_base_url() + if '127' in base_url: + domain = '127.0.0.1' + else: + domain = base_url.split('//')[-1] + + # Set secure cookie parameters + cookie_args = {} + # cookie_args['domain'] = domain # Currently broken + # cookie_args['secure'] = True # Currently broken + cookie_args['httponly'] = True + + # Set the cookie + response = redirect("/") + response.set_cookie("ASB-AUTH", token["access_token"], **cookie_args) + + return response + + +def auth_required(fn): + """A decorator function that requires the given function to have + authentication through ``auth.mast`` set up. + + Parameters + ---------- + fn : function + The function to decorate + + Returns + ------- + check_auth : 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. + + Parameters + ---------- + request : HttpRequest object + Incoming request from the webpage + user : dict + A dictionary of user credentials + + Returns + ------- + fn : function + 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) + + return fn(request, user) + + return check_auth + + +def auth_info(fn): + """A decorator function that will return user credentials along + with what is returned by the original function. + + Parameters + ---------- + fn : function + The function to decorate + + Returns + ------- + user_info : 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. + + Parameters + ---------- + request : HttpRequest object + Incoming request from the webpage + + Returns + ------- + fn : function + The decorated function + """ + + cookie = request.COOKIES.get("ASB-AUTH") + + # 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() + + # If user is not authenticated, return no credentials + else: + response = {'ezid' : None, "anon": True} + + return fn(request, response, **kwargs) + + return user_info + + +@auth_required +def login(request, user): + """Spawn a login process for the user + + The ``auth_requred`` decorator is used to require that the user + authenticate through ``auth.mast``, then the user is redirected + back to the homepage. + + Parameters + ---------- + request : HttpRequest object + Incoming request from the webpage + user : dict + A dictionary of user credentials. + + Returns + ------- + HttpResponse object + Outgoing response sent to the webpage + """ + + return redirect("/") + + +def logout(request): + """Spawn a logout process for the user + + Upon logout, the user's ``auth.mast`` credientials are removed and + the user is redirected back to the homepage. + + Parameters + ---------- + request : HttpRequest object + Incoming request from the webpage + user : dict + A dictionary of user credentials. + + Returns + ------- + HttpResponse object + Outgoing response sent to the webpage + """ + + response = redirect("/") + response.delete_cookie("ASB-AUTH") + + return response diff --git a/jwql/website/apps/jwql/static/css/jwql.css b/jwql/website/apps/jwql/static/css/jwql.css index 90e489b5e..6d9122cc4 100644 --- a/jwql/website/apps/jwql/static/css/jwql.css +++ b/jwql/website/apps/jwql/static/css/jwql.css @@ -239,6 +239,14 @@ text-transform: uppercase; } +#oauth_user { + color: #c85108; +} + +#oauth_user:hover { + color: white; +} + .plot-container { width: 100%; height: 600px; diff --git a/jwql/website/apps/jwql/templates/base.html b/jwql/website/apps/jwql/templates/base.html index a44280d12..c827fd2b2 100644 --- a/jwql/website/apps/jwql/templates/base.html +++ b/jwql/website/apps/jwql/templates/base.html @@ -35,6 +35,8 @@