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

Commit ef7fe09

Browse files
authored
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)
Fixes #10846
1 parent 57501d9 commit ef7fe09

File tree

4 files changed

+321
-37
lines changed

4 files changed

+321
-37
lines changed

changelog.d/11051.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

synapse/rest/admin/users.py

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
assert_user_is_admin,
3636
)
3737
from synapse.rest.client._base import client_patterns
38+
from synapse.storage.databases.main.registration import ExternalIDReuseException
3839
from synapse.storage.databases.main.stats import UserSortOrder
3940
from synapse.types import JsonDict, UserID
4041

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

231-
# convert List[Dict[str, str]] into Set[Tuple[str, str]]
232+
# convert List[Dict[str, str]] into List[Tuple[str, str]]
232233
if external_ids is not None:
233-
new_external_ids = {
234+
new_external_ids = [
234235
(external_id["auth_provider"], external_id["external_id"])
235236
for external_id in external_ids
236-
}
237+
]
237238

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

277278
if external_ids is not None:
278-
# get changed external_ids (added and removed)
279-
cur_external_ids = set(
280-
await self.store.get_external_ids_by_user(user_id)
281-
)
282-
add_external_ids = new_external_ids - cur_external_ids
283-
del_external_ids = cur_external_ids - new_external_ids
284-
285-
# remove old external_ids
286-
for auth_provider, external_id in del_external_ids:
287-
await self.store.remove_user_external_id(
288-
auth_provider,
289-
external_id,
290-
user_id,
291-
)
292-
293-
# add new external_ids
294-
for auth_provider, external_id in add_external_ids:
295-
await self.store.record_user_external_id(
296-
auth_provider,
297-
external_id,
279+
try:
280+
await self.store.replace_user_external_id(
281+
new_external_ids,
298282
user_id,
299283
)
284+
except ExternalIDReuseException:
285+
raise SynapseError(409, "External id is already in use.")
300286

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

386372
if external_ids is not None:
387-
for auth_provider, external_id in new_external_ids:
388-
await self.store.record_user_external_id(
389-
auth_provider,
390-
external_id,
391-
user_id,
392-
)
373+
try:
374+
for auth_provider, external_id in new_external_ids:
375+
await self.store.record_user_external_id(
376+
auth_provider,
377+
external_id,
378+
user_id,
379+
)
380+
except ExternalIDReuseException:
381+
raise SynapseError(409, "External id is already in use.")
393382

394383
if "avatar_url" in body and isinstance(body["avatar_url"], str):
395384
await self.profile_handler.set_avatar_url(

synapse/storage/databases/main/registration.py

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@
2323
from synapse.api.constants import UserTypes
2424
from synapse.api.errors import Codes, StoreError, SynapseError, ThreepidValidationError
2525
from synapse.metrics.background_process_metrics import wrap_as_background_process
26-
from synapse.storage.database import DatabasePool, LoggingDatabaseConnection
26+
from synapse.storage.database import (
27+
DatabasePool,
28+
LoggingDatabaseConnection,
29+
LoggingTransaction,
30+
)
2731
from synapse.storage.databases.main.cache import CacheInvalidationWorkerStore
2832
from synapse.storage.databases.main.stats import StatsStore
2933
from synapse.storage.types import Cursor
@@ -40,6 +44,13 @@
4044
logger = logging.getLogger(__name__)
4145

4246

47+
class ExternalIDReuseException(Exception):
48+
"""Exception if writing an external id for a user fails,
49+
because this external id is given to an other user."""
50+
51+
pass
52+
53+
4354
@attr.s(frozen=True, slots=True)
4455
class TokenLookupResult:
4556
"""Result of looking up an access token.
@@ -588,24 +599,44 @@ async def record_user_external_id(
588599
auth_provider: identifier for the remote auth provider
589600
external_id: id on that system
590601
user_id: complete mxid that it is mapped to
602+
Raises:
603+
ExternalIDReuseException if the new external_id could not be mapped.
591604
"""
592-
await self.db_pool.simple_insert(
605+
606+
try:
607+
await self.db_pool.runInteraction(
608+
"record_user_external_id",
609+
self._record_user_external_id_txn,
610+
auth_provider,
611+
external_id,
612+
user_id,
613+
)
614+
except self.database_engine.module.IntegrityError:
615+
raise ExternalIDReuseException()
616+
617+
def _record_user_external_id_txn(
618+
self,
619+
txn: LoggingTransaction,
620+
auth_provider: str,
621+
external_id: str,
622+
user_id: str,
623+
) -> None:
624+
625+
self.db_pool.simple_insert_txn(
626+
txn,
593627
table="user_external_ids",
594628
values={
595629
"auth_provider": auth_provider,
596630
"external_id": external_id,
597631
"user_id": user_id,
598632
},
599-
desc="record_user_external_id",
600633
)
601634

602635
async def remove_user_external_id(
603636
self, auth_provider: str, external_id: str, user_id: str
604637
) -> None:
605638
"""Remove a mapping from an external user id to a mxid
606-
607639
If the mapping is not found, this method does nothing.
608-
609640
Args:
610641
auth_provider: identifier for the remote auth provider
611642
external_id: id on that system
@@ -621,6 +652,60 @@ async def remove_user_external_id(
621652
desc="remove_user_external_id",
622653
)
623654

655+
async def replace_user_external_id(
656+
self,
657+
record_external_ids: List[Tuple[str, str]],
658+
user_id: str,
659+
) -> None:
660+
"""Replace mappings from external user ids to a mxid in a single transaction.
661+
All mappings are deleted and the new ones are created.
662+
663+
Args:
664+
record_external_ids:
665+
List with tuple of auth_provider and external_id to record
666+
user_id: complete mxid that it is mapped to
667+
Raises:
668+
ExternalIDReuseException if the new external_id could not be mapped.
669+
"""
670+
671+
def _remove_user_external_ids_txn(
672+
txn: LoggingTransaction,
673+
user_id: str,
674+
) -> None:
675+
"""Remove all mappings from external user ids to a mxid
676+
If these mappings are not found, this method does nothing.
677+
678+
Args:
679+
user_id: complete mxid that it is mapped to
680+
"""
681+
682+
self.db_pool.simple_delete_txn(
683+
txn,
684+
table="user_external_ids",
685+
keyvalues={"user_id": user_id},
686+
)
687+
688+
def _replace_user_external_id_txn(
689+
txn: LoggingTransaction,
690+
):
691+
_remove_user_external_ids_txn(txn, user_id)
692+
693+
for auth_provider, external_id in record_external_ids:
694+
self._record_user_external_id_txn(
695+
txn,
696+
auth_provider,
697+
external_id,
698+
user_id,
699+
)
700+
701+
try:
702+
await self.db_pool.runInteraction(
703+
"replace_user_external_id",
704+
_replace_user_external_id_txn,
705+
)
706+
except self.database_engine.module.IntegrityError:
707+
raise ExternalIDReuseException()
708+
624709
async def get_user_by_external_id(
625710
self, auth_provider: str, external_id: str
626711
) -> Optional[str]:

0 commit comments

Comments
 (0)