Fix SSO on workers (#9271)

Fixes #8966.

* Factor out build_synapse_client_resource_tree

Start a function which will mount resources common to all workers.

* Move sso init into build_synapse_client_resource_tree

... so that we don't have to do it for each worker

* Fix SSO-login-via-a-worker

Expose the SSO login endpoints on workers, like the documentation says.

* Update workers config for new endpoints

Add documentation for endpoints recently added (#8942, #9017, #9262)

* remove submit_token from workers endpoints list

this *doesn't* work on workers (yet).

* changelog

* Add a comment about the odd path for SAML2Resource
This commit is contained in:
Richard van der Hoff 2021-02-01 15:47:59 +00:00 committed by GitHub
parent f78d07bf00
commit 9c715a5f19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 93 additions and 65 deletions

1
changelog.d/9271.bugfix Normal file
View file

@ -0,0 +1 @@
Fix single-sign-on when the endpoints are routed to synapse workers.

View file

@ -225,7 +225,6 @@ expressions:
^/_matrix/client/(api/v1|r0|unstable)/joined_groups$ ^/_matrix/client/(api/v1|r0|unstable)/joined_groups$
^/_matrix/client/(api/v1|r0|unstable)/publicised_groups$ ^/_matrix/client/(api/v1|r0|unstable)/publicised_groups$
^/_matrix/client/(api/v1|r0|unstable)/publicised_groups/ ^/_matrix/client/(api/v1|r0|unstable)/publicised_groups/
^/_synapse/client/password_reset/email/submit_token$
# Registration/login requests # Registration/login requests
^/_matrix/client/(api/v1|r0|unstable)/login$ ^/_matrix/client/(api/v1|r0|unstable)/login$
@ -256,25 +255,28 @@ Additionally, the following endpoints should be included if Synapse is configure
to use SSO (you only need to include the ones for whichever SSO provider you're to use SSO (you only need to include the ones for whichever SSO provider you're
using): using):
# for all SSO providers
^/_matrix/client/(api/v1|r0|unstable)/login/sso/redirect
^/_synapse/client/pick_idp$
^/_synapse/client/pick_username
^/_synapse/client/sso_register$
# OpenID Connect requests. # OpenID Connect requests.
^/_matrix/client/(api/v1|r0|unstable)/login/sso/redirect$
^/_synapse/oidc/callback$ ^/_synapse/oidc/callback$
# SAML requests. # SAML requests.
^/_matrix/client/(api/v1|r0|unstable)/login/sso/redirect$
^/_matrix/saml2/authn_response$ ^/_matrix/saml2/authn_response$
# CAS requests. # CAS requests.
^/_matrix/client/(api/v1|r0|unstable)/login/(cas|sso)/redirect$
^/_matrix/client/(api/v1|r0|unstable)/login/cas/ticket$ ^/_matrix/client/(api/v1|r0|unstable)/login/cas/ticket$
Ensure that all SSO logins go to a single process.
For multiple workers not handling the SSO endpoints properly, see
[#7530](https://github.com/matrix-org/synapse/issues/7530).
Note that a HTTP listener with `client` and `federation` resources must be Note that a HTTP listener with `client` and `federation` resources must be
configured in the `worker_listeners` option in the worker config. configured in the `worker_listeners` option in the worker config.
Ensure that all SSO logins go to a single process (usually the main process).
For multiple workers not handling the SSO endpoints properly, see
[#7530](https://github.com/matrix-org/synapse/issues/7530).
#### Load balancing #### Load balancing
It is possible to run multiple instances of this worker app, with incoming requests It is possible to run multiple instances of this worker app, with incoming requests

View file

@ -22,6 +22,7 @@ from typing import Dict, Iterable, Optional, Set
from typing_extensions import ContextManager from typing_extensions import ContextManager
from twisted.internet import address from twisted.internet import address
from twisted.web.resource import IResource
import synapse import synapse
import synapse.events import synapse.events
@ -90,9 +91,8 @@ from synapse.replication.tcp.streams import (
ToDeviceStream, ToDeviceStream,
) )
from synapse.rest.admin import register_servlets_for_media_repo from synapse.rest.admin import register_servlets_for_media_repo
from synapse.rest.client.v1 import events, room from synapse.rest.client.v1 import events, login, room
from synapse.rest.client.v1.initial_sync import InitialSyncRestServlet from synapse.rest.client.v1.initial_sync import InitialSyncRestServlet
from synapse.rest.client.v1.login import LoginRestServlet
from synapse.rest.client.v1.profile import ( from synapse.rest.client.v1.profile import (
ProfileAvatarURLRestServlet, ProfileAvatarURLRestServlet,
ProfileDisplaynameRestServlet, ProfileDisplaynameRestServlet,
@ -127,6 +127,7 @@ from synapse.rest.client.v2_alpha.sendtodevice import SendToDeviceRestServlet
from synapse.rest.client.versions import VersionsRestServlet from synapse.rest.client.versions import VersionsRestServlet
from synapse.rest.health import HealthResource from synapse.rest.health import HealthResource
from synapse.rest.key.v2 import KeyApiV2Resource from synapse.rest.key.v2 import KeyApiV2Resource
from synapse.rest.synapse.client import build_synapse_client_resource_tree
from synapse.server import HomeServer, cache_in_self from synapse.server import HomeServer, cache_in_self
from synapse.storage.databases.main.censor_events import CensorEventsStore from synapse.storage.databases.main.censor_events import CensorEventsStore
from synapse.storage.databases.main.client_ips import ClientIpWorkerStore from synapse.storage.databases.main.client_ips import ClientIpWorkerStore
@ -507,7 +508,7 @@ class GenericWorkerServer(HomeServer):
site_tag = port site_tag = port
# We always include a health resource. # We always include a health resource.
resources = {"/health": HealthResource()} resources = {"/health": HealthResource()} # type: Dict[str, IResource]
for res in listener_config.http_options.resources: for res in listener_config.http_options.resources:
for name in res.names: for name in res.names:
@ -517,7 +518,7 @@ class GenericWorkerServer(HomeServer):
resource = JsonResource(self, canonical_json=False) resource = JsonResource(self, canonical_json=False)
RegisterRestServlet(self).register(resource) RegisterRestServlet(self).register(resource)
LoginRestServlet(self).register(resource) login.register_servlets(self, resource)
ThreepidRestServlet(self).register(resource) ThreepidRestServlet(self).register(resource)
DevicesRestServlet(self).register(resource) DevicesRestServlet(self).register(resource)
KeyQueryServlet(self).register(resource) KeyQueryServlet(self).register(resource)
@ -557,6 +558,8 @@ class GenericWorkerServer(HomeServer):
groups.register_servlets(self, resource) groups.register_servlets(self, resource)
resources.update({CLIENT_API_PREFIX: resource}) resources.update({CLIENT_API_PREFIX: resource})
resources.update(build_synapse_client_resource_tree(self))
elif name == "federation": elif name == "federation":
resources.update({FEDERATION_PREFIX: TransportLayerServer(self)}) resources.update({FEDERATION_PREFIX: TransportLayerServer(self)})
elif name == "media": elif name == "media":

View file

@ -60,9 +60,7 @@ from synapse.rest import ClientRestResource
from synapse.rest.admin import AdminRestResource from synapse.rest.admin import AdminRestResource
from synapse.rest.health import HealthResource from synapse.rest.health import HealthResource
from synapse.rest.key.v2 import KeyApiV2Resource from synapse.rest.key.v2 import KeyApiV2Resource
from synapse.rest.synapse.client.pick_idp import PickIdpResource from synapse.rest.synapse.client import build_synapse_client_resource_tree
from synapse.rest.synapse.client.pick_username import pick_username_resource
from synapse.rest.synapse.client.sso_register import SsoRegisterResource
from synapse.rest.well_known import WellKnownResource from synapse.rest.well_known import WellKnownResource
from synapse.server import HomeServer from synapse.server import HomeServer
from synapse.storage import DataStore from synapse.storage import DataStore
@ -191,22 +189,10 @@ class SynapseHomeServer(HomeServer):
"/_matrix/client/versions": client_resource, "/_matrix/client/versions": client_resource,
"/.well-known/matrix/client": WellKnownResource(self), "/.well-known/matrix/client": WellKnownResource(self),
"/_synapse/admin": AdminRestResource(self), "/_synapse/admin": AdminRestResource(self),
"/_synapse/client/pick_username": pick_username_resource(self), **build_synapse_client_resource_tree(self),
"/_synapse/client/pick_idp": PickIdpResource(self),
"/_synapse/client/sso_register": SsoRegisterResource(self),
} }
) )
if self.get_config().oidc_enabled:
from synapse.rest.oidc import OIDCResource
resources["/_synapse/oidc"] = OIDCResource(self)
if self.get_config().saml2_enabled:
from synapse.rest.saml2 import SAML2Resource
resources["/_matrix/saml2"] = SAML2Resource(self)
if self.get_config().threepid_behaviour_email == ThreepidBehaviour.LOCAL: if self.get_config().threepid_behaviour_email == ThreepidBehaviour.LOCAL:
from synapse.rest.synapse.client.password_reset import ( from synapse.rest.synapse.client.password_reset import (
PasswordResetSubmitTokenResource, PasswordResetSubmitTokenResource,

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2020 The Matrix.org Foundation C.I.C. # Copyright 2021 The Matrix.org Foundation C.I.C.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -12,3 +12,50 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from typing import TYPE_CHECKING, Mapping
from twisted.web.resource import Resource
from synapse.rest.synapse.client.pick_idp import PickIdpResource
from synapse.rest.synapse.client.pick_username import pick_username_resource
from synapse.rest.synapse.client.sso_register import SsoRegisterResource
if TYPE_CHECKING:
from synapse.server import HomeServer
def build_synapse_client_resource_tree(hs: "HomeServer") -> Mapping[str, Resource]:
"""Builds a resource tree to include synapse-specific client resources
These are resources which should be loaded on all workers which expose a C-S API:
ie, the main process, and any generic workers so configured.
Returns:
map from path to Resource.
"""
resources = {
# SSO bits. These are always loaded, whether or not SSO login is actually
# enabled (they just won't work very well if it's not)
"/_synapse/client/pick_idp": PickIdpResource(hs),
"/_synapse/client/pick_username": pick_username_resource(hs),
"/_synapse/client/sso_register": SsoRegisterResource(hs),
}
# provider-specific SSO bits. Only load these if they are enabled, since they
# rely on optional dependencies.
if hs.config.oidc_enabled:
from synapse.rest.oidc import OIDCResource
resources["/_synapse/oidc"] = OIDCResource(hs)
if hs.config.saml2_enabled:
from synapse.rest.saml2 import SAML2Resource
# This is mounted under '/_matrix' for backwards-compatibility.
resources["/_matrix/saml2"] = SAML2Resource(hs)
return resources
__all__ = ["build_synapse_client_resource_tree"]

View file

@ -443,6 +443,26 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
return await self.db_pool.runInteraction("get_users_by_id_case_insensitive", f) return await self.db_pool.runInteraction("get_users_by_id_case_insensitive", f)
async def record_user_external_id(
self, auth_provider: str, external_id: str, user_id: str
) -> None:
"""Record a mapping from an external user id to a mxid
Args:
auth_provider: identifier for the remote auth provider
external_id: id on that system
user_id: complete mxid that it is mapped to
"""
await self.db_pool.simple_insert(
table="user_external_ids",
values={
"auth_provider": auth_provider,
"external_id": external_id,
"user_id": user_id,
},
desc="record_user_external_id",
)
async def get_user_by_external_id( async def get_user_by_external_id(
self, auth_provider: str, external_id: str self, auth_provider: str, external_id: str
) -> Optional[str]: ) -> Optional[str]:
@ -1371,26 +1391,6 @@ class RegistrationStore(StatsStore, RegistrationBackgroundUpdateStore):
self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,)) self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,))
async def record_user_external_id(
self, auth_provider: str, external_id: str, user_id: str
) -> None:
"""Record a mapping from an external user id to a mxid
Args:
auth_provider: identifier for the remote auth provider
external_id: id on that system
user_id: complete mxid that it is mapped to
"""
await self.db_pool.simple_insert(
table="user_external_ids",
values={
"auth_provider": auth_provider,
"external_id": external_id,
"user_id": user_id,
},
desc="record_user_external_id",
)
async def user_set_password_hash( async def user_set_password_hash(
self, user_id: str, password_hash: Optional[str] self, user_id: str, password_hash: Optional[str]
) -> None: ) -> None:

