Skip to content

Commit

Permalink
fix: SCIM API actually downloads the avatar pictures and store them
Browse files Browse the repository at this point in the history
locally
  • Loading branch information
azmeuk committed Nov 15, 2024
1 parent 0e0abef commit aeaf74d
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 29 deletions.
67 changes: 48 additions & 19 deletions synapse/rest/scim.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from synapse.http.site import SynapseRequest
from synapse.rest.admin._base import assert_requester_is_admin, assert_user_is_admin
from synapse.types import JsonDict, UserID
from synapse.util.templates import mxc_to_http

try:
# As of version 0.2, scim2-models requires Pydantic 2.7+ but synapse only require Pydantic 1.7
Expand Down Expand Up @@ -193,11 +194,14 @@ async def get_scim_user(self, user_id: str) -> "User":
)

if profile.avatar_url:
http_url = mxc_to_http(
self.hs.config.server.public_baseurl, profile.avatar_url
)
scim_user.photos = [
Photo(
type=Photo.Type.photo,
primary=True,
value=profile.avatar_url,
value=http_url,
)
]

Expand Down Expand Up @@ -278,8 +282,19 @@ async def on_PUT(
)

if user.photos and user.photos[0].value:
await self.profile_handler.set_avatar_url(
user_id_obj, requester, str(user.photos[0].value), True
media_repo = (
self.hs.get_media_repository()
if self.hs.config.media.can_load_media_repo
else None
)
http_client = self.hs.get_proxied_blocklisted_http_client()

await self.profile_handler.set_avatar_from_http_url(
user_id,
str(user.photos[0].value),
media_repo,
http_client,
"scim_",
)

if threepids is not None:
Expand Down Expand Up @@ -381,48 +396,62 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
await assert_user_is_admin(self.auth, requester)

body = parse_json_object_from_request(request)
user = User.model_validate(body, scim_ctx=Context.RESOURCE_CREATION_REQUEST)
request_user = User.model_validate(
body, scim_ctx=Context.RESOURCE_CREATION_REQUEST
)

from synapse.rest.client.register import RegisterRestServlet

register = RegisterRestServlet(self.hs)

password_hash = (
await self.auth_handler.hash(user.password) if user.password else None
await self.auth_handler.hash(request_user.password)
if request_user.password
else None
)
user_id = await register.registration_handler.register_user(
by_admin=True,
approved=True,
localpart=user.user_name,
localpart=request_user.user_name,
password_hash=password_hash,
default_display_name=user.display_name,
default_display_name=request_user.display_name,
)

now_ts = self.hs.get_clock().time_msec()
if user.emails:
for email in user.emails:
if request_user.emails:
for email in request_user.emails:
if email.value:
await self.store.user_add_threepid(
user_id, "email", email.value, now_ts, now_ts
)

if user.phone_numbers:
for phone_number in user.phone_numbers:
if request_user.phone_numbers:
for phone_number in request_user.phone_numbers:
if phone_number.value:
await self.store.user_add_threepid(
user_id, "msisdn", phone_number.value, now_ts, now_ts
)

if user.photos and user.photos[0].value:
await self.profile_handler.set_avatar_url(
UserID.from_string(user_id),
requester,
str(user.photos[0].value),
True,
if request_user.photos and request_user.photos[0].value:
media_repo = (
self.hs.get_media_repository()
if self.hs.config.media.can_load_media_repo
else None
)
http_client = self.hs.get_proxied_blocklisted_http_client()

await self.profile_handler.set_avatar_from_http_url(
user_id,
str(request_user.photos[0].value),
media_repo,
http_client,
"scim_",
)

user = await self.get_scim_user(user_id)
payload = user.model_dump(scim_ctx=Context.RESOURCE_CREATION_RESPONSE)
response_user = await self.get_scim_user(user_id)
payload = response_user.model_dump(
scim_ctx=Context.RESOURCE_CREATION_RESPONSE
)
return HTTPStatus.CREATED, payload

except SynapseError as exc:
Expand Down
41 changes: 31 additions & 10 deletions tests/rest/test_scim.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from unittest import mock
from unittest.mock import Mock

from twisted.test.proto_helpers import MemoryReactor

Expand All @@ -12,6 +13,7 @@
from synapse.types import JsonDict, UserID
from synapse.util import Clock

from tests.handlers.test_profile import mock_get_file
from tests.unittest import HomeserverTestCase, skip_unless
from tests.utils import default_config

Expand Down Expand Up @@ -75,6 +77,17 @@ class UserProvisioningTestCase(HomeserverTestCase):
login.register_servlets,
]
url = "/_synapse/admin/scim/v2"
maxDiff = None

def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
self.http_client = Mock(spec=["get_file"])
self.http_client.get_file.side_effect = mock_get_file
self.http_client.user_agent = b"Synapse Test"

hs = self.setup_test_homeserver(
proxied_blocklisted_http_client=self.http_client,
)
return hs

def default_config(self) -> JsonDict:
conf = super().default_config()
Expand Down Expand Up @@ -108,7 +121,7 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.get_success(
self.store.set_profile_avatar_url(
UserID.from_string(self.user_user_id),
"https://mydomain.tld/photo.webp",
"mxc://servername/mediaid",
)
)

Expand Down Expand Up @@ -143,7 +156,7 @@ def test_get_user(self) -> None:
{
"type": "photo",
"primary": True,
"value": "https://mydomain.tld/photo.webp",
"value": "https://test/_matrix/media/v1/thumbnail/servername/mediaid",
}
],
},
Expand Down Expand Up @@ -200,7 +213,7 @@ def test_get_user_exclude_attribute(self) -> None:
{
"type": "photo",
"primary": True,
"value": "https://mydomain.tld/photo.webp",
"value": "https://test/_matrix/media/v1/thumbnail/servername/mediaid",
}
],
},
Expand Down Expand Up @@ -243,7 +256,7 @@ def test_get_users(self) -> None:
{
"type": "photo",
"primary": True,
"value": "https://mydomain.tld/photo.webp",
"value": "https://test/_matrix/media/v1/thumbnail/servername/mediaid",
}
],
}
Expand Down Expand Up @@ -337,7 +350,7 @@ def test_post_user(self) -> None:
{
"type": "photo",
"primary": True,
"value": "https://mydomain.tld/photo.webp",
"value": "http://my.server/me.png",
}
],
"active": True,
Expand Down Expand Up @@ -370,12 +383,16 @@ def test_post_user(self) -> None:
{
"type": "photo",
"primary": True,
"value": "https://mydomain.tld/photo.webp",
"value": mock.ANY,
}
],
"displayName": "bjensen display name",
}
self.assertEqual(expected, channel.json_body)
self.assertSubstring(
"https://test/_matrix/media/v1/thumbnail/test/",
channel.json_body["photos"][0]["value"],
)

