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>