View file

@ -29,9 +29,7 @@ from synapse.appservice import ApplicationService
from synapse.rest.client.v1 import login, logout from synapse.rest.client.v1 import login, logout
from synapse.rest.client.v2_alpha import devices, register from synapse.rest.client.v2_alpha import devices, register
from synapse.rest.client.v2_alpha.account import WhoamiRestServlet from synapse.rest.client.v2_alpha.account import WhoamiRestServlet
from synapse.rest.synapse.client.pick_idp import PickIdpResource from synapse.rest.synapse.client import build_synapse_client_resource_tree
from synapse.rest.synapse.client.pick_username import pick_username_resource
from synapse.rest.synapse.client.sso_register import SsoRegisterResource
from synapse.types import create_requester from synapse.types import create_requester
from tests import unittest from tests import unittest
@ -424,11 +422,8 @@ class MultiSSOTestCase(unittest.HomeserverTestCase):
return config return config
def create_resource_dict(self) -> Dict[str, Resource]: def create_resource_dict(self) -> Dict[str, Resource]:
from synapse.rest.oidc import OIDCResource
d = super().create_resource_dict() d = super().create_resource_dict()
d["/_synapse/client/pick_idp"] = PickIdpResource(self.hs) d.update(build_synapse_client_resource_tree(self.hs))
d["/_synapse/oidc"] = OIDCResource(self.hs)
return d return d
def test_get_login_flows(self): def test_get_login_flows(self):
@ -1212,12 +1207,8 @@ class UsernamePickerTestCase(HomeserverTestCase):
return config return config
def create_resource_dict(self) -> Dict[str, Resource]: def create_resource_dict(self) -> Dict[str, Resource]:
from synapse.rest.oidc import OIDCResource
d = super().create_resource_dict() d = super().create_resource_dict()
d["/_synapse/client/pick_username"] = pick_username_resource(self.hs) d.update(build_synapse_client_resource_tree(self.hs))
d["/_synapse/client/sso_register"] = SsoRegisterResource(self.hs)
d["/_synapse/oidc"] = OIDCResource(self.hs)
return d return d
def test_username_picker(self): def test_username_picker(self):

View file

@ -22,7 +22,7 @@ from synapse.api.constants import LoginType
from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker
from synapse.rest.client.v1 import login from synapse.rest.client.v1 import login
from synapse.rest.client.v2_alpha import auth, devices, register from synapse.rest.client.v2_alpha import auth, devices, register
from synapse.rest.oidc import OIDCResource from synapse.rest.synapse.client import build_synapse_client_resource_tree
from synapse.types import JsonDict, UserID from synapse.types import JsonDict, UserID
from tests import unittest from tests import unittest
@ -173,9 +173,7 @@ class UIAuthTests(unittest.HomeserverTestCase):
def create_resource_dict(self): def create_resource_dict(self):
resource_dict = super().create_resource_dict() resource_dict = super().create_resource_dict()
if HAS_OIDC: resource_dict.update(build_synapse_client_resource_tree(self.hs))
# mount the OIDC resource at /_synapse/oidc
resource_dict["/_synapse/oidc"] = OIDCResource(self.hs)
return resource_dict return resource_dict
def prepare(self, reactor, clock, hs): def prepare(self, reactor, clock, hs):