You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by sm...@apache.org on 2020/05/01 03:07:19 UTC

[airavata-custos-portal] 08/20: Added username and password login, allow access to Admin only when user has admin role

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

smarru pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/airavata-custos-portal.git

commit 81b4c684fa5b8fec3f44136f53f4aef74dc93e0f
Author: Shivam Rastogi <sh...@yahoo.com>
AuthorDate: Thu Mar 26 02:56:04 2020 -0400

    Added username and password login, allow access to Admin only when user has admin role
---
 custos_portal/custos_portal/app_config.py          |  4 ++
 custos_portal/custos_portal/apps/admin/apps.py     | 12 +++++-
 custos_portal/custos_portal/apps/auth/backends.py  | 39 ++++++++++-------
 .../custos_portal/apps/auth/middleware.py          | 11 +++++
 custos_portal/custos_portal/apps/auth/urls.py      |  2 +
 custos_portal/custos_portal/apps/auth/views.py     | 49 +++++++++++++++++++---
 custos_portal/custos_portal/apps/workspace/apps.py |  9 ++--
 custos_portal/custos_portal/context_processors.py  | 16 ++++++-
 custos_portal/custos_portal/settings.py            |  2 +
 custos_portal/custos_portal/settings_local.py      |  3 +-
 custos_portal/custos_portal/templates/base.html    |  2 +-
 .../partials/username_password_login_form.html     |  2 +-
 12 files changed, 118 insertions(+), 33 deletions(-)

diff --git a/custos_portal/custos_portal/app_config.py b/custos_portal/custos_portal/app_config.py
index b99cf99..6eecec9 100644
--- a/custos_portal/custos_portal/app_config.py
+++ b/custos_portal/custos_portal/app_config.py
@@ -40,6 +40,9 @@ class CustosAppConfig(AppConfig, ABC):
         """Some user friendly text to briefly describe the application."""
         pass
 
+    @abstractmethod
+    def app_enabled(self, request):
+        pass
 
 def enhance_custom_app_config(app):
     """As necessary add default values for properties to custom AppConfigs."""
@@ -99,3 +102,4 @@ def get_app_description(app_config):
 
 def get_app_urls(app_config):
     return import_module(".urls", app_config.name)
+
diff --git a/custos_portal/custos_portal/apps/admin/apps.py b/custos_portal/custos_portal/apps/admin/apps.py
index 76b2521..7e63812 100644
--- a/custos_portal/custos_portal/apps/admin/apps.py
+++ b/custos_portal/custos_portal/apps/admin/apps.py
@@ -16,6 +16,14 @@ class AdminConfig(CustosAppConfig):
             'label': 'Application Catalog',
             'icon': 'fa fa-list',
             'url': 'custos_portal_admin:list_requests',
-            'active_prefixes': ['applications', 'list-requests']
+            'active_prefixes': ['applications', 'list-requests'],
+            'enabled': lambda req: (req.is_gateway_admin or
+                                    req.is_read_only_gateway_admin),
         }
-    ]
\ No newline at end of file
+    ]
+
+    def app_enabled(self, request):
+        if hasattr(request, "is_gateway_admin") and request.is_gateway_admin:
+            return True
+        else:
+            return False
diff --git a/custos_portal/custos_portal/apps/auth/backends.py b/custos_portal/custos_portal/apps/auth/backends.py
index 5f2df5a..2db7af3 100644
--- a/custos_portal/custos_portal/apps/auth/backends.py
+++ b/custos_portal/custos_portal/apps/auth/backends.py
@@ -21,18 +21,22 @@ class CustosAuthBackend(ModelBackend):
     @sensitive_variables('password')
     def authenticate(self, request=None, username=None, password=None, refresh_token=None):
         try:
+            userinfo = None
             if username and password:
-                token = self._get_token_and_userinfo_password_flow(
-                    username, password)
+                token = self._get_token_and_userinfo_password_flow(username, password)
+                request.session["ACCESS_TOKEN"] = token
                 userinfo = self._get_userinfo_from_token(token)
-                self._process_token(request, token)
-                return self._process_userinfo(request, userinfo)
-            # user is already logged in and can use refresh token
+                self._get_user_groups(request, token)
+
+            # user login using CIlogon
             else:
                 token = self._get_token_and_userinfo_redirect_flow(request)
-                userinfo = self._get_userinfo_from_token(token)
+                # the custos api returns different token responses for 'authenticate' and 'token' methods
+                userinfo = self._get_userinfo_from_token(token["access_token"])
                 self._process_token(request, token)
