summary refs log blame commit diff
path: root/tests/rest/admin/test_admin.py
blob: 3adadcb46bc49dc41d86084b318993dfc2db81f1 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
                                                      











                                                                          
                   
                           
                              
 
                                           
                         
                                            
                                                           
                                             
                                                   
                          
                                               
                                      
 
                                                   
                                             
 
                                   

                                                  
                                  
                                                                     
 
                                                                            
                         
                                                                               
         
 
 
                                                       
                                                                      



                                          






                                                                         
                                    
                   
                             
                                             
                                          
         
                                                                            

                                                
                                                              


                                                                              
                                    
                                                                                    
         
                                                                            
                                                           
                                    
                                                                                      
         
                                                                            




                                                                                   
                                                           
                                    
                   
                                
                                             
                                          
         
                                                                            
 
                                                                     









                                                                                      
                                    
                                                                        
         
                                                                          
                                                   
                                                                                  
                                                                                        
 
                                                                            
                                          
 
                                                           
                                           







                                                            


                                                                      



                                              
                                                                                       











                                                                             
                                                   





















                                                                                          
                                                                                     

                 
                                                                              
                               
                         
                                                           


                                        
         

                               
                                 
                 
                                                                                               


                                     




                                                                          
                                    

                                            
         

                                  
                                 



                                                                            
                                    

                                            
         

                                  
                                 










                                                                          
                                                               

                                            
                                                                                  

                                                                   
                               
                         
                                                           



                                            
                              
                                                     




                                                             



                                        
                      
                                                                            
                                     
                                                                          
 
                                                                            









                                                                                 
                                                                   
                                              
                                                                   





















                                                                            




                                                                                     



                                        
                      
                                                                            
                         
                                                                                     





                                                               
                                                                       
 
                                                                    

                                                                                        







                                                                                 
                                                                   
                                              
                                                                   








                                                                                 
                                    

                                        
         
                      
                                                                            
                         
                                                                                     

                                               










                                                                                 
                                                                   
                                              
                                                                   






                                                             


                                                                                       
                                                                            



                                                                                 
                                    

                                        
         
                      
                                                                            
                         
                                                                                    
         


                                                                           
                                               
                               
                         
                                                           



                                            
 
                                  
                         
                          
                 
                                                                                            

                                       























                                                                         
                                                                                     







                                                                       
                                                                            








                                                    
                                                                            
                                                                 
# Copyright 2018-2021 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import urllib.parse
from http import HTTPStatus
from unittest.mock import Mock

from twisted.internet.defer import Deferred

import synapse.rest.admin
from synapse.http.server import JsonResource
from synapse.logging.context import make_deferred_yieldable
from synapse.rest.admin import VersionServlet
from synapse.rest.client import groups, login, room

from tests import unittest
from tests.server import FakeSite, make_request
from tests.test_utils import SMALL_PNG


class VersionTestCase(unittest.HomeserverTestCase):
    url = "/_synapse/admin/v1/server_version"

    def create_test_resource(self):
        resource = JsonResource(self.hs)
        VersionServlet(self.hs).register(resource)
        return resource

    def test_version_string(self):
        channel = self.make_request("GET", self.url, shorthand=False)

        self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
        self.assertEqual(
            {"server_version", "python_version"}, set(channel.json_body.keys())
        )


