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

Add a module type for account validity #9884

Merged
merged 55 commits into from
Jul 16, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
2d71db2
Add additional required capabilities to the module API
babolivier Apr 26, 2021
163ca38
Change the account validity configuration
babolivier Apr 26, 2021
2dd3a35
Add an API for account validity modules
babolivier Apr 26, 2021
850b303
Plug the new account validity APIs onto the right places
babolivier Apr 26, 2021
f8c8ca8
Changelog
babolivier Apr 26, 2021
f492ef3
Allow modules to query whether the user is a server admin
babolivier Apr 26, 2021
5596cfe
The module API already exposes run_in_background which makes backgrou…
babolivier Apr 26, 2021
456a06c
Fix user authentication in the module API
babolivier Apr 27, 2021
94706a9
Add a hook for the legacy admin API
babolivier Apr 27, 2021
576b9f3
Fix comment with right URL path
babolivier Apr 27, 2021
907b655
Add a deprecation notice to the upgrade notes
babolivier Apr 27, 2021
9a9e83c
Lint
babolivier Apr 27, 2021
588fa41
Lint
babolivier Apr 27, 2021
13a72ee
Mention the module in the config warning (+typo)
babolivier Apr 27, 2021
fe2e8ca
Incorporate part of the review
babolivier May 7, 2021
2147f06
Split multiplart email sending into a dedicated handler
babolivier May 13, 2021
1df0c8c
Changelog
babolivier May 13, 2021
a26185a
Lint
babolivier May 13, 2021
7b59ea5
Merge branch 'babolivier/send_mail' into babolivier/account_validity_…
babolivier May 13, 2021
76aed9e
Incorporate review
babolivier May 13, 2021
efc82b7
Fix typo in changelog file
babolivier May 13, 2021
1df5d74
Incorporate review
babolivier May 13, 2021
0d7cb16
Typo
babolivier May 13, 2021
a36be02
Merge branch 'babolivier/send_mail' into babolivier/account_validity_…
babolivier May 13, 2021
7b553e6
Expose more things needed by the email account validity module on the…
babolivier May 14, 2021
d0a1cd5
Update docs + lint
babolivier May 19, 2021
602d2ce
Get the email app name from the email config
babolivier May 20, 2021
df82691
Merge branch 'develop' into babolivier/account_validity_plugin
babolivier May 20, 2021
14885d3
Fix types
babolivier May 20, 2021
3108884
Fix imports
babolivier May 20, 2021
d0d8a5b
Merge branch 'develop' into babolivier/account_validity_plugin
babolivier Jun 28, 2021
c52921e
Move the account validity module interface to the new system
babolivier Jul 1, 2021
c964099
Revert changes account validity config since the module config lives …
babolivier Jul 2, 2021
a6346e9
Remove deprecation warning
babolivier Jul 2, 2021
51b5233
Sample config
babolivier Jul 2, 2021
c2185cf
Incorporate review comments
babolivier Jul 2, 2021
a0ca661
Fix type of on_user_registration callbacks
babolivier Jul 2, 2021
94ca9a7
Fix types
babolivier Jul 2, 2021
34dc6ea
Restore the possibility for is_user_expired to return None
babolivier Jul 2, 2021
49c4582
Document the account validity callbacks
babolivier Jul 2, 2021
005725d
Fix tests
babolivier Jul 2, 2021
f8754fe
Document why we need legacy hooks
babolivier Jul 5, 2021
58bb7b1
Apply suggestions from code review
babolivier Jul 7, 2021
4fb4b49
Incorporate part of the review
babolivier Jul 7, 2021
b45b45d
Use the account validity handler in the pusher pool
babolivier Jul 7, 2021
972091d
Lint
babolivier Jul 7, 2021
07aa404
Fix changelog
babolivier Jul 7, 2021
ba4e069
Improve docs
babolivier Jul 8, 2021
d9ee0f9
Line break
babolivier Jul 9, 2021
c2b6689
Incorporate review
babolivier Jul 14, 2021
a4adb3d
Lint
babolivier Jul 14, 2021
c3debd5
Apply suggestions from code review
babolivier Jul 16, 2021
e87c3cb
Incorporate review
babolivier Jul 16, 2021
01b6bf3
Merge branch 'babolivier/account_validity_plugin' of github.com:matri…
babolivier Jul 16, 2021
1a8af46
Lint
babolivier Jul 16, 2021
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/9884.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a module type for the account validity feature.
1 change: 0 additions & 1 deletion changelog.d/9884.removal

