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

Commit

Permalink
Allow admins to require a manual approval process before new accounts…
Browse files Browse the repository at this point in the history
… can be used (using MSC3866) (#13556)
  • Loading branch information
babolivier authored Sep 29, 2022
1 parent 8625ad8 commit be76cd8
Show file tree
Hide file tree
Showing 21 changed files with 731 additions and 34 deletions.
1 change: 1 addition & 0 deletions changelog.d/13556.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow server admins to require a manual approval process before new accounts can be used (using [MSC3866](https://github.com/matrix-org/matrix-spec-proposals/pull/3866)).
2 changes: 1 addition & 1 deletion synapse/_scripts/synapse_port_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"redactions": ["have_censored"],
"room_stats_state": ["is_federatable"],
"local_media_repository": ["safe_from_quarantine"],
"users": ["shadow_banned"],
"users": ["shadow_banned", "approved"],
"e2e_fallback_keys_json": ["used"],
"access_tokens": ["used"],
"device_lists_changes_in_room": ["converted_to_destinations"],
Expand Down
11 changes: 11 additions & 0 deletions synapse/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,14 @@ class PublicRoomsFilterFields:

GENERIC_SEARCH_TERM: Final = "generic_search_term"
ROOM_TYPES: Final = "room_types"


class ApprovalNoticeMedium:
"""Identifier for the medium this server will use to serve notice of approval for a
specific user's registration.
As defined in https://github.com/matrix-org/matrix-spec-proposals/blob/babolivier/m_not_approved/proposals/3866-user-not-approved-error.md
"""

NONE = "org.matrix.msc3866.none"
EMAIL = "org.matrix.msc3866.email"
16 changes: 16 additions & 0 deletions synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ class Codes(str, Enum):
# Part of MSC3895.
UNABLE_DUE_TO_PARTIAL_STATE = "ORG.MATRIX.MSC3895_UNABLE_DUE_TO_PARTIAL_STATE"

USER_AWAITING_APPROVAL = "ORG.MATRIX.MSC3866_USER_AWAITING_APPROVAL"


class CodeMessageException(RuntimeError):
"""An exception with integer code and message string attributes.
Expand Down Expand Up @@ -566,6 +568,20 @@ def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
return cs_error(self.msg, self.errcode, **extra)


class NotApprovedError(SynapseError):
def __init__(
self,
msg: str,
approval_notice_medium: str,
):
super().__init__(
code=403,
msg=msg,
errcode=Codes.USER_AWAITING_APPROVAL,
additional_fields={"approval_notice_medium": approval_notice_medium},
)


def cs_error(msg: str, code: str = Codes.UNKNOWN, **kwargs: Any) -> "JsonDict":
"""Utility method for constructing an error response for client-server
interactions.
Expand Down
19 changes: 19 additions & 0 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,25 @@

from typing import Any

import attr

from synapse.config._base import Config
from synapse.types import JsonDict


@attr.s(auto_attribs=True, frozen=True, slots=True)
class MSC3866Config:
"""Configuration for MSC3866 (mandating approval for new users)"""

# Whether the base support for the approval process is enabled. This includes the
# ability for administrators to check and update the approval of users, even if no
# approval is currently required.
enabled: bool = False
# Whether to require that new users are approved by an admin before their account
# can be used. Note that this setting is ignored if 'enabled' is false.
require_approval_for_new_accounts: bool = False


class ExperimentalConfig(Config):
"""Config section for enabling experimental features"""

Expand Down Expand Up @@ -97,6 +112,10 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
# MSC3852: Expose last seen user agent field on /_matrix/client/v3/devices.
self.msc3852_enabled: bool = experimental.get("msc3852_enabled", False)

# MSC3866: M_USER_AWAITING_APPROVAL error code
raw_msc3866_config = experimental.get("msc3866", {})
self.msc3866 = MSC3866Config(**raw_msc3866_config)

# MSC3881: Remotely toggle push notifications for another client
self.msc3881_enabled: bool = experimental.get("msc3881_enabled", False)

Expand Down
5 changes: 5 additions & 0 deletions synapse/handlers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def __init__(self, hs: "HomeServer"):
self.store = hs.get_datastores().main
self._storage_controllers = hs.get_storage_controllers()
self._state_storage_controller = self._storage_controllers.state
self._msc3866_enabled = hs.config.experimental.msc3866.enabled

async def get_whois(self, user: UserID) -> JsonDict:
connections = []
Expand Down Expand Up @@ -75,6 +76,10 @@ async def get_user(self, user: UserID) -> Optional[JsonDict]:
"is_guest",
}

if self._msc3866_enabled:
# Only include the approved flag if support for MSC3866 is enabled.
user_info_to_return.add("approved")

# Restrict returned keys to a known set.
user_info_dict = {
key: value
Expand Down
11 changes: 11 additions & 0 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,17 @@ async def check_user_exists(self, user_id: str) -> Optional[str]:
return res[0]
return None

async def is_user_approved(self, user_id: str) -> bool:
"""Checks if a user is approved and therefore can be allowed to log in.
Args:
user_id: the user to check the approval status of.
Returns:
A boolean that is True if the user is approved, False otherwise.
"""
return await self.store.is_user_approved(user_id)

async def _find_user_id_and_pwd_hash(
self, user_id: str
) -> Optional[Tuple[str, str]]:
Expand Down
8 changes: 8 additions & 0 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ async def register_user(
by_admin: bool = False,
user_agent_ips: Optional[List[Tuple[str, str]]] = None,
auth_provider_id: Optional[str] = None,
approved: bool = False,
) -> str:
"""Registers a new client on the server.
Expand All @@ -246,6 +247,8 @@ async def register_user(
user_agent_ips: Tuples of user-agents and IP addresses used
during the registration process.
auth_provider_id: The SSO IdP the user used, if any.
approved: True if the new user should be considered already
approved by an administrator.
Returns:
The registered user_id.
Raises:
Expand Down Expand Up @@ -307,6 +310,7 @@ async def register_user(
user_type=user_type,
address=address,
shadow_banned=shadow_banned,
approved=approved,
)

profile = await self.store.get_profileinfo(localpart)
Expand Down Expand Up @@ -695,6 +699,7 @@ async def register_with_store(
user_type: Optional[str] = None,
address: Optional[str] = None,
shadow_banned: bool = False,
approved: bool = False,
) -> None:
"""Register user in the datastore.
Expand All @@ -713,6 +718,7 @@ async def register_with_store(
api.constants.UserTypes, or None for a normal user.
address: the IP address used to perform the registration.
shadow_banned: Whether to shadow-ban the user
approved: Whether to mark the user as approved by an administrator
"""
if self.hs.config.worker.worker_app:
await self._register_client(
Expand All @@ -726,6 +732,7 @@ async def register_with_store(
user_type=user_type,
address=address,
shadow_banned=shadow_banned,
approved=approved,
)
else:
await self.store.register_user(
Expand All @@ -738,6 +745,7 @@ async def register_with_store(
admin=admin,
user_type=user_type,
shadow_banned=shadow_banned,
approved=approved,
)

# Only call the account validity module(s) on the main process, to avoid
Expand Down
5 changes: 5 additions & 0 deletions synapse/replication/http/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ async def _serialize_payload( # type: ignore[override]
user_type: Optional[str],
address: Optional[str],
shadow_banned: bool,
approved: bool,
) -> JsonDict:
"""
Args:
Expand All @@ -68,6 +69,8 @@ async def _serialize_payload( # type: ignore[override]
or None for a normal user.
address: the IP address used to perform the regitration.
shadow_banned: Whether to shadow-ban the user
approved: Whether the user should be considered already approved by an
administrator.
"""
return {
"password_hash": password_hash,
Expand All @@ -79,6 +82,7 @@ async def _serialize_payload( # type: ignore[override]
"user_type": user_type,
"address": address,
"shadow_banned": shadow_banned,
"approved": approved,
}

async def _handle_request( # type: ignore[override]
Expand All @@ -99,6 +103,7 @@ async def _handle_request( # type: ignore[override]
user_type=content["user_type"],
address=content["address"],
shadow_banned=content["shadow_banned"],
approved=content["approved"],
)

return 200, {}
Expand Down
43 changes: 42 additions & 1 deletion synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def __init__(self, hs: "HomeServer"):
self.store = hs.get_datastores().main
self.auth = hs.get_auth()
self.admin_handler = hs.get_admin_handler()
self._msc3866_enabled = hs.config.experimental.msc3866.enabled

async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
await assert_requester_is_admin(self.auth, request)
Expand All @@ -95,6 +96,13 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
guests = parse_boolean(request, "guests", default=True)
deactivated = parse_boolean(request, "deactivated", default=False)

# If support for MSC3866 is not enabled, apply no filtering based on the
# `approved` column.
if self._msc3866_enabled:
approved = parse_boolean(request, "approved", default=True)
else:
approved = True

order_by = parse_string(
request,
"order_by",
Expand All @@ -115,8 +123,22 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
direction = parse_string(request, "dir", default="f", allowed_values=("f", "b"))

users, total = await self.store.get_users_paginate(
start, limit, user_id, name, guests, deactivated, order_by, direction
start,
limit,
user_id,
name,
guests,
deactivated,
order_by,
direction,
approved,
)

# If support for MSC3866 is not enabled, don't show the approval flag.
if not self._msc3866_enabled:
for user in users:
del user["approved"]

ret = {"users": users, "total": total}
if (start + limit) < total:
ret["next_token"] = str(start + len(users))
Expand Down Expand Up @@ -163,6 +185,7 @@ def __init__(self, hs: "HomeServer"):
self.deactivate_account_handler = hs.get_deactivate_account_handler()
self.registration_handler = hs.get_registration_handler()
self.pusher_pool = hs.get_pusherpool()
self._msc3866_enabled = hs.config.experimental.msc3866.enabled

async def on_GET(
self, request: SynapseRequest, user_id: str
Expand Down Expand Up @@ -239,6 +262,15 @@ async def on_PUT(
HTTPStatus.BAD_REQUEST, "'deactivated' parameter is not of type boolean"
)

approved: Optional[bool] = None
if "approved" in body and self._msc3866_enabled:
approved = body["approved"]
if not isinstance(approved, bool):
raise SynapseError(
HTTPStatus.BAD_REQUEST,
"'approved' parameter is not of type boolean",
)

# convert List[Dict[str, str]] into List[Tuple[str, str]]
if external_ids is not None:
new_external_ids = [
Expand Down Expand Up @@ -343,6 +375,9 @@ async def on_PUT(
if "user_type" in body:
await self.store.set_user_type(target_user, user_type)

if approved is not None:
await self.store.update_user_approval_status(target_user, approved)

user = await self.admin_handler.get_user(target_user)
assert user is not None

Expand All @@ -355,13 +390,18 @@ async def on_PUT(
if password is not None:
password_hash = await self.auth_handler.hash(password)

new_user_approved = True
if self._msc3866_enabled and approved is not None:
new_user_approved = approved

user_id = await self.registration_handler.register_user(
localpart=target_user.localpart,
password_hash=password_hash,
admin=set_admin_to,
default_display_name=displayname,
user_type=user_type,
by_admin=True,
approved=new_user_approved,
)

if threepids is not None:
Expand Down Expand Up @@ -550,6 +590,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
user_type=user_type,
default_display_name=displayname,
by_admin=True,
approved=True,
)

result = await register._create_registration_details(user_id, body)
Expand Down
Loading

0 comments on commit be76cd8

Please sign in to comment.