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 2021/06/14 20:09:05 UTC

[airavata-django-portal] branch develop updated (9ec9b00 -> 3b022ea)

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

machristie pushed a change to branch develop
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git.


    from 9ec9b00  Optimize docker image size: exclude node_modules
     new 8cce97c  AIRAVATA-3383 settings_local.py download
     new 27dc31b  Merge branch 'airavata-3383' into develop
     new f08c213  AIRAVATA-3833 Bug fixes
     new 3b022ea  Merge branch 'airavata-3383' into develop

The 4 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/admin/apps.py                 |  8 ++
 .../components/developers/DevelopersContainer.vue  | 18 ++++
 .../static/django_airavata_admin/src/router.js     |  8 +-
 django_airavata/apps/admin/urls.py                 |  2 +
 django_airavata/apps/admin/views.py                |  6 ++
 .../settings_local.py.template}                    | 67 ++++++---------
 django_airavata/apps/auth/urls.py                  |  2 +
 django_airavata/apps/auth/views.py                 | 99 +++++++++++++++++++++-
 8 files changed, 165 insertions(+), 45 deletions(-)
 create mode 100644 django_airavata/apps/admin/static/django_airavata_admin/src/components/developers/DevelopersContainer.vue
 copy django_airavata/{settings_local.py.sample => apps/auth/templates/django_airavata_auth/settings_local.py.template} (74%)

[airavata-django-portal] 04/04: Merge branch 'airavata-3383' into develop

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

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

commit 3b022ea407ed75873515930f12ac31d6b31260cc
Merge: 27dc31b f08c213
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Mon Jun 14 16:08:29 2021 -0400

    Merge branch 'airavata-3383' into develop

 django_airavata/apps/auth/views.py | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --cc django_airavata/apps/auth/views.py
index 33684b5,1ef0259..fb0d295
--- a/django_airavata/apps/auth/views.py
+++ b/django_airavata/apps/auth/views.py
@@@ -520,91 -509,9 +520,91 @@@ def _create_login_desktop_failed_respon
  
  
  @login_required
 +def access_token_redirect(request):
 +    redirect_uri = request.GET['redirect_uri']
 +    config = next(filter(lambda d: d.get('URI') == redirect_uri,
 +                         settings.ACCESS_TOKEN_REDIRECT_ALLOWED_URIS), None)
 +    if config is None:
 +        logger.warning(f"redirect_uri value '{redirect_uri}' is not configured "
 +                       "in ACCESS_TOKEN_REDIRECT_ALLOWED_URIS setting")
 +        return HttpResponseForbidden("Invalid redirect_uri value")
 +    return redirect(redirect_uri + f"{'&' if '?' in redirect_uri else '?'}{config.get('PARAM_NAME', 'access_token')}="
 +                    f"{quote(request.authz_token.accessToken)}")
 +
 +
 +def user_profile(request):
 +    return render(request, "django_airavata_auth/base.html", {
 +        'bundle_name': "user-profile"
 +    })
 +
 +
 +class IsUserOrReadOnlyForAdmins(permissions.BasePermission):
 +    def has_permission(self, request, view):
 +        return request.user.is_authenticated
 +
 +    def has_object_permission(self, request, view, obj):
 +        if (request.method in permissions.SAFE_METHODS and
 +                request.is_gateway_admin):
 +            return True
 +        return obj == request.user
 +
 +
 +# TODO: disable deleting and creating?
 +class UserViewSet(viewsets.ModelViewSet):
 +    serializer_class = serializers.UserSerializer
 +    queryset = get_user_model().objects.all()
 +    permission_classes = [IsUserOrReadOnlyForAdmins]
 +
 +    def get_queryset(self):
 +        user = self.request.user
 +        if user.is_superuser:
 +            return get_user_model().objects.all()
 +        else:
 +            return get_user_model().objects.get(pk=user.pk)
 +
 +    @action(detail=False)
 +    def current(self, request):
 +        return redirect(reverse('django_airavata_auth:user-detail', kwargs={'pk': request.user.id}))
 +
 +    @action(methods=['post'], detail=True)
 +    def resend_email_verification(self, request, pk=None):
 +        pending_email_change = models.PendingEmailChange.objects.get(user=request.user, verified=False)
 +        if pending_email_change is not None:
 +            serializer = serializers.UserSerializer()
 +            serializer._send_email_verification_link(request, pending_email_change)
 +        return JsonResponse({})
 +
 +    @action(methods=['post'], detail=True)
 +    @atomic
 +    def verify_email_change(self, request, pk=None):
 +        user = self.get_object()
 +        code = request.data['code']
 +
 +        try:
 +            pending_email_change = models.PendingEmailChange.objects.get(user=user, verification_code=code)
 +        except models.PendingEmailChange.DoesNotExist:
 +            raise Exception('Verification code is invalid. Please try again.')
 +        pending_email_change.verified = True
 +        pending_email_change.save()
 +        user.email = pending_email_change.email_address
 +        user.save()
 +        user.refresh_from_db()
 +
 +        try:
 +            user_profile_client = request.profile_service['user_profile']
 +            airavata_user_profile = user_profile_client.getUserProfileById(
 +                request.authz_token, user.username, settings.GATEWAY_ID)
 +            airavata_user_profile.emails = [pending_email_change.email_address]
 +            user_profile_client.updateUserProfile(request.authz_token, airavata_user_profile)
 +        except Exception as e:
 +            raise Exception(f"Failed to update Airavata User Profile with new email address: {e}") from e
 +        serializer = self.get_serializer(user)
 +        return Response(serializer.data)
 +
 +
  def download_settings_local(request):
  
