You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2020/09/03 23:54:48 UTC

[airavata-django-portal] branch AIRAVATA-3346-implement-remote-fs-abstraction-of-user-storage updated (81f0dfb -> 580967d)

This is an automated email from the ASF dual-hosted git repository.

machristie pushed a change to branch AIRAVATA-3346-implement-remote-fs-abstraction-of-user-storage
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git.


    from 81f0dfb  AIRAVATA-3342 Upgrade pip before installing dependencies from git
     new 6375d8d  AIRAVATA-3346 Support bearer token authentication with REST API
     new 580967d  AIRAVATA-3346 WIP

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 django_airavata/apps/api/authentication.py | 32 ++++++++++++++++++++++++++++++
 django_airavata/apps/api/views.py          |  4 ++--
 django_airavata/apps/auth/backends.py      | 20 +++++++++++++++++++
 django_airavata/apps/auth/utils.py         | 27 +++++++++++++++++++------
 django_airavata/middleware.py              | 28 ++++++++++----------------
 django_airavata/settings.py                |  1 +
 6 files changed, 87 insertions(+), 25 deletions(-)
 create mode 100644 django_airavata/apps/api/authentication.py


[airavata-django-portal] 01/02: AIRAVATA-3346 Support bearer token authentication with REST API

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch AIRAVATA-3346-implement-remote-fs-abstraction-of-user-storage
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 6375d8d5338a7a55af7275cf27b7274843699134
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Sun Jun 21 13:30:26 2020 -0400

    AIRAVATA-3346 Support bearer token authentication with REST API
---
 django_airavata/apps/api/authentication.py | 32 ++++++++++++++++++++++++++++++
 django_airavata/apps/auth/backends.py      | 20 +++++++++++++++++++
 django_airavata/apps/auth/utils.py         | 27 +++++++++++++++++++------
 django_airavata/middleware.py              | 28 ++++++++++----------------
 django_airavata/settings.py                |  1 +
 5 files changed, 85 insertions(+), 23 deletions(-)

diff --git a/django_airavata/apps/api/authentication.py b/django_airavata/apps/api/authentication.py
new file mode 100644
index 0000000..acda113
--- /dev/null
+++ b/django_airavata/apps/api/authentication.py
@@ -0,0 +1,32 @@
+import logging
+
+from django.contrib.auth import authenticate
+from rest_framework import authentication, exceptions
+
+from django_airavata.apps.auth.utils import get_authz_token
+
+logger = logging.getLogger(__name__)
+
+
+class OAuthAuthentication(authentication.BaseAuthentication):
+    def authenticate(self, request):
+
+        if 'HTTP_AUTHORIZATION' in request.META:
+            try:
+                user = authenticate(request=request)
+                _, token = request.META.get('HTTP_AUTHORIZATION').split()
+
+                # authz_token_middleware has already run, so must manually add
+                # the `request.authz_token` attribute
+
+                # Must pass user directly since `request.user` access will
+                # trigger this Authentication being called again, resulting in
+                # an infinite loop
+                request.authz_token = get_authz_token(request, user=user, access_token=token)
+                logger.debug(f"OAuthAuthentication authenticated user {user}")
+                return (user, token)
+            except Exception as e:
+                raise exceptions.AuthenticationFailed(
+                    "Token failed to authenticate") from e
+        else:
+            return None
diff --git a/django_airavata/apps/auth/backends.py b/django_airavata/apps/auth/backends.py
index 89827a5..4dd913d 100644
--- a/django_airavata/apps/auth/backends.py
+++ b/django_airavata/apps/auth/backends.py
@@ -35,6 +35,14 @@ class KeycloakBackend(object):
                     return None
                 self._process_token(request, token)
                 return self._process_userinfo(request, userinfo)
+            elif 'HTTP_AUTHORIZATION' in request.META:
+                bearer, token = request.META.get('HTTP_AUTHORIZATION').split()
+                if bearer != "Bearer":
+                    raise Exception("Unexpected Authorization header")
+                userinfo = self._get_userinfo_from_token(request, token)
+                # Token should be added as a request attribute (request.auth)
+                # self._process_token(request, token)
+                return self._process_userinfo(request, userinfo)
             # user is already logged in and can use refresh token
             elif request.user and not utils.is_refresh_token_expired(request):
                 logger.debug("Refreshing token...")
@@ -136,6 +144,18 @@ class KeycloakBackend(object):
         userinfo = oauth2_session.get(userinfo_url).json()
         return token, userinfo
 
+    def _get_userinfo_from_token(self, request, token):
+        client_id = settings.KEYCLOAK_CLIENT_ID
+        userinfo_url = settings.KEYCLOAK_USERINFO_URL
+        verify_ssl = settings.KEYCLOAK_VERIFY_SSL
+        oauth2_session = OAuth2Session(
+            client_id, token={'access_token': token})
+        if hasattr(settings, 'KEYCLOAK_CA_CERTFILE'):
+            oauth2_session.verify = settings.KEYCLOAK_CA_CERTFILE
+        userinfo = oauth2_session.get(
+            userinfo_url, verify=verify_ssl).json()
+        return userinfo
+
     def _process_token(self, request, token):
         # TODO validate the JWS signature
         logger.debug("token: {}".format(token))
diff --git a/django_airavata/apps/auth/utils.py b/django_airavata/apps/auth/utils.py
index abef379..d350850 100644
--- a/django_airavata/apps/auth/utils.py
+++ b/django_airavata/apps/auth/utils.py
@@ -15,15 +15,15 @@ from airavata.model.security.ttypes import AuthzToken
 from . import models
 
 
