From 49c66baafbceec660ffde44d4d356ad3b39d538d Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Thu, 12 Jul 2018 09:25:03 -0400 Subject: [PATCH 1/9] add GET/DELETE endpoint for external identities with tests --- api/users/serializers.py | 14 ++++ api/users/urls.py | 2 + api/users/views.py | 56 ++++++++++++++ .../views/test_user_external_identities.py | 76 +++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 api_tests/users/views/test_user_external_identities.py diff --git a/api/users/serializers.py b/api/users/serializers.py index 83bea628dab..e32e2c36d3f 100644 --- a/api/users/serializers.py +++ b/api/users/serializers.py @@ -252,3 +252,17 @@ def get_absolute_url(self, obj): class Meta: type_ = 'institutions' + + +class UserIdentitiesSerializer(BaseAPISerializer): + data = ser.DictField() + links = LinksField({'self': 'get_self_url'}) + + def get_self_url(self, obj): + return absolute_reverse('users:user-identities-list', kwargs={ + 'version': self.context['request'].parser_context['kwargs']['version'], + 'user_id': obj['self']._id + }) + + class Meta: + type_ = 'external_identities' diff --git a/api/users/urls.py b/api/users/urls.py index ab72931e6ff..0e94b418fb8 100644 --- a/api/users/urls.py +++ b/api/users/urls.py @@ -16,4 +16,6 @@ url(r'^(?P\w+)/registrations/$', views.UserRegistrations.as_view(), name=views.UserRegistrations.view_name), url(r'^(?P\w+)/quickfiles/$', views.UserQuickFiles.as_view(), name=views.UserQuickFiles.view_name), url(r'^(?P\w+)/relationships/institutions/$', views.UserInstitutionsRelationship.as_view(), name=views.UserInstitutionsRelationship.view_name), + url(r'^(?P\w+)/settings/identities/$', views.UserIdentitiesList.as_view(), name=views.UserIdentitiesList.view_name), + url(r'^(?P\w+)/settings/identities/(?P\w+)/$', views.UserIdentitiesDetail.as_view(), name=views.UserIdentitiesDetail.view_name), ] diff --git a/api/users/views.py b/api/users/views.py index 2be87d5a9f8..bc3109ed240 100644 --- a/api/users/views.py +++ b/api/users/views.py @@ -21,6 +21,7 @@ ReadOnlyOrCurrentUserRelationship) from api.users.serializers import (UserAddonSettingsSerializer, UserDetailSerializer, + UserIdentitiesSerializer, UserInstitutionsRelationshipSerializer, UserSerializer, UserQuickFilesSerializer, @@ -436,3 +437,58 @@ def perform_destroy(self, instance): if val['id'] in current_institutions: user.remove_institution(val['id']) user.save() + + +class UserIdentitiesList(JSONAPIBaseView, generics.RetrieveAPIView, UserMixin): + """ + REMEMBER TO WRITE DEV DOCS!!! + The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/...). + """ + permission_classes = ( + drf_permissions.IsAuthenticatedOrReadOnly, + CurrentUser, + ) + + serializer_class = UserIdentitiesSerializer + + view_category = 'users' + view_name = 'user-identities-list' + + def get_object(self): + user = self.get_user() + + return {'data': user.external_identity, 'self': user, 'type': 'external-identities'} + + +class UserIdentitiesDetail(JSONAPIBaseView, generics.RetrieveDestroyAPIView, UserMixin): + """ + REMEMBER TO WRITE DEV DOCS!!! + The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/...). + """ + permission_classes = ( + drf_permissions.IsAuthenticatedOrReadOnly, + CurrentUser, + ) + + serializer_class = UserIdentitiesSerializer + + view_category = 'users' + view_name = 'user-identities-detail' + + def get_object(self): + user = self.get_user() + identity_id = self.kwargs['identity_id'] + try: + return {'data': {identity_id: user.external_identity[identity_id]}, 'self': user} + except KeyError: + raise NotFound('Requested external identity could not be found.') + + def perform_destroy(self, instance): + user = self.get_user() + identity_id = self.kwargs['identity_id'] + try: + user.external_identity.pop(identity_id) + except KeyError: + raise NotFound('Requested external identity could not be found.') + + user.save() diff --git a/api_tests/users/views/test_user_external_identities.py b/api_tests/users/views/test_user_external_identities.py new file mode 100644 index 00000000000..882bb2bf940 --- /dev/null +++ b/api_tests/users/views/test_user_external_identities.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +import pytest + +from osf_tests.factories import AuthUserFactory +from api.base.settings.defaults import API_BASE + + +@pytest.fixture() +def user(): + user = AuthUserFactory() + user.external_identity = { + 'ORCID': { + '0000-0001-9143-4653': 'VERIFIED' + }, + 'LOTUS': { + '0000-0001-9143-4652': 'LINK' + } + } + user.save() + return user + + +@pytest.mark.django_db +class TestUserIdentitiesList: + + @pytest.fixture() + def url(self, user): + return '/{}users/{}/settings/identities/'.format(API_BASE, user._id) + + def test_authorized_gets_200(self, app, user, url): + res = app.get(url, auth=user.auth) + assert res.status_code == 200 + assert res.content_type == 'application/vnd.api+json' + assert res.json['data'] == { + 'ORCID': { + '0000-0001-9143-4653': 'VERIFIED' + }, + 'LOTUS': { + '0000-0001-9143-4652': 'LINK' + } + } + + def test_anonymous_gets_401(self, app, url): + res = app.get(url, expect_errors=True) + assert res.status_code == 401 + assert res.content_type == 'application/vnd.api+json' + + +@pytest.mark.django_db +class TestUserIdentitiesDetail: + + @pytest.fixture() + def url(self, user): + return '/{}users/{}/settings/identities/ORCID/'.format(API_BASE, user._id) + + def test_authorized_gets_200(self, app, user, url): + res = app.get(url, auth=user.auth) + assert res.status_code == 200 + assert res.content_type == 'application/vnd.api+json' + assert res.json['data'] == {'ORCID': {'0000-0001-9143-4653': 'VERIFIED'}} + + def test_anonymous_gets_401(self, app, url): + res = app.get(url, expect_errors=True) + assert res.status_code == 401 + assert res.content_type == 'application/vnd.api+json' + + def test_authorized_delete_204(self, app, user, url): + res = app.delete(url, auth=user.auth) + assert res.status_code == 204 + + user.refresh_from_db() + assert user.external_identity == { + 'LOTUS': { + '0000-0001-9143-4652': 'LINK' + } + } From 5f78e6779d9c57b05685f0c8a15e5173e48583f7 Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Thu, 12 Jul 2018 12:09:57 -0400 Subject: [PATCH 2/9] add proper permissions --- api/users/views.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api/users/views.py b/api/users/views.py index bc3109ed240..dae1274b36a 100644 --- a/api/users/views.py +++ b/api/users/views.py @@ -445,12 +445,16 @@ class UserIdentitiesList(JSONAPIBaseView, generics.RetrieveAPIView, UserMixin): The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/...). """ permission_classes = ( + base_permissions.TokenHasScope, drf_permissions.IsAuthenticatedOrReadOnly, CurrentUser, ) serializer_class = UserIdentitiesSerializer + required_read_scopes = [CoreScopes.USERS_READ] + required_write_scopes = [CoreScopes.NULL] + view_category = 'users' view_name = 'user-identities-list' @@ -466,10 +470,14 @@ class UserIdentitiesDetail(JSONAPIBaseView, generics.RetrieveDestroyAPIView, Use The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/...). """ permission_classes = ( + base_permissions.TokenHasScope, drf_permissions.IsAuthenticatedOrReadOnly, CurrentUser, ) + required_read_scopes = [CoreScopes.USERS_READ] + required_write_scopes = [CoreScopes.USERS_WRITE] + serializer_class = UserIdentitiesSerializer view_category = 'users' From d9155f5a770728b58549058a086c753ff658b24a Mon Sep 17 00:00:00 2001 From: Dawn Pattison Date: Mon, 16 Jul 2018 13:13:38 -0500 Subject: [PATCH 3/9] Make UserIdentities responses adhere to JSONAPI. --- api/users/serializers.py | 28 +++++++++++++++++++--------- api/users/views.py | 15 +++++++++++---- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/api/users/serializers.py b/api/users/serializers.py index e32e2c36d3f..4b4c9d85450 100644 --- a/api/users/serializers.py +++ b/api/users/serializers.py @@ -254,15 +254,25 @@ class Meta: type_ = 'institutions' -class UserIdentitiesSerializer(BaseAPISerializer): - data = ser.DictField() - links = LinksField({'self': 'get_self_url'}) +class UserIdentitiesSerializer(JSONAPISerializer): + id = IDField(source='_id', read_only=True) + type = TypeField() + external_id = ser.CharField(read_only=True) + status = ser.CharField(read_only=True) - def get_self_url(self, obj): - return absolute_reverse('users:user-identities-list', kwargs={ - 'version': self.context['request'].parser_context['kwargs']['version'], - 'user_id': obj['self']._id - }) + links = LinksField({ + 'self': 'get_absolute_url', + }) + + def get_absolute_url(self, obj): + return absolute_reverse( + 'users:user-identities-detail', + kwargs={ + 'user_id': self.context['request'].parser_context['kwargs']['user_id'], + 'version': self.context['request'].parser_context['kwargs']['version'], + 'identity_id': obj['_id'] + } + ) class Meta: - type_ = 'external_identities' + type_ = 'external-identities' diff --git a/api/users/views.py b/api/users/views.py index dae1274b36a..20b0dd964e7 100644 --- a/api/users/views.py +++ b/api/users/views.py @@ -439,7 +439,7 @@ def perform_destroy(self, instance): user.save() -class UserIdentitiesList(JSONAPIBaseView, generics.RetrieveAPIView, UserMixin): +class UserIdentitiesList(JSONAPIBaseView, generics.ListAPIView, UserMixin): """ REMEMBER TO WRITE DEV DOCS!!! The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/...). @@ -458,10 +458,14 @@ class UserIdentitiesList(JSONAPIBaseView, generics.RetrieveAPIView, UserMixin): view_category = 'users' view_name = 'user-identities-list' - def get_object(self): + # overrides ListAPIView + def get_queryset(self): user = self.get_user() + identities = [] + for key, value in user.external_identity.iteritems(): + identities.append({'_id': key, 'external_id': value.keys()[0], 'status': value.values()[0]}) - return {'data': user.external_identity, 'self': user, 'type': 'external-identities'} + return identities class UserIdentitiesDetail(JSONAPIBaseView, generics.RetrieveDestroyAPIView, UserMixin): @@ -487,10 +491,13 @@ def get_object(self): user = self.get_user() identity_id = self.kwargs['identity_id'] try: - return {'data': {identity_id: user.external_identity[identity_id]}, 'self': user} + identity = user.external_identity[identity_id] except KeyError: raise NotFound('Requested external identity could not be found.') + return {'_id': identity_id, 'external_id': identity.keys()[0], 'status': identity.values()[0]} + + def perform_destroy(self, instance): user = self.get_user() identity_id = self.kwargs['identity_id'] From 70441aa82b951a86c05704f779ccce1142963151 Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Mon, 16 Jul 2018 15:57:27 -0400 Subject: [PATCH 4/9] add and improve tests --- api/users/views.py | 1 - .../views/test_user_external_identities.py | 45 ++++++++++++++----- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/api/users/views.py b/api/users/views.py index 20b0dd964e7..806c187d1ac 100644 --- a/api/users/views.py +++ b/api/users/views.py @@ -497,7 +497,6 @@ def get_object(self): return {'_id': identity_id, 'external_id': identity.keys()[0], 'status': identity.values()[0]} - def perform_destroy(self, instance): user = self.get_user() identity_id = self.kwargs['identity_id'] diff --git a/api_tests/users/views/test_user_external_identities.py b/api_tests/users/views/test_user_external_identities.py index 882bb2bf940..40b01ef9b2e 100644 --- a/api_tests/users/views/test_user_external_identities.py +++ b/api_tests/users/views/test_user_external_identities.py @@ -20,6 +20,11 @@ def user(): return user +@pytest.fixture() +def unauthorized_user(): + return AuthUserFactory() + + @pytest.mark.django_db class TestUserIdentitiesList: @@ -29,22 +34,27 @@ def url(self, user): def test_authorized_gets_200(self, app, user, url): res = app.get(url, auth=user.auth) + print res.json['data'] assert res.status_code == 200 assert res.content_type == 'application/vnd.api+json' - assert res.json['data'] == { - 'ORCID': { - '0000-0001-9143-4653': 'VERIFIED' - }, - 'LOTUS': { - '0000-0001-9143-4652': 'LINK' - } - } + assert res.json['data'][0]['attributes'] == {u'status': u'LINK', u'external_id': u'0000-0001-9143-4652'} + assert res.json['data'][0]['type'] == 'external-identities' + assert res.json['data'][0]['id'] == 'LOTUS' + + assert res.json['data'][1]['attributes'] == {u'status': u'VERIFIED', u'external_id': u'0000-0001-9143-4653'} + assert res.json['data'][1]['type'] == 'external-identities' + assert res.json['data'][1]['id'] == 'ORCID' def test_anonymous_gets_401(self, app, url): res = app.get(url, expect_errors=True) assert res.status_code == 401 assert res.content_type == 'application/vnd.api+json' + def test_unauthorized_gets_403(self, app, url, unauthorized_user): + res = app.get(url, auth=unauthorized_user.auth, expect_errors=True) + assert res.status_code == 403 + assert res.content_type == 'application/vnd.api+json' + @pytest.mark.django_db class TestUserIdentitiesDetail: @@ -57,14 +67,24 @@ def test_authorized_gets_200(self, app, user, url): res = app.get(url, auth=user.auth) assert res.status_code == 200 assert res.content_type == 'application/vnd.api+json' - assert res.json['data'] == {'ORCID': {'0000-0001-9143-4653': 'VERIFIED'}} + assert res.json['data'] == { + u'attributes': { + u'external_id': u'0000-0001-9143-4653', + u'status': u'VERIFIED' + }, + u'id': u'ORCID', + u'links': { + u'self': u'http://localhost:8000/v2/users/{}/settings/identities/ORCID/'.format(user._id) + }, + u'type': u'external-identities' + } def test_anonymous_gets_401(self, app, url): res = app.get(url, expect_errors=True) assert res.status_code == 401 assert res.content_type == 'application/vnd.api+json' - def test_authorized_delete_204(self, app, user, url): + def test_no_creds_delete_204(self, app, user, url): res = app.delete(url, auth=user.auth) assert res.status_code == 204 @@ -74,3 +94,8 @@ def test_authorized_delete_204(self, app, user, url): '0000-0001-9143-4652': 'LINK' } } + + def test_unauthorized_delete_403(self, app, url, unauthorized_user): + res = app.get(url, auth=unauthorized_user.auth, expect_errors=True) + assert res.status_code == 403 + assert res.content_type == 'application/vnd.api+json' From fc8a88301f07a5acf844b2675d0ed7c45cd3cc2a Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Tue, 17 Jul 2018 16:24:47 -0400 Subject: [PATCH 5/9] clean up test and add more --- .../views/test_user_external_identities.py | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/api_tests/users/views/test_user_external_identities.py b/api_tests/users/views/test_user_external_identities.py index 40b01ef9b2e..cc95e1f528d 100644 --- a/api_tests/users/views/test_user_external_identities.py +++ b/api_tests/users/views/test_user_external_identities.py @@ -34,14 +34,15 @@ def url(self, user): def test_authorized_gets_200(self, app, user, url): res = app.get(url, auth=user.auth) - print res.json['data'] assert res.status_code == 200 assert res.content_type == 'application/vnd.api+json' - assert res.json['data'][0]['attributes'] == {u'status': u'LINK', u'external_id': u'0000-0001-9143-4652'} + assert res.json['data'][0]['attributes']['status'] == 'LINK' + assert res.json['data'][0]['attributes']['external_id'] == '0000-0001-9143-4652' assert res.json['data'][0]['type'] == 'external-identities' assert res.json['data'][0]['id'] == 'LOTUS' - assert res.json['data'][1]['attributes'] == {u'status': u'VERIFIED', u'external_id': u'0000-0001-9143-4653'} + assert res.json['data'][1]['attributes']['status'] == 'VERIFIED' + assert res.json['data'][1]['attributes']['external_id'] == '0000-0001-9143-4653' assert res.json['data'][1]['type'] == 'external-identities' assert res.json['data'][1]['id'] == 'ORCID' @@ -63,28 +64,20 @@ class TestUserIdentitiesDetail: def url(self, user): return '/{}users/{}/settings/identities/ORCID/'.format(API_BASE, user._id) + @pytest.fixture() + def bad_url(self, user): + return '/{}users/{}/settings/identities/404-CID/'.format(API_BASE, user._id) + def test_authorized_gets_200(self, app, user, url): res = app.get(url, auth=user.auth) assert res.status_code == 200 assert res.content_type == 'application/vnd.api+json' - assert res.json['data'] == { - u'attributes': { - u'external_id': u'0000-0001-9143-4653', - u'status': u'VERIFIED' - }, - u'id': u'ORCID', - u'links': { - u'self': u'http://localhost:8000/v2/users/{}/settings/identities/ORCID/'.format(user._id) - }, - u'type': u'external-identities' - } + assert res.json['data']['attributes']['status'] == 'VERIFIED' + assert res.json['data']['attributes']['external_id'] == '0000-0001-9143-4653' + assert res.json['data']['type'] == 'external-identities' + assert res.json['data']['links']['self'] == 'http://localhost:8000/v2/users/{}/settings/identities/ORCID/'.format(user._id) - def test_anonymous_gets_401(self, app, url): - res = app.get(url, expect_errors=True) - assert res.status_code == 401 - assert res.content_type == 'application/vnd.api+json' - - def test_no_creds_delete_204(self, app, user, url): + def test_delete_204(self, app, user, url): res = app.delete(url, auth=user.auth) assert res.status_code == 204 @@ -95,7 +88,22 @@ def test_no_creds_delete_204(self, app, user, url): } } + def test_anonymous_gets_401(self, app, url): + res = app.get(url, expect_errors=True) + assert res.status_code == 401 + assert res.content_type == 'application/vnd.api+json' + def test_unauthorized_delete_403(self, app, url, unauthorized_user): res = app.get(url, auth=unauthorized_user.auth, expect_errors=True) assert res.status_code == 403 assert res.content_type == 'application/vnd.api+json' + + def test_bad_request_gets_404(self, app, bad_url): + res = app.get(bad_url, expect_errors=True) + assert res.status_code == 404 + assert res.content_type == 'application/vnd.api+json' + + def test_patch_405(self, app, user, url): + res = app.patch(url, auth=user.auth, expect_errors=True) + assert res.status_code == 405 + assert res.content_type == 'application/vnd.api+json' From f562d0870d2b5c646a6544e69d90a61b81def02b Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Tue, 24 Jul 2018 11:12:07 -0400 Subject: [PATCH 6/9] update docstrings for new dev docs --- api/users/views.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/api/users/views.py b/api/users/views.py index 806c187d1ac..cca76a91389 100644 --- a/api/users/views.py +++ b/api/users/views.py @@ -441,8 +441,7 @@ def perform_destroy(self, instance): class UserIdentitiesList(JSONAPIBaseView, generics.ListAPIView, UserMixin): """ - REMEMBER TO WRITE DEV DOCS!!! - The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/...). + The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/external_identities_list). """ permission_classes = ( base_permissions.TokenHasScope, @@ -470,8 +469,7 @@ def get_queryset(self): class UserIdentitiesDetail(JSONAPIBaseView, generics.RetrieveDestroyAPIView, UserMixin): """ - REMEMBER TO WRITE DEV DOCS!!! - The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/...). + The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/external_identities_detail). """ permission_classes = ( base_permissions.TokenHasScope, From d389848d7a434dfd4a4262c2d0e6b633083eb8b5 Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Tue, 24 Jul 2018 11:17:06 -0400 Subject: [PATCH 7/9] fix external identities test link --- api_tests/users/views/test_user_external_identities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api_tests/users/views/test_user_external_identities.py b/api_tests/users/views/test_user_external_identities.py index cc95e1f528d..034ead0705c 100644 --- a/api_tests/users/views/test_user_external_identities.py +++ b/api_tests/users/views/test_user_external_identities.py @@ -75,7 +75,7 @@ def test_authorized_gets_200(self, app, user, url): assert res.json['data']['attributes']['status'] == 'VERIFIED' assert res.json['data']['attributes']['external_id'] == '0000-0001-9143-4653' assert res.json['data']['type'] == 'external-identities' - assert res.json['data']['links']['self'] == 'http://localhost:8000/v2/users/{}/settings/identities/ORCID/'.format(user._id) + assert '/v2/users/{}/settings/identities/ORCID/'.format(user._id) in res.json['data']['links']['self'] def test_delete_204(self, app, user, url): res = app.delete(url, auth=user.auth) From b85ce4a0279365e370a8e467b2d51901da953dca Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Wed, 25 Jul 2018 12:25:41 -0400 Subject: [PATCH 8/9] change scope to user_settings_read/user_settings_write Signed-off-by: John Tordoff --- api/users/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/users/views.py b/api/users/views.py index 0c118241cd1..7f00e4e71df 100644 --- a/api/users/views.py +++ b/api/users/views.py @@ -458,8 +458,8 @@ class UserIdentitiesList(JSONAPIBaseView, generics.ListAPIView, UserMixin): serializer_class = UserIdentitiesSerializer - required_read_scopes = [CoreScopes.USERS_READ] - required_write_scopes = [CoreScopes.NULL] + required_read_scopes = [CoreScopes.USER_SETTINGS_READ] + required_write_scopes = [CoreScopes.USER_SETTINGS_WRITE] view_category = 'users' view_name = 'user-identities-list' @@ -484,8 +484,8 @@ class UserIdentitiesDetail(JSONAPIBaseView, generics.RetrieveDestroyAPIView, Use CurrentUser, ) - required_read_scopes = [CoreScopes.USERS_READ] - required_write_scopes = [CoreScopes.USERS_WRITE] + required_read_scopes = [CoreScopes.USER_SETTINGS_READ] + required_write_scopes = [CoreScopes.USER_SETTINGS_WRITE] serializer_class = UserIdentitiesSerializer From c465fbf02eca56c1e118c61963406b56480911a2 Mon Sep 17 00:00:00 2001 From: John Tordoff Date: Wed, 25 Jul 2018 13:58:08 -0400 Subject: [PATCH 9/9] change write scope to null user external identities list Signed-off-by: John Tordoff --- api/users/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/users/views.py b/api/users/views.py index 7f00e4e71df..6985e2510fd 100644 --- a/api/users/views.py +++ b/api/users/views.py @@ -459,7 +459,7 @@ class UserIdentitiesList(JSONAPIBaseView, generics.ListAPIView, UserMixin): serializer_class = UserIdentitiesSerializer required_read_scopes = [CoreScopes.USER_SETTINGS_READ] - required_write_scopes = [CoreScopes.USER_SETTINGS_WRITE] + required_write_scopes = [CoreScopes.NULL] view_category = 'users' view_name = 'user-identities-list'