Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Allow to edit external_ids by Edit User admin API #10598

Merged
merged 7 commits into from
Aug 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/10598.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow editing a user's `external_ids` via the "Edit User" admin API. Contributed by @dklimpel.
40 changes: 29 additions & 11 deletions docs/admin_api/user_admin_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ with a body of:
"address": "<user_mail_2>"
}
],
"external_ids": [
{
"auth_provider": "<provider1>",
"external_id": "<user_id_provider_1>"
},
{
"auth_provider": "<provider2>",
"external_id": "<user_id_provider_2>"
}
],
"avatar_url": "<avatar_url>",
"admin": false,
"deactivated": false
Expand All @@ -90,26 +100,34 @@ with a body of:
To use it, you will need to authenticate by providing an `access_token` for a
server admin: [Admin API](../usage/administration/admin_api)

Returns HTTP status code:
- `201` - When a new user object was created.
- `200` - When a user was modified.

URL parameters:

- `user_id`: fully-qualified user id: for example, `@user:server.com`.

Body parameters:

- `password`, optional. If provided, the user's password is updated and all
- `password` - string, optional. If provided, the user's password is updated and all
devices are logged out.

- `displayname`, optional, defaults to the value of `user_id`.

- `threepids`, optional, allows setting the third-party IDs (email, msisdn)
- `displayname` - string, optional, defaults to the value of `user_id`.
- `threepids` - array, optional, allows setting the third-party IDs (email, msisdn)
- `medium` - string. Kind of third-party ID, either `email` or `msisdn`.
- `address` - string. Value of third-party ID.
belonging to a user.

