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

Update the check whether a password may be set #9636

Merged
merged 3 commits into from
Mar 18, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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/9636.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Checks if passwords are allowed before setting it for the user.
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we should consolidate the various three (?) PRs we've made around this into a single changelog entry. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, we can do it. But the first one (#9587) is already in 1.30.0rc1 included.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, well maybe just for the last two then!

2 changes: 1 addition & 1 deletion synapse/handlers/set_password.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async def set_password(
logout_devices: bool,
requester: Optional[Requester] = None,
) -> None:
if not self.hs.config.password_localdb_enabled:
if not self._auth_handler.can_change_password():
raise SynapseError(403, "Password change disabled", errcode=Codes.FORBIDDEN)

try:
Expand Down
2 changes: 1 addition & 1 deletion synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ async def on_PUT(
elif not deactivate and user["deactivated"]:
if (
"password" not in body
and self.hs.config.password_localdb_enabled
and self.auth_handler.can_change_password()
):
raise SynapseError(
400, "Must provide a password to re-activate an account."
Expand Down
1 change: 1 addition & 0 deletions synapse/storage/databases/main/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,7 @@ def set_user_deactivated_status_txn(self, txn, user_id: str, deactivated: bool):
self._invalidate_cache_and_stream(
txn, self.get_user_deactivated_status, (user_id,)
)
self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,))
txn.call_after(self.is_guest.invalidate, (user_id,))
Comment on lines +1213 to 1214
Copy link
Member

Choose a reason for hiding this comment

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

Good catch! 👍

I don't see why we're invalidating the guest status here, but it seems like it has always been that way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had started with implementing unit tests. The tests had always failed. That was the reason for catch this bug.


@cached()
Expand Down
151 changes: 116 additions & 35 deletions tests/rest/admin/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -1003,12 +1003,23 @@ class UserRestTestCase(unittest.HomeserverTestCase):

def prepare(self, reactor, clock, hs):
self.store = hs.get_datastore()
self.auth_handler = hs.get_auth_handler()

# create users and get access tokens
# regardless of whether password login or SSO is allowed
self.admin_user = self.register_user("admin", "pass", admin=True)
self.admin_user_tok = self.login("admin", "pass")
self.admin_user_tok = self.get_success(
self.auth_handler.get_access_token_for_user_id(
self.admin_user, device_id=None, valid_until_ms=None
)
)

self.other_user = self.register_user("user", "pass", displayname="User")
self.other_user_token = self.login("user", "pass")
self.other_user_token = self.get_success(
self.auth_handler.get_access_token_for_user_id(
self.other_user, device_id=None, valid_until_ms=None
)
)
self.url_other_user = "/_synapse/admin/v2/users/%s" % urllib.parse.quote(
self.other_user
)
Expand Down Expand Up @@ -1081,7 +1092,7 @@ def test_create_server_admin(self):
self.assertEqual("Bob's name", channel.json_body["displayname"])
self.assertEqual("email", channel.json_body["threepids"][0]["medium"])
self.assertEqual("[email protected]", channel.json_body["threepids"][0]["address"])
self.assertEqual(True, channel.json_body["admin"])
self.assertTrue(channel.json_body["admin"])
self.assertEqual("mxc://fibble/wibble", channel.json_body["avatar_url"])

# Get user
Expand All @@ -1096,9 +1107,9 @@ def test_create_server_admin(self):
self.assertEqual("Bob's name", channel.json_body["displayname"])
self.assertEqual("email", channel.json_body["threepids"][0]["medium"])
self.assertEqual("[email protected]", channel.json_body["threepids"][0]["address"])
self.assertEqual(True, channel.json_body["admin"])
self.assertEqual(False, channel.json_body["is_guest"])
self.assertEqual(False, channel.json_body["deactivated"])
self.assertTrue(channel.json_body["admin"])
self.assertFalse(channel.json_body["is_guest"])
self.assertFalse(channel.json_body["deactivated"])
self.assertEqual("mxc://fibble/wibble", channel.json_body["avatar_url"])

def test_create_user(self):
Expand Down Expand Up @@ -1130,7 +1141,7 @@ def test_create_user(self):
self.assertEqual("Bob's name", channel.json_body["displayname"])
self.assertEqual("email", channel.json_body["threepids"][0]["medium"])
self.assertEqual("[email protected]", channel.json_body["threepids"][0]["address"])
self.assertEqual(False, channel.json_body["admin"])
self.assertFalse(channel.json_body["admin"])
self.assertEqual("mxc://fibble/wibble", channel.json_body["avatar_url"])

