Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Admin API for creating new users #3415

Merged
merged 5 commits into from
Jul 20, 2018
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added changelog.d/3415.misc
Empty file.
53 changes: 53 additions & 0 deletions docs/admin_api/register_api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
Shared-Secret Registration
==========================

This API allows for the creation of users in an administrative and non-interactive way. This is generally used for bootstrapping a Synapse instance with administrator accounts.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we wrap at 80 chars please?


To authenticate yourself to the server, you will need both the shared secret (``registration_shared_secret`` in the homeserver configuration), and a one-time nonce. If the registration shared secret is not configured, this API is not enabled.

To fetch the nonce, you need to request one from the API::

> GET /_matrix/client/r0/admin/register

< {"nonce": "thisisanonce"}

Once you have the nonce, you can make a ``POST`` to the same URL with a JSON body containing the nonce, username, password, whether they are an admin (optional, False by default), and a HMAC digest of the content.

As an example::

> POST /_matrix/client/r0/admin/register
> {
"nonce": "thisisanonce",
"username": "pepper_roni",
"password": "pizza",
"admin": true,
"mac": "mac_digest_here"
}

< {
"access_token": "token_here",
"user_id": "@pepper_roni@test",
"home_server": "test",
"device_id": "device_id_here"
}

The MAC is the hex digest output of the HMAC-SHA1 algorithm, with the key being the shared secret and the content being the nonce, user, password, and either the string "admin" or "notadmin", each separated by NULLs. For an example of generation in Python::
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NULs


import hmac, hashlib

def generate_mac(nonce, user, password, admin=False):

mac = hmac.new(
key=shared_secret,
digestmod=hashlib.sha1,
)

mac.update(nonce.encode('utf8'))
mac.update(b"\x00")
mac.update(user.encode('utf8'))
mac.update(b"\x00")
mac.update(password.encode('utf8'))
mac.update(b"\x00")
mac.update(b"admin" if admin else b"notadmin")

return mac.hexdigest()
32 changes: 29 additions & 3 deletions scripts/register_new_matrix_user
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,37 @@ import yaml


def request_registration(user, password, server_location, shared_secret, admin=False):
req = urllib2.Request(
"%s/_matrix/client/r0/admin/register" % (server_location,),
headers={'Content-Type': 'application/json'}
)

try:
if sys.version_info[:3] >= (2, 7, 9):
# As of version 2.7.9, urllib2 now checks SSL certs
import ssl
f = urllib2.urlopen(req, context=ssl.SSLContext(ssl.PROTOCOL_SSLv23))
else:
f = urllib2.urlopen(req)
body = f.read()
f.close()
nonce = json.loads(body)["nonce"]
except urllib2.HTTPError as e:
print "ERROR! Received %d %s" % (e.code, e.reason,)
if 400 <= e.code < 500:
if e.info().type == "application/json":
resp = json.load(e)
if "error" in resp:
print resp["error"]
sys.exit(1)

mac = hmac.new(
key=shared_secret,
digestmod=hashlib.sha1,
)

