Skip to content
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/18456.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support configuration of default and extra user types.
3 changes: 2 additions & 1 deletion docs/admin_api/user_admin_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ Body parameters:
- `locked` - **bool**, optional. If unspecified, locked state will be left unchanged.
- `user_type` - **string** or null, optional. If not provided, the user type will be
not be changed. If `null` is given, the user type will be cleared.
Other allowed options are: `bot` and `support`.
Other allowed options are: `bot` and `support` and any extra values defined in the homserver
[configuration](../usage/configuration/config_documentation.md#user_types).

## List Accounts
### List Accounts (V2)
Expand Down
18 changes: 18 additions & 0 deletions docs/usage/configuration/config_documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,24 @@ Example configuration:
max_event_delay_duration: 24h
```
---
### `user_types`

Configuration settings related to the user types feature.

This setting has the following sub-options:
* `default_user_type`: The default user type to use for registering new users when no value has been specified.
Defaults to none.
* `extra_user_types`: Array of additional user types to allow. These are treated as real users. Defaults to [].

Example configuration:
```yaml
user_types:
default_user_type: "custom"
extra_user_types:
- "custom"
- "custom2"
```

## Homeserver blocking

Useful options for Synapse admins.
Expand Down
10 changes: 8 additions & 2 deletions synapse/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,18 @@ class ThirdPartyEntityKind:

class UserTypes:
"""Allows for user type specific behaviour. With the benefit of hindsight
'admin' and 'guest' users should also be UserTypes. Normal users are type None
'admin' and 'guest' users should also be UserTypes. Extra user types can be
added in the configuration. Normal users are type None or one of the extra
user types (if configured).
"""

SUPPORT: Final = "support"
BOT: Final = "bot"
ALL_USER_TYPES: Final = (SUPPORT, BOT)
ALL_BUILTIN_USER_TYPES: Final = (SUPPORT, BOT)
"""
The user types that are built-in to Synapse. Extra user types can be
added in the configuration.
"""


class RelationTypes:
Expand Down
2 changes: 2 additions & 0 deletions synapse/config/_base.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ from synapse.config import ( # noqa: F401
tls,
tracer,
user_directory,
user_types,
voip,
workers,
)
Expand Down Expand Up @@ -122,6 +123,7 @@ class RootConfig:
retention: retention.RetentionConfig
background_updates: background_updates.BackgroundUpdateConfig
auto_accept_invites: auto_accept_invites.AutoAcceptInvitesConfig
user_types: user_types.UserTypesConfig

config_classes: List[Type["Config"]] = ...
config_files: List[str]
Expand Down
2 changes: 2 additions & 0 deletions synapse/config/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
from .tls import TlsConfig
from .tracer import TracerConfig
from .user_directory import UserDirectoryConfig
from .user_types import UserTypesConfig
from .voip import VoipConfig
from .workers import WorkerConfig

Expand Down Expand Up @@ -107,4 +108,5 @@ class HomeServerConfig(RootConfig):
ExperimentalConfig,
BackgroundUpdateConfig,
AutoAcceptInvitesConfig,
UserTypesConfig,
]
44 changes: 44 additions & 0 deletions synapse/config/user_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2025 New Vector, Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.
#

from typing import Any, List, Optional

from synapse.api.constants import UserTypes
from synapse.types import JsonDict

from ._base import Config, ConfigError


class UserTypesConfig(Config):
section = "user_types"

def read_config(self, config: JsonDict, **kwargs: Any) -> None:
user_types: JsonDict = config.get("user_types", {})

self.default_user_type: Optional[str] = user_types.get(
"default_user_type", None
)
self.extra_user_types: List[str] = user_types.get("extra_user_types", [])

all_user_types: List[str] = []
all_user_types.extend(UserTypes.ALL_BUILTIN_USER_TYPES)
all_user_types.extend(self.extra_user_types)

self.all_user_types = all_user_types

if self.default_user_type is not None:
if self.default_user_type not in all_user_types:
raise ConfigError(
f"Default user type {self.default_user_type} is not in the list of all user types: {all_user_types}"
)
4 changes: 4 additions & 0 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def __init__(self, hs: "HomeServer"):
self._user_consent_version = self.hs.config.consent.user_consent_version
self._server_notices_mxid = hs.config.servernotices.server_notices_mxid
self._server_name = hs.hostname
self._user_types_config = hs.config.user_types

self._spam_checker_module_callbacks = hs.get_module_api_callbacks().spam_checker

Expand Down Expand Up @@ -306,6 +307,9 @@ async def register_user(
elif default_display_name is None:
default_display_name = localpart

if user_type is None:
user_type = self._user_types_config.default_user_type

await self.register_with_store(
user_id=user_id,
password_hash=password_hash,
Expand Down
8 changes: 5 additions & 3 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import attr

from synapse._pydantic_compat import StrictBool, StrictInt, StrictStr
from synapse.api.constants import Direction, UserTypes
from synapse.api.constants import Direction
from synapse.api.errors import Codes, NotFoundError, SynapseError
from synapse.http.servlet import (
RestServlet,
Expand Down Expand Up @@ -230,6 +230,7 @@ def __init__(self, hs: "HomeServer"):
self.registration_handler = hs.get_registration_handler()
self.pusher_pool = hs.get_pusherpool()
self._msc3866_enabled = hs.config.experimental.msc3866.enabled
self._all_user_types = hs.config.user_types.all_user_types

async def on_GET(
self, request: SynapseRequest, user_id: str
Expand Down Expand Up @@ -277,7 +278,7 @@ async def on_PUT(
assert_params_in_dict(external_id, ["auth_provider", "external_id"])

user_type = body.get("user_type", None)
if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES:
if user_type is not None and user_type not in self._all_user_types:
raise SynapseError(HTTPStatus.BAD_REQUEST, "Invalid user type")

set_admin_to = body.get("admin", False)
Expand Down Expand Up @@ -524,6 +525,7 @@ def __init__(self, hs: "HomeServer"):
self.reactor = hs.get_reactor()
self.nonces: Dict[str, int] = {}
self.hs = hs
self._all_user_types = hs.config.user_types.all_user_types

def _clear_old_nonces(self) -> None:
"""
Expand Down Expand Up @@ -605,7 +607,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
user_type = body.get("user_type", None)
displayname = body.get("displayname", None)

if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES:
if user_type is not None and user_type not in self._all_user_types:
raise SynapseError(HTTPStatus.BAD_REQUEST, "Invalid user type")

if "mac" not in body:
Expand Down
15 changes: 10 additions & 5 deletions synapse/storage/databases/main/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,9 @@ def set_shadow_banned_txn(txn: LoggingTransaction) -> None:

await self.db_pool.runInteraction("set_shadow_banned", set_shadow_banned_txn)

async def set_user_type(self, user: UserID, user_type: Optional[UserTypes]) -> None:
async def set_user_type(
self, user: UserID, user_type: Optional[Union[UserTypes, str]]
) -> None:
"""Sets the user type.

Args:
Expand Down Expand Up @@ -683,7 +685,7 @@ def is_real_user_txn(self, txn: LoggingTransaction, user_id: str) -> bool:
retcol="user_type",
allow_none=True,
)
return res is None
return res is None or res not in [UserTypes.BOT, UserTypes.SUPPORT]

def is_support_user_txn(self, txn: LoggingTransaction, user_id: str) -> bool:
res = self.db_pool.simple_select_one_onecol_txn(
Expand Down Expand Up @@ -959,10 +961,12 @@ def _count_users(txn: LoggingTransaction) -> int:
return await self.db_pool.runInteraction("count_users", _count_users)

async def count_real_users(self) -> int:
"""Counts all users without a special user_type registered on the homeserver."""
"""Counts all users without the bot or support user_types registered on the homeserver."""

def _count_users(txn: LoggingTransaction) -> int:
txn.execute("SELECT COUNT(*) FROM users where user_type is null")
txn.execute(
f"SELECT COUNT(*) FROM users WHERE user_type IS NULL OR user_type NOT IN ('{UserTypes.BOT}', '{UserTypes.SUPPORT}')"
)
row = txn.fetchone()
assert row is not None
return row[0]
Expand Down Expand Up @@ -2545,7 +2549,8 @@ async def register_user(
the user, setting their displayname to the given value
admin: is an admin user?
user_type: type of user. One of the values from api.constants.UserTypes,
or None for a normal user.
a custom value set in the configuration file, or None for a normal
user.
shadow_banned: Whether the user is shadow-banned, i.e. they may be
told their requests succeeded but we ignore them.
approved: Whether to consider the user has already been approved by an
Expand Down
35 changes: 35 additions & 0 deletions tests/handlers/test_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,41 @@ def test_spam_checker_receives_sso_type(self) -> None:
self.handler.register_user(localpart="bobflimflob", auth_provider_id="saml")
)

def test_register_default_user_type(self) -> None:
"""Test that the default user type is none when registering a user."""
user_id = self.get_success(self.handler.register_user(localpart="user"))
user_info = self.get_success(self.store.get_user_by_id(user_id))
assert user_info is not None
self.assertEqual(user_info.user_type, None)

def test_register_extra_user_types_valid(self) -> None:
"""
Test that the specified user type is set correctly when registering a user.
n.b. No validation is done on the user type, so this test
is only to ensure that the user type can be set to any value.
"""
user_id = self.get_success(
self.handler.register_user(localpart="user", user_type="anyvalue")
)
user_info = self.get_success(self.store.get_user_by_id(user_id))
assert user_info is not None
self.assertEqual(user_info.user_type, "anyvalue")

@override_config(
{
"user_types": {
"extra_user_types": ["extra1", "extra2"],
"default_user_type": "extra1",
}
}
)
def test_register_extra_user_types_with_default(self) -> None:
"""Test that the default_user_type in config is set correctly when registering a user."""
user_id = self.get_success(self.handler.register_user(localpart="user"))
user_info = self.get_success(self.store.get_user_by_id(user_id))
assert user_info is not None
self.assertEqual(user_info.user_type, "extra1")

async def get_or_create_user(
self,
requester: Requester,
Expand Down
Loading
Loading