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 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/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
95 changes: 90 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,60 @@ async def remove_user_external_id(
desc="remove_user_external_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 _remove_user_external_ids_txn(
txn: LoggingTransaction,
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
"""

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

def _replace_user_external_id_txn(
txn: LoggingTransaction,
):
_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