This file was deleted.

2 changes: 1 addition & 1 deletion docs/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ and is under no obligation to implement all callbacks from the categories it reg
callbacks for.

Modules can register callbacks using one of the module API's `register_[...]_callbacks`
methods. The callbacks functions are passed to these methods as keyword arguments, with
methods. The callback functions are passed to these methods as keyword arguments, with
the callback name as the argument name and the function as its value. This is demonstrated
in the example below. A `register_[...]_callbacks` method exists for each module type
documented in this section.
Expand Down
3 changes: 0 additions & 3 deletions synapse/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,6 @@ def __init__(self, hs: "HomeServer"):

self._auth_blocking = AuthBlocking(self.hs)

self._account_validity_enabled = (
hs.config.account_validity.account_validity_enabled
)
self._track_appservice_user_ips = hs.config.track_appservice_user_ips
self._macaroon_secret_key = hs.config.macaroon_secret_key
self._force_tracing_for_users = hs.config.tracing.force_tracing_for_users
Expand Down
15 changes: 15 additions & 0 deletions synapse/config/account_validity.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ class AccountValidityConfig(Config):
section = "account_validity"

def read_config(self, config, **kwargs):
"""Parses the old account validity config. The config format looks like this:

account_validity:
enabled: true
period: 6w
renew_at: 1w
renew_email_subject: "Renew your %(app)s account"
template_dir: "res/templates"
account_renewed_html_path: "account_renewed.html"
invalid_token_html_path: "invalid_token.html"

We expect admins to use modules for this feature (which is why it doesn't appear
in the sample config file), but we want to keep support for it around for a bit
for backwards compatibility.
"""
account_validity_config = config.get("account_validity") or {}
self.account_validity_enabled = account_validity_config.get("enabled", False)
self.account_validity_renew_by_email_enabled = (
Expand Down
39 changes: 24 additions & 15 deletions synapse/handlers/account_validity.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@

logger = logging.getLogger(__name__)

# Types for callbacks to be registered via the module api
IS_USER_EXPIRED_CALLBACK = Callable[[str], Awaitable[Optional[bool]]]
ON_USER_REGISTRATION_CALLBACK = Callable[[str], Awaitable]
# Legacy callbacks to allow for a smoother transition between the legacy native account
# validity feature and synapse-email-account-validity.
# Temporary hooks to allow for a transition from `/_matrix/client` endpoints
# to `/_synapse/client/account_validity`. See `register_account_validity_callbacks`.
ON_LEGACY_SEND_MAIL_CALLBACK = Callable[[str], Awaitable]
ON_LEGACY_RENEW_CALLBACK = Callable[[str], Awaitable[Tuple[bool, bool, int]]]
ON_LEGACY_ADMIN_REQUEST = Callable[[Request], Awaitable]
Expand All @@ -48,8 +49,6 @@ def __init__(self, hs: "HomeServer"):

self._app_name = self.hs.config.email_app_name

self._app_name = self.hs.config.email_app_name

self._account_validity_enabled = (
hs.config.account_validity.account_validity_enabled
)
Expand Down Expand Up @@ -108,6 +107,19 @@ def register_account_validity_callbacks(
if on_user_registration is not None:
self._on_user_registration_callbacks.append(on_user_registration)

# The builtin account validity feature exposes 3 endpoints (send_mail, renew, and
# an admin one). As part of moving the feature into a module, we need to change
# the path from /_matrix/client/unstable/account_validity/... to
# /_synapse/client/account_validity, because:
babolivier marked this conversation as resolved.
Show resolved Hide resolved
# * the feature isn't part of the Matrix spec thus shouldn't live under /_matrix
# * because of the way we register servlets modules can't register resources
babolivier marked this conversation as resolved.
Show resolved Hide resolved
# under /_matrix/client
babolivier marked this conversation as resolved.
Show resolved Hide resolved
# We need to allow for a transition period between the old and new endpoints
# in order to allow for clients to update (and for emails to be processed).
# Therefore we need to allow modules (in practice just the one implementing the
babolivier marked this conversation as resolved.
Show resolved Hide resolved
# email-based account validity) to temporarily hook into the legacy endpoint so we
babolivier marked this conversation as resolved.
Show resolved Hide resolved
# can route the traffic coming into the old endpoints into the module, which is
# why we have the following three temporary hooks.
richvdh marked this conversation as resolved.
Show resolved Hide resolved
if on_legacy_send_mail is not None:
if self._on_legacy_send_mail_callback is not None:
raise RuntimeError("Tried to register on_legacy_send_mail twice")
Expand Down Expand Up @@ -135,17 +147,14 @@ async def is_user_expired(self, user_id: str) -> bool:
Returns:
Whether the user has expired.
"""
if self._is_user_expired_callbacks:
# If we have at least one module implementing this callback, ignore the
# legacy configuration and use the module(s) to determine if the user has
# expired.
for callback in self._is_user_expired_callbacks:
expired = await callback(user_id)
if expired is not None:
return expired
elif self._account_validity_enabled:
# Otherwise, if the legacy configuration is enabled, use it to figure out if
# the user has expired.
for callback in self._is_user_expired_callbacks:
expired = await callback(user_id)
if expired is not None:
return expired

if self._account_validity_enabled:
# If no module could determine whether the user has expired and the legacy
# configuration is enabled, fall back to it.
return await self.store.is_account_expired(user_id, self.clock.time_msec())

return False
Expand Down
82 changes: 43 additions & 39 deletions synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,18 +159,16 @@ def public_room_list_manager(self):
return self._public_room_list_manager

@property
def public_baseurl(self):
"""Allow accessing the configured public base URL for this homeserver."""
def public_baseurl(self) -> str:
"""The configured public base URL for this homeserver."""
return self._hs.config.public_baseurl

@property
def email_app_name(self):
"""Allow accessing the application name configured in the homeserver's
configuration.
"""
def email_app_name(self) -> str:
"""The application name configured in the homeserver's configuration."""
return self._hs.config.email.email_app_name

def get_user_by_req(
async def get_user_by_req(
babolivier marked this conversation as resolved.
Show resolved Hide resolved
self,
req: SynapseRequest,
allow_guest: bool = False,
Expand All @@ -187,13 +185,16 @@ def get_user_by_req(
is False, and the access token is for an expired user, an
AuthError will be thrown
Returns:
twisted.internet.defer.Deferred[synapse.types.Requester]:
the requester for this request
synapse.types.Requester: the requester for this request
Raises:
synapse.api.errors.AuthError: if no user by that token exists,
InvalidClientCredentialsError: if no user by that token exists,
or the token is invalid.
"""
return self._auth.get_user_by_req(req, allow_guest, allow_expired=allow_expired)
return await self._auth.get_user_by_req(
req,
allow_guest,
allow_expired=allow_expired,
)

async def is_user_admin(self, user_id: str) -> bool:
"""Checks if a user is a server admin.
Expand Down Expand Up @@ -222,6 +223,32 @@ def get_qualified_user_id(self, username):
return username
return UserID(username, self._hs.hostname).to_string()

async def get_profile_for_user(self, localpart: str) -> ProfileInfo:
"""Look up the profile info for the user with the given localpart.

Args:
localpart: The localpart to look up profile information for.

Returns:
The profile information (i.e. display name and avatar URL).
"""
return await self._store.get_profileinfo(localpart)

async def get_threepids_for_user(self, user_id: str) -> List[Dict[str, str]]:
"""Look up the threepids (email addresses and phone numbers) associated with the
given Matrix user ID.

Args:
user_id: The Matrix user ID to look up threepids for.

Returns:
A list of threepids, each threepid being represented by a dictionary
containing a "medium" key which value is "email" for email addresses and
"msisdn" for phone numbers, and an "address" key which value is the
threepid's address.
"""
return await self._store.user_get_threepids(user_id)

def check_user_exists(self, user_id):
"""Check if user exists.

Expand Down Expand Up @@ -556,16 +583,19 @@ def looping_background_call(
self,
f: Callable,
msec: float,
desc: Optional[str] = None,
*args,
desc: Optional[str] = None,
**kwargs,
):
"""Wraps a function as a background process and calls it repeatedly.

Waits `msec` initially before calling `f` for the first time.

Args:
f: The function to call repeatedly.
f: The function to call repeatedly. f can be either synchronous or
asynchronous, and must follow Synapse's logcontext rules.
More info about logcontexts is available at
https://matrix-org.github.io/synapse/latest/log_contexts.html
msec: How long to wait between calls in milliseconds.
desc: The background task's description. Default to the function's name.
*args: Positional arguments to pass to function.
babolivier marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -631,32 +661,6 @@ def read_templates(
"""
return self._hs.config.read_templates(filenames, custom_template_directory)

async def get_profile_for_user(self, localpart: str) -> ProfileInfo:
"""Look up the profile info for the user with the given localpart.

Args:
localpart: The localpart to look up profile information for.

Returns:
The profile information (i.e. display name and avatar URL).
"""
return await self._store.get_profileinfo(localpart)

async def get_threepids_for_user(self, user_id: str) -> List[Dict[str, str]]:
"""Look up the threepids (email addresses and phone numbers) associated with the
given Matrix user ID.

Args:
user_id: The Matrix user ID to look up threepids for.

Returns:
A list of threepids, each threepid being represented by a dictionary
containing a "medium" key which value is "email" for email addresses and
"msisdn" for phone numbers, and an "address" key which value is the
threepid's address.
"""
return await self._store.user_get_threepids(user_id)


class PublicRoomListManager:
"""Contains methods for adding to, removing from and querying whether a room
Expand Down
24 changes: 8 additions & 16 deletions synapse/push/pusherpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,6 @@ def __init__(self, hs: "HomeServer"):
self.store = self.hs.get_datastore()
self.clock = self.hs.get_clock()

self._account_validity_enabled = (
hs.config.account_validity.account_validity_enabled
)

# We shard the handling of push notifications by user ID.
self._pusher_shard_config = hs.config.push.pusher_shard_config
self._instance_name = hs.get_instance_name()
Expand All @@ -89,6 +85,8 @@ def __init__(self, hs: "HomeServer"):
# map from user id to app_id:pushkey to pusher
self.pushers = {} # type: Dict[str, Dict[str, Pusher]]

self._account_validity_handler = hs.get_account_validity_handler()

def start(self) -> None:
"""Starts the pushers off in a background process."""
if not self._should_start_pushers:
Expand Down Expand Up @@ -238,12 +236,9 @@ async def _on_new_notifications(self, max_token: RoomStreamToken) -> None:

for u in users_affected:
# Don't push if the user account has expired
if self._account_validity_enabled:
expired = await self.store.is_account_expired(
u, self.clock.time_msec()
)
if expired:
continue
expired = await self._account_validity_handler.is_user_expired(u)
if expired:
continue

if u in self.pushers:
for p in self.pushers[u].values():
Expand All @@ -268,12 +263,9 @@ async def on_new_receipts(

for u in users_affected:
# Don't push if the user account has expired
if self._account_validity_enabled:
expired = await self.store.is_account_expired(
u, self.clock.time_msec()
)
if expired:
continue
expired = await self._account_validity_handler.is_user_expired(u)
if expired:
continue

if u in self.pushers:
for p in self.pushers[u].values():
Expand Down