-     if not request.is_gateway_admin or not request.is_read_only_gateway_admin:
+     if not (request.is_gateway_admin or request.is_read_only_gateway_admin):
          raise PermissionDenied()
  
      if settings.DEBUG:

[airavata-django-portal] 03/04: AIRAVATA-3833 Bug fixes

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

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

commit f08c213c42eec55a20d2895e5d30d653b68b13d3
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Mon Jun 14 16:08:22 2021 -0400

    AIRAVATA-3833 Bug fixes
---
 django_airavata/apps/auth/views.py | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/django_airavata/apps/auth/views.py b/django_airavata/apps/auth/views.py
index f005a67..1ef0259 100644
--- a/django_airavata/apps/auth/views.py
+++ b/django_airavata/apps/auth/views.py
@@ -2,7 +2,7 @@ import io
 import logging
 import time
 from datetime import datetime, timedelta, timezone
-from urllib.parse import quote, urlencode
+from urllib.parse import quote, urlencode, urlparse
 
 import requests
 from django.conf import settings
@@ -511,7 +511,7 @@ def _create_login_desktop_failed_response(request, idp_alias=None):
 @login_required
 def download_settings_local(request):
 
-    if not request.is_gateway_admin or not request.is_read_only_gateway_admin:
+    if not (request.is_gateway_admin or request.is_read_only_gateway_admin):
         raise PermissionDenied()
 
     if settings.DEBUG:
@@ -566,7 +566,8 @@ def get_client(access_token, clients_endpoint, client_id):
 
 def get_clients_endpoint():
     realm = settings.GATEWAY_ID
-    clients_endpoint = f"https://iamdev.scigap.org/auth/admin/realms/{realm}/clients"
+    parse_result = urlparse(settings.KEYCLOAK_AUTHORIZE_URL)
+    clients_endpoint = f"{parse_result.scheme}://{parse_result.netloc}/auth/admin/realms/{realm}/clients"
     return clients_endpoint
 
 

[airavata-django-portal] 02/04: Merge branch 'airavata-3383' into develop

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

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

commit 27dc31b8f5b307289e3d1fd4a4adbe329825fbf9
Merge: 9ec9b00 8cce97c
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Mon Jun 14 16:02:28 2021 -0400

    Merge branch 'airavata-3383' into develop

 django_airavata/apps/admin/apps.py                 |   8 ++
 .../components/developers/DevelopersContainer.vue  |  18 +++
 .../static/django_airavata_admin/src/router.js     |   8 +-
 django_airavata/apps/admin/urls.py                 |   2 +
 django_airavata/apps/admin/views.py                |   6 +
 .../settings_local.py.template                     | 137 +++++++++++++++++++++
 django_airavata/apps/auth/urls.py                  |   2 +
 django_airavata/apps/auth/views.py                 |  96 ++++++++++++++-
 8 files changed, 275 insertions(+), 2 deletions(-)

diff --cc django_airavata/apps/auth/urls.py
index ba343f4,25454ad..45936ba
--- a/django_airavata/apps/auth/urls.py
+++ b/django_airavata/apps/auth/urls.py
@@@ -1,6 -1,6 +1,7 @@@
  
 -from django.conf.urls import url
 +from django.conf.urls import include, url
+ from django.urls import path
 +from rest_framework import routers
  
  from . import views
  