channel = self.make_request(
"GET",
Expand Down Expand Up @@ -443,7 +460,7 @@ def test_replace_user(self) -> None:
{
"type": "photo",
"primary": True,
"value": "https://mydomain.tld/photo.webp",
"value": "https://test/_matrix/media/v1/thumbnail/servername/mediaid",
}
],
"active": True,
Expand All @@ -462,7 +479,7 @@ def test_replace_user(self) -> None:
{
"type": "photo",
"primary": True,
"value": "https://mydomain.tld/photo.webp",
"value": "http://my.server/me.png",
}
],
}
Expand Down Expand Up @@ -494,11 +511,15 @@ def test_replace_user(self) -> None:
{
"type": "photo",
"primary": True,
"value": "https://mydomain.tld/photo.webp",
"value": mock.ANY,
}
],
}
self.assertEqual(expected, channel.json_body)
self.assertSubstring(
"https://test/_matrix/media/v1/thumbnail/test/",
channel.json_body["photos"][0]["value"],
)

channel = self.make_request(
"GET",
Expand All @@ -523,7 +544,7 @@ def test_replace_invalid_user(self) -> None:
{
"type": "photo",
"primary": True,
"value": "https://mydomain.tld/photo.webp",
"value": "http://my.server/me.png",
}
],
}
Expand Down

0 comments on commit aeaf74d

Please sign in to comment.