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/28 16:54:14 UTC

[airavata-django-portal] branch master updated: AIRAVATA-2988 Initial WIP user management view

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


The following commit(s) were added to refs/heads/master by this push:
     new 5609380  AIRAVATA-2988 Initial WIP user management view
5609380 is described below

commit 5609380c7b01cb9abae0bf96c522a1124a81cdfb
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Tue May 28 12:33:12 2019 -0400

    AIRAVATA-2988 Initial WIP user management view
---
 .../iam/admin/services/cpi/IamAdminServices-remote |   7 +
 .../iam/admin/services/cpi/IamAdminServices.py     | 302 ++++++++++++++++++++-
 .../components/users/UserManagementContainer.vue   |  96 +++++++
 .../static/django_airavata_admin/src/router.js     |   6 +
 .../apps/admin/templates/admin/admin_base.html     |   5 +
 django_airavata/apps/admin/urls.py                 |   1 +
 django_airavata/apps/admin/views.py                |   9 +-
 django_airavata/apps/api/serializers.py            |  16 ++
 .../api/static/django_airavata_api/js/index.js     |   1 +
 .../js/models/ManagedUserProfile.js                |  25 ++
 .../django_airavata_api/js/service_config.js       |  12 +-
 django_airavata/apps/api/urls.py                   |   2 +
 django_airavata/apps/api/views.py                  |  41 +++
 django_airavata/apps/auth/iam_admin_client.py      |   5 +
 14 files changed, 512 insertions(+), 16 deletions(-)

diff --git a/airavata/service/profile/iam/admin/services/cpi/IamAdminServices-remote b/airavata/service/profile/iam/admin/services/cpi/IamAdminServices-remote
index ba10cb0..b08c976 100755
--- a/airavata/service/profile/iam/admin/services/cpi/IamAdminServices-remote
+++ b/airavata/service/profile/iam/admin/services/cpi/IamAdminServices-remote
@@ -32,6 +32,7 @@ if len(sys.argv) <= 1 or sys.argv[1] == '--help':
     print('  bool isUserEnabled(AuthzToken authzToken, string username)')
     print('  bool isUserExist(AuthzToken authzToken, string username)')
     print('  UserProfile getUser(AuthzToken authzToken, string username)')
+    print('   getUsers(AuthzToken authzToken, i32 offset, i32 limit, string search)')
     print('  bool resetUserPassword(AuthzToken authzToken, string username, string newPassword)')
     print('   findUsers(AuthzToken authzToken, string email, string userId)')
     print('  void updateUserProfile(AuthzToken authzToken, UserProfile userDetails)')
@@ -165,6 +166,12 @@ elif cmd == 'getUser':
         sys.exit(1)
     pp.pprint(client.getUser(eval(args[0]), args[1],))
 
+elif cmd == 'getUsers':
+    if len(args) != 4:
+        print('getUsers requires 4 args')
+        sys.exit(1)
+    pp.pprint(client.getUsers(eval(args[0]), eval(args[1]), eval(args[2]), args[3],))
+
 elif cmd == 'resetUserPassword':
     if len(args) != 3:
         print('resetUserPassword requires 3 args')
diff --git a/airavata/service/profile/iam/admin/services/cpi/IamAdminServices.py b/airavata/service/profile/iam/admin/services/cpi/IamAdminServices.py
index 2e6fac8..7a5ac8b 100644
--- a/airavata/service/profile/iam/admin/services/cpi/IamAdminServices.py
+++ b/airavata/service/profile/iam/admin/services/cpi/IamAdminServices.py
@@ -79,6 +79,16 @@ class Iface(object):
         """
         pass
 
+    def getUsers(self, authzToken, offset, limit, search):
+        """
+        Parameters:
+         - authzToken
+         - offset
+         - limit
+         - search
+        """
+        pass
+
     def resetUserPassword(self, authzToken, username, newPassword):
         """
         Parameters:
@@ -434,6 +444,47 @@ class Client(Iface):
             raise result.ae
         raise TApplicationException(TApplicationException.MISSING_RESULT, "getUser failed: unknown result")
 