@@@ -32,6 -29,5 +33,7 @@@ urlpatterns = 
          views.login_desktop_success, name="login_desktop_success"),
      url(r'^refreshed-token-desktop$', views.refreshed_token_desktop,
          name="refreshed_token_desktop"),
 +    url(r'^access-token-redirect$', views.access_token_redirect, name="access_token_redirect"),
 +    url(r'^user-profile/', views.user_profile, name="user_profile"),
+     path('settings-local/', views.download_settings_local, name="download_settings_local"),
  ]
diff --cc django_airavata/apps/auth/views.py
index 914b632,f005a67..33684b5
--- a/django_airavata/apps/auth/views.py
+++ b/django_airavata/apps/auth/views.py
@@@ -3,20 -4,17 +4,23 @@@ import tim
  from datetime import datetime, timedelta, timezone
  from urllib.parse import quote, urlencode
  
+ import requests
  from django.conf import settings
  from django.contrib import messages
 -from django.contrib.auth import authenticate, login, logout
 +from django.contrib.auth import authenticate, get_user_model, login, logout
  from django.contrib.auth.decorators import login_required
- from django.core.exceptions import ObjectDoesNotExist
+ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
 +from django.db.transaction import atomic
  from django.forms import ValidationError
 -from django.http import FileResponse, HttpResponseBadRequest, JsonResponse
 +from django.http import (
++    FileResponse,
 +    HttpResponseBadRequest,
 +    HttpResponseForbidden,
 +    JsonResponse
 +)
  from django.shortcuts import redirect, render, resolve_url
  from django.template import Context
+ from django.template.loader import render_to_string
  from django.urls import reverse
  from django.views.decorators.debug import sensitive_variables
  from requests_oauthlib import OAuth2Session
@@@ -516,83 -509,91 +520,173 @@@ def _create_login_desktop_failed_respon
  
  
  @login_required
 +def access_token_redirect(request):
 +    redirect_uri = request.GET['redirect_uri']
 +    config = next(filter(lambda d: d.get('URI') == redirect_uri,
 +                         settings.ACCESS_TOKEN_REDIRECT_ALLOWED_URIS), None)
 +    if config is None:
 +        logger.warning(f"redirect_uri value '{redirect_uri}' is not configured "
 +                       "in ACCESS_TOKEN_REDIRECT_ALLOWED_URIS setting")
 +        return HttpResponseForbidden("Invalid redirect_uri value")
 +    return redirect(redirect_uri + f"{'&' if '?' in redirect_uri else '?'}{config.get('PARAM_NAME', 'access_token')}="
 +                    f"{quote(request.authz_token.accessToken)}")
 +
 +
 +def user_profile(request):
 +    return render(request, "django_airavata_auth/base.html", {
 +        'bundle_name': "user-profile"
 +    })
 +
 +
 +class IsUserOrReadOnlyForAdmins(permissions.BasePermission):
 +    def has_permission(self, request, view):
 +        return request.user.is_authenticated
 +
 +    def has_object_permission(self, request, view, obj):
 +        if (request.method in permissions.SAFE_METHODS and
 +                request.is_gateway_admin):
 +            return True
 +        return obj == request.user
 +
 +
 +# TODO: disable deleting and creating?
 +class UserViewSet(viewsets.ModelViewSet):
 +    serializer_class = serializers.UserSerializer
 +    queryset = get_user_model().objects.all()
 +    permission_classes = [IsUserOrReadOnlyForAdmins]
 +
 +    def get_queryset(self):
 +        user = self.request.user
 +        if user.is_superuser:
 +            return get_user_model().objects.all()
 +        else:
 +            return get_user_model().objects.get(pk=user.pk)
 +
 +    @action(detail=False)
 +    def current(self, request):
 +        return redirect(reverse('django_airavata_auth:user-detail', kwargs={'pk': request.user.id}))
 +
 +    @action(methods=['post'], detail=True)
 +    def resend_email_verification(self, request, pk=None):
 +        pending_email_change = models.PendingEmailChange.objects.get(user=request.user, verified=False)
 +        if pending_email_change is not None:
 +            serializer = serializers.UserSerializer()
 +            serializer._send_email_verification_link(request, pending_email_change)
 +        return JsonResponse({})
 +
 +    @action(methods=['post'], detail=True)
 +    @atomic
 +    def verify_email_change(self, request, pk=None):
 +        user = self.get_object()
 +        code = request.data['code']
 +
 +        try:
 +            pending_email_change = models.PendingEmailChange.objects.get(user=user, verification_code=code)
 +        except models.PendingEmailChange.DoesNotExist:
 +            raise Exception('Verification code is invalid. Please try again.')
 +        pending_email_change.verified = True
 +        pending_email_change.save()
 +        user.email = pending_email_change.email_address
 +        user.save()
 +        user.refresh_from_db()
 +
 +        try:
 +            user_profile_client = request.profile_service['user_profile']
 +            airavata_user_profile = user_profile_client.getUserProfileById(
 +                request.authz_token, user.username, settings.GATEWAY_ID)
 +            airavata_user_profile.emails = [pending_email_change.email_address]
 +            user_profile_client.updateUserProfile(request.authz_token, airavata_user_profile)
 +        except Exception as e:
 +            raise Exception(f"Failed to update Airavata User Profile with new email address: {e}") from e
 +        serializer = self.get_serializer(user)
 +        return Response(serializer.data)
