Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/18946.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update [MSC4190](https://github.com/matrix-org/matrix-spec-proposals/pull/4190) support to return correct errors and allow appservices to reset cross-signing keys without user-interactive authentication. Contributed by @tulir @ Beeper.
3 changes: 3 additions & 0 deletions synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ class Codes(str, Enum):
# Part of MSC4155
INVITE_BLOCKED = "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED"

# Part of MSC4190
APPSERVICE_LOGIN_UNSUPPORTED = "IO.ELEMENT.MSC4190.M_APPSERVICE_LOGIN_UNSUPPORTED"

# Part of MSC4306: Thread Subscriptions
MSC4306_CONFLICTING_UNSUBSCRIPTION = (
"IO.ELEMENT.MSC4306.M_CONFLICTING_UNSUBSCRIPTION"
Expand Down
7 changes: 6 additions & 1 deletion synapse/rest/client/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,15 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
if not keys_are_different:
return 200, {}

# MSC4190 can skip UIA for replacing cross-signing keys as well.
is_appservice_with_msc4190 = (
requester.app_service and requester.app_service.msc4190_device_management
)

# The keys are different; is x-signing set up? If no, then this is first-time
# setup, and that is allowed without UIA, per MSC3967.
# If yes, then we need to authenticate the change.
if is_cross_signing_setup:
if is_cross_signing_setup and not is_appservice_with_msc4190:
# With MSC3861, UIA is not possible. Instead, the auth service has to
# explicitly mark the master key as replaceable.
if self.hs.config.mas.enabled:
Expand Down
7 changes: 7 additions & 0 deletions synapse/rest/client/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,13 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, LoginResponse]:
"This login method is only valid for application services"
)

if appservice.msc4190_device_management:
raise SynapseError(
400,
"This appservice has MSC4190 enabled, so appservice login cannot be used.",
errcode=Codes.APPSERVICE_LOGIN_UNSUPPORTED,
)