mac.update(nonce)
mac.update("\x00")
mac.update(user)
mac.update("\x00")
mac.update(password)
Expand All @@ -40,10 +66,10 @@ def request_registration(user, password, server_location, shared_secret, admin=F
mac = mac.hexdigest()

data = {
"user": user,
"nonce": nonce,
"username": user,
"password": password,
"mac": mac,
"type": "org.matrix.login.shared_secret",
"admin": admin,
}

Expand All @@ -52,7 +78,7 @@ def request_registration(user, password, server_location, shared_secret, admin=F
print "Sending registration request..."

req = urllib2.Request(
"%s/_matrix/client/api/v1/register" % (server_location,),
"%s/_matrix/client/r0/admin/register" % (server_location,),
data=json.dumps(data),
headers={'Content-Type': 'application/json'}
)
Expand Down
122 changes: 122 additions & 0 deletions synapse/rest/client/v1/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import hashlib
import hmac
import logging

from six.moves import http_client
Expand Down Expand Up @@ -63,6 +65,125 @@ def on_GET(self, request, user_id):
defer.returnValue((200, ret))


class UserRegisterServlet(ClientV1RestServlet):
"""
Attributes:
NONCE_TIMEOUT (int): Seconds until a generated nonce won't be accepted
nonces (dict): The nonces that we will accept. A dict of nonce to the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so it's dict[str, int] ?

time it was generated, in int seconds.
"""
PATTERNS = client_path_patterns("/admin/register")
NONCE_TIMEOUT = 60

def __init__(self, hs):
super(UserRegisterServlet, self).__init__(hs)
self.handlers = hs.get_handlers()
self.reactor = hs.get_reactor()
self.nonces = {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have a comment for this, please? what does it map to and from?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

self.hs = hs

def _clear_old_nonces(self):
"""
Clear out old nonces that are older than NONCE_TIMEOUT.
"""
now = int(self.reactor.seconds())

for k, v in list(self.nonces.items()):
if now - v > self.NONCE_TIMEOUT:
del self.nonces[k]

def on_GET(self, request):
"""
Generate a new nonce.
"""
self._clear_old_nonces()

nonce = self.hs.get_secrets().token_hex(64)
self.nonces[nonce] = int(self.reactor.seconds())
return (200, {"nonce": nonce.encode('ascii')})

@defer.inlineCallbacks
def on_POST(self, request):
self._clear_old_nonces()

if not self.hs.config.registration_shared_secret:
raise SynapseError(400, "Shared secret registration is not enabled")

body = parse_json_object_from_request(request)

if "nonce" not in body:
raise SynapseError(
400, "nonce must be specified", errcode=Codes.BAD_JSON,
)

nonce = body["nonce"]

if nonce not in self.nonces:
raise SynapseError(
400, "unrecognised nonce",
)

# Delete the nonce, so it can't be reused, even if it's invalid
del self.nonces[nonce]

if "username" not in body:
raise SynapseError(
400, "username must be specified", errcode=Codes.BAD_JSON,
)
else:
if (not isinstance(body['username'], str) or len(body['username']) > 512):
raise SynapseError(400, "Invalid username")

username = body["username"].encode("utf-8")
if b"\x00" in username:
raise SynapseError(400, "Invalid username")

if "password" not in body:
raise SynapseError(
400, "password must be specified", errcode=Codes.BAD_JSON,
)
else:
if (not isinstance(body['password'], str) or len(body['password']) > 512):
raise SynapseError(400, "Invalid password")

password = body["password"].encode("utf-8")
if b"\x00" in password:
raise SynapseError(400, "Invalid password")

admin = body.get("admin", None)
got_mac = body["mac"]

want_mac = hmac.new(
key=self.hs.config.registration_shared_secret.encode(),
digestmod=hashlib.sha1,
)
want_mac.update(nonce)
want_mac.update(b"\x00")
want_mac.update(username)
want_mac.update(b"\x00")
want_mac.update(password)
want_mac.update(b"\x00")
want_mac.update(b"admin" if admin else b"notadmin")
want_mac = want_mac.hexdigest()

if not hmac.compare_digest(want_mac, got_mac):
raise SynapseError(
403, "HMAC incorrect",
)

# Reuse the parts of RegisterRestServlet to reduce code duplication
from synapse.rest.client.v2_alpha.register import RegisterRestServlet
register = RegisterRestServlet(self.hs)

(user_id, _) = yield register.registration_handler.register(
localpart=username.lower(), password=password, admin=bool(admin),
generate_token=False,
)

result = yield register._create_registration_details(user_id, body)
defer.returnValue((200, result))


class WhoisRestServlet(ClientV1RestServlet):
PATTERNS = client_path_patterns("/admin/whois/(?P<user_id>[^/]*)")

Expand Down Expand Up @@ -614,3 +735,4 @@ def register_servlets(hs, http_server):
ShutdownRoomRestServlet(hs).register(http_server)
QuarantineMediaInRoom(hs).register(http_server)
ListMediaInRoom(hs).register(http_server)
UserRegisterServlet(hs).register(http_server)
42 changes: 42 additions & 0 deletions synapse/secrets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Copyright 2018 New Vector Ltd
#
# 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.

"""
Injectable secrets module for Synapse.

See https://docs.python.org/3/library/secrets.html#module-secrets for the API
used in Python 3.6, and the API emulated in Python 2.7.
"""

import six

if six.PY3:
import secrets

def Secrets():
return secrets


else:

import os
import binascii

class Secrets(object):
def token_bytes(self, nbytes=32):
return os.urandom(nbytes)

def token_hex(self, nbytes=32):
return binascii.hexlify(self.token_bytes(nbytes))
5 changes: 5 additions & 0 deletions synapse/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
MediaRepository,
MediaRepositoryResource,
)
from synapse.secrets import Secrets
from synapse.server_notices.server_notices_manager import ServerNoticesManager
from synapse.server_notices.server_notices_sender import ServerNoticesSender
from synapse.server_notices.worker_server_notices_sender import WorkerServerNoticesSender
Expand Down Expand Up @@ -158,6 +159,7 @@ def build_DEPENDENCY(self)
'groups_server_handler',
'groups_attestation_signing',
'groups_attestation_renewer',
'secrets',
'spam_checker',
'room_member_handler',
'federation_registry',
Expand Down Expand Up @@ -405,6 +407,9 @@ def build_groups_attestation_signing(self):
def build_groups_attestation_renewer(self):
return GroupAttestionRenewer(self)

def build_secrets(self):
return Secrets()

def build_spam_checker(self):
return SpamChecker(self)

Expand Down
Loading