Skip to content
This repository was 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 2 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 to edit `external_ids` by Edit User admin API.
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
31 changes: 31 additions & 0 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,37 @@ async def on_PUT(
user_id, threepid["medium"], threepid["address"], current_time
)

if "external_ids" in body:
# check for required parameters for each external_id
# and convert into List[Tuple[str, str]]
new_external_ids = []
for external_id in body["external_ids"]:
assert_params_in_dict(external_id, ["auth_provider", "external_id"])
Copy link
Member

Choose a reason for hiding this comment

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

If the call specifies a valid displayname or threepids, but an invalid external_ids, we will update the displayname/threepids, but then return a 400 error.

We should validate the parameters before we make any changes.

new_external_ids.append(
(external_id["auth_provider"], external_id["external_id"])
)

# 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,
)

if "avatar_url" in body and type(body["avatar_url"]) == str:
await self.profile_handler.set_avatar_url(
target_user, requester, body["avatar_url"], True
Expand Down
20 changes: 20 additions & 0 deletions synapse/storage/databases/main/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,26 @@ 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

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
134 changes: 97 additions & 37 deletions tests/rest/admin/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -1255,43 +1255,6 @@ def test_get_user(self):
self.assertEqual("User", channel.json_body["displayname"])
self._check_fields(channel.json_body)

def test_get_user_with_sso(self):
"""
Test get a user with SSO details.
"""
self.get_success(
self.store.record_user_external_id(
"auth_provider1", "external_id1", self.other_user
)
)
self.get_success(
self.store.record_user_external_id(
"auth_provider2", "external_id2", self.other_user
)
)

channel = self.make_request(
"GET",
self.url_other_user,
access_token=self.admin_user_tok,
)

self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual("@user:test", channel.json_body["name"])
self.assertEqual(
"external_id1", channel.json_body["external_ids"][0]["external_id"]
)
self.assertEqual(
"auth_provider1", channel.json_body["external_ids"][0]["auth_provider"]
)
self.assertEqual(
"external_id2", channel.json_body["external_ids"][1]["external_id"]
)
self.assertEqual(
"auth_provider2", channel.json_body["external_ids"][1]["auth_provider"]
)
self._check_fields(channel.json_body)

def test_create_server_admin(self):
"""
Check that a new admin user is created successfully.
Expand Down Expand Up @@ -1632,6 +1595,103 @@ def test_set_threepid(self):
self.assertEqual("email", channel.json_body["threepids"][0]["medium"])
self.assertEqual("[email protected]", channel.json_body["threepids"][0]["address"])

def test_set_external_id(self):
"""
Test setting external id for an other user.
"""

# Add two external_ids
channel = self.make_request(
"PUT",
self.url_other_user,
access_token=self.admin_user_tok,
content={
"external_ids": [
{
"external_id": "external_id1",
"auth_provider": "auth_provider1",
},
{
"external_id": "external_id2",
"auth_provider": "auth_provider2",
},
]
},
)

self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual("@user:test", channel.json_body["name"])
self.assertEqual(2, len(channel.json_body["external_ids"]))
# result does not always have the same sort order, therefore it becomes sorted
self.assertEqual(
sorted(channel.json_body["external_ids"], key=lambda k: k["auth_provider"]),
[
{"auth_provider": "auth_provider1", "external_id": "external_id1"},
{"auth_provider": "auth_provider2", "external_id": "external_id2"},
],
)
self._check_fields(channel.json_body)

# Set a new and remove an external_id
channel = self.make_request(
"PUT",
self.url_other_user,
access_token=self.admin_user_tok,
content={
"external_ids": [
{
"external_id": "external_id2",
"auth_provider": "auth_provider2",
},
{
"external_id": "external_id3",
"auth_provider": "auth_provider3",
},
]
},
)

self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual("@user:test", channel.json_body["name"])
self.assertEqual(2, len(channel.json_body["external_ids"]))
self.assertEqual(
channel.json_body["external_ids"],
[
{"auth_provider": "auth_provider2", "external_id": "external_id2"},
{"auth_provider": "auth_provider3", "external_id": "external_id3"},
],
)
self._check_fields(channel.json_body)

# Get user
channel = self.make_request(
"GET",
self.url_other_user,
access_token=self.admin_user_tok,
)

self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual("@user:test", channel.json_body["name"])
self.assertEqual(
channel.json_body["external_ids"],
[
{"auth_provider": "auth_provider2", "external_id": "external_id2"},
{"auth_provider": "auth_provider3", "external_id": "external_id3"},
],
)
self._check_fields(channel.json_body)

# Remove external_ids
channel = self.make_request(
"PUT",
self.url_other_user,
access_token=self.admin_user_tok,
content={"external_ids": []},
)
self.assertEqual(200, channel.code, msg=channel.json_body)
self.assertEqual("@user:test", channel.json_body["name"])
self.assertEqual(0, len(channel.json_body["external_ids"]))

def test_deactivate_user(self):
"""
Test deactivating another user.
Expand Down