mirror of
https://mau.dev/maunium/synapse.git
synced 2024-12-13 13:03:27 +01:00
Make AS tokens work & allow ASes to /register
This commit is contained in:
parent
c008b44b4f
commit
4d0231b364
5 changed files with 159 additions and 85 deletions
|
@ -14,6 +14,8 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Optional, Tuple
|
from typing import TYPE_CHECKING, Optional, Tuple
|
||||||
|
|
||||||
|
from netaddr import IPAddress
|
||||||
|
|
||||||
from twisted.web.server import Request
|
from twisted.web.server import Request
|
||||||
|
|
||||||
from synapse import event_auth
|
from synapse import event_auth
|
||||||
|
@ -26,7 +28,8 @@ from synapse.api.errors import (
|
||||||
)
|
)
|
||||||
from synapse.appservice import ApplicationService
|
from synapse.appservice import ApplicationService
|
||||||
from synapse.logging.opentracing import trace
|
from synapse.logging.opentracing import trace
|
||||||
from synapse.types import Requester
|
from synapse.types import Requester, create_requester
|
||||||
|
from synapse.util.cancellation import cancellable
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
@ -271,3 +274,78 @@ class BaseAuth:
|
||||||
raise MissingClientTokenError()
|
raise MissingClientTokenError()
|
||||||
|
|
||||||
return query_params[0].decode("ascii")
|
return query_params[0].decode("ascii")
|
||||||
|
|
||||||
|
@cancellable
|
||||||
|
async def get_appservice_user(
|
||||||
|
self, request: Request, access_token: str
|
||||||
|
) -> Optional[Requester]:
|
||||||
|
"""
|
||||||
|
Given a request, reads the request parameters to determine:
|
||||||
|
- whether it's an application service that's making this request
|
||||||
|
- what user the application service should be treated as controlling
|
||||||
|
(the user_id URI parameter allows an application service to masquerade
|
||||||
|
any applicable user in its namespace)
|
||||||
|
- what device the application service should be treated as controlling
|
||||||
|
(the device_id[^1] URI parameter allows an application service to masquerade
|
||||||
|
as any device that exists for the relevant user)
|
||||||
|
|
||||||
|
[^1] Unstable and provided by MSC3202.
|
||||||
|
Must use `org.matrix.msc3202.device_id` in place of `device_id` for now.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the application service `Requester` of that request
|
||||||
|
|
||||||
|
Postconditions:
|
||||||
|
- The `app_service` field in the returned `Requester` is set
|
||||||
|
- The `user_id` field in the returned `Requester` is either the application
|
||||||
|
service sender or the controlled user set by the `user_id` URI parameter
|
||||||
|
- The returned application service is permitted to control the returned user ID.
|
||||||
|
- The returned device ID, if present, has been checked to be a valid device ID
|
||||||
|
for the returned user ID.
|
||||||
|
"""
|
||||||
|
DEVICE_ID_ARG_NAME = b"org.matrix.msc3202.device_id"
|
||||||
|
|
||||||
|
app_service = self.store.get_app_service_by_token(access_token)
|
||||||
|
if app_service is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if app_service.ip_range_whitelist:
|
||||||
|
ip_address = IPAddress(request.getClientAddress().host)
|
||||||
|
if ip_address not in app_service.ip_range_whitelist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# This will always be set by the time Twisted calls us.
|
||||||
|
assert request.args is not None
|
||||||
|
|
||||||
|
if b"user_id" in request.args:
|
||||||
|
effective_user_id = request.args[b"user_id"][0].decode("utf8")
|
||||||
|
await self.validate_appservice_can_control_user_id(
|
||||||
|
app_service, effective_user_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
effective_user_id = app_service.sender
|
||||||
|
|
||||||
|
effective_device_id: Optional[str] = None
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.hs.config.experimental.msc3202_device_masquerading_enabled
|
||||||
|
and DEVICE_ID_ARG_NAME in request.args
|
||||||
|
):
|
||||||
|
effective_device_id = request.args[DEVICE_ID_ARG_NAME][0].decode("utf8")
|
||||||
|
# We only just set this so it can't be None!
|
||||||
|
assert effective_device_id is not None
|
||||||
|
device_opt = await self.store.get_device(
|
||||||
|
effective_user_id, effective_device_id
|
||||||
|
)
|
||||||
|
if device_opt is None:
|
||||||
|
# For now, use 400 M_EXCLUSIVE if the device doesn't exist.
|
||||||
|
# This is an open thread of discussion on MSC3202 as of 2021-12-09.
|
||||||
|
raise AuthError(
|
||||||
|
400,
|
||||||
|
f"Application service trying to use a device that doesn't exist ('{effective_device_id}' for {effective_user_id})",
|
||||||
|
Codes.EXCLUSIVE,
|
||||||
|
)
|
||||||
|
|
||||||
|
return create_requester(
|
||||||
|
effective_user_id, app_service=app_service, device_id=effective_device_id
|
||||||
|
)
|
||||||
|
|
|
@ -12,12 +12,9 @@
|
||||||
# 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.
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import pymacaroons
|
import pymacaroons
|
||||||
from netaddr import IPAddress
|
|
||||||
|
|
||||||
from twisted.web.server import Request
|
|
||||||
|
|
||||||
from synapse.api.errors import (
|
from synapse.api.errors import (
|
||||||
AuthError,
|
AuthError,
|
||||||
|
@ -122,7 +119,7 @@ class InternalAuth(BaseAuth):
|
||||||
access_token = self.get_access_token_from_request(request)
|
access_token = self.get_access_token_from_request(request)
|
||||||
|
|
||||||
# First check if it could be a request from an appservice
|
# First check if it could be a request from an appservice
|
||||||
requester = await self._get_appservice_user(request)
|
requester = await self.get_appservice_user(request, access_token)
|
||||||
if not requester:
|
if not requester:
|
||||||
# If not, it should be from a regular user
|
# If not, it should be from a regular user
|
||||||
requester = await self.get_user_by_access_token(
|
requester = await self.get_user_by_access_token(
|
||||||
|
@ -189,81 +186,6 @@ class InternalAuth(BaseAuth):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise MissingClientTokenError()
|
raise MissingClientTokenError()
|
||||||
|
|
||||||
@cancellable
|
|
||||||
async def _get_appservice_user(self, request: Request) -> Optional[Requester]:
|
|
||||||
"""
|
|
||||||
Given a request, reads the request parameters to determine:
|
|
||||||
- whether it's an application service that's making this request
|
|
||||||
- what user the application service should be treated as controlling
|
|
||||||
(the user_id URI parameter allows an application service to masquerade
|
|
||||||
any applicable user in its namespace)
|
|
||||||
- what device the application service should be treated as controlling
|
|
||||||
(the device_id[^1] URI parameter allows an application service to masquerade
|
|
||||||
as any device that exists for the relevant user)
|
|
||||||
|
|
||||||
[^1] Unstable and provided by MSC3202.
|
|
||||||
Must use `org.matrix.msc3202.device_id` in place of `device_id` for now.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
the application service `Requester` of that request
|
|
||||||
|
|
||||||
Postconditions:
|
|
||||||
- The `app_service` field in the returned `Requester` is set
|
|
||||||
- The `user_id` field in the returned `Requester` is either the application
|
|
||||||
service sender or the controlled user set by the `user_id` URI parameter
|
|
||||||
- The returned application service is permitted to control the returned user ID.
|
|
||||||
- The returned device ID, if present, has been checked to be a valid device ID
|
|
||||||
for the returned user ID.
|
|
||||||
"""
|
|
||||||
DEVICE_ID_ARG_NAME = b"org.matrix.msc3202.device_id"
|
|
||||||
|
|
||||||
app_service = self.store.get_app_service_by_token(
|
|
||||||
self.get_access_token_from_request(request)
|
|
||||||
)
|
|
||||||
if app_service is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if app_service.ip_range_whitelist:
|
|
||||||
ip_address = IPAddress(request.getClientAddress().host)
|
|
||||||
if ip_address not in app_service.ip_range_whitelist:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# This will always be set by the time Twisted calls us.
|
|
||||||
assert request.args is not None
|
|
||||||
|
|
||||||
if b"user_id" in request.args:
|
|
||||||
effective_user_id = request.args[b"user_id"][0].decode("utf8")
|
|
||||||
await self.validate_appservice_can_control_user_id(
|
|
||||||
app_service, effective_user_id
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
effective_user_id = app_service.sender
|
|
||||||
|
|
||||||
effective_device_id: Optional[str] = None
|
|
||||||
|
|
||||||
if (
|
|
||||||
self.hs.config.experimental.msc3202_device_masquerading_enabled
|
|
||||||
and DEVICE_ID_ARG_NAME in request.args
|
|
||||||
):
|
|
||||||
effective_device_id = request.args[DEVICE_ID_ARG_NAME][0].decode("utf8")
|
|
||||||
# We only just set this so it can't be None!
|
|
||||||
assert effective_device_id is not None
|
|
||||||
device_opt = await self.store.get_device(
|
|
||||||
effective_user_id, effective_device_id
|
|
||||||
)
|
|
||||||
if device_opt is None:
|
|
||||||
# For now, use 400 M_EXCLUSIVE if the device doesn't exist.
|
|
||||||
# This is an open thread of discussion on MSC3202 as of 2021-12-09.
|
|
||||||
raise AuthError(
|
|
||||||
400,
|
|
||||||
f"Application service trying to use a device that doesn't exist ('{effective_device_id}' for {effective_user_id})",
|
|
||||||
Codes.EXCLUSIVE,
|
|
||||||
)
|
|
||||||
|
|
||||||
return create_requester(
|
|
||||||
effective_user_id, app_service=app_service, device_id=effective_device_id
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_user_by_access_token(
|
async def get_user_by_access_token(
|
||||||
self,
|
self,
|
||||||
token: str,
|
token: str,
|
||||||
|
|
|
@ -162,7 +162,10 @@ class MSC3861DelegatedAuth(BaseAuth):
|
||||||
) -> Requester:
|
) -> Requester:
|
||||||
access_token = self.get_access_token_from_request(request)
|
access_token = self.get_access_token_from_request(request)
|
||||||
|
|
||||||
# TODO: we probably want to assert the allow_guest inside this call so that we don't provision the user if they don't have enough permission:
|
requester = await self.get_appservice_user(request, access_token)
|
||||||
|
if not requester:
|
||||||
|
# TODO: we probably want to assert the allow_guest inside this call
|
||||||
|
# so that we don't provision the user if they don't have enough permission:
|
||||||
requester = await self.get_user_by_access_token(access_token, allow_expired)
|
requester = await self.get_user_by_access_token(access_token, allow_expired)
|
||||||
|
|
||||||
if not allow_guest and requester.is_guest:
|
if not allow_guest and requester.is_guest:
|
||||||
|
@ -170,6 +173,8 @@ class MSC3861DelegatedAuth(BaseAuth):
|
||||||
["urn:matrix:org.matrix.msc2967.client:api:*"]
|
["urn:matrix:org.matrix.msc2967.client:api:*"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
request.requester = requester
|
||||||
|
|
||||||
return requester
|
return requester
|
||||||
|
|
||||||
async def get_user_by_access_token(
|
async def get_user_by_access_token(
|
||||||
|
|
|
@ -869,6 +869,74 @@ class RegisterRestServlet(RestServlet):
|
||||||
return 200, result
|
return 200, result
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterAppServiceOnlyRestServlet(RestServlet):
|
||||||
|
"""An alternative registration API endpoint that only allows ASes to register
|
||||||
|
|
||||||
|
This replaces the regular /register endpoint if MSC3861. There are two notable
|
||||||
|
differences with the regular /register endpoint:
|
||||||
|
- It only allows the `m.login.application_service` login type
|
||||||
|
- It does not create a device or access token for the just-registered user
|
||||||
|
|
||||||
|
Note that the exact behaviour of this endpoint is not yet finalised. It should be
|
||||||
|
just good enough to make most ASes work.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PATTERNS = client_patterns("/register$")
|
||||||
|
CATEGORY = "Registration/login requests"
|
||||||
|
|
||||||
|
def __init__(self, hs: "HomeServer"):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.auth = hs.get_auth()
|
||||||
|
self.registration_handler = hs.get_registration_handler()
|
||||||
|
self.ratelimiter = hs.get_registration_ratelimiter()
|
||||||
|
|
||||||
|
@interactive_auth_handler
|
||||||
|
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||||
|
body = parse_json_object_from_request(request)
|
||||||
|
|
||||||
|
client_addr = request.getClientAddress().host
|
||||||
|
|
||||||
|
await self.ratelimiter.ratelimit(None, client_addr, update=False)
|
||||||
|
|
||||||
|
kind = parse_string(request, "kind", default="user")
|
||||||
|
|
||||||
|
if kind == "guest":
|
||||||
|
raise SynapseError(403, "Guest access is disabled")
|
||||||
|
elif kind != "user":
|
||||||
|
raise UnrecognizedRequestError(
|
||||||
|
f"Do not understand membership kind: {kind}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pull out the provided username and do basic sanity checks early since
|
||||||
|
# the auth layer will store these in sessions.
|
||||||
|
desired_username = body.get("username")
|
||||||
|
if not isinstance(desired_username, str) or len(desired_username) > 512:
|
||||||
|
raise SynapseError(400, "Invalid username")
|
||||||
|
|
||||||
|
# Allow only ASes to use this API.
|
||||||
|
if body.get("type") != APP_SERVICE_REGISTRATION_TYPE:
|
||||||
|
raise SynapseError(403, "Non-application service registration type")
|
||||||
|
|
||||||
|
if not self.auth.has_access_token(request):
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Appservice token must be provided when using a type of m.login.application_service",
|
||||||
|
)
|
||||||
|
|
||||||
|
# XXX we should check that desired_username is valid. Currently
|
||||||
|
# we give appservices carte blanche for any insanity in mxids,
|
||||||
|
# because the IRC bridges rely on being able to register stupid
|
||||||
|
# IDs.
|
||||||
|
|
||||||
|
as_token = self.auth.get_access_token_from_request(request)
|
||||||
|
|
||||||
|
user_id = await self.registration_handler.appservice_register(
|
||||||
|
desired_username, as_token
|
||||||
|
)
|
||||||
|
return 200, {"user_id": user_id}
|
||||||
|
|
||||||
|
|
||||||
def _calculate_registration_flows(
|
def _calculate_registration_flows(
|
||||||
config: HomeServerConfig, auth_handler: AuthHandler
|
config: HomeServerConfig, auth_handler: AuthHandler
|
||||||
) -> List[List[str]]:
|
) -> List[List[str]]:
|
||||||
|
@ -956,6 +1024,7 @@ def _calculate_registration_flows(
|
||||||
|
|
||||||
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
||||||
if hs.config.experimental.msc3861.enabled:
|
if hs.config.experimental.msc3861.enabled:
|
||||||
|
RegisterAppServiceOnlyRestServlet(hs).register(http_server)
|
||||||
return
|
return
|
||||||
|
|
||||||
if hs.config.worker.worker_app is None:
|
if hs.config.worker.worker_app is None:
|
||||||
|
|
|
@ -527,8 +527,8 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
|
||||||
self.expect_unrecognized(
|
self.expect_unrecognized(
|
||||||
"GET", "/_matrix/client/v1/register/m.login.registration_token/validity"
|
"GET", "/_matrix/client/v1/register/m.login.registration_token/validity"
|
||||||
)
|
)
|
||||||
self.expect_unrecognized("POST", "/_matrix/client/v3/register")
|
# This is still available for AS registrations
|
||||||
self.expect_unrecognized("GET", "/_matrix/client/v3/register")
|
# self.expect_unrecognized("POST", "/_matrix/client/v3/register")
|
||||||
self.expect_unrecognized("GET", "/_matrix/client/v3/register/available")
|
self.expect_unrecognized("GET", "/_matrix/client/v3/register/available")
|
||||||
self.expect_unrecognized(
|
self.expect_unrecognized(
|
||||||
"POST", "/_matrix/client/v3/register/email/requestToken"
|
"POST", "/_matrix/client/v3/register/email/requestToken"
|
||||||
|
|
Loading…
Reference in a new issue