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 2019/05/31 18:18:15 UTC

[airavata-django-portal] branch master updated (f03f492 -> b8c67a6)

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

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


    from f03f492  AIRAVATA-3034 Fix skipping encoding of path params
     new f447a12  AIRAVATA-3040 Persist changes to user group membership
     new c3cc34b  AIRAVATA-3040 Persist detail toggled state
     new 89bb973  AIRAVATA-3040 Put gateway groups at top of list
     new 61f918b  AIRAVATA-3040 Don't allow editing owner of group
     new b8c67a6  Fixing linting error

The 5 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:
 .../src/components/users/UserDetailsContainer.vue  |  45 +++++++
 .../components/users/UserGroupMembershipEditor.vue | 124 +++++++++++++----
 .../components/users/UserManagementContainer.vue   |  45 ++++++-
 django_airavata/apps/api/serializers.py            |  18 +--
 .../api/static/django_airavata_api/js/index.js     |   2 +
 .../static/django_airavata_api/js/models/Group.js  |   4 +
 django_airavata/apps/api/views.py                  |  12 +-
 django_airavata/settings_local_seagrid.py          | 147 +++++++++++++++++++++
 8 files changed, 354 insertions(+), 43 deletions(-)
 create mode 100644 django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
 create mode 100644 django_airavata/settings_local_seagrid.py


[airavata-django-portal] 05/05: Fixing linting error

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

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

commit b8c67a686bd084c0bdb5c28a5be418868472b2f9
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Fri May 31 14:18:05 2019 -0400

    Fixing linting error
---
 django_airavata/apps/api/views.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index b0797ec..011eabe 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -49,7 +49,6 @@ from . import (
     thrift_utils
 )
 
-
 READ_PERMISSION_TYPE = '{}:READ'
 
 log = logging.getLogger(__name__)


[airavata-django-portal] 02/05: AIRAVATA-3040 Persist detail toggled state

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

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

commit c3cc34b9e07dd2c64586ce9b0eb62ce6145679b4
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu May 30 12:03:40 2019 -0400

    AIRAVATA-3040 Persist detail toggled state
---
 .../src/components/users/UserManagementContainer.vue   | 18 +++++++++++++++---
 1 file changed, 15 insertions(+), 3 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue
index 49ca5db..25c5ba7 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue
@@ -20,7 +20,7 @@
               >
                 <b-button
                   v-if="data.item.airavataUserProfileExists"
-                  @click="data.toggleDetails"
+                  @click="toggleDetails(data)"
                 >
                   Edit Groups
                 </b-button>
@@ -58,7 +58,8 @@ export default {
   data() {
     return {
       usersPaginator: null,
-      allGroups: null
+      allGroups: null,
+      showingDetails: {}
     };
   },
   components: {
@@ -107,7 +108,13 @@ export default {
       ];
     },
     items() {
-      return this.usersPaginator ? this.usersPaginator.results : [];
+      return this.usersPaginator
+        ? this.usersPaginator.results.map(u => {
+            const user = u.clone();
+            user._showDetails = this.showingDetails[u.airavataInternalUserId] || false;
+            return user;
+          })
+        : [];
     },
     editableGroups() {
       return this.allGroups
@@ -138,6 +145,11 @@ export default {
         limit: 10,
         offset: this.currentOffset
       }).then(users => (this.usersPaginator = users));
+    },
+    toggleDetails(row) {
+      row.toggleDetails();
+      this.showingDetails[row.item.airavataInternalUserId] = !this
+        .showingDetails[row.item.airavataInternalUserId];
     }
   }
 };


[airavata-django-portal] 01/05: AIRAVATA-3040 Persist changes to user group membership

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

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

commit f447a126ff4f1fbb731af09d7660736e1d461a79
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu May 30 11:34:19 2019 -0400

    AIRAVATA-3040 Persist changes to user group membership
---
 .../src/components/users/UserDetailsContainer.vue  |  44 ++++++
 .../components/users/UserGroupMembershipEditor.vue |  38 ++++--
 .../components/users/UserManagementContainer.vue   |  27 +++-
 django_airavata/apps/api/serializers.py            |  18 +--
 .../api/static/django_airavata_api/js/index.js     |   2 +
 django_airavata/apps/api/views.py                  |  11 ++
 django_airavata/settings_local_seagrid.py          | 147 +++++++++++++++++++++
 7 files changed, 265 insertions(+), 22 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
