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

Allow admin to register a new user #5742

Closed
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions changelog.d/5742.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow admin to register a new user. Contributed by Awesome Technologies Innovationslabor GmbH.
57 changes: 57 additions & 0 deletions docs/admin_api/user_admin_api.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,60 @@
Create Account
Copy link
Member

Choose a reason for hiding this comment

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

could you document the response format?

==============

This API allows an administrator to create a new user account.

This api is::

POST /_synapse/admin/v1/users

with a body of:

.. code:: json

{
"username": "user_id",
"password": "user_password",
"displayname": "User",
"admin": false
}

including an ``access_token`` of a server admin.

The parameter ``displayname`` is optional and defaults to ``username``.
The parameter ``admin`` is optional and defaults to 'false'.


List Accounts
=============

This API returns all local user accounts.

The api is::

GET /_synapse/admin/v1/users

including an ``access_token`` of a server admin.

It returns a JSON body like the following:

.. code:: json
[
{
"name": "<user_id1>",
"password_hash": "<password_hash1>",
"is_guest": 0,
"admin": 0,
"user_type": null
}, {
"name": "<user_id2>",
"password_hash": "<password_hash2>",
"is_guest": 0,
"admin": 1,
"user_type": null
}
]


Query Account
=============

Expand Down
87 changes: 80 additions & 7 deletions synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,25 +52,98 @@


class UsersRestServlet(RestServlet):
PATTERNS = historical_admin_path_patterns("/users/(?P<user_id>[^/]*)")
PATTERNS = historical_admin_path_patterns("/users")
Copy link
Member

Choose a reason for hiding this comment

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

Hrm. Can you make this continue to accept (but ignore) a /user_id, for compatibility with applications which have hacked around the existing broken API?


"""Get request to list all local users.
This needs user to have administrator access in Synapse.

GET /_synapse/admin/v1/users

returns:
200 OK with list of users if success otherwise an error.


Post request to allow an administrator to add a user.
This needs user to have administrator access in Synapse.

POST /_synapse/admin/v1/users
{
"username": "user",
"password": "secret",
"displayname": "User"
}

returns:
200 OK with empty object if success otherwise an error.
Copy link
Member

Choose a reason for hiding this comment

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

shouldn't be an empty object afaict. (Indeed, your test even asserts that.)

"""

def __init__(self, hs):
self.hs = hs
self.auth = hs.get_auth()
self.handlers = hs.get_handlers()

@defer.inlineCallbacks
def on_GET(self, request, user_id):
target_user = UserID.from_string(user_id)
yield assert_requester_is_admin(self.auth, request)
def on_GET(self, request):
requester = yield self.auth.get_user_by_req(request)
yield assert_user_is_admin(self.auth, requester.user)

if not self.hs.is_mine(target_user):
raise SynapseError(400, "Can only users a local user")
if not self.hs.is_mine(requester.user):
Copy link
Member

Choose a reason for hiding this comment

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

This is redundant. The requester is a local user by definition.

raise SynapseError(400, "Can only list local users")

ret = yield self.handlers.admin_handler.get_users()

return (200, ret)

@defer.inlineCallbacks
def on_POST(self, request):
yield assert_requester_is_admin(self.auth, request)

body = parse_json_object_from_request(request)

if "username" not in body:
Copy link
Member

Choose a reason for hiding this comment

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

raise SynapseError(
400, "username must be specified", errcode=Codes.BAD_JSON
)
else:
if (
not isinstance(body["username"], text_type)
or len(body["username"]) > 512
):
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"], text_type)
or len(body["password"]) > 512
):
raise SynapseError(400, "Invalid password")

admin = body.get("admin", None)
user_type = body.get("user_type", None)
displayname = body.get("displayname", body["username"])

if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES:
raise SynapseError(400, "Invalid user type")

# 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_user(
Copy link
Member

Choose a reason for hiding this comment

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

there are better ways of getting hold of the registration_handler than via the RegisterRestServlet. Just stick
self.registration_handler = hs.get_registration_handler() in the constructor.

localpart=body["username"].lower(),
password=body["password"],
admin=bool(admin),
default_display_name=displayname,
user_type=user_type,
)

result = yield register._create_registration_details(user_id, body)
Copy link
Member

Choose a reason for hiding this comment

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

this will issue an access token for the user, which I'm not sure is the right thing to do. (even if you do want to end up with an access token for the new user, I think that might be better served via a different API per #6054).

In short, I don't think you need to call _create_registration_details here, which is good since it's meant to be a private method and instantiating a RegisterRestServlet here is ugly.

return (200, result)


class VersionServlet(RestServlet):
PATTERNS = (re.compile("^/_synapse/admin/v1/server_version$"),)
Expand Down
78 changes: 78 additions & 0 deletions tests/rest/admin/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,3 +561,81 @@ def _get_groups_user_is_in(self, access_token):
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])

return channel.json_body["groups"]


class UserCreateTestCase(unittest.HomeserverTestCase):

servlets = [synapse.rest.admin.register_servlets_for_client_rest_resource]

def make_homeserver(self, reactor, clock):

self.url = "/_matrix/client/v1/admin/users"

self.registration_handler = Mock()
self.identity_handler = Mock()
self.login_handler = Mock()
self.device_handler = Mock()
self.device_handler.check_device_registered = Mock(return_value="FAKE")

self.datastore = Mock(return_value=Mock())
self.datastore.get_current_state_deltas = Mock(return_value=[])

self.secrets = Mock()

self.hs = self.setup_test_homeserver()

self.hs.get_media_repository = Mock()
self.hs.get_deactivate_account_handler = Mock()

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")

return self.hs

def test_requester_is_no_admin(self):
"""
If the user is not a server admin, an error is returned.
"""
self.hs.config.registration_shared_secret = None

request, channel = self.make_request(
"POST",
self.url,
access_token=self.other_user_tok,
content=b"{}",
)
self.render(request)

self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(
"You are not a server admin",
channel.json_body["error"]
)

def test_requester_is_admin(self):
"""
If the user is a server admin, a new user is created.
"""
self.hs.config.registration_shared_secret = None

body = json.dumps(
{
"username": "bob",
"password": "abc123",
"admin": False,
}
)

request, channel = self.make_request(
"POST",
self.url,
access_token=self.admin_user_tok,
content=body.encode(encoding='utf_8'),
)
self.render(request)

self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("@bob:test", channel.json_body["user_id"])