From dfafdda9b3c1206fb0f018fe24160334c2b77af2 Mon Sep 17 00:00:00 2001 From: Tushar912 Date: Sat, 2 Jan 2021 19:35:29 +0530 Subject: [PATCH] api: Add REST API endpoint for looking up a user by email address. Add new rest api endpoint GET users/{email} for looking up a user by email, which is useful especially for corporate API applications that might already have a user's email address. Fixes #14302. --- templates/zerver/api/changelog.md | 4 + templates/zerver/api/get-user-by-email.md | 38 ++++++++ templates/zerver/api/get-users.md | 2 - .../zerver/help/include/rest-endpoints.md | 1 + version.py | 2 +- zerver/openapi/python_examples.py | 15 +++ zerver/openapi/zulip.yaml | 93 ++++++++++++++++++- zerver/tests/test_openapi.py | 1 + zerver/tests/test_users.py | 29 ++++++ zerver/views/users.py | 23 ++++- zproject/urls.py | 2 + 11 files changed, 205 insertions(+), 5 deletions(-) create mode 100644 templates/zerver/api/get-user-by-email.md diff --git a/templates/zerver/api/changelog.md b/templates/zerver/api/changelog.md index f7076351b1..2f4f0fbd49 100644 --- a/templates/zerver/api/changelog.md +++ b/templates/zerver/api/changelog.md @@ -10,6 +10,10 @@ below features are supported. ## Changes in Zulip 4.0 +**Feature level 39** + +* Added new [GET /users/{email}](/api/get-user-by-email) endpoint. + **Feature level 38** * [`POST /register`](/api/register-queue): Increased diff --git a/templates/zerver/api/get-user-by-email.md b/templates/zerver/api/get-user-by-email.md new file mode 100644 index 0000000000..894819a785 --- /dev/null +++ b/templates/zerver/api/get-user-by-email.md @@ -0,0 +1,38 @@ +# Get a user by email + +{generate_api_description(/users/{email}:get)} + +## Usage examples + +{start_tabs} +{tab|python} + +{generate_code_example(python)|/users/{email}:get|example} + +{tab|curl} + +{generate_code_example(curl, include=[""])|/users/{email}:get|example} + +You may pass the `client_gravatar` or `include_custom_profile_fields` query parameter as follows: + +{generate_code_example(curl)|/users/{email}:get|example} + +{end_tabs} + +## Parameters + +**Note**: The following parameters are all URL query parameters. + +{generate_api_arguments_table|zulip.yaml|/users/{email}:get} + +## Response + +#### Return values + +{generate_return_values_table|zulip.yaml}|/users/{email}:get} + +#### Example response + +A typical successful JSON response may look like: + +{generate_code_example|/users/{email}:get|fixture(200)} diff --git a/templates/zerver/api/get-users.md b/templates/zerver/api/get-users.md index 7d393cf173..af6b8fae2c 100644 --- a/templates/zerver/api/get-users.md +++ b/templates/zerver/api/get-users.md @@ -2,8 +2,6 @@ {generate_api_description(/users:get)} -You can also [fetch details on a single user](/api/get-user). - ## Usage examples {start_tabs} diff --git a/templates/zerver/help/include/rest-endpoints.md b/templates/zerver/help/include/rest-endpoints.md index 87b069f260..955ac8f342 100644 --- a/templates/zerver/help/include/rest-endpoints.md +++ b/templates/zerver/help/include/rest-endpoints.md @@ -35,6 +35,7 @@ * [Get all users](/api/get-users) * [Get own user](/api/get-own-user) * [Get a user](/api/get-user) +* [Get a user by email](/api/get-user-by-email) * [Update a user](/api/update-user) * [Create a user](/api/create-user) * [Deactivate a user](/api/deactivate-user) diff --git a/version.py b/version.py index deeb5c1148..1adfd634a5 100644 --- a/version.py +++ b/version.py @@ -30,7 +30,7 @@ DESKTOP_WARNING_VERSION = "5.2.0" # # Changes should be accompanied by documentation explaining what the # new level means in templates/zerver/api/changelog.md. -API_FEATURE_LEVEL = 38 +API_FEATURE_LEVEL = 39 # Bump the minor PROVISION_VERSION to indicate that folks should provision # only when going from an old version of the code to a newer version. Bump diff --git a/zerver/openapi/python_examples.py b/zerver/openapi/python_examples.py index 44f2ea1c63..90e3824bd5 100644 --- a/zerver/openapi/python_examples.py +++ b/zerver/openapi/python_examples.py @@ -222,6 +222,20 @@ def get_members(client: Client) -> None: assert member.get("profile_data", None) is not None +@openapi_test_function("/users/{email}:get") +def get_user_by_email(client: Client) -> None: + + # {code_example|start} + # Fetch details on a user given a user ID + email = "iago@zulip.com" + result = client.call_endpoint( + url=f"/users/{email}", + method="GET", + ) + # {code_example|end} + validate_against_openapi_schema(result, "/users/{email}", "get", "200") + + @openapi_test_function("/users/{user_id}:get") def get_single_user(client: Client) -> None: @@ -1287,6 +1301,7 @@ def test_users(client: Client) -> None: deactivate_user(client) reactivate_user(client) update_user(client) + get_user_by_email(client) get_subscription_status(client) get_profile(client) update_notification_settings(client) diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index e0fd0bbc3e..83a613671a 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -4027,6 +4027,8 @@ paths: includes values of [custom profile field](/help/add-custom-profile-fields). `GET {{ api_url }}/v1/users` + + You can also [fetch details on a single user](/api/get-user). parameters: - $ref: "#/components/parameters/ClientGravatar" - $ref: "#/components/parameters/IncludeCustomProfileFields" @@ -5450,6 +5452,94 @@ paths: "result": "success", "msg": "", } + /users/{email}: + get: + operationId: get_user_by_email + tags: ["users"] + description: | + Fetch details for a single user in the organization given a Zulip display + email address. + + `GET {{ api_url }}/v1/users/{email}` + + Note that this endpoint uses Zulip display emails addresses + for organizations that have configured limited [email address + visibility](/help/restrict-visibility-of-email-addresses). + + You can also fetch details on [all users in the organization](/api/get-users) or + [by user ID](/api/get-user). Fetching by user ID is generally recommended + when possible, as users can + [change their email address](/help/change-your-email-address). + + *This endpoint is new in Zulip Server 4.0 (feature level 39).* + parameters: + - name: email + in: path + description: | + The email address of the user whose details you want to fetch. + schema: + type: string + example: iago@zulip.com + required: true + - $ref: "#/components/parameters/ClientGravatar" + - $ref: "#/components/parameters/IncludeCustomProfileFields" + responses: + "200": + description: Success. + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/JsonSuccessBase" + - additionalProperties: false + properties: + result: {} + msg: {} + user: + $ref: "#/components/schemas/User" + example: + { + "msg": "", + "result": "success", + "user": + { + "date_joined": "2019-10-20T07:50:53.729659+00:00", + "full_name": "King Hamlet", + "is_guest": false, + "profile_data": + { + "4": {"value": "vim"}, + "2": + { + "value": "I am:\n* The prince of Denmark\n* Nephew to the usurping Claudius", + "rendered_value": "