class DeleteGroupTestCase(unittest.HomeserverTestCase):
    servlets = [
        synapse.rest.admin.register_servlets_for_client_rest_resource,
        login.register_servlets,
        groups.register_servlets,
    ]

    def prepare(self, reactor, clock, hs):
        self.admin_user = self.register_user("admin", "pass", admin=True)
        self.admin_user_tok = self.login("admin", "pass")

        self.other_user = self.register_user("user", "pass")
        self.other_user_token = self.login("user", "pass")

    def test_delete_group(self):
        # Create a new group
        channel = self.make_request(
            "POST",
            b"/create_group",
            access_token=self.admin_user_tok,
            content={"localpart": "test"},
        )

        self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)

        group_id = channel.json_body["group_id"]

        self._check_group(group_id, expect_code=HTTPStatus.OK)

        # Invite/join another user

        url = "/groups/%s/admin/users/invite/%s" % (group_id, self.other_user)
        channel = self.make_request(
            "PUT", url.encode("ascii"), access_token=self.admin_user_tok, content={}
        )
        self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)

        url = "/groups/%s/self/accept_invite" % (group_id,)
        channel = self.make_request(
            "PUT", url.encode("ascii"), access_token=self.other_user_token, content={}
        )
        self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)

        # Check other user knows they're in the group
        self.assertIn(group_id, self._get_groups_user_is_in(self.admin_user_tok))
        self.assertIn(group_id, self._get_groups_user_is_in(self.other_user_token))

        # Now delete the group
        url = "/_synapse/admin/v1/delete_group/" + group_id
        channel = self.make_request(
            "POST",
            url.encode("ascii"),
            access_token=self.admin_user_tok,
            content={"localpart": "test"},
        )

        self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)

        # Check group returns HTTPStatus.NOT_FOUND
        self._check_group(group_id, expect_code=HTTPStatus.NOT_FOUND)

        # Check users don't think they're in the group
        self.assertNotIn(group_id, self._get_groups_user_is_in(self.admin_user_tok))
        self.assertNotIn(group_id, self._get_groups_user_is_in(self.other_user_token))

    def _check_group(self, group_id, expect_code):
        """Assert that trying to fetch the given group results in the given
        HTTP status code
        """

        url = "/groups/%s/profile" % (group_id,)
        channel = self.make_request(
            "GET", url.encode("ascii"), access_token=self.admin_user_tok
        )

        self.assertEqual(expect_code, channel.code, msg=channel.json_body)

    def _get_groups_user_is_in(self, access_token):
        """Returns the list of groups the user is in (given their access token)"""
        channel = self.make_request("GET", b"/joined_groups", access_token=access_token)

        self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)

        return channel.json_body["groups"]


