register_new_matrix_user.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. # Copyright 2015, 2016 OpenMarket Ltd
  2. # Copyright 2018 New Vector
  3. # Copyright 2021-22 The Matrix.org Foundation C.I.C.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. import argparse
  17. import getpass
  18. import hashlib
  19. import hmac
  20. import logging
  21. import sys
  22. from typing import Any, Callable, Dict, Optional
  23. import requests
  24. import yaml
  25. _CONFLICTING_SHARED_SECRET_OPTS_ERROR = """\
  26. Conflicting options 'registration_shared_secret' and 'registration_shared_secret_path'
  27. are both defined in config file.
  28. """
  29. _NO_SHARED_SECRET_OPTS_ERROR = """\
  30. No 'registration_shared_secret' or 'registration_shared_secret_path' defined in config.
  31. """
  32. _DEFAULT_SERVER_URL = "http://localhost:8008"
  33. def request_registration(
  34. user: str,
  35. password: str,
  36. server_location: str,
  37. shared_secret: str,
  38. admin: bool = False,
  39. user_type: Optional[str] = None,
  40. _print: Callable[[str], None] = print,
  41. exit: Callable[[int], None] = sys.exit,
  42. ) -> None:
  43. url = "%s/_synapse/admin/v1/register" % (server_location.rstrip("/"),)
  44. # Get the nonce
  45. r = requests.get(url, verify=False)
  46. if r.status_code != 200:
  47. _print("ERROR! Received %d %s" % (r.status_code, r.reason))
  48. if 400 <= r.status_code < 500:
  49. try:
  50. _print(r.json()["error"])
  51. except Exception:
  52. pass
  53. return exit(1)
  54. nonce = r.json()["nonce"]
  55. mac = hmac.new(key=shared_secret.encode("utf8"), digestmod=hashlib.sha1)
  56. mac.update(nonce.encode("utf8"))
  57. mac.update(b"\x00")
  58. mac.update(user.encode("utf8"))
  59. mac.update(b"\x00")
  60. mac.update(password.encode("utf8"))
  61. mac.update(b"\x00")
  62. mac.update(b"admin" if admin else b"notadmin")
  63. if user_type:
  64. mac.update(b"\x00")
  65. mac.update(user_type.encode("utf8"))
  66. hex_mac = mac.hexdigest()
  67. data = {
  68. "nonce": nonce,
  69. "username": user,
  70. "password": password,
  71. "mac": hex_mac,
  72. "admin": admin,
  73. "user_type": user_type,
  74. }
  75. _print("Sending registration request...")
  76. r = requests.post(url, json=data, verify=False)
  77. if r.status_code != 200:
  78. _print("ERROR! Received %d %s" % (r.status_code, r.reason))
  79. if 400 <= r.status_code < 500:
  80. try:
  81. _print(r.json()["error"])
  82. except Exception:
  83. pass
  84. return exit(1)
  85. _print("Success!")
  86. def register_new_user(
  87. user: str,
  88. password: str,
  89. server_location: str,
  90. shared_secret: str,
  91. admin: Optional[bool],
  92. user_type: Optional[str],
  93. ) -> None:
  94. if not user:
  95. try:
  96. default_user: Optional[str] = getpass.getuser()
  97. except Exception:
  98. default_user = None
  99. if default_user:
  100. user = input("New user localpart [%s]: " % (default_user,))
  101. if not user:
  102. user = default_user
  103. else:
  104. user = input("New user localpart: ")
  105. if not user:
  106. print("Invalid user name")
  107. sys.exit(1)
  108. if not password:
  109. password = getpass.getpass("Password: ")
  110. if not password:
  111. print("Password cannot be blank.")
  112. sys.exit(1)
  113. confirm_password = getpass.getpass("Confirm password: ")
  114. if password != confirm_password:
  115. print("Passwords do not match")
  116. sys.exit(1)
  117. if admin is None:
  118. admin_inp = input("Make admin [no]: ")
  119. if admin_inp in ("y", "yes", "true"):
  120. admin = True
  121. else:
  122. admin = False
  123. request_registration(
  124. user, password, server_location, shared_secret, bool(admin), user_type
  125. )
  126. def main() -> None:
  127. logging.captureWarnings(True)
  128. parser = argparse.ArgumentParser(
  129. description="Used to register new users with a given homeserver when"
  130. " registration has been disabled. The homeserver must be"
  131. " configured with the 'registration_shared_secret' option"
  132. " set."
  133. )
  134. parser.add_argument(
  135. "-u",
  136. "--user",
  137. default=None,
  138. help="Local part of the new user. Will prompt if omitted.",
  139. )
  140. parser.add_argument(
  141. "-p",
  142. "--password",
  143. default=None,
  144. help="New password for user. Will prompt if omitted.",
  145. )
  146. parser.add_argument(
  147. "-t",
  148. "--user_type",
  149. default=None,
  150. help="User type as specified in synapse.api.constants.UserTypes",
  151. )
  152. admin_group = parser.add_mutually_exclusive_group()
  153. admin_group.add_argument(
  154. "-a",
  155. "--admin",
  156. action="store_true",
  157. help=(
  158. "Register new user as an admin. "
  159. "Will prompt if --no-admin is not set either."
  160. ),
  161. )
  162. admin_group.add_argument(
  163. "--no-admin",
  164. action="store_true",
  165. help=(
  166. "Register new user as a regular user. "
  167. "Will prompt if --admin is not set either."
  168. ),
  169. )
  170. group = parser.add_mutually_exclusive_group(required=True)
  171. group.add_argument(
  172. "-c",
  173. "--config",
  174. type=argparse.FileType("r"),
  175. help="Path to server config file. Used to read in shared secret.",
  176. )
  177. group.add_argument(
  178. "-k", "--shared-secret", help="Shared secret as defined in server config file."
  179. )
  180. parser.add_argument(
  181. "server_url",
  182. nargs="?",
  183. help="URL to use to talk to the homeserver. By default, tries to find a "
  184. "suitable URL from the configuration file. Otherwise, defaults to "
  185. f"'{_DEFAULT_SERVER_URL}'.",
  186. )
  187. args = parser.parse_args()
  188. if "config" in args and args.config:
  189. config = yaml.safe_load(args.config)
  190. if args.shared_secret:
  191. secret = args.shared_secret
  192. else:
  193. # argparse should check that we have either config or shared secret
  194. assert config
  195. secret = config.get("registration_shared_secret")
  196. secret_file = config.get("registration_shared_secret_path")
  197. if secret_file:
  198. if secret:
  199. print(_CONFLICTING_SHARED_SECRET_OPTS_ERROR, file=sys.stderr)
  200. sys.exit(1)
  201. secret = _read_file(secret_file, "registration_shared_secret_path").strip()
  202. if not secret:
  203. print(_NO_SHARED_SECRET_OPTS_ERROR, file=sys.stderr)
  204. sys.exit(1)
  205. if args.server_url:
  206. server_url = args.server_url
  207. elif config:
  208. server_url = _find_client_listener(config)
  209. if not server_url:
  210. server_url = _DEFAULT_SERVER_URL
  211. print(
  212. "Unable to find a suitable HTTP listener in the configuration file. "
  213. f"Trying {server_url} as a last resort.",
  214. file=sys.stderr,
  215. )
  216. else:
  217. server_url = _DEFAULT_SERVER_URL
  218. print(
  219. f"No server url or configuration file given. Defaulting to {server_url}.",
  220. file=sys.stderr,
  221. )
  222. admin = None
  223. if args.admin or args.no_admin:
  224. admin = args.admin
  225. register_new_user(
  226. args.user, args.password, server_url, secret, admin, args.user_type
  227. )
  228. def _read_file(file_path: Any, config_path: str) -> str:
  229. """Check the given file exists, and read it into a string
  230. If it does not, exit with an error indicating the problem
  231. Args:
  232. file_path: the file to be read
  233. config_path: where in the configuration file_path came from, so that a useful
  234. error can be emitted if it does not exist.
  235. Returns:
  236. content of the file.
  237. """
  238. if not isinstance(file_path, str):
  239. print(f"{config_path} setting is not a string", file=sys.stderr)
  240. sys.exit(1)
  241. try:
  242. with open(file_path) as file_stream:
  243. return file_stream.read()
  244. except OSError as e:
  245. print(f"Error accessing file {file_path}: {e}", file=sys.stderr)
  246. sys.exit(1)
  247. def _find_client_listener(config: Dict[str, Any]) -> Optional[str]:
  248. # try to find a listener in the config. Returns a host:port pair
  249. for listener in config.get("listeners", []):
  250. if listener.get("type") != "http" or listener.get("tls", False):
  251. continue
  252. if not any(
  253. name == "client"
  254. for resource in listener.get("resources", [])
  255. for name in resource.get("names", [])
  256. ):
  257. continue
  258. # TODO: consider bind_addresses
  259. return f"http://localhost:{listener['port']}"
  260. # no suitable listeners?
  261. return None
  262. if __name__ == "__main__":
  263. main()