You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

248 lines
8.2 KiB

  1. #!/usr/bin/env python
  2. # Copyright 2019 Matrix.org Foundation C.I.C.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import argparse
  16. import json
  17. import logging
  18. import os
  19. import sys
  20. import tempfile
  21. from twisted.internet import defer, task
  22. import synapse
  23. from synapse.app import _base
  24. from synapse.config._base import ConfigError
  25. from synapse.config.homeserver import HomeServerConfig
  26. from synapse.config.logger import setup_logging
  27. from synapse.handlers.admin import ExfiltrationWriter
  28. from synapse.replication.slave.storage._base import BaseSlavedStore
  29. from synapse.replication.slave.storage.account_data import SlavedAccountDataStore
  30. from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore
  31. from synapse.replication.slave.storage.client_ips import SlavedClientIpStore
  32. from synapse.replication.slave.storage.deviceinbox import SlavedDeviceInboxStore
  33. from synapse.replication.slave.storage.devices import SlavedDeviceStore
  34. from synapse.replication.slave.storage.events import SlavedEventStore
  35. from synapse.replication.slave.storage.filtering import SlavedFilteringStore
  36. from synapse.replication.slave.storage.groups import SlavedGroupServerStore
  37. from synapse.replication.slave.storage.presence import SlavedPresenceStore
  38. from synapse.replication.slave.storage.push_rule import SlavedPushRuleStore
  39. from synapse.replication.slave.storage.receipts import SlavedReceiptsStore
  40. from synapse.replication.slave.storage.registration import SlavedRegistrationStore
  41. from synapse.replication.slave.storage.room import RoomStore
  42. from synapse.server import HomeServer
  43. from synapse.util.logcontext import LoggingContext
  44. from synapse.util.versionstring import get_version_string
  45. logger = logging.getLogger("synapse.app.admin_cmd")
  46. class AdminCmdSlavedStore(
  47. SlavedReceiptsStore,
  48. SlavedAccountDataStore,
  49. SlavedApplicationServiceStore,
  50. SlavedRegistrationStore,
  51. SlavedFilteringStore,
  52. SlavedPresenceStore,
  53. SlavedGroupServerStore,
  54. SlavedDeviceInboxStore,
  55. SlavedDeviceStore,
  56. SlavedPushRuleStore,
  57. SlavedEventStore,
  58. SlavedClientIpStore,
  59. RoomStore,
  60. BaseSlavedStore,
  61. ):
  62. pass
  63. class AdminCmdServer(HomeServer):
  64. DATASTORE_CLASS = AdminCmdSlavedStore
  65. def _listen_http(self, listener_config):
  66. pass
  67. def start_listening(self, listeners):
  68. pass
  69. async def export_data_command(hs, args):
  70. """Export data for a user.
  71. Args:
  72. hs (HomeServer)
  73. args (argparse.Namespace)
  74. """
  75. user_id = args.user_id
  76. directory = args.output_directory
  77. res = await hs.get_admin_handler().export_user_data(
  78. user_id, FileExfiltrationWriter(user_id, directory=directory)
  79. )
  80. print(res)
  81. class FileExfiltrationWriter(ExfiltrationWriter):
  82. """An ExfiltrationWriter that writes the users data to a directory.
  83. Returns the directory location on completion.
  84. Note: This writes to disk on the main reactor thread.
  85. Args:
  86. user_id (str): The user whose data is being exfiltrated.
  87. directory (str|None): The directory to write the data to, if None then
  88. will write to a temporary directory.
  89. """
  90. def __init__(self, user_id, directory=None):
  91. self.user_id = user_id
  92. if directory:
  93. self.base_directory = directory
  94. else:
  95. self.base_directory = tempfile.mkdtemp(
  96. prefix="synapse-exfiltrate__%s__" % (user_id,)
  97. )
  98. os.makedirs(self.base_directory, exist_ok=True)
  99. if list(os.listdir(self.base_directory)):
  100. raise Exception("Directory must be empty")
  101. def write_events(self, room_id, events):
  102. room_directory = os.path.join(self.base_directory, "rooms", room_id)
  103. os.makedirs(room_directory, exist_ok=True)
  104. events_file = os.path.join(room_directory, "events")
  105. with open(events_file, "a") as f:
  106. for event in events:
  107. print(json.dumps(event.get_pdu_json()), file=f)
  108. def write_state(self, room_id, event_id, state):
  109. room_directory = os.path.join(self.base_directory, "rooms", room_id)
  110. state_directory = os.path.join(room_directory, "state")
  111. os.makedirs(state_directory, exist_ok=True)
  112. event_file = os.path.join(state_directory, event_id)
  113. with open(event_file, "a") as f:
  114. for event in state.values():
  115. print(json.dumps(event.get_pdu_json()), file=f)
  116. def write_invite(self, room_id, event, state):
  117. self.write_events(room_id, [event])
  118. # We write the invite state somewhere else as they aren't full events
  119. # and are only a subset of the state at the event.
  120. room_directory = os.path.join(self.base_directory, "rooms", room_id)
  121. os.makedirs(room_directory, exist_ok=True)
  122. invite_state = os.path.join(room_directory, "invite_state")
  123. with open(invite_state, "a") as f:
  124. for event in state.values():
  125. print(json.dumps(event), file=f)
  126. def finished(self):
  127. return self.base_directory
  128. def start(config_options):
  129. parser = argparse.ArgumentParser(description="Synapse Admin Command")
  130. HomeServerConfig.add_arguments_to_parser(parser)
  131. subparser = parser.add_subparsers(
  132. title="Admin Commands",
  133. required=True,
  134. dest="command",
  135. metavar="<admin_command>",
  136. help="The admin command to perform.",
  137. )
  138. export_data_parser = subparser.add_parser(
  139. "export-data", help="Export all data for a user"
  140. )
  141. export_data_parser.add_argument("user_id", help="User to extra data from")
  142. export_data_parser.add_argument(
  143. "--output-directory",
  144. action="store",
  145. metavar="DIRECTORY",
  146. required=False,
  147. help="The directory to store the exported data in. Must be empty. Defaults"
  148. " to creating a temp directory.",
  149. )
  150. export_data_parser.set_defaults(func=export_data_command)
  151. try:
  152. config, args = HomeServerConfig.load_config_with_parser(parser, config_options)
  153. except ConfigError as e:
  154. sys.stderr.write("\n" + str(e) + "\n")
  155. sys.exit(1)
  156. if config.worker_app is not None:
  157. assert config.worker_app == "synapse.app.admin_cmd"
  158. # Update the config with some basic overrides so that don't have to specify
  159. # a full worker config.
  160. config.worker_app = "synapse.app.admin_cmd"
  161. if (
  162. not config.worker_daemonize
  163. and not config.worker_log_file
  164. and not config.worker_log_config
  165. ):
  166. # Since we're meant to be run as a "command" let's not redirect stdio
  167. # unless we've actually set log config.
  168. config.no_redirect_stdio = True
  169. # Explicitly disable background processes
  170. config.update_user_directory = False
  171. config.run_background_tasks = False
  172. config.start_pushers = False
  173. config.pusher_shard_config.instances = []
  174. config.send_federation = False
  175. config.federation_shard_config.instances = []
  176. synapse.events.USE_FROZEN_DICTS = config.use_frozen_dicts
  177. ss = AdminCmdServer(
  178. config.server_name,
  179. config=config,
  180. version_string="Synapse/" + get_version_string(synapse),
  181. )
  182. setup_logging(ss, config, use_worker_options=True)
  183. ss.setup()
  184. # We use task.react as the basic run command as it correctly handles tearing
  185. # down the reactor when the deferreds resolve and setting the return value.
  186. # We also make sure that `_base.start` gets run before we actually run the
  187. # command.
  188. async def run():
  189. with LoggingContext("command"):
  190. _base.start(ss, [])
  191. await args.func(ss, args)
  192. _base.start_worker_reactor(
  193. "synapse-admin-cmd",
  194. config,
  195. run_command=lambda: task.react(lambda _reactor: defer.ensureDeferred(run())),
  196. )
  197. if __name__ == "__main__":
  198. with LoggingContext("main"):
  199. start(sys.argv[1:])