-                return self._process_userinfo(request, userinfo)
+                self._get_user_groups(request, token["access_token"])
+
+            return self._process_userinfo(request, userinfo)
         except Exception as e:
             logger.exception("login failed")
             return None
@@ -45,13 +49,12 @@ class CustosAuthBackend(ModelBackend):
             return None
 
     def _get_token_and_userinfo_password_flow(self, username, password):
-        token = identity_management_client.authenticate(settings.CUSTOS_TOKEN, username, password)
-        print(type(token))
-        logger.info(token["access_token"])
+        response = identity_management_client.authenticate(settings.CUSTOS_TOKEN, username, password)
 
-        # TODO: Add user info
-        # userinfo = None
-        return token, userinfo
+        token = MessageToDict(response)["accessToken"]
+
+        logger.debug("Token: {}".format(token))
+        return token
 
     def _get_token_and_userinfo_redirect_flow(self, request):
 
@@ -75,7 +78,6 @@ class CustosAuthBackend(ModelBackend):
 
     def _process_token(self, request, token):
         # TODO validate the JWS signature
-        logger.debug("token: {}".format(token))
         now = time.time()
         # Put access_token into session to be used for authenticating with API
         # server
@@ -88,14 +90,19 @@ class CustosAuthBackend(ModelBackend):
     def _get_userinfo_from_token(self, token):
         userinfo = {}
 
-        decoded_id_token = jwt.decode(token["id_token"], verify=False)
-
+        decoded_id_token = jwt.decode(token, verify=False)
         userinfo["username"] = decoded_id_token["preferred_username"]
         userinfo["first_name"] = decoded_id_token["given_name"]
         userinfo["last_name"] = decoded_id_token["family_name"]
         userinfo["email"] = decoded_id_token["email"]
         return userinfo
 
+    def _get_user_groups(self, request, access_token):
+        decoded_id_token = jwt.decode(access_token, verify=False)
+        user_groups = decoded_id_token["realm_access"]["roles"]
+        request.session["GATEWAY_GROUPS"] = user_groups
+        request.is_gateway_admin = 'admin' in user_groups
+
     def _process_userinfo(self, request, userinfo):
         logger.debug("Userinfo: {}".format(userinfo))
 
diff --git a/custos_portal/custos_portal/apps/auth/middleware.py b/custos_portal/custos_portal/apps/auth/middleware.py
index e69de29..f9bf02e 100644
--- a/custos_portal/custos_portal/apps/auth/middleware.py
+++ b/custos_portal/custos_portal/apps/auth/middleware.py
@@ -0,0 +1,11 @@
+
+
+def gateway_groups_middleware(get_response):
+    """Add 'is_gateway_admin' and 'is_read_only_gateway_admin' to request."""
+    def middleware(request):
+        request.is_gateway_admin = False
+        if request.user.is_authenticated and request.session.get('GATEWAY_GROUPS'):
+            gateway_groups = request.session['GATEWAY_GROUPS']
+            request.is_gateway_admin = 'admin' in gateway_groups
+        return get_response(request)
+    return middleware
diff --git a/custos_portal/custos_portal/apps/auth/urls.py b/custos_portal/custos_portal/apps/auth/urls.py
index 6b339d0..ef1de64 100644
--- a/custos_portal/custos_portal/apps/auth/urls.py
+++ b/custos_portal/custos_portal/apps/auth/urls.py
@@ -12,5 +12,7 @@ urlpatterns = [
     url(r'^callback/$', views.callback, name='callback'),
     url(r'^callback-error/(?P<idp_alias>\w+)/$', views.callback_error,
         name='callback-error'),
+    url(r'handle-login', views.handle_login, name="handle_login"),
+    url(r'^logout$', views.start_logout, name='logout'),
 
 ]
diff --git a/custos_portal/custos_portal/apps/auth/views.py b/custos_portal/custos_portal/apps/auth/views.py
index 4afc3b1..dc2e688 100644
--- a/custos_portal/custos_portal/apps/auth/views.py
+++ b/custos_portal/custos_portal/apps/auth/views.py
@@ -6,10 +6,10 @@ from clients.identity_management_client import IdentityManagementClient
 from clients.user_management_client import UserManagementClient
 from django.conf import settings
 from django.contrib import messages
-from django.contrib.auth import authenticate, login
+from django.contrib.auth import authenticate, login, logout
 from django.core.exceptions import ValidationError
 from django.forms import formset_factory
-from django.shortcuts import render, redirect
+from django.shortcuts import render, redirect, resolve_url
 from django.urls import reverse
 from requests_oauthlib import OAuth2Session
 