class QuarantineMediaTestCase(unittest.HomeserverTestCase):
    """Test /quarantine_media admin API."""

    servlets = [
        synapse.rest.admin.register_servlets,
        synapse.rest.admin.register_servlets_for_media_repo,
        login.register_servlets,
        room.register_servlets,
    ]

    def prepare(self, reactor, clock, hs):
        # Allow for uploading and downloading to/from the media repo
        self.media_repo = hs.get_media_repository_resource()
        self.download_resource = self.media_repo.children[b"download"]
        self.upload_resource = self.media_repo.children[b"upload"]

    def make_homeserver(self, reactor, clock):

        self.fetches = []

        async def get_file(destination, path, output_stream, args=None, max_size=None):
            """
            Returns tuple[int,dict,str,int] of file length, response headers,
            absolute URI, and response code.
            """

            def write_to(r):
                data, response = r
                output_stream.write(data)
                return response

            d = Deferred()
            d.addCallback(write_to)
            self.fetches.append((d, destination, path, args))
            return await make_deferred_yieldable(d)

        client = Mock()
        client.get_file = get_file

        self.storage_path = self.mktemp()
        self.media_store_path = self.mktemp()
        os.mkdir(self.storage_path)
        os.mkdir(self.media_store_path)

        config = self.default_config()
        config["media_store_path"] = self.media_store_path
        config["thumbnail_requirements"] = {}
        config["max_image_pixels"] = 2000000

        provider_config = {
            "module": "synapse.rest.media.v1.storage_provider.FileStorageProviderBackend",
            "store_local": True,
            "store_synchronous": False,
            "store_remote": True,
            "config": {"directory": self.storage_path},
        }
        config["media_storage_providers"] = [provider_config]

        hs = self.setup_test_homeserver(config=config, federation_http_client=client)

        return hs

    def _ensure_quarantined(self, admin_user_tok, server_and_media_id):
        """Ensure a piece of media is quarantined when trying to access it."""
        channel = make_request(
            self.reactor,
            FakeSite(self.download_resource, self.reactor),
            "GET",
            server_and_media_id,
            shorthand=False,
            access_token=admin_user_tok,
        )

        # Should be quarantined
        self.assertEqual(
            HTTPStatus.NOT_FOUND,
            channel.code,
            msg=(
                "Expected to receive a HTTPStatus.NOT_FOUND on accessing quarantined media: %s"
                % server_and_media_id
            ),
        )

    def test_quarantine_media_requires_admin(self):
        self.register_user("nonadmin", "pass", admin=False)
        non_admin_user_tok = self.login("nonadmin", "pass")

        # Attempt quarantine media APIs as non-admin
        url = "/_synapse/admin/v1/media/quarantine/example.org/abcde12345"
        channel = self.make_request(
            "POST",
            url.encode("ascii"),
            access_token=non_admin_user_tok,
        )

        # Expect a forbidden error
        self.assertEqual(
            HTTPStatus.FORBIDDEN,
            channel.code,
            msg="Expected forbidden on quarantining media as a non-admin",
        )

        # And the roomID/userID endpoint
        url = "/_synapse/admin/v1/room/!room%3Aexample.com/media/quarantine"
        channel = self.make_request(
            "POST",
            url.encode("ascii"),
            access_token=non_admin_user_tok,
        )

        # Expect a forbidden error
        self.assertEqual(
            HTTPStatus.FORBIDDEN,
            channel.code,
            msg="Expected forbidden on quarantining media as a non-admin",
        )

    def test_quarantine_media_by_id(self):
        self.register_user("id_admin", "pass", admin=True)
        admin_user_tok = self.login("id_admin", "pass")

        self.register_user("id_nonadmin", "pass", admin=False)
        non_admin_user_tok = self.login("id_nonadmin", "pass")

        # Upload some media into the room
        response = self.helper.upload_media(
            self.upload_resource, SMALL_PNG, tok=admin_user_tok
        )

        # Extract media ID from the response
        server_name_and_media_id = response["content_uri"][6:]  # Cut off 'mxc://'
        server_name, media_id = server_name_and_media_id.split("/")

        # Attempt to access the media
        channel = make_request(
            self.reactor,
            FakeSite(self.download_resource, self.reactor),
            "GET",
            server_name_and_media_id,
            shorthand=False,
            access_token=non_admin_user_tok,
        )

        # Should be successful
        self.assertEqual(HTTPStatus.OK, channel.code)

        # Quarantine the media
        url = "/_synapse/admin/v1/media/quarantine/%s/%s" % (
            urllib.parse.quote(server_name),
            urllib.parse.quote(media_id),
        )
        channel = self.make_request(
            "POST",
            url,
            access_token=admin_user_tok,
        )
        self.pump(1.0)
        self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)

        # Attempt to access the media
        self._ensure_quarantined(admin_user_tok, server_name_and_media_id)

    def test_quarantine_all_media_in_room(self, override_url_template=None):
        self.register_user("room_admin", "pass", admin=True)
        admin_user_tok = self.login("room_admin", "pass")

        non_admin_user = self.register_user("room_nonadmin", "pass", admin=False)
        non_admin_user_tok = self.login("room_nonadmin", "pass")

        room_id = self.helper.create_room_as(non_admin_user, tok=admin_user_tok)
        self.helper.join(room_id, non_admin_user, tok=non_admin_user_tok)

        # Upload some media
        response_1 = self.helper.upload_media(
            self.upload_resource, SMALL_PNG, tok=non_admin_user_tok
        )
        response_2 = self.helper.upload_media(
            self.upload_resource, SMALL_PNG, tok=non_admin_user_tok
        )

        # Extract mxcs
        mxc_1 = response_1["content_uri"]
        mxc_2 = response_2["content_uri"]

        # Send it into the room
        self.helper.send_event(
            room_id,
            "m.room.message",
            content={"body": "image-1", "msgtype": "m.image", "url": mxc_1},
            txn_id="111",
            tok=non_admin_user_tok,
        )
        self.helper.send_event(
            room_id,
            "m.room.message",
            content={"body": "image-2", "msgtype": "m.image", "url": mxc_2},
            txn_id="222",
            tok=non_admin_user_tok,
        )

        # Quarantine all media in the room
        if override_url_template:
            url = override_url_template % urllib.parse.quote(room_id)
        else:
            url = "/_synapse/admin/v1/room/%s/media/quarantine" % urllib.parse.quote(
                room_id
            )
        channel = self.make_request(
            "POST",
            url,
            access_token=admin_user_tok,
        )
        self.pump(1.0)
        self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
        self.assertEqual(
            channel.json_body, {"num_quarantined": 2}, "Expected 2 quarantined items"
        )

        # Convert mxc URLs to server/media_id strings
        server_and_media_id_1 = mxc_1[6:]
        server_and_media_id_2 = mxc_2[6:]

        # Test that we cannot download any of the media anymore
        self._ensure_quarantined(admin_user_tok, server_and_media_id_1)
        self._ensure_quarantined(admin_user_tok, server_and_media_id_2)

    def test_quarantine_all_media_in_room_deprecated_api_path(self):
        # Perform the above test with the deprecated API path
        self.test_quarantine_all_media_in_room("/_synapse/admin/v1/quarantine_media/%s")

    def test_quarantine_all_media_by_user(self):
        self.register_user("user_admin", "pass", admin=True)
        admin_user_tok = self.login("user_admin", "pass")

        non_admin_user = self.register_user("user_nonadmin", "pass", admin=False)
        non_admin_user_tok = self.login("user_nonadmin", "pass")

        # Upload some media
        response_1 = self.helper.upload_media(
            self.upload_resource, SMALL_PNG, tok=non_admin_user_tok
        )
        response_2 = self.helper.upload_media(
            self.upload_resource, SMALL_PNG, tok=non_admin_user_tok
        )

        # Extract media IDs
        server_and_media_id_1 = response_1["content_uri"][6:]
        server_and_media_id_2 = response_2["content_uri"][6:]

        # Quarantine all media by this user
        url = "/_synapse/admin/v1/user/%s/media/quarantine" % urllib.parse.quote(
            non_admin_user
        )
        channel = self.make_request(
            "POST",
            url.encode("ascii"),
            access_token=admin_user_tok,
        )
        self.pump(1.0)
        self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
        self.assertEqual(
            channel.json_body, {"num_quarantined": 2}, "Expected 2 quarantined items"
        )

        # Attempt to access each piece of media
        self._ensure_quarantined(admin_user_tok, server_and_media_id_1)
        self._ensure_quarantined(admin_user_tok, server_and_media_id_2)

    def test_cannot_quarantine_safe_media(self):
        self.register_user("user_admin", "pass", admin=True)
        admin_user_tok = self.login("user_admin", "pass")

        non_admin_user = self.register_user("user_nonadmin", "pass", admin=False)
        non_admin_user_tok = self.login("user_nonadmin", "pass")

        # Upload some media
        response_1 = self.helper.upload_media(
            self.upload_resource, SMALL_PNG, tok=non_admin_user_tok
        )
        response_2 = self.helper.upload_media(
            self.upload_resource, SMALL_PNG, tok=non_admin_user_tok
        )

        # Extract media IDs
        server_and_media_id_1 = response_1["content_uri"][6:]
        server_and_media_id_2 = response_2["content_uri"][6:]

        # Mark the second item as safe from quarantine.
        _, media_id_2 = server_and_media_id_2.split("/")
        # Quarantine the media
        url = "/_synapse/admin/v1/media/protect/%s" % (urllib.parse.quote(media_id_2),)
        channel = self.make_request("POST", url, access_token=admin_user_tok)
        self.pump(1.0)
        self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)

        # Quarantine all media by this user
        url = "/_synapse/admin/v1/user/%s/media/quarantine" % urllib.parse.quote(
            non_admin_user
        )
        channel = self.make_request(
            "POST",
            url.encode("ascii"),
            access_token=admin_user_tok,
        )
        self.pump(1.0)
        self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
        self.assertEqual(
            channel.json_body, {"num_quarantined": 1}, "Expected 1 quarantined item"
        )

        # Attempt to access each piece of media, the first should fail, the
        # second should succeed.
        self._ensure_quarantined(admin_user_tok, server_and_media_id_1)

        # Attempt to access each piece of media
        channel = make_request(
            self.reactor,
            FakeSite(self.download_resource, self.reactor),
            "GET",
            server_and_media_id_2,
            shorthand=False,
            access_token=non_admin_user_tok,
        )

        # Shouldn't be quarantined
        self.assertEqual(
            HTTPStatus.OK,
            channel.code,
            msg=(
                "Expected to receive a HTTPStatus.OK on accessing not-quarantined media: %s"
                % server_and_media_id_2
            ),
        )