new file mode 100644
index 0000000..f47101b
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
@@ -0,0 +1,44 @@
+<template>
+  <user-group-membership-editor
+    v-model="localManagedUserProfile.groups"
+    :editable-groups="editableGroups"
+    @input="groupsUpdated"
+  />
+</template>
+<script>
+import { models } from "django-airavata-api";
+import UserGroupMembershipEditor from "./UserGroupMembershipEditor";
+
+export default {
+  name: "user-details-container",
+  props: {
+    managedUserProfile: {
+      type: models.ManagedUserProfile,
+      required: true
+    },
+    editableGroups: {
+      type: Array,
+      required: true
+    }
+  },
+  components: {
+    UserGroupMembershipEditor
+  },
+  data() {
+    return {
+      localManagedUserProfile: this.managedUserProfile.clone()
+    };
+  },
+  watch: {
+    managedUserProfile(newValue) {
+      this.localManagedUserProfile = newValue.clone();
+    }
+  },
+  methods: {
+    groupsUpdated() {
+      this.$emit("groups-updated", this.localManagedUserProfile);
+    }
+  }
+};
+</script>
+
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserGroupMembershipEditor.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserGroupMembershipEditor.vue
index 494f862..ba8a940 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserGroupMembershipEditor.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserGroupMembershipEditor.vue
@@ -3,16 +3,19 @@
     <b-form-checkbox-group
       :checked="selected"
       :options="options"
+      @input="groupsUpdated"
       stacked
     ></b-form-checkbox-group>
   </b-form-group>
 </template>
 
 <script>
+import { mixins } from "django-airavata-common-ui";
 export default {
   name: "user-group-membership-editor",
+  mixins: [mixins.VModelMixin],
   props: {
-    groups: {
+    value: {
       type: Array,
       required: true
     },
@@ -21,21 +24,17 @@ export default {
       required: true
     }
   },
-  data() {
-    return {
-    }
-  },
   computed: {
     selected() {
-      return this.groups.map(g => g.id);
+      return this.data.map(g => g.id);
     },
     options() {
-      const options = this.groups.map(g => {
+      const options = this.data.map(g => {
         return {
           text: g.name,
           value: g.id,
           disabled: true
-        }
+        };
       });
       this.editableGroups.forEach(g => {
         const editableOption = options.find(opt => opt.value === g.id);
@@ -46,11 +45,30 @@ export default {
             text: g.name,
             value: g.id,
             disabled: false
-          })
+          });
         }
-      })
+      });
+      // TODO: sort the options?
       return options;
     }
+  },
+  methods: {
+    groupsUpdated(checkedGroups) {
+      // Check for added groups
+      for (const checkedGroupId of checkedGroups) {
+        if (!this.data.find(g => g.id === checkedGroupId)) {
+          const addedGroup = this.editableGroups.find(g => g.id === checkedGroupId);
+          this.data.push(addedGroup);
+        }
+      }
+      // Check for removed groups
+      for (const group of this.data) {
+        if (!checkedGroups.find(groupId => groupId === group.id)) {
+          const groupIndex = this.data.findIndex(g => g.id === group.id);
+          this.data.splice(groupIndex, 1);
+        }
+      }
+    }
   }
 };
 </script>
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue
index dbbcb13..49ca5db 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue
@@ -29,7 +29,11 @@
                 slot="row-details"
                 slot-scope="data"
               >
-                <user-group-membership-editor :groups="data.item.groups" :editableGroups="editableGroups" />
+                <user-details-container
+                  :managed-user-profile="data.item"
+                  :editable-groups="editableGroups"
+                  @groups-updated="groupsUpdated"
+                />
               </template>
             </b-table>
             <pager
@@ -47,7 +51,7 @@
 <script>
 import { services } from "django-airavata-api";
 import { components } from "django-airavata-common-ui";
