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

Fix setting a user's external_id via the admin API returns 500 and deletes users existing external mappings if that external ID is already mapped #11051

Merged
merged 7 commits into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from 5 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/11051.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a bug where setting a user's external_id via the admin API returns 500 and deletes users existing external mappings if that external ID is already mapped.
47 changes: 18 additions & 29 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
assert_user_is_admin,
)
from synapse.rest.client._base import client_patterns
from synapse.storage.databases.main.registration import ExternalIDReuseException
from synapse.storage.databases.main.stats import UserSortOrder
from synapse.types import JsonDict, UserID

Expand Down Expand Up @@ -228,12 +229,12 @@ async def on_PUT(
if not isinstance(deactivate, bool):
raise SynapseError(400, "'deactivated' parameter is not of type boolean")

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

# convert List[Dict[str, str]] into Set[Tuple[str, str]]
if threepids is not None:
Expand Down Expand Up @@ -275,28 +276,13 @@ async def on_PUT(
)

if external_ids is not None:
# get changed external_ids (added and removed)
cur_external_ids = set(
await self.store.get_external_ids_by_user(user_id)
)
add_external_ids = new_external_ids - cur_external_ids
del_external_ids = cur_external_ids - 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:
await self.store.record_user_external_id(
auth_provider,
external_id,
try:
await self.store.replace_user_external_id(
new_external_ids,
user_id,
)
except ExternalIDReuseException:
raise SynapseError(409, "External id is already in use.")

if "avatar_url" in body and isinstance(body["avatar_url"], str):
await self.profile_handler.set_avatar_url(
Expand Down Expand Up @@ -384,12 +370,15 @@ async def on_PUT(
)

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,
)
try:
for auth_provider, external_id in new_external_ids:
await self.store.record_user_external_id(
auth_provider,
external_id,
user_id,
)
except ExternalIDReuseException:
raise SynapseError(409, "External id is already in use.")

if "avatar_url" in body and isinstance(body["avatar_url"], str):
await self.profile_handler.set_avatar_url(
Expand Down
106 changes: 101 additions & 5 deletions synapse/storage/databases/main/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
from synapse.api.constants import UserTypes
from synapse.api.errors import Codes, StoreError, SynapseError, ThreepidValidationError
from synapse.metrics.background_process_metrics import wrap_as_background_process
from synapse.storage.database import DatabasePool, LoggingDatabaseConnection
from synapse.storage.database import (
DatabasePool,
LoggingDatabaseConnection,
LoggingTransaction,
)
from synapse.storage.databases.main.cache import CacheInvalidationWorkerStore
from synapse.storage.databases.main.stats import StatsStore
from synapse.storage.types import Connection, Cursor
Expand All @@ -40,6 +44,13 @@
logger = logging.getLogger(__name__)


class ExternalIDReuseException(Exception):
"""Exception if writing an external id for a user fails,
because this external id is given to an other user."""

pass


@attr.s(frozen=True, slots=True)
class TokenLookupResult:
"""Result of looking up an access token.
Expand Down Expand Up @@ -588,24 +599,44 @@ async def record_user_external_id(
auth_provider: identifier for the remote auth provider
external_id: id on that system
user_id: complete mxid that it is mapped to
Raises:
ExternalIDReuseException if the new external_id could not be mapped.
"""
await self.db_pool.simple_insert(

try:
await self.db_pool.runInteraction(
"record_user_external_id",
self._record_user_external_id_txn,
auth_provider,
external_id,
user_id,
)
except self.database_engine.module.IntegrityError:
raise ExternalIDReuseException()

def _record_user_external_id_txn(
self,
txn: LoggingTransaction,
auth_provider: str,
external_id: str,
user_id: str,
) -> None:

self.db_pool.simple_insert_txn(
txn,
table="user_external_ids",
values={
"auth_provider": auth_provider,
"external_id": external_id,
"user_id": user_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
Expand All @@ -621,6 +652,71 @@ async def remove_user_external_id(
desc="remove_user_external_id",
)

async def remove_user_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.

This is no longer used, so I don't think we need it? In which case let's delete this, then we can just inline _remove_user_external_ids_txn into replace_user_external_id? WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea was remove_user_external_ids use for #11072. But this needs discussions at the moment.
I will make the changes.

Copy link
Member

Choose a reason for hiding this comment

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

Aaaah, that explains it. Thanks.

self, auth_provider: str, external_id: str, user_id: str
) -> None:
"""Remove all mappings from external user ids to a mxid

If these mappings are not found, this method does nothing.

Args:
user_id: complete mxid that it is mapped to
"""
await self.db_pool.runInteraction(
"remove_user_external_ids",
self._remove_user_external_ids_txn,
user_id,
)

def _remove_user_external_ids_txn(
self,
txn: LoggingTransaction,
user_id: str,
) -> None:

self.db_pool.simple_delete_txn(
txn,
table="user_external_ids",
keyvalues={"user_id": user_id},
)

async def replace_user_external_id(
self,
record_external_ids: List[Tuple[str, str]],
user_id: str,
) -> None:
"""Replace mappings from external user ids to a mxid in a single transaction.
All mappings are deleted and the new ones are created.

Args:
record_external_ids:
List with tuple of auth_provider and external_id to record
user_id: complete mxid that it is mapped to
Raises:
ExternalIDReuseException if the new external_id could not be mapped.
"""

def _replace_user_external_id_txn(
txn: LoggingTransaction,
):
self._remove_user_external_ids_txn(txn, user_id)

for auth_provider, external_id in record_external_ids:
self._record_user_external_id_txn(
txn,
auth_provider,
external_id,
user_id,
)

try:
await self.db_pool.runInteraction(
"replace_user_external_id",
_replace_user_external_id_txn,
)
except self.database_engine.module.IntegrityError:
raise ExternalIDReuseException()

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