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 <[email protected]> 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", "[email protected]", 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>