++
++
+ def download_settings_local(request):
+ 
+     if not request.is_gateway_admin or not request.is_read_only_gateway_admin:
+         raise PermissionDenied()
+ 
+     if settings.DEBUG:
+         raise Exception("Downloading a settings_local.py file isn't allowed in DEBUG mode.")
+ 
+     development_client_id = f"local-django-{request.user.username}"
+     access_token = utils.get_service_account_authz_token().accessToken
+     clients_endpoint = get_clients_endpoint()
+     development_client = get_client(access_token, clients_endpoint, development_client_id)
+     if development_client is None:
+         development_client_endpoint = create_client(access_token, clients_endpoint, development_client_id)
+     else:
+         development_client_endpoint = get_client_endpoint(development_client)
+     development_client_secret = get_client_secret(access_token, development_client_endpoint)
+ 
+     context = {}
+     context['AUTHENTICATION_OPTIONS'] = settings.AUTHENTICATION_OPTIONS
+     context['keycloak_client_id'] = development_client_id
+     context['keycloak_client_secret'] = development_client_secret
+     context['KEYCLOAK_AUTHORIZE_URL'] = settings.KEYCLOAK_AUTHORIZE_URL
+     context['KEYCLOAK_TOKEN_URL'] = settings.KEYCLOAK_TOKEN_URL
+     context['KEYCLOAK_USERINFO_URL'] = settings.KEYCLOAK_USERINFO_URL
+     context['KEYCLOAK_LOGOUT_URL'] = settings.KEYCLOAK_LOGOUT_URL
+     context['GATEWAY_ID'] = settings.GATEWAY_ID
+     context['AIRAVATA_API_HOST'] = settings.AIRAVATA_API_HOST
+     context['AIRAVATA_API_PORT'] = settings.AIRAVATA_API_PORT
+     context['AIRAVATA_API_SECURE'] = settings.AIRAVATA_API_SECURE
+     context['GATEWAY_DATA_STORE_RESOURCE_ID'] = settings.GATEWAY_DATA_STORE_RESOURCE_ID
+     if hasattr(settings, 'GATEWAY_DATA_STORE_REMOTE_API'):
+         context['GATEWAY_DATA_STORE_REMOTE_API'] = settings.GATEWAY_DATA_STORE_REMOTE_API
+     else:
+         context['GATEWAY_DATA_STORE_REMOTE_API'] = request.build_absolute_uri("/api")
+     context['PROFILE_SERVICE_HOST'] = settings.PROFILE_SERVICE_HOST
+     context['PROFILE_SERVICE_PORT'] = settings.PROFILE_SERVICE_PORT
+     context['PROFILE_SERVICE_SECURE'] = settings.PROFILE_SERVICE_SECURE
+     context['PORTAL_TITLE'] = settings.PORTAL_TITLE
+     settings_local_str = render_to_string("django_airavata_auth/settings_local.py.template", context)
+     settings_local_bytesio = io.BytesIO(settings_local_str.encode())
+     return FileResponse(settings_local_bytesio, as_attachment=True, filename="settings_local.py")
+ 
+ 
+ def get_client(access_token, clients_endpoint, client_id):
+     headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json'}
+     r = requests.get(clients_endpoint, {'clientId': client_id}, headers=headers)
+     r.raise_for_status()
+     clients = r.json()
+     if len(clients) == 0:
+         return None
+     else:
+         return clients[0]
+ 
+ 
+ def get_clients_endpoint():
+     realm = settings.GATEWAY_ID
+     clients_endpoint = f"https://iamdev.scigap.org/auth/admin/realms/{realm}/clients"
+     return clients_endpoint
+ 
+ 
+ def get_client_endpoint(client):
+     return f"{get_clients_endpoint()}/{client['id']}"
+ 
+ 
+ def create_client(access_token, clients_endpoint, client_id):
+     client = {
+         'clientId': client_id,
+         "redirectUris": [
+             "http://localhost:8000/",
+             "http://localhost:8000/auth/callback*",
+             "http://127.0.0.1:8000/",
+             "http://127.0.0.1:8000/auth/callback*"
+         ],
+         "directAccessGrantsEnabled": True
+     }
+     headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json'}
+     r = requests.post(clients_endpoint, json=client, headers=headers)
+     r.raise_for_status()
+     return r.headers['Location']
+ 
+ 
+ def get_client_secret(access_token, client_endpoint):
+ 
+     headers = {'Authorization': f'Bearer {access_token}'}
+     r = requests.get(client_endpoint + "/client-secret", headers=headers)
+     r.raise_for_status()
+     return r.json()['value']