@@ -66,10 +66,8 @@ def callback(request):
     try:
         user = authenticate(request=request)
         logger.debug("Saving user to session: {}".format(user))
-
         login(request, user)
-
-        return redirect(reverse('custos_portal_workspace:list_requests'))
+        return _handle_login_redirect(request)
     except Exception as err:
         logger.exception("An error occurred while processing OAuth2 "
                          "callback: {}".format(request.build_absolute_uri()))
@@ -94,6 +92,45 @@ def callback_error(request, idp_alias):
     })
 
 
+def handle_login(request):
+    username = request.POST['username']
+    password = request.POST['password']
+    login_type = request.POST.get('login_type', None)
+    template = "custos_portal_auth/login.html"
+    if login_type and login_type == 'password':
+        template = "custos_portal_auth/login_username_password.html"
+    user = authenticate(username=username, password=password, request=request)
+    logger.debug("authenticated user: {}".format(user))
+    try:
+        if user is not None:
+            login(request, user)
+            return _handle_login_redirect(request)
+        else:
+            messages.error(request, "Login failed. Please try again.")
+    except Exception as err:
+        messages.error(request,
+                       "Login failed: {}. Please try again.".format(str(err)))
+    return render(request, template, {
+        'username': username,
+        'next': request.POST.get('next', None),
+        'options': settings.AUTHENTICATION_OPTIONS,
+        'login_type': login_type,
+    })
+
+
+def _handle_login_redirect(request):
+    if request.is_gateway_admin:
+        return redirect(reverse('custos_portal_admin:list_requests'))
+    else:
+        return redirect(reverse('custos_portal_workspace:list_requests'))
+
+
+def start_logout(request):
+    logout(request)
+    redirect_url = request.build_absolute_uri(resolve_url(settings.LOGOUT_REDIRECT_URL))
+    return redirect(settings.KEYCLOAK_LOGOUT_URL + "?redirect_uri=" + quote(redirect_url))
+
+
 def create_account(request):
     print("Create account is called")
     if request.method == 'POST':
@@ -107,7 +144,7 @@ def create_account(request):
                 password = form.cleaned_data['password']
                 is_temp_password = True
                 result = user_management_client.register_user(settings.CUSTOS_TOKEN,
-                                                              username, email, first_name, last_name, password,
+                                                              username, first_name, last_name, password, email,
                                                               is_temp_password)
                 if result.is_registered:
                     messages.success(
diff --git a/custos_portal/custos_portal/apps/workspace/apps.py b/custos_portal/custos_portal/apps/workspace/apps.py
index cfafc19..3b433d0 100644
--- a/custos_portal/custos_portal/apps/workspace/apps.py
+++ b/custos_portal/custos_portal/apps/workspace/apps.py
@@ -16,12 +16,15 @@ class WorkspaceConfig(CustosAppConfig):
             'label': 'Create new tenant request',
             'icon': 'fa fa-plus-square',
             'url': 'custos_portal_workspace:request_new_tenant',
-            'active_prefixes': ['applications', 'request-new-tenant']
+            'active_prefixes': ['applications', 'request-new-tenant'],
         },
         {
             'label': 'List of all existing tenant requests',
             'icon': 'fa fa-list',
             'url': 'custos_portal_workspace:list_requests',
-            'active_prefixes': ['applications', 'list-requests']
+            'active_prefixes': ['applications', 'list-requests'],
         }
-    ]
\ No newline at end of file
+    ]
+
+    def app_enabled(self, request):
+        return True
\ No newline at end of file
diff --git a/custos_portal/custos_portal/context_processors.py b/custos_portal/custos_portal/context_processors.py
index f106bf8..e9e48ae 100644
--- a/custos_portal/custos_portal/context_processors.py
+++ b/custos_portal/custos_portal/context_processors.py
@@ -18,15 +18,24 @@ id_client = IdentityManagementClient()
 
 token = "Y3VzdG9zLTZud29xb2RzdHBlNW12Y3EwOWxoLTEwMDAwMTAxOkdpS3JHR1ZMVzd6RG9QWnd6Z0NpRk03V1V6M1BoSXVtVG1GeEFrcjc=";
 
+
 def register_user():
 
     response = client.register_user(token, "TestingUser", "Jhon", "Smith", "12345", "jhon@iu.edu", True)
     print(response)
 