I am:

\n", + }, + "5": {"value": "1900-01-01"}, + "7": {"value": "[11]"}, + "6": {"value": "https://blog.zulig.org"}, + "1": + { + "value": "+0-11-23-456-7890", + "rendered_value": "

+0-11-23-456-7890

", + }, + "8": {"value": "zulipbot"}, + "3": + { + "rendered_value": "

Dark chocolate

", + "value": "Dark chocolate", + }, + }, + "user_id": 10, + "is_bot": false, + "bot_type": null, + "timezone": "", + "is_admin": false, + "is_owner": false, + "avatar_url": "https://secure.gravatar.com/avatar/6d8cad0fd00256e7b40691d27ddfd466?d=identicon&version=1", + "is_active": true, + "email": "hamlet@zulip.com", + }, + } /users/{user_id}: get: operationId: get_user @@ -5459,7 +5549,8 @@ paths: `GET {{ api_url }}/v1/users/{user_id}` - You can also fetch details on [all users in the organization](/api/get-users). + You can also fetch details on [all users in the organization](/api/get-users) + or [by email](/api/get-user-by-email). *This endpoint is new in Zulip Server 3.0 (feature level 1).* parameters: diff --git a/zerver/tests/test_openapi.py b/zerver/tests/test_openapi.py index 770d1c9ceb..ecc8e73862 100644 --- a/zerver/tests/test_openapi.py +++ b/zerver/tests/test_openapi.py @@ -1064,6 +1064,7 @@ class OpenAPIRegexTest(ZulipTestCase): == "/users/{user_id}/subscriptions/{stream_id}" ) assert find_openapi_endpoint("/users/iago@zulip.com/presence") == "/users/{email}/presence" + assert find_openapi_endpoint("/users/iago@zulip.com") == "/users/{email}" assert find_openapi_endpoint("/messages/23") == "/messages/{message_id}" assert find_openapi_endpoint("/realm/emoji/realm_emoji_1") == "/realm/emoji/{emoji_name}" diff --git a/zerver/tests/test_users.py b/zerver/tests/test_users.py index 25e9dd54c6..589817a6af 100644 --- a/zerver/tests/test_users.py +++ b/zerver/tests/test_users.py @@ -1815,6 +1815,35 @@ class GetProfileTest(ZulipTestCase): self.assertEqual(result["user"]["email"], bot.email) self.assertTrue(result["user"]["is_bot"]) + def test_get_user_by_email(self) -> None: + user = self.example_user("hamlet") + self.login("hamlet") + result = orjson.loads(self.client_get(f"/json/users/{user.email}").content) + + self.assertEqual(result["user"]["email"], user.email) + + self.assertEqual(result["user"]["full_name"], user.full_name) + self.assertIn("user_id", result["user"]) + self.assertNotIn("profile_data", result["user"]) + self.assertFalse(result["user"]["is_bot"]) + self.assertFalse(result["user"]["is_admin"]) + self.assertFalse(result["user"]["is_owner"]) + + result = orjson.loads( + self.client_get( + f"/json/users/{user.email}", {"include_custom_profile_fields": "true"} + ).content + ) + self.assertIn("profile_data", result["user"]) + + result = self.client_get("/json/users/invalid") + self.assert_json_error(result, "No such user") + + bot = self.example_user("default_bot") + result = orjson.loads(self.client_get(f"/json/users/{bot.email}").content) + self.assertEqual(result["user"]["email"], bot.email) + self.assertTrue(result["user"]["is_bot"]) + def test_get_all_profiles_avatar_urls(self) -> None: hamlet = self.example_user("hamlet") result = self.api_get(hamlet, "/api/v1/users") diff --git a/zerver/views/users.py b/zerver/views/users.py index d4746f3097..8a468dc34f 100644 --- a/zerver/views/users.py +++ b/zerver/views/users.py @@ -31,7 +31,7 @@ from zerver.lib.bot_config import set_bot_config from zerver.lib.email_validation import email_allowed_for_realm from zerver.lib.exceptions import CannotDeactivateLastUserError, OrganizationOwnerRequired from zerver.lib.integrations import EMBEDDED_BOTS -from zerver.lib.request import REQ, has_request_variables +from zerver.lib.request import REQ, JsonableError, has_request_variables from zerver.lib.response import json_error, json_success from zerver.lib.streams import access_stream_by_id, access_stream_by_name, subscribed_to_stream from zerver.lib.types import Validator @@ -75,6 +75,7 @@ from zerver.models import ( Service, Stream, UserProfile, + get_user, get_user_by_delivery_email, get_user_by_id_in_realm_including_cross_realm, get_user_including_cross_realm, @@ -637,3 +638,23 @@ def get_subscription_backend( subscription_status = {"is_subscribed": subscribed_to_stream(target_user, stream_id)} return json_success(subscription_status) + + +@has_request_variables +def get_user_by_email( + request: HttpRequest, + user_profile: UserProfile, + email: str, + include_custom_profile_fields: bool = REQ(validator=check_bool, default=False), + client_gravatar: bool = REQ(validator=check_bool, default=False), +) -> HttpResponse: + realm = user_profile.realm + + target_user = None + if email is not None: + try: + target_user = get_user(email, realm) + except UserProfile.DoesNotExist: + raise JsonableError(_("No such user")) + + return get_members_backend(request, user_profile, user_id=target_user.id) diff --git a/zproject/urls.py b/zproject/urls.py index 9e79ee76e2..a586f6b4c8 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -199,6 +199,7 @@ from zerver.views.users import ( get_members_backend, get_profile_backend, get_subscription_backend, + get_user_by_email, patch_bot_backend, reactivate_user_backend, regenerate_bot_api_key, @@ -292,6 +293,7 @@ v1_api_and_json_patterns = [ DELETE=deactivate_user_backend, ), rest_path("users//subscriptions/", GET=get_subscription_backend), + rest_path("users/", GET=get_user_by_email), rest_path("bots", GET=get_bots_backend, POST=add_bot_backend), rest_path("bots//api_key/regenerate", POST=regenerate_bot_api_key), rest_path("bots/", PATCH=patch_bot_backend, DELETE=deactivate_bot_backend),