[airavata-django-portal] 01/04: AIRAVATA-3383 settings_local.py download

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

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

commit 8cce97c2b08bc672c9939491e95b82766e9e46f4
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Mon Jun 14 15:58:51 2021 -0400

    AIRAVATA-3383 settings_local.py download
---
 django_airavata/apps/admin/apps.py                 |   8 ++
 .../components/developers/DevelopersContainer.vue  |  18 +++
 .../static/django_airavata_admin/src/router.js     |   8 +-
 django_airavata/apps/admin/urls.py                 |   2 +
 django_airavata/apps/admin/views.py                |   6 +
 .../settings_local.py.template                     | 137 +++++++++++++++++++++
 django_airavata/apps/auth/urls.py                  |   2 +
 django_airavata/apps/auth/views.py                 |  99 ++++++++++++++-
 8 files changed, 277 insertions(+), 3 deletions(-)

diff --git a/django_airavata/apps/admin/apps.py b/django_airavata/apps/admin/apps.py
index 5501733..82876b9 100644
--- a/django_airavata/apps/admin/apps.py
+++ b/django_airavata/apps/admin/apps.py
@@ -65,4 +65,12 @@ class AdminConfig(AiravataAppConfig):
             'enabled': lambda req: (req.is_gateway_admin or
                                     req.is_read_only_gateway_admin)
         },
+        {
+            'label': 'Developer Console',
+            'icon': 'fa fa-code',
+            'url': 'django_airavata_admin:developers',
+            'active_prefixes': ['developers'],
+            'enabled': lambda req: (req.is_gateway_admin or
+                                    req.is_read_only_gateway_admin)
+        },
     ]
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/developers/DevelopersContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/developers/DevelopersContainer.vue
new file mode 100644
index 0000000..674676f
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/developers/DevelopersContainer.vue
@@ -0,0 +1,18 @@
+<template>
+  <div>
+    <div class="row">
+      <div class="col">
+        <h1 class="h4 mb-4">Developer Console</h1>
+      </div>
+    </div>
+    <b-card header="Download a settings_local.py file for local development">
+      <b-link href="/auth/settings-local">
+        <i class="fas fa-download"></i>
+        Download
+      </b-link>
+    </b-card>
+  </div>
+</template>
+<script>
+export default {};
+</script>
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/router.js b/django_airavata/apps/admin/static/django_airavata_admin/src/router.js
index 6d803b5..de0c0d4 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/router.js
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/router.js
@@ -6,8 +6,9 @@ import ApplicationModuleEditor from "./components/applications/ApplicationModule
 import ApplicationsDashboard from "./components/dashboards/ApplicationsDashboard.vue";
 import ComputePreference from "./components/admin/group_resource_preferences/ComputePreference";
 import ComputeResourcePreferenceDashboard from "./components/dashboards/ComputeResourcePreferenceDashboard";
-import ExperimentStatisticsContainer from "./components/statistics/ExperimentStatisticsContainer";
 import CredentialStoreDashboard from "./components/dashboards/CredentialStoreDashboard";
+import DevelopersContainer from "./components/developers//DevelopersContainer.vue";
+import ExperimentStatisticsContainer from "./components/statistics/ExperimentStatisticsContainer";
 import GatewayResourceProfileEditorContainer from "./components/gatewayprofile/GatewayResourceProfileEditorContainer.vue";
 import GroupComputeResourcePreference from "./components/admin/group_resource_preferences/GroupComputeResourcePreference";
 import IdentityServiceUserManagementContainer from "./components/users/IdentityServiceUserManagementContainer.vue";
@@ -149,6 +150,11 @@ const routes = [
     component: ExperimentStatisticsContainer,
     name: "experiment-statistics",
   },