# Get user
Expand All @@ -1145,10 +1156,10 @@ def test_create_user(self):
self.assertEqual("Bob's name", channel.json_body["displayname"])
self.assertEqual("email", channel.json_body["threepids"][0]["medium"])
self.assertEqual("[email protected]", channel.json_body["threepids"][0]["address"])
self.assertEqual(False, channel.json_body["admin"])
self.assertEqual(False, channel.json_body["is_guest"])
self.assertEqual(False, channel.json_body["deactivated"])
self.assertEqual(False, channel.json_body["shadow_banned"])
self.assertFalse(channel.json_body["admin"])
self.assertFalse(channel.json_body["is_guest"])
self.assertFalse(channel.json_body["deactivated"])
self.assertFalse(channel.json_body["shadow_banned"])
self.assertEqual("mxc://fibble/wibble", channel.json_body["avatar_url"])

@override_config(
Expand Down Expand Up @@ -1197,7 +1208,7 @@ def test_create_user_mau_limit_reached_active_admin(self):

self.assertEqual(201, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("@bob:test", channel.json_body["name"])
self.assertEqual(False, channel.json_body["admin"])
self.assertFalse(channel.json_body["admin"])

@override_config(
{"limit_usage_by_mau": True, "max_mau_value": 2, "mau_trial_days": 0}
Expand Down Expand Up @@ -1237,7 +1248,7 @@ def test_create_user_mau_limit_reached_passive_admin(self):
# Admin user is not blocked by mau anymore
self.assertEqual(201, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("@bob:test", channel.json_body["name"])
self.assertEqual(False, channel.json_body["admin"])
self.assertFalse(channel.json_body["admin"])

@override_config(
{
Expand Down Expand Up @@ -1429,7 +1440,7 @@ def test_deactivate_user(self):

self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("@user:test", channel.json_body["name"])
self.assertEqual(False, channel.json_body["deactivated"])
self.assertFalse(channel.json_body["deactivated"])
self.assertEqual("[email protected]", channel.json_body["threepids"][0]["address"])
self.assertEqual("mxc://servername/mediaid", channel.json_body["avatar_url"])
self.assertEqual("User", channel.json_body["displayname"])
Expand All @@ -1446,7 +1457,8 @@ def test_deactivate_user(self):

self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("@user:test", channel.json_body["name"])
self.assertEqual(True, channel.json_body["deactivated"])
self.assertTrue(channel.json_body["deactivated"])
self.assertIsNone(channel.json_body["password_hash"])
self.assertEqual(0, len(channel.json_body["threepids"]))
self.assertEqual("mxc://servername/mediaid", channel.json_body["avatar_url"])
self.assertEqual("User", channel.json_body["displayname"])
Expand All @@ -1461,7 +1473,8 @@ def test_deactivate_user(self):

self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("@user:test", channel.json_body["name"])
self.assertEqual(True, channel.json_body["deactivated"])
self.assertTrue(channel.json_body["deactivated"])
self.assertIsNone(channel.json_body["password_hash"])
self.assertEqual(0, len(channel.json_body["threepids"]))
self.assertEqual("mxc://servername/mediaid", channel.json_body["avatar_url"])
self.assertEqual("User", channel.json_body["displayname"])
Expand Down Expand Up @@ -1489,11 +1502,11 @@ def test_change_name_deactivate_user_user_directory(self):

self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("@user:test", channel.json_body["name"])
self.assertEqual(True, channel.json_body["deactivated"])
self.assertTrue(channel.json_body["deactivated"])

# is not in user directory
profile = self.get_success(self.store.get_user_in_directory(self.other_user))
self.assertTrue(profile is None)
self.assertIsNone(profile)

# Set new displayname user
body = json.dumps({"displayname": "Foobar"})
Expand All @@ -1507,41 +1520,89 @@ def test_change_name_deactivate_user_user_directory(self):

self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("@user:test", channel.json_body["name"])
self.assertEqual(True, channel.json_body["deactivated"])
self.assertTrue(channel.json_body["deactivated"])
self.assertEqual("Foobar", channel.json_body["displayname"])

# is not in user directory
profile = self.get_success(self.store.get_user_in_directory(self.other_user))
self.assertTrue(profile is None)
self.assertIsNone(profile)

def test_reactivate_user(self):
"""
Test reactivating another user.
"""

# Deactivate the user.
self._deactivate_user("@user:test")

# Attempt to reactivate the user (without a password).
channel = self.make_request(
"PUT",
self.url_other_user,
access_token=self.admin_user_tok,
content=json.dumps({"deactivated": True}).encode(encoding="utf_8"),
content=json.dumps({"deactivated": False}).encode(encoding="utf_8"),
Copy link
Member

Choose a reason for hiding this comment

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

We can pass just the dictionary in here instead of worrying about dumping and encoding (and the framework will handle that). It should simplify some of the code in here a bit! (There's a bunch of places to make this change, I only marked this one.)

Suggested change
content=json.dumps({"deactivated": False}).encode(encoding="utf_8"),
content={"deactivated": False},

)
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])

# Reactivate the user.
channel = self.make_request(
"PUT",
self.url_other_user,
access_token=self.admin_user_tok,
content=json.dumps({"deactivated": False, "password": "foo"}).encode(
encoding="utf_8"
),
)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("@user:test", channel.json_body["name"])
self.assertFalse(channel.json_body["deactivated"])
self.assertIsNotNone(channel.json_body["password_hash"])
self._is_erased("@user:test", False)
d = self.store.mark_user_erased("@user:test")
self.assertIsNone(self.get_success(d))
self._is_erased("@user:test", True)

# Attempt to reactivate the user (without a password).
@override_config({"password_config": {"localdb_enabled": False}})
def test_reactivate_user_localdb_disabled(self):
"""
Test reactivating another user when using SSO/SAML
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
"""

# Deactivate the user.
self._deactivate_user("@user:test")

# Reactivate the user with a password
channel = self.make_request(
"PUT",
self.url_other_user,
access_token=self.admin_user_tok,
content=json.dumps({"deactivated": False, "password": "foo"}).encode(
encoding="utf_8"
),
)
self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])

# Reactivate the user without a password.
channel = self.make_request(
"PUT",
self.url_other_user,
access_token=self.admin_user_tok,
content=json.dumps({"deactivated": False}).encode(encoding="utf_8"),
)
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("@user:test", channel.json_body["name"])
self.assertFalse(channel.json_body["deactivated"])
self.assertIsNone(channel.json_body["password_hash"])
self._is_erased("@user:test", False)

# Reactivate the user.
@override_config({"password_config": {"enabled": False}})
def test_reactivate_user_password_disabled(self):
"""
Test reactivating another user when using SSO/SAML
dklimpel marked this conversation as resolved.
Show resolved Hide resolved
"""

# Deactivate the user.
self._deactivate_user("@user:test")

# Reactivate the user with a password
channel = self.make_request(
"PUT",
self.url_other_user,
Expand All @@ -1550,18 +1611,20 @@ def test_reactivate_user(self):
encoding="utf_8"
),
)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])