-def get_authz_token(request):
+def get_authz_token(request, user=None, access_token=None):
     """Construct AuthzToken instance from session; refresh token if needed."""
     if not is_access_token_expired(request):
-        return _create_authz_token(request)
+        return _create_authz_token(request, user=user, access_token=access_token)
     elif not is_refresh_token_expired(request):
         # Have backend reauthenticate the user with the refresh token
         user = authenticate(request)
         if user:
-            return _create_authz_token(request)
+            return _create_authz_token(request, user=user)
     return None
 
 
@@ -50,17 +50,32 @@ def get_service_account_authz_token():
         claimsMap={'gatewayID': settings.GATEWAY_ID})
 
 
-def _create_authz_token(request):
-    access_token = request.session['ACCESS_TOKEN']
-    username = request.user.username
+def _create_authz_token(request, user=None, access_token=None):
+    if access_token is None:
+        access_token = _get_access_token(request)
+    if user is None:
+        user = request.user
+    username = user.username
     gateway_id = settings.GATEWAY_ID
     return AuthzToken(accessToken=access_token,
                       claimsMap={'gatewayID': gateway_id,
                                  'userName': username})
 
 
+def _get_access_token(request):
+    if hasattr(request, 'auth') and request.auth is not None:
+        return request.auth
+    else:
+        return request.session['ACCESS_TOKEN']
+
+
 def is_access_token_expired(request):
     """Return True if access_token is not available or is expired."""
+    # If access token not stored in session, then token expiration/refreshing
+    # isn't supported. When token is provided by REST API client it is expected
+    # that the client will manage the token lifetime.
+    if 'ACCESS_TOKEN' not in request.session:
+        return False
     now = time.time()
     return not request.user.is_authenticated \
         or 'ACCESS_TOKEN' not in request.session \
diff --git a/django_airavata/middleware.py b/django_airavata/middleware.py
index e82480d..8dfaaa8 100644
--- a/django_airavata/middleware.py
+++ b/django_airavata/middleware.py
@@ -9,17 +9,15 @@ from . import utils
 logger = logging.getLogger(__name__)
 
 
+# TODO: use the pooled clients in the airavata-python-sdk directly instead of
+# these request attributes
 class AiravataClientMiddleware:
     def __init__(self, get_response):
         self.get_response = get_response
 
     def __call__(self, request):
-        # If user is logged in create an airavata api client for the request
-        if request.user.is_authenticated:
-            request.airavata_client = utils.airavata_api_client_pool
-            response = self.get_response(request)
-        else:
-            response = self.get_response(request)
+        request.airavata_client = utils.airavata_api_client_pool
+        response = self.get_response(request)
 
         return response
 
@@ -63,17 +61,13 @@ def profile_service_client(get_response):
     """
     def middleware(request):
 
-        # If user is logged in create an profile service client for the request
-        if request.user.is_authenticated:
-            request.profile_service = {
-                'group_manager': utils.group_manager_client_pool,
-                'iam_admin': utils.iamadmin_client_pool,
-                'tenant_profile': utils.tenant_profile_client_pool,
-                'user_profile': utils.user_profile_client_pool,
-            }
-            response = get_response(request)
-        else:
-            response = get_response(request)
+        request.profile_service = {
+            'group_manager': utils.group_manager_client_pool,
+            'iam_admin': utils.iamadmin_client_pool,
+            'tenant_profile': utils.tenant_profile_client_pool,
+            'user_profile': utils.user_profile_client_pool,
+        }
+        response = get_response(request)
 
         return response
 
diff --git a/django_airavata/settings.py b/django_airavata/settings.py
index be73644..5f76c73 100644
--- a/django_airavata/settings.py
+++ b/django_airavata/settings.py
@@ -234,6 +234,7 @@ PGA_URL = None
 REST_FRAMEWORK = {
     'DEFAULT_AUTHENTICATION_CLASSES': (
         'rest_framework.authentication.SessionAuthentication',
+        'django_airavata.apps.api.authentication.OAuthAuthentication',
     ),
     'DEFAULT_PERMISSION_CLASSES': (
         'rest_framework.permissions.IsAuthenticated',


[airavata-django-portal] 02/02: AIRAVATA-3346 WIP

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch AIRAVATA-3346-implement-remote-fs-abstraction-of-user-storage
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git

commit 580967df8014c77c7071036b660a9662d46dcee7
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu Sep 3 19:54:33 2020 -0400

    AIRAVATA-3346 WIP
---
 django_airavata/apps/api/views.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index d7047be..cad999e 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -10,7 +10,7 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
 from django.http import FileResponse, Http404, HttpResponse, JsonResponse
 from django.urls import reverse
 from rest_framework import mixins
-from rest_framework.decorators import action, detail_route, list_route
+from rest_framework.decorators import action, api_view, detail_route, list_route
 from rest_framework.exceptions import ParseError
 from rest_framework.renderers import JSONRenderer
 from rest_framework.response import Response
@@ -974,7 +974,7 @@ def tus_upload_finish(request):
         return exceptions.generic_json_exception_response(e, status=400)
 
 
-@login_required
+@api_view()
 def download_file(request):
     # TODO check that user has access to this file using sharing API
     data_product_uri = request.GET.get('data-product-uri', '')