+  {
+    path: "/developers",
+    component: DevelopersContainer,
+    name: "developers",
+  }
 ];
 export default new VueRouter({
   mode: "history",
diff --git a/django_airavata/apps/admin/urls.py b/django_airavata/apps/admin/urls.py
index 2c5d481..9404e64 100644
--- a/django_airavata/apps/admin/urls.py
+++ b/django_airavata/apps/admin/urls.py
@@ -1,4 +1,5 @@
 from django.conf.urls import url
+from django.urls import path
 
 from . import views
 
@@ -15,4 +16,5 @@ urlpatterns = [
         name='gateway_resource_profile'),
     url(r'^notices/', views.notices, name='notices'),
     url(r'^users/', views.users, name='users'),
+    path('developers/', views.developers, name='developers'),
 ]
diff --git a/django_airavata/apps/admin/views.py b/django_airavata/apps/admin/views.py
index 59ed496..caca63a 100644
--- a/django_airavata/apps/admin/views.py
+++ b/django_airavata/apps/admin/views.py
@@ -57,3 +57,9 @@ def users(request):
 def experiment_statistics(request):
     request.active_nav_item = 'experiment-statistics'
     return render(request, 'admin/admin_base.html')
+
+
+@login_required
+def developers(request):
+    request.active_nav_item = 'developers'
+    return render(request, 'admin/admin_base.html')
diff --git a/django_airavata/apps/auth/templates/django_airavata_auth/settings_local.py.template b/django_airavata/apps/auth/templates/django_airavata_auth/settings_local.py.template
new file mode 100644
index 0000000..cdda4ce
--- /dev/null
+++ b/django_airavata/apps/auth/templates/django_airavata_auth/settings_local.py.template
@@ -0,0 +1,137 @@
+"""
+Override default Django settings for a particular instance.
+
+Copy this file to settings_local.py and modify as appropriate. This file will
+be imported into settings.py last of all so settings in this file override any
+defaults specified in settings.py.
+"""
+
+{% autoescape off %}
+import os
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# Django - general settings
+# Uncomment and specify for production deployments
+# DEBUG = False
+# STATIC_ROOT = "/var/www/path/to/sitename/static/"
+# ALLOWED_HOSTS = ['production.hostname']
+
+# Django - database settings
+# MySQL - to use MySQL, uncomment and specify the settings
+# DATABASES = {
+#     'default': {
+#         'ENGINE': 'django.db.backends.mysql',
+#         'NAME': '...',
+#         'HOST': '...',
+#         'USER': '...',
+#         'PASSWORD': '...',
+#         'OPTIONS': {
+#             'init_command': 'SET default_storage_engine=INNODB,collation_connection=utf8_bin',
+#         }
+# }
+
+# Django - Email settings
+# Uncomment and specify the following for sending emails (default email backend
+# just prints to the console)
+# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
+# EMAIL_HOST = '...'
+# EMAIL_PORT = '...'
+# EMAIL_HOST_USER = '...'
+# EMAIL_HOST_PASSWORD = '...'
+# EMAIL_USE_TLS = True
+# ADMINS receive error emails
+# ADMINS = [('Admin Name', 'admin@example.com')]
+# Optional: PORTAL_ADMINS receive administrative emails, like when a new user is created
+# This can be set to a different value than ADMINS so that the PORTAL_ADMINS
+# don't receive error emails. Defaults to the same value as ADMINS.
+# PORTAL_ADMINS = ADMINS
+# SERVER_EMAIL = 'portal@example.com'
+
+# Keycloak Configuration
+KEYCLOAK_CLIENT_ID = '{{ keycloak_client_id }}'
+KEYCLOAK_CLIENT_SECRET = '{{ keycloak_client_secret }}'
+KEYCLOAK_AUTHORIZE_URL = '{{ KEYCLOAK_AUTHORIZE_URL }}'
+KEYCLOAK_TOKEN_URL = '{{ KEYCLOAK_TOKEN_URL }}'
+KEYCLOAK_USERINFO_URL = '{{ KEYCLOAK_USERINFO_URL }}'
+KEYCLOAK_LOGOUT_URL = '{{ KEYCLOAK_LOGOUT_URL }}'
+# 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 = True
+
+AUTHENTICATION_OPTIONS = {{ AUTHENTICATION_OPTIONS }}
+
+# Airavata API Configuration
+GATEWAY_ID = '{{ GATEWAY_ID }}'
+AIRAVATA_API_HOST = '{{ AIRAVATA_API_HOST }}'
+AIRAVATA_API_PORT = {{ AIRAVATA_API_PORT }}
+AIRAVATA_API_SECURE = {{ AIRAVATA_API_SECURE }}
+GATEWAY_DATA_STORE_RESOURCE_ID = '{{ GATEWAY_DATA_STORE_RESOURCE_ID }}'
+GATEWAY_DATA_STORE_DIR = '/tmp'
+GATEWAY_DATA_STORE_REMOTE_API = '{{ GATEWAY_DATA_STORE_REMOTE_API }}'
+# FILE_UPLOAD_TEMP_DIR = '/tmp'
+
+# Profile Service Configuration
+PROFILE_SERVICE_HOST = '{{ PROFILE_SERVICE_HOST }}'
+PROFILE_SERVICE_PORT = {{ PROFILE_SERVICE_PORT }}
+PROFILE_SERVICE_SECURE = {{ PROFILE_SERVICE_SECURE }}
+
+# Portal settings
+PORTAL_TITLE = '{{ PORTAL_TITLE }}'
+
+# Tus upload - uncomment the following to enable tus uploads
+# Override and set to a valid tus endpoint
+# TUS_ENDPOINT = "https://tus.domainname.org/files/"
+# Override and set to the directory where tus uploads will be stored.
+# TUS_DATA_DIR = "/path/to/tus-temp-dir"
+
+# Legacy (PGA) Portal link - uncomment to provide a link to the legacy portal
+# PGA_URL = '...'
+
+# Google Analytics Tracking ID ("UA-XXXXXXXX-X"). If this setting is set, then
+# Google Analytics tracking will be added to all pages.
+# GOOGLE_ANALYTICS_TRACKING_ID = '...'
+
+# Logging configuration. Uncomment following to override default log configuration
+# LOGGING = {
+#     'version': 1,
+#     'disable_existing_loggers': False,
+#     'filters': {
+#         'require_debug_false': {
+#             '()': 'django.utils.log.RequireDebugFalse',
+#         },
+#         'require_debug_true': {
+#             '()': 'django.utils.log.RequireDebugTrue',
+#         },
+#     },
+#     'formatters': {
+#         'verbose': {
+#             'format': '[%(asctime)s %(name)s:%(lineno)d %(levelname)s] %(message)s'
+#         },
+#     },
+#     'handlers': {
+#         'console': {
+#             'class': 'logging.StreamHandler',
+#             'formatter': 'verbose'
+#         },
+#         'mail_admins': {
+#             'filters': ['require_debug_false'],
+#             'level': 'ERROR',
+#             'class': 'django.utils.log.AdminEmailHandler',
+#             'include_html': True,
+#         }
+#     },
+#     'loggers': {
+#         'django_airavata': {
+#             'handlers': ['console', 'mail_admins'],
+#             'level': 'DEBUG' if DEBUG else 'INFO'
+#         },
+#         'root': {
+#             'handlers': ['console', 'mail_admins'],
+#             'level': 'WARNING'
+#         }
+#     },
+# }
+
+{% endautoescape %}
diff --git a/django_airavata/apps/auth/urls.py b/django_airavata/apps/auth/urls.py
index b5d874d..25454ad 100644
--- a/django_airavata/apps/auth/urls.py
+++ b/django_airavata/apps/auth/urls.py
@@ -1,5 +1,6 @@
 
 from django.conf.urls import url
+from django.urls import path
 
 from . import views
 
@@ -28,4 +29,5 @@ urlpatterns = [
         views.login_desktop_success, name="login_desktop_success"),
     url(r'^refreshed-token-desktop$', views.refreshed_token_desktop,
         name="refreshed_token_desktop"),
+    path('settings-local/', views.download_settings_local, name="download_settings_local"),
 ]
