mirror of https://github.com/zulip/zulip.git
users: Add API endpoint to update_user_backend by real email.
The old endpoint for updating a user worked only via user id. Now we add a different entry to this functionality, fetching the user by .delivery_email. update_user_backend becomes the main function handling all the logic, invoked by the two endpoints.
This commit is contained in:
parent
389b851f81
commit
77e7a2d30f
|
@ -66,6 +66,7 @@
|
||||||
* [Get all users](/api/get-users)
|
* [Get all users](/api/get-users)
|
||||||
* [Create a user](/api/create-user)
|
* [Create a user](/api/create-user)
|
||||||
* [Update a user](/api/update-user)
|
* [Update a user](/api/update-user)
|
||||||
|
* [Update a user by email](/api/update-user-by-email)
|
||||||
* [Deactivate a user](/api/deactivate-user)
|
* [Deactivate a user](/api/deactivate-user)
|
||||||
* [Deactivate own user](/api/deactivate-own-user)
|
* [Deactivate own user](/api/deactivate-own-user)
|
||||||
* [Reactivate a user](/api/reactivate-user)
|
* [Reactivate a user](/api/reactivate-user)
|
||||||
|
|
|
@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
|
||||||
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
||||||
# entries in the endpoint's documentation in `zulip.yaml`.
|
# entries in the endpoint's documentation in `zulip.yaml`.
|
||||||
|
|
||||||
API_FEATURE_LEVEL = 313 # Last bumped for adding `new_email` to /users/{user_id}
|
API_FEATURE_LEVEL = 313 # Last bumped for adding `new_email` to /users/{user_id} and the new PATCH /users/{email} endpoint
|
||||||
|
|
||||||
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
# 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
|
# only when going from an old version of the code to a newer version. Bump
|
||||||
|
|
|
@ -23,7 +23,7 @@ from zerver.lib.upload import upload_message_attachment
|
||||||
from zerver.lib.users import get_api_key
|
from zerver.lib.users import get_api_key
|
||||||
from zerver.models import Client, Message, NamedUserGroup, UserPresence
|
from zerver.models import Client, Message, NamedUserGroup, UserPresence
|
||||||
from zerver.models.realms import get_realm
|
from zerver.models.realms import get_realm
|
||||||
from zerver.models.users import get_user
|
from zerver.models.users import UserProfile, get_user
|
||||||
from zerver.openapi.openapi import Parameter
|
from zerver.openapi.openapi import Parameter
|
||||||
|
|
||||||
GENERATOR_FUNCTIONS: dict[str, Callable[[], dict[str, object]]] = {}
|
GENERATOR_FUNCTIONS: dict[str, Callable[[], dict[str, object]]] = {}
|
||||||
|
@ -252,6 +252,19 @@ def create_user() -> dict[str, object]:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@openapi_param_value_generator(["/users/{email]:patch", "/users/{user_id}:patch"])
|
||||||
|
def new_email_value() -> dict[str, object]:
|
||||||
|
count = 0
|
||||||
|
exists = True
|
||||||
|
while exists:
|
||||||
|
email = f"new{count}@zulip.com"
|
||||||
|
exists = UserProfile.objects.filter(delivery_email=email).exists()
|
||||||
|
count += 1
|
||||||
|
return {
|
||||||
|
"new_email": email,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@openapi_param_value_generator(["/user_groups/create:post"])
|
@openapi_param_value_generator(["/user_groups/create:post"])
|
||||||
def create_user_group_data() -> dict[str, object]:
|
def create_user_group_data() -> dict[str, object]:
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -12634,6 +12634,52 @@ paths:
|
||||||
"delivery_email": null,
|
"delivery_email": null,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
patch:
|
||||||
|
operationId: update-user-by-email
|
||||||
|
summary: Update a user by email
|
||||||
|
tags: ["users"]
|
||||||
|
x-requires-administrator: true
|
||||||
|
description: |
|
||||||
|
Administrative endpoint to update the details of another user in the organization by their email address.
|
||||||
|
Works the same way as [`PATCH /users/{user_id}`](/api/update-user) but fetching the target user by their
|
||||||
|
real email address.
|
||||||
|
|
||||||
|
The requester needs to have permission to view the target user's real email address, subject to the
|
||||||
|
user's email address visibility setting. Otherwise, the dummy address of the format
|
||||||
|
`user{id}@{realm.host}` needs be used. This follows the same rules as `GET /users/{email}`.
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 10.0 (feature level 313).
|
||||||
|
parameters:
|
||||||
|
- name: email
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: |
|
||||||
|
The email address of the user, specified following the same rules as
|
||||||
|
[`GET /users/{email}`](/api/get-user-by-email).
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: hamlet@zulip.com
|
||||||
|
requestBody:
|
||||||
|
$ref: "#/components/requestBodies/UpdateUser"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/SimpleSuccess"
|
||||||
|
|
||||||
|
"400":
|
||||||
|
description: Bad request.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/CodedError"
|
||||||
|
- example:
|
||||||
|
{
|
||||||
|
"result": "error",
|
||||||
|
"msg": "Guests cannot be organization administrators",
|
||||||
|
"code": "BAD_REQUEST",
|
||||||
|
}
|
||||||
|
description: |
|
||||||
|
A typical unsuccessful JSON response:
|
||||||
/users/{user_id}:
|
/users/{user_id}:
|
||||||
get:
|
get:
|
||||||
operationId: get-user
|
operationId: get-user
|
||||||
|
@ -12738,62 +12784,7 @@ paths:
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/UserId"
|
- $ref: "#/components/parameters/UserId"
|
||||||
requestBody:
|
requestBody:
|
||||||
required: false
|
$ref: "#/components/requestBodies/UpdateUser"
|
||||||
content:
|
|
||||||
application/x-www-form-urlencoded:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
full_name:
|
|
||||||
description: |
|
|
||||||
The user's full name.
|
|
||||||
|
|
||||||
**Changes**: Removed unnecessary JSON-encoding of this parameter in
|
|
||||||
Zulip 5.0 (feature level 106).
|
|
||||||
type: string
|
|
||||||
example: NewName
|
|
||||||
role:
|
|
||||||
description: |
|
|
||||||
New [role](/api/roles-and-permissions) for the user. Roles are encoded as:
|
|
||||||
|
|
||||||
- Organization owner: 100
|
|
||||||
- Organization administrator: 200
|
|
||||||
- Organization moderator: 300
|
|
||||||
- Member: 400
|
|
||||||
- Guest: 600
|
|
||||||
|
|
||||||
Only organization owners can add or remove the owner role.
|
|
||||||
|
|
||||||
The owner role cannot be removed from the only organization owner.
|
|
||||||
|
|
||||||
**Changes**: New in Zulip 3.0 (feature level 8), replacing the previous
|
|
||||||
pair of `is_admin` and `is_guest` boolean parameters. Organization moderator
|
|
||||||
role added in Zulip 4.0 (feature level 60).
|
|
||||||
type: integer
|
|
||||||
example: 400
|
|
||||||
profile_data:
|
|
||||||
description: |
|
|
||||||
A dictionary containing the to be updated custom profile field data for the user.
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
example:
|
|
||||||
[{"id": 4, "value": "0"}, {"id": 5, "value": "1909-04-05"}]
|
|
||||||
new_email:
|
|
||||||
description: |
|
|
||||||
New email address for the user. Requires the user making
|
|
||||||
the request to be an organization administrator and
|
|
||||||
additionally have the `.can_change_user_emails` special
|
|
||||||
permission.
|
|
||||||
|
|
||||||
**Changes**: New in Zulip 10.0 (feature level 313).
|
|
||||||
type: string
|
|
||||||
example: username@example.com
|
|
||||||
encoding:
|
|
||||||
role:
|
|
||||||
contentType: application/json
|
|
||||||
profile_data:
|
|
||||||
contentType: application/json
|
|
||||||
|
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
|
@ -24475,6 +24466,67 @@ components:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: "#/components/schemas/IgnoredParametersSuccess"
|
- $ref: "#/components/schemas/IgnoredParametersSuccess"
|
||||||
|
|
||||||
|
################
|
||||||
|
# Shared request bodies
|
||||||
|
################
|
||||||
|
requestBodies:
|
||||||
|
UpdateUser:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/x-www-form-urlencoded:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
full_name:
|
||||||
|
description: |
|
||||||
|
The user's full name.
|
||||||
|
|
||||||
|
**Changes**: Removed unnecessary JSON-encoding of this parameter in
|
||||||
|
Zulip 5.0 (feature level 106).
|
||||||
|
type: string
|
||||||
|
example: NewName
|
||||||
|
role:
|
||||||
|
description: |
|
||||||
|
New [role](/api/roles-and-permissions) for the user. Roles are encoded as:
|
||||||
|
|
||||||
|
- Organization owner: 100
|
||||||
|
- Organization administrator: 200
|
||||||
|
- Organization moderator: 300
|
||||||
|
- Member: 400
|
||||||
|
- Guest: 600
|
||||||
|
|
||||||
|
Only organization owners can add or remove the owner role.
|
||||||
|
|
||||||
|
The owner role cannot be removed from the only organization owner.
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 3.0 (feature level 8), replacing the previous
|
||||||
|
pair of `is_admin` and `is_guest` boolean parameters. Organization moderator
|
||||||
|
role added in Zulip 4.0 (feature level 60).
|
||||||
|
type: integer
|
||||||
|
example: 400
|
||||||
|
profile_data:
|
||||||
|
description: |
|
||||||
|
A dictionary containing the updated custom profile field data for the user.
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
example:
|
||||||
|
[{"id": 4, "value": "0"}, {"id": 5, "value": "1909-04-05"}]
|
||||||
|
new_email:
|
||||||
|
description: |
|
||||||
|
New email address for the user. Requires the user making the request
|
||||||
|
to be an organization owner and additionally have the `.can_change_user_emails`
|
||||||
|
special permission.
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 10.0 (feature level 285).
|
||||||
|
type: string
|
||||||
|
example: username@example.com
|
||||||
|
encoding:
|
||||||
|
role:
|
||||||
|
contentType: application/json
|
||||||
|
profile_data:
|
||||||
|
contentType: application/json
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Shared parameters
|
# Shared parameters
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -1121,6 +1121,63 @@ class BulkCreateUserTest(ZulipTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateUserByEmailEndpointTest(ZulipTestCase):
|
||||||
|
def test_update_user_by_email(self) -> None:
|
||||||
|
self.login("iago")
|
||||||
|
hamlet = self.example_user("hamlet")
|
||||||
|
do_change_user_setting(
|
||||||
|
hamlet,
|
||||||
|
"email_address_visibility",
|
||||||
|
UserProfile.EMAIL_ADDRESS_VISIBILITY_EVERYONE,
|
||||||
|
acting_user=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.client_patch(
|
||||||
|
f"/json/users/{hamlet.delivery_email}",
|
||||||
|
dict(full_name="Newname"),
|
||||||
|
)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
hamlet.refresh_from_db()
|
||||||
|
self.assertEqual(hamlet.full_name, "Newname")
|
||||||
|
|
||||||
|
do_change_user_setting(
|
||||||
|
hamlet,
|
||||||
|
"email_address_visibility",
|
||||||
|
UserProfile.EMAIL_ADDRESS_VISIBILITY_MEMBERS,
|
||||||
|
acting_user=None,
|
||||||
|
)
|
||||||
|
result = self.client_patch(
|
||||||
|
f"/json/users/{hamlet.delivery_email}",
|
||||||
|
dict(full_name="Newname2"),
|
||||||
|
)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
hamlet.refresh_from_db()
|
||||||
|
self.assertEqual(hamlet.full_name, "Newname2")
|
||||||
|
|
||||||
|
do_change_user_setting(
|
||||||
|
hamlet,
|
||||||
|
"email_address_visibility",
|
||||||
|
UserProfile.EMAIL_ADDRESS_VISIBILITY_NOBODY,
|
||||||
|
acting_user=None,
|
||||||
|
)
|
||||||
|
result = self.client_patch(
|
||||||
|
f"/json/users/{hamlet.delivery_email}",
|
||||||
|
dict(full_name="Newname2"),
|
||||||
|
)
|
||||||
|
self.assert_json_error(result, "No such user")
|
||||||
|
|
||||||
|
# The dummy email can be used, when we don't have access
|
||||||
|
# to the target's email address.
|
||||||
|
dummy_email = hamlet.email
|
||||||
|
result = self.client_patch(
|
||||||
|
f"/json/users/{dummy_email}",
|
||||||
|
dict(full_name="Newname3"),
|
||||||
|
)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
hamlet.refresh_from_db()
|
||||||
|
self.assertEqual(hamlet.full_name, "Newname3")
|
||||||
|
|
||||||
|
|
||||||
class AdminChangeUserEmailTest(ZulipTestCase):
|
class AdminChangeUserEmailTest(ZulipTestCase):
|
||||||
def test_change_user_email_backend(self) -> None:
|
def test_change_user_email_backend(self) -> None:
|
||||||
cordelia = self.example_user("cordelia")
|
cordelia = self.example_user("cordelia")
|
||||||
|
|
|
@ -202,7 +202,7 @@ class ProfileDataElement(BaseModel):
|
||||||
|
|
||||||
@typed_endpoint
|
@typed_endpoint
|
||||||
@transaction.atomic(durable=True)
|
@transaction.atomic(durable=True)
|
||||||
def update_user_backend(
|
def update_user_by_id_api(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
*,
|
*,
|
||||||
|
@ -215,7 +215,53 @@ def update_user_backend(
|
||||||
target = access_user_by_id(
|
target = access_user_by_id(
|
||||||
user_profile, user_id, allow_deactivated=True, allow_bots=True, for_admin=True
|
user_profile, user_id, allow_deactivated=True, allow_bots=True, for_admin=True
|
||||||
)
|
)
|
||||||
|
return update_user_backend(
|
||||||
|
request,
|
||||||
|
user_profile,
|
||||||
|
target,
|
||||||
|
full_name=full_name,
|
||||||
|
role=role,
|
||||||
|
profile_data=profile_data,
|
||||||
|
new_email=new_email,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@typed_endpoint
|
||||||
|
@transaction.atomic(durable=True)
|
||||||
|
def update_user_by_email_api(
|
||||||
|
request: HttpRequest,
|
||||||
|
user_profile: UserProfile,
|
||||||
|
*,
|
||||||
|
email: PathOnly[str],
|
||||||
|
full_name: str | None = None,
|
||||||
|
role: Json[RoleParamType] | None = None,
|
||||||
|
profile_data: Json[list[ProfileDataElement]] | None = None,
|
||||||
|
new_email: str | None = None,
|
||||||
|
) -> HttpResponse:
|
||||||
|
target = access_user_by_email(
|
||||||
|
user_profile, email, allow_deactivated=True, allow_bots=True, for_admin=True
|
||||||
|
)
|
||||||
|
return update_user_backend(
|
||||||
|
request,
|
||||||
|
user_profile,
|
||||||
|
target,
|
||||||
|
full_name=full_name,
|
||||||
|
role=role,
|
||||||
|
profile_data=profile_data,
|
||||||
|
new_email=new_email,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_user_backend(
|
||||||
|
request: HttpRequest,
|
||||||
|
user_profile: UserProfile,
|
||||||
|
target: UserProfile,
|
||||||
|
*,
|
||||||
|
full_name: str | None = None,
|
||||||
|
role: Json[RoleParamType] | None = None,
|
||||||
|
profile_data: Json[list[ProfileDataElement]] | None = None,
|
||||||
|
new_email: str | None = None,
|
||||||
|
) -> HttpResponse:
|
||||||
if new_email is not None and (
|
if new_email is not None and (
|
||||||
not user_profile.can_change_user_emails or not user_profile.is_realm_admin
|
not user_profile.can_change_user_emails or not user_profile.is_realm_admin
|
||||||
):
|
):
|
||||||
|
|
|
@ -232,7 +232,8 @@ from zerver.views.users import (
|
||||||
patch_bot_backend,
|
patch_bot_backend,
|
||||||
reactivate_user_backend,
|
reactivate_user_backend,
|
||||||
regenerate_bot_api_key,
|
regenerate_bot_api_key,
|
||||||
update_user_backend,
|
update_user_by_email_api,
|
||||||
|
update_user_by_id_api,
|
||||||
)
|
)
|
||||||
from zerver.views.video_calls import (
|
from zerver.views.video_calls import (
|
||||||
complete_zoom_user,
|
complete_zoom_user,
|
||||||
|
@ -316,11 +317,11 @@ v1_api_and_json_patterns = [
|
||||||
rest_path(
|
rest_path(
|
||||||
"users/<int:user_id>",
|
"users/<int:user_id>",
|
||||||
GET=get_members_backend,
|
GET=get_members_backend,
|
||||||
PATCH=update_user_backend,
|
PATCH=update_user_by_id_api,
|
||||||
DELETE=deactivate_user_backend,
|
DELETE=deactivate_user_backend,
|
||||||
),
|
),
|
||||||
rest_path("users/<int:user_id>/subscriptions/<int:stream_id>", GET=get_subscription_backend),
|
rest_path("users/<int:user_id>/subscriptions/<int:stream_id>", GET=get_subscription_backend),
|
||||||
rest_path("users/<email>", GET=get_user_by_email),
|
rest_path("users/<email>", GET=get_user_by_email, PATCH=update_user_by_email_api),
|
||||||
rest_path("bots", GET=get_bots_backend, POST=add_bot_backend),
|
rest_path("bots", GET=get_bots_backend, POST=add_bot_backend),
|
||||||
rest_path("bots/<int:bot_id>/api_key/regenerate", POST=regenerate_bot_api_key),
|
rest_path("bots/<int:bot_id>/api_key/regenerate", POST=regenerate_bot_api_key),
|
||||||
rest_path("bots/<int:bot_id>", PATCH=patch_bot_backend, DELETE=deactivate_bot_backend),
|
rest_path("bots/<int:bot_id>", PATCH=patch_bot_backend, DELETE=deactivate_bot_backend),
|
||||||
|
|
Loading…
Reference in New Issue