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 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/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.
20 changes: 5 additions & 15 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,21 +282,11 @@ async def on_PUT(
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,
user_id,
)
await self.store.record_and_remove_user_external_id(
add_external_ids,
del_external_ids,
user_id,
)
Copy link
Member

Choose a reason for hiding this comment

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

I think it might be easier to instead have a storage function that takes the external_ids and just delete all existing mappings for the user and insert all external_ids. It's saves us from having to do the dance of pulling the existing IDs out

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have

  • rename record_and_remove_user_external_id to replace_user_external_id
  • modify replace_user_external_id to delete all mappings and add given mappings
  • restore remove_user_external_id to orign/delop branch, no _txn call is needed
  • add remove_user_external_ids to delete all mappings with one sql query
  • also add _remove_user_external_ids_txn
  • change rest handler to use new functions


if "avatar_url" in body and isinstance(body["avatar_url"], str):
await self.profile_handler.set_avatar_url(
Expand Down
94 changes: 88 additions & 6 deletions synapse/storage/databases/main/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@
import logging
import random
import re
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union

import attr

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 Down Expand Up @@ -589,14 +593,30 @@ async def record_user_external_id(
external_id: id on that system
user_id: complete mxid that it is mapped to
"""
await self.db_pool.simple_insert(
await self.db_pool.runInteraction(
"record_user_external_id",
self._record_user_external_id_txn,
auth_provider,
external_id,
user_id,
)

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(
Expand All @@ -611,16 +631,78 @@ async def remove_user_external_id(
external_id: id on that system
user_id: complete mxid that it is mapped to
"""
await self.db_pool.simple_delete(
await self.db_pool.runInteraction(
"remove_user_external_id",
self._remove_user_external_id_txn,
auth_provider,
external_id,
user_id,
)

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

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

async def record_and_remove_user_external_id(
self,
record_external_ids: Set[Tuple[str, str]],
remove_external_ids: Set[Tuple[str, str]],
user_id: str,
) -> None:
"""Record and remove mappings from external user ids to a mxid
in a single transaction.

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

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

for auth_provider, external_id in remove_external_ids:
self._remove_user_external_id_txn(
txn,
auth_provider,
external_id,
user_id,
)

try:
await self.db_pool.runInteraction(
"record_and_remove_user_external_id",
_record_and_remove_user_external_id_txn,
)
except self.database_engine.module.IntegrityError:
raise StoreError(409, "External id already in use.")
Copy link
Member

Choose a reason for hiding this comment

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

We're trying to avoid setting HTTP status codes in the guts of the storage layer, as it leads to weird bugs when code gets reused. Could you add a quick new exception type and catch that in the rest handler please? It just needs to be something like:

class ExternalIDReuseException(Exception):
    pass


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