+
 def airavata_app_registry(request):
     """Put airavata django apps into the context."""
     airavata_apps = [app for app in apps.get_app_configs()
-                     if isinstance(app, CustosAppConfig)]
+                     if isinstance(app, CustosAppConfig) and
+                     (app.app_enabled(request)
+                      )]
+    for app in apps.get_app_configs():
+        if isinstance(app, CustosAppConfig):
+            print(app.url_app_name)
+            print(getattr(app, 'enabled', None))
+            print(app.app_enabled(request))
     print("Custos apps", airavata_apps)
     # Sort by app_order then by verbose_name (case-insensitive)
     airavata_apps.sort(
@@ -66,7 +75,10 @@ def _get_current_app(request, apps):
 
 def _get_app_nav(request, current_app):
     if hasattr(current_app, 'nav'):
-        nav = copy.copy(current_app.nav)
+        # Copy and filter current_app's nav items
+        nav = [item
+               for item in copy.copy(current_app.nav)
+               if 'enabled' not in item or item['enabled'](request)]
         # convert "/djangoapp/path/in/app" to "path/in/app"
         app_path = "/".join(request.path.split("/")[2:])
         print(app_path)
diff --git a/custos_portal/custos_portal/settings.py b/custos_portal/custos_portal/settings.py
index 1d79883..188b2b4 100644
--- a/custos_portal/custos_portal/settings.py
+++ b/custos_portal/custos_portal/settings.py
@@ -53,6 +53,8 @@ MIDDLEWARE = [
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
+
+    'custos_portal.apps.auth.middleware.gateway_groups_middleware',
 ]
 
 ROOT_URLCONF = 'custos_portal.urls'
diff --git a/custos_portal/custos_portal/settings_local.py b/custos_portal/custos_portal/settings_local.py
index 414b83e..b4e3175 100644
--- a/custos_portal/custos_portal/settings_local.py
+++ b/custos_portal/custos_portal/settings_local.py
@@ -16,8 +16,7 @@ KEYCLOAK_CLIENT_ID = 'custos-6nwoqodstpe5mvcq09lh-10000101'
 KEYCLOAK_CLIENT_SECRET = 'GiKrGGVLW7zDoPZwzgCiFM7WUz3PhIumTmFxAkr7'
 KEYCLOAK_AUTHORIZE_URL = 'https://keycloak.custos.scigap.org:31000/auth/realms/10000101/protocol/openid-connect/auth'
 KEYCLOAK_TOKEN_URL = 'https://airavata.host:8443/auth/realms/default/protocol/openid-connect/token'
-KEYCLOAK_USERINFO_URL = 'https://keycloak.custos.scigap.org:31000/auth/realms/10000101/protocol/openid-connect/auth'
-KEYCLOAK_LOGOUT_URL = 'https://airavata.host:8443/auth/realms/default/protocol/openid-connect/logout'
+KEYCLOAK_LOGOUT_URL = 'https://keycloak.custos.scigap.org:31000/auth/realms/10000101/protocol/openid-connect/logout'
 # Optional: specify if using self-signed certificate or certificate from unrecognized CA
 #KEYCLOAK_CA_CERTFILE = os.path.join(BASE_DIR, "django_airavata", "resources", "incommon_rsa_server_ca.pem")
 KEYCLOAK_VERIFY_SSL = False
diff --git a/custos_portal/custos_portal/templates/base.html b/custos_portal/custos_portal/templates/base.html
index 94361e8..d07d985 100644
--- a/custos_portal/custos_portal/templates/base.html
+++ b/custos_portal/custos_portal/templates/base.html
@@ -205,7 +205,7 @@
           </a>
           <div class=dropdown-menu aria-labelledby=dropdownMenuButton>
             <a class=dropdown-item href=#>User settings</a>
-            <a class=dropdown-item href="#">
+            <a class=dropdown-item href="{% url 'custos_portal_auth:logout' %}">
               Logout <i class="fa fa-sign-out-alt"></i>
             </a>
           </div>
diff --git a/custos_portal/custos_portal/templates/custos_portal_auth/partials/username_password_login_form.html b/custos_portal/custos_portal/templates/custos_portal_auth/partials/username_password_login_form.html
index aea0cf5..e2ca5b3 100644
--- a/custos_portal/custos_portal/templates/custos_portal_auth/partials/username_password_login_form.html
+++ b/custos_portal/custos_portal/templates/custos_portal_auth/partials/username_password_login_form.html
@@ -4,7 +4,7 @@
             <div class="card-body">
                 <h5 class="card-title">Log in with {{ options.password.name|default:"a username and password" }}</h5>
                 {% include "./messages.html" %}
-                <form action="#" method="post">
+                <form action="{% url 'custos_portal_auth:handle_login' %}" method="post">
                     {% csrf_token %}
                     <div class="form-group">
                         <label for="username">Username</label>