if appservice.is_rate_limited():
await self._address_ratelimiter.ratelimit(
None, request.getClientAddress().host
Expand Down
14 changes: 12 additions & 2 deletions synapse/rest/client/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -782,8 +782,12 @@ async def _do_appservice_registration(
user_id, appservice = await self.registration_handler.appservice_register(
username, as_token
)
if appservice.msc4190_device_management:
body["inhibit_login"] = True
if appservice.msc4190_device_management and not body.get("inhibit_login"):
raise SynapseError(
400,
"This appservice has MSC4190 enabled, so the inhibit_login parameter must be set to true.",
errcode=Codes.APPSERVICE_LOGIN_UNSUPPORTED,
)

return await self._create_registration_details(
user_id,
Expand Down Expand Up @@ -923,6 +927,12 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
"Registration has been disabled. Only m.login.application_service registrations are allowed.",
errcode=Codes.FORBIDDEN,
)
if not body.get("inhibit_login"):
raise SynapseError(
400,
"This server uses OAuth2, so the inhibit_login parameter must be set to true for appservice registrations.",
errcode=Codes.APPSERVICE_LOGIN_UNSUPPORTED,
)

kind = parse_string(request, "kind", default="user")

Expand Down
4 changes: 4 additions & 0 deletions synapse/storage/databases/main/appservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ def __init__(
hs.hostname, hs.config.appservice.app_service_config_files
)
self.exclusive_user_regex = _make_exclusive_regex(self.services_cache)
# When OAuth is enabled, force all appservices to enable MSC4190 too.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't MSC4190 only required when using encryption?

So unencrypted App services wouldn't need it from how I understood it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The primary use case is encryption, but that's not the reason MSC4190 exists. Existing endpoints for appservices return access tokens, which is not possible when using native OAuth2. MSC4190 was made to solve any such incompatibilities.

if hs.config.mas.enabled or hs.config.experimental.msc3861.enabled:
for appservice in self.services_cache:
appservice.msc4190_device_management = True

def get_max_as_txn_id(txn: Cursor) -> int:
logger.warning("Falling back to slow query, you should port to postgres")
Expand Down
6 changes: 5 additions & 1 deletion tests/handlers/test_oauth_delegation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1219,7 +1219,11 @@ def test_registration_endpoints_removed(self) -> None:
channel = self.make_request(
"POST",
"/_matrix/client/v3/register",
{"username": "alice", "type": "m.login.application_service"},
{
"username": "alice",
"type": "m.login.application_service",
"inhibit_login": True,
},
shorthand=False,
access_token="i_am_an_app_service",
)
Expand Down
12 changes: 9 additions & 3 deletions tests/rest/client/test_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,9 @@ def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
return self.hs

def test_PUT_device(self) -> None:
self.register_appservice_user("alice", self.msc4190_service.token)
self.register_appservice_user(
"alice", self.msc4190_service.token, inhibit_login=True
)
self.register_appservice_user("bob", self.pre_msc_service.token)

channel = self.make_request(
Expand Down Expand Up @@ -542,7 +544,9 @@ def test_PUT_device(self) -> None:
self.assertEqual(channel.code, 404, channel.json_body)

def test_DELETE_device(self) -> None:
self.register_appservice_user("alice", self.msc4190_service.token)
self.register_appservice_user(
"alice", self.msc4190_service.token, inhibit_login=True
)

# There should be no device
channel = self.make_request(
Expand Down Expand Up @@ -589,7 +593,9 @@ def test_DELETE_device(self) -> None:
self.assertEqual(channel.json_body, {"devices": []})

def test_POST_delete_devices(self) -> None:
self.register_appservice_user("alice", self.msc4190_service.token)
self.register_appservice_user(
"alice", self.msc4190_service.token, inhibit_login=True
)

# There should be no device
channel = self.make_request(
Expand Down
35 changes: 35 additions & 0 deletions tests/rest/client/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -1498,9 +1498,23 @@ def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
ApplicationService.NS_ALIASES: [],
},
)
self.msc4190_service = ApplicationService(
id="third__identifier",
token="third_token",
sender=UserID.from_string("@as3bot:example.com"),
namespaces={
ApplicationService.NS_USERS: [
{"regex": r"@as3_user.*", "exclusive": False}
],
ApplicationService.NS_ROOMS: [],
ApplicationService.NS_ALIASES: [],
},
msc4190_device_management=True,
)

self.hs.get_datastores().main.services_cache.append(self.service)
self.hs.get_datastores().main.services_cache.append(self.another_service)
self.hs.get_datastores().main.services_cache.append(self.msc4190_service)
return self.hs

def test_login_appservice_user(self) -> None:
Expand All @@ -1517,6 +1531,27 @@ def test_login_appservice_user(self) -> None:

self.assertEqual(channel.code, 200, msg=channel.result)

def test_login_appservice_msc4190_fail(self) -> None:
"""Test that an appservice user can use /login"""
self.register_appservice_user(
"as3_user_alice", self.msc4190_service.token, inhibit_login=True
)

params = {
"type": login.LoginRestServlet.APPSERVICE_TYPE,
"identifier": {"type": "m.id.user", "user": "as3_user_alice"},
}
channel = self.make_request(
b"POST", LOGIN_URL, params, access_token=self.msc4190_service.token
)

self.assertEqual(channel.code, 400, msg=channel.result)
self.assertEqual(
channel.json_body.get("errcode"),
Codes.APPSERVICE_LOGIN_UNSUPPORTED,
channel.json_body,
)

def test_login_appservice_user_bot(self) -> None:
"""Test that the appservice bot can use /login"""
self.register_appservice_user(AS_USER, self.service.token)
Expand Down
29 changes: 29 additions & 0 deletions tests/rest/client/test_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def test_POST_appservice_msc4190_enabled(self) -> None:
request_data = {
"username": "as_user_kermit",
"type": APP_SERVICE_REGISTRATION_TYPE,
"inhibit_login": True,
}

channel = self.make_request(
Expand All @@ -147,6 +148,34 @@ def test_POST_appservice_msc4190_enabled(self) -> None:
self.assertLessEqual(det_data.items(), channel.json_body.items())
self.assertNotIn("access_token", channel.json_body)

def test_POST_appservice_msc4190_enabled_fail(self) -> None:
# With MSC4190 enabled, the registration should fail unless inhibit_login is set
as_token = "i_am_an_app_service"

appservice = ApplicationService(
as_token,
id="1234",
namespaces={"users": [{"regex": r"@as_user.*", "exclusive": True}]},
sender=UserID.from_string("@as:test"),
msc4190_device_management=True,
)

self.hs.get_datastores().main.services_cache.append(appservice)
request_data = {
"username": "as_user_kermit",
"type": APP_SERVICE_REGISTRATION_TYPE,
}

channel = self.make_request(
b"POST", self.url + b"?access_token=i_am_an_app_service", request_data
)
self.assertEqual(channel.code, 400, channel.json_body)
self.assertEqual(
channel.json_body.get("errcode"),
Codes.APPSERVICE_LOGIN_UNSUPPORTED,
channel.json_body,
)

def test_POST_bad_password(self) -> None:
request_data = {"username": "kermit", "password": 666}
channel = self.make_request(b"POST", self.url, request_data)
Expand Down
2 changes: 2 additions & 0 deletions tests/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,7 @@ def register_appservice_user(
self,
username: str,
appservice_token: str,
inhibit_login: bool = False,
) -> Tuple[str, Optional[str]]:
"""Register an appservice user as an application service.
Requires the client-facing registration API be registered.
Expand All @@ -802,6 +803,7 @@ def register_appservice_user(
{
"username": username,
"type": "m.login.application_service",
"inhibit_login": inhibit_login,
},
access_token=appservice_token,
)
Expand Down
Loading