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
Changes from 1 commit
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
202 changes: 195 additions & 7 deletions synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,36 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import email.utils
import logging
from typing import TYPE_CHECKING, Any, Generator, Iterable, List, Optional, Tuple
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Generator,
Iterable,
List,
Optional,
Tuple,
)

import jinja2

from twisted.internet import defer
from twisted.web.server import Request

from synapse.events import EventBase
from synapse.http.client import SimpleHttpClient
from synapse.http.site import SynapseRequest
from synapse.logging.context import make_deferred_yieldable, run_in_background
from synapse.metrics.background_process_metrics import wrap_as_background_process
from synapse.storage.databases.main.roommember import ProfileInfo
from synapse.storage.state import StateFilter
from synapse.types import JsonDict, UserID, create_requester
from synapse.util import Clock

if TYPE_CHECKING:
from synapse.server import HomeServer
Expand All @@ -46,11 +65,22 @@ def __init__(self, hs, auth_handler):
self._hs = hs

self._store = hs.get_datastore()
self._auth = hs.get_auth()
self._auth_handler = auth_handler
self._server_name = hs.hostname
self._presence_stream = hs.get_event_sources().sources["presence"]
self._state = hs.get_state_handler()
self._sendmail = hs.get_sendmail()
self._clock = hs.get_clock() # type: Clock

try:
app_name = self._hs.config.email_app_name

self._from_string = self._hs.config.email_notif_from % {"app": app_name}
except Exception:
# If substitution failed, fall back to the bare strings.
babolivier marked this conversation as resolved.
Show resolved Hide resolved
self._from_string = self._hs.config.email_notif_from

self._raw_from = email.utils.parseaddr(self._from_string)[1]

# We expose these as properties below in order to attach a helpful docstring.
self._http_client = hs.get_simple_http_client() # type: SimpleHttpClient
Expand Down Expand Up @@ -81,22 +111,42 @@ def public_room_list_manager(self):
"""
return self._public_room_list_manager

def get_user_by_req(self, req, allow_guest=False):
@property
def public_baseurl(self):
babolivier marked this conversation as resolved.
Show resolved Hide resolved
"""Allow accessing the configured public base URL for this homeserver."""
babolivier marked this conversation as resolved.
Show resolved Hide resolved
return self._hs.config.public_baseurl

@property
def email_app_name(self):
babolivier marked this conversation as resolved.
Show resolved Hide resolved
"""Allow accessing the application name configured in the homeserver's
configuration.
babolivier marked this conversation as resolved.
Show resolved Hide resolved
"""
return self._hs.config.email_app_name

def get_user_by_req(
babolivier marked this conversation as resolved.
Show resolved Hide resolved
self,
req: Request,
allow_guest: bool = False,
allow_expired: bool = False,
):
"""Check the access_token provided for a request

Args:
req (twisted.web.server.Request): Incoming HTTP request
allow_guest (bool): True if guest users should be allowed. If this
req: Incoming HTTP request
allow_guest: True if guest users should be allowed. If this
is False, and the access token is for a guest user, an
AuthError will be thrown
allow_expired: True if expired users should be allowed. If this
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
Raises:
synapse.api.errors.AuthError: if no user by that token exists,
or the token is invalid.
"""
return self._auth.get_user_by_req(req, allow_guest)
return self._hs.get_auth().get_user_by_req(req, allow_guest, allow_expired)

def get_qualified_user_id(self, username):
"""Qualify a user id, if necessary
Expand Down Expand Up @@ -252,7 +302,7 @@ def invalidate_access_token(self, access_token):
"""
# see if the access token corresponds to a device
user_info = yield defer.ensureDeferred(
self._auth.get_user_by_access_token(access_token)
self._hs.get_auth().get_user_by_access_token(access_token)
babolivier marked this conversation as resolved.
Show resolved Hide resolved
)
device_id = user_info.get("device_id")
user_id = user_info["user"].to_string()
Expand Down Expand Up @@ -439,6 +489,144 @@ async def send_local_online_presence_to(self, users: Iterable[str]) -> None:
presence_events
)

def background_call_async(self, f: Callable, *args, **kwargs):
"""Wraps an async function as a background process and runs it.

Args:
f(function): The function to call.
*args: Positional arguments to pass to function.
**kwargs: Key arguments to pass to function.

"""

@wrap_as_background_process(f.__name__)
async def background_call(*args, **kwargs):
await f(*args, **kwargs)

if self._hs.config.run_background_tasks:
self._clock.call_later(0.0, background_call, *args, **kwargs)
else:
logger.warning(
"Not running looping call %s as the configuration forbids it",
babolivier marked this conversation as resolved.
Show resolved Hide resolved
f,
)
babolivier marked this conversation as resolved.
Show resolved Hide resolved

def looping_background_call_async(self, f: Callable, msec: float, *args, **kwargs):
"""Wraps an async function as a background process and calls it repeatedly.

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

Args:
f(function): The function to call repeatedly.
msec(float): How long to wait between calls in milliseconds.
*args: Positional arguments to pass to function.
**kwargs: Key arguments to pass to function.
"""

@wrap_as_background_process(f.__name__)
async def background_call(*args, **kwargs):
await f(*args, **kwargs)

if self._hs.config.run_background_tasks:
self._clock.looping_call(background_call, msec, *args, **kwargs)
else:
logger.warning(
"Not running looping call %s as the configuration forbids it",
f,
)

async def send_mail(
self,
recipient: str,
subject: str,
html: str,
text: str,
):
"""Send an email on behalf of the homeserver.

Args:
recipient: The email address for the recipient.
subject: The email's subject.
html: The email's HTML content.
text: The email's text content.
"""
raw_to = email.utils.parseaddr(recipient)[1]

multipart_msg = MIMEMultipart("alternative")
multipart_msg["Subject"] = subject
multipart_msg["From"] = self._from_string
multipart_msg["To"] = recipient
multipart_msg["Date"] = email.utils.formatdate()
multipart_msg["Message-ID"] = email.utils.make_msgid()
multipart_msg.attach(MIMEText(html, "html", "utf8"))
multipart_msg.attach(MIMEText(text, "plain", "utf8"))

logger.info("Sending email to %s", recipient)

await make_deferred_yieldable(
self._sendmail(
self._hs.config.email_smtp_host,
self._raw_from,
raw_to,
multipart_msg.as_string().encode("utf8"),
reactor=self._hs.get_reactor(),
port=self._hs.config.email_smtp_port,
requireAuthentication=self._hs.config.email_smtp_user is not None,
username=self._hs.config.email_smtp_user,
password=self._hs.config.email_smtp_pass,
requireTransportSecurity=self._hs.config.require_transport_security,
)
babolivier marked this conversation as resolved.
Show resolved Hide resolved
)

def read_templates(
self,
filenames: List[str],
custom_template_directory: Optional[str] = None,
) -> List[jinja2.Template]:
"""Read and load the content of the template files at the given location.
By default, Synapse will look for these templates in its configured template
directory, but another directory to search in can be provided.

Args:
filenames: The name of the template files to look for.
custom_template_directory: An additional directory to look for the files in.

Returns:
A list containing the loaded templates, with the orders matching the one of
the filenames parameter.
"""
return self._hs.config.read_templates(filenames, custom_template_directory)

async def get_profile_for_user(self, localpart: str) -> ProfileInfo:
"""Lookup the profile info for the user with the given localpart.
babolivier marked this conversation as resolved.
Show resolved Hide resolved

Args:
localpart: The localpart to lookup profile information for.
babolivier marked this conversation as resolved.
Show resolved Hide resolved

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]]:
"""Lookup the threepids (email addresses and phone numbers) associated with the
given Matrix user ID.

Args:
user_id: The Matrix user ID to lookup 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 current_time_ms(self) -> int:
"""Get the current time in milliseconds."""
return self._clock.time_msec()
babolivier marked this conversation as resolved.
Show resolved Hide resolved


class PublicRoomListManager:
"""Contains methods for adding to, removing from and querying whether a room
Expand Down