diff --git a/django_airavata/apps/auth/views.py b/django_airavata/apps/auth/views.py
index 0fe55b0..f005a67 100644
--- a/django_airavata/apps/auth/views.py
+++ b/django_airavata/apps/auth/views.py
@@ -1,16 +1,20 @@
+import io
 import logging
 import time
 from datetime import datetime, timedelta, timezone
 from urllib.parse import quote, urlencode
 
+import requests
 from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth import authenticate, login, logout
-from django.core.exceptions import ObjectDoesNotExist
+from django.contrib.auth.decorators import login_required
+from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
 from django.forms import ValidationError
-from django.http import HttpResponseBadRequest, JsonResponse
+from django.http import FileResponse, HttpResponseBadRequest, JsonResponse
 from django.shortcuts import redirect, render, resolve_url
 from django.template import Context
+from django.template.loader import render_to_string
 from django.urls import reverse
 from django.views.decorators.debug import sensitive_variables
 from requests_oauthlib import OAuth2Session
@@ -502,3 +506,94 @@ def _create_login_desktop_failed_response(request, idp_alias=None):
         params['username'] = request.POST['username']
     return redirect(reverse('django_airavata_auth:login_desktop') +
                     "?" + urlencode(params))
+
+
+@login_required
+def download_settings_local(request):
+
+    if not request.is_gateway_admin or not request.is_read_only_gateway_admin:
+        raise PermissionDenied()
+
+    if settings.DEBUG:
+        raise Exception("Downloading a settings_local.py file isn't allowed in DEBUG mode.")
+
+    development_client_id = f"local-django-{request.user.username}"
+    access_token = utils.get_service_account_authz_token().accessToken
+    clients_endpoint = get_clients_endpoint()
+    development_client = get_client(access_token, clients_endpoint, development_client_id)
+    if development_client is None:
+        development_client_endpoint = create_client(access_token, clients_endpoint, development_client_id)
+    else:
+        development_client_endpoint = get_client_endpoint(development_client)
+    development_client_secret = get_client_secret(access_token, development_client_endpoint)
+
+    context = {}
+    context['AUTHENTICATION_OPTIONS'] = settings.AUTHENTICATION_OPTIONS
+    context['keycloak_client_id'] = development_client_id
+    context['keycloak_client_secret'] = development_client_secret
+    context['KEYCLOAK_AUTHORIZE_URL'] = settings.KEYCLOAK_AUTHORIZE_URL
+    context['KEYCLOAK_TOKEN_URL'] = settings.KEYCLOAK_TOKEN_URL
+    context['KEYCLOAK_USERINFO_URL'] = settings.KEYCLOAK_USERINFO_URL
+    context['KEYCLOAK_LOGOUT_URL'] = settings.KEYCLOAK_LOGOUT_URL
+    context['GATEWAY_ID'] = settings.GATEWAY_ID
+    context['AIRAVATA_API_HOST'] = settings.AIRAVATA_API_HOST
+    context['AIRAVATA_API_PORT'] = settings.AIRAVATA_API_PORT
+    context['AIRAVATA_API_SECURE'] = settings.AIRAVATA_API_SECURE
+    context['GATEWAY_DATA_STORE_RESOURCE_ID'] = settings.GATEWAY_DATA_STORE_RESOURCE_ID
+    if hasattr(settings, 'GATEWAY_DATA_STORE_REMOTE_API'):
+        context['GATEWAY_DATA_STORE_REMOTE_API'] = settings.GATEWAY_DATA_STORE_REMOTE_API
+    else:
+        context['GATEWAY_DATA_STORE_REMOTE_API'] = request.build_absolute_uri("/api")
+    context['PROFILE_SERVICE_HOST'] = settings.PROFILE_SERVICE_HOST
+    context['PROFILE_SERVICE_PORT'] = settings.PROFILE_SERVICE_PORT
+    context['PROFILE_SERVICE_SECURE'] = settings.PROFILE_SERVICE_SECURE
+    context['PORTAL_TITLE'] = settings.PORTAL_TITLE
+    settings_local_str = render_to_string("django_airavata_auth/settings_local.py.template", context)
+    settings_local_bytesio = io.BytesIO(settings_local_str.encode())
+    return FileResponse(settings_local_bytesio, as_attachment=True, filename="settings_local.py")
+
+
+def get_client(access_token, clients_endpoint, client_id):
+    headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json'}
+    r = requests.get(clients_endpoint, {'clientId': client_id}, headers=headers)
+    r.raise_for_status()
+    clients = r.json()
+    if len(clients) == 0:
+        return None
+    else:
+        return clients[0]
+
+
+def get_clients_endpoint():
+    realm = settings.GATEWAY_ID
+    clients_endpoint = f"https://iamdev.scigap.org/auth/admin/realms/{realm}/clients"
+    return clients_endpoint
+
+
+def get_client_endpoint(client):
+    return f"{get_clients_endpoint()}/{client['id']}"
+
+
+def create_client(access_token, clients_endpoint, client_id):
+    client = {
+        'clientId': client_id,
+        "redirectUris": [
+            "http://localhost:8000/",
+            "http://localhost:8000/auth/callback*",
+            "http://127.0.0.1:8000/",
+            "http://127.0.0.1:8000/auth/callback*"
+        ],
+        "directAccessGrantsEnabled": True
+    }
+    headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json'}
+    r = requests.post(clients_endpoint, json=client, headers=headers)
+    r.raise_for_status()
+    return r.headers['Location']
+
+
+def get_client_secret(access_token, client_endpoint):
+
+    headers = {'Authorization': f'Bearer {access_token}'}
+    r = requests.get(client_endpoint + "/client-secret", headers=headers)
+    r.raise_for_status()
+    return r.json()['value']