-import UserGroupMembershipEditor from "./UserGroupMembershipEditor.vue";
+import UserDetailsContainer from "./UserDetailsContainer.vue";
 
 export default {
   name: "user-management-container",
@@ -59,7 +63,7 @@ export default {
   },
   components: {
     pager: components.Pager,
-    UserGroupMembershipEditor
+    UserDetailsContainer
   },
   created() {
     services.ManagedUserProfileService.list({ limit: 10 }).then(
@@ -109,6 +113,9 @@ export default {
       return this.allGroups
         ? this.allGroups.filter(g => g.isAdmin || g.isOwner)
         : [];
+    },
+    currentOffset() {
+      return this.usersPaginator ? this.usersPaginator.offset : 0;
     }
   },
   methods: {
@@ -117,6 +124,20 @@ export default {
     },
     previous() {
       this.usersPaginator.previous();
+    },
+    groupsUpdated(managedUserProfile) {
+      services.ManagedUserProfileService.update({
+        lookup: managedUserProfile.userId,
+        data: managedUserProfile
+      }).finally(() => {
+        this.reloadUserProfiles();
+      });
+    },
+    reloadUserProfiles() {
+      services.ManagedUserProfileService.list({
+        limit: 10,
+        offset: this.currentOffset
+      }).then(users => (this.usersPaginator = users));
     }
   }
 };
diff --git a/django_airavata/apps/api/serializers.py b/django_airavata/apps/api/serializers.py
index bbb66a8..1589702 100644
--- a/django_airavata/apps/api/serializers.py
+++ b/django_airavata/apps/api/serializers.py
@@ -592,15 +592,6 @@ class GroupResourceProfileSerializer(
             ResourcePermissionType.WRITE)
 
 
-class SharedGroups(serializers.Serializer):
-    groupList = serializers.ListField(child=serializers.CharField())
-    entityId = serializers.CharField()
-
-    def update(self, instance, validated_data):
-        instance["groupList"] = validated_data["groupList"]
-        return instance
-
-
 class UserPermissionSerializer(serializers.Serializer):
     user = UserProfileSerializer()
     permissionType = serializers.IntegerField()
@@ -830,3 +821,12 @@ class ManagedUserProfile(serializers.Serializer):
         view_name='django_airavata_api:managed-user-profile-detail',
         lookup_field='userId',
         lookup_url_kwarg='user_id')
+
+    def update(self, instance, validated_data):
+        existing_group_ids = [group.id for group in instance['groups']]
+        new_group_ids = [group['id'] for group in validated_data['groups']]
+        instance['_added_group_ids'] = list(
+            set(new_group_ids) - set(existing_group_ids))
+        instance['_removed_group_ids'] = list(
+            set(existing_group_ids) - set(new_group_ids))
+        return instance
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/index.js b/django_airavata/apps/api/static/django_airavata_api/js/index.js
index 49b8f75..67a26d5 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/index.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/index.js
@@ -22,6 +22,7 @@ import GroupComputeResourcePreference from "./models/GroupComputeResourcePrefere
 import GroupPermission from "./models/GroupPermission";
 import GroupResourceProfile from "./models/GroupResourceProfile";
 import InputDataObjectType from "./models/InputDataObjectType";
+import ManagedUserProfile from "./models/ManagedUserProfile";
 import OutputDataObjectType from "./models/OutputDataObjectType";
 import ParallelismType from "./models/ParallelismType";
 import Project from "./models/Project";
