mirror of
https://github.com/element-hq/synapse.git
synced 2025-12-13 01:50:46 +00:00
Compare commits
5 Commits
madlittlem
...
quenting/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e5034186b | ||
|
|
ea84db8458 | ||
|
|
cb9fe38407 | ||
|
|
9abd4044b9 | ||
|
|
f41e21da54 |
1
changelog.d/18549.feature
Normal file
1
changelog.d/18549.feature
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Support for the stable endpoint and scopes of [MSC3861](https://github.com/matrix-org/matrix-spec-proposals/pull/3861) & co.
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional, Set
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from synapse._pydantic_compat import (
|
from synapse._pydantic_compat import (
|
||||||
@@ -57,8 +57,10 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# Scope as defined by MSC2967
|
# Scope as defined by MSC2967
|
||||||
# https://github.com/matrix-org/matrix-spec-proposals/pull/2967
|
# https://github.com/matrix-org/matrix-spec-proposals/pull/2967
|
||||||
SCOPE_MATRIX_API = "urn:matrix:org.matrix.msc2967.client:api:*"
|
UNSTABLE_SCOPE_MATRIX_API = "urn:matrix:org.matrix.msc2967.client:api:*"
|
||||||
SCOPE_MATRIX_DEVICE_PREFIX = "urn:matrix:org.matrix.msc2967.client:device:"
|
UNSTABLE_SCOPE_MATRIX_DEVICE_PREFIX = "urn:matrix:org.matrix.msc2967.client:device:"
|
||||||
|
STABLE_SCOPE_MATRIX_API = "urn:matrix:client:api:*"
|
||||||
|
STABLE_SCOPE_MATRIX_DEVICE_PREFIX = "urn:matrix:client:device:"
|
||||||
|
|
||||||
|
|
||||||
class ServerMetadata(BaseModel):
|
class ServerMetadata(BaseModel):
|
||||||
@@ -334,7 +336,10 @@ class MasDelegatedAuth(BaseAuth):
|
|||||||
scope = introspection_result.get_scope_set()
|
scope = introspection_result.get_scope_set()
|
||||||
|
|
||||||
# Determine type of user based on presence of particular scopes
|
# Determine type of user based on presence of particular scopes
|
||||||
if SCOPE_MATRIX_API not in scope:
|
if (
|
||||||
|
UNSTABLE_SCOPE_MATRIX_API not in scope
|
||||||
|
and STABLE_SCOPE_MATRIX_API not in scope
|
||||||
|
):
|
||||||
raise InvalidClientTokenError(
|
raise InvalidClientTokenError(
|
||||||
"Token doesn't grant access to the Matrix C-S API"
|
"Token doesn't grant access to the Matrix C-S API"
|
||||||
)
|
)
|
||||||
@@ -366,11 +371,12 @@ class MasDelegatedAuth(BaseAuth):
|
|||||||
# We only allow a single device_id in the scope, so we find them all in the
|
# We only allow a single device_id in the scope, so we find them all in the
|
||||||
# scope list, and raise if there are more than one. The OIDC server should be
|
# scope list, and raise if there are more than one. The OIDC server should be
|
||||||
# the one enforcing valid scopes, so we raise a 500 if we find an invalid scope.
|
# the one enforcing valid scopes, so we raise a 500 if we find an invalid scope.
|
||||||
device_ids = [
|
device_ids: Set[str] = set()
|
||||||
tok[len(SCOPE_MATRIX_DEVICE_PREFIX) :]
|
for tok in scope:
|
||||||
for tok in scope
|
if tok.startswith(UNSTABLE_SCOPE_MATRIX_DEVICE_PREFIX):
|
||||||
if tok.startswith(SCOPE_MATRIX_DEVICE_PREFIX)
|
device_ids.add(tok[len(UNSTABLE_SCOPE_MATRIX_DEVICE_PREFIX) :])
|
||||||
]
|
elif tok.startswith(STABLE_SCOPE_MATRIX_DEVICE_PREFIX):
|
||||||
|
device_ids.add(tok[len(STABLE_SCOPE_MATRIX_DEVICE_PREFIX) :])
|
||||||
|
|
||||||
if len(device_ids) > 1:
|
if len(device_ids) > 1:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
@@ -378,7 +384,7 @@ class MasDelegatedAuth(BaseAuth):
|
|||||||
"Multiple device IDs in scope",
|
"Multiple device IDs in scope",
|
||||||
)
|
)
|
||||||
|
|
||||||
device_id = device_ids[0] if device_ids else None
|
device_id = next(iter(device_ids), None)
|
||||||
|
|
||||||
if device_id is not None:
|
if device_id is not None:
|
||||||
# Sanity check the device_id
|
# Sanity check the device_id
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
#
|
#
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
|
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from authlib.oauth2 import ClientAuth
|
from authlib.oauth2 import ClientAuth
|
||||||
@@ -34,7 +34,6 @@ from synapse.api.errors import (
|
|||||||
AuthError,
|
AuthError,
|
||||||
HttpResponseException,
|
HttpResponseException,
|
||||||
InvalidClientTokenError,
|
InvalidClientTokenError,
|
||||||
OAuthInsufficientScopeError,
|
|
||||||
SynapseError,
|
SynapseError,
|
||||||
UnrecognizedRequestError,
|
UnrecognizedRequestError,
|
||||||
)
|
)
|
||||||
@@ -63,9 +62,10 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# Scope as defined by MSC2967
|
# Scope as defined by MSC2967
|
||||||
# https://github.com/matrix-org/matrix-spec-proposals/pull/2967
|
# https://github.com/matrix-org/matrix-spec-proposals/pull/2967
|
||||||
SCOPE_MATRIX_API = "urn:matrix:org.matrix.msc2967.client:api:*"
|
UNSTABLE_SCOPE_MATRIX_API = "urn:matrix:org.matrix.msc2967.client:api:*"
|
||||||
SCOPE_MATRIX_GUEST = "urn:matrix:org.matrix.msc2967.client:api:guest"
|
UNSTABLE_SCOPE_MATRIX_DEVICE_PREFIX = "urn:matrix:org.matrix.msc2967.client:device:"
|
||||||
SCOPE_MATRIX_DEVICE_PREFIX = "urn:matrix:org.matrix.msc2967.client:device:"
|
STABLE_SCOPE_MATRIX_API = "urn:matrix:client:api:*"
|
||||||
|
STABLE_SCOPE_MATRIX_DEVICE_PREFIX = "urn:matrix:client:device:"
|
||||||
|
|
||||||
# Scope which allows access to the Synapse admin API
|
# Scope which allows access to the Synapse admin API
|
||||||
SCOPE_SYNAPSE_ADMIN = "urn:synapse:admin:*"
|
SCOPE_SYNAPSE_ADMIN = "urn:synapse:admin:*"
|
||||||
@@ -444,9 +444,6 @@ class MSC3861DelegatedAuth(BaseAuth):
|
|||||||
if not self._is_access_token_the_admin_token(access_token):
|
if not self._is_access_token_the_admin_token(access_token):
|
||||||
await self._record_request(request, requester)
|
await self._record_request(request, requester)
|
||||||
|
|
||||||
if not allow_guest and requester.is_guest:
|
|
||||||
raise OAuthInsufficientScopeError([SCOPE_MATRIX_API])
|
|
||||||
|
|
||||||
request.requester = requester
|
request.requester = requester
|
||||||
|
|
||||||
return requester
|
return requester
|
||||||
@@ -528,10 +525,11 @@ class MSC3861DelegatedAuth(BaseAuth):
|
|||||||
scope: List[str] = introspection_result.get_scope_list()
|
scope: List[str] = introspection_result.get_scope_list()
|
||||||
|
|
||||||
# Determine type of user based on presence of particular scopes
|
# Determine type of user based on presence of particular scopes
|
||||||
has_user_scope = SCOPE_MATRIX_API in scope
|
has_user_scope = (
|
||||||
has_guest_scope = SCOPE_MATRIX_GUEST in scope
|
UNSTABLE_SCOPE_MATRIX_API in scope or STABLE_SCOPE_MATRIX_API in scope
|
||||||
|
)
|
||||||
|
|
||||||
if not has_user_scope and not has_guest_scope:
|
if not has_user_scope:
|
||||||
raise InvalidClientTokenError("No scope in token granting user rights")
|
raise InvalidClientTokenError("No scope in token granting user rights")
|
||||||
|
|
||||||
# Match via the sub claim
|
# Match via the sub claim
|
||||||
@@ -579,11 +577,12 @@ class MSC3861DelegatedAuth(BaseAuth):
|
|||||||
# We only allow a single device_id in the scope, so we find them all in the
|
# We only allow a single device_id in the scope, so we find them all in the
|
||||||
# scope list, and raise if there are more than one. The OIDC server should be
|
# scope list, and raise if there are more than one. The OIDC server should be
|
||||||
# the one enforcing valid scopes, so we raise a 500 if we find an invalid scope.
|
# the one enforcing valid scopes, so we raise a 500 if we find an invalid scope.
|
||||||
device_ids = [
|
device_ids: Set[str] = set()
|
||||||
tok[len(SCOPE_MATRIX_DEVICE_PREFIX) :]
|
for tok in scope:
|
||||||
for tok in scope
|
if tok.startswith(UNSTABLE_SCOPE_MATRIX_DEVICE_PREFIX):
|
||||||
if tok.startswith(SCOPE_MATRIX_DEVICE_PREFIX)
|
device_ids.add(tok[len(UNSTABLE_SCOPE_MATRIX_DEVICE_PREFIX) :])
|
||||||
]
|
elif tok.startswith(STABLE_SCOPE_MATRIX_DEVICE_PREFIX):
|
||||||
|
device_ids.add(tok[len(STABLE_SCOPE_MATRIX_DEVICE_PREFIX) :])
|
||||||
|
|
||||||
if len(device_ids) > 1:
|
if len(device_ids) > 1:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
@@ -591,7 +590,7 @@ class MSC3861DelegatedAuth(BaseAuth):
|
|||||||
"Multiple device IDs in scope",
|
"Multiple device IDs in scope",
|
||||||
)
|
)
|
||||||
|
|
||||||
device_id = device_ids[0] if device_ids else None
|
device_id = next(iter(device_ids), None)
|
||||||
|
|
||||||
if device_id is not None:
|
if device_id is not None:
|
||||||
# Sanity check the device_id
|
# Sanity check the device_id
|
||||||
@@ -617,5 +616,4 @@ class MSC3861DelegatedAuth(BaseAuth):
|
|||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
scope=scope,
|
scope=scope,
|
||||||
is_guest=(has_guest_scope and not has_user_scope),
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -76,11 +76,17 @@ class AuthMetadataServlet(RestServlet):
|
|||||||
Advertises the OAuth 2.0 server metadata for the homeserver.
|
Advertises the OAuth 2.0 server metadata for the homeserver.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PATTERNS = client_patterns(
|
PATTERNS = [
|
||||||
"/org.matrix.msc2965/auth_metadata$",
|
*client_patterns(
|
||||||
unstable=True,
|
"/auth_metadata$",
|
||||||
releases=(),
|
releases=("v1",),
|
||||||
)
|
),
|
||||||
|
*client_patterns(
|
||||||
|
"/org.matrix.msc2965/auth_metadata$",
|
||||||
|
unstable=True,
|
||||||
|
releases=(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ import time
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Any, Coroutine, Dict, Generator, Optional, TypeVar, Union
|
from typing import Any, ClassVar, Coroutine, Dict, Generator, Optional, TypeVar, Union
|
||||||
from unittest.mock import ANY, AsyncMock, Mock
|
from unittest.mock import ANY, AsyncMock, Mock
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
from parameterized import parameterized_class
|
from parameterized.parameterized import parameterized_class
|
||||||
from signedjson.key import (
|
from signedjson.key import (
|
||||||
encode_verify_key_base64,
|
encode_verify_key_base64,
|
||||||
generate_signing_key,
|
generate_signing_key,
|
||||||
@@ -46,7 +46,6 @@ from synapse.api.errors import (
|
|||||||
Codes,
|
Codes,
|
||||||
HttpResponseException,
|
HttpResponseException,
|
||||||
InvalidClientTokenError,
|
InvalidClientTokenError,
|
||||||
OAuthInsufficientScopeError,
|
|
||||||
SynapseError,
|
SynapseError,
|
||||||
)
|
)
|
||||||
from synapse.appservice import ApplicationService
|
from synapse.appservice import ApplicationService
|
||||||
@@ -78,11 +77,7 @@ JWKS_URI = ISSUER + ".well-known/jwks.json"
|
|||||||
INTROSPECTION_ENDPOINT = ISSUER + "introspect"
|
INTROSPECTION_ENDPOINT = ISSUER + "introspect"
|
||||||
|
|
||||||
SYNAPSE_ADMIN_SCOPE = "urn:synapse:admin:*"
|
SYNAPSE_ADMIN_SCOPE = "urn:synapse:admin:*"
|
||||||
MATRIX_USER_SCOPE = "urn:matrix:org.matrix.msc2967.client:api:*"
|
|
||||||
MATRIX_GUEST_SCOPE = "urn:matrix:org.matrix.msc2967.client:api:guest"
|
|
||||||
MATRIX_DEVICE_SCOPE_PREFIX = "urn:matrix:org.matrix.msc2967.client:device:"
|
|
||||||
DEVICE = "AABBCCDD"
|
DEVICE = "AABBCCDD"
|
||||||
MATRIX_DEVICE_SCOPE = MATRIX_DEVICE_SCOPE_PREFIX + DEVICE
|
|
||||||
SUBJECT = "abc-def-ghi"
|
SUBJECT = "abc-def-ghi"
|
||||||
USERNAME = "test-user"
|
USERNAME = "test-user"
|
||||||
USER_ID = "@" + USERNAME + ":" + SERVER_NAME
|
USER_ID = "@" + USERNAME + ":" + SERVER_NAME
|
||||||
@@ -112,7 +107,24 @@ async def get_json(url: str) -> JsonDict:
|
|||||||
|
|
||||||
|
|
||||||
@skip_unless(HAS_AUTHLIB, "requires authlib")
|
@skip_unless(HAS_AUTHLIB, "requires authlib")
|
||||||
|
@parameterized_class(
|
||||||
|
("device_scope_prefix", "api_scope"),
|
||||||
|
[
|
||||||
|
("urn:matrix:client:device:", "urn:matrix:client:api:*"),
|
||||||
|
(
|
||||||
|
"urn:matrix:org.matrix.msc2967.client:device:",
|
||||||
|
"urn:matrix:org.matrix.msc2967.client:api:*",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
class MSC3861OAuthDelegation(HomeserverTestCase):
|
class MSC3861OAuthDelegation(HomeserverTestCase):
|
||||||
|
device_scope_prefix: ClassVar[str]
|
||||||
|
api_scope: ClassVar[str]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_scope(self) -> str:
|
||||||
|
return self.device_scope_prefix + DEVICE
|
||||||
|
|
||||||
servlets = [
|
servlets = [
|
||||||
account.register_servlets,
|
account.register_servlets,
|
||||||
keys.register_servlets,
|
keys.register_servlets,
|
||||||
@@ -212,7 +224,7 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
|
|||||||
"""The handler should return a 500 when no subject is present."""
|
"""The handler should return a 500 when no subject is present."""
|
||||||
|
|
||||||
self._set_introspection_returnvalue(
|
self._set_introspection_returnvalue(
|
||||||
{"active": True, "scope": " ".join([MATRIX_USER_SCOPE])}
|
{"active": True, "scope": " ".join([self.api_scope])},
|
||||||
)
|
)
|
||||||
|
|
||||||
request = Mock(args={})
|
request = Mock(args={})
|
||||||
@@ -235,7 +247,7 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
|
|||||||
{
|
{
|
||||||
"active": True,
|
"active": True,
|
||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"scope": " ".join([MATRIX_DEVICE_SCOPE]),
|
"scope": " ".join([self.device_scope]),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
request = Mock(args={})
|
request = Mock(args={})
|
||||||
@@ -282,7 +294,7 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
|
|||||||
{
|
{
|
||||||
"active": True,
|
"active": True,
|
||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"scope": " ".join([SYNAPSE_ADMIN_SCOPE, MATRIX_USER_SCOPE]),
|
"scope": " ".join([SYNAPSE_ADMIN_SCOPE, self.api_scope]),
|
||||||
"username": USERNAME,
|
"username": USERNAME,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -312,9 +324,7 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
|
|||||||
{
|
{
|
||||||
"active": True,
|
"active": True,
|
||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"scope": " ".join(
|
"scope": " ".join([SYNAPSE_ADMIN_SCOPE, self.api_scope]),
|
||||||
[SYNAPSE_ADMIN_SCOPE, MATRIX_USER_SCOPE, MATRIX_GUEST_SCOPE]
|
|
||||||
),
|
|
||||||
"username": USERNAME,
|
"username": USERNAME,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -344,7 +354,7 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
|
|||||||
{
|
{
|
||||||
"active": True,
|
"active": True,
|
||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"scope": " ".join([MATRIX_USER_SCOPE]),
|
"scope": " ".join([self.api_scope]),
|
||||||
"username": USERNAME,
|
"username": USERNAME,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -374,7 +384,7 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
|
|||||||
{
|
{
|
||||||
"active": True,
|
"active": True,
|
||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"scope": " ".join([MATRIX_USER_SCOPE, MATRIX_DEVICE_SCOPE]),
|
"scope": " ".join([self.api_scope, self.device_scope]),
|
||||||
"username": USERNAME,
|
"username": USERNAME,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -404,7 +414,7 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
|
|||||||
{
|
{
|
||||||
"active": True,
|
"active": True,
|
||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"scope": " ".join([MATRIX_USER_SCOPE]),
|
"scope": " ".join([self.api_scope]),
|
||||||
"device_id": DEVICE,
|
"device_id": DEVICE,
|
||||||
"username": USERNAME,
|
"username": USERNAME,
|
||||||
}
|
}
|
||||||
@@ -444,9 +454,9 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
|
|||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"scope": " ".join(
|
"scope": " ".join(
|
||||||
[
|
[
|
||||||
MATRIX_USER_SCOPE,
|
self.api_scope,
|
||||||
f"{MATRIX_DEVICE_SCOPE_PREFIX}AABBCC",
|
f"{self.device_scope_prefix}AABBCC",
|
||||||
f"{MATRIX_DEVICE_SCOPE_PREFIX}DDEEFF",
|
f"{self.device_scope_prefix}DDEEFF",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
"username": USERNAME,
|
"username": USERNAME,
|
||||||
@@ -457,68 +467,6 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
|
|||||||
request.requestHeaders.getRawHeaders = mock_getRawHeaders()
|
request.requestHeaders.getRawHeaders = mock_getRawHeaders()
|
||||||
self.get_failure(self.auth.get_user_by_req(request), AuthError)
|
self.get_failure(self.auth.get_user_by_req(request), AuthError)
|
||||||
|
|
||||||
def test_active_guest_not_allowed(self) -> None:
|
|
||||||
"""The handler should return an insufficient scope error."""
|
|
||||||
|
|
||||||
self._set_introspection_returnvalue(
|
|
||||||
{
|
|
||||||
"active": True,
|
|
||||||
"sub": SUBJECT,
|
|
||||||
"scope": " ".join([MATRIX_GUEST_SCOPE, MATRIX_DEVICE_SCOPE]),
|
|
||||||
"username": USERNAME,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
request = Mock(args={})
|
|
||||||
request.args[b"access_token"] = [b"mockAccessToken"]
|
|
||||||
request.requestHeaders.getRawHeaders = mock_getRawHeaders()
|
|
||||||
error = self.get_failure(
|
|
||||||
self.auth.get_user_by_req(request), OAuthInsufficientScopeError
|
|
||||||
)
|
|
||||||
self.http_client.get_json.assert_called_once_with(WELL_KNOWN)
|
|
||||||
self._rust_client.post.assert_called_once_with(
|
|
||||||
url=INTROSPECTION_ENDPOINT,
|
|
||||||
response_limit=ANY,
|
|
||||||
request_body=ANY,
|
|
||||||
headers=ANY,
|
|
||||||
)
|
|
||||||
self._assertParams()
|
|
||||||
self.assertEqual(
|
|
||||||
getattr(error.value, "headers", {})["WWW-Authenticate"],
|
|
||||||
'Bearer error="insufficient_scope", scope="urn:matrix:org.matrix.msc2967.client:api:*"',
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_active_guest_allowed(self) -> None:
|
|
||||||
"""The handler should return a requester with guest user rights and a device ID."""
|
|
||||||
|
|
||||||
self._set_introspection_returnvalue(
|
|
||||||
{
|
|
||||||
"active": True,
|
|
||||||
"sub": SUBJECT,
|
|
||||||
"scope": " ".join([MATRIX_GUEST_SCOPE, MATRIX_DEVICE_SCOPE]),
|
|
||||||
"username": USERNAME,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
request = Mock(args={})
|
|
||||||
request.args[b"access_token"] = [b"mockAccessToken"]
|
|
||||||
request.requestHeaders.getRawHeaders = mock_getRawHeaders()
|
|
||||||
requester = self.get_success(
|
|
||||||
self.auth.get_user_by_req(request, allow_guest=True)
|
|
||||||
)
|
|
||||||
self.http_client.get_json.assert_called_once_with(WELL_KNOWN)
|
|
||||||
self._rust_client.post.assert_called_once_with(
|
|
||||||
url=INTROSPECTION_ENDPOINT,
|
|
||||||
response_limit=ANY,
|
|
||||||
request_body=ANY,
|
|
||||||
headers=ANY,
|
|
||||||
)
|
|
||||||
self._assertParams()
|
|
||||||
self.assertEqual(requester.user.to_string(), "@%s:%s" % (USERNAME, SERVER_NAME))
|
|
||||||
self.assertEqual(requester.is_guest, True)
|
|
||||||
self.assertEqual(
|
|
||||||
get_awaitable_result(self.auth.is_server_admin(requester)), False
|
|
||||||
)
|
|
||||||
self.assertEqual(requester.device_id, DEVICE)
|
|
||||||
|
|
||||||
def test_unavailable_introspection_endpoint(self) -> None:
|
def test_unavailable_introspection_endpoint(self) -> None:
|
||||||
"""The handler should return an internal server error."""
|
"""The handler should return an internal server error."""
|
||||||
request = Mock(args={})
|
request = Mock(args={})
|
||||||
@@ -562,8 +510,8 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
|
|||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"scope": " ".join(
|
"scope": " ".join(
|
||||||
[
|
[
|
||||||
MATRIX_USER_SCOPE,
|
self.api_scope,
|
||||||
f"{MATRIX_DEVICE_SCOPE_PREFIX}AABBCC",
|
f"{self.device_scope_prefix}AABBCC",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
"username": USERNAME,
|
"username": USERNAME,
|
||||||
@@ -611,7 +559,7 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
|
|||||||
{
|
{
|
||||||
"active": True,
|
"active": True,
|
||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"scope": " ".join([MATRIX_USER_SCOPE, MATRIX_DEVICE_SCOPE]),
|
"scope": " ".join([self.api_scope, self.device_scope]),
|
||||||
"username": USERNAME,
|
"username": USERNAME,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -676,7 +624,7 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
|
|||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
"active": True,
|
"active": True,
|
||||||
"scope": MATRIX_USER_SCOPE,
|
"scope": self.api_scope,
|
||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"username": USERNAME,
|
"username": USERNAME,
|
||||||
},
|
},
|
||||||
@@ -842,8 +790,24 @@ class FakeMasServer(HTTPServer):
|
|||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
@parameterized_class(
|
||||||
|
("device_scope_prefix", "api_scope"),
|
||||||
|
[
|
||||||
|
("urn:matrix:client:device:", "urn:matrix:client:api:*"),
|
||||||
|
(
|
||||||
|
"urn:matrix:org.matrix.msc2967.client:device:",
|
||||||
|
"urn:matrix:org.matrix.msc2967.client:api:*",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
class MasAuthDelegation(HomeserverTestCase):
|
class MasAuthDelegation(HomeserverTestCase):
|
||||||
server: FakeMasServer
|
server: FakeMasServer
|
||||||
|
device_scope_prefix: ClassVar[str]
|
||||||
|
api_scope: ClassVar[str]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_scope(self) -> str:
|
||||||
|
return self.device_scope_prefix + DEVICE
|
||||||
|
|
||||||
def till_deferred_has_result(
|
def till_deferred_has_result(
|
||||||
self,
|
self,
|
||||||
@@ -914,12 +878,7 @@ class MasAuthDelegation(HomeserverTestCase):
|
|||||||
self.server.introspection_response = {
|
self.server.introspection_response = {
|
||||||
"active": True,
|
"active": True,
|
||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"scope": " ".join(
|
"scope": " ".join([self.api_scope, self.device_scope]),
|
||||||
[
|
|
||||||
MATRIX_USER_SCOPE,
|
|
||||||
f"{MATRIX_DEVICE_SCOPE_PREFIX}{DEVICE}",
|
|
||||||
]
|
|
||||||
),
|
|
||||||
"username": USERNAME,
|
"username": USERNAME,
|
||||||
"expires_in": 60,
|
"expires_in": 60,
|
||||||
}
|
}
|
||||||
@@ -943,12 +902,7 @@ class MasAuthDelegation(HomeserverTestCase):
|
|||||||
self.server.introspection_response = {
|
self.server.introspection_response = {
|
||||||
"active": True,
|
"active": True,
|
||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"scope": " ".join(
|
"scope": " ".join([self.api_scope, self.device_scope]),
|
||||||
[
|
|
||||||
MATRIX_USER_SCOPE,
|
|
||||||
f"{MATRIX_DEVICE_SCOPE_PREFIX}{DEVICE}",
|
|
||||||
]
|
|
||||||
),
|
|
||||||
"username": USERNAME,
|
"username": USERNAME,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -971,12 +925,7 @@ class MasAuthDelegation(HomeserverTestCase):
|
|||||||
self.server.introspection_response = {
|
self.server.introspection_response = {
|
||||||
"active": True,
|
"active": True,
|
||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"scope": " ".join(
|
"scope": " ".join([self.api_scope, f"{self.device_scope_prefix}ABCDEF"]),
|
||||||
[
|
|
||||||
MATRIX_USER_SCOPE,
|
|
||||||
f"{MATRIX_DEVICE_SCOPE_PREFIX}ABCDEF",
|
|
||||||
]
|
|
||||||
),
|
|
||||||
"username": USERNAME,
|
"username": USERNAME,
|
||||||
"expires_in": 60,
|
"expires_in": 60,
|
||||||
}
|
}
|
||||||
@@ -993,7 +942,7 @@ class MasAuthDelegation(HomeserverTestCase):
|
|||||||
self.server.introspection_response = {
|
self.server.introspection_response = {
|
||||||
"active": True,
|
"active": True,
|
||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"scope": " ".join([MATRIX_USER_SCOPE]),
|
"scope": " ".join([self.api_scope]),
|
||||||
"username": "inexistent_user",
|
"username": "inexistent_user",
|
||||||
"expires_in": 60,
|
"expires_in": 60,
|
||||||
}
|
}
|
||||||
@@ -1039,7 +988,7 @@ class MasAuthDelegation(HomeserverTestCase):
|
|||||||
self.server.introspection_response = {
|
self.server.introspection_response = {
|
||||||
"active": True,
|
"active": True,
|
||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"scope": MATRIX_USER_SCOPE,
|
"scope": self.api_scope,
|
||||||
"username": USERNAME,
|
"username": USERNAME,
|
||||||
"expires_in": 60,
|
"expires_in": 60,
|
||||||
"device_id": DEVICE,
|
"device_id": DEVICE,
|
||||||
@@ -1057,7 +1006,7 @@ class MasAuthDelegation(HomeserverTestCase):
|
|||||||
self.server.introspection_response = {
|
self.server.introspection_response = {
|
||||||
"active": True,
|
"active": True,
|
||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"scope": " ".join([SYNAPSE_ADMIN_SCOPE, MATRIX_USER_SCOPE]),
|
"scope": " ".join([SYNAPSE_ADMIN_SCOPE, self.api_scope]),
|
||||||
"username": USERNAME,
|
"username": USERNAME,
|
||||||
"expires_in": 60,
|
"expires_in": 60,
|
||||||
}
|
}
|
||||||
@@ -1079,12 +1028,7 @@ class MasAuthDelegation(HomeserverTestCase):
|
|||||||
self.server.introspection_response = {
|
self.server.introspection_response = {
|
||||||
"active": True,
|
"active": True,
|
||||||
"sub": SUBJECT,
|
"sub": SUBJECT,
|
||||||
"scope": " ".join(
|
"scope": " ".join([self.api_scope, self.device_scope]),
|
||||||
[
|
|
||||||
MATRIX_USER_SCOPE,
|
|
||||||
f"{MATRIX_DEVICE_SCOPE_PREFIX}{DEVICE}",
|
|
||||||
]
|
|
||||||
),
|
|
||||||
"username": USERNAME,
|
"username": USERNAME,
|
||||||
"expires_in": 60,
|
"expires_in": 60,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,11 @@
|
|||||||
# [This file includes modifications made by New Vector Limited]
|
# [This file includes modifications made by New Vector Limited]
|
||||||
#
|
#
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from typing import ClassVar
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from parameterized import parameterized_class
|
||||||
|
|
||||||
from synapse.rest.client import auth_metadata
|
from synapse.rest.client import auth_metadata
|
||||||
|
|
||||||
from tests.unittest import HomeserverTestCase, override_config, skip_unless
|
from tests.unittest import HomeserverTestCase, override_config, skip_unless
|
||||||
@@ -85,17 +88,22 @@ class AuthIssuerTestCase(HomeserverTestCase):
|
|||||||
req_mock.assert_not_called()
|
req_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@parameterized_class(
|
||||||
|
("endpoint",),
|
||||||
|
[
|
||||||
|
("/_matrix/client/unstable/org.matrix.msc2965/auth_metadata",),
|
||||||
|
("/_matrix/client/v1/auth_metadata",),
|
||||||
|
],
|
||||||
|
)
|
||||||
class AuthMetadataTestCase(HomeserverTestCase):
|
class AuthMetadataTestCase(HomeserverTestCase):
|
||||||
|
endpoint: ClassVar[str]
|
||||||
servlets = [
|
servlets = [
|
||||||
auth_metadata.register_servlets,
|
auth_metadata.register_servlets,
|
||||||
]
|
]
|
||||||
|
|
||||||
def test_returns_404_when_msc3861_disabled(self) -> None:
|
def test_returns_404_when_msc3861_disabled(self) -> None:
|
||||||
# Make an unauthenticated request for the discovery info.
|
# Make an unauthenticated request for the discovery info.
|
||||||
channel = self.make_request(
|
channel = self.make_request("GET", self.endpoint)
|
||||||
"GET",
|
|
||||||
"/_matrix/client/unstable/org.matrix.msc2965/auth_metadata",
|
|
||||||
)
|
|
||||||
self.assertEqual(channel.code, HTTPStatus.NOT_FOUND)
|
self.assertEqual(channel.code, HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
@skip_unless(HAS_AUTHLIB, "requires authlib")
|
@skip_unless(HAS_AUTHLIB, "requires authlib")
|
||||||
@@ -124,10 +132,7 @@ class AuthMetadataTestCase(HomeserverTestCase):
|
|||||||
)
|
)
|
||||||
self.hs.get_proxied_http_client().get_json = req_mock # type: ignore[method-assign]
|
self.hs.get_proxied_http_client().get_json = req_mock # type: ignore[method-assign]
|
||||||
|
|
||||||
channel = self.make_request(
|
channel = self.make_request("GET", self.endpoint)
|
||||||
"GET",
|
|
||||||
"/_matrix/client/unstable/org.matrix.msc2965/auth_metadata",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(channel.code, HTTPStatus.OK)
|
self.assertEqual(channel.code, HTTPStatus.OK)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|||||||
Reference in New Issue
Block a user