- `avatar_url`, optional, must be a
- `external_ids` - array, optional. Allow setting the identifier of the external identity
provider for SSO (Single sign-on). Details in
[Sample Configuration File](../usage/configuration/homeserver_sample_config.html)
section `sso` and `oidc_providers`.
- `auth_provider` - string. ID of the external identity provider. Value of `idp_id`
in homeserver configuration.
- `external_id` - string, user ID in the external identity provider.
- `avatar_url` - string, optional, must be a
[MXC URI](https://matrix.org/docs/spec/client_server/r0.6.0#matrix-content-mxc-uris).

- `admin`, optional, defaults to `false`.

- `deactivated`, optional. If unspecified, deactivation state will be left
- `admin` - bool, optional, defaults to `false`.
- `deactivated` - bool, optional. If unspecified, deactivation state will be left
unchanged on existing accounts and set to `false` for new accounts.
A user cannot be erased by deactivating with this API. For details on
deactivating users see [Deactivate Account](#deactivate-account).
Expand Down
139 changes: 91 additions & 48 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,20 +196,57 @@ async def on_PUT(
user = await self.admin_handler.get_user(target_user)
user_id = target_user.to_string()

# check for required parameters for each threepid
threepids = body.get("threepids")
if threepids is not None:
for threepid in threepids:
assert_params_in_dict(threepid, ["medium", "address"])

# check for required parameters for each external_id
external_ids = body.get("external_ids")
if external_ids is not None:
for external_id in external_ids:
assert_params_in_dict(external_id, ["auth_provider", "external_id"])

user_type = body.get("user_type", None)
if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES:
raise SynapseError(400, "Invalid user type")

set_admin_to = body.get("admin", False)
if not isinstance(set_admin_to, bool):
raise SynapseError(
HTTPStatus.BAD_REQUEST,
"Param 'admin' must be a boolean, if given",
Codes.BAD_JSON,
)

password = body.get("password", None)
if password is not None:
if not isinstance(password, str) or len(password) > 512:
raise SynapseError(400, "Invalid password")

deactivate = body.get("deactivated", False)
if not isinstance(deactivate, bool):
raise SynapseError(400, "'deactivated' parameter is not of type boolean")

# convert into List[Tuple[str, str]]
if external_ids is not None:
new_external_ids = []
for external_id in external_ids:
new_external_ids.append(
(external_id["auth_provider"], external_id["external_id"])
)

if user: # modify user
if "displayname" in body:
await self.profile_handler.set_displayname(
target_user, requester, body["displayname"], True
)

if "threepids" in body:
# check for required parameters for each threepid
for threepid in body["threepids"]:
assert_params_in_dict(threepid, ["medium", "address"])

if threepids is not None:
# remove old threepids from user
threepids = await self.store.user_get_threepids(user_id)
for threepid in threepids:
old_threepids = await self.store.user_get_threepids(user_id)
for threepid in old_threepids:
try:
await self.auth_handler.delete_threepid(
user_id, threepid["medium"], threepid["address"], None
Expand All @@ -220,48 +257,58 @@ async def on_PUT(

# add new threepids to user
current_time = self.hs.get_clock().time_msec()
for threepid in body["threepids"]:
for threepid in threepids:
await self.auth_handler.add_threepid(
user_id, threepid["medium"], threepid["address"], current_time
)

if "avatar_url" in body and type(body["avatar_url"]) == str:
if external_ids is not None:
# get changed external_ids (added and removed)
cur_external_ids = await self.store.get_external_ids_by_user(user_id)
add_external_ids = set(new_external_ids) - set(cur_external_ids)
del_external_ids = set(cur_external_ids) - set(new_external_ids)

# remove old external_ids
for auth_provider, external_id in del_external_ids:
await self.store.remove_user_external_id(
auth_provider,
external_id,
user_id,
)

# add new external_ids
for auth_provider, external_id in add_external_ids:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we also need to do this when creating a new user. (Please don't do it by c&ping the code, like the threepid code!)

await self.store.record_user_external_id(
auth_provider,
external_id,
user_id,
)
richvdh marked this conversation as resolved.
Show resolved Hide resolved

if "avatar_url" in body and isinstance(body["avatar_url"], str):
await self.profile_handler.set_avatar_url(
target_user, requester, body["avatar_url"], True
)

if "admin" in body:
set_admin_to = bool(body["admin"])
if set_admin_to != user["admin"]:
auth_user = requester.user
if target_user == auth_user and not set_admin_to:
raise SynapseError(400, "You may not demote yourself.")

await self.store.set_server_admin(target_user, set_admin_to)

if "password" in body:
if not isinstance(body["password"], str) or len(body["password"]) > 512:
raise SynapseError(400, "Invalid password")
else:
new_password = body["password"]
logout_devices = True

new_password_hash = await self.auth_handler.hash(new_password)

await self.set_password_handler.set_password(
target_user.to_string(),
new_password_hash,
logout_devices,
requester,
)
if password is not None:
logout_devices = True
new_password_hash = await self.auth_handler.hash(password)

await self.set_password_handler.set_password(
target_user.to_string(),
new_password_hash,
logout_devices,
requester,
)

if "deactivated" in body:
deactivate = body["deactivated"]
if not isinstance(deactivate, bool):
raise SynapseError(
400, "'deactivated' parameter is not of type boolean"
)

if deactivate and not user["deactivated"]:
await self.deactivate_account_handler.deactivate_account(
target_user.to_string(), False, requester, by_admin=True
Expand All @@ -285,36 +332,24 @@ async def on_PUT(
return 200, user

else: # create user
password = body.get("password")
displayname = body.get("displayname", None)

password_hash = None
if password is not None:
if not isinstance(password, str) or len(password) > 512:
raise SynapseError(400, "Invalid password")
password_hash = await self.auth_handler.hash(password)

admin = body.get("admin", None)
user_type = body.get("user_type", None)
displayname = body.get("displayname", None)

if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES:
raise SynapseError(400, "Invalid user type")

user_id = await self.registration_handler.register_user(
localpart=target_user.localpart,
password_hash=password_hash,
admin=bool(admin),
admin=set_admin_to,
default_display_name=displayname,
user_type=user_type,
by_admin=True,
)

if "threepids" in body:
# check for required parameters for each threepid
for threepid in body["threepids"]:
assert_params_in_dict(threepid, ["medium", "address"])

if threepids is not None:
current_time = self.hs.get_clock().time_msec()
for threepid in body["threepids"]:
for threepid in threepids:
await self.auth_handler.add_threepid(
user_id, threepid["medium"], threepid["address"], current_time
)
Expand All @@ -334,6 +369,14 @@ async def on_PUT(
data={},
)

if external_ids is not None:
for auth_provider, external_id in new_external_ids:
await self.store.record_user_external_id(
auth_provider,
external_id,
user_id,
)

if "avatar_url" in body and isinstance(body["avatar_url"], str):
await self.profile_handler.set_avatar_url(
target_user, requester, body["avatar_url"], True
Expand Down
22 changes: 22 additions & 0 deletions synapse/storage/databases/main/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,28 @@ async def record_user_external_id(
desc="record_user_external_id",
)

async def remove_user_external_id(
self, auth_provider: str, external_id: str, user_id: str
) -> None:
"""Remove a mapping from an external user id to a mxid

If the mapping is not found, this method does nothing.

Args:
auth_provider: identifier for the remote auth provider
external_id: id on that system
user_id: complete mxid that it is mapped to
"""
await self.db_pool.simple_delete(
table="user_external_ids",
keyvalues={
"auth_provider": auth_provider,
"external_id": external_id,
"user_id": user_id,
},
desc="remove_user_external_id",
)

async def get_user_by_external_id(
self, auth_provider: str, external_id: str
) -> Optional[str]:
Expand Down
Loading