+    def getUsers(self, authzToken, offset, limit, search):
+        """
+        Parameters:
+         - authzToken
+         - offset
+         - limit
+         - search
+        """
+        self.send_getUsers(authzToken, offset, limit, search)
+        return self.recv_getUsers()
+
+    def send_getUsers(self, authzToken, offset, limit, search):
+        self._oprot.writeMessageBegin('getUsers', TMessageType.CALL, self._seqid)
+        args = getUsers_args()
+        args.authzToken = authzToken
+        args.offset = offset
+        args.limit = limit
+        args.search = search
+        args.write(self._oprot)
+        self._oprot.writeMessageEnd()
+        self._oprot.trans.flush()
+
+    def recv_getUsers(self):
+        iprot = self._iprot
+        (fname, mtype, rseqid) = iprot.readMessageBegin()
+        if mtype == TMessageType.EXCEPTION:
+            x = TApplicationException()
+            x.read(iprot)
+            iprot.readMessageEnd()
+            raise x
+        result = getUsers_result()
+        result.read(iprot)
+        iprot.readMessageEnd()
+        if result.success is not None:
+            return result.success
+        if result.Idse is not None:
+            raise result.Idse
+        if result.ae is not None:
+            raise result.ae
+        raise TApplicationException(TApplicationException.MISSING_RESULT, "getUsers failed: unknown result")
+
     def resetUserPassword(self, authzToken, username, newPassword):
         """
         Parameters:
@@ -675,6 +726,7 @@ class Processor(Iface, TProcessor):
         self._processMap["isUserEnabled"] = Processor.process_isUserEnabled
         self._processMap["isUserExist"] = Processor.process_isUserExist
         self._processMap["getUser"] = Processor.process_getUser
+        self._processMap["getUsers"] = Processor.process_getUsers
         self._processMap["resetUserPassword"] = Processor.process_resetUserPassword
         self._processMap["findUsers"] = Processor.process_findUsers
         self._processMap["updateUserProfile"] = Processor.process_updateUserProfile
@@ -894,6 +946,31 @@ class Processor(Iface, TProcessor):
         oprot.writeMessageEnd()
         oprot.trans.flush()
 
+    def process_getUsers(self, seqid, iprot, oprot):
+        args = getUsers_args()
+        args.read(iprot)
+        iprot.readMessageEnd()
+        result = getUsers_result()
+        try:
+            result.success = self._handler.getUsers(args.authzToken, args.offset, args.limit, args.search)
+            msg_type = TMessageType.REPLY
+        except (TTransport.TTransportException, KeyboardInterrupt, SystemExit):
+            raise
+        except airavata.service.profile.iam.admin.services.cpi.error.ttypes.IamAdminServicesException as Idse:
+            msg_type = TMessageType.REPLY
+            result.Idse = Idse
+        except airavata.api.error.ttypes.AuthorizationException as ae:
+            msg_type = TMessageType.REPLY
+            result.ae = ae
+        except Exception as ex:
+            msg_type = TMessageType.EXCEPTION
+            logging.exception(ex)
+            result = TApplicationException(TApplicationException.INTERNAL_ERROR, 'Internal error')
+        oprot.writeMessageBegin("getUsers", msg_type, seqid)
+        result.write(oprot)
+        oprot.writeMessageEnd()
+        oprot.trans.flush()
+
     def process_resetUserPassword(self, seqid, iprot, oprot):
         args = resetUserPassword_args()
         args.read(iprot)
@@ -2354,6 +2431,203 @@ class getUser_result(object):
         return not (self == other)
 
 
+class getUsers_args(object):
+    """
+    Attributes:
+     - authzToken
+     - offset
+     - limit
+     - search
+    """
+
+    thrift_spec = (
+        None,  # 0
+        (1, TType.STRUCT, 'authzToken', (airavata.model.security.ttypes.AuthzToken, airavata.model.security.ttypes.AuthzToken.thrift_spec), None, ),  # 1
+        (2, TType.I32, 'offset', None, None, ),  # 2
+        (3, TType.I32, 'limit', None, None, ),  # 3
+        (4, TType.STRING, 'search', 'UTF8', None, ),  # 4
+    )
+
+    def __init__(self, authzToken=None, offset=None, limit=None, search=None,):
+        self.authzToken = authzToken
+        self.offset = offset
+        self.limit = limit
+        self.search = search
+
+    def read(self, iprot):
+        if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None:
+            iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec))
+            return
+        iprot.readStructBegin()
+        while True:
+            (fname, ftype, fid) = iprot.readFieldBegin()
+            if ftype == TType.STOP:
+                break
+            if fid == 1:
+                if ftype == TType.STRUCT:
+                    self.authzToken = airavata.model.security.ttypes.AuthzToken()
+                    self.authzToken.read(iprot)
+                else:
+                    iprot.skip(ftype)
+            elif fid == 2:
+                if ftype == TType.I32:
+                    self.offset = iprot.readI32()
+                else:
+                    iprot.skip(ftype)
+            elif fid == 3:
+                if ftype == TType.I32:
+                    self.limit = iprot.readI32()
+                else:
+                    iprot.skip(ftype)
+            elif fid == 4:
+                if ftype == TType.STRING:
+                    self.search = iprot.readString().decode('utf-8') if sys.version_info[0] == 2 else iprot.readString()
+                else:
+                    iprot.skip(ftype)
+            else:
+                iprot.skip(ftype)
+            iprot.readFieldEnd()
+        iprot.readStructEnd()
+
+    def write(self, oprot):
+        if oprot._fast_encode is not None and self.thrift_spec is not None:
+            oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec)))
+            return
+        oprot.writeStructBegin('getUsers_args')
+        if self.authzToken is not None:
+            oprot.writeFieldBegin('authzToken', TType.STRUCT, 1)
+            self.authzToken.write(oprot)
+            oprot.writeFieldEnd()
+        if self.offset is not None:
+            oprot.writeFieldBegin('offset', TType.I32, 2)
+            oprot.writeI32(self.offset)
+            oprot.writeFieldEnd()
+        if self.limit is not None:
+            oprot.writeFieldBegin('limit', TType.I32, 3)
+            oprot.writeI32(self.limit)
+            oprot.writeFieldEnd()
+        if self.search is not None:
+            oprot.writeFieldBegin('search', TType.STRING, 4)
+            oprot.writeString(self.search.encode('utf-8') if sys.version_info[0] == 2 else self.search)
+            oprot.writeFieldEnd()
+        oprot.writeFieldStop()
+        oprot.writeStructEnd()
+
+    def validate(self):
+        if self.authzToken is None:
+            raise TProtocolException(message='Required field authzToken is unset!')
+        if self.offset is None:
+            raise TProtocolException(message='Required field offset is unset!')
+        if self.limit is None:
+            raise TProtocolException(message='Required field limit is unset!')
+        return
+
+    def __repr__(self):
+        L = ['%s=%r' % (key, value)
+             for key, value in self.__dict__.items()]
+        return '%s(%s)' % (self.__class__.__name__, ', '.join(L))
+
+    def __eq__(self, other):
+        return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
+
+    def __ne__(self, other):
+        return not (self == other)
+
+
+class getUsers_result(object):
+    """
+    Attributes:
+     - success
+     - Idse
+     - ae
+    """
+
+    thrift_spec = (
+        (0, TType.LIST, 'success', (TType.STRUCT, (airavata.model.user.ttypes.UserProfile, airavata.model.user.ttypes.UserProfile.thrift_spec), False), None, ),  # 0
+        (1, TType.STRUCT, 'Idse', (airavata.service.profile.iam.admin.services.cpi.error.ttypes.IamAdminServicesException, airavata.service.profile.iam.admin.services.cpi.error.ttypes.IamAdminServicesException.thrift_spec), None, ),  # 1
+        (2, TType.STRUCT, 'ae', (airavata.api.error.ttypes.AuthorizationException, airavata.api.error.ttypes.AuthorizationException.thrift_spec), None, ),  # 2
+    )
+
+    def __init__(self, success=None, Idse=None, ae=None,):
+        self.success = success
+        self.Idse = Idse
+        self.ae = ae
+
+    def read(self, iprot):
+        if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None:
+            iprot._fast_decode(self, iprot, (self.__class__, self.thrift_spec))
+            return
+        iprot.readStructBegin()
+        while True:
+            (fname, ftype, fid) = iprot.readFieldBegin()
+            if ftype == TType.STOP:
+                break
+            if fid == 0:
+                if ftype == TType.LIST:
+                    self.success = []
+                    (_etype3, _size0) = iprot.readListBegin()
+                    for _i4 in range(_size0):
+                        _elem5 = airavata.model.user.ttypes.UserProfile()
+                        _elem5.read(iprot)
+                        self.success.append(_elem5)
+                    iprot.readListEnd()
+                else:
+                    iprot.skip(ftype)
+            elif fid == 1:
+                if ftype == TType.STRUCT:
+                    self.Idse = airavata.service.profile.iam.admin.services.cpi.error.ttypes.IamAdminServicesException()
+                    self.Idse.read(iprot)
+                else:
+                    iprot.skip(ftype)
+            elif fid == 2:
+                if ftype == TType.STRUCT:
+                    self.ae = airavata.api.error.ttypes.AuthorizationException()
+                    self.ae.read(iprot)
+                else:
+                    iprot.skip(ftype)
+            else:
+                iprot.skip(ftype)
+            iprot.readFieldEnd()
+        iprot.readStructEnd()
+
+    def write(self, oprot):
+        if oprot._fast_encode is not None and self.thrift_spec is not None:
+            oprot.trans.write(oprot._fast_encode(self, (self.__class__, self.thrift_spec)))
+            return
+        oprot.writeStructBegin('getUsers_result')
+        if self.success is not None:
+            oprot.writeFieldBegin('success', TType.LIST, 0)
+            oprot.writeListBegin(TType.STRUCT, len(self.success))
+            for iter6 in self.success:
+                iter6.write(oprot)
+            oprot.writeListEnd()
+            oprot.writeFieldEnd()
+        if self.Idse is not None:
+            oprot.writeFieldBegin('Idse', TType.STRUCT, 1)
+            self.Idse.write(oprot)
+            oprot.writeFieldEnd()
+        if self.ae is not None:
+            oprot.writeFieldBegin('ae', TType.STRUCT, 2)
+            self.ae.write(oprot)
+            oprot.writeFieldEnd()
+        oprot.writeFieldStop()
+        oprot.writeStructEnd()
+
+    def validate(self):
+        return
+
+    def __repr__(self):
+        L = ['%s=%r' % (key, value)
+             for key, value in self.__dict__.items()]
+        return '%s(%s)' % (self.__class__.__name__, ', '.join(L))
+
+    def __eq__(self, other):
+        return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
+
+    def __ne__(self, other):
+        return not (self == other)
+
+
 class resetUserPassword_args(object):
     """
     Attributes:
@@ -2652,11 +2926,11 @@ class findUsers_result(object):
             if fid == 0:
                 if ftype == TType.LIST:
                     self.success = []
-                    (_etype3, _size0) = iprot.readListBegin()
-                    for _i4 in range(_size0):
-                        _elem5 = airavata.model.user.ttypes.UserProfile()
-                        _elem5.read(iprot)
-                        self.success.append(_elem5)
+                    (_etype10, _size7) = iprot.readListBegin()
+                    for _i11 in range(_size7):
+                        _elem12 = airavata.model.user.ttypes.UserProfile()
+                        _elem12.read(iprot)
+                        self.success.append(_elem12)
                     iprot.readListEnd()
                 else:
                     iprot.skip(ftype)
@@ -2685,8 +2959,8 @@ class findUsers_result(object):
         if self.success is not None:
             oprot.writeFieldBegin('success', TType.LIST, 0)
             oprot.writeListBegin(TType.STRUCT, len(self.success))
-            for iter6 in self.success:
-                iter6.write(oprot)
+            for iter13 in self.success:
+                iter13.write(oprot)
             oprot.writeListEnd()
             oprot.writeFieldEnd()
         if self.Idse is not None:
@@ -3327,11 +3601,11 @@ class getUsersWithRole_result(object):
             if fid == 0:
                 if ftype == TType.LIST:
                     self.success = []