@@ -76,6 +77,7 @@ const models = {
   GroupPermission,
   GroupResourceProfile,
   InputDataObjectType,
+  ManagedUserProfile,
   OutputDataObjectType,
   ParallelismType,
   Project,
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index c42d304..b0797ec 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -1406,6 +1406,17 @@ class ManagedUserViewSet(mixins.CreateModelMixin,
         return self._convert_user_profile(
             iam_admin_client.get_user(lookup_value))
 
+    def perform_update(self, serializer):
+        managed_user_profile = serializer.save()
+        group_manager_client = self.request.profile_service['group_manager']
+        user_id = managed_user_profile['airavataInternalUserId']
+        for group_id in managed_user_profile['_added_group_ids']:
+            group_manager_client.addUsersToGroup(
+                self.authz_token, [user_id], group_id)
+        for group_id in managed_user_profile['_removed_group_ids']:
+            group_manager_client.removeUsersFromGroup(
+                self.authz_token, [user_id], group_id)
+
     def _convert_user_profile(self, user_profile):
         user_profile_client = self.request.profile_service['user_profile']
         group_manager_client = self.request.profile_service['group_manager']
diff --git a/django_airavata/settings_local_seagrid.py b/django_airavata/settings_local_seagrid.py
new file mode 100644
index 0000000..c193a3a
--- /dev/null
+++ b/django_airavata/settings_local_seagrid.py
@@ -0,0 +1,147 @@
+"""
+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.
+"""
+
+import os
+
+from . import webpack_loader_util
+
+# 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 configuration
+
+DEBUG = True
+ALLOWED_HOSTS = ['localhost']
+
+# 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 = 'smtp.gmail.com'
+# EMAIL_PORT = 587
+# EMAIL_HOST_USER = 'pga.airavata@gmail.com'
+# EMAIL_HOST_PASSWORD = 'airavata12'
+# EMAIL_USE_TLS = True
+ADMINS = [('Marcus Christie', 'machrist@iu.edu')]
+SERVER_EMAIL = 'pga.airavata@gmail.com'
+
+# Keycloak Configuration
+KEYCLOAK_CLIENT_ID = 'pga'
+KEYCLOAK_CLIENT_SECRET = '5d2dc66a-f54e-4fa9-b78f-80d33aa862c1'
+KEYCLOAK_AUTHORIZE_URL = 'https://iamdev.scigap.org/auth/realms/seagrid/protocol/openid-connect/auth'
+KEYCLOAK_TOKEN_URL = 'https://iamdev.scigap.org/auth/realms/seagrid/protocol/openid-connect/token'
+KEYCLOAK_USERINFO_URL = 'https://iamdev.scigap.org/auth/realms/seagrid/protocol/openid-connect/userinfo'
+KEYCLOAK_LOGOUT_URL = 'https://iamdev.scigap.org/auth/realms/seagrid/protocol/openid-connect/logout'
+KEYCLOAK_CA_CERTFILE = os.path.join(
+    BASE_DIR,
+    "django_airavata",
+    "resources",
+    "incommon_rsa_server_ca.pem")
+KEYCLOAK_VERIFY_SSL = True
+
+AUTHENTICATION_OPTIONS = {
+    'password': {
+        'name': 'SEAGrid'
+    },
+    'external': [
+        {
+            'idp_alias': 'oidc',
+            'name': 'CILogon',
+        },
+        # {
+        #     'idp_alias': 'iucas',
+        #     'name': 'IU CAS',
+        # },
+    ]
+}
+
+# Airavata API Configuration
+GATEWAY_ID = 'seagrid'
+AIRAVATA_API_HOST = 'apidev.scigap.org'
+AIRAVATA_API_PORT = 9930
+AIRAVATA_API_SECURE = True
+# AIRAVATA_API_HOST = 'localhost'
+# AIRAVATA_API_PORT = 8930
+# AIRAVATA_API_SECURE = False
+GATEWAY_DATA_STORE_RESOURCE_ID = 'pgadev.scigap.org_7ddf28fd-d503-4ff8-bbc5-3279a7c3b99e'
+GATEWAY_DATA_STORE_DIR = '/tmp/experiment-data-dir'
+GATEWAY_DATA_STORE_HOSTNAME = 'localhost'
+
+# Profile Service Configuration
+PROFILE_SERVICE_HOST = AIRAVATA_API_HOST
+PROFILE_SERVICE_PORT = 8962
+PROFILE_SERVICE_SECURE = False
+
+# Sharing API Configuration
+SHARING_API_HOST = AIRAVATA_API_HOST
+SHARING_API_PORT = 7878
+SHARING_API_SECURE = False
+
+# Portal settings
+PORTAL_TITLE = 'Django Airavata Gateway'
+
+# Logging configuration
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': False,
+    'formatters': {
+        'verbose': {
+            'format': '[%(asctime)s %(name)s:%(lineno)d %(levelname)s] %(message)s'
+        },
+    },
+    'filters': {
+        'require_debug_true': {
+            '()': 'django.utils.log.RequireDebugTrue',
+        },
+    },
+    'handlers': {
+        'console': {
+            'filters': ['require_debug_true'],
+            'class': 'logging.StreamHandler',
+            'formatter': 'verbose',
+            'level': 'DEBUG',
+        },
+        # 'file': {
+        #     'class': 'logging.FileHandler',
+        #     'filename': 'django_airavata.log',
+        #     'formatter': 'verbose',
+        # },
+    },
+    'loggers': {
+        'django': {
+            'handlers': ['console'],
+            'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
+        },
+        'airavata': {
+            'handlers': ['console'],
+            'level': 'DEBUG',
+        },
+        'django_airavata': {
+            'handlers': ['console'],
+            'level': 'DEBUG',
+        },
+        'django_airavata.utils': {
+            'handlers': ['console'],
+            'level': 'DEBUG',
+        },
+        'thrift_connector': {
+            'handlers': ['console'],
+            'level': 'DEBUG',
+        },
+        'simccs_maptool': {
+            'handlers': ['console'],
+            'level': 'DEBUG',
+        },
+        # 'requests_oauthlib': {
+        #     'handlers': ['console'],
+        #     'level': 'DEBUG',
+        # },
+    },
+}
+
+# WEBPACK_LOADER = webpack_loader_util.create_webpack_loader_config("/tmp")


[airavata-django-portal] 04/05: AIRAVATA-3040 Don't allow editing owner of group

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

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

commit 61f918ba30f8f6422b471596e33c0bc007022391
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu May 30 12:59:16 2019 -0400

    AIRAVATA-3040 Don't allow editing owner of group
---
 .../src/components/users/UserDetailsContainer.vue                 | 1 +
 .../src/components/users/UserGroupMembershipEditor.vue            | 8 +++++++-
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
index f47101b..33f96c5 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
@@ -2,6 +2,7 @@
   <user-group-membership-editor
     v-model="localManagedUserProfile.groups"
     :editable-groups="editableGroups"
+    :airavata-internal-user-id="managedUserProfile.airavataInternalUserId"
     @input="groupsUpdated"
   />
 </template>
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserGroupMembershipEditor.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserGroupMembershipEditor.vue
index 2695827..37bee7c 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserGroupMembershipEditor.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserGroupMembershipEditor.vue
@@ -41,6 +41,10 @@ export default {
       type: Array,
       required: true
     },
+    airavataInternalUserId: {
+      type: String,
+      required: true
+    },
     editableGroups: {
       type: Array,
       required: true
@@ -112,7 +116,9 @@ export default {
       return {
         text: group.name,
         value: group.id,
-        disabled: !group.userHasWriteAccess
+        disabled:
+          !group.userHasWriteAccess ||
+          group.ownerId === this.airavataInternalUserId
       };
     }
   }


[airavata-django-portal] 03/05: AIRAVATA-3040 Put gateway groups at top of list

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

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

commit 89bb973dc7f5399c911b43884ec06321e894d4a4
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Thu May 30 12:55:33 2019 -0400

    AIRAVATA-3040 Put gateway groups at top of list
---
 .../components/users/UserGroupMembershipEditor.vue | 94 ++++++++++++++++------
 .../static/django_airavata_api/js/models/Group.js  |  4 +
 2 files changed, 74 insertions(+), 24 deletions(-)

diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserGroupMembershipEditor.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserGroupMembershipEditor.vue
index ba8a940..2695827 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserGroupMembershipEditor.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserGroupMembershipEditor.vue
@@ -2,14 +2,36 @@
   <b-form-group label="Groups">
     <b-form-checkbox-group
       :checked="selected"
-      :options="options"
+      :options="userDefinedGroupOptions"
       @input="groupsUpdated"
       stacked
-    ></b-form-checkbox-group>
+    >
+      <template slot="first">
+        <b-form-checkbox
+          v-if="gatewayUsersGroupOption"
+          :value="gatewayUsersGroupOption.value"
+          :disabled="gatewayUsersGroupOption.disabled"
+        >{{ gatewayUsersGroupOption.text }} <b-badge>Default</b-badge>
+        </b-form-checkbox>
+        <b-form-checkbox
+          v-if="adminsGroupOption"
+          :value="adminsGroupOption.value"
+          :disabled="adminsGroupOption.disabled"
+        >{{ adminsGroupOption.text }} <b-badge>Admins</b-badge>
+        </b-form-checkbox>
+        <b-form-checkbox
+          v-if="readOnlyAdminsGroupOption"
+          :value="readOnlyAdminsGroupOption.value"
+          :disabled="readOnlyAdminsGroupOption.disabled"
+        >{{ readOnlyAdminsGroupOption.text }} <b-badge>Read Only Admins</b-badge>
+        </b-form-checkbox>
+      </template>
+    </b-form-checkbox-group>
   </b-form-group>
 </template>
 
 <script>
+import { utils } from "django-airavata-api";
 import { mixins } from "django-airavata-common-ui";
 export default {
   name: "user-group-membership-editor",
@@ -28,28 +50,43 @@ export default {
     selected() {
       return this.data.map(g => g.id);
     },
-    options() {
-      const options = this.data.map(g => {
-        return {
-          text: g.name,
-          value: g.id,
-          disabled: true
-        };
+    combinedGroups() {
+      const groups = {};
+      this.value.concat(this.editableGroups).forEach(g => {
+        groups[g.id] = g;
       });
-      this.editableGroups.forEach(g => {
-        const editableOption = options.find(opt => opt.value === g.id);
-        if (editableOption) {
-          editableOption.disabled = false;
-        } else {
-          options.push({
-            text: g.name,
-            value: g.id,
-            disabled: false
-          });
-        }
-      });
-      // TODO: sort the options?
-      return options;
+      return Object.values(groups);
+    },
+    userDefinedGroups() {
+      return this.combinedGroups
+        ? this.combinedGroups.filter(g => {
+            return (
+              !g.isDefaultGatewayUsersGroup &&
+              !g.isGatewayAdminsGroup &&
+              !g.isReadOnlyGatewayAdminsGroup
+            );
+          })
+        : [];
+    },
+    userDefinedGroupOptions() {
+      const options = this.userDefinedGroups.map(g =>
+        this.createGroupOption(g)
+      );
+      return utils.StringUtils.sortIgnoreCase(options, o => o.text);
+    },
+    gatewayUsersGroupOption() {
+      const group = this.combinedGroups.find(g => g.isDefaultGatewayUsersGroup);
+      return group ? this.createGroupOption(group) : null;
+    },
+    adminsGroupOption() {
+      const group = this.combinedGroups.find(g => g.isGatewayAdminsGroup);
+      return group ? this.createGroupOption(group) : null;
+    },
+    readOnlyAdminsGroupOption() {
+      const group = this.combinedGroups.find(
+        g => g.isReadOnlyGatewayAdminsGroup
+      );
+      return group ? this.createGroupOption(group) : null;
     }
   },
   methods: {
@@ -57,7 +94,9 @@ export default {
       // Check for added groups
       for (const checkedGroupId of checkedGroups) {
         if (!this.data.find(g => g.id === checkedGroupId)) {
-          const addedGroup = this.editableGroups.find(g => g.id === checkedGroupId);
+          const addedGroup = this.editableGroups.find(
+            g => g.id === checkedGroupId
+          );
           this.data.push(addedGroup);
         }
       }
@@ -68,6 +107,13 @@ export default {
           this.data.splice(groupIndex, 1);
         }
       }
+    },
+    createGroupOption(group) {
+      return {
+        text: group.name,
+        value: group.id,
+        disabled: !group.userHasWriteAccess
+      };
     }
   }
 };
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/Group.js b/django_airavata/apps/api/static/django_airavata_api/js/models/Group.js
index 77e12cd..fff93c0 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/Group.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/Group.js
@@ -57,4 +57,8 @@ export default class Group extends BaseModel {
   get isAdminGroup() {
     return this.isReadOnlyGatewayAdminsGroup || this.isGatewayAdminsGroup;
   }
+
+  get userHasWriteAccess() {
+    return this.isOwner || this.isAdmin;
+  }
 }