# Get user
# Reactivate the user without a password.
channel = self.make_request(
"GET",
"PUT",
self.url_other_user,
access_token=self.admin_user_tok,
content=json.dumps({"deactivated": False}).encode(encoding="utf_8"),
)

self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("@user:test", channel.json_body["name"])
self.assertEqual(False, channel.json_body["deactivated"])
self.assertFalse(channel.json_body["deactivated"])
self.assertIsNone(channel.json_body["password_hash"])
self._is_erased("@user:test", False)

def test_set_user_as_admin(self):
Expand All @@ -1581,7 +1644,7 @@ def test_set_user_as_admin(self):

self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("@user:test", channel.json_body["name"])
self.assertEqual(True, channel.json_body["admin"])
self.assertTrue(channel.json_body["admin"])

# Get user
channel = self.make_request(
Expand All @@ -1592,7 +1655,7 @@ def test_set_user_as_admin(self):

self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("@user:test", channel.json_body["name"])
self.assertEqual(True, channel.json_body["admin"])
self.assertTrue(channel.json_body["admin"])

def test_accidental_deactivation_prevention(self):
"""
Expand Down Expand Up @@ -1653,14 +1716,32 @@ def test_accidental_deactivation_prevention(self):
# Ensure they're still alive
self.assertEqual(0, channel.json_body["deactivated"])

def _is_erased(self, user_id, expect):
def _is_erased(self, user_id: str, expect: bool) -> None:
"""Assert that the user is erased or not"""
d = self.store.is_user_erased(user_id)
if expect:
self.assertTrue(self.get_success(d))
else:
self.assertFalse(self.get_success(d))

def _deactivate_user(self, user_id: str) -> None:
"""Deactivate user and set as erased"""

# Deactivate the user.
channel = self.make_request(
"PUT",
"/_synapse/admin/v2/users/%s" % urllib.parse.quote(user_id),
access_token=self.admin_user_tok,
content=json.dumps({"deactivated": True}).encode(encoding="utf_8"),
)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertTrue(channel.json_body["deactivated"])
self.assertIsNone(channel.json_body["password_hash"])
self._is_erased(user_id, False)
d = self.store.mark_user_erased(user_id)
self.assertIsNone(self.get_success(d))
Comment on lines +1723 to +1724
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit confused at why we're erasing data here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To test the erase was part of #8362.
There was a bug because there was no test.
Now I have moved the whole part of deactivation in a private function because it is needed more than one times for these tests.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, so the erasure isn't really only needed for some of the tests, but we do it for all just to simplify? That sounds fine!

self._is_erased(user_id, True)


class UserMembershipRestTestCase(unittest.HomeserverTestCase):

Expand Down