class PurgeHistoryTestCase(unittest.HomeserverTestCase):
    servlets = [
        synapse.rest.admin.register_servlets,
        login.register_servlets,
        room.register_servlets,
    ]

    def prepare(self, reactor, clock, hs):
        self.admin_user = self.register_user("admin", "pass", admin=True)
        self.admin_user_tok = self.login("admin", "pass")

        self.other_user = self.register_user("user", "pass")
        self.other_user_tok = self.login("user", "pass")

        self.room_id = self.helper.create_room_as(
            self.other_user, tok=self.other_user_tok
        )
        self.url = f"/_synapse/admin/v1/purge_history/{self.room_id}"
        self.url_status = "/_synapse/admin/v1/purge_history_status/"

    def test_purge_history(self):
        """
        Simple test of purge history API.
        Test only that is is possible to call, get status HTTPStatus.OK and purge_id.
        """

        channel = self.make_request(
            "POST",
            self.url,
            content={"delete_local_events": True, "purge_up_to_ts": 0},
            access_token=self.admin_user_tok,
        )

        self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
        self.assertIn("purge_id", channel.json_body)
        purge_id = channel.json_body["purge_id"]

        # get status
        channel = self.make_request(
            "GET",
            self.url_status + purge_id,
            access_token=self.admin_user_tok,
        )

        self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body)
        self.assertEqual("complete", channel.json_body["status"])