-                    (_etype10, _size7) = iprot.readListBegin()
-                    for _i11 in range(_size7):
-                        _elem12 = airavata.model.user.ttypes.UserProfile()
-                        _elem12.read(iprot)
-                        self.success.append(_elem12)
+                    (_etype17, _size14) = iprot.readListBegin()
+                    for _i18 in range(_size14):
+                        _elem19 = airavata.model.user.ttypes.UserProfile()
+                        _elem19.read(iprot)
+                        self.success.append(_elem19)
                     iprot.readListEnd()
                 else:
                     iprot.skip(ftype)
@@ -3360,8 +3634,8 @@ class getUsersWithRole_result(object):
         if self.success is not None:
             oprot.writeFieldBegin('success', TType.LIST, 0)
             oprot.writeListBegin(TType.STRUCT, len(self.success))
-            for iter13 in self.success:
-                iter13.write(oprot)
+            for iter20 in self.success:
+                iter20.write(oprot)
             oprot.writeListEnd()
             oprot.writeFieldEnd()
         if self.Idse is not None:
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
new file mode 100644
index 0000000..4a5d7c0
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue
@@ -0,0 +1,96 @@
+<template>
+  <div>
+    <div class="row">
+      <div class="col">
+        <h1 class="h4 mb-4">Users</h1>
+      </div>
+    </div>
+    <div class="row">
+      <div class="col">
+        <div class="card">
+          <div class="card-body">
+            <b-table
+              hover
+              :fields="fields"
+              :items="items"
+            >
+            </b-table>
+            <pager
+              v-bind:paginator="usersPaginator"
+              v-on:next="next"
+              v-on:previous="previous"
+            ></pager>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { services } from "django-airavata-api";
+import { components } from "django-airavata-common-ui";
+
+export default {
+  name: "user-management-container",
+  data() {
+    return {
+      usersPaginator: null
+    };
+  },
+  components: {
+    pager: components.Pager
+  },
+  created() {
+    services.ManagedUserProfileService.list({ limit: 10 }).then(
+      users => (this.usersPaginator = users)
+    );
+  },
+  computed: {
+    fields() {
+      return [
+        {
+          label: "First Name",
+          key: "firstName"
+        },
+        {
+          label: "Last Name",
+          key: "lastName"
+        },
+        {
+          label: "Username",
+          key: "userId"
+        },
+        {
+          label: "Email",
+          key: "email"
+        },
+        {
+          label: "Enabled",
+          key: "enabled"
+        },
+        {
+          label: "Email Verified",
+          key: "emailVerified"
+        },
+        {
+          label: "Action",
+          key: "action"
+        }
+      ];
+    },
+    items() {
+      return this.usersPaginator ? this.usersPaginator.results : [];
+    }
+  },
+  methods: {
+    next() {
+      this.usersPaginator.next();
+    },
+    previous() {
+      this.usersPaginator.previous();
+    }
+  }
+};
+</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 942cc64..be3b2c0 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
@@ -9,6 +9,7 @@ import ComputeResourcePreferenceDashboard from "./components/dashboards/ComputeR
 import CredentialStoreDashboard from "./components/dashboards/CredentialStoreDashboard";
 import GatewayResourceProfileEditorContainer from "./components/gatewayprofile/GatewayResourceProfileEditorContainer.vue";
 import GroupComputeResourcePreference from "./components/admin/group_resource_preferences/GroupComputeResourcePreference";
+import UserManagementContainer from "./components/users/UserManagementContainer.vue";
 import VueRouter from "vue-router";
 
 const routes = [
@@ -116,6 +117,11 @@ const routes = [
     path: "/gateway-resource-profile",
     component: GatewayResourceProfileEditorContainer,
     name: "gateway-resource-profile"
+  },
+  {
+    path: "/users",
+    component: UserManagementContainer,
+    name: "users"
   }
 ];
 export default new VueRouter({
diff --git a/django_airavata/apps/admin/templates/admin/admin_base.html b/django_airavata/apps/admin/templates/admin/admin_base.html
index 57fd3c3..e883735 100644
--- a/django_airavata/apps/admin/templates/admin/admin_base.html
+++ b/django_airavata/apps/admin/templates/admin/admin_base.html
@@ -14,6 +14,11 @@
             <i class="fa fa-cogs"></i> <span class=sr-only>Application Catalog</span>
         </a>
         {% endif %}
+        {% if request.is_gateway_admin %}
+        <a href="{% url 'django_airavata_admin:users' %}" class="c-nav__item {% if request.active_nav_item == 'users' %}is-active{% endif %}" data-toggle=tooltip data-placement=right title="Manage Users">
+            <i class="fa fa-users"></i> <span class=sr-only>Manage Users</span>
+        </a>
+        {% endif %}
         <a href="{% url 'django_airavata_admin:credential_store' %}" class="c-nav__item {% if request.active_nav_item == 'credential_store' %}is-active{% endif %}" data-toggle=tooltip data-placement=right title="Credential Store">
             <i class="fa fa-lock"></i> <span class=sr-only>Credential Store</span>
         </a>
diff --git a/django_airavata/apps/admin/urls.py b/django_airavata/apps/admin/urls.py
index 47ab077..d706078 100644
--- a/django_airavata/apps/admin/urls.py
+++ b/django_airavata/apps/admin/urls.py
@@ -11,4 +11,5 @@ urlpatterns = [
         name='group_resource_profile'),
     url(r'^gateway-resource-profile/', views.gateway_resource_profile,
         name='gateway_resource_profile'),
+    url(r'^users/', views.users, name='users'),
 ]
diff --git a/django_airavata/apps/admin/views.py b/django_airavata/apps/admin/views.py
index 58c94d0..627febf 100644
--- a/django_airavata/apps/admin/views.py
+++ b/django_airavata/apps/admin/views.py
@@ -8,7 +8,8 @@ def home(request):
     if request.is_gateway_admin or request.is_read_only_gateway_admin:
         return redirect(reverse('django_airavata_admin:app_catalog'))
     else:
-        return redirect(reverse('django_airavata_admin:group_resource_profile'))
+        return redirect(
+            reverse('django_airavata_admin:group_resource_profile'))
 
 
 @login_required
@@ -38,3 +39,9 @@ def group_resource_profile(request):
 def gateway_resource_profile(request):
     request.active_nav_item = 'gateway_resource_profile'
     return render(request, 'admin/admin_base.html')
+
+
+@login_required
+def users(request):
+    request.active_nav_item = 'users'
+    return render(request, 'admin/admin_base.html')
diff --git a/django_airavata/apps/api/serializers.py b/django_airavata/apps/api/serializers.py
index 5f0685c..b36524e 100644
--- a/django_airavata/apps/api/serializers.py
+++ b/django_airavata/apps/api/serializers.py
@@ -753,3 +753,19 @@ class WorkspacePreferencesSerializer(serializers.ModelSerializer):
     class Meta:
         model = models.WorkspacePreferences
         exclude = ('username',)
+
+
+class ManagedUserProfile(serializers.Serializer):
+    airavataInternalUserId = serializers.CharField()
+    userId = serializers.CharField()
+    gatewayId = serializers.CharField()
+    email = serializers.CharField()
+    firstName = serializers.CharField()
+    lastName = serializers.CharField()
+    enabled = serializers.BooleanField()
+    emailVerified = serializers.BooleanField()
+    groups = GroupSerializer(many=True)
+    url = FullyEncodedHyperlinkedIdentityField(
+        view_name='django_airavata_api:managed-user-profile-detail',
+        lookup_field='userId',
+        lookup_url_kwarg='user_id')
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 c4d24c8..19c53a5 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
@@ -106,6 +106,7 @@ const services = {
   GroupResourceProfileService: ServiceFactory.service("GroupResourceProfiles"),
   GroupService: ServiceFactory.service("Groups"),
   LocaJobSubmissionService,
+  ManagedUserProfileService: ServiceFactory.service("ManagedUserProfiles"),
   ParserService: ServiceFactory.service("Parsers"),
   ProjectService: ServiceFactory.service("Projects"),
   SCPDataMovementService,
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ManagedUserProfile.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ManagedUserProfile.js
new file mode 100644
index 0000000..d29065a
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ManagedUserProfile.js
@@ -0,0 +1,25 @@
+import BaseModel from "./BaseModel";
+import Group from "./Group";
+
+const FIELDS = [
+  "userModelVersion",
+  "airavataInternalUserId",
+  "userId",
+  "gatewayId",
+  "email",
+  "firstName",
+  "lastName",
+  "enabled",
+  "emailVerified",
+  {
+    name: "groups",
+    type: Group,
+    list: true
+  }
+];
+
+export default class ManagedUserProfile extends BaseModel {
+  constructor(data = {}) {
+    super(FIELDS, data);
+  }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
index 9cdc380..ac651b7 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js
@@ -12,6 +12,7 @@ import FullExperiment from "./models/FullExperiment";
 import GatewayResourceProfile from "./models/GatewayResourceProfile";
 import Group from "./models/Group";
 import GroupResourceProfile from "./models/GroupResourceProfile";
+import ManagedUserProfile from "./models/ManagedUserProfile";
 import Parser from "./models/Parser";
 import Project from "./models/Project";
 import SharedEntity from "./models/SharedEntity";
@@ -181,7 +182,9 @@ export default {
     ],
     modelClass: ExperimentSummary,
     pagination: true,
-    queryParams: ["limit", "offset"].concat(ExperimentSearchFields.values.map(f => f.name))
+    queryParams: ["limit", "offset"].concat(
+      ExperimentSearchFields.values.map(f => f.name)
+    )
   },
   FullExperiments: {
     url: "/api/full-experiments",
@@ -217,6 +220,13 @@ export default {
     queryParams: ["limit", "offset"],
     modelClass: Group
   },
+  ManagedUserProfiles: {
+    url: "/api/managed-user-profiles",
+    viewSet: true,
+    pagination: true,
+    queryParams: ["limit", "offset", "search"],
+    modelClass: ManagedUserProfile
+  },
   Parsers: {
     url: "/api/parsers",
     viewSet: true,
diff --git a/django_airavata/apps/api/urls.py b/django_airavata/apps/api/urls.py
index d835967..d7c716f 100644
--- a/django_airavata/apps/api/urls.py
+++ b/django_airavata/apps/api/urls.py
@@ -41,6 +41,8 @@ router.register(r'storage-preferences',
                 views.StoragePreferenceViewSet,
                 base_name='storage-preference')
 router.register(r'parsers', views.ParserViewSet, base_name='parser')
+router.register(r'managed-user-profiles', views.ManagedUserViewSet,
+                base_name='managed-user-profile')
 
 app_name = 'django_airavata_api'
 urlpatterns = [
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index 989e4ae..bf25e84 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -31,12 +31,14 @@ from airavata.model.data.movement.ttypes import (
 )
 from airavata.model.experiment.ttypes import ExperimentSearchFields
 from airavata.model.group.ttypes import ResourcePermissionType
+from airavata.model.user.ttypes import Status
 from django_airavata.apps.api.view_utils import (
     APIBackedViewSet,
     APIResultIterator,
     APIResultPagination,
     GenericAPIBackedViewSet
 )
+from django_airavata.apps.auth import iam_admin_client
 from django_airavata.apps.workspace.models import User_Files
 
 from . import datastore, helpers, models, serializers, thrift_utils
@@ -1361,3 +1363,42 @@ class WorkspacePreferencesView(APIView):
         serializer = self.serializer_class(
             workspace_preferences, context={'request': request})
         return Response(serializer.data)
+
+
+class ManagedUserViewSet(mixins.CreateModelMixin,
+                         mixins.RetrieveModelMixin,
+                         mixins.UpdateModelMixin,
+                         mixins.ListModelMixin,
+                         GenericAPIBackedViewSet):
+    serializer_class = serializers.ManagedUserProfile
+    pagination_class = APIResultPagination
+    lookup_field = 'user_id'
+
+    def get_list(self):
+        search = self.request.GET.get('search', None)
+
+        convert_user_profile = self._convert_user_profile
+
+        class ManagedUsersResultIterator(APIResultIterator):
+            def get_results(self, limit=-1, offset=0):
+                return map(convert_user_profile,
+                           iam_admin_client.get_users(offset, limit, search))
+        return ManagedUsersResultIterator()
+
+    def get_instance(self, lookup_value):
+        return self._convert_user_profile(
+            iam_admin_client.get_user(lookup_value))
+
+    def _convert_user_profile(self, user_profile):
+        return {
+            'airavataInternalUserId': user_profile.airavataInternalUserId,
+            'userId': user_profile.userId,
+            'gatewayId': user_profile.gatewayId,
+            'email': user_profile.emails[0],
+            'firstName': user_profile.firstName,
+            'lastName': user_profile.lastName,
+            # TODO: fix this to distinguish between enabled and emailVerified
+            'enabled': user_profile.State == Status.CONFIRMED,
+            'emailVerified': user_profile.State == Status.CONFIRMED,
+            'groups': []
+        }
diff --git a/django_airavata/apps/auth/iam_admin_client.py b/django_airavata/apps/auth/iam_admin_client.py
index 6dc2ceb..05e3b2b 100644
--- a/django_airavata/apps/auth/iam_admin_client.py
+++ b/django_airavata/apps/auth/iam_admin_client.py
@@ -47,6 +47,11 @@ def get_user(username):
     return iamadmin_client_pool.getUser(authz_token, username)
 
 
+def get_users(offset, limit, search=None):
+    authz_token = utils.get_service_account_authz_token()
+    return iamadmin_client_pool.getUsers(authz_token, offset, limit, search)
+
+
 def reset_user_password(username, new_password):
     authz_token = utils.get_service_account_authz_token()
     return iamadmin_client_pool.resetUserPassword(