Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f98bc6512 | |||
| 50a63d5fda | |||
| 280bfb15ce | |||
| fff89c33d8 | |||
| 18c516698e | |||
| d86321300a | |||
| d336b51331 | |||
| 5f158ec039 | |||
| db0a50bc40 | |||
| 24aa0e0a5b | |||
| 4c17a87606 | |||
| d445b3ae57 | |||
| 59f15309ca | |||
| f369164761 | |||
| 6bb0357c94 | |||
| a83577d64f | |||
| 39e9839a04 | |||
| b4f5416dd9 | |||
| eadb13d2e9 | |||
| 7f0d8e4288 | |||
| 9ccea16d45 | |||
| c061d4f237 | |||
| d0b849c86d | |||
| 65dd5543f6 | |||
| 8ee69f299c |
@@ -179,6 +179,7 @@ steps:
|
||||
image: "matrixdotorg/sytest-synapse:py35"
|
||||
propagate-environment: true
|
||||
always-pull: true
|
||||
workdir: "/src"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: -1
|
||||
@@ -199,6 +200,7 @@ steps:
|
||||
image: "matrixdotorg/sytest-synapse:py35"
|
||||
propagate-environment: true
|
||||
always-pull: true
|
||||
workdir: "/src"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: -1
|
||||
@@ -220,6 +222,7 @@ steps:
|
||||
image: "matrixdotorg/sytest-synapse:py35"
|
||||
propagate-environment: true
|
||||
always-pull: true
|
||||
workdir: "/src"
|
||||
soft_fail: true
|
||||
retry:
|
||||
automatic:
|
||||
|
||||
+18
-11
@@ -30,11 +30,10 @@ use github's pull request workflow to review the contribution, and either ask
|
||||
you to make any refinements needed or merge it and make them ourselves. The
|
||||
changes will then land on master when we next do a release.
|
||||
|
||||
We use `CircleCI <https://circleci.com/gh/matrix-org>`_ and `Buildkite
|
||||
<https://buildkite.com/matrix-dot-org/synapse>`_ for continuous integration.
|
||||
Buildkite builds need to be authorised by a maintainer. If your change breaks
|
||||
the build, this will be shown in GitHub, so please keep an eye on the pull
|
||||
request for feedback.
|
||||
We use `Buildkite <https://buildkite.com/matrix-dot-org/synapse>`_ for
|
||||
continuous integration. Buildkite builds need to be authorised by a
|
||||
maintainer. If your change breaks the build, this will be shown in GitHub, so
|
||||
please keep an eye on the pull request for feedback.
|
||||
|
||||
To run unit tests in a local development environment, you can use:
|
||||
|
||||
@@ -70,13 +69,21 @@ All changes, even minor ones, need a corresponding changelog / newsfragment
|
||||
entry. These are managed by Towncrier
|
||||
(https://github.com/hawkowl/towncrier).
|
||||
|
||||
To create a changelog entry, make a new file in the ``changelog.d``
|
||||
file named in the format of ``PRnumber.type``. The type can be
|
||||
one of ``feature``, ``bugfix``, ``removal`` (also used for
|
||||
deprecations), or ``misc`` (for internal-only changes).
|
||||
To create a changelog entry, make a new file in the ``changelog.d`` file named
|
||||
in the format of ``PRnumber.type``. The type can be one of the following:
|
||||
|
||||
The content of the file is your changelog entry, which can contain Markdown
|
||||
formatting. The entry should end with a full stop ('.') for consistency.
|
||||
* ``feature``.
|
||||
* ``bugfix``.
|
||||
* ``docker`` (for updates to the Docker image).
|
||||
* ``doc`` (for updates to the documentation).
|
||||
* ``removal`` (also used for deprecations).
|
||||
* ``misc`` (for internal-only changes).
|
||||
|
||||
The content of the file is your changelog entry, which should be a short
|
||||
description of your change in the same style as the rest of our `changelog
|
||||
<https://github.com/matrix-org/synapse/blob/master/CHANGES.md>`_. The file can
|
||||
contain Markdown formatting, and should end with a full stop ('.') for
|
||||
consistency.
|
||||
|
||||
Adding credits to the changelog is encouraged, we value your
|
||||
contributions and would like to have you shouted out in the release notes!
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Add information about nginx normalisation to reverse_proxy.rst. Contributed by @skalarproduktraum - thanks!
|
||||
@@ -0,0 +1 @@
|
||||
Add ability to pull all locally stored events out of synapse that a particular user can see.
|
||||
@@ -0,0 +1 @@
|
||||
Base Docker image on a newer Alpine Linux version (3.8 -> 3.10).
|
||||
@@ -0,0 +1 @@
|
||||
Add missing space in default logging file format generated by the Docker image.
|
||||
@@ -0,0 +1 @@
|
||||
Add a mechanism for per-test homeserver configuration in the unit tests.
|
||||
@@ -0,0 +1 @@
|
||||
Implement `session_lifetime` configuration option, after which access tokens will expire.
|
||||
@@ -0,0 +1 @@
|
||||
Improvements to Postgres setup instructions. Contributed by @Lrizika - thanks!
|
||||
@@ -0,0 +1 @@
|
||||
Update the sytest BuildKite configuration to checkout Synapse in `/src`.
|
||||
@@ -0,0 +1 @@
|
||||
Add a `docker` type to the towncrier configuration.
|
||||
@@ -0,0 +1 @@
|
||||
Return "This account has been deactivated" when a deactivated user tries to login.
|
||||
@@ -0,0 +1 @@
|
||||
Use `M_USER_DEACTIVATED` instead of `M_UNKNOWN` for errcode when a deactivated user attempts to login.
|
||||
+2
-2
@@ -16,7 +16,7 @@ ARG PYTHON_VERSION=3.7
|
||||
###
|
||||
### Stage 0: builder
|
||||
###
|
||||
FROM docker.io/python:${PYTHON_VERSION}-alpine3.8 as builder
|
||||
FROM docker.io/python:${PYTHON_VERSION}-alpine3.10 as builder
|
||||
|
||||
# install the OS build deps
|
||||
|
||||
@@ -55,7 +55,7 @@ RUN pip install --prefix="/install" --no-warn-script-location \
|
||||
### Stage 1: runtime
|
||||
###
|
||||
|
||||
FROM docker.io/python:${PYTHON_VERSION}-alpine3.8
|
||||
FROM docker.io/python:${PYTHON_VERSION}-alpine3.10
|
||||
|
||||
# xmlsec is required for saml support
|
||||
RUN apk add --no-cache --virtual .runtime_deps \
|
||||
|
||||
@@ -2,7 +2,7 @@ version: 1
|
||||
|
||||
formatters:
|
||||
precise:
|
||||
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s- %(message)s'
|
||||
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
|
||||
|
||||
filters:
|
||||
context:
|
||||
|
||||
+15
-4
@@ -34,9 +34,14 @@ Assuming your PostgreSQL database user is called ``postgres``, create a user
|
||||
su - postgres
|
||||
createuser --pwprompt synapse_user
|
||||
|
||||
The PostgreSQL database used *must* have the correct encoding set, otherwise it
|
||||
would not be able to store UTF8 strings. To create a database with the correct
|
||||
encoding use, e.g.::
|
||||
Before you can authenticate with the ``synapse_user``, you must create a
|
||||
database that it can access. To create a database, first connect to the database
|
||||
with your database user::
|
||||
|
||||
su - postgres
|
||||
psql
|
||||
|
||||
and then run::
|
||||
|
||||
CREATE DATABASE synapse
|
||||
ENCODING 'UTF8'
|
||||
@@ -46,7 +51,13 @@ encoding use, e.g.::
|
||||
OWNER synapse_user;
|
||||
|
||||
This would create an appropriate database named ``synapse`` owned by the
|
||||
``synapse_user`` user (which must already exist).
|
||||
``synapse_user`` user (which must already have been created as above).
|
||||
|
||||
Note that the PostgreSQL database *must* have the correct encoding set (as
|
||||
shown above), otherwise it will not be able to store UTF8 strings.
|
||||
|
||||
You may need to enable password authentication so ``synapse_user`` can connect
|
||||
to the database. See https://www.postgresql.org/docs/11/auth-pg-hba-conf.html.
|
||||
|
||||
Tuning Postgres
|
||||
===============
|
||||
|
||||
@@ -48,6 +48,8 @@ Let's assume that we expect clients to connect to our server at
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
}
|
||||
}
|
||||
|
||||
Do not add a `/` after the port in `proxy_pass`, otherwise nginx will canonicalise/normalise the URI.
|
||||
|
||||
* Caddy::
|
||||
|
||||
|
||||
@@ -786,6 +786,17 @@ uploads_path: "DATADIR/uploads"
|
||||
# renew_at: 1w
|
||||
# renew_email_subject: "Renew your %(app)s account"
|
||||
|
||||
# Time that a user's session remains valid for, after they log in.
|
||||
#
|
||||
# Note that this is not currently compatible with guest logins.
|
||||
#
|
||||
# Note also that this is calculated at login time: changes are not applied
|
||||
# retrospectively to users who have already logged in.
|
||||
#
|
||||
# By default, this is infinite.
|
||||
#
|
||||
#session_lifetime: 24h
|
||||
|
||||
# The user must provide all of the below types of 3PID when registering.
|
||||
#
|
||||
#registrations_require_3pid:
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
name = "Bugfixes"
|
||||
showcontent = true
|
||||
|
||||
[[tool.towncrier.type]]
|
||||
directory = "docker"
|
||||
name = "Updates to the Docker image"
|
||||
showcontent = true
|
||||
|
||||
[[tool.towncrier.type]]
|
||||
directory = "doc"
|
||||
name = "Improved Documentation"
|
||||
|
||||
@@ -319,6 +319,17 @@ class Auth(object):
|
||||
# first look in the database
|
||||
r = yield self._look_up_user_by_access_token(token)
|
||||
if r:
|
||||
valid_until_ms = r["valid_until_ms"]
|
||||
if (
|
||||
valid_until_ms is not None
|
||||
and valid_until_ms < self.clock.time_msec()
|
||||
):
|
||||
# there was a valid access token, but it has expired.
|
||||
# soft-logout the user.
|
||||
raise InvalidClientTokenError(
|
||||
msg="Access token has expired", soft_logout=True
|
||||
)
|
||||
|
||||
defer.returnValue(r)
|
||||
|
||||
# otherwise it needs to be a valid macaroon
|
||||
@@ -505,6 +516,7 @@ class Auth(object):
|
||||
"token_id": ret.get("token_id", None),
|
||||
"is_guest": False,
|
||||
"device_id": ret.get("device_id"),
|
||||
"valid_until_ms": ret.get("valid_until_ms"),
|
||||
}
|
||||
defer.returnValue(user_info)
|
||||
|
||||
|
||||
+24
-1
@@ -61,6 +61,7 @@ class Codes(object):
|
||||
INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"
|
||||
WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
|
||||
EXPIRED_ACCOUNT = "ORG_MATRIX_EXPIRED_ACCOUNT"
|
||||
USER_DEACTIVATED = "M_USER_DEACTIVATED"
|
||||
|
||||
|
||||
class CodeMessageException(RuntimeError):
|
||||
@@ -139,6 +140,22 @@ class ConsentNotGivenError(SynapseError):
|
||||
return cs_error(self.msg, self.errcode, consent_uri=self._consent_uri)
|
||||
|
||||
|
||||
class UserDeactivatedError(SynapseError):
|
||||
"""The error returned to the client when the user attempted to access an
|
||||
authenticated endpoint, but the account has been deactivated.
|
||||
"""
|
||||
|
||||
def __init__(self, msg):
|
||||
"""Constructs a UserDeactivatedError
|
||||
|
||||
Args:
|
||||
msg (str): The human-readable error message
|
||||
"""
|
||||
super(UserDeactivatedError, self).__init__(
|
||||
code=http_client.FORBIDDEN, msg=msg, errcode=Codes.USER_DEACTIVATED
|
||||
)
|
||||
|
||||
|
||||
class RegistrationError(SynapseError):
|
||||
"""An error raised when a registration event fails."""
|
||||
|
||||
@@ -245,8 +262,14 @@ class MissingClientTokenError(InvalidClientCredentialsError):
|
||||
class InvalidClientTokenError(InvalidClientCredentialsError):
|
||||
"""Raised when we didn't understand the access token in a request"""
|
||||
|
||||
def __init__(self, msg="Unrecognised access token"):
|
||||
def __init__(self, msg="Unrecognised access token", soft_logout=False):
|
||||
super().__init__(msg=msg, errcode="M_UNKNOWN_TOKEN")
|
||||
self._soft_logout = soft_logout
|
||||
|
||||
def error_dict(self):
|
||||
d = super().error_dict()
|
||||
d["soft_logout"] = self._soft_logout
|
||||
return d
|
||||
|
||||
|
||||
class ResourceLimitError(SynapseError):
|
||||
|
||||
@@ -84,6 +84,11 @@ class RegistrationConfig(Config):
|
||||
"disable_msisdn_registration", False
|
||||
)
|
||||
|
||||
session_lifetime = config.get("session_lifetime")
|
||||
if session_lifetime is not None:
|
||||
session_lifetime = self.parse_duration(session_lifetime)
|
||||
self.session_lifetime = session_lifetime
|
||||
|
||||
def generate_config_section(self, generate_secrets=False, **kwargs):
|
||||
if generate_secrets:
|
||||
registration_shared_secret = 'registration_shared_secret: "%s"' % (
|
||||
@@ -141,6 +146,17 @@ class RegistrationConfig(Config):
|
||||
# renew_at: 1w
|
||||
# renew_email_subject: "Renew your %%(app)s account"
|
||||
|
||||
# Time that a user's session remains valid for, after they log in.
|
||||
#
|
||||
# Note that this is not currently compatible with guest logins.
|
||||
#
|
||||
# Note also that this is calculated at login time: changes are not applied
|
||||
# retrospectively to users who have already logged in.
|
||||
#
|
||||
# By default, this is infinite.
|
||||
#
|
||||
#session_lifetime: 24h
|
||||
|
||||
# The user must provide all of the below types of 3PID when registering.
|
||||
#
|
||||
#registrations_require_3pid:
|
||||
|
||||
@@ -17,6 +17,10 @@ import logging
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.types import RoomStreamToken
|
||||
from synapse.visibility import filter_events_for_client
|
||||
|
||||
from ._base import BaseHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -89,3 +93,182 @@ class AdminHandler(BaseHandler):
|
||||
ret = yield self.store.search_users(term)
|
||||
|
||||
defer.returnValue(ret)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def export_user_data(self, user_id, writer):
|
||||
"""Write all data we have on the user to the given writer.
|
||||
|
||||
Args:
|
||||
user_id (str)
|
||||
writer (ExfiltrationWriter)
|
||||
|
||||
Returns:
|
||||
defer.Deferred: Resolves when all data for a user has been written.
|
||||
The returned value is that returned by `writer.finished()`.
|
||||
"""
|
||||
# Get all rooms the user is in or has been in
|
||||
rooms = yield self.store.get_rooms_for_user_where_membership_is(
|
||||
user_id,
|
||||
membership_list=(
|
||||
Membership.JOIN,
|
||||
Membership.LEAVE,
|
||||
Membership.BAN,
|
||||
Membership.INVITE,
|
||||
),
|
||||
)
|
||||
|
||||
# We only try and fetch events for rooms the user has been in. If
|
||||
# they've been e.g. invited to a room without joining then we handle
|
||||
# those seperately.
|
||||
rooms_user_has_been_in = yield self.store.get_rooms_user_has_been_in(user_id)
|
||||
|
||||
for index, room in enumerate(rooms):
|
||||
room_id = room.room_id
|
||||
|
||||
logger.info(
|
||||
"[%s] Handling room %s, %d/%d", user_id, room_id, index + 1, len(rooms)
|
||||
)
|
||||
|
||||
forgotten = yield self.store.did_forget(user_id, room_id)
|
||||
if forgotten:
|
||||
logger.info("[%s] User forgot room %d, ignoring", user_id, room_id)
|
||||
continue
|
||||
|
||||
if room_id not in rooms_user_has_been_in:
|
||||
# If we haven't been in the rooms then the filtering code below
|
||||
# won't return anything, so we need to handle these cases
|
||||
# explicitly.
|
||||
|
||||
if room.membership == Membership.INVITE:
|
||||
event_id = room.event_id
|
||||
invite = yield self.store.get_event(event_id, allow_none=True)
|
||||
if invite:
|
||||
invited_state = invite.unsigned["invite_room_state"]
|
||||
writer.write_invite(room_id, invite, invited_state)
|
||||
|
||||
continue
|
||||
|
||||
# We only want to bother fetching events up to the last time they
|
||||
# were joined. We estimate that point by looking at the
|
||||
# stream_ordering of the last membership if it wasn't a join.
|
||||
if room.membership == Membership.JOIN:
|
||||
stream_ordering = yield self.store.get_room_max_stream_ordering()
|
||||
else:
|
||||
stream_ordering = room.stream_ordering
|
||||
|
||||
from_key = str(RoomStreamToken(0, 0))
|
||||
to_key = str(RoomStreamToken(None, stream_ordering))
|
||||
|
||||
written_events = set() # Events that we've processed in this room
|
||||
|
||||
# We need to track gaps in the events stream so that we can then
|
||||
# write out the state at those events. We do this by keeping track
|
||||
# of events whose prev events we haven't seen.
|
||||
|
||||
# Map from event ID to prev events that haven't been processed,
|
||||
# dict[str, set[str]].
|
||||
event_to_unseen_prevs = {}
|
||||
|
||||
# The reverse mapping to above, i.e. map from unseen event to events
|
||||
# that have the unseen event in their prev_events, i.e. the unseen
|
||||
# events "children". dict[str, set[str]]
|
||||
unseen_to_child_events = {}
|
||||
|
||||
# We fetch events in the room the user could see by fetching *all*
|
||||
# events that we have and then filtering, this isn't the most
|
||||
# efficient method perhaps but it does guarantee we get everything.
|
||||
while True:
|
||||
events, _ = yield self.store.paginate_room_events(
|
||||
room_id, from_key, to_key, limit=100, direction="f"
|
||||
)
|
||||
if not events:
|
||||
break
|
||||
|
||||
from_key = events[-1].internal_metadata.after
|
||||
|
||||
events = yield filter_events_for_client(self.store, user_id, events)
|
||||
|
||||
writer.write_events(room_id, events)
|
||||
|
||||
# Update the extremity tracking dicts
|
||||
for event in events:
|
||||
# Check if we have any prev events that haven't been
|
||||
# processed yet, and add those to the appropriate dicts.
|
||||
unseen_events = set(event.prev_event_ids()) - written_events
|
||||
if unseen_events:
|
||||
event_to_unseen_prevs[event.event_id] = unseen_events
|
||||
for unseen in unseen_events:
|
||||
unseen_to_child_events.setdefault(unseen, set()).add(
|
||||
event.event_id
|
||||
)
|
||||
|
||||
# Now check if this event is an unseen prev event, if so
|
||||
# then we remove this event from the appropriate dicts.
|
||||
for child_id in unseen_to_child_events.pop(event.event_id, []):
|
||||
event_to_unseen_prevs[child_id].discard(event.event_id)
|
||||
|
||||
written_events.add(event.event_id)
|
||||
|
||||
logger.info(
|
||||
"Written %d events in room %s", len(written_events), room_id
|
||||
)
|
||||
|
||||
# Extremities are the events who have at least one unseen prev event.
|
||||
extremities = (
|
||||
event_id
|
||||
for event_id, unseen_prevs in event_to_unseen_prevs.items()
|
||||
if unseen_prevs
|
||||
)
|
||||
for event_id in extremities:
|
||||
if not event_to_unseen_prevs[event_id]:
|
||||
continue
|
||||
state = yield self.store.get_state_for_event(event_id)
|
||||
writer.write_state(room_id, event_id, state)
|
||||
|
||||
defer.returnValue(writer.finished())
|
||||
|
||||
|
||||
class ExfiltrationWriter(object):
|
||||
"""Interface used to specify how to write exported data.
|
||||
"""
|
||||
|
||||
def write_events(self, room_id, events):
|
||||
"""Write a batch of events for a room.
|
||||
|
||||
Args:
|
||||
room_id (str)
|
||||
events (list[FrozenEvent])
|
||||
"""
|
||||
pass
|
||||
|
||||
def write_state(self, room_id, event_id, state):
|
||||
"""Write the state at the given event in the room.
|
||||
|
||||
This only gets called for backward extremities rather than for each
|
||||
event.
|
||||
|
||||
Args:
|
||||
room_id (str)
|
||||
event_id (str)
|
||||
state (dict[tuple[str, str], FrozenEvent])
|
||||
"""
|
||||
pass
|
||||
|
||||
def write_invite(self, room_id, event, state):
|
||||
"""Write an invite for the room, with associated invite state.
|
||||
|
||||
Args:
|
||||
room_id (str)
|
||||
event (FrozenEvent)
|
||||
state (dict[tuple[str, str], dict]): A subset of the state at the
|
||||
invite, with a subset of the event keys (type, state_key
|
||||
content and sender)
|
||||
"""
|
||||
|
||||
def finished(self):
|
||||
"""Called when all data has succesfully been exported and written.
|
||||
|
||||
This functions return value is passed to the caller of
|
||||
`export_user_data`.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import time
|
||||
import unicodedata
|
||||
|
||||
import attr
|
||||
@@ -34,6 +35,7 @@ from synapse.api.errors import (
|
||||
LoginError,
|
||||
StoreError,
|
||||
SynapseError,
|
||||
UserDeactivatedError,
|
||||
)
|
||||
from synapse.api.ratelimiting import Ratelimiter
|
||||
from synapse.logging.context import defer_to_thread
|
||||
@@ -558,7 +560,7 @@ class AuthHandler(BaseHandler):
|
||||
return self.sessions[session_id]
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_access_token_for_user_id(self, user_id, device_id=None):
|
||||
def get_access_token_for_user_id(self, user_id, device_id, valid_until_ms):
|
||||
"""
|
||||
Creates a new access token for the user with the given user ID.
|
||||
|
||||
@@ -572,16 +574,26 @@ class AuthHandler(BaseHandler):
|
||||
device_id (str|None): the device ID to associate with the tokens.
|
||||
None to leave the tokens unassociated with a device (deprecated:
|
||||
we should always have a device ID)
|
||||
valid_until_ms (int|None): when the token is valid until. None for
|
||||
no expiry.
|
||||
Returns:
|
||||
The access token for the user's session.
|
||||
Raises:
|
||||
StoreError if there was a problem storing the token.
|
||||
"""
|
||||
logger.info("Logging in user %s on device %s", user_id, device_id)
|
||||
fmt_expiry = ""
|
||||
if valid_until_ms is not None:
|
||||
fmt_expiry = time.strftime(
|
||||
" until %Y-%m-%d %H:%M:%S", time.localtime(valid_until_ms / 1000.0)
|
||||
)
|
||||
logger.info("Logging in user %s on device %s%s", user_id, device_id, fmt_expiry)
|
||||
|
||||
yield self.auth.check_auth_blocking(user_id)
|
||||
|
||||
access_token = self.macaroon_gen.generate_access_token(user_id)
|
||||
yield self.store.add_access_token_to_user(user_id, access_token, device_id)
|
||||
yield self.store.add_access_token_to_user(
|
||||
user_id, access_token, device_id, valid_until_ms
|
||||
)
|
||||
|
||||
# the device *should* have been registered before we got here; however,
|
||||
# it's possible we raced against a DELETE operation. The thing we
|
||||
@@ -612,6 +624,7 @@ class AuthHandler(BaseHandler):
|
||||
Raises:
|
||||
LimitExceededError if the ratelimiter's login requests count for this
|
||||
user is too high too proceed.
|
||||
UserDeactivatedError if a user is found but is deactivated.
|
||||
"""
|
||||
self.ratelimit_login_per_account(user_id)
|
||||
res = yield self._find_user_id_and_pwd_hash(user_id)
|
||||
@@ -827,6 +840,13 @@ class AuthHandler(BaseHandler):
|
||||
if not lookupres:
|
||||
defer.returnValue(None)
|
||||
(user_id, password_hash) = lookupres
|
||||
|
||||
# If the password hash is None, the account has likely been deactivated
|
||||
if not password_hash:
|
||||
deactivated = yield self.store.get_user_deactivated_status(user_id)
|
||||
if deactivated:
|
||||
raise UserDeactivatedError("This account has been deactivated")
|
||||
|
||||
result = yield self.validate_hash(password, password_hash)
|
||||
if not result:
|
||||
logger.warn("Failed password login for user %s", user_id)
|
||||
|
||||
@@ -84,6 +84,8 @@ class RegistrationHandler(BaseHandler):
|
||||
self.device_handler = hs.get_device_handler()
|
||||
self.pusher_pool = hs.get_pusherpool()
|
||||
|
||||
self.session_lifetime = hs.config.session_lifetime
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def check_username(self, localpart, guest_access_token=None, assigned_user_id=None):
|
||||
if types.contains_invalid_mxid_characters(localpart):
|
||||
@@ -599,6 +601,8 @@ class RegistrationHandler(BaseHandler):
|
||||
def register_device(self, user_id, device_id, initial_display_name, is_guest=False):
|
||||
"""Register a device for a user and generate an access token.
|
||||
|
||||
The access token will be limited by the homeserver's session_lifetime config.
|
||||
|
||||
Args:
|
||||
user_id (str): full canonical @user:id
|
||||
device_id (str|None): The device ID to check, or None to generate
|
||||
@@ -619,20 +623,29 @@ class RegistrationHandler(BaseHandler):
|
||||
is_guest=is_guest,
|
||||
)
|
||||
defer.returnValue((r["device_id"], r["access_token"]))
|
||||
else:
|
||||
device_id = yield self.device_handler.check_device_registered(
|
||||
user_id, device_id, initial_display_name
|
||||
)
|
||||
if is_guest:
|
||||
access_token = self.macaroon_gen.generate_access_token(
|
||||
user_id, ["guest = true"]
|
||||
)
|
||||
else:
|
||||
access_token = yield self._auth_handler.get_access_token_for_user_id(
|
||||
user_id, device_id=device_id
|
||||
)
|
||||
|
||||
defer.returnValue((device_id, access_token))
|
||||
valid_until_ms = None
|
||||
if self.session_lifetime is not None:
|
||||
if is_guest:
|
||||
raise Exception(
|
||||
"session_lifetime is not currently implemented for guest access"
|
||||
)
|
||||
valid_until_ms = self.clock.time_msec() + self.session_lifetime
|
||||
|
||||
device_id = yield self.device_handler.check_device_registered(
|
||||
user_id, device_id, initial_display_name
|
||||
)
|
||||
if is_guest:
|
||||
assert valid_until_ms is None
|
||||
access_token = self.macaroon_gen.generate_access_token(
|
||||
user_id, ["guest = true"]
|
||||
)
|
||||
else:
|
||||
access_token = yield self._auth_handler.get_access_token_for_user_id(
|
||||
user_id, device_id=device_id, valid_until_ms=valid_until_ms
|
||||
)
|
||||
|
||||
defer.returnValue((device_id, access_token))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def post_registration_actions(
|
||||
|
||||
@@ -67,7 +67,7 @@ class StorageProviderWrapper(StorageProvider):
|
||||
backend (StorageProvider)
|
||||
store_local (bool): Whether to store new local files or not.
|
||||
store_synchronous (bool): Whether to wait for file to be successfully
|
||||
uploaded, or todo the upload in the backgroud.
|
||||
uploaded, or todo the upload in the background.
|
||||
store_remote (bool): Whether remote media should be uploaded
|
||||
"""
|
||||
|
||||
|
||||
@@ -90,7 +90,8 @@ class RegistrationWorkerStore(SQLBaseStore):
|
||||
token (str): The access token of a user.
|
||||
Returns:
|
||||
defer.Deferred: None, if the token did not match, otherwise dict
|
||||
including the keys `name`, `is_guest`, `device_id`, `token_id`.
|
||||
including the keys `name`, `is_guest`, `device_id`, `token_id`,
|
||||
`valid_until_ms`.
|
||||
"""
|
||||
return self.runInteraction(
|
||||
"get_user_by_access_token", self._query_for_auth, token
|
||||
@@ -284,7 +285,7 @@ class RegistrationWorkerStore(SQLBaseStore):
|
||||
def _query_for_auth(self, txn, token):
|
||||
sql = (
|
||||
"SELECT users.name, users.is_guest, access_tokens.id as token_id,"
|
||||
" access_tokens.device_id"
|
||||
" access_tokens.device_id, access_tokens.valid_until_ms"
|
||||
" FROM users"
|
||||
" INNER JOIN access_tokens on users.name = access_tokens.user_id"
|
||||
" WHERE token = ?"
|
||||
@@ -603,7 +604,7 @@ class RegistrationStore(
|
||||
)
|
||||
|
||||
self.register_background_update_handler(
|
||||
"users_set_deactivated_flag", self._backgroud_update_set_deactivated_flag
|
||||
"users_set_deactivated_flag", self._background_update_set_deactivated_flag
|
||||
)
|
||||
|
||||
# Create a background job for culling expired 3PID validity tokens
|
||||
@@ -618,14 +619,14 @@ class RegistrationStore(
|
||||
hs.get_clock().looping_call(start_cull, THIRTY_MINUTES_IN_MS)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _backgroud_update_set_deactivated_flag(self, progress, batch_size):
|
||||
def _background_update_set_deactivated_flag(self, progress, batch_size):
|
||||
"""Retrieves a list of all deactivated users and sets the 'deactivated' flag to 1
|
||||
for each of them.
|
||||
"""
|
||||
|
||||
last_user = progress.get("user_id", "")
|
||||
|
||||
def _backgroud_update_set_deactivated_flag_txn(txn):
|
||||
def _background_update_set_deactivated_flag_txn(txn):
|
||||
txn.execute(
|
||||
"""
|
||||
SELECT
|
||||
@@ -670,7 +671,7 @@ class RegistrationStore(
|
||||
return False
|
||||
|
||||
end = yield self.runInteraction(
|
||||
"users_set_deactivated_flag", _backgroud_update_set_deactivated_flag_txn
|
||||
"users_set_deactivated_flag", _background_update_set_deactivated_flag_txn
|
||||
)
|
||||
|
||||
if end:
|
||||
@@ -679,14 +680,16 @@ class RegistrationStore(
|
||||
defer.returnValue(batch_size)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def add_access_token_to_user(self, user_id, token, device_id=None):
|
||||
def add_access_token_to_user(self, user_id, token, device_id, valid_until_ms):
|
||||
"""Adds an access token for the given user.
|
||||
|
||||
Args:
|
||||
user_id (str): The user ID.
|
||||
token (str): The new access token to add.
|
||||
device_id (str): ID of the device to associate with the access
|
||||
token
|
||||
token
|
||||
valid_until_ms (int|None): when the token is valid until. None for
|
||||
no expiry.
|
||||
Raises:
|
||||
StoreError if there was a problem adding this.
|
||||
"""
|
||||
@@ -694,7 +697,13 @@ class RegistrationStore(
|
||||
|
||||
yield self._simple_insert(
|
||||
"access_tokens",
|
||||
{"id": next_id, "user_id": user_id, "token": token, "device_id": device_id},
|
||||
{
|
||||
"id": next_id,
|
||||
"user_id": user_id,
|
||||
"token": token,
|
||||
"device_id": device_id,
|
||||
"valid_until_ms": valid_until_ms,
|
||||
},
|
||||
desc="add_access_token_to_user",
|
||||
)
|
||||
|
||||
|
||||
@@ -575,6 +575,26 @@ class RoomMemberWorkerStore(EventsWorkerStore):
|
||||
count = yield self.runInteraction("did_forget_membership", f)
|
||||
defer.returnValue(count == 0)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_rooms_user_has_been_in(self, user_id):
|
||||
"""Get all rooms that the user has ever been in.
|
||||
|
||||
Args:
|
||||
user_id (str)
|
||||
|
||||
Returns:
|
||||
Deferred[set[str]]: Set of room IDs.
|
||||
"""
|
||||
|
||||
room_ids = yield self._simple_select_onecol(
|
||||
table="room_memberships",
|
||||
keyvalues={"membership": Membership.JOIN, "user_id": user_id},
|
||||
retcol="room_id",
|
||||
desc="get_rooms_user_has_been_in",
|
||||
)
|
||||
|
||||
return set(room_ids)
|
||||
|
||||
|
||||
class RoomMemberStore(RoomMemberWorkerStore):
|
||||
def __init__(self, db_conn, hs):
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/* Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* 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.
|
||||
*/
|
||||
|
||||
-- when this access token can be used until, in ms since the epoch. NULL means the token
|
||||
-- never expires.
|
||||
ALTER TABLE access_tokens ADD COLUMN valid_until_ms BIGINT;
|
||||
@@ -833,7 +833,9 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore):
|
||||
Returns:
|
||||
Deferred[tuple[list[_EventDictReturn], str]]: Returns the results
|
||||
as a list of _EventDictReturn and a token that points to the end
|
||||
of the result set.
|
||||
of the result set. If no events are returned then the end of the
|
||||
stream has been reached (i.e. there are no events between
|
||||
`from_token` and `to_token`), or `limit` is zero.
|
||||
"""
|
||||
|
||||
assert int(limit) >= 0
|
||||
@@ -905,15 +907,15 @@ class StreamWorkerStore(EventsWorkerStore, SQLBaseStore):
|
||||
only those before
|
||||
direction(char): Either 'b' or 'f' to indicate whether we are
|
||||
paginating forwards or backwards from `from_key`.
|
||||
limit (int): The maximum number of events to return. Zero or less
|
||||
means no limit.
|
||||
limit (int): The maximum number of events to return.
|
||||
event_filter (Filter|None): If provided filters the events to
|
||||
those that match the filter.
|
||||
|
||||
Returns:
|
||||
tuple[list[dict], str]: Returns the results as a list of dicts and
|
||||
a token that points to the end of the result set. The dicts have
|
||||
the keys "event_id", "topological_ordering" and "stream_orderign".
|
||||
tuple[list[FrozenEvent], str]: Returns the results as a list of
|
||||
events and a token that points to the end of the result set. If no
|
||||
events are returned then the end of the stream has been reached
|
||||
(i.e. there are no events between `from_key` and `to_key`).
|
||||
"""
|
||||
|
||||
from_key = RoomStreamToken.parse(from_key)
|
||||
|
||||
@@ -262,9 +262,11 @@ class AuthTestCase(unittest.TestCase):
|
||||
self.store.add_access_token_to_user = Mock()
|
||||
|
||||
token = yield self.hs.handlers.auth_handler.get_access_token_for_user_id(
|
||||
USER_ID, "DEVICE"
|
||||
USER_ID, "DEVICE", valid_until_ms=None
|
||||
)
|
||||
self.store.add_access_token_to_user.assert_called_with(
|
||||
USER_ID, token, "DEVICE", None
|
||||
)
|
||||
self.store.add_access_token_to_user.assert_called_with(USER_ID, token, "DEVICE")
|
||||
|
||||
def get_user(tok):
|
||||
if token != tok:
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# 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.
|
||||
|
||||
from collections import Counter
|
||||
|
||||
from mock import Mock
|
||||
|
||||
import synapse.api.errors
|
||||
import synapse.handlers.admin
|
||||
import synapse.rest.admin
|
||||
import synapse.storage
|
||||
from synapse.api.constants import EventTypes
|
||||
from synapse.rest.client.v1 import login, room
|
||||
|
||||
from tests import unittest
|
||||
|
||||
|
||||
class ExfiltrateData(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
login.register_servlets,
|
||||
room.register_servlets,
|
||||
]
|
||||
|
||||
def prepare(self, reactor, clock, hs):
|
||||
self.admin_handler = hs.get_handlers().admin_handler
|
||||
|
||||
self.user1 = self.register_user("user1", "password")
|
||||
self.token1 = self.login("user1", "password")
|
||||
|
||||
self.user2 = self.register_user("user2", "password")
|
||||
self.token2 = self.login("user2", "password")
|
||||
|
||||
def test_single_public_joined_room(self):
|
||||
"""Test that we write *all* events for a public room
|
||||
"""
|
||||
room_id = self.helper.create_room_as(
|
||||
self.user1, tok=self.token1, is_public=True
|
||||
)
|
||||
self.helper.send(room_id, body="Hello!", tok=self.token1)
|
||||
self.helper.join(room_id, self.user2, tok=self.token2)
|
||||
self.helper.send(room_id, body="Hello again!", tok=self.token1)
|
||||
|
||||
writer = Mock()
|
||||
|
||||
self.get_success(self.admin_handler.export_user_data(self.user2, writer))
|
||||
|
||||
writer.write_events.assert_called()
|
||||
|
||||
# Since we can see all events there shouldn't be any extremities, so no
|
||||
# state should be written
|
||||
writer.write_state.assert_not_called()
|
||||
|
||||
# Collect all events that were written
|
||||
written_events = []
|
||||
for (called_room_id, events), _ in writer.write_events.call_args_list:
|
||||
self.assertEqual(called_room_id, room_id)
|
||||
written_events.extend(events)
|
||||
|
||||
# Check that the right number of events were written
|
||||
counter = Counter(
|
||||
(event.type, getattr(event, "state_key", None)) for event in written_events
|
||||
)
|
||||
self.assertEqual(counter[(EventTypes.Message, None)], 2)
|
||||
self.assertEqual(counter[(EventTypes.Member, self.user1)], 1)
|
||||
self.assertEqual(counter[(EventTypes.Member, self.user2)], 1)
|
||||
|
||||
def test_single_private_joined_room(self):
|
||||
"""Tests that we correctly write state when we can't see all events in
|
||||
a room.
|
||||
"""
|
||||
room_id = self.helper.create_room_as(self.user1, tok=self.token1)
|
||||
self.helper.send_state(
|
||||
room_id,
|
||||
EventTypes.RoomHistoryVisibility,
|
||||
body={"history_visibility": "joined"},
|
||||
tok=self.token1,
|
||||
)
|
||||
self.helper.send(room_id, body="Hello!", tok=self.token1)
|
||||
self.helper.join(room_id, self.user2, tok=self.token2)
|
||||
self.helper.send(room_id, body="Hello again!", tok=self.token1)
|
||||
|
||||
writer = Mock()
|
||||
|
||||
self.get_success(self.admin_handler.export_user_data(self.user2, writer))
|
||||
|
||||
writer.write_events.assert_called()
|
||||
|
||||
# Since we can't see all events there should be one extremity.
|
||||
writer.write_state.assert_called_once()
|
||||
|
||||
# Collect all events that were written
|
||||
written_events = []
|
||||
for (called_room_id, events), _ in writer.write_events.call_args_list:
|
||||
self.assertEqual(called_room_id, room_id)
|
||||
written_events.extend(events)
|
||||
|
||||
# Check that the right number of events were written
|
||||
counter = Counter(
|
||||
(event.type, getattr(event, "state_key", None)) for event in written_events
|
||||
)
|
||||
self.assertEqual(counter[(EventTypes.Message, None)], 1)
|
||||
self.assertEqual(counter[(EventTypes.Member, self.user1)], 1)
|
||||
self.assertEqual(counter[(EventTypes.Member, self.user2)], 1)
|
||||
|
||||
def test_single_left_room(self):
|
||||
"""Tests that we don't see events in the room after we leave.
|
||||
"""
|
||||
room_id = self.helper.create_room_as(self.user1, tok=self.token1)
|
||||
self.helper.send(room_id, body="Hello!", tok=self.token1)
|
||||
self.helper.join(room_id, self.user2, tok=self.token2)
|
||||
self.helper.send(room_id, body="Hello again!", tok=self.token1)
|
||||
self.helper.leave(room_id, self.user2, tok=self.token2)
|
||||
self.helper.send(room_id, body="Helloooooo!", tok=self.token1)
|
||||
|
||||
writer = Mock()
|
||||
|
||||
self.get_success(self.admin_handler.export_user_data(self.user2, writer))
|
||||
|
||||
writer.write_events.assert_called()
|
||||
|
||||
# Since we can see all events there shouldn't be any extremities, so no
|
||||
# state should be written
|
||||
writer.write_state.assert_not_called()
|
||||
|
||||
written_events = []
|
||||
for (called_room_id, events), _ in writer.write_events.call_args_list:
|
||||
self.assertEqual(called_room_id, room_id)
|
||||
written_events.extend(events)
|
||||
|
||||
# Check that the right number of events were written
|
||||
counter = Counter(
|
||||
(event.type, getattr(event, "state_key", None)) for event in written_events
|
||||
)
|
||||
self.assertEqual(counter[(EventTypes.Message, None)], 2)
|
||||
self.assertEqual(counter[(EventTypes.Member, self.user1)], 1)
|
||||
self.assertEqual(counter[(EventTypes.Member, self.user2)], 2)
|
||||
|
||||
def test_single_left_rejoined_private_room(self):
|
||||
"""Tests that see the correct events in private rooms when we
|
||||
repeatedly join and leave.
|
||||
"""
|
||||
room_id = self.helper.create_room_as(self.user1, tok=self.token1)
|
||||
self.helper.send_state(
|
||||
room_id,
|
||||
EventTypes.RoomHistoryVisibility,
|
||||
body={"history_visibility": "joined"},
|
||||
tok=self.token1,
|
||||
)
|
||||
self.helper.send(room_id, body="Hello!", tok=self.token1)
|
||||
self.helper.join(room_id, self.user2, tok=self.token2)
|
||||
self.helper.send(room_id, body="Hello again!", tok=self.token1)
|
||||
self.helper.leave(room_id, self.user2, tok=self.token2)
|
||||
self.helper.send(room_id, body="Helloooooo!", tok=self.token1)
|
||||
self.helper.join(room_id, self.user2, tok=self.token2)
|
||||
self.helper.send(room_id, body="Helloooooo!!", tok=self.token1)
|
||||
|
||||
writer = Mock()
|
||||
|
||||
self.get_success(self.admin_handler.export_user_data(self.user2, writer))
|
||||
|
||||
writer.write_events.assert_called_once()
|
||||
|
||||
# Since we joined/left/joined again we expect there to be two gaps.
|
||||
self.assertEqual(writer.write_state.call_count, 2)
|
||||
|
||||
written_events = []
|
||||
for (called_room_id, events), _ in writer.write_events.call_args_list:
|
||||
self.assertEqual(called_room_id, room_id)
|
||||
written_events.extend(events)
|
||||
|
||||
# Check that the right number of events were written
|
||||
counter = Counter(
|
||||
(event.type, getattr(event, "state_key", None)) for event in written_events
|
||||
)
|
||||
self.assertEqual(counter[(EventTypes.Message, None)], 2)
|
||||
self.assertEqual(counter[(EventTypes.Member, self.user1)], 1)
|
||||
self.assertEqual(counter[(EventTypes.Member, self.user2)], 3)
|
||||
|
||||
def test_invite(self):
|
||||
"""Tests that pending invites get handled correctly.
|
||||
"""
|
||||
room_id = self.helper.create_room_as(self.user1, tok=self.token1)
|
||||
self.helper.send(room_id, body="Hello!", tok=self.token1)
|
||||
self.helper.invite(room_id, self.user1, self.user2, tok=self.token1)
|
||||
|
||||
writer = Mock()
|
||||
|
||||
self.get_success(self.admin_handler.export_user_data(self.user2, writer))
|
||||
|
||||
writer.write_events.assert_not_called()
|
||||
writer.write_state.assert_not_called()
|
||||
writer.write_invite.assert_called_once()
|
||||
|
||||
args = writer.write_invite.call_args[0]
|
||||
self.assertEqual(args[0], room_id)
|
||||
self.assertEqual(args[1].content["membership"], "invite")
|
||||
self.assertTrue(args[2]) # Assert there is at least one bit of state
|
||||
@@ -117,7 +117,9 @@ class AuthTestCase(unittest.TestCase):
|
||||
def test_mau_limits_disabled(self):
|
||||
self.hs.config.limit_usage_by_mau = False
|
||||
# Ensure does not throw exception
|
||||
yield self.auth_handler.get_access_token_for_user_id("user_a")
|
||||
yield self.auth_handler.get_access_token_for_user_id(
|
||||
"user_a", device_id=None, valid_until_ms=None
|
||||
)
|
||||
|
||||
yield self.auth_handler.validate_short_term_login_token_and_get_user_id(
|
||||
self._get_macaroon().serialize()
|
||||
@@ -131,7 +133,9 @@ class AuthTestCase(unittest.TestCase):
|
||||
)
|
||||
|
||||
with self.assertRaises(ResourceLimitError):
|
||||
yield self.auth_handler.get_access_token_for_user_id("user_a")
|
||||
yield self.auth_handler.get_access_token_for_user_id(
|
||||
"user_a", device_id=None, valid_until_ms=None
|
||||
)
|
||||
|
||||
self.hs.get_datastore().get_monthly_active_count = Mock(
|
||||
return_value=defer.succeed(self.large_number_of_users)
|
||||
@@ -150,7 +154,9 @@ class AuthTestCase(unittest.TestCase):
|
||||
return_value=defer.succeed(self.hs.config.max_mau_value)
|
||||
)
|
||||
with self.assertRaises(ResourceLimitError):
|
||||
yield self.auth_handler.get_access_token_for_user_id("user_a")
|
||||
yield self.auth_handler.get_access_token_for_user_id(
|
||||
"user_a", device_id=None, valid_until_ms=None
|
||||
)
|
||||
|
||||
self.hs.get_datastore().get_monthly_active_count = Mock(
|
||||
return_value=defer.succeed(self.hs.config.max_mau_value)
|
||||
@@ -166,7 +172,9 @@ class AuthTestCase(unittest.TestCase):
|
||||
self.hs.get_datastore().get_monthly_active_count = Mock(
|
||||
return_value=defer.succeed(self.hs.config.max_mau_value)
|
||||
)
|
||||
yield self.auth_handler.get_access_token_for_user_id("user_a")
|
||||
yield self.auth_handler.get_access_token_for_user_id(
|
||||
"user_a", device_id=None, valid_until_ms=None
|
||||
)
|
||||
self.hs.get_datastore().user_last_seen_monthly_active = Mock(
|
||||
return_value=defer.succeed(self.hs.get_clock().time_msec())
|
||||
)
|
||||
@@ -185,7 +193,9 @@ class AuthTestCase(unittest.TestCase):
|
||||
return_value=defer.succeed(self.small_number_of_users)
|
||||
)
|
||||
# Ensure does not raise exception
|
||||
yield self.auth_handler.get_access_token_for_user_id("user_a")
|
||||
yield self.auth_handler.get_access_token_for_user_id(
|
||||
"user_a", device_id=None, valid_until_ms=None
|
||||
)
|
||||
|
||||
self.hs.get_datastore().get_monthly_active_count = Mock(
|
||||
return_value=defer.succeed(self.small_number_of_users)
|
||||
|
||||
@@ -272,7 +272,10 @@ class RegistrationTestCase(unittest.HomeserverTestCase):
|
||||
)
|
||||
else:
|
||||
yield self.hs.get_auth_handler().delete_access_tokens_for_user(user_id)
|
||||
yield self.store.add_access_token_to_user(user_id=user_id, token=token)
|
||||
|
||||
yield self.store.add_access_token_to_user(
|
||||
user_id=user_id, token=token, device_id=None, valid_until_ms=None
|
||||
)
|
||||
|
||||
if displayname is not None:
|
||||
# logger.info("setting user display name: %s -> %s", user_id, displayname)
|
||||
|
||||
@@ -2,10 +2,14 @@ import json
|
||||
|
||||
import synapse.rest.admin
|
||||
from synapse.rest.client.v1 import login
|
||||
from synapse.rest.client.v2_alpha import devices
|
||||
from synapse.rest.client.v2_alpha.account import WhoamiRestServlet
|
||||
|
||||
from tests import unittest
|
||||
from tests.unittest import override_config
|
||||
|
||||
LOGIN_URL = b"/_matrix/client/r0/login"
|
||||
TEST_URL = b"/_matrix/client/r0/account/whoami"
|
||||
|
||||
|
||||
class LoginRestServletTestCase(unittest.HomeserverTestCase):
|
||||
@@ -13,6 +17,8 @@ class LoginRestServletTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
login.register_servlets,
|
||||
devices.register_servlets,
|
||||
lambda hs, http_server: WhoamiRestServlet(hs).register(http_server),
|
||||
]
|
||||
|
||||
def make_homeserver(self, reactor, clock):
|
||||
@@ -144,3 +150,105 @@ class LoginRestServletTestCase(unittest.HomeserverTestCase):
|
||||
self.render(request)
|
||||
|
||||
self.assertEquals(channel.result["code"], b"403", channel.result)
|
||||
|
||||
@override_config({"session_lifetime": "24h"})
|
||||
def test_soft_logout(self):
|
||||
self.register_user("kermit", "monkey")
|
||||
|
||||
# we shouldn't be able to make requests without an access token
|
||||
request, channel = self.make_request(b"GET", TEST_URL)
|
||||
self.render(request)
|
||||
self.assertEquals(channel.result["code"], b"401", channel.result)
|
||||
self.assertEquals(channel.json_body["errcode"], "M_MISSING_TOKEN")
|
||||
|
||||
# log in as normal
|
||||
params = {
|
||||
"type": "m.login.password",
|
||||
"identifier": {"type": "m.id.user", "user": "kermit"},
|
||||
"password": "monkey",
|
||||
}
|
||||
request, channel = self.make_request(b"POST", LOGIN_URL, params)
|
||||
self.render(request)
|
||||
|
||||
self.assertEquals(channel.code, 200, channel.result)
|
||||
access_token = channel.json_body["access_token"]
|
||||
device_id = channel.json_body["device_id"]
|
||||
|
||||
# we should now be able to make requests with the access token
|
||||
request, channel = self.make_request(
|
||||
b"GET", TEST_URL, access_token=access_token
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEquals(channel.code, 200, channel.result)
|
||||
|
||||
# time passes
|
||||
self.reactor.advance(24 * 3600)
|
||||
|
||||
# ... and we should be soft-logouted
|
||||
request, channel = self.make_request(
|
||||
b"GET", TEST_URL, access_token=access_token
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEquals(channel.code, 401, channel.result)
|
||||
self.assertEquals(channel.json_body["errcode"], "M_UNKNOWN_TOKEN")
|
||||
self.assertEquals(channel.json_body["soft_logout"], True)
|
||||
|
||||
#
|
||||
# test behaviour after deleting the expired device
|
||||
#
|
||||
|
||||
# we now log in as a different device
|
||||
access_token_2 = self.login("kermit", "monkey")
|
||||
|
||||
# more requests with the expired token should still return a soft-logout
|
||||
self.reactor.advance(3600)
|
||||
request, channel = self.make_request(
|
||||
b"GET", TEST_URL, access_token=access_token
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEquals(channel.code, 401, channel.result)
|
||||
self.assertEquals(channel.json_body["errcode"], "M_UNKNOWN_TOKEN")
|
||||
self.assertEquals(channel.json_body["soft_logout"], True)
|
||||
|
||||
# ... but if we delete that device, it will be a proper logout
|
||||
self._delete_device(access_token_2, "kermit", "monkey", device_id)
|
||||
|
||||
request, channel = self.make_request(
|
||||
b"GET", TEST_URL, access_token=access_token
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEquals(channel.code, 401, channel.result)
|
||||
self.assertEquals(channel.json_body["errcode"], "M_UNKNOWN_TOKEN")
|
||||
self.assertEquals(channel.json_body["soft_logout"], False)
|
||||
|
||||
def _delete_device(self, access_token, user_id, password, device_id):
|
||||
"""Perform the UI-Auth to delete a device"""
|
||||
request, channel = self.make_request(
|
||||
b"DELETE", "devices/" + device_id, access_token=access_token
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEquals(channel.code, 401, channel.result)
|
||||
# check it's a UI-Auth fail
|
||||
self.assertEqual(
|
||||
set(channel.json_body.keys()),
|
||||
{"flows", "params", "session"},
|
||||
channel.result,
|
||||
)
|
||||
|
||||
auth = {
|
||||
"type": "m.login.password",
|
||||
# https://github.com/matrix-org/synapse/issues/5665
|
||||
# "identifier": {"type": "m.id.user", "user": user_id},
|
||||
"user": user_id,
|
||||
"password": password,
|
||||
"session": channel.json_body["session"],
|
||||
}
|
||||
|
||||
request, channel = self.make_request(
|
||||
b"DELETE",
|
||||
"devices/" + device_id,
|
||||
access_token=access_token,
|
||||
content={"auth": auth},
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEquals(channel.code, 200, channel.result)
|
||||
|
||||
@@ -57,7 +57,7 @@ class RegistrationStoreTestCase(unittest.TestCase):
|
||||
def test_add_tokens(self):
|
||||
yield self.store.register_user(self.user_id, self.pwhash)
|
||||
yield self.store.add_access_token_to_user(
|
||||
self.user_id, self.tokens[1], self.device_id
|
||||
self.user_id, self.tokens[1], self.device_id, valid_until_ms=None
|
||||
)
|
||||
|
||||
result = yield self.store.get_user_by_access_token(self.tokens[1])
|
||||
@@ -72,9 +72,11 @@ class RegistrationStoreTestCase(unittest.TestCase):
|
||||
def test_user_delete_access_tokens(self):
|
||||
# add some tokens
|
||||
yield self.store.register_user(self.user_id, self.pwhash)
|
||||
yield self.store.add_access_token_to_user(self.user_id, self.tokens[0])
|
||||
yield self.store.add_access_token_to_user(
|
||||
self.user_id, self.tokens[1], self.device_id
|
||||
self.user_id, self.tokens[0], device_id=None, valid_until_ms=None
|
||||
)
|
||||
yield self.store.add_access_token_to_user(
|
||||
self.user_id, self.tokens[1], self.device_id, valid_until_ms=None
|
||||
)
|
||||
|
||||
# now delete some
|
||||
|
||||
+55
-2
@@ -157,6 +157,21 @@ class HomeserverTestCase(TestCase):
|
||||
"""
|
||||
A base TestCase that reduces boilerplate for HomeServer-using test cases.
|
||||
|
||||
Defines a setUp method which creates a mock reactor, and instantiates a homeserver
|
||||
running on that reactor.
|
||||
|
||||
There are various hooks for modifying the way that the homeserver is instantiated:
|
||||
|
||||
* override make_homeserver, for example by making it pass different parameters into
|
||||
setup_test_homeserver.
|
||||
|
||||
* override default_config, to return a modified configuration dictionary for use
|
||||
by setup_test_homeserver.
|
||||
|
||||
* On a per-test basis, you can use the @override_config decorator to give a
|
||||
dictionary containing additional configuration settings to be added to the basic
|
||||
config dict.
|
||||
|
||||
Attributes:
|
||||
servlets (list[function]): List of servlet registration function.
|
||||
user_id (str): The user ID to assume if auth is hijacked.
|
||||
@@ -168,6 +183,13 @@ class HomeserverTestCase(TestCase):
|
||||
hijack_auth = True
|
||||
needs_threadpool = False
|
||||
|
||||
def __init__(self, methodName, *args, **kwargs):
|
||||
super().__init__(methodName, *args, **kwargs)
|
||||
|
||||
# see if we have any additional config for this test
|
||||
method = getattr(self, methodName)
|
||||
self._extra_config = getattr(method, "_extra_config", None)
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up the TestCase by calling the homeserver constructor, optionally
|
||||
@@ -276,7 +298,14 @@ class HomeserverTestCase(TestCase):
|
||||
Args:
|
||||
name (str): The homeserver name/domain.
|
||||
"""
|
||||
return default_config(name)
|
||||
config = default_config(name)
|
||||
|
||||
# apply any additional config which was specified via the override_config
|
||||
# decorator.
|
||||
if self._extra_config is not None:
|
||||
config.update(self._extra_config)
|
||||
|
||||
return config
|
||||
|
||||
def prepare(self, reactor, clock, homeserver):
|
||||
"""
|
||||
@@ -443,7 +472,7 @@ class HomeserverTestCase(TestCase):
|
||||
"POST", "/_matrix/client/r0/admin/register", body.encode("utf8")
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEqual(channel.code, 200)
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
|
||||
user_id = channel.json_body["user_id"]
|
||||
return user_id
|
||||
@@ -534,3 +563,27 @@ class HomeserverTestCase(TestCase):
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEqual(channel.code, 403, channel.result)
|
||||
|
||||
|
||||
def override_config(extra_config):
|
||||
"""A decorator which can be applied to test functions to give additional HS config
|
||||
|
||||
For use
|
||||
|
||||
For example:
|
||||
|
||||
class MyTestCase(HomeserverTestCase):
|
||||
@override_config({"enable_registration": False, ...})
|
||||
def test_foo(self):
|
||||
...
|
||||
|
||||
Args:
|
||||
extra_config(dict): Additional config settings to be merged into the default
|
||||
config dict before instantiating the test homeserver.
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
func._extra_config = extra_config
